@ftisindia/create-app 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/package.json +1 -1
  2. package/template/.env.example +28 -0
  3. package/template/README.md +51 -0
  4. package/template/_gitignore +6 -0
  5. package/template/_package.json +10 -1
  6. package/template/docs/FORMS.md +188 -0
  7. package/template/docs/FORMS_CHECKLIST.md +69 -0
  8. package/template/docs/REPORTS.md +255 -0
  9. package/template/docs/REPORTS_CHECKLIST.md +152 -0
  10. package/template/prisma/migrations/20260612000000_add_form_builder/migration.sql +147 -0
  11. package/template/prisma/migrations/20260613000000_add_report_builder/migration.sql +129 -0
  12. package/template/prisma/migrations/20260616000000_add_form_outbox_claimed_by/migration.sql +5 -0
  13. package/template/prisma/schema.prisma +289 -0
  14. package/template/scripts/export-openapi.ts +85 -0
  15. package/template/scripts/gen-form.mjs +149 -0
  16. package/template/scripts/push-form.ts +124 -0
  17. package/template/src/app.module.ts +30 -8
  18. package/template/src/common/dto/membership-response.dto.ts +1 -0
  19. package/template/src/common/dto/role-summary.dto.ts +3 -3
  20. package/template/src/common/dto/user-summary.dto.ts +3 -3
  21. package/template/src/config/env.validation.ts +28 -0
  22. package/template/src/config/forms.config.ts +13 -0
  23. package/template/src/config/index.ts +2 -0
  24. package/template/src/config/openapi.ts +12 -0
  25. package/template/src/config/reports-secret.ts +15 -0
  26. package/template/src/config/reports.config.ts +18 -0
  27. package/template/src/main.ts +3 -12
  28. package/template/src/modules/access-control/dto/access-control-response.dto.ts +3 -0
  29. package/template/src/modules/access-control/dto/current-access-control-response.dto.ts +5 -1
  30. package/template/src/modules/access-control/types/permission-key.ts +27 -0
  31. package/template/src/modules/access-control/types/route-permission-registry.ts +183 -0
  32. package/template/src/modules/audit/dto/audit-response.dto.ts +7 -3
  33. package/template/src/modules/auth/auth.module.ts +3 -1
  34. package/template/src/modules/auth/dto/auth-response.dto.ts +1 -1
  35. package/template/src/modules/forms/application/services/file-gc.service.ts +85 -0
  36. package/template/src/modules/forms/application/services/forms-definitions.service.ts +137 -0
  37. package/template/src/modules/forms/application/services/forms-error.mapper.ts +64 -0
  38. package/template/src/modules/forms/application/services/forms-export.service.ts +210 -0
  39. package/template/src/modules/forms/application/services/forms-files.service.ts +164 -0
  40. package/template/src/modules/forms/application/services/forms-public.service.ts +49 -0
  41. package/template/src/modules/forms/application/services/forms-settings-reader.service.ts +53 -0
  42. package/template/src/modules/forms/application/services/forms-submissions.service.ts +103 -0
  43. package/template/src/modules/forms/application/services/handlers/authenticate.action.ts +37 -0
  44. package/template/src/modules/forms/application/services/handlers/logging-email.handler.ts +22 -0
  45. package/template/src/modules/forms/application/services/handlers/send-confirmation-email.action.ts +40 -0
  46. package/template/src/modules/forms/application/services/handlers/webhook-delivery.transport.ts +319 -0
  47. package/template/src/modules/forms/application/services/handlers/webhook.handler.ts +89 -0
  48. package/template/src/modules/forms/application/services/outbox-dispatcher.service.ts +131 -0
  49. package/template/src/modules/forms/dto/create-form-definition.dto.ts +12 -0
  50. package/template/src/modules/forms/dto/data-source-response.dto.ts +19 -0
  51. package/template/src/modules/forms/dto/export-submissions-query.dto.ts +33 -0
  52. package/template/src/modules/forms/dto/file-upload-response.dto.ts +24 -0
  53. package/template/src/modules/forms/dto/form-definition-response.dto.ts +50 -0
  54. package/template/src/modules/forms/dto/form-render-response.dto.ts +17 -0
  55. package/template/src/modules/forms/dto/list-form-definitions-query.dto.ts +10 -0
  56. package/template/src/modules/forms/dto/list-submissions-query.dto.ts +10 -0
  57. package/template/src/modules/forms/dto/public-submit-form.dto.ts +24 -0
  58. package/template/src/modules/forms/dto/set-public-access.dto.ts +8 -0
  59. package/template/src/modules/forms/dto/submission-response.dto.ts +99 -0
  60. package/template/src/modules/forms/dto/submit-form.dto.ts +50 -0
  61. package/template/src/modules/forms/dto/update-form-definition.dto.ts +12 -0
  62. package/template/src/modules/forms/dto/upload-file-query.dto.ts +33 -0
  63. package/template/src/modules/forms/dto/validate-submission.dto.ts +22 -0
  64. package/template/src/modules/forms/examples/abstract-submission.form.json +80 -0
  65. package/template/src/modules/forms/examples/login.form.json +24 -0
  66. package/template/src/modules/forms/examples/registration.form.json +44 -0
  67. package/template/src/modules/forms/forms.module.ts +228 -0
  68. package/template/src/modules/forms/forms.tokens.ts +6 -0
  69. package/template/src/modules/forms/infrastructure/audit-sink.adapter.ts +30 -0
  70. package/template/src/modules/forms/infrastructure/casl-forms-authorization.ts +31 -0
  71. package/template/src/modules/forms/infrastructure/prisma-tx-runner.ts +17 -0
  72. package/template/src/modules/forms/infrastructure/registry/form-extension.decorators.ts +17 -0
  73. package/template/src/modules/forms/infrastructure/registry/registry-bootstrap.service.ts +82 -0
  74. package/template/src/modules/forms/infrastructure/request-forms-context.ts +60 -0
  75. package/template/src/modules/forms/infrastructure/schema-check/forms-schema-check.service.ts +76 -0
  76. package/template/src/modules/forms/infrastructure/storage/local-disk-storage.adapter.ts +43 -0
  77. package/template/src/modules/forms/infrastructure/stores/index.ts +5 -0
  78. package/template/src/modules/forms/infrastructure/stores/prisma-action-log.store.ts +37 -0
  79. package/template/src/modules/forms/infrastructure/stores/prisma-file.store.ts +108 -0
  80. package/template/src/modules/forms/infrastructure/stores/prisma-form-definition.store.ts +147 -0
  81. package/template/src/modules/forms/infrastructure/stores/prisma-outbox.store.ts +156 -0
  82. package/template/src/modules/forms/infrastructure/stores/prisma-submission.store.ts +164 -0
  83. package/template/src/modules/forms/presentation/forms-data-sources.controller.ts +58 -0
  84. package/template/src/modules/forms/presentation/forms-definitions.controller.ts +191 -0
  85. package/template/src/modules/forms/presentation/forms-files.controller.ts +79 -0
  86. package/template/src/modules/forms/presentation/forms-submissions.controller.ts +154 -0
  87. package/template/src/modules/forms/presentation/forms-upload.interceptor.ts +33 -0
  88. package/template/src/modules/forms/presentation/public-forms.controller.ts +51 -0
  89. package/template/src/modules/invitations/dto/invitation-response.dto.ts +4 -0
  90. package/template/src/modules/organisations/dto/organisation-response.dto.ts +1 -0
  91. package/template/src/modules/reports/application/services/reports-actions.service.ts +54 -0
  92. package/template/src/modules/reports/application/services/reports-definitions.service.ts +66 -0
  93. package/template/src/modules/reports/application/services/reports-error.mapper.ts +97 -0
  94. package/template/src/modules/reports/application/services/reports-export-dispatcher.service.ts +205 -0
  95. package/template/src/modules/reports/application/services/reports-exports.service.ts +78 -0
  96. package/template/src/modules/reports/application/services/reports-queries.service.ts +35 -0
  97. package/template/src/modules/reports/application/services/reports-settings-reader.service.ts +49 -0
  98. package/template/src/modules/reports/application/services/reports-views.service.ts +79 -0
  99. package/template/src/modules/reports/dto/action-result-response.dto.ts +21 -0
  100. package/template/src/modules/reports/dto/create-report-definition.dto.ts +86 -0
  101. package/template/src/modules/reports/dto/create-saved-view.dto.ts +26 -0
  102. package/template/src/modules/reports/dto/execute-action.dto.ts +71 -0
  103. package/template/src/modules/reports/dto/export-job-response.dto.ts +60 -0
  104. package/template/src/modules/reports/dto/export-request.dto.ts +34 -0
  105. package/template/src/modules/reports/dto/list-reports-query.dto.ts +10 -0
  106. package/template/src/modules/reports/dto/list-views-query.dto.ts +17 -0
  107. package/template/src/modules/reports/dto/prepare-action-response.dto.ts +14 -0
  108. package/template/src/modules/reports/dto/prepare-action.dto.ts +27 -0
  109. package/template/src/modules/reports/dto/query-response.dto.ts +64 -0
  110. package/template/src/modules/reports/dto/query-spec.dto.ts +120 -0
  111. package/template/src/modules/reports/dto/report-definition-response.dto.ts +64 -0
  112. package/template/src/modules/reports/dto/report-meta-query.dto.ts +16 -0
  113. package/template/src/modules/reports/dto/report-meta-response.dto.ts +113 -0
  114. package/template/src/modules/reports/dto/saved-view-response.dto.ts +66 -0
  115. package/template/src/modules/reports/dto/update-report-definition.dto.ts +9 -0
  116. package/template/src/modules/reports/dto/update-saved-view.dto.ts +27 -0
  117. package/template/src/modules/reports/examples/abstract-review-board.report.json +54 -0
  118. package/template/src/modules/reports/examples/org-members.report.json +55 -0
  119. package/template/src/modules/reports/infrastructure/audit-sink.adapter.ts +31 -0
  120. package/template/src/modules/reports/infrastructure/casl-reports-authorization.ts +39 -0
  121. package/template/src/modules/reports/infrastructure/forms-adapter/form-report-source.adapter.ts +292 -0
  122. package/template/src/modules/reports/infrastructure/forms-adapter/form-row-actions.ts +171 -0
  123. package/template/src/modules/reports/infrastructure/forms-adapter/forms-bridge-bootstrap.service.ts +32 -0
  124. package/template/src/modules/reports/infrastructure/prisma-catalog.adapter.ts +95 -0
  125. package/template/src/modules/reports/infrastructure/prisma-query-executor.ts +103 -0
  126. package/template/src/modules/reports/infrastructure/prisma-snapshot-runner.ts +47 -0
  127. package/template/src/modules/reports/infrastructure/prisma-tx-runner.ts +18 -0
  128. package/template/src/modules/reports/infrastructure/registry/registry-bootstrap.service.ts +61 -0
  129. package/template/src/modules/reports/infrastructure/registry/report-extension.decorators.ts +14 -0
  130. package/template/src/modules/reports/infrastructure/reports-job-queue.adapter.ts +28 -0
  131. package/template/src/modules/reports/infrastructure/request-reports-context.ts +42 -0
  132. package/template/src/modules/reports/infrastructure/schema-check/reports-schema-check.service.ts +116 -0
  133. package/template/src/modules/reports/infrastructure/storage/local-disk-export-storage.adapter.ts +92 -0
  134. package/template/src/modules/reports/infrastructure/stores/index.ts +5 -0
  135. package/template/src/modules/reports/infrastructure/stores/prisma-bulk-action-run.store.ts +89 -0
  136. package/template/src/modules/reports/infrastructure/stores/prisma-export-job.store.ts +93 -0
  137. package/template/src/modules/reports/infrastructure/stores/prisma-report-definition.store.ts +171 -0
  138. package/template/src/modules/reports/infrastructure/stores/prisma-row-tag.store.ts +110 -0
  139. package/template/src/modules/reports/infrastructure/stores/prisma-saved-view.store.ts +144 -0
  140. package/template/src/modules/reports/presentation/reports-actions.controller.ts +83 -0
  141. package/template/src/modules/reports/presentation/reports-definitions.controller.ts +156 -0
  142. package/template/src/modules/reports/presentation/reports-export-jobs.controller.ts +61 -0
  143. package/template/src/modules/reports/presentation/reports-export.controller.ts +76 -0
  144. package/template/src/modules/reports/presentation/reports-query.controller.ts +52 -0
  145. package/template/src/modules/reports/presentation/reports-views.controller.ts +140 -0
  146. package/template/src/modules/reports/reports-forms.module.ts +33 -0
  147. package/template/src/modules/reports/reports.module.ts +335 -0
  148. package/template/src/modules/reports/reports.tokens.ts +11 -0
  149. package/template/src/modules/reports/sources/org-members.source.ts +112 -0
  150. package/template/src/modules/settings/types/setting-definitions.ts +94 -0
  151. package/template/test/forms-captcha.e2e-spec.ts +163 -0
  152. package/template/test/forms-definitions.e2e-spec.ts +394 -0
  153. package/template/test/forms-export.e2e-spec.ts +390 -0
  154. package/template/test/forms-files.e2e-spec.ts +345 -0
  155. package/template/test/forms-outbox.e2e-spec.ts +570 -0
  156. package/template/test/forms-permission-sync.spec.ts +27 -0
  157. package/template/test/forms-public.e2e-spec.ts +293 -0
  158. package/template/test/forms-schema-check.e2e-spec.ts +65 -0
  159. package/template/test/forms-submissions.e2e-spec.ts +500 -0
  160. package/template/test/forms-throttling.e2e-spec.ts +146 -0
  161. package/template/test/forms-webhooks.e2e-spec.ts +403 -0
  162. package/template/test/jest-e2e.json +1 -0
  163. package/template/test/reports-advanced.e2e-spec.ts +381 -0
  164. package/template/test/reports-permission-sync.spec.ts +30 -0
  165. package/template/test/reports-query.e2e-spec.ts +402 -0
  166. package/template/test/reports-tiers.e2e-spec.ts +343 -0
  167. package/template/test/route-registry.validator.spec.ts +22 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ftisindia/create-app",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "One-command scaffolder for the Phase 1 NestJS foundation starter.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,6 +13,10 @@ AUTH_GOOGLE_ENABLED=false
13
13
  AUTH_GOOGLE_CLIENT_ID=
14
14
  AUTH_GOOGLE_CLIENT_SECRET=
15
15
  AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/callback
16
+ # The backend redirects the browser here with a one-time ?code= after Google
17
+ # login. When a frontend app consumes this API (for example one scaffolded with
18
+ # @ftisindia/create-frontend), point this at the frontend's login route, e.g.
19
+ # AUTH_GOOGLE_SUCCESS_REDIRECT_URL=http://localhost:4200/login
16
20
  AUTH_GOOGLE_SUCCESS_REDIRECT_URL=http://localhost:3000/auth/google/success
17
21
  AUTH_GOOGLE_ERROR_REDIRECT_URL=http://localhost:3000/auth/google/error
18
22
  # Required when AUTH_GOOGLE_ENABLED=true. Use at least 32 random characters.
@@ -40,3 +44,27 @@ TEST_USER_PASSWORD=
40
44
  TEST_USER_DISPLAY_NAME=Starter Owner
41
45
  TEST_ORG_NAME=Demo Organisation
42
46
  TEST_ORG_SLUG=demo
47
+
48
+ # Form builder (@ftisindia/form-builder)
49
+ FORMS_OUTBOX_ENABLED=true
50
+ FORMS_OUTBOX_POLL_MS=5000
51
+ FORMS_OUTBOX_HEARTBEAT_MS=60000
52
+ FORMS_FILE_GC_INTERVAL_MS=3600000
53
+ FORMS_FILE_TEMP_TTL_HOURS=24
54
+ FORMS_MAX_UPLOAD_MB=25
55
+ FORMS_FILE_STORAGE_DIR=./var/uploads
56
+ # Set to off only to bypass the boot-time engine schema check (not recommended).
57
+ FORMS_SCHEMA_CHECK=on
58
+
59
+ # Report builder (@ftisindia/report-builder)
60
+ # Boot-time schema check for the engine tables (set off only to bypass temporarily).
61
+ REPORTS_SCHEMA_CHECK=on
62
+ # Optional HMAC secret for report cursors/bulk-action tokens (min 32 chars).
63
+ # When empty, a dedicated key is derived from JWT_SECRET via HKDF.
64
+ REPORTS_TOKEN_SECRET=
65
+ # Reports-owned async export worker + file storage (no forms dependency).
66
+ REPORTS_EXPORT_WORKER_ENABLED=true
67
+ REPORTS_EXPORT_POLL_MS=5000
68
+ REPORTS_EXPORT_STORAGE_DIR=./var/report-exports
69
+ REPORTS_EXPORT_RETENTION_DAYS=7
70
+ REPORTS_EXPORT_RETENTION_SWEEP_MS=3600000
@@ -117,6 +117,28 @@ store the selected `orgId`, then call
117
117
  `permissionKeys` for route, menu, and button visibility in the frontend. Backend
118
118
  guards remain the source of truth for access control.
119
119
 
120
+ A companion frontend starter exists: `npx @ftisindia/create-frontend my-app`
121
+ scaffolds an Angular app already wired to these endpoints.
122
+
123
+ ### Google OAuth with a frontend
124
+
125
+ `GET /auth/google/callback` redirects the browser to
126
+ `AUTH_GOOGLE_SUCCESS_REDIRECT_URL` with a one-time `?code=` that the frontend
127
+ must exchange via `POST /auth/oauth/exchange`. The default value points at the
128
+ backend's own origin, so set it to the frontend's login route (for example
129
+ `http://localhost:4200/login`) or the code never reaches the frontend.
130
+
131
+ ### Exporting the OpenAPI document
132
+
133
+ Frontend tooling can consume the API contract without a running server:
134
+
135
+ ```bash
136
+ npm run export:openapi
137
+ ```
138
+
139
+ This writes `docs-json.json` (the same document served at `/docs-json`) to the
140
+ project root. No database connection is required.
141
+
120
142
  ## Architecture Rules
121
143
 
122
144
  - JWTs contain identity only. Organisation membership, roles, and permissions are read per request.
@@ -126,6 +148,35 @@ guards remain the source of truth for access control.
126
148
  - Mutating business flows should write audit rows.
127
149
  - Setup and CI commands are non-destructive. Use `npm run db:reset` only when you explicitly want to reset a local database.
128
150
 
151
+ ## Dynamic Forms
152
+
153
+ The app ships with a definition-driven form engine (`@ftisindia/form-builder`
154
+ plus the `src/modules/forms` glue module). Form definitions are versioned JSON
155
+ documents created over the API; submissions run a validated, transactional
156
+ action pipeline with a reliable outbox for emails/webhooks. Most new forms
157
+ need zero backend code — register a field type, action, or data source only
158
+ when you need genuinely new behavior.
159
+
160
+ - Guide: `docs/FORMS.md` (endpoints, extension story, webhooks, public forms, files, upgrades)
161
+ - Completion standard: `docs/FORMS_CHECKLIST.md`
162
+ - Example definitions: `src/modules/forms/examples/` (registration, abstract submission, login-as-a-form)
163
+ - Definitions as code: `npm run gen:form -- <key>` scaffolds a definition + lint spec; `npm run forms:push` loads it into an org (linted, versioned, audited)
164
+
165
+ ## Dynamic Reports
166
+
167
+ The app also ships with a definition-driven report engine
168
+ (`@ftisindia/report-builder` plus the `src/modules/reports` glue module).
169
+ Report definitions are versioned JSON views over registered data sources —
170
+ form submissions are one convenient kind, any table/join is another — with
171
+ filtering, sorting, search, keyset pagination, drift-safe bulk row actions,
172
+ tags, saved views, and snapshot-consistent CSV/XLSX exports through one
173
+ declarative API. Performance is enforced at publish time: unindexed shapes
174
+ fail to publish and the error carries the exact migration SQL to fix them.
175
+
176
+ - Guide: `docs/REPORTS.md` (QuerySpec, sources, tiers, actions, exports, upgrades)
177
+ - Completion standard: `docs/REPORTS_CHECKLIST.md`
178
+ - Example definitions: `src/modules/reports/examples/` (org-members over a custom source, abstract-review-board over a form)
179
+
129
180
  ## Adding Features
130
181
 
131
182
  Use the sample module as the copy-paste reference:
@@ -5,3 +5,9 @@ coverage/
5
5
  .env
6
6
  .env.*
7
7
  !.env.example
8
+
9
+ # Generated by npm run export:openapi
10
+ docs-json.json
11
+
12
+ # Form-builder local file storage
13
+ var/
@@ -6,8 +6,11 @@
6
6
  "scripts": {
7
7
  "build": "nest build",
8
8
  "db:create": "node scripts/db-create.mjs",
9
+ "export:openapi": "ts-node scripts/export-openapi.ts",
9
10
  "format": "prettier --write .",
10
11
  "format:check": "prettier --check .",
12
+ "forms:push": "ts-node scripts/push-form.ts",
13
+ "gen:form": "node scripts/gen-form.mjs",
11
14
  "gen:module": "node scripts/gen-module.mjs",
12
15
  "lint": "eslint .",
13
16
  "prepare": "husky || true",
@@ -24,12 +27,14 @@
24
27
  "setup:ci": "node scripts/setup-local.mjs --yes",
25
28
  "test": "jest",
26
29
  "test:cov": "jest --coverage",
27
- "test:e2e": "node scripts/test-db.mjs && jest --config test/jest-e2e.json --runInBand",
30
+ "test:e2e": "node scripts/test-db.mjs && node ./node_modules/jest/bin/jest.js --config test/jest-e2e.json --runInBand",
28
31
  "test:watch": "jest --watch",
29
32
  "db:reset": "prisma migrate reset"
30
33
  },
31
34
  "dependencies": {
32
35
  "@casl/ability": "6.8.1",
36
+ "@ftisindia/form-builder": "^0.1.0",
37
+ "@ftisindia/report-builder": "^0.1.0",
33
38
  "@nestjs/common": "11.1.24",
34
39
  "@nestjs/config": "4.0.4",
35
40
  "@nestjs/core": "11.1.24",
@@ -56,6 +61,7 @@
56
61
  "@nestjs/schematics": "11.1.0",
57
62
  "@nestjs/testing": "11.1.24",
58
63
  "@types/jest": "30.0.0",
64
+ "@types/multer": "1.4.13",
59
65
  "@types/node": "20.19.41",
60
66
  "@types/passport": "1.0.17",
61
67
  "@types/passport-google-oauth20": "2.0.17",
@@ -78,6 +84,9 @@
78
84
  "typescript-eslint": "8.60.0",
79
85
  "typescript": "5.9.3"
80
86
  },
87
+ "overrides": {
88
+ "js-yaml": "4.2.0"
89
+ },
81
90
  "lint-staged": {
82
91
  "*.{js,mjs,ts,json,md,yml,yaml}": "prettier --write",
83
92
  "*.{js,mjs,ts}": "eslint --fix"
@@ -0,0 +1,188 @@
1
+ # Forms Module (`@ftisindia/form-builder`)
2
+
3
+ This app ships with a definition-driven dynamic form engine. Form definitions
4
+ are versioned JSON documents in the database; behavior — field types, button
5
+ actions, data sources — is code this app registers. New forms are **data**:
6
+ most ship with zero backend code.
7
+
8
+ - Engine architecture: the `form-builder-design.md` / ecosystem guide pair in
9
+ the starter repository.
10
+ - Completion standard: [`FORMS_CHECKLIST.md`](./FORMS_CHECKLIST.md) on top of
11
+ [`MODULE_COMPLETION_CHECKLIST.md`](./MODULE_COMPLETION_CHECKLIST.md).
12
+
13
+ ## The 60-second tour
14
+
15
+ ```text
16
+ POST /organisations/:orgId/forms forms.create (new draft version)
17
+ PATCH /organisations/:orgId/forms/:key/versions/:v forms.update (drafts only)
18
+ POST /organisations/:orgId/forms/:key/versions/:v/publish forms.publish (lints, freezes, archives prior)
19
+ POST /organisations/:orgId/forms/:key/versions/:v/public-access forms.managePublicAccess (audited)
20
+ GET /organisations/:orgId/forms/:key/render formSubmissions.create (definition + options)
21
+ POST /organisations/:orgId/forms/:key/submissions formSubmissions.create { mode, data, ... }
22
+ POST /organisations/:orgId/forms/:key/files?field=<name> formSubmissions.create (upload before submit)
23
+ GET /organisations/:orgId/forms/:key/submissions/export formSubmissions.export (audited PII egress)
24
+ GET /public/organisations/:orgId/forms/:key/render|submissions (anonymous, public forms only)
25
+ ```
26
+
27
+ Example definitions live in `src/modules/forms/examples/` — registration
28
+ (plain persist), abstract-submission (nested repeatable groups, data source,
29
+ rules, file upload), and login (public form whose submit action delegates to
30
+ the real `AuthService`).
31
+
32
+ ## How it executes
33
+
34
+ A submit runs: coerce → overwrite `source: 'context'` hidden fields from the
35
+ request context (client values for them are always discarded) → lifecycle
36
+ hooks → full validation (JSON Schema in draft or submit mode + bounded
37
+ json-logic rules + data-source membership + file references) → the action
38
+ pipeline: validation actions, then **all transactional actions in ONE
39
+ database transaction** (submission, action logs, audit rows, outbox rows
40
+ commit or roll back together), then post-commit side effects via the
41
+ **transactional outbox** (in-process poller with Postgres row-lock claiming,
42
+ idempotent enqueue, claim-token fencing, active-job heartbeat, retries with
43
+ backoff, parks as FAILED after max attempts; delivery writes an audit row). A
44
+ failed email never rolls back a valid submission; a failed transactional step
45
+ rolls back everything.
46
+
47
+ ## Adding behavior (the three registries)
48
+
49
+ Implement the engine interface, decorate the provider, list it in a module —
50
+ the registry bootstrap finds it at startup:
51
+
52
+ ```ts
53
+ import { FormDataSource } from './modules/forms/infrastructure/registry/form-extension.decorators';
54
+ import type { DataSourceDef, DataSourceContext, DataSourceOption } from '@ftisindia/form-builder';
55
+
56
+ @Injectable()
57
+ @FormDataSource()
58
+ export class TrackSource implements DataSourceDef {
59
+ key = 'conference-tracks';
60
+ constructor(private readonly prisma: PrismaService) {}
61
+ async fetch(
62
+ _params: Record<string, unknown>,
63
+ ctx: DataSourceContext,
64
+ ): Promise<DataSourceOption[]> {
65
+ return this.prisma.track
66
+ .findMany({
67
+ where: { orgId: ctx.orgId, active: true },
68
+ select: { id: true, name: true },
69
+ })
70
+ .then((rows) => rows.map((row) => ({ value: row.id, label: row.name })));
71
+ }
72
+ }
73
+ ```
74
+
75
+ Same pattern with `@FormFieldType()` (implement `FieldTypeDef`),
76
+ `@FormActionHandler()` (implement `FormAction`), `@FormLifecycleHook()`
77
+ (implement `FormLifecycle`). Builtin actions: `validateAll`, `persist`,
78
+ `persistDraft`, `lockEditing`; shipped app actions: `sendConfirmationEmail`
79
+ (post-commit, enqueues email), `authenticate` (dangerous, delegates to
80
+ `AuthService.login`).
81
+
82
+ ## Dangerous actions
83
+
84
+ An action with `dangerous: true` (payments, auth, anything with teeth) is
85
+ triple-gated: wiring it into a definition requires `forms.wireDangerous` AND
86
+ membership in the org's `forms.allowedDangerousActions` typed setting (both
87
+ checked at save/publish); at execute time the allowlist is re-checked. The
88
+ linter also refuses pipelines combining `authenticate` with `persist` —
89
+ credentials are never stored, and dangerous actions' input/output are logged
90
+ as `[REDACTED]`.
91
+
92
+ ## Public forms
93
+
94
+ Flip a published definition with `POST .../public-access { "access": "public" }`
95
+ (requires `forms.managePublicAccess`, writes an audit row). Public routes are
96
+ throttled tighter than the app default; the engine enforces the per-IP/day
97
+ cap (`forms.maxSubmissionsPerIpPerDay` setting, per-form override in the
98
+ definition's `settings`) and the captcha seam (`settings.captcha: true` —
99
+ publish fails closed unless a `CaptchaVerifier` is bound to the
100
+ `FORMS_CAPTCHA_VERIFIER` token). Public forms cannot contain file fields.
101
+
102
+ ## Files
103
+
104
+ Upload before submit (`POST .../files?field=<name>`); the submission carries
105
+ only `{ "fieldName": { "fileId": "..." } }`. The server sniffs the MIME type
106
+ from content, enforces field + org size caps, computes a sha256, and binds
107
+ the file to uploader AND org. Submit-time linking re-verifies ownership
108
+ in-transaction and promotes TEMPORARY → LINKED; orphans are garbage-collected
109
+ after `FORMS_FILE_TEMP_TTL_HOURS`. Bytes live under `FORMS_FILE_STORAGE_DIR`
110
+ (swap `LocalDiskStorageAdapter` for S3/GCS without engine changes).
111
+
112
+ ## Webhooks
113
+
114
+ A definition can declare webhook destinations that fire when a submission is
115
+ **submitted** (never for drafts, never for pipelines without `persist`):
116
+
117
+ ```jsonc
118
+ "settings": {
119
+ "webhooks": [
120
+ { "url": "https://hooks.example.com/forms", "secret": "rotate-me", "includeData": true }
121
+ ]
122
+ }
123
+ ```
124
+
125
+ - **Reliable by construction:** the job is written to the transactional outbox
126
+ inside the same transaction as the submission, then delivered post-commit
127
+ with retries/backoff (parks as FAILED after max attempts; success writes a
128
+ `forms.outbox.delivered` audit row). The Prisma store claims jobs with
129
+ `FOR UPDATE SKIP LOCKED`, assigns an opaque claim token, heartbeats active
130
+ `PROCESSING` rows with that token, and guards `DONE`/`PENDING`/`FAILED`
131
+ transitions with the same token so stale workers cannot settle reclaimed
132
+ jobs. A duplicate idempotency key is a no-op so retries do not roll back the
133
+ submission. Idempotency key: `<submissionId>:webhook:<index>`, also sent as
134
+ `x-forms-idempotency-key` for receiver-side dedupe.
135
+ - **SSRF-gated:** the destination host must be in the org's audited
136
+ `forms.webhookAllowedHosts` typed setting (default empty = webhooks
137
+ disabled), and the URL must be `https` — `http` is allowed for loopback
138
+ hosts only (local development). Enforced at save AND publish lint
139
+ (`WEBHOOK_HOST_NOT_ALLOWED`, `WEBHOOK_URL_INSECURE`, …). Delivery does a
140
+ fresh DNS resolution, rejects private/reserved/link-local destinations in
141
+ `NODE_ENV=production`, pins the request to the resolved IP while preserving
142
+ Host/SNI, and refuses redirects instead of following them.
143
+ - **Payload:** `{ event: 'submitted', orgId, formKey, formVersion, submissionId,
144
+ occurredAt, data? }` — `data` passes sensitive-field redaction; set
145
+ `includeData: false` to send a pure notification.
146
+ - **Signed:** with a `secret`, deliveries carry
147
+ `x-forms-signature: sha256=<hmac-sha256(rawBody)>`. The engine computes the
148
+ signature before enqueueing; the outbox payload stores the raw body and
149
+ signature, not the shared secret. Old-format jobs containing `secret` are
150
+ sanitized to the new `{ rawBody, signature }` payload before delivery. The
151
+ secret lives only in the definition
152
+ document (DB) — treat it as a rotatable shared token, not a master
153
+ credential. Max 5 webhooks per definition.
154
+
155
+ ## Definitions as code (`gen:form` + `forms:push`)
156
+
157
+ ```bash
158
+ npm run gen:form -- customer-feedback # scaffolds definition + lint spec
159
+ npm test # spec lints the JSON, no DB needed
160
+ npm run forms:push -- --file src/modules/forms/definitions/customer-feedback.form.json \
161
+ --org demo --user owner@example.com --publish
162
+ ```
163
+
164
+ `gen:form` writes `src/modules/forms/definitions/<key>.form.json` and
165
+ `test/<key>.form.spec.ts` (meta-schema + publish-stage lint — fails `npm test`
166
+ the moment the definition would be rejected by the API). `forms:push` loads a
167
+ definition file into an org through the real engine path: it impersonates an
168
+ existing member (their RBAC applies), so every push is linted, versioned, and
169
+ audited exactly like an HTTP call. Re-pushing an unchanged file is a no-op;
170
+ changed content creates the next version (add `--publish` to go live).
171
+
172
+ ## Org settings & env
173
+
174
+ Typed settings: `forms.allowedDangerousActions` (string[]),
175
+ `forms.maxFileSizeMb`, `forms.enableRuleIteration`, `forms.virusScanRequired`
176
+ (seam only — no scanner shipped), `forms.maxSubmissionsPerIpPerDay`.
177
+ Env knobs (see `.env.example`): `FORMS_OUTBOX_ENABLED`, `FORMS_OUTBOX_POLL_MS`,
178
+ `FORMS_FILE_GC_INTERVAL_MS`, `FORMS_FILE_TEMP_TTL_HOURS`, `FORMS_MAX_UPLOAD_MB`,
179
+ `FORMS_FILE_STORAGE_DIR`, `FORMS_SCHEMA_CHECK`.
180
+
181
+ ## Engine upgrades (schema ownership)
182
+
183
+ This app owns `schema.prisma` and the migration history. When a new engine
184
+ version changes its data model: read its changelog, copy the updated snippet
185
+ from `node_modules/@ftisindia/form-builder/prisma/forms.prisma` into
186
+ `prisma/schema.prisma`, run `npx prisma migrate dev`. The boot-time schema
187
+ check fails fast with exactly that instruction if the database and engine
188
+ ever disagree (`FORMS_SCHEMA_CHECK=off` is the emergency hatch).
@@ -0,0 +1,69 @@
1
+ # Forms Module Completion Checklist
2
+
3
+ The forms module must pass `MODULE_COMPLETION_CHECKLIST.md` verbatim, plus
4
+ these form-builder-specific items from the ecosystem guide. Re-verify after
5
+ any engine upgrade or forms-module change.
6
+
7
+ Last verified on 2026-06-13 against the generated dogfood app (`test-out`) with
8
+ local PostgreSQL `postgres/root`: `npm run dogfood`, generated app
9
+ `npm run build`, `npm run lint`, `npm test`, `npm run test:e2e`; engine
10
+ `npm test`, `npm run lint`, `npm run typecheck`.
11
+
12
+ ## Boundary
13
+
14
+ - [x] The engine package (`@ftisindia/form-builder`) compiles and unit-tests without the template; only `src/modules/forms` imports template services.
15
+ - [x] The engine's boundary lint passes: no `@nestjs/*`, `@prisma/*`, `@casl/*`, or template imports anywhere in the engine core; engine errors are engine-typed and mapped to HTTP exceptions only in the glue (`forms-error.mapper.ts`).
16
+
17
+ ## Schema ownership
18
+
19
+ - [x] Boot-time schema check proven both ways: green on a freshly migrated app; clear fail-fast message naming the engine schema version and snippet path on a deliberately stale schema.
20
+ - [x] `prisma/forms.prisma` in the engine matches the models in the app's `schema.prisma`; upgrades follow copy-snippet -> `prisma migrate dev`.
21
+
22
+ ## Permissions & registry
23
+
24
+ - [x] All 11 form permission keys exist in `permissionKeys`, are seeded, and `test/forms-permission-sync.spec.ts` passes (engine <-> template set equality).
25
+ - [x] Every protected form route is in `routePermissionRegistry` (the boot-time registry validator passes).
26
+
27
+ ## Gating
28
+
29
+ - [x] Attach-time gating proven by test: lacking `forms.wireDangerous` cannot save/publish a definition wiring a `dangerous` action; the `forms.allowedDangerousActions` org setting is respected at save/publish AND at execute time.
30
+ - [x] `forms.managePublicAccess` gating proven: lacking it cannot set or unset `settings.access = 'public'` (including publishing an already-public draft); every flip writes an audit row with previous/next.
31
+
32
+ ## Tenancy
33
+
34
+ - [x] Cross-org tests: definition fetch, submission read, and file reference across orgs are rejected as not-found.
35
+
36
+ ## Transactionality & outbox
37
+
38
+ - [x] Phase-2 transactionality proven: an induced `lockEditing` failure rolls back persist and in-tx action logs and enqueued outbox rows.
39
+ - [x] Outbox worker runs inside `requestContext.run({ source: 'worker', jobId, orgId, userId, requestId })`; retries honor backoff; exhausted jobs park as `FAILED`; an audit row (`forms.outbox.delivered`) is written on final success.
40
+ - [x] Duplicate outbox enqueue with the same idempotency key is a no-op, not a submission-rolling unique violation.
41
+ - [x] Claimed jobs carry an opaque `claimedBy` lease token; heartbeat and `DONE`/retry/failed terminal transitions are guarded by that token so stale workers cannot settle reclaimed jobs.
42
+ - [x] Active `PROCESSING` jobs heartbeat while handlers run; e2e covers both the store primitive and the actual dispatcher wiring with a slow handler.
43
+
44
+ ## Public forms
45
+
46
+ - [x] Public path: `@Public()` render/submit works without membership; a non-public form 404s on the public routes; org-scoped storage still enforced; permissioned paths unaffected.
47
+ - [x] Tier-1 abuse baseline active: public routes throttled tighter than the app default; per-IP/day caps read from typed settings (with per-form override) and enforced; IP/UA stamped on anonymous submissions; captcha seam invoked when enabled; captcha-enabled-without-adapter fails at publish, not at submit.
48
+
49
+ ## Files
50
+
51
+ - [x] Upload -> submit -> `LINKED` flow proven; a foreign user's `fileId` is rejected (IDOR); GC sweeps orphaned `TEMPORARY` files past TTL; public forms with file fields are rejected at publish.
52
+
53
+ ## Webhooks
54
+
55
+ - [x] Webhook destinations are gated by the `forms.webhookAllowedHosts` setting at save AND publish (`WEBHOOK_HOST_NOT_ALLOWED`); https required except loopback (`WEBHOOK_URL_INSECURE`).
56
+ - [x] Webhook jobs enqueue inside the submit transaction (idempotency key `<submissionId>:webhook:<i>`), never on draft saves or persist-less pipelines.
57
+ - [x] Deliveries are signed (`x-forms-signature`) when a secret is set and carry `x-forms-idempotency-key` for receiver-side dedupe; new outbox payloads store the precomputed signature, not the shared secret; legacy secret-bearing payloads are sanitized before delivery; payload data passes sensitive-field redaction; failures retry then park as FAILED.
58
+ - [x] `forms:push` loads definitions only through the audited service path (real member RBAC, save-time lint, versioning) and never raw DB writes.
59
+
60
+ ## Export & redaction
61
+
62
+ - [x] `formSubmissions.export` enforced independently of `.read`; every export writes an audit row with form key, filters, row count, and included fields.
63
+ - [x] Credential/sensitive fields never appear in `FormActionLog` or `AuditLog` metadata (redaction test); dangerous actions log `[REDACTED]` for input and output; unknown credential-like keys (`apiKey`, `token`, etc.) are defensively redacted.
64
+
65
+ ## Envelope & docs
66
+
67
+ - [x] Engine validation errors arrive as `{ error: { code, message, details: { errors: [...] } } }` through the global filter; lint failures carry `details.issues`.
68
+ - [x] Engine-thrown denials and not-founds are indistinguishable in shape from the template's own.
69
+ - [x] Swagger documents all form routes (dynamic payloads as `object`).