Skip to content

Add Sprint 3 forms schema, member types, and shared contracts - #304

Merged
shaply merged 4 commits into
mainfrom
claude/hopeful-bohr-3evtc1
Sep 30, 2026
Merged

shaply merged 4 commits into
mainfrom
claude/hopeful-bohr-3evtc1

Conversation

@shaply

@shaply shaply commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Part of #295: the schema, shared types, config keys, migration, and test helpers. The theme tokens and navbar prefix matching were in #305 (merged). The Visionaries seed is in #306, which is stacked on this PR and #276 (merged).

Merge order: this PR, then #306 (retarget it to main after this merges).

What's here

  • Enums in lib/schema.ts: FormStatus, FormComponentType (the seven application types), and MemberType, each with a derived pgEnum.
  • Tables with relations: forms, form_components, form_submissions, form_answers (unique per submission and component), and form_uploads (kept apart from media).
  • Contract in lib/forms/schema.ts:
    • per-type config schemas (FORM_COMPONENT_CONFIG_SCHEMAS) and the { type, config } discriminated union
    • per-type answer shapes (FORM_ANSWER_VALUE_SCHEMAS)
    • FormSettingsSchema (strict) with defaults in lib/forms/constants.ts
    • FormDefinition, which requires components in position order with unique IDs
    • the submission payload (answers keyed by component ID)
    • FormIdSchema, using [Forms] Build form management: service, admin API, forms list, and CSV export #303's rule: lowercase letters, numbers, and hyphens, up to 64 characters.
  • Config keys: FormsEnabled, MemberTypesEnabled, AutoApproveSignups, and the six theme keys. Only FormsEnabled has a get/set (default off) and joins ToggleableOrganizationFeature. [Visionaries] Make the app theme configurable per organization #300 and [Visionaries] Add applicant and attendee member types, auto-approved signups, and admin invites #302 add the rest.
  • members.member_type (nullable), registered in BetterAuth's member.additionalFields.
  • One migration, generated with pnpm run db:generate.
  • Test helpers in tests/unit/testUtils.ts: createForm (any status; defaults to a copy of the Visionaries application), createFormSubmission, createFormUpload, and buildSampleAnswers. VISIONARIES_APPLICATION_FORM holds the 12 questions in the order from [Pre-sprint] Sprint 3 schema, shared contracts, and Visionaries org setup #295.

Things sprint devs should know

  • formId means two things. forms.formId is the human key in URLs (camp-application-2027). The formId columns on the child tables reference forms.id, the UUID. A slug passed where a UUID is expected fails loudly in Postgres, and the schema has comments on each column.
  • Answers stay on their form. form_answers has its own formId, and composite foreign keys require the submission and the question to be on that form. Pass formId when inserting answers.
  • Settings are one jsonb column validated by FormSettingsSchema. That way, adding a setting mid-sprint needs no migration. Open and close times are ISO strings, and unknown keys are rejected.
  • Config keys without handlers are rejected. OrganizationConfigService now accepts enum keys that have no handler yet: setConfig throws "Invalid key" and getConfig skips them. It also uses Object.hasOwn, so keys like constructor no longer crash getConfig.
  • memberType is input: false. Clients can't set it through BetterAuth input, since [Visionaries] Add applicant and attendee member types, auto-approved signups, and admin invites #302 uses it to gate the member directory. Set it from server code instead, e.g. a beforeAddMember hook that returns { data: { memberType } }, or MembersService.
  • Drizzle jsonb bug: Drizzle's jsonb re-parses strings node-postgres has already parsed, so a stored "4045550123" or "7" read back as a number. form_answers.value now uses a jsonb custom type without that second parse. The SQL type is unchanged. A regression test covers both select and relational queries, and it fails on Drizzle's plain jsonb.
  • Deleting a user keeps their responses. Deleting a user clears form_submissions.user_id and form_uploads.uploaded_by, so the org keeps its responses. Deleting a question deletes its answers, but only drafts' questions get deleted, and drafts have no answers.

Testing

  • New: contract tests with valid and invalid examples for every type's config and answer, a check that the fixture parses as a FormDefinition, table constraints and cascades (including cross-form answers and user deletion), FormsEnabled defaults and isolation, and memberType read and input stripping.
  • Full unit suite: 53 files, 663 tests passing locally, plus pnpm lint, pnpm format, tsc --noEmit, and pnpm run db:check.
  • CI and CD / migrate-db pass. The preview DB was rebuilt, and it migrates and seeds with this migration. Setup Preview / create-db fails only at its last step, the Juno email registration, as it does on other PRs.

Checklist:

🤖 Generated with Claude Code

https://claude.ai/code/session_017RYaE2zBcm7SfFBJdydeuR

Six Sprint 3 tickets build on the same tables and types in parallel, so
land them, and their one migration, before the sprint:

- Forms tables (forms, form_components, form_submissions, form_answers,
  form_uploads) with relations, plus FormStatus and FormComponentType
- lib/forms: per-type config and answer schemas, form settings,
  FormDefinition, and the submission payload
- Config keys for forms, member types, auto-approved signups, and theme,
  with get/set for FormsEnabled (off by default) only; the others'
  handlers come with their tickets
- members.member_type, registered with BetterAuth as server-only
- Test helpers for forms, submissions, and uploads, and a fixture of the
  Visionaries camp application

form_answers.value uses a jsonb column that doesn't re-parse strings:
Drizzle's jsonb JSON.parses the already-parsed value, so an answer like
"4045550123" read back as a number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RYaE2zBcm7SfFBJdydeuR
@netlify

netlify Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for servicestart ready!

Name Link
🔨 Latest commit 30f12fb
🔍 Latest deploy log https://app.netlify.com/projects/servicestart/deploys/6abc89baf8297c00075a84b1
😎 Deploy Preview https://deploy-preview-304--servicestart.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Disclaimer: This comment was AI-generated and is not necessarily completely accurate. Please take code comments with a grain of salt.

RetriggerConfidence Score: 5/5

[Critical risk] Adds database schema for forms and member types.

The code appears safe to merge once the acknowledged preview migration workflow is green.

Summary

Adds the forms tables, shared validation contracts, member type, configuration keys, migration, fixtures, and test helpers.

  • The four previous findings are fixed: user deletion retains responses, answers cannot cross forms, unknown settings are rejected, and failed answer inserts roll back their submission.
  • Repository-owner checklist: no further code improvements identified in this review; 95/100.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Forms[forms] --> Components[form_components]
  Forms --> Submissions[form_submissions]
  Forms --> Uploads[form_uploads]
  Submissions -->|submission ID and form ID| Answers[form_answers]
  Components -->|component ID and form ID| Answers
  Users[users] -.->|deletion clears reference| Submissions
  Users -.->|deletion clears reference| Uploads
Loading

Reviews (2) · Last reviewed commit: "Keep form answers on their own form and ..."

Comment thread lib/schema.ts
Comment thread lib/schema.ts Outdated
Comment thread lib/forms/schema.ts
Comment thread tests/unit/testUtils.ts Outdated
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RYaE2zBcm7SfFBJdydeuR
From review:

- form_answers gets a form_id with composite foreign keys to the
  submission and the component, so an answer can't pair a submission
  with another form's question.
- Deleting a user clears form_submissions.user_id and
  form_uploads.uploaded_by instead of deleting the org's responses.
- FormSettingsSchema rejects unknown keys, like the config schemas.
- createFormSubmission inserts the submission and its answers in one
  transaction.

The migration is regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RYaE2zBcm7SfFBJdydeuR

shaply commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

Two red checks here are about the preview environment, not the code:

  • CD / migrate-db: caused by this PR, needs a close/reopen. The preview database pr304 applied the first version of this PR's migration. After review I deleted and regenerated it (2f50d35), as CLAUDE.md prescribes, so migrate-db now fails with CREATE TYPE "form_component_type": the type already exists. Per docs/DB_INFRA.md, closing and reopening the PR drops pr304 and recreates, migrates, and seeds it. I haven't done that myself. CI's own check-migrations and unit tests use fresh databases and aren't affected.
  • Setup Preview / create-db: not this PR's. Migrate database and Seed database passed on pr304. The failure is the last step, Setup organization, where Juno's email/register-domain returns 400. The same step fails on most recent PRs, including Fix inbox double dot, dashboard notifications card, and tenant INVALID_ORIGIN; add run skill #296, which merged with it red. No fix exists yet (Preserve email DNS records and track staging verification #269 doesn't change domain registration).

Generated by Claude Code

@shaply
shaply merged commit c413280 into main Sep 30, 2026
25 checks passed

This branch was successfully deployed

1 active deployment
Preview — 30f12fb3 Deployed Sep 30, 2026 by shaply via drop-db #266
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants