@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
@@ -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;
@@ -0,0 +1,5 @@
1
+ -- Add an opaque lease token for outbox claim ownership. Dispatchers must match
2
+ -- this token when heartbeating or settling PROCESSING jobs.
3
+ ALTER TABLE "FormOutboxJob" ADD COLUMN "claimedBy" TEXT;
4
+
5
+ CREATE INDEX "FormOutboxJob_status_claimedBy_idx" ON "FormOutboxJob"("status", "claimedBy");
@@ -297,3 +297,292 @@ 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
+ // Opaque worker lease token. Heartbeats and terminal transitions must match
420
+ // this value so a stale worker cannot finish another worker's reclaimed job.
421
+ claimedBy String?
422
+
423
+ // Worker-context restoration + audit correlation (ecosystem guide §3).
424
+ orgId String?
425
+ actorUserId String?
426
+ originRequestId String?
427
+
428
+ createdAt DateTime @default(now())
429
+ updatedAt DateTime @updatedAt
430
+
431
+ @@index([status, runAfter])
432
+ @@index([status, claimedBy])
433
+ }
434
+
435
+ model UploadedFile {
436
+ id String @id @default(uuid())
437
+ storageKey String @unique
438
+ originalName String
439
+ // SERVER-VERIFIED (sniffed) MIME type, never the client-sent Content-Type.
440
+ mimeType String
441
+ size Int
442
+ // sha256 hex.
443
+ checksum String
444
+ // Two-factor ownership: uploader identity AND tenant scope (§10).
445
+ ownerId String
446
+ orgId String
447
+
448
+ status FileStatus @default(TEMPORARY)
449
+ submissionId String?
450
+
451
+ createdAt DateTime @default(now())
452
+
453
+ @@index([orgId, status])
454
+ @@index([ownerId, status])
455
+ @@index([status, createdAt])
456
+ }
457
+
458
+ // ─────────────────────────────────────────────────────────────────────────────
459
+ // Report builder (@ftisindia/report-builder) — engine tables
460
+ // (REPORTS_ENGINE_SCHEMA_VERSION 1). Copied from the engine's canonical
461
+ // snippet (@ftisindia/report-builder/prisma/reports.prisma); the glue
462
+ // module's boot check verifies the live database matches it. Sources stay
463
+ // where they live — these are the engine's OWN tables only (report design §12).
464
+ // ─────────────────────────────────────────────────────────────────────────────
465
+
466
+ enum ReportDefinitionStatus {
467
+ DRAFT
468
+ PUBLISHED
469
+ ARCHIVED
470
+ }
471
+
472
+ enum ReportExportStatus {
473
+ PENDING
474
+ RUNNING
475
+ DONE
476
+ FAILED
477
+ }
478
+
479
+ enum ReportActionRunStatus {
480
+ RUNNING
481
+ DONE
482
+ FAILED
483
+ }
484
+
485
+ model ReportDefinition {
486
+ id String @id @default(uuid())
487
+ orgId String
488
+ key String
489
+ // Bumped on every published change; published rows are immutable.
490
+ version Int
491
+ status ReportDefinitionStatus @default(DRAFT)
492
+ // The full ReportDefinition document (JSONB).
493
+ schema Json
494
+ // Set at publish: logical→physical column map + plan hashes (report design §5.2/§8.1).
495
+ compiled Json?
496
+
497
+ createdAt DateTime @default(now())
498
+ updatedAt DateTime @updatedAt
499
+
500
+ @@unique([orgId, key, version])
501
+ @@index([orgId])
502
+ @@index([orgId, status])
503
+ }
504
+
505
+ model ReportSavedView {
506
+ id String @id @default(uuid())
507
+ orgId String
508
+ reportKey String
509
+ // The definition version the view was authored against (report design §4).
510
+ reportVersion Int
511
+ name String
512
+ // The saved QuerySpec (JSONB).
513
+ spec Json
514
+ // null ⇒ shared with the whole org (creation gated by reports.update).
515
+ ownerId String?
516
+ createdAt DateTime @default(now())
517
+
518
+ @@index([orgId, reportKey])
519
+ // Uniqueness lives in PARTIAL UNIQUE INDEXES shipped as raw SQL inside the
520
+ // add_report_builder migration — Postgres treats NULLs as distinct, so a
521
+ // @@unique over nullable ownerId would NOT deduplicate shared views (report
522
+ // design §12, finding #5). If a later `prisma migrate dev` proposes
523
+ // DROP INDEX "report_view_shared_uq" / "report_view_personal_uq", DELETE
524
+ // those lines — the reports boot check fails fast if they go missing.
525
+ }
526
+
527
+ model ReportExportJob {
528
+ id String @id @default(uuid())
529
+ orgId String
530
+ reportKey String
531
+ // The version the export ran against (report design §9, finding #1).
532
+ reportVersion Int
533
+ // FULL export spec {spec, format, columns} — replayable, auditable (not a hash).
534
+ spec Json
535
+ // REPEATABLE READ snapshot timestamp, set when the job runs (finding #8).
536
+ asOf DateTime?
537
+ status ReportExportStatus @default(PENDING)
538
+ // Reports-owned storage key once the async export lands in file storage.
539
+ fileId String?
540
+ rowCount Int?
541
+ error String?
542
+ requestedBy String
543
+
544
+ createdAt DateTime @default(now())
545
+ updatedAt DateTime @updatedAt
546
+
547
+ @@index([orgId, status])
548
+ @@index([orgId, createdAt])
549
+ }
550
+
551
+ model ReportBulkActionRun {
552
+ id String @id @default(uuid())
553
+ orgId String
554
+ reportKey String
555
+ reportVersion Int
556
+ action String
557
+ // Client-supplied; a retried execute with the same key returns the recorded
558
+ // outcome instead of re-running (report design §6.3, finding #7).
559
+ idempotencyKey String
560
+ status ReportActionRunStatus @default(RUNNING)
561
+ result Json?
562
+
563
+ createdAt DateTime @default(now())
564
+ updatedAt DateTime @updatedAt
565
+
566
+ @@unique([orgId, idempotencyKey])
567
+ @@index([orgId, reportKey, action])
568
+ }
569
+
570
+ model ReportRowTag {
571
+ id String @id @default(uuid())
572
+ orgId String
573
+ // 'custom' | 'form' | any registered source-provider kind.
574
+ sourceKind String
575
+ // Custom source key or form key.
576
+ sourceKey String
577
+ // The source's declared, STABLE row identity (report design §7).
578
+ rowId String
579
+ // Normalized: lowercased, trimmed. Labels are single-valued by convention
580
+ // ("label:approved") — one table, one mechanism.
581
+ tag String
582
+ createdBy String
583
+ createdAt DateTime @default(now())
584
+
585
+ @@unique([orgId, sourceKind, sourceKey, rowId, tag])
586
+ @@index([orgId, sourceKind, sourceKey, tag])
587
+ @@index([orgId, sourceKind, sourceKey, rowId])
588
+ }
@@ -0,0 +1,85 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { writeFile } from 'fs/promises';
3
+ import { resolve } from 'path';
4
+
5
+ loadEnvFile();
6
+ ensurePlaceholderEnv();
7
+
8
+ main().catch((error: unknown) => {
9
+ console.error(error);
10
+ process.exit(1);
11
+ });
12
+
13
+ async function main() {
14
+ // Imported after env preparation so config validation sees the placeholders.
15
+ const { NestFactory } = await import('@nestjs/core');
16
+ const { SwaggerModule } = await import('@nestjs/swagger');
17
+ const { AppModule } = await import('../src/app.module');
18
+ const { buildOpenApiConfig } = await import('../src/config/openapi');
19
+
20
+ const app = await NestFactory.create(AppModule, {
21
+ logger: false,
22
+ abortOnError: false,
23
+ });
24
+
25
+ try {
26
+ const document = SwaggerModule.createDocument(app, buildOpenApiConfig());
27
+ const outputPath = resolve(process.cwd(), 'docs-json.json');
28
+ await writeFile(outputPath, `${JSON.stringify(document, null, 2)}\n`);
29
+ console.log(`OpenAPI document written to ${outputPath}`);
30
+ } finally {
31
+ await app.close().catch(() => undefined);
32
+ }
33
+ }
34
+
35
+ // The document is built from route metadata only; no server starts and no
36
+ // database connection is made, so missing secrets get safe placeholders.
37
+ function ensurePlaceholderEnv() {
38
+ if (!process.env.DATABASE_URL) {
39
+ process.env.DATABASE_URL =
40
+ 'postgresql://placeholder:placeholder@localhost:5432/placeholder';
41
+ }
42
+
43
+ if (!process.env.JWT_SECRET) {
44
+ process.env.JWT_SECRET = 'openapi-export-placeholder-secret-0123456789';
45
+ }
46
+ }
47
+
48
+ function loadEnvFile() {
49
+ const envPath = resolve(process.cwd(), '.env');
50
+ if (!existsSync(envPath)) {
51
+ return;
52
+ }
53
+
54
+ const lines = readFileSync(envPath, 'utf8').split(/\r?\n/);
55
+ for (const line of lines) {
56
+ const trimmed = line.trim();
57
+ if (!trimmed || trimmed.startsWith('#')) {
58
+ continue;
59
+ }
60
+
61
+ const equalsIndex = trimmed.indexOf('=');
62
+ if (equalsIndex === -1) {
63
+ continue;
64
+ }
65
+
66
+ const key = trimmed.slice(0, equalsIndex).trim();
67
+ const rawValue = trimmed.slice(equalsIndex + 1).trim();
68
+ if (!key || process.env[key] !== undefined) {
69
+ continue;
70
+ }
71
+
72
+ process.env[key] = stripQuotes(rawValue);
73
+ }
74
+ }
75
+
76
+ function stripQuotes(value: string) {
77
+ if (
78
+ (value.startsWith('"') && value.endsWith('"')) ||
79
+ (value.startsWith("'") && value.endsWith("'"))
80
+ ) {
81
+ return value.slice(1, -1);
82
+ }
83
+
84
+ return value;
85
+ }
@@ -0,0 +1,149 @@
1
+ // Scaffold a form definition + a DB-free lint spec (definitions-as-code).
2
+ //
3
+ // npm run gen:form -- customer-feedback
4
+ // npm run gen:form -- customer-feedback --title "Customer feedback"
5
+ //
6
+ // Generates:
7
+ // src/modules/forms/definitions/<key>.form.json (starter definition)
8
+ // test/<key>.form.spec.ts (meta-schema + publish lint, runs in `npm test`)
9
+ //
10
+ // Load it into an organisation with:
11
+ // npm run forms:push -- --file src/modules/forms/definitions/<key>.form.json --org <org> --user <email> --publish
12
+
13
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
14
+ import { dirname, join } from 'path';
15
+
16
+ const args = process.argv.slice(2);
17
+ const key = args[0];
18
+ if (!key || key.startsWith('--')) {
19
+ throw new Error('Usage: npm run gen:form -- <form-key> [--title "Display title"]');
20
+ }
21
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(key) || key.length > 100) {
22
+ throw new Error(
23
+ `Form key "${key}" is invalid. Use lowercase letters, digits, and hyphens (must start with a letter or digit).`,
24
+ );
25
+ }
26
+
27
+ const titleFlag = args.indexOf('--title');
28
+ const title =
29
+ titleFlag !== -1 && args[titleFlag + 1]
30
+ ? args[titleFlag + 1]
31
+ : key
32
+ .split('-')
33
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
34
+ .join(' ');
35
+
36
+ const definitionPath = join('src', 'modules', 'forms', 'definitions', `${key}.form.json`);
37
+ const specPath = join('test', `${key}.form.spec.ts`);
38
+
39
+ for (const path of [definitionPath, specPath]) {
40
+ if (existsSync(path)) {
41
+ throw new Error(`Refusing to overwrite existing file: ${path}`);
42
+ }
43
+ }
44
+
45
+ const definition = {
46
+ key,
47
+ version: 1,
48
+ title,
49
+ fields: [
50
+ {
51
+ type: 'text',
52
+ name: 'title',
53
+ label: 'Title',
54
+ validators: { required: true, maxLength: 200 },
55
+ reportable: true,
56
+ },
57
+ {
58
+ type: 'text',
59
+ name: 'notes',
60
+ label: 'Notes',
61
+ validators: { maxLength: 2000 },
62
+ },
63
+ ],
64
+ actions: {
65
+ submit: ['validateAll', 'persist'],
66
+ saveDraft: ['persistDraft'],
67
+ },
68
+ };
69
+
70
+ const spec = `import { readFileSync } from 'node:fs';
71
+ import { join } from 'node:path';
72
+ import {
73
+ ActionRegistry,
74
+ DEFAULT_ORG_FORMS_POLICY,
75
+ DEFAULT_RULE_LIMITS,
76
+ DataSourceRegistry,
77
+ FieldTypeRegistry,
78
+ lintDefinition,
79
+ lintRules,
80
+ registerBuiltinActions,
81
+ registerCoreFieldTypes,
82
+ validateDefinitionShape,
83
+ } from '@ftisindia/form-builder';
84
+ import type { FormDefinition, SubmissionStore } from '@ftisindia/form-builder';
85
+
86
+ /**
87
+ * DB-free definition lint for ${key}.form.json — fails the unit-test run the
88
+ * moment the definition would be rejected at save/publish time. If your
89
+ * definition references custom actions or data sources, register matching
90
+ * stubs below (same names/kinds as the real providers).
91
+ */
92
+ const definition = JSON.parse(
93
+ readFileSync(
94
+ join(__dirname, '..', 'src', 'modules', 'forms', 'definitions', '${key}.form.json'),
95
+ 'utf8',
96
+ ),
97
+ ) as FormDefinition;
98
+
99
+ describe('${key} form definition', () => {
100
+ const fieldTypes = new FieldTypeRegistry();
101
+ registerCoreFieldTypes(fieldTypes);
102
+
103
+ const actions = new ActionRegistry();
104
+ registerBuiltinActions(actions, {
105
+ validate: () => ({ valid: true, errors: [] }),
106
+ submissions: {} as SubmissionStore,
107
+ });
108
+ actions.register({ name: 'sendConfirmationEmail', kind: 'post-commit', execute: async () => ({}) });
109
+ actions.register({
110
+ name: 'authenticate',
111
+ kind: 'transactional',
112
+ dangerous: true,
113
+ execute: async () => ({}),
114
+ });
115
+
116
+ it('passes the engine meta-schema', () => {
117
+ expect(validateDefinitionShape(definition)).toEqual([]);
118
+ });
119
+
120
+ it('passes publish-stage linting', () => {
121
+ expect(
122
+ lintDefinition(definition, {
123
+ fieldTypes,
124
+ actions,
125
+ dataSources: new DataSourceRegistry(),
126
+ policy: { ...DEFAULT_ORG_FORMS_POLICY, allowedDangerousActions: ['authenticate'] },
127
+ ruleLimits: DEFAULT_RULE_LIMITS,
128
+ lintRules,
129
+ stage: 'publish',
130
+ canWireDangerous: true,
131
+ }),
132
+ ).toEqual([]);
133
+ });
134
+ });
135
+ `;
136
+
137
+ mkdirSync(dirname(definitionPath), { recursive: true });
138
+ writeFileSync(definitionPath, `${JSON.stringify(definition, null, 2)}\n`);
139
+ writeFileSync(specPath, spec);
140
+
141
+ console.log(`Created ${definitionPath}`);
142
+ console.log(`Created ${specPath}`);
143
+ console.log('');
144
+ console.log('Next steps:');
145
+ console.log(' 1. Edit the definition (fields, rules, actions, settings).');
146
+ console.log(' 2. npm test # lint spec runs without a database');
147
+ console.log(
148
+ ` 3. npm run forms:push -- --file ${definitionPath.replaceAll('\\', '/')} --org <org-id-or-slug> --user <owner-email> --publish`,
149
+ );