@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.
- package/package.json +1 -1
- package/template/.env.example +28 -0
- package/template/README.md +51 -0
- package/template/_gitignore +6 -0
- package/template/_package.json +10 -1
- package/template/docs/FORMS.md +188 -0
- package/template/docs/FORMS_CHECKLIST.md +69 -0
- package/template/docs/REPORTS.md +255 -0
- package/template/docs/REPORTS_CHECKLIST.md +152 -0
- package/template/prisma/migrations/20260612000000_add_form_builder/migration.sql +147 -0
- package/template/prisma/migrations/20260613000000_add_report_builder/migration.sql +129 -0
- package/template/prisma/migrations/20260616000000_add_form_outbox_claimed_by/migration.sql +5 -0
- package/template/prisma/schema.prisma +289 -0
- package/template/scripts/export-openapi.ts +85 -0
- package/template/scripts/gen-form.mjs +149 -0
- package/template/scripts/push-form.ts +124 -0
- package/template/src/app.module.ts +30 -8
- package/template/src/common/dto/membership-response.dto.ts +1 -0
- package/template/src/common/dto/role-summary.dto.ts +3 -3
- package/template/src/common/dto/user-summary.dto.ts +3 -3
- package/template/src/config/env.validation.ts +28 -0
- package/template/src/config/forms.config.ts +13 -0
- package/template/src/config/index.ts +2 -0
- package/template/src/config/openapi.ts +12 -0
- package/template/src/config/reports-secret.ts +15 -0
- package/template/src/config/reports.config.ts +18 -0
- package/template/src/main.ts +3 -12
- package/template/src/modules/access-control/dto/access-control-response.dto.ts +3 -0
- package/template/src/modules/access-control/dto/current-access-control-response.dto.ts +5 -1
- package/template/src/modules/access-control/types/permission-key.ts +27 -0
- package/template/src/modules/access-control/types/route-permission-registry.ts +183 -0
- package/template/src/modules/audit/dto/audit-response.dto.ts +7 -3
- package/template/src/modules/auth/auth.module.ts +3 -1
- package/template/src/modules/auth/dto/auth-response.dto.ts +1 -1
- package/template/src/modules/forms/application/services/file-gc.service.ts +85 -0
- package/template/src/modules/forms/application/services/forms-definitions.service.ts +137 -0
- package/template/src/modules/forms/application/services/forms-error.mapper.ts +64 -0
- package/template/src/modules/forms/application/services/forms-export.service.ts +210 -0
- package/template/src/modules/forms/application/services/forms-files.service.ts +164 -0
- package/template/src/modules/forms/application/services/forms-public.service.ts +49 -0
- package/template/src/modules/forms/application/services/forms-settings-reader.service.ts +53 -0
- package/template/src/modules/forms/application/services/forms-submissions.service.ts +103 -0
- package/template/src/modules/forms/application/services/handlers/authenticate.action.ts +37 -0
- package/template/src/modules/forms/application/services/handlers/logging-email.handler.ts +22 -0
- package/template/src/modules/forms/application/services/handlers/send-confirmation-email.action.ts +40 -0
- package/template/src/modules/forms/application/services/handlers/webhook-delivery.transport.ts +319 -0
- package/template/src/modules/forms/application/services/handlers/webhook.handler.ts +89 -0
- package/template/src/modules/forms/application/services/outbox-dispatcher.service.ts +131 -0
- package/template/src/modules/forms/dto/create-form-definition.dto.ts +12 -0
- package/template/src/modules/forms/dto/data-source-response.dto.ts +19 -0
- package/template/src/modules/forms/dto/export-submissions-query.dto.ts +33 -0
- package/template/src/modules/forms/dto/file-upload-response.dto.ts +24 -0
- package/template/src/modules/forms/dto/form-definition-response.dto.ts +50 -0
- package/template/src/modules/forms/dto/form-render-response.dto.ts +17 -0
- package/template/src/modules/forms/dto/list-form-definitions-query.dto.ts +10 -0
- package/template/src/modules/forms/dto/list-submissions-query.dto.ts +10 -0
- package/template/src/modules/forms/dto/public-submit-form.dto.ts +24 -0
- package/template/src/modules/forms/dto/set-public-access.dto.ts +8 -0
- package/template/src/modules/forms/dto/submission-response.dto.ts +99 -0
- package/template/src/modules/forms/dto/submit-form.dto.ts +50 -0
- package/template/src/modules/forms/dto/update-form-definition.dto.ts +12 -0
- package/template/src/modules/forms/dto/upload-file-query.dto.ts +33 -0
- package/template/src/modules/forms/dto/validate-submission.dto.ts +22 -0
- package/template/src/modules/forms/examples/abstract-submission.form.json +80 -0
- package/template/src/modules/forms/examples/login.form.json +24 -0
- package/template/src/modules/forms/examples/registration.form.json +44 -0
- package/template/src/modules/forms/forms.module.ts +228 -0
- package/template/src/modules/forms/forms.tokens.ts +6 -0
- package/template/src/modules/forms/infrastructure/audit-sink.adapter.ts +30 -0
- package/template/src/modules/forms/infrastructure/casl-forms-authorization.ts +31 -0
- package/template/src/modules/forms/infrastructure/prisma-tx-runner.ts +17 -0
- package/template/src/modules/forms/infrastructure/registry/form-extension.decorators.ts +17 -0
- package/template/src/modules/forms/infrastructure/registry/registry-bootstrap.service.ts +82 -0
- package/template/src/modules/forms/infrastructure/request-forms-context.ts +60 -0
- package/template/src/modules/forms/infrastructure/schema-check/forms-schema-check.service.ts +76 -0
- package/template/src/modules/forms/infrastructure/storage/local-disk-storage.adapter.ts +43 -0
- package/template/src/modules/forms/infrastructure/stores/index.ts +5 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-action-log.store.ts +37 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-file.store.ts +108 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-form-definition.store.ts +147 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-outbox.store.ts +156 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-submission.store.ts +164 -0
- package/template/src/modules/forms/presentation/forms-data-sources.controller.ts +58 -0
- package/template/src/modules/forms/presentation/forms-definitions.controller.ts +191 -0
- package/template/src/modules/forms/presentation/forms-files.controller.ts +79 -0
- package/template/src/modules/forms/presentation/forms-submissions.controller.ts +154 -0
- package/template/src/modules/forms/presentation/forms-upload.interceptor.ts +33 -0
- package/template/src/modules/forms/presentation/public-forms.controller.ts +51 -0
- package/template/src/modules/invitations/dto/invitation-response.dto.ts +4 -0
- package/template/src/modules/organisations/dto/organisation-response.dto.ts +1 -0
- package/template/src/modules/reports/application/services/reports-actions.service.ts +54 -0
- package/template/src/modules/reports/application/services/reports-definitions.service.ts +66 -0
- package/template/src/modules/reports/application/services/reports-error.mapper.ts +97 -0
- package/template/src/modules/reports/application/services/reports-export-dispatcher.service.ts +205 -0
- package/template/src/modules/reports/application/services/reports-exports.service.ts +78 -0
- package/template/src/modules/reports/application/services/reports-queries.service.ts +35 -0
- package/template/src/modules/reports/application/services/reports-settings-reader.service.ts +49 -0
- package/template/src/modules/reports/application/services/reports-views.service.ts +79 -0
- package/template/src/modules/reports/dto/action-result-response.dto.ts +21 -0
- package/template/src/modules/reports/dto/create-report-definition.dto.ts +86 -0
- package/template/src/modules/reports/dto/create-saved-view.dto.ts +26 -0
- package/template/src/modules/reports/dto/execute-action.dto.ts +71 -0
- package/template/src/modules/reports/dto/export-job-response.dto.ts +60 -0
- package/template/src/modules/reports/dto/export-request.dto.ts +34 -0
- package/template/src/modules/reports/dto/list-reports-query.dto.ts +10 -0
- package/template/src/modules/reports/dto/list-views-query.dto.ts +17 -0
- package/template/src/modules/reports/dto/prepare-action-response.dto.ts +14 -0
- package/template/src/modules/reports/dto/prepare-action.dto.ts +27 -0
- package/template/src/modules/reports/dto/query-response.dto.ts +64 -0
- package/template/src/modules/reports/dto/query-spec.dto.ts +120 -0
- package/template/src/modules/reports/dto/report-definition-response.dto.ts +64 -0
- package/template/src/modules/reports/dto/report-meta-query.dto.ts +16 -0
- package/template/src/modules/reports/dto/report-meta-response.dto.ts +113 -0
- package/template/src/modules/reports/dto/saved-view-response.dto.ts +66 -0
- package/template/src/modules/reports/dto/update-report-definition.dto.ts +9 -0
- package/template/src/modules/reports/dto/update-saved-view.dto.ts +27 -0
- package/template/src/modules/reports/examples/abstract-review-board.report.json +54 -0
- package/template/src/modules/reports/examples/org-members.report.json +55 -0
- package/template/src/modules/reports/infrastructure/audit-sink.adapter.ts +31 -0
- package/template/src/modules/reports/infrastructure/casl-reports-authorization.ts +39 -0
- package/template/src/modules/reports/infrastructure/forms-adapter/form-report-source.adapter.ts +292 -0
- package/template/src/modules/reports/infrastructure/forms-adapter/form-row-actions.ts +171 -0
- package/template/src/modules/reports/infrastructure/forms-adapter/forms-bridge-bootstrap.service.ts +32 -0
- package/template/src/modules/reports/infrastructure/prisma-catalog.adapter.ts +95 -0
- package/template/src/modules/reports/infrastructure/prisma-query-executor.ts +103 -0
- package/template/src/modules/reports/infrastructure/prisma-snapshot-runner.ts +47 -0
- package/template/src/modules/reports/infrastructure/prisma-tx-runner.ts +18 -0
- package/template/src/modules/reports/infrastructure/registry/registry-bootstrap.service.ts +61 -0
- package/template/src/modules/reports/infrastructure/registry/report-extension.decorators.ts +14 -0
- package/template/src/modules/reports/infrastructure/reports-job-queue.adapter.ts +28 -0
- package/template/src/modules/reports/infrastructure/request-reports-context.ts +42 -0
- package/template/src/modules/reports/infrastructure/schema-check/reports-schema-check.service.ts +116 -0
- package/template/src/modules/reports/infrastructure/storage/local-disk-export-storage.adapter.ts +92 -0
- package/template/src/modules/reports/infrastructure/stores/index.ts +5 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-bulk-action-run.store.ts +89 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-export-job.store.ts +93 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-report-definition.store.ts +171 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-row-tag.store.ts +110 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-saved-view.store.ts +144 -0
- package/template/src/modules/reports/presentation/reports-actions.controller.ts +83 -0
- package/template/src/modules/reports/presentation/reports-definitions.controller.ts +156 -0
- package/template/src/modules/reports/presentation/reports-export-jobs.controller.ts +61 -0
- package/template/src/modules/reports/presentation/reports-export.controller.ts +76 -0
- package/template/src/modules/reports/presentation/reports-query.controller.ts +52 -0
- package/template/src/modules/reports/presentation/reports-views.controller.ts +140 -0
- package/template/src/modules/reports/reports-forms.module.ts +33 -0
- package/template/src/modules/reports/reports.module.ts +335 -0
- package/template/src/modules/reports/reports.tokens.ts +11 -0
- package/template/src/modules/reports/sources/org-members.source.ts +112 -0
- package/template/src/modules/settings/types/setting-definitions.ts +94 -0
- package/template/test/forms-captcha.e2e-spec.ts +163 -0
- package/template/test/forms-definitions.e2e-spec.ts +394 -0
- package/template/test/forms-export.e2e-spec.ts +390 -0
- package/template/test/forms-files.e2e-spec.ts +345 -0
- package/template/test/forms-outbox.e2e-spec.ts +570 -0
- package/template/test/forms-permission-sync.spec.ts +27 -0
- package/template/test/forms-public.e2e-spec.ts +293 -0
- package/template/test/forms-schema-check.e2e-spec.ts +65 -0
- package/template/test/forms-submissions.e2e-spec.ts +500 -0
- package/template/test/forms-throttling.e2e-spec.ts +146 -0
- package/template/test/forms-webhooks.e2e-spec.ts +403 -0
- package/template/test/jest-e2e.json +1 -0
- package/template/test/reports-advanced.e2e-spec.ts +381 -0
- package/template/test/reports-permission-sync.spec.ts +30 -0
- package/template/test/reports-query.e2e-spec.ts +402 -0
- package/template/test/reports-tiers.e2e-spec.ts +343 -0
- package/template/test/route-registry.validator.spec.ts +22 -0
package/package.json
CHANGED
package/template/.env.example
CHANGED
|
@@ -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
|
package/template/README.md
CHANGED
|
@@ -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:
|
package/template/_gitignore
CHANGED
package/template/_package.json
CHANGED
|
@@ -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`).
|