@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,24 @@
1
+ {
2
+ "key": "login",
3
+ "version": 1,
4
+ "title": "Sign in",
5
+ "settings": { "access": "public" },
6
+ "fields": [
7
+ {
8
+ "type": "email",
9
+ "name": "email",
10
+ "label": "Email address",
11
+ "validators": { "required": true }
12
+ },
13
+ {
14
+ "type": "password",
15
+ "name": "password",
16
+ "label": "Password",
17
+ "validators": { "required": true, "minLength": 8 },
18
+ "ui": { "widget": "password" }
19
+ }
20
+ ],
21
+ "actions": {
22
+ "submit": ["authenticate"]
23
+ }
24
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "key": "registration",
3
+ "version": 1,
4
+ "title": "Event registration",
5
+ "fields": [
6
+ {
7
+ "type": "text",
8
+ "name": "fullName",
9
+ "label": "Full name",
10
+ "validators": { "required": true, "maxLength": 120 },
11
+ "reportable": true
12
+ },
13
+ {
14
+ "type": "email",
15
+ "name": "email",
16
+ "label": "Email address",
17
+ "validators": { "required": true },
18
+ "reportable": true,
19
+ "indexHint": true
20
+ },
21
+ {
22
+ "type": "select",
23
+ "name": "ticketType",
24
+ "label": "Ticket type",
25
+ "validators": { "required": true, "options": ["standard", "student", "vip"] },
26
+ "reportable": true
27
+ },
28
+ {
29
+ "type": "boolean",
30
+ "name": "newsletter",
31
+ "label": "Subscribe to updates?"
32
+ },
33
+ {
34
+ "type": "hidden",
35
+ "name": "registeredBy",
36
+ "source": "context",
37
+ "path": "user.id"
38
+ }
39
+ ],
40
+ "actions": {
41
+ "submit": ["validateAll", "persist", "sendConfirmationEmail"],
42
+ "saveDraft": ["persistDraft"]
43
+ }
44
+ }
@@ -0,0 +1,226 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { DiscoveryModule } from '@nestjs/core';
3
+ import {
4
+ ActionRegistry,
5
+ DataSourceRegistry,
6
+ DataSourceResolver,
7
+ DefinitionService,
8
+ FieldTypeRegistry,
9
+ LifecycleRegistry,
10
+ OutboxHandlerRegistry,
11
+ SubmissionService,
12
+ ValidationEngine,
13
+ registerCoreFieldTypes,
14
+ } from '@ftisindia/form-builder';
15
+ import { AuthModule } from '../auth/auth.module';
16
+ import { FileGcService } from './application/services/file-gc.service';
17
+ import { FormsDefinitionsService } from './application/services/forms-definitions.service';
18
+ import { FormsExportService } from './application/services/forms-export.service';
19
+ import { FormsFilesService } from './application/services/forms-files.service';
20
+ import { FormsPublicService } from './application/services/forms-public.service';
21
+ import { FormsSettingsReader } from './application/services/forms-settings-reader.service';
22
+ import { FormsSubmissionsService } from './application/services/forms-submissions.service';
23
+ import { AuthenticateActionHandler } from './application/services/handlers/authenticate.action';
24
+ import { LoggingEmailHandler } from './application/services/handlers/logging-email.handler';
25
+ import { SendConfirmationEmailAction } from './application/services/handlers/send-confirmation-email.action';
26
+ import { WebhookOutboxHandler } from './application/services/handlers/webhook.handler';
27
+ import { OutboxDispatcherService } from './application/services/outbox-dispatcher.service';
28
+ import { CaslFormsAuthorization } from './infrastructure/casl-forms-authorization';
29
+ import { PrismaFormsAuditSink } from './infrastructure/audit-sink.adapter';
30
+ import { PrismaTxRunner } from './infrastructure/prisma-tx-runner';
31
+ import { RequestFormsContext } from './infrastructure/request-forms-context';
32
+ import { RegistryBootstrapService } from './infrastructure/registry/registry-bootstrap.service';
33
+ import { FormsSchemaCheckService } from './infrastructure/schema-check/forms-schema-check.service';
34
+ import { LocalDiskStorageAdapter } from './infrastructure/storage/local-disk-storage.adapter';
35
+ import { PrismaActionLogStore } from './infrastructure/stores/prisma-action-log.store';
36
+ import { PrismaFileStore } from './infrastructure/stores/prisma-file.store';
37
+ import { PrismaFormDefinitionStore } from './infrastructure/stores/prisma-form-definition.store';
38
+ import { PrismaOutboxStore } from './infrastructure/stores/prisma-outbox.store';
39
+ import { PrismaSubmissionStore } from './infrastructure/stores/prisma-submission.store';
40
+ import { FormsDataSourcesController } from './presentation/forms-data-sources.controller';
41
+ import { FormsDefinitionsController } from './presentation/forms-definitions.controller';
42
+ import { FormsFilesController } from './presentation/forms-files.controller';
43
+ import { FormsSubmissionsController } from './presentation/forms-submissions.controller';
44
+ import { FormsUploadInterceptor } from './presentation/forms-upload.interceptor';
45
+ import { PublicFormsController } from './presentation/public-forms.controller';
46
+
47
+ /**
48
+ * The forms glue module — the ONLY place that binds the framework-free
49
+ * @ftisindia/form-builder engine to this app's services (request context,
50
+ * CASL RBAC, AuditService, Prisma). Engine services are composed here via
51
+ * factories over the four adapters and five stores; app extensions register
52
+ * through the @FormFieldType/@FormActionHandler/@FormDataSource decorators.
53
+ */
54
+ @Module({
55
+ imports: [AuthModule, DiscoveryModule],
56
+ controllers: [
57
+ // Listed before the definitions controller so the literal 'data-sources'
58
+ // segment wins over the ':formKey' parameter on the shared base path.
59
+ FormsDataSourcesController,
60
+ FormsDefinitionsController,
61
+ FormsFilesController,
62
+ FormsSubmissionsController,
63
+ PublicFormsController,
64
+ ],
65
+ providers: [
66
+ // Adapters over the ecosystem seams.
67
+ RequestFormsContext,
68
+ CaslFormsAuthorization,
69
+ PrismaFormsAuditSink,
70
+ PrismaTxRunner,
71
+ // Prisma store implementations of the engine ports.
72
+ PrismaFormDefinitionStore,
73
+ PrismaSubmissionStore,
74
+ PrismaOutboxStore,
75
+ PrismaFileStore,
76
+ PrismaActionLogStore,
77
+ FormsSettingsReader,
78
+ // Engine registries — singletons; core field types registered up front.
79
+ {
80
+ provide: FieldTypeRegistry,
81
+ useFactory: () => {
82
+ const registry = new FieldTypeRegistry();
83
+ registerCoreFieldTypes(registry);
84
+ return registry;
85
+ },
86
+ },
87
+ { provide: ActionRegistry, useFactory: () => new ActionRegistry() },
88
+ { provide: DataSourceRegistry, useFactory: () => new DataSourceRegistry() },
89
+ { provide: LifecycleRegistry, useFactory: () => new LifecycleRegistry() },
90
+ { provide: OutboxHandlerRegistry, useFactory: () => new OutboxHandlerRegistry() },
91
+ {
92
+ provide: ValidationEngine,
93
+ useFactory: (fieldTypes: FieldTypeRegistry) => new ValidationEngine(fieldTypes),
94
+ inject: [FieldTypeRegistry],
95
+ },
96
+ {
97
+ provide: DataSourceResolver,
98
+ useFactory: (dataSources: DataSourceRegistry) => new DataSourceResolver(dataSources),
99
+ inject: [DataSourceRegistry],
100
+ },
101
+ // Engine orchestration services, composed over the adapters and stores.
102
+ {
103
+ provide: DefinitionService,
104
+ useFactory: (
105
+ store: PrismaFormDefinitionStore,
106
+ authz: CaslFormsAuthorization,
107
+ audit: PrismaFormsAuditSink,
108
+ txRunner: PrismaTxRunner,
109
+ fieldTypes: FieldTypeRegistry,
110
+ actions: ActionRegistry,
111
+ dataSources: DataSourceRegistry,
112
+ resolver: DataSourceResolver,
113
+ settings: FormsSettingsReader,
114
+ ) =>
115
+ new DefinitionService({
116
+ store,
117
+ authz,
118
+ audit,
119
+ txRunner,
120
+ fieldTypes,
121
+ actions,
122
+ dataSources,
123
+ resolver,
124
+ policyFor: (orgId) => settings.policyFor(orgId),
125
+ }),
126
+ inject: [
127
+ PrismaFormDefinitionStore,
128
+ CaslFormsAuthorization,
129
+ PrismaFormsAuditSink,
130
+ PrismaTxRunner,
131
+ FieldTypeRegistry,
132
+ ActionRegistry,
133
+ DataSourceRegistry,
134
+ DataSourceResolver,
135
+ FormsSettingsReader,
136
+ ],
137
+ },
138
+ {
139
+ provide: SubmissionService,
140
+ useFactory: (
141
+ definitions: PrismaFormDefinitionStore,
142
+ submissions: PrismaSubmissionStore,
143
+ files: PrismaFileStore,
144
+ outbox: PrismaOutboxStore,
145
+ actionLog: PrismaActionLogStore,
146
+ audit: PrismaFormsAuditSink,
147
+ txRunner: PrismaTxRunner,
148
+ authz: CaslFormsAuthorization,
149
+ fieldTypes: FieldTypeRegistry,
150
+ actions: ActionRegistry,
151
+ lifecycles: LifecycleRegistry,
152
+ validation: ValidationEngine,
153
+ resolver: DataSourceResolver,
154
+ settings: FormsSettingsReader,
155
+ ) =>
156
+ new SubmissionService({
157
+ definitions,
158
+ submissions,
159
+ files,
160
+ outbox,
161
+ actionLog,
162
+ audit,
163
+ txRunner,
164
+ authz,
165
+ fieldTypes,
166
+ actions,
167
+ lifecycles,
168
+ validation,
169
+ resolver,
170
+ captcha: settings.captchaVerifier,
171
+ policyFor: (orgId) => settings.policyFor(orgId),
172
+ }),
173
+ inject: [
174
+ PrismaFormDefinitionStore,
175
+ PrismaSubmissionStore,
176
+ PrismaFileStore,
177
+ PrismaOutboxStore,
178
+ PrismaActionLogStore,
179
+ PrismaFormsAuditSink,
180
+ PrismaTxRunner,
181
+ CaslFormsAuthorization,
182
+ FieldTypeRegistry,
183
+ ActionRegistry,
184
+ LifecycleRegistry,
185
+ ValidationEngine,
186
+ DataSourceResolver,
187
+ FormsSettingsReader,
188
+ ],
189
+ },
190
+ // Startup wiring + background work.
191
+ RegistryBootstrapService,
192
+ FormsSchemaCheckService,
193
+ OutboxDispatcherService,
194
+ LocalDiskStorageAdapter,
195
+ FileGcService,
196
+ FormsUploadInterceptor,
197
+ // Default outbox handlers + shipped form actions.
198
+ LoggingEmailHandler,
199
+ WebhookOutboxHandler,
200
+ AuthenticateActionHandler,
201
+ SendConfirmationEmailAction,
202
+ // Thin HTTP-facing services.
203
+ FormsDefinitionsService,
204
+ FormsSubmissionsService,
205
+ FormsFilesService,
206
+ FormsPublicService,
207
+ FormsExportService,
208
+ ],
209
+ exports: [
210
+ FieldTypeRegistry,
211
+ ActionRegistry,
212
+ DataSourceRegistry,
213
+ LifecycleRegistry,
214
+ OutboxHandlerRegistry,
215
+ OutboxDispatcherService,
216
+ // Consumed ONLY by the optional ReportsFormsModule bridge (report design
217
+ // §3.2/§13): the form definition store backs the form→report source
218
+ // adapter, and SubmissionService + the forms request context let the
219
+ // delegated editSubmission/updateStatus grid verbs run the form engine's
220
+ // own validation pipeline. ReportsModule itself imports none of this.
221
+ PrismaFormDefinitionStore,
222
+ SubmissionService,
223
+ RequestFormsContext,
224
+ ],
225
+ })
226
+ export class FormsModule {}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Optional-provider tokens for the forms module's seams. Bind an
3
+ * implementation in your AppModule to activate the seam; absence fails
4
+ * closed (e.g. captcha-enabled definitions refuse to publish).
5
+ */
6
+ export const FORMS_CAPTCHA_VERIFIER = 'FORMS_CAPTCHA_VERIFIER';
@@ -0,0 +1,30 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { Prisma } from '@prisma/client';
3
+ import type { AuditEntry, EngineTx, FormsAuditSink } from '@ftisindia/form-builder';
4
+ import { PrismaService } from '../../../database/prisma/prisma.service';
5
+ import { AuditService } from '../../audit/application/services/audit.service';
6
+
7
+ /**
8
+ * Routes engine audit entries through the template's AuditService. When the
9
+ * engine supplies a transaction, the audit row is written through it so it
10
+ * commits atomically with the change it describes (ecosystem guide §7).
11
+ */
12
+ @Injectable()
13
+ export class PrismaFormsAuditSink implements FormsAuditSink {
14
+ constructor(
15
+ private readonly audit: AuditService,
16
+ private readonly prisma: PrismaService,
17
+ ) {}
18
+
19
+ async write(entry: AuditEntry, tx?: EngineTx): Promise<void> {
20
+ const client = tx ? (tx as unknown as Prisma.TransactionClient) : this.prisma;
21
+ await this.audit.write(client, {
22
+ orgId: entry.orgId ?? null,
23
+ actorUserId: entry.actorUserId ?? null,
24
+ action: entry.action,
25
+ targetType: entry.targetType,
26
+ targetId: entry.targetId ?? null,
27
+ metadata: entry.metadata as Prisma.InputJsonValue | undefined,
28
+ });
29
+ }
30
+ }
@@ -0,0 +1,31 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import type { FormPermissionKey, FormsAuthorization } from '@ftisindia/form-builder';
3
+ import { AbilityFactory } from '../../access-control/application/services/ability.factory';
4
+ import { permissionKeyToRule } from '../../access-control/types/permission-key';
5
+ import { RequestContextService } from '../../request-context/application/services/request-context.service';
6
+
7
+ /**
8
+ * The engine's internal authorization seam over CASL — mirrors
9
+ * PermissionGuard's own logic and fails closed when no RBAC context exists
10
+ * (anonymous/public requests). Route guards remain the first enforcement
11
+ * layer; this is the defense-in-depth second layer (ecosystem guide §5.3).
12
+ */
13
+ @Injectable()
14
+ export class CaslFormsAuthorization implements FormsAuthorization {
15
+ constructor(
16
+ private readonly abilities: AbilityFactory,
17
+ private readonly ctx: RequestContextService,
18
+ ) {}
19
+
20
+ can(required: FormPermissionKey[]): boolean {
21
+ const rbac = this.ctx.getRbacContext();
22
+ if (!rbac) {
23
+ return false;
24
+ }
25
+ const ability = this.abilities.createForContext(rbac);
26
+ return required.every((key) => {
27
+ const { action, subject } = permissionKeyToRule(key);
28
+ return ability.can(action, subject);
29
+ });
30
+ }
31
+ }
@@ -0,0 +1,17 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import type { EngineTx, TxRunner } from '@ftisindia/form-builder';
3
+ import { PrismaService } from '../../../database/prisma/prisma.service';
4
+
5
+ /**
6
+ * The engine's transaction seam over PrismaService.$transaction. The opaque
7
+ * EngineTx brand is a Prisma.TransactionClient underneath; the stores cast it
8
+ * back. One TxRunner.run call IS the design doc's Phase-2 atomicity boundary.
9
+ */
10
+ @Injectable()
11
+ export class PrismaTxRunner implements TxRunner {
12
+ constructor(private readonly prisma: PrismaService) {}
13
+
14
+ run<T>(fn: (tx: EngineTx) => Promise<T>): Promise<T> {
15
+ return this.prisma.$transaction((tx) => fn(tx as unknown as EngineTx));
16
+ }
17
+ }
@@ -0,0 +1,17 @@
1
+ import { SetMetadata } from '@nestjs/common';
2
+
3
+ export const FORM_FIELD_TYPE_METADATA = 'ftis:forms:field-type';
4
+ export const FORM_ACTION_METADATA = 'ftis:forms:action';
5
+ export const FORM_DATA_SOURCE_METADATA = 'ftis:forms:data-source';
6
+ export const FORM_LIFECYCLE_METADATA = 'ftis:forms:lifecycle';
7
+
8
+ /**
9
+ * The standardized extension story: implement the engine interface
10
+ * (FieldTypeDef / FormAction / DataSourceDef / FormLifecycle), decorate the
11
+ * @Injectable class, list it as a provider — RegistryBootstrapService
12
+ * discovers and registers it at startup. Same three steps for every kind.
13
+ */
14
+ export const FormFieldType = () => SetMetadata(FORM_FIELD_TYPE_METADATA, true);
15
+ export const FormActionHandler = () => SetMetadata(FORM_ACTION_METADATA, true);
16
+ export const FormDataSource = () => SetMetadata(FORM_DATA_SOURCE_METADATA, true);
17
+ export const FormLifecycleHook = () => SetMetadata(FORM_LIFECYCLE_METADATA, true);
@@ -0,0 +1,82 @@
1
+ import { Injectable, OnModuleInit } from '@nestjs/common';
2
+ import { DiscoveryService, Reflector } from '@nestjs/core';
3
+ import {
4
+ ActionRegistry,
5
+ DataSourceRegistry,
6
+ FieldTypeRegistry,
7
+ LifecycleRegistry,
8
+ OutboxHandlerRegistry,
9
+ ValidationEngine,
10
+ registerBuiltinActions,
11
+ } from '@ftisindia/form-builder';
12
+ import type {
13
+ DataSourceDef,
14
+ FieldTypeDef,
15
+ FormAction,
16
+ FormLifecycle,
17
+ } from '@ftisindia/form-builder';
18
+ import { PrismaSubmissionStore } from '../stores/prisma-submission.store';
19
+ import { LoggingEmailHandler } from '../../application/services/handlers/logging-email.handler';
20
+ import { WebhookOutboxHandler } from '../../application/services/handlers/webhook.handler';
21
+ import {
22
+ FORM_ACTION_METADATA,
23
+ FORM_DATA_SOURCE_METADATA,
24
+ FORM_FIELD_TYPE_METADATA,
25
+ FORM_LIFECYCLE_METADATA,
26
+ } from './form-extension.decorators';
27
+
28
+ /**
29
+ * Populates the engine registries at startup: builtin actions first
30
+ * (validateAll/persist/persistDraft/lockEditing), the default outbox
31
+ * handlers, then every provider decorated with @FormFieldType /
32
+ * @FormActionHandler / @FormDataSource / @FormLifecycleHook — the app
33
+ * declares its extensions as ordinary providers; the engine finds them.
34
+ */
35
+ @Injectable()
36
+ export class RegistryBootstrapService implements OnModuleInit {
37
+ constructor(
38
+ private readonly discovery: DiscoveryService,
39
+ private readonly reflector: Reflector,
40
+ private readonly fieldTypes: FieldTypeRegistry,
41
+ private readonly actions: ActionRegistry,
42
+ private readonly dataSources: DataSourceRegistry,
43
+ private readonly lifecycles: LifecycleRegistry,
44
+ private readonly outboxHandlers: OutboxHandlerRegistry,
45
+ private readonly validation: ValidationEngine,
46
+ private readonly submissions: PrismaSubmissionStore,
47
+ private readonly emailHandler: LoggingEmailHandler,
48
+ private readonly webhookHandler: WebhookOutboxHandler,
49
+ ) {}
50
+
51
+ onModuleInit(): void {
52
+ registerBuiltinActions(this.actions, {
53
+ validate: (def, data, mode) => this.validation.validate(def, data, mode),
54
+ submissions: this.submissions,
55
+ });
56
+ this.outboxHandlers.register(this.emailHandler);
57
+ this.outboxHandlers.register(this.webhookHandler);
58
+
59
+ for (const wrapper of this.discovery.getProviders()) {
60
+ const instance: unknown = wrapper.instance;
61
+ if (!instance || typeof instance !== 'object') {
62
+ continue;
63
+ }
64
+ const ctor = instance.constructor as object | undefined;
65
+ if (!ctor || ctor === Object) {
66
+ continue;
67
+ }
68
+ if (this.reflector.get<boolean>(FORM_FIELD_TYPE_METADATA, ctor as never)) {
69
+ this.fieldTypes.register(instance as FieldTypeDef);
70
+ }
71
+ if (this.reflector.get<boolean>(FORM_ACTION_METADATA, ctor as never)) {
72
+ this.actions.register(instance as FormAction);
73
+ }
74
+ if (this.reflector.get<boolean>(FORM_DATA_SOURCE_METADATA, ctor as never)) {
75
+ this.dataSources.register(instance as DataSourceDef);
76
+ }
77
+ if (this.reflector.get<boolean>(FORM_LIFECYCLE_METADATA, ctor as never)) {
78
+ this.lifecycles.register(instance as FormLifecycle);
79
+ }
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,60 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import type { ContextPath, FormsContext } from '@ftisindia/form-builder';
3
+ import { RequestContextService } from '../../request-context/application/services/request-context.service';
4
+
5
+ /**
6
+ * Binds the engine's FormsContext seam to the app's AsyncLocalStorage request
7
+ * context. The closed ContextPath union IS the design doc's anti-tenant-leak
8
+ * rule for hidden fields, enforced by the type system (ecosystem guide §3).
9
+ */
10
+ @Injectable()
11
+ export class RequestFormsContext implements FormsContext {
12
+ constructor(private readonly ctx: RequestContextService) {}
13
+
14
+ requestId() {
15
+ return this.ctx.getRequestId();
16
+ }
17
+
18
+ source(): 'http' | 'worker' {
19
+ return this.ctx.get()?.source ?? 'worker';
20
+ }
21
+
22
+ userId() {
23
+ return this.ctx.getUserId();
24
+ }
25
+
26
+ orgId() {
27
+ return this.ctx.getOrgId();
28
+ }
29
+
30
+ permissionKeys() {
31
+ return this.ctx.getPermissions();
32
+ }
33
+
34
+ isOwner() {
35
+ return this.ctx.getRbacContext()?.isOwner ?? false;
36
+ }
37
+
38
+ assertOrgScope(orgId: string) {
39
+ this.ctx.assertOrgScope(orgId);
40
+ }
41
+
42
+ resolve(path: ContextPath) {
43
+ switch (path) {
44
+ case 'user.id':
45
+ return this.ctx.getUserId();
46
+ case 'org.id':
47
+ return this.ctx.getOrgId();
48
+ case 'request.id':
49
+ return this.ctx.getRequestId();
50
+ }
51
+ }
52
+
53
+ ipAddress() {
54
+ return this.ctx.getIpAddress();
55
+ }
56
+
57
+ userAgent() {
58
+ return this.ctx.getUserAgent();
59
+ }
60
+ }
@@ -0,0 +1,76 @@
1
+ import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { Prisma } from '@prisma/client';
4
+ import {
5
+ ENGINE_SCHEMA_VERSION,
6
+ EXPECTED_FORMS_SCHEMA,
7
+ FORMS_PRISMA_SNIPPET_PATH,
8
+ } from '@ftisindia/form-builder';
9
+ import { PrismaService } from '../../../../database/prisma/prisma.service';
10
+
11
+ /**
12
+ * Boot-time schema compatibility check (ecosystem guide §10.1): the app owns
13
+ * schema.prisma and migrations; the engine ships the canonical snippet and a
14
+ * tables/columns manifest. On mismatch the app fails fast at startup with an
15
+ * actionable message instead of crashing mid-request with a cryptic Prisma
16
+ * error. Escape hatch: FORMS_SCHEMA_CHECK=off.
17
+ */
18
+ @Injectable()
19
+ export class FormsSchemaCheckService implements OnApplicationBootstrap {
20
+ private readonly logger = new Logger('FormsSchemaCheck');
21
+
22
+ constructor(
23
+ private readonly prisma: PrismaService,
24
+ private readonly config: ConfigService,
25
+ ) {}
26
+
27
+ async onApplicationBootstrap(): Promise<void> {
28
+ if (this.config.get<string>('forms.schemaCheck') === 'off') {
29
+ this.logger.warn('Forms schema check is disabled (FORMS_SCHEMA_CHECK=off).');
30
+ return;
31
+ }
32
+ await this.check();
33
+ this.logger.log(`Forms schema check passed (engine schema version ${ENGINE_SCHEMA_VERSION}).`);
34
+ }
35
+
36
+ async check(): Promise<void> {
37
+ const tables = EXPECTED_FORMS_SCHEMA.map((expected) => expected.table);
38
+ const rows = await this.prisma.$queryRaw<Array<{ table_name: string; column_name: string }>>`
39
+ SELECT table_name, column_name
40
+ FROM information_schema.columns
41
+ WHERE table_schema = current_schema()
42
+ AND table_name IN (${Prisma.join(tables)})
43
+ `;
44
+
45
+ const present = new Map<string, Set<string>>();
46
+ for (const row of rows) {
47
+ const columns = present.get(row.table_name) ?? new Set<string>();
48
+ columns.add(row.column_name);
49
+ present.set(row.table_name, columns);
50
+ }
51
+
52
+ const problems: string[] = [];
53
+ for (const expected of EXPECTED_FORMS_SCHEMA) {
54
+ const columns = present.get(expected.table);
55
+ if (!columns) {
56
+ problems.push(`missing table "${expected.table}"`);
57
+ continue;
58
+ }
59
+ const missingColumns = expected.columns.filter((column) => !columns.has(column));
60
+ if (missingColumns.length > 0) {
61
+ problems.push(`table "${expected.table}" is missing column(s): ${missingColumns.join(', ')}`);
62
+ }
63
+ }
64
+
65
+ if (problems.length > 0) {
66
+ throw new Error(
67
+ [
68
+ `The database does not match @ftisindia/form-builder engine schema version ${ENGINE_SCHEMA_VERSION}:`,
69
+ ...problems.map((problem) => ` - ${problem}`),
70
+ `Fix: copy the canonical models from "${FORMS_PRISMA_SNIPPET_PATH}" (in node_modules) into prisma/schema.prisma and run "npx prisma migrate dev".`,
71
+ 'To bypass temporarily (NOT recommended), set FORMS_SCHEMA_CHECK=off.',
72
+ ].join('\n'),
73
+ );
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,43 @@
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { dirname, resolve, sep } from 'node:path';
3
+ import { Injectable } from '@nestjs/common';
4
+ import { ConfigService } from '@nestjs/config';
5
+ import type { FileStoragePort } from '@ftisindia/form-builder';
6
+
7
+ /**
8
+ * Default byte storage under FORMS_FILE_STORAGE_DIR. Storage keys are always
9
+ * server-generated (orgId/uuid — never client-controlled names), and the
10
+ * resolved path is verified to stay inside the base directory as defense in
11
+ * depth against traversal. Swap this provider for an S3/GCS adapter without
12
+ * touching the engine.
13
+ */
14
+ @Injectable()
15
+ export class LocalDiskStorageAdapter implements FileStoragePort {
16
+ private readonly baseDir: string;
17
+
18
+ constructor(config: ConfigService) {
19
+ this.baseDir = resolve(config.get<string>('forms.storageDir') ?? './var/uploads');
20
+ }
21
+
22
+ async put(storageKey: string, bytes: Uint8Array): Promise<void> {
23
+ const path = this.fullPath(storageKey);
24
+ await mkdir(dirname(path), { recursive: true });
25
+ await writeFile(path, bytes);
26
+ }
27
+
28
+ async get(storageKey: string): Promise<Uint8Array> {
29
+ return readFile(this.fullPath(storageKey));
30
+ }
31
+
32
+ async delete(storageKey: string): Promise<void> {
33
+ await rm(this.fullPath(storageKey), { force: true });
34
+ }
35
+
36
+ private fullPath(storageKey: string): string {
37
+ const path = resolve(this.baseDir, storageKey);
38
+ if (path !== this.baseDir && !path.startsWith(this.baseDir + sep)) {
39
+ throw new Error('Storage key resolves outside the configured storage directory.');
40
+ }
41
+ return path;
42
+ }
43
+ }
@@ -0,0 +1,5 @@
1
+ export * from './prisma-action-log.store';
2
+ export * from './prisma-file.store';
3
+ export * from './prisma-form-definition.store';
4
+ export * from './prisma-outbox.store';
5
+ export * from './prisma-submission.store';