@ftisindia/create-app 0.1.4 → 0.1.6

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 (169) hide show
  1. package/package.json +1 -1
  2. package/template/.env.example +31 -0
  3. package/template/README.md +61 -0
  4. package/template/_gitignore +6 -0
  5. package/template/_package.json +6 -0
  6. package/template/docs/FORMS.md +169 -0
  7. package/template/docs/FORMS_CHECKLIST.md +61 -0
  8. package/template/docs/REPORTS.md +246 -0
  9. package/template/docs/REPORTS_CHECKLIST.md +97 -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/schema.prisma +285 -0
  13. package/template/scripts/export-openapi.ts +85 -0
  14. package/template/scripts/gen-form.mjs +149 -0
  15. package/template/scripts/push-form.ts +124 -0
  16. package/template/src/app.module.ts +29 -8
  17. package/template/src/common/dto/membership-response.dto.ts +1 -0
  18. package/template/src/common/dto/role-summary.dto.ts +3 -3
  19. package/template/src/common/dto/user-summary.dto.ts +3 -3
  20. package/template/src/config/app.config.ts +6 -1
  21. package/template/src/config/env.validation.ts +45 -0
  22. package/template/src/config/forms.config.ts +12 -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 +16 -0
  27. package/template/src/main.ts +16 -12
  28. package/template/src/modules/access-control/access-control.module.ts +2 -1
  29. package/template/src/modules/access-control/dto/access-control-response.dto.ts +3 -0
  30. package/template/src/modules/access-control/dto/current-access-control-response.dto.ts +35 -0
  31. package/template/src/modules/access-control/presentation/current-access-control.controller.ts +40 -0
  32. package/template/src/modules/access-control/types/permission-key.ts +27 -0
  33. package/template/src/modules/access-control/types/route-permission-registry.ts +183 -0
  34. package/template/src/modules/audit/dto/audit-response.dto.ts +7 -3
  35. package/template/src/modules/auth/auth.module.ts +3 -1
  36. package/template/src/modules/auth/dto/auth-response.dto.ts +1 -1
  37. package/template/src/modules/forms/application/services/file-gc.service.ts +85 -0
  38. package/template/src/modules/forms/application/services/forms-definitions.service.ts +137 -0
  39. package/template/src/modules/forms/application/services/forms-error.mapper.ts +64 -0
  40. package/template/src/modules/forms/application/services/forms-export.service.ts +210 -0
  41. package/template/src/modules/forms/application/services/forms-files.service.ts +164 -0
  42. package/template/src/modules/forms/application/services/forms-public.service.ts +49 -0
  43. package/template/src/modules/forms/application/services/forms-settings-reader.service.ts +53 -0
  44. package/template/src/modules/forms/application/services/forms-submissions.service.ts +103 -0
  45. package/template/src/modules/forms/application/services/handlers/authenticate.action.ts +37 -0
  46. package/template/src/modules/forms/application/services/handlers/logging-email.handler.ts +22 -0
  47. package/template/src/modules/forms/application/services/handlers/send-confirmation-email.action.ts +40 -0
  48. package/template/src/modules/forms/application/services/handlers/webhook.handler.ts +41 -0
  49. package/template/src/modules/forms/application/services/outbox-dispatcher.service.ts +109 -0
  50. package/template/src/modules/forms/dto/create-form-definition.dto.ts +12 -0
  51. package/template/src/modules/forms/dto/data-source-response.dto.ts +19 -0
  52. package/template/src/modules/forms/dto/export-submissions-query.dto.ts +33 -0
  53. package/template/src/modules/forms/dto/file-upload-response.dto.ts +24 -0
  54. package/template/src/modules/forms/dto/form-definition-response.dto.ts +50 -0
  55. package/template/src/modules/forms/dto/form-render-response.dto.ts +17 -0
  56. package/template/src/modules/forms/dto/list-form-definitions-query.dto.ts +10 -0
  57. package/template/src/modules/forms/dto/list-submissions-query.dto.ts +10 -0
  58. package/template/src/modules/forms/dto/public-submit-form.dto.ts +24 -0
  59. package/template/src/modules/forms/dto/set-public-access.dto.ts +8 -0
  60. package/template/src/modules/forms/dto/submission-response.dto.ts +99 -0
  61. package/template/src/modules/forms/dto/submit-form.dto.ts +50 -0
  62. package/template/src/modules/forms/dto/update-form-definition.dto.ts +12 -0
  63. package/template/src/modules/forms/dto/upload-file-query.dto.ts +33 -0
  64. package/template/src/modules/forms/dto/validate-submission.dto.ts +22 -0
  65. package/template/src/modules/forms/examples/abstract-submission.form.json +80 -0
  66. package/template/src/modules/forms/examples/login.form.json +24 -0
  67. package/template/src/modules/forms/examples/registration.form.json +44 -0
  68. package/template/src/modules/forms/forms.module.ts +226 -0
  69. package/template/src/modules/forms/forms.tokens.ts +6 -0
  70. package/template/src/modules/forms/infrastructure/audit-sink.adapter.ts +30 -0
  71. package/template/src/modules/forms/infrastructure/casl-forms-authorization.ts +31 -0
  72. package/template/src/modules/forms/infrastructure/prisma-tx-runner.ts +17 -0
  73. package/template/src/modules/forms/infrastructure/registry/form-extension.decorators.ts +17 -0
  74. package/template/src/modules/forms/infrastructure/registry/registry-bootstrap.service.ts +82 -0
  75. package/template/src/modules/forms/infrastructure/request-forms-context.ts +60 -0
  76. package/template/src/modules/forms/infrastructure/schema-check/forms-schema-check.service.ts +76 -0
  77. package/template/src/modules/forms/infrastructure/storage/local-disk-storage.adapter.ts +43 -0
  78. package/template/src/modules/forms/infrastructure/stores/index.ts +5 -0
  79. package/template/src/modules/forms/infrastructure/stores/prisma-action-log.store.ts +37 -0
  80. package/template/src/modules/forms/infrastructure/stores/prisma-file.store.ts +108 -0
  81. package/template/src/modules/forms/infrastructure/stores/prisma-form-definition.store.ts +147 -0
  82. package/template/src/modules/forms/infrastructure/stores/prisma-outbox.store.ts +133 -0
  83. package/template/src/modules/forms/infrastructure/stores/prisma-submission.store.ts +164 -0
  84. package/template/src/modules/forms/presentation/forms-data-sources.controller.ts +58 -0
  85. package/template/src/modules/forms/presentation/forms-definitions.controller.ts +191 -0
  86. package/template/src/modules/forms/presentation/forms-files.controller.ts +79 -0
  87. package/template/src/modules/forms/presentation/forms-submissions.controller.ts +154 -0
  88. package/template/src/modules/forms/presentation/forms-upload.interceptor.ts +33 -0
  89. package/template/src/modules/forms/presentation/public-forms.controller.ts +51 -0
  90. package/template/src/modules/invitations/dto/invitation-response.dto.ts +4 -0
  91. package/template/src/modules/organisations/application/services/organisations.service.ts +67 -1
  92. package/template/src/modules/organisations/dto/organisation-response.dto.ts +52 -0
  93. package/template/src/modules/organisations/presentation/organisations.controller.ts +25 -3
  94. package/template/src/modules/reports/application/services/reports-actions.service.ts +54 -0
  95. package/template/src/modules/reports/application/services/reports-definitions.service.ts +66 -0
  96. package/template/src/modules/reports/application/services/reports-error.mapper.ts +97 -0
  97. package/template/src/modules/reports/application/services/reports-export-dispatcher.service.ts +124 -0
  98. package/template/src/modules/reports/application/services/reports-exports.service.ts +74 -0
  99. package/template/src/modules/reports/application/services/reports-queries.service.ts +35 -0
  100. package/template/src/modules/reports/application/services/reports-settings-reader.service.ts +49 -0
  101. package/template/src/modules/reports/application/services/reports-views.service.ts +79 -0
  102. package/template/src/modules/reports/dto/action-result-response.dto.ts +21 -0
  103. package/template/src/modules/reports/dto/create-report-definition.dto.ts +86 -0
  104. package/template/src/modules/reports/dto/create-saved-view.dto.ts +26 -0
  105. package/template/src/modules/reports/dto/execute-action.dto.ts +71 -0
  106. package/template/src/modules/reports/dto/export-job-response.dto.ts +60 -0
  107. package/template/src/modules/reports/dto/export-request.dto.ts +34 -0
  108. package/template/src/modules/reports/dto/list-reports-query.dto.ts +10 -0
  109. package/template/src/modules/reports/dto/list-views-query.dto.ts +17 -0
  110. package/template/src/modules/reports/dto/prepare-action-response.dto.ts +14 -0
  111. package/template/src/modules/reports/dto/prepare-action.dto.ts +27 -0
  112. package/template/src/modules/reports/dto/query-response.dto.ts +64 -0
  113. package/template/src/modules/reports/dto/query-spec.dto.ts +120 -0
  114. package/template/src/modules/reports/dto/report-definition-response.dto.ts +64 -0
  115. package/template/src/modules/reports/dto/report-meta-query.dto.ts +16 -0
  116. package/template/src/modules/reports/dto/report-meta-response.dto.ts +113 -0
  117. package/template/src/modules/reports/dto/saved-view-response.dto.ts +66 -0
  118. package/template/src/modules/reports/dto/update-report-definition.dto.ts +9 -0
  119. package/template/src/modules/reports/dto/update-saved-view.dto.ts +27 -0
  120. package/template/src/modules/reports/examples/abstract-review-board.report.json +54 -0
  121. package/template/src/modules/reports/examples/org-members.report.json +55 -0
  122. package/template/src/modules/reports/infrastructure/audit-sink.adapter.ts +31 -0
  123. package/template/src/modules/reports/infrastructure/casl-reports-authorization.ts +39 -0
  124. package/template/src/modules/reports/infrastructure/forms-adapter/form-report-source.adapter.ts +292 -0
  125. package/template/src/modules/reports/infrastructure/forms-adapter/form-row-actions.ts +171 -0
  126. package/template/src/modules/reports/infrastructure/forms-adapter/forms-bridge-bootstrap.service.ts +32 -0
  127. package/template/src/modules/reports/infrastructure/prisma-catalog.adapter.ts +95 -0
  128. package/template/src/modules/reports/infrastructure/prisma-query-executor.ts +103 -0
  129. package/template/src/modules/reports/infrastructure/prisma-snapshot-runner.ts +47 -0
  130. package/template/src/modules/reports/infrastructure/prisma-tx-runner.ts +18 -0
  131. package/template/src/modules/reports/infrastructure/registry/registry-bootstrap.service.ts +61 -0
  132. package/template/src/modules/reports/infrastructure/registry/report-extension.decorators.ts +14 -0
  133. package/template/src/modules/reports/infrastructure/reports-job-queue.adapter.ts +28 -0
  134. package/template/src/modules/reports/infrastructure/request-reports-context.ts +42 -0
  135. package/template/src/modules/reports/infrastructure/schema-check/reports-schema-check.service.ts +116 -0
  136. package/template/src/modules/reports/infrastructure/storage/local-disk-export-storage.adapter.ts +79 -0
  137. package/template/src/modules/reports/infrastructure/stores/index.ts +5 -0
  138. package/template/src/modules/reports/infrastructure/stores/prisma-bulk-action-run.store.ts +89 -0
  139. package/template/src/modules/reports/infrastructure/stores/prisma-export-job.store.ts +93 -0
  140. package/template/src/modules/reports/infrastructure/stores/prisma-report-definition.store.ts +171 -0
  141. package/template/src/modules/reports/infrastructure/stores/prisma-row-tag.store.ts +110 -0
  142. package/template/src/modules/reports/infrastructure/stores/prisma-saved-view.store.ts +144 -0
  143. package/template/src/modules/reports/presentation/reports-actions.controller.ts +83 -0
  144. package/template/src/modules/reports/presentation/reports-definitions.controller.ts +156 -0
  145. package/template/src/modules/reports/presentation/reports-export-jobs.controller.ts +61 -0
  146. package/template/src/modules/reports/presentation/reports-export.controller.ts +76 -0
  147. package/template/src/modules/reports/presentation/reports-query.controller.ts +52 -0
  148. package/template/src/modules/reports/presentation/reports-views.controller.ts +140 -0
  149. package/template/src/modules/reports/reports-forms.module.ts +33 -0
  150. package/template/src/modules/reports/reports.module.ts +335 -0
  151. package/template/src/modules/reports/reports.tokens.ts +11 -0
  152. package/template/src/modules/reports/sources/org-members.source.ts +112 -0
  153. package/template/src/modules/settings/types/setting-definitions.ts +94 -0
  154. package/template/test/forms-definitions.e2e-spec.ts +394 -0
  155. package/template/test/forms-export.e2e-spec.ts +390 -0
  156. package/template/test/forms-files.e2e-spec.ts +345 -0
  157. package/template/test/forms-outbox.e2e-spec.ts +309 -0
  158. package/template/test/forms-permission-sync.spec.ts +27 -0
  159. package/template/test/forms-public.e2e-spec.ts +269 -0
  160. package/template/test/forms-schema-check.e2e-spec.ts +65 -0
  161. package/template/test/forms-submissions.e2e-spec.ts +500 -0
  162. package/template/test/forms-webhooks.e2e-spec.ts +261 -0
  163. package/template/test/frontend-bootstrap.spec.ts +181 -0
  164. package/template/test/reports-advanced.e2e-spec.ts +368 -0
  165. package/template/test/reports-permission-sync.spec.ts +30 -0
  166. package/template/test/reports-query.e2e-spec.ts +350 -0
  167. package/template/test/reports-tiers.e2e-spec.ts +257 -0
  168. package/template/test/route-registry.validator.spec.ts +34 -0
  169. package/template/test/security.e2e-spec.ts +134 -2
@@ -0,0 +1,97 @@
1
+ # Reports Module Completion Checklist
2
+
3
+ The report builder counts as DONE only when every box below holds, on top of
4
+ the generic [`MODULE_COMPLETION_CHECKLIST.md`](./MODULE_COMPLETION_CHECKLIST.md).
5
+ Section references are to `report-builder/report-builder-design.md` (Rev 2).
6
+
7
+ ## Boundary & the standalone guarantee
8
+
9
+ - [ ] The engine package (`@ftisindia/report-builder`) compiles and unit-tests **without** the template AND **without** `@ftisindia/form-builder` installed (§3.2/§13); only `src/modules/reports` imports template services.
10
+ - [ ] The engine's boundary lint passes: no `@nestjs/*`, `@prisma/*`, `@casl/*`, `@ftisindia/form-builder`, or template imports anywhere in the core; engine errors are engine-typed and mapped to HTTP exceptions only in the glue (`reports-error.mapper.ts`).
11
+ - [ ] **`ReportsModule` imports no FormsModule.** It runs over custom sources with its OWN async-export worker + file storage (no forms outbox / UploadedFile). Form-backed sources and the delegated `editSubmission`/`updateStatus` verbs live in the **optional** `ReportsFormsModule` bridge (`reports-forms.module.ts` + `infrastructure/forms-adapter/`), the ONLY artifact aware of both engines. Dropping the bridge from `app.module.ts` leaves reports fully functional.
12
+
13
+ ## Schema ownership
14
+
15
+ - [ ] Boot-time schema check proven both ways: green on a freshly migrated app; clear fail-fast message (naming `REPORTS_ENGINE_SCHEMA_VERSION` and the snippet path) on a stale schema.
16
+ - [ ] The check also verifies the two `ReportSavedView` partial unique indexes and prints the raw SQL when they are missing (finding #5).
17
+ - [ ] `prisma/reports.prisma` in the engine matches the models in the app's `schema.prisma`; upgrades follow copy-snippet → `prisma migrate dev`.
18
+
19
+ ## Permissions & registry
20
+
21
+ - [ ] All 7 report permission keys (+ `formSubmissions.update`) exist in `permissionKeys`, are seeded, and `test/reports-permission-sync.spec.ts` passes (engine ↔ template set equality).
22
+ - [ ] Every report route is in `routePermissionRegistry` (the boot-time registry validator passes); `reports.export` is enforced independently of `reports.read`.
23
+ - [ ] Attach-time gating proven: saving/publishing a definition wiring a row action whose `requiredPermissions` the author lacks is rejected (§6.1); the same keys are re-checked at execute time.
24
+
25
+ ## The SQL boundary (§2.1)
26
+
27
+ - [ ] A definition column carrying an `sql` property is rejected by the meta-schema; SQL-shaped strings in `path`/`columnId` are rejected by the save lint (`SQL_IN_DEFINITION`).
28
+ - [ ] `$row.*` paths resolve only against the physical columns the source manifest exposes.
29
+ - [ ] Compiled statements carry user values exclusively as bind parameters (compiler snapshot tests assert no value ever lands in SQL text).
30
+
31
+ ## Query semantics (§4/§5)
32
+
33
+ - [ ] Keyset pagination proven at depth: walking pages by cursor is constant-cost; `OFFSET` appears nowhere; the rowId tiebreaker is always appended.
34
+ - [ ] Cursor integrity: tampered cursors, and valid cursors replayed against a different sort/filter/version, are rejected with the restart message.
35
+ - [ ] Typed operators: `contains` on a number column is a 400; enum values outside the declared set are a 400.
36
+ - [ ] Counts: default none; `estimated` uses the planner; `exact-capped` scans at most cap+1 rows (the subquery-LIMIT shape, finding #2).
37
+ - [ ] Org predicate compiles first on every statement; a source with `rowScope` filters rows for the scoped user (cross-org reads return empty/404, never data).
38
+ - [ ] The statement budget fires as a typed error (408), not a hung connection.
39
+
40
+ ## Tiers & publish lint (§5.2/§8)
41
+
42
+ - [ ] `live` tier publish fails when the source exceeds `reports.liveTierMaxRows`.
43
+ - [ ] `indexed` tier publish fails on missing generated columns/indexes WITH the exact suggestion SQL (`GENERATED ALWAYS AS`, `COMMENT ON COLUMN`, `CREATE INDEX`, `pg_trgm` extension when searchable); applying the snippet verbatim makes the same publish succeed (§8.1 round-trip proven by test).
44
+ - [ ] Plan lint records plan hashes into `compiled` at publish; queries after publish use the physical `rb_*` columns (not raw JSONB).
45
+ - [ ] `materialized` tier requires the declared relation to exist; responses carry `meta.freshAsOf`.
46
+
47
+ ## Row actions & bulk (§6)
48
+
49
+ - [ ] Mutations delegate to the owning domain — grep proves the reports module performs no UPDATE/DELETE on source tables; `editSubmission` re-validates through the form engine against the stamped `formVersion` (an invalid patch is rejected by the FORM engine, not the grid).
50
+ - [ ] byFilter without a token is rejected; prepare→execute happy path works; drift beyond tolerance → 409 with `currentCount`; expired token → 410.
51
+ - [ ] Idempotency: a retried execute with the same key returns the recorded outcome (`replayed: true`) without re-running.
52
+ - [ ] byIds capped; cross-org ids resolve to nothing (tenancy).
53
+
54
+ ## Tags (§7)
55
+
56
+ - [ ] `manageTags` gated by `reportTags.manage`; tags normalized; the org vocabulary setting enforced when non-empty; `label:*` single-valued replacement works.
57
+ - [ ] `$tags` hydration and `hasTag`/`hasAnyTag` filters proven against the `ReportRowTag` table.
58
+
59
+ ## Exports (§9)
60
+
61
+ - [ ] Sync export streams with `Content-Disposition` and the right MIME; async export creates the job + outbox row in ONE transaction, runs on the worker, lands an org-bound `UploadedFile`, and is pollable.
62
+ - [ ] Both run inside one `REPEATABLE READ` snapshot; the duration bound rejects over-long exports with the tier guidance.
63
+ - [ ] **Every** export writes an audit row with report key, version, filter snapshot, row count, and the exact column list; `exportable: false` columns never appear even when explicitly requested.
64
+ - [ ] XLSX output opens (structurally valid OOXML; CSV carries the formula-injection guard).
65
+
66
+ ## Saved views (§4)
67
+
68
+ - [ ] Shared-view create/update requires `reports.update`; personal views are invisible to other users; duplicate names rejected per the partial uniques.
69
+ - [ ] Publishing a version that drops/retypes a referenced column turns the view `degraded` (named columns), not a 500.
70
+
71
+ ## Envelope & docs
72
+
73
+ - [ ] Engine error codes map to the documented HTTP statuses through `reports-error.mapper.ts` only; Swagger documents every route (the frontend contract flows from OpenAPI).
74
+ - [ ] `docs/REPORTS.md` matches reality; examples in `src/modules/reports/examples/` publish and serve.
75
+
76
+ ## Test coverage (current state — keep honest)
77
+
78
+ **Engine unit tests** (`packages/report-builder`, vitest): compiler SQL shapes
79
+ (keyset OR-ladder, null surrogates, collation, bind ordering), cursor/token
80
+ roundtrip + tamper, typed-operator rejection, count subquery, meta-schema +
81
+ linter (incl. the §2.1 plain-path manifest validation), generated-column
82
+ naming/casting, catalog/plan/tier lint, suggestion-SQL emission, CSV/XLSX,
83
+ and all six services over in-memory port fakes.
84
+
85
+ **Template e2e** (`test/reports-query.e2e-spec.ts`, `test/reports-advanced.e2e-spec.ts`,
86
+ real Postgres over the org-members CUSTOM source): publish→query, keyset
87
+ paging, typed filter rejection, meta, permission gating, saved views +
88
+ compatibility, tags, sync CSV export; byFilter prepare/execute **token
89
+ protocol + drift (409) + idempotent replay**, the reports-owned **async
90
+ export worker + download**, **XLSX zip validity**, the **PII-egress audit
91
+ row**, **shared-view partial-unique** (409), and **indexed-tier publish
92
+ rejection with the `CREATE INDEX` migration snippet**.
93
+
94
+ **Not yet covered by e2e (unit-proven only — flag before claiming complete):**
95
+ the `materialized` tier (needs a real materialized view), the indexed-tier
96
+ publish *success* round-trip after applying the suggested migration, and the
97
+ boot schema-check *failure* path (the success path is proven by every boot).
@@ -0,0 +1,147 @@
1
+ -- CreateEnum
2
+ CREATE TYPE "FormDefinitionStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED');
3
+
4
+ -- CreateEnum
5
+ CREATE TYPE "SubmissionStatus" AS ENUM ('DRAFT', 'SUBMITTED');
6
+
7
+ -- CreateEnum
8
+ CREATE TYPE "FormActionStatus" AS ENUM ('OK', 'ERROR');
9
+
10
+ -- CreateEnum
11
+ CREATE TYPE "OutboxStatus" AS ENUM ('PENDING', 'PROCESSING', 'DONE', 'FAILED');
12
+
13
+ -- CreateEnum
14
+ CREATE TYPE "FileStatus" AS ENUM ('TEMPORARY', 'SCANNING', 'CLEAN', 'INFECTED', 'LINKED');
15
+
16
+ -- CreateTable
17
+ CREATE TABLE "FormDefinition" (
18
+ "id" TEXT NOT NULL,
19
+ "orgId" TEXT NOT NULL,
20
+ "key" TEXT NOT NULL,
21
+ "version" INTEGER NOT NULL,
22
+ "status" "FormDefinitionStatus" NOT NULL DEFAULT 'DRAFT',
23
+ "schema" JSONB NOT NULL,
24
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
25
+ "updatedAt" TIMESTAMP(3) NOT NULL,
26
+
27
+ CONSTRAINT "FormDefinition_pkey" PRIMARY KEY ("id")
28
+ );
29
+
30
+ -- CreateTable
31
+ CREATE TABLE "FormSubmission" (
32
+ "id" TEXT NOT NULL,
33
+ "orgId" TEXT NOT NULL,
34
+ "formKey" TEXT NOT NULL,
35
+ "formVersion" INTEGER NOT NULL,
36
+ "data" JSONB NOT NULL,
37
+ "status" "SubmissionStatus" NOT NULL DEFAULT 'DRAFT',
38
+ "locked" BOOLEAN NOT NULL DEFAULT false,
39
+ "createdBy" TEXT,
40
+ "ipAddress" TEXT,
41
+ "userAgent" TEXT,
42
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
43
+ "updatedAt" TIMESTAMP(3) NOT NULL,
44
+
45
+ CONSTRAINT "FormSubmission_pkey" PRIMARY KEY ("id")
46
+ );
47
+
48
+ -- CreateTable
49
+ CREATE TABLE "FormActionLog" (
50
+ "id" TEXT NOT NULL,
51
+ "orgId" TEXT NOT NULL,
52
+ "submissionId" TEXT,
53
+ "formKey" TEXT NOT NULL,
54
+ "formVersion" INTEGER,
55
+ "action" TEXT NOT NULL,
56
+ "status" "FormActionStatus" NOT NULL,
57
+ "input" JSONB,
58
+ "output" JSONB,
59
+ "errorCode" TEXT,
60
+ "errorMessage" TEXT,
61
+ "actorId" TEXT,
62
+ "requestId" TEXT,
63
+ "durationMs" INTEGER,
64
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
65
+
66
+ CONSTRAINT "FormActionLog_pkey" PRIMARY KEY ("id")
67
+ );
68
+
69
+ -- CreateTable
70
+ CREATE TABLE "FormOutboxJob" (
71
+ "id" TEXT NOT NULL,
72
+ "type" TEXT NOT NULL,
73
+ "payload" JSONB NOT NULL,
74
+ "status" "OutboxStatus" NOT NULL DEFAULT 'PENDING',
75
+ "attempts" INTEGER NOT NULL DEFAULT 0,
76
+ "maxAttempts" INTEGER NOT NULL DEFAULT 8,
77
+ "idempotencyKey" TEXT,
78
+ "lastError" TEXT,
79
+ "runAfter" TIMESTAMP(3),
80
+ "orgId" TEXT,
81
+ "actorUserId" TEXT,
82
+ "originRequestId" TEXT,
83
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
84
+ "updatedAt" TIMESTAMP(3) NOT NULL,
85
+
86
+ CONSTRAINT "FormOutboxJob_pkey" PRIMARY KEY ("id")
87
+ );
88
+
89
+ -- CreateTable
90
+ CREATE TABLE "UploadedFile" (
91
+ "id" TEXT NOT NULL,
92
+ "storageKey" TEXT NOT NULL,
93
+ "originalName" TEXT NOT NULL,
94
+ "mimeType" TEXT NOT NULL,
95
+ "size" INTEGER NOT NULL,
96
+ "checksum" TEXT NOT NULL,
97
+ "ownerId" TEXT NOT NULL,
98
+ "orgId" TEXT NOT NULL,
99
+ "status" "FileStatus" NOT NULL DEFAULT 'TEMPORARY',
100
+ "submissionId" TEXT,
101
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
102
+
103
+ CONSTRAINT "UploadedFile_pkey" PRIMARY KEY ("id")
104
+ );
105
+
106
+ -- CreateIndex
107
+ CREATE INDEX "FormDefinition_orgId_idx" ON "FormDefinition"("orgId");
108
+
109
+ -- CreateIndex
110
+ CREATE INDEX "FormDefinition_orgId_status_idx" ON "FormDefinition"("orgId", "status");
111
+
112
+ -- CreateIndex
113
+ CREATE UNIQUE INDEX "FormDefinition_orgId_key_version_key" ON "FormDefinition"("orgId", "key", "version");
114
+
115
+ -- CreateIndex
116
+ CREATE INDEX "FormSubmission_orgId_formKey_status_idx" ON "FormSubmission"("orgId", "formKey", "status");
117
+
118
+ -- CreateIndex
119
+ CREATE INDEX "FormSubmission_orgId_createdAt_idx" ON "FormSubmission"("orgId", "createdAt");
120
+
121
+ -- CreateIndex
122
+ CREATE INDEX "FormSubmission_orgId_formKey_ipAddress_createdAt_idx" ON "FormSubmission"("orgId", "formKey", "ipAddress", "createdAt");
123
+
124
+ -- CreateIndex
125
+ CREATE INDEX "FormActionLog_orgId_createdAt_idx" ON "FormActionLog"("orgId", "createdAt");
126
+
127
+ -- CreateIndex
128
+ CREATE INDEX "FormActionLog_orgId_formKey_action_status_idx" ON "FormActionLog"("orgId", "formKey", "action", "status");
129
+
130
+ -- CreateIndex
131
+ CREATE UNIQUE INDEX "FormOutboxJob_idempotencyKey_key" ON "FormOutboxJob"("idempotencyKey");
132
+
133
+ -- CreateIndex
134
+ CREATE INDEX "FormOutboxJob_status_runAfter_idx" ON "FormOutboxJob"("status", "runAfter");
135
+
136
+ -- CreateIndex
137
+ CREATE UNIQUE INDEX "UploadedFile_storageKey_key" ON "UploadedFile"("storageKey");
138
+
139
+ -- CreateIndex
140
+ CREATE INDEX "UploadedFile_orgId_status_idx" ON "UploadedFile"("orgId", "status");
141
+
142
+ -- CreateIndex
143
+ CREATE INDEX "UploadedFile_ownerId_status_idx" ON "UploadedFile"("ownerId", "status");
144
+
145
+ -- CreateIndex
146
+ CREATE INDEX "UploadedFile_status_createdAt_idx" ON "UploadedFile"("status", "createdAt");
147
+
@@ -0,0 +1,129 @@
1
+ -- CreateEnum
2
+ CREATE TYPE "ReportDefinitionStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED');
3
+
4
+ -- CreateEnum
5
+ CREATE TYPE "ReportExportStatus" AS ENUM ('PENDING', 'RUNNING', 'DONE', 'FAILED');
6
+
7
+ -- CreateEnum
8
+ CREATE TYPE "ReportActionRunStatus" AS ENUM ('RUNNING', 'DONE', 'FAILED');
9
+
10
+ -- CreateTable
11
+ CREATE TABLE "ReportDefinition" (
12
+ "id" TEXT NOT NULL,
13
+ "orgId" TEXT NOT NULL,
14
+ "key" TEXT NOT NULL,
15
+ "version" INTEGER NOT NULL,
16
+ "status" "ReportDefinitionStatus" NOT NULL DEFAULT 'DRAFT',
17
+ "schema" JSONB NOT NULL,
18
+ "compiled" JSONB,
19
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
20
+ "updatedAt" TIMESTAMP(3) NOT NULL,
21
+
22
+ CONSTRAINT "ReportDefinition_pkey" PRIMARY KEY ("id")
23
+ );
24
+
25
+ -- CreateTable
26
+ CREATE TABLE "ReportSavedView" (
27
+ "id" TEXT NOT NULL,
28
+ "orgId" TEXT NOT NULL,
29
+ "reportKey" TEXT NOT NULL,
30
+ "reportVersion" INTEGER NOT NULL,
31
+ "name" TEXT NOT NULL,
32
+ "spec" JSONB NOT NULL,
33
+ "ownerId" TEXT,
34
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
35
+
36
+ CONSTRAINT "ReportSavedView_pkey" PRIMARY KEY ("id")
37
+ );
38
+
39
+ -- CreateTable
40
+ CREATE TABLE "ReportExportJob" (
41
+ "id" TEXT NOT NULL,
42
+ "orgId" TEXT NOT NULL,
43
+ "reportKey" TEXT NOT NULL,
44
+ "reportVersion" INTEGER NOT NULL,
45
+ "spec" JSONB NOT NULL,
46
+ "asOf" TIMESTAMP(3),
47
+ "status" "ReportExportStatus" NOT NULL DEFAULT 'PENDING',
48
+ "fileId" TEXT,
49
+ "rowCount" INTEGER,
50
+ "error" TEXT,
51
+ "requestedBy" TEXT NOT NULL,
52
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
53
+ "updatedAt" TIMESTAMP(3) NOT NULL,
54
+
55
+ CONSTRAINT "ReportExportJob_pkey" PRIMARY KEY ("id")
56
+ );
57
+
58
+ -- CreateTable
59
+ CREATE TABLE "ReportBulkActionRun" (
60
+ "id" TEXT NOT NULL,
61
+ "orgId" TEXT NOT NULL,
62
+ "reportKey" TEXT NOT NULL,
63
+ "reportVersion" INTEGER NOT NULL,
64
+ "action" TEXT NOT NULL,
65
+ "idempotencyKey" TEXT NOT NULL,
66
+ "status" "ReportActionRunStatus" NOT NULL DEFAULT 'RUNNING',
67
+ "result" JSONB,
68
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
69
+ "updatedAt" TIMESTAMP(3) NOT NULL,
70
+
71
+ CONSTRAINT "ReportBulkActionRun_pkey" PRIMARY KEY ("id")
72
+ );
73
+
74
+ -- CreateTable
75
+ CREATE TABLE "ReportRowTag" (
76
+ "id" TEXT NOT NULL,
77
+ "orgId" TEXT NOT NULL,
78
+ "sourceKind" TEXT NOT NULL,
79
+ "sourceKey" TEXT NOT NULL,
80
+ "rowId" TEXT NOT NULL,
81
+ "tag" TEXT NOT NULL,
82
+ "createdBy" TEXT NOT NULL,
83
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
84
+
85
+ CONSTRAINT "ReportRowTag_pkey" PRIMARY KEY ("id")
86
+ );
87
+
88
+ -- CreateIndex
89
+ CREATE UNIQUE INDEX "ReportDefinition_orgId_key_version_key" ON "ReportDefinition"("orgId", "key", "version");
90
+
91
+ -- CreateIndex
92
+ CREATE INDEX "ReportDefinition_orgId_idx" ON "ReportDefinition"("orgId");
93
+
94
+ -- CreateIndex
95
+ CREATE INDEX "ReportDefinition_orgId_status_idx" ON "ReportDefinition"("orgId", "status");
96
+
97
+ -- CreateIndex
98
+ CREATE INDEX "ReportSavedView_orgId_reportKey_idx" ON "ReportSavedView"("orgId", "reportKey");
99
+
100
+ -- CreateIndex
101
+ CREATE INDEX "ReportExportJob_orgId_status_idx" ON "ReportExportJob"("orgId", "status");
102
+
103
+ -- CreateIndex
104
+ CREATE INDEX "ReportExportJob_orgId_createdAt_idx" ON "ReportExportJob"("orgId", "createdAt");
105
+
106
+ -- CreateIndex
107
+ CREATE UNIQUE INDEX "ReportBulkActionRun_orgId_idempotencyKey_key" ON "ReportBulkActionRun"("orgId", "idempotencyKey");
108
+
109
+ -- CreateIndex
110
+ CREATE INDEX "ReportBulkActionRun_orgId_reportKey_action_idx" ON "ReportBulkActionRun"("orgId", "reportKey", "action");
111
+
112
+ -- CreateIndex
113
+ CREATE UNIQUE INDEX "ReportRowTag_orgId_sourceKind_sourceKey_rowId_tag_key" ON "ReportRowTag"("orgId", "sourceKind", "sourceKey", "rowId", "tag");
114
+
115
+ -- CreateIndex
116
+ CREATE INDEX "ReportRowTag_orgId_sourceKind_sourceKey_tag_idx" ON "ReportRowTag"("orgId", "sourceKind", "sourceKey", "tag");
117
+
118
+ -- CreateIndex
119
+ CREATE INDEX "ReportRowTag_orgId_sourceKind_sourceKey_rowId_idx" ON "ReportRowTag"("orgId", "sourceKind", "sourceKey", "rowId");
120
+
121
+ -- Partial unique indexes for saved-view names (report design §12, finding #5).
122
+ -- Postgres treats NULLs as distinct, so a plain unique over nullable "ownerId"
123
+ -- would not deduplicate SHARED views. Prisma cannot express partial indexes:
124
+ -- if a later `prisma migrate dev` proposes dropping these, DELETE those DROP
125
+ -- lines — the reports boot check fails fast when they are missing.
126
+ CREATE UNIQUE INDEX "report_view_shared_uq"
127
+ ON "ReportSavedView" ("orgId", "reportKey", "name") WHERE "ownerId" IS NULL;
128
+ CREATE UNIQUE INDEX "report_view_personal_uq"
129
+ ON "ReportSavedView" ("orgId", "reportKey", "ownerId", "name") WHERE "ownerId" IS NOT NULL;
@@ -297,3 +297,288 @@ model AuditLog {
297
297
  @@index([targetType, targetId])
298
298
  @@index([createdAt])
299
299
  }
300
+ // ─────────────────────────────────────────────────────────────────────────────
301
+ // @ftisindia/form-builder — canonical Prisma model snippet (ENGINE_SCHEMA_VERSION 1)
302
+ //
303
+ // The APP owns schema.prisma and the migration history (ecosystem guide §10.1).
304
+ // Copy these models into your app's prisma/schema.prisma verbatim and run
305
+ // `prisma migrate dev`. The glue module's boot-time check verifies the live
306
+ // database matches this snippet's tables/columns and fails fast otherwise.
307
+ // Conventions follow the FTIS template: uuid() ids, UPPER_CASE enum values,
308
+ // org-leading hot indexes, explicit orgId on every engine table.
309
+ // ─────────────────────────────────────────────────────────────────────────────
310
+
311
+ enum FormDefinitionStatus {
312
+ DRAFT
313
+ PUBLISHED
314
+ ARCHIVED
315
+ }
316
+
317
+ enum SubmissionStatus {
318
+ DRAFT
319
+ SUBMITTED
320
+ }
321
+
322
+ enum FormActionStatus {
323
+ OK
324
+ ERROR
325
+ }
326
+
327
+ enum OutboxStatus {
328
+ PENDING
329
+ PROCESSING
330
+ DONE
331
+ FAILED
332
+ }
333
+
334
+ enum FileStatus {
335
+ TEMPORARY
336
+ SCANNING
337
+ CLEAN
338
+ INFECTED
339
+ LINKED
340
+ }
341
+
342
+ model FormDefinition {
343
+ id String @id @default(uuid())
344
+ orgId String
345
+ key String
346
+ // Bumped on every published change; published rows are immutable.
347
+ version Int
348
+ status FormDefinitionStatus @default(DRAFT)
349
+ // The full FormDefinition document (JSONB).
350
+ schema Json
351
+
352
+ createdAt DateTime @default(now())
353
+ updatedAt DateTime @updatedAt
354
+
355
+ @@unique([orgId, key, version])
356
+ @@index([orgId])
357
+ @@index([orgId, status])
358
+ }
359
+
360
+ model FormSubmission {
361
+ id String @id @default(uuid())
362
+ orgId String
363
+ formKey String
364
+ // The exact definition version the data was captured under. Non-negotiable.
365
+ formVersion Int
366
+ // Nested submission payload (JSONB) — submissions are documents, not rows.
367
+ data Json
368
+ status SubmissionStatus @default(DRAFT)
369
+ // Set by the lockEditing action; locked submissions reject further edits.
370
+ locked Boolean @default(false)
371
+
372
+ createdBy String?
373
+ // Stamped only on anonymous/public submissions (abuse-control Tier 1).
374
+ ipAddress String?
375
+ userAgent String?
376
+
377
+ createdAt DateTime @default(now())
378
+ updatedAt DateTime @updatedAt
379
+
380
+ @@index([orgId, formKey, status])
381
+ @@index([orgId, createdAt])
382
+ @@index([orgId, formKey, ipAddress, createdAt])
383
+ }
384
+
385
+ model FormActionLog {
386
+ id String @id @default(uuid())
387
+ orgId String
388
+ submissionId String?
389
+ formKey String
390
+ formVersion Int?
391
+ action String
392
+ status FormActionStatus
393
+ // Redacted by the engine before write — sensitive fields never land here.
394
+ input Json?
395
+ output Json?
396
+ errorCode String?
397
+ errorMessage String?
398
+ actorId String?
399
+ requestId String?
400
+ durationMs Int?
401
+ createdAt DateTime @default(now())
402
+
403
+ @@index([orgId, createdAt])
404
+ @@index([orgId, formKey, action, status])
405
+ }
406
+
407
+ model FormOutboxJob {
408
+ id String @id @default(uuid())
409
+ type String
410
+ payload Json
411
+ status OutboxStatus @default(PENDING)
412
+
413
+ attempts Int @default(0)
414
+ maxAttempts Int @default(8)
415
+ // Prevents double-send on retry (e.g. submissionId:type:action).
416
+ idempotencyKey String? @unique
417
+ lastError String?
418
+ runAfter DateTime?
419
+
420
+ // Worker-context restoration + audit correlation (ecosystem guide §3).
421
+ orgId String?
422
+ actorUserId String?
423
+ originRequestId String?
424
+
425
+ createdAt DateTime @default(now())
426
+ updatedAt DateTime @updatedAt
427
+
428
+ @@index([status, runAfter])
429
+ }
430
+
431
+ model UploadedFile {
432
+ id String @id @default(uuid())
433
+ storageKey String @unique
434
+ originalName String
435
+ // SERVER-VERIFIED (sniffed) MIME type, never the client-sent Content-Type.
436
+ mimeType String
437
+ size Int
438
+ // sha256 hex.
439
+ checksum String
440
+ // Two-factor ownership: uploader identity AND tenant scope (§10).
441
+ ownerId String
442
+ orgId String
443
+
444
+ status FileStatus @default(TEMPORARY)
445
+ submissionId String?
446
+
447
+ createdAt DateTime @default(now())
448
+
449
+ @@index([orgId, status])
450
+ @@index([ownerId, status])
451
+ @@index([status, createdAt])
452
+ }
453
+
454
+ // ─────────────────────────────────────────────────────────────────────────────
455
+ // Report builder (@ftisindia/report-builder) — engine tables
456
+ // (REPORTS_ENGINE_SCHEMA_VERSION 1). Copied from the engine's canonical
457
+ // snippet (@ftisindia/report-builder/prisma/reports.prisma); the glue
458
+ // module's boot check verifies the live database matches it. Sources stay
459
+ // where they live — these are the engine's OWN tables only (report design §12).
460
+ // ─────────────────────────────────────────────────────────────────────────────
461
+
462
+ enum ReportDefinitionStatus {
463
+ DRAFT
464
+ PUBLISHED
465
+ ARCHIVED
466
+ }
467
+
468
+ enum ReportExportStatus {
469
+ PENDING
470
+ RUNNING
471
+ DONE
472
+ FAILED
473
+ }
474
+
475
+ enum ReportActionRunStatus {
476
+ RUNNING
477
+ DONE
478
+ FAILED
479
+ }
480
+
481
+ model ReportDefinition {
482
+ id String @id @default(uuid())
483
+ orgId String
484
+ key String
485
+ // Bumped on every published change; published rows are immutable.
486
+ version Int
487
+ status ReportDefinitionStatus @default(DRAFT)
488
+ // The full ReportDefinition document (JSONB).
489
+ schema Json
490
+ // Set at publish: logical→physical column map + plan hashes (report design §5.2/§8.1).
491
+ compiled Json?
492
+
493
+ createdAt DateTime @default(now())
494
+ updatedAt DateTime @updatedAt
495
+
496
+ @@unique([orgId, key, version])
497
+ @@index([orgId])
498
+ @@index([orgId, status])
499
+ }
500
+
501
+ model ReportSavedView {
502
+ id String @id @default(uuid())
503
+ orgId String
504
+ reportKey String
505
+ // The definition version the view was authored against (report design §4).
506
+ reportVersion Int
507
+ name String
508
+ // The saved QuerySpec (JSONB).
509
+ spec Json
510
+ // null ⇒ shared with the whole org (creation gated by reports.update).
511
+ ownerId String?
512
+ createdAt DateTime @default(now())
513
+
514
+ @@index([orgId, reportKey])
515
+ // Uniqueness lives in PARTIAL UNIQUE INDEXES shipped as raw SQL inside the
516
+ // add_report_builder migration — Postgres treats NULLs as distinct, so a
517
+ // @@unique over nullable ownerId would NOT deduplicate shared views (report
518
+ // design §12, finding #5). If a later `prisma migrate dev` proposes
519
+ // DROP INDEX "report_view_shared_uq" / "report_view_personal_uq", DELETE
520
+ // those lines — the reports boot check fails fast if they go missing.
521
+ }
522
+
523
+ model ReportExportJob {
524
+ id String @id @default(uuid())
525
+ orgId String
526
+ reportKey String
527
+ // The version the export ran against (report design §9, finding #1).
528
+ reportVersion Int
529
+ // FULL export spec {spec, format, columns} — replayable, auditable (not a hash).
530
+ spec Json
531
+ // REPEATABLE READ snapshot timestamp, set when the job runs (finding #8).
532
+ asOf DateTime?
533
+ status ReportExportStatus @default(PENDING)
534
+ // UploadedFile id once the async export lands in file storage.
535
+ fileId String?
536
+ rowCount Int?
537
+ error String?
538
+ requestedBy String
539
+
540
+ createdAt DateTime @default(now())
541
+ updatedAt DateTime @updatedAt
542
+
543
+ @@index([orgId, status])
544
+ @@index([orgId, createdAt])
545
+ }
546
+
547
+ model ReportBulkActionRun {
548
+ id String @id @default(uuid())
549
+ orgId String
550
+ reportKey String
551
+ reportVersion Int
552
+ action String
553
+ // Client-supplied; a retried execute with the same key returns the recorded
554
+ // outcome instead of re-running (report design §6.3, finding #7).
555
+ idempotencyKey String
556
+ status ReportActionRunStatus @default(RUNNING)
557
+ result Json?
558
+
559
+ createdAt DateTime @default(now())
560
+ updatedAt DateTime @updatedAt
561
+
562
+ @@unique([orgId, idempotencyKey])
563
+ @@index([orgId, reportKey, action])
564
+ }
565
+
566
+ model ReportRowTag {
567
+ id String @id @default(uuid())
568
+ orgId String
569
+ // 'custom' | 'form' | any registered source-provider kind.
570
+ sourceKind String
571
+ // Custom source key or form key.
572
+ sourceKey String
573
+ // The source's declared, STABLE row identity (report design §7).
574
+ rowId String
575
+ // Normalized: lowercased, trimmed. Labels are single-valued by convention
576
+ // ("label:approved") — one table, one mechanism.
577
+ tag String
578
+ createdBy String
579
+ createdAt DateTime @default(now())
580
+
581
+ @@unique([orgId, sourceKind, sourceKey, rowId, tag])
582
+ @@index([orgId, sourceKind, sourceKey, tag])
583
+ @@index([orgId, sourceKind, sourceKey, rowId])
584
+ }