@ftisindia/create-app 0.1.5 → 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 (162) hide show
  1. package/package.json +1 -1
  2. package/template/.env.example +25 -0
  3. package/template/README.md +51 -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/env.validation.ts +25 -0
  21. package/template/src/config/forms.config.ts +12 -0
  22. package/template/src/config/index.ts +2 -0
  23. package/template/src/config/openapi.ts +12 -0
  24. package/template/src/config/reports-secret.ts +15 -0
  25. package/template/src/config/reports.config.ts +16 -0
  26. package/template/src/main.ts +3 -12
  27. package/template/src/modules/access-control/dto/access-control-response.dto.ts +3 -0
  28. package/template/src/modules/access-control/dto/current-access-control-response.dto.ts +5 -1
  29. package/template/src/modules/access-control/types/permission-key.ts +27 -0
  30. package/template/src/modules/access-control/types/route-permission-registry.ts +183 -0
  31. package/template/src/modules/audit/dto/audit-response.dto.ts +7 -3
  32. package/template/src/modules/auth/auth.module.ts +3 -1
  33. package/template/src/modules/auth/dto/auth-response.dto.ts +1 -1
  34. package/template/src/modules/forms/application/services/file-gc.service.ts +85 -0
  35. package/template/src/modules/forms/application/services/forms-definitions.service.ts +137 -0
  36. package/template/src/modules/forms/application/services/forms-error.mapper.ts +64 -0
  37. package/template/src/modules/forms/application/services/forms-export.service.ts +210 -0
  38. package/template/src/modules/forms/application/services/forms-files.service.ts +164 -0
  39. package/template/src/modules/forms/application/services/forms-public.service.ts +49 -0
  40. package/template/src/modules/forms/application/services/forms-settings-reader.service.ts +53 -0
  41. package/template/src/modules/forms/application/services/forms-submissions.service.ts +103 -0
  42. package/template/src/modules/forms/application/services/handlers/authenticate.action.ts +37 -0
  43. package/template/src/modules/forms/application/services/handlers/logging-email.handler.ts +22 -0
  44. package/template/src/modules/forms/application/services/handlers/send-confirmation-email.action.ts +40 -0
  45. package/template/src/modules/forms/application/services/handlers/webhook.handler.ts +41 -0
  46. package/template/src/modules/forms/application/services/outbox-dispatcher.service.ts +109 -0
  47. package/template/src/modules/forms/dto/create-form-definition.dto.ts +12 -0
  48. package/template/src/modules/forms/dto/data-source-response.dto.ts +19 -0
  49. package/template/src/modules/forms/dto/export-submissions-query.dto.ts +33 -0
  50. package/template/src/modules/forms/dto/file-upload-response.dto.ts +24 -0
  51. package/template/src/modules/forms/dto/form-definition-response.dto.ts +50 -0
  52. package/template/src/modules/forms/dto/form-render-response.dto.ts +17 -0
  53. package/template/src/modules/forms/dto/list-form-definitions-query.dto.ts +10 -0
  54. package/template/src/modules/forms/dto/list-submissions-query.dto.ts +10 -0
  55. package/template/src/modules/forms/dto/public-submit-form.dto.ts +24 -0
  56. package/template/src/modules/forms/dto/set-public-access.dto.ts +8 -0
  57. package/template/src/modules/forms/dto/submission-response.dto.ts +99 -0
  58. package/template/src/modules/forms/dto/submit-form.dto.ts +50 -0
  59. package/template/src/modules/forms/dto/update-form-definition.dto.ts +12 -0
  60. package/template/src/modules/forms/dto/upload-file-query.dto.ts +33 -0
  61. package/template/src/modules/forms/dto/validate-submission.dto.ts +22 -0
  62. package/template/src/modules/forms/examples/abstract-submission.form.json +80 -0
  63. package/template/src/modules/forms/examples/login.form.json +24 -0
  64. package/template/src/modules/forms/examples/registration.form.json +44 -0
  65. package/template/src/modules/forms/forms.module.ts +226 -0
  66. package/template/src/modules/forms/forms.tokens.ts +6 -0
  67. package/template/src/modules/forms/infrastructure/audit-sink.adapter.ts +30 -0
  68. package/template/src/modules/forms/infrastructure/casl-forms-authorization.ts +31 -0
  69. package/template/src/modules/forms/infrastructure/prisma-tx-runner.ts +17 -0
  70. package/template/src/modules/forms/infrastructure/registry/form-extension.decorators.ts +17 -0
  71. package/template/src/modules/forms/infrastructure/registry/registry-bootstrap.service.ts +82 -0
  72. package/template/src/modules/forms/infrastructure/request-forms-context.ts +60 -0
  73. package/template/src/modules/forms/infrastructure/schema-check/forms-schema-check.service.ts +76 -0
  74. package/template/src/modules/forms/infrastructure/storage/local-disk-storage.adapter.ts +43 -0
  75. package/template/src/modules/forms/infrastructure/stores/index.ts +5 -0
  76. package/template/src/modules/forms/infrastructure/stores/prisma-action-log.store.ts +37 -0
  77. package/template/src/modules/forms/infrastructure/stores/prisma-file.store.ts +108 -0
  78. package/template/src/modules/forms/infrastructure/stores/prisma-form-definition.store.ts +147 -0
  79. package/template/src/modules/forms/infrastructure/stores/prisma-outbox.store.ts +133 -0
  80. package/template/src/modules/forms/infrastructure/stores/prisma-submission.store.ts +164 -0
  81. package/template/src/modules/forms/presentation/forms-data-sources.controller.ts +58 -0
  82. package/template/src/modules/forms/presentation/forms-definitions.controller.ts +191 -0
  83. package/template/src/modules/forms/presentation/forms-files.controller.ts +79 -0
  84. package/template/src/modules/forms/presentation/forms-submissions.controller.ts +154 -0
  85. package/template/src/modules/forms/presentation/forms-upload.interceptor.ts +33 -0
  86. package/template/src/modules/forms/presentation/public-forms.controller.ts +51 -0
  87. package/template/src/modules/invitations/dto/invitation-response.dto.ts +4 -0
  88. package/template/src/modules/organisations/dto/organisation-response.dto.ts +1 -0
  89. package/template/src/modules/reports/application/services/reports-actions.service.ts +54 -0
  90. package/template/src/modules/reports/application/services/reports-definitions.service.ts +66 -0
  91. package/template/src/modules/reports/application/services/reports-error.mapper.ts +97 -0
  92. package/template/src/modules/reports/application/services/reports-export-dispatcher.service.ts +124 -0
  93. package/template/src/modules/reports/application/services/reports-exports.service.ts +74 -0
  94. package/template/src/modules/reports/application/services/reports-queries.service.ts +35 -0
  95. package/template/src/modules/reports/application/services/reports-settings-reader.service.ts +49 -0
  96. package/template/src/modules/reports/application/services/reports-views.service.ts +79 -0
  97. package/template/src/modules/reports/dto/action-result-response.dto.ts +21 -0
  98. package/template/src/modules/reports/dto/create-report-definition.dto.ts +86 -0
  99. package/template/src/modules/reports/dto/create-saved-view.dto.ts +26 -0
  100. package/template/src/modules/reports/dto/execute-action.dto.ts +71 -0
  101. package/template/src/modules/reports/dto/export-job-response.dto.ts +60 -0
  102. package/template/src/modules/reports/dto/export-request.dto.ts +34 -0
  103. package/template/src/modules/reports/dto/list-reports-query.dto.ts +10 -0
  104. package/template/src/modules/reports/dto/list-views-query.dto.ts +17 -0
  105. package/template/src/modules/reports/dto/prepare-action-response.dto.ts +14 -0
  106. package/template/src/modules/reports/dto/prepare-action.dto.ts +27 -0
  107. package/template/src/modules/reports/dto/query-response.dto.ts +64 -0
  108. package/template/src/modules/reports/dto/query-spec.dto.ts +120 -0
  109. package/template/src/modules/reports/dto/report-definition-response.dto.ts +64 -0
  110. package/template/src/modules/reports/dto/report-meta-query.dto.ts +16 -0
  111. package/template/src/modules/reports/dto/report-meta-response.dto.ts +113 -0
  112. package/template/src/modules/reports/dto/saved-view-response.dto.ts +66 -0
  113. package/template/src/modules/reports/dto/update-report-definition.dto.ts +9 -0
  114. package/template/src/modules/reports/dto/update-saved-view.dto.ts +27 -0
  115. package/template/src/modules/reports/examples/abstract-review-board.report.json +54 -0
  116. package/template/src/modules/reports/examples/org-members.report.json +55 -0
  117. package/template/src/modules/reports/infrastructure/audit-sink.adapter.ts +31 -0
  118. package/template/src/modules/reports/infrastructure/casl-reports-authorization.ts +39 -0
  119. package/template/src/modules/reports/infrastructure/forms-adapter/form-report-source.adapter.ts +292 -0
  120. package/template/src/modules/reports/infrastructure/forms-adapter/form-row-actions.ts +171 -0
  121. package/template/src/modules/reports/infrastructure/forms-adapter/forms-bridge-bootstrap.service.ts +32 -0
  122. package/template/src/modules/reports/infrastructure/prisma-catalog.adapter.ts +95 -0
  123. package/template/src/modules/reports/infrastructure/prisma-query-executor.ts +103 -0
  124. package/template/src/modules/reports/infrastructure/prisma-snapshot-runner.ts +47 -0
  125. package/template/src/modules/reports/infrastructure/prisma-tx-runner.ts +18 -0
  126. package/template/src/modules/reports/infrastructure/registry/registry-bootstrap.service.ts +61 -0
  127. package/template/src/modules/reports/infrastructure/registry/report-extension.decorators.ts +14 -0
  128. package/template/src/modules/reports/infrastructure/reports-job-queue.adapter.ts +28 -0
  129. package/template/src/modules/reports/infrastructure/request-reports-context.ts +42 -0
  130. package/template/src/modules/reports/infrastructure/schema-check/reports-schema-check.service.ts +116 -0
  131. package/template/src/modules/reports/infrastructure/storage/local-disk-export-storage.adapter.ts +79 -0
  132. package/template/src/modules/reports/infrastructure/stores/index.ts +5 -0
  133. package/template/src/modules/reports/infrastructure/stores/prisma-bulk-action-run.store.ts +89 -0
  134. package/template/src/modules/reports/infrastructure/stores/prisma-export-job.store.ts +93 -0
  135. package/template/src/modules/reports/infrastructure/stores/prisma-report-definition.store.ts +171 -0
  136. package/template/src/modules/reports/infrastructure/stores/prisma-row-tag.store.ts +110 -0
  137. package/template/src/modules/reports/infrastructure/stores/prisma-saved-view.store.ts +144 -0
  138. package/template/src/modules/reports/presentation/reports-actions.controller.ts +83 -0
  139. package/template/src/modules/reports/presentation/reports-definitions.controller.ts +156 -0
  140. package/template/src/modules/reports/presentation/reports-export-jobs.controller.ts +61 -0
  141. package/template/src/modules/reports/presentation/reports-export.controller.ts +76 -0
  142. package/template/src/modules/reports/presentation/reports-query.controller.ts +52 -0
  143. package/template/src/modules/reports/presentation/reports-views.controller.ts +140 -0
  144. package/template/src/modules/reports/reports-forms.module.ts +33 -0
  145. package/template/src/modules/reports/reports.module.ts +335 -0
  146. package/template/src/modules/reports/reports.tokens.ts +11 -0
  147. package/template/src/modules/reports/sources/org-members.source.ts +112 -0
  148. package/template/src/modules/settings/types/setting-definitions.ts +94 -0
  149. package/template/test/forms-definitions.e2e-spec.ts +394 -0
  150. package/template/test/forms-export.e2e-spec.ts +390 -0
  151. package/template/test/forms-files.e2e-spec.ts +345 -0
  152. package/template/test/forms-outbox.e2e-spec.ts +309 -0
  153. package/template/test/forms-permission-sync.spec.ts +27 -0
  154. package/template/test/forms-public.e2e-spec.ts +269 -0
  155. package/template/test/forms-schema-check.e2e-spec.ts +65 -0
  156. package/template/test/forms-submissions.e2e-spec.ts +500 -0
  157. package/template/test/forms-webhooks.e2e-spec.ts +261 -0
  158. package/template/test/reports-advanced.e2e-spec.ts +368 -0
  159. package/template/test/reports-permission-sync.spec.ts +30 -0
  160. package/template/test/reports-query.e2e-spec.ts +350 -0
  161. package/template/test/reports-tiers.e2e-spec.ts +257 -0
  162. package/template/test/route-registry.validator.spec.ts +22 -0
@@ -0,0 +1,335 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { DiscoveryModule } from '@nestjs/core';
4
+ import {
5
+ createInMemoryResultCache,
6
+ ReportActionService,
7
+ ReportDefinitionService,
8
+ ReportExportService,
9
+ ReportQueryService,
10
+ ReportRowActionRegistry,
11
+ ReportSourceRegistry,
12
+ ReportTagService,
13
+ ReportViewService,
14
+ SourceProviderRegistry,
15
+ type ResultCache,
16
+ } from '@ftisindia/report-builder';
17
+ import { AuthModule } from '../auth/auth.module';
18
+ import { ReportsActionsService } from './application/services/reports-actions.service';
19
+ import { ReportsDefinitionsService } from './application/services/reports-definitions.service';
20
+ import { ReportsExportsService } from './application/services/reports-exports.service';
21
+ import { ReportsExportDispatcherService } from './application/services/reports-export-dispatcher.service';
22
+ import { ReportsQueriesService } from './application/services/reports-queries.service';
23
+ import { ReportsSettingsReader } from './application/services/reports-settings-reader.service';
24
+ import { ReportsViewsService } from './application/services/reports-views.service';
25
+ import { CaslReportsAuthorization } from './infrastructure/casl-reports-authorization';
26
+ import { PrismaReportsAuditSink } from './infrastructure/audit-sink.adapter';
27
+ import { ReportsPrismaTxRunner } from './infrastructure/prisma-tx-runner';
28
+ import { PrismaQueryExecutor } from './infrastructure/prisma-query-executor';
29
+ import { PrismaSnapshotRunner } from './infrastructure/prisma-snapshot-runner';
30
+ import { PrismaReportsCatalog } from './infrastructure/prisma-catalog.adapter';
31
+ import { ReportsJobQueueNoop } from './infrastructure/reports-job-queue.adapter';
32
+ import { LocalDiskReportExportStorage } from './infrastructure/storage/local-disk-export-storage.adapter';
33
+ import { RequestReportsContext } from './infrastructure/request-reports-context';
34
+ import { ReportsRegistryBootstrapService } from './infrastructure/registry/registry-bootstrap.service';
35
+ import { ReportsSchemaCheckService } from './infrastructure/schema-check/reports-schema-check.service';
36
+ import {
37
+ PrismaBulkActionRunStore,
38
+ PrismaExportJobStore,
39
+ PrismaReportDefinitionStore,
40
+ PrismaRowTagStore,
41
+ PrismaSavedViewStore,
42
+ } from './infrastructure/stores';
43
+ import { ReportsActionsController } from './presentation/reports-actions.controller';
44
+ import { ReportsDefinitionsController } from './presentation/reports-definitions.controller';
45
+ import { ReportsExportController } from './presentation/reports-export.controller';
46
+ import { ReportsExportJobsController } from './presentation/reports-export-jobs.controller';
47
+ import { ReportsQueryController } from './presentation/reports-query.controller';
48
+ import { ReportsViewsController } from './presentation/reports-views.controller';
49
+ import { OrgMembersReportSource } from './sources/org-members.source';
50
+ import { REPORTS_RESULT_CACHE, REPORTS_TOKEN_SECRET } from './reports.tokens';
51
+
52
+ /**
53
+ * The reports glue module — binds the framework-free @ftisindia/report-builder
54
+ * engine to this app's services (request context, CASL RBAC, AuditService,
55
+ * Prisma) and ships reports-OWNED async-export infrastructure (its own worker
56
+ * over ReportExportJob + org-scoped file storage). It imports NO FormsModule:
57
+ * form-backed sources and the delegated grid verbs live in the optional
58
+ * ReportsFormsModule bridge (§3.2 standalone guarantee). Engine services are
59
+ * composed here via factories over the adapters and stores; app extensions
60
+ * register through the @ReportSource/@ReportRowAction decorators.
61
+ *
62
+ * The engine core knows nothing about the form builder (the standalone
63
+ * guarantee, report design §3.2); the forms adapter wired below is what makes
64
+ * `kind: "form"` sources exist in THIS app.
65
+ */
66
+ @Module({
67
+ imports: [AuthModule, DiscoveryModule],
68
+ controllers: [
69
+ // Listed before the definitions controller so the literal 'exports'
70
+ // segment wins over the ':key' parameter on the shared base path.
71
+ ReportsExportJobsController,
72
+ ReportsDefinitionsController,
73
+ ReportsQueryController,
74
+ ReportsViewsController,
75
+ ReportsActionsController,
76
+ ReportsExportController,
77
+ ],
78
+ providers: [
79
+ // Adapters over the ecosystem seams.
80
+ RequestReportsContext,
81
+ CaslReportsAuthorization,
82
+ PrismaReportsAuditSink,
83
+ ReportsPrismaTxRunner,
84
+ PrismaQueryExecutor,
85
+ PrismaSnapshotRunner,
86
+ PrismaReportsCatalog,
87
+ // Reports-OWNED async-export infrastructure — no forms outbox / UploadedFile.
88
+ ReportsJobQueueNoop,
89
+ LocalDiskReportExportStorage,
90
+ ReportsExportDispatcherService,
91
+ // Prisma store implementations of the engine ports.
92
+ PrismaReportDefinitionStore,
93
+ PrismaSavedViewStore,
94
+ PrismaExportJobStore,
95
+ PrismaBulkActionRunStore,
96
+ PrismaRowTagStore,
97
+ ReportsSettingsReader,
98
+ // Engine registries — singletons; extensions land via the bootstrap scan.
99
+ { provide: ReportSourceRegistry, useFactory: () => new ReportSourceRegistry() },
100
+ { provide: SourceProviderRegistry, useFactory: () => new SourceProviderRegistry() },
101
+ { provide: ReportRowActionRegistry, useFactory: () => new ReportRowActionRegistry() },
102
+ // Result cache (§5.7) — in-memory by default; rebind the token for Redis.
103
+ { provide: REPORTS_RESULT_CACHE, useFactory: () => createInMemoryResultCache() },
104
+ // HMAC key bytes for cursors and action tokens (resolved in reports.config).
105
+ {
106
+ provide: REPORTS_TOKEN_SECRET,
107
+ useFactory: (config: ConfigService) => {
108
+ const secret = config.get<Uint8Array>('reports.tokenSecret');
109
+ if (secret === undefined || secret.length === 0) {
110
+ throw new Error('reports.tokenSecret failed to resolve — check reports.config.ts.');
111
+ }
112
+ return secret;
113
+ },
114
+ inject: [ConfigService],
115
+ },
116
+ // Engine orchestration services, composed over the seams above.
117
+ {
118
+ provide: ReportDefinitionService,
119
+ useFactory: (
120
+ store: PrismaReportDefinitionStore,
121
+ providers: SourceProviderRegistry,
122
+ rowActions: ReportRowActionRegistry,
123
+ authz: CaslReportsAuthorization,
124
+ audit: PrismaReportsAuditSink,
125
+ txRunner: ReportsPrismaTxRunner,
126
+ catalog: PrismaReportsCatalog,
127
+ executor: PrismaQueryExecutor,
128
+ settings: ReportsSettingsReader,
129
+ secret: Uint8Array,
130
+ ) =>
131
+ new ReportDefinitionService({
132
+ store,
133
+ providers,
134
+ rowActions,
135
+ authz,
136
+ audit,
137
+ txRunner,
138
+ catalog,
139
+ executor,
140
+ policyFor: (orgId) => settings.policyFor(orgId),
141
+ secret,
142
+ }),
143
+ inject: [
144
+ PrismaReportDefinitionStore,
145
+ SourceProviderRegistry,
146
+ ReportRowActionRegistry,
147
+ CaslReportsAuthorization,
148
+ PrismaReportsAuditSink,
149
+ ReportsPrismaTxRunner,
150
+ PrismaReportsCatalog,
151
+ PrismaQueryExecutor,
152
+ ReportsSettingsReader,
153
+ REPORTS_TOKEN_SECRET,
154
+ ],
155
+ },
156
+ {
157
+ provide: ReportQueryService,
158
+ useFactory: (
159
+ store: PrismaReportDefinitionStore,
160
+ providers: SourceProviderRegistry,
161
+ rowActions: ReportRowActionRegistry,
162
+ tags: PrismaRowTagStore,
163
+ executor: PrismaQueryExecutor,
164
+ authz: CaslReportsAuthorization,
165
+ cache: ResultCache,
166
+ settings: ReportsSettingsReader,
167
+ secret: Uint8Array,
168
+ ) =>
169
+ new ReportQueryService({
170
+ store,
171
+ providers,
172
+ rowActions,
173
+ tags,
174
+ executor,
175
+ authz,
176
+ cache,
177
+ policyFor: (orgId) => settings.policyFor(orgId),
178
+ secret,
179
+ }),
180
+ inject: [
181
+ PrismaReportDefinitionStore,
182
+ SourceProviderRegistry,
183
+ ReportRowActionRegistry,
184
+ PrismaRowTagStore,
185
+ PrismaQueryExecutor,
186
+ CaslReportsAuthorization,
187
+ REPORTS_RESULT_CACHE,
188
+ ReportsSettingsReader,
189
+ REPORTS_TOKEN_SECRET,
190
+ ],
191
+ },
192
+ {
193
+ provide: ReportViewService,
194
+ useFactory: (
195
+ views: PrismaSavedViewStore,
196
+ definitions: PrismaReportDefinitionStore,
197
+ authz: CaslReportsAuthorization,
198
+ audit: PrismaReportsAuditSink,
199
+ txRunner: ReportsPrismaTxRunner,
200
+ ) => new ReportViewService({ views, definitions, authz, audit, txRunner }),
201
+ inject: [
202
+ PrismaSavedViewStore,
203
+ PrismaReportDefinitionStore,
204
+ CaslReportsAuthorization,
205
+ PrismaReportsAuditSink,
206
+ ReportsPrismaTxRunner,
207
+ ],
208
+ },
209
+ {
210
+ provide: ReportTagService,
211
+ useFactory: (
212
+ tags: PrismaRowTagStore,
213
+ authz: CaslReportsAuthorization,
214
+ audit: PrismaReportsAuditSink,
215
+ settings: ReportsSettingsReader,
216
+ ) =>
217
+ new ReportTagService({
218
+ tags,
219
+ authz,
220
+ audit,
221
+ policyFor: (orgId) => settings.policyFor(orgId),
222
+ }),
223
+ inject: [
224
+ PrismaRowTagStore,
225
+ CaslReportsAuthorization,
226
+ PrismaReportsAuditSink,
227
+ ReportsSettingsReader,
228
+ ],
229
+ },
230
+ {
231
+ provide: ReportActionService,
232
+ useFactory: (
233
+ definitions: PrismaReportDefinitionStore,
234
+ providers: SourceProviderRegistry,
235
+ actions: ReportRowActionRegistry,
236
+ runs: PrismaBulkActionRunStore,
237
+ executor: PrismaQueryExecutor,
238
+ authz: CaslReportsAuthorization,
239
+ audit: PrismaReportsAuditSink,
240
+ txRunner: ReportsPrismaTxRunner,
241
+ cache: ResultCache,
242
+ settings: ReportsSettingsReader,
243
+ secret: Uint8Array,
244
+ ) =>
245
+ new ReportActionService({
246
+ definitions,
247
+ providers,
248
+ actions,
249
+ runs,
250
+ executor,
251
+ authz,
252
+ audit,
253
+ txRunner,
254
+ cache,
255
+ policyFor: (orgId) => settings.policyFor(orgId),
256
+ secret,
257
+ // Glue row actions are Nest providers with their own injection —
258
+ // the engine's service-locator seam stays empty in this app.
259
+ services: {},
260
+ }),
261
+ inject: [
262
+ PrismaReportDefinitionStore,
263
+ SourceProviderRegistry,
264
+ ReportRowActionRegistry,
265
+ PrismaBulkActionRunStore,
266
+ PrismaQueryExecutor,
267
+ CaslReportsAuthorization,
268
+ PrismaReportsAuditSink,
269
+ ReportsPrismaTxRunner,
270
+ REPORTS_RESULT_CACHE,
271
+ ReportsSettingsReader,
272
+ REPORTS_TOKEN_SECRET,
273
+ ],
274
+ },
275
+ {
276
+ provide: ReportExportService,
277
+ useFactory: (
278
+ definitions: PrismaReportDefinitionStore,
279
+ providers: SourceProviderRegistry,
280
+ jobs: PrismaExportJobStore,
281
+ queue: ReportsJobQueueNoop,
282
+ sink: LocalDiskReportExportStorage,
283
+ snapshot: PrismaSnapshotRunner,
284
+ executor: PrismaQueryExecutor,
285
+ authz: CaslReportsAuthorization,
286
+ audit: PrismaReportsAuditSink,
287
+ txRunner: ReportsPrismaTxRunner,
288
+ settings: ReportsSettingsReader,
289
+ secret: Uint8Array,
290
+ ) =>
291
+ new ReportExportService({
292
+ definitions,
293
+ providers,
294
+ jobs,
295
+ queue,
296
+ sink,
297
+ snapshot,
298
+ executor,
299
+ authz,
300
+ audit,
301
+ txRunner,
302
+ policyFor: (orgId) => settings.policyFor(orgId),
303
+ secret,
304
+ }),
305
+ inject: [
306
+ PrismaReportDefinitionStore,
307
+ SourceProviderRegistry,
308
+ PrismaExportJobStore,
309
+ ReportsJobQueueNoop,
310
+ LocalDiskReportExportStorage,
311
+ PrismaSnapshotRunner,
312
+ PrismaQueryExecutor,
313
+ CaslReportsAuthorization,
314
+ PrismaReportsAuditSink,
315
+ ReportsPrismaTxRunner,
316
+ ReportsSettingsReader,
317
+ REPORTS_TOKEN_SECRET,
318
+ ],
319
+ },
320
+ // The example custom source (form-backed sources + delegated form verbs
321
+ // live in the optional ReportsFormsModule, not here — §3.2 standalone).
322
+ OrgMembersReportSource,
323
+ // Startup wiring + background work.
324
+ ReportsRegistryBootstrapService,
325
+ ReportsSchemaCheckService,
326
+ // Thin HTTP-facing services.
327
+ ReportsDefinitionsService,
328
+ ReportsQueriesService,
329
+ ReportsViewsService,
330
+ ReportsActionsService,
331
+ ReportsExportsService,
332
+ ],
333
+ exports: [ReportSourceRegistry, SourceProviderRegistry, ReportRowActionRegistry],
334
+ })
335
+ export class ReportsModule {}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * DI tokens for swappable reports seams. The defaults are bound in
3
+ * reports.module.ts; apps override them with custom providers (e.g. a
4
+ * Redis-backed ResultCache) without touching the module internals.
5
+ */
6
+
7
+ /** Binds the engine's ResultCache port (report design §5.7). */
8
+ export const REPORTS_RESULT_CACHE = Symbol('REPORTS_RESULT_CACHE');
9
+
10
+ /** Key bytes for cursor/action-token HMACs (report design §5.3/§6.3). */
11
+ export const REPORTS_TOKEN_SECRET = Symbol('REPORTS_TOKEN_SECRET');
@@ -0,0 +1,112 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import type { ReportSourceDef, SourceManifest, SourceQuery } from '@ftisindia/report-builder';
3
+ import { ReportSource } from '../infrastructure/registry/report-extension.decorators';
4
+
5
+ /**
6
+ * `kind: "custom"` example source (report design §3.2) over the template's own
7
+ * Membership/User/Role tables — the Phase-1 milestone proof (§14): a complete
8
+ * report with NO form-builder involvement, demonstrating the standalone
9
+ * guarantee before form integration exists.
10
+ *
11
+ * The contract split (§3): this class declares STRUCTURE only — the compiler
12
+ * owns query construction. It never sees a QuerySpec, so it cannot be injected
13
+ * and cannot bypass the allowlist.
14
+ *
15
+ * Nullability is read off prisma/schema.prisma, not assumed: User.displayName
16
+ * and User.email are nullable (String?), so their sortable columns are
17
+ * COALESCE-wrapped to '' and declared nullable: false — NULL poisons keyset
18
+ * predicates (§5.3). Role.name and the Membership columns are NOT NULL.
19
+ *
20
+ * Index specs are honest documentation (§5.2): they name the indexes an app
21
+ * WOULD create to publish this source on the indexed tier (org-leading btrees
22
+ * are impossible for User-side columns — User carries no orgId — so those
23
+ * specs live on "User" without the org prefix). The shipped example report
24
+ * runs on the LIVE tier, where the catalog lint does not require them.
25
+ */
26
+ @Injectable()
27
+ @ReportSource()
28
+ export class OrgMembersReportSource implements ReportSourceDef {
29
+ readonly key = 'org-members';
30
+
31
+ manifest(): SourceManifest {
32
+ return {
33
+ // Membership id — the stable row identity tags and actions bind to (§7).
34
+ rowId: 'm."id"',
35
+ orgScoped: true,
36
+ columns: [
37
+ {
38
+ id: 'displayName',
39
+ sql: `COALESCE(u."displayName", '')`,
40
+ type: 'text',
41
+ sortable: true,
42
+ filterable: true,
43
+ searchable: true,
44
+ search: { mode: 'trgm' },
45
+ nullable: false,
46
+ collation: 'C',
47
+ index: {
48
+ name: 'idx_rb_member_display',
49
+ kind: 'btree',
50
+ table: 'User',
51
+ expr: `COALESCE("displayName", '') COLLATE "C"`,
52
+ },
53
+ // A btree cannot carry gin_trgm_ops — search gets its own index.
54
+ searchIndex: {
55
+ name: 'idx_rb_member_display_trgm',
56
+ kind: 'gin',
57
+ table: 'User',
58
+ expr: `COALESCE("displayName", '') gin_trgm_ops`,
59
+ },
60
+ },
61
+ {
62
+ id: 'email',
63
+ // User.email is String? in schema.prisma — surrogate '' keeps the
64
+ // sort key non-null (§5.3).
65
+ sql: `COALESCE(u."email", '')`,
66
+ type: 'text',
67
+ sortable: true,
68
+ filterable: true,
69
+ nullable: false,
70
+ collation: 'C',
71
+ },
72
+ {
73
+ id: 'role',
74
+ sql: 'r."name"',
75
+ type: 'text',
76
+ sortable: true,
77
+ filterable: true,
78
+ nullable: false,
79
+ collation: 'C',
80
+ },
81
+ {
82
+ id: 'status',
83
+ // MembershipStatus enum → text for binds and projection.
84
+ sql: 'm."status"::text',
85
+ type: 'enum',
86
+ filterable: true,
87
+ nullable: false,
88
+ },
89
+ {
90
+ id: 'joinedAt',
91
+ sql: 'm."createdAt"',
92
+ type: 'datetime',
93
+ valueKind: 'native',
94
+ sortable: true,
95
+ nullable: false,
96
+ },
97
+ ],
98
+ };
99
+ }
100
+
101
+ /** FROM/JOIN only — WHERE/ORDER/LIMIT belong to the compiler (§3). */
102
+ baseQuery(): SourceQuery {
103
+ return {
104
+ from:
105
+ '"Membership" m' +
106
+ ' JOIN "User" u ON u."id" = m."userId"' +
107
+ ' JOIN "Role" r ON r."id" = m."roleId"',
108
+ orgColumn: 'm."orgId"',
109
+ primaryTable: 'Membership',
110
+ };
111
+ }
112
+ }
@@ -35,6 +35,66 @@ export const settingDefinitions = {
35
35
  defaultValue: '',
36
36
  parse: parseOptionalUrl,
37
37
  },
38
+ // Form builder (@ftisindia/form-builder) — org-variable policy lives here so
39
+ // it is typed, validated, and audited like every other org setting.
40
+ 'forms.allowedDangerousActions': {
41
+ defaultValue: [],
42
+ parse: parseStringArray('forms.allowedDangerousActions', 50),
43
+ },
44
+ 'forms.maxFileSizeMb': {
45
+ defaultValue: 10,
46
+ parse: parsePositiveInt('forms.maxFileSizeMb', 1024),
47
+ },
48
+ 'forms.enableRuleIteration': {
49
+ defaultValue: false,
50
+ parse: parseBoolean,
51
+ },
52
+ 'forms.virusScanRequired': {
53
+ defaultValue: false,
54
+ parse: parseBoolean,
55
+ },
56
+ 'forms.maxSubmissionsPerIpPerDay': {
57
+ defaultValue: 50,
58
+ parse: parsePositiveInt('forms.maxSubmissionsPerIpPerDay', 100000),
59
+ },
60
+ // SSRF gate for definition webhooks: empty list = webhooks disabled until
61
+ // an admin explicitly allows destination hosts (changes are audited).
62
+ 'forms.webhookAllowedHosts': {
63
+ defaultValue: [],
64
+ parse: parseStringArray('forms.webhookAllowedHosts', 50),
65
+ },
66
+ // Report builder (@ftisindia/report-builder) — runtime budgets and caps
67
+ // (report design §5.2/§8/§9). Typed, validated, audited like every setting.
68
+ 'reports.statementTimeoutMs': {
69
+ defaultValue: 5000,
70
+ parse: parsePositiveInt('reports.statementTimeoutMs', 60000),
71
+ },
72
+ 'reports.exportMaxSnapshotSeconds': {
73
+ defaultValue: 120,
74
+ parse: parsePositiveInt('reports.exportMaxSnapshotSeconds', 3600),
75
+ },
76
+ 'reports.maxRowsSync': {
77
+ defaultValue: 10000,
78
+ parse: parsePositiveInt('reports.maxRowsSync', 100000),
79
+ },
80
+ 'reports.countCap': {
81
+ defaultValue: 10000,
82
+ parse: parsePositiveInt('reports.countCap', 1000000),
83
+ },
84
+ 'reports.liveTierMaxRows': {
85
+ defaultValue: 50000,
86
+ parse: parsePositiveInt('reports.liveTierMaxRows', 10000000),
87
+ },
88
+ // 0 disables the short-TTL result cache (report design §5.7).
89
+ 'reports.resultCacheTtlMs': {
90
+ defaultValue: 0,
91
+ parse: parseNonNegativeInt('reports.resultCacheTtlMs', 300000),
92
+ },
93
+ // Curated tag vocabulary; empty = free-form tags (report design §7).
94
+ 'reports.tagVocabulary': {
95
+ defaultValue: [],
96
+ parse: parseStringArray('reports.tagVocabulary', 200),
97
+ },
38
98
  timezone: {
39
99
  defaultValue: 'UTC',
40
100
  parse: parseTimezone,
@@ -101,6 +161,40 @@ function parseOptionalUrl(value: unknown) {
101
161
  }
102
162
  }
103
163
 
164
+ function parseStringArray(key: string, maxItems: number) {
165
+ return (value: unknown) => {
166
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
167
+ throw new BadRequestException(`${key} must be an array of strings.`);
168
+ }
169
+
170
+ if (value.length > maxItems) {
171
+ throw new BadRequestException(`${key} must not contain more than ${maxItems} items.`);
172
+ }
173
+
174
+ return value.map((item) => (item as string).trim()).filter((item) => item.length > 0);
175
+ };
176
+ }
177
+
178
+ function parsePositiveInt(key: string, max: number) {
179
+ return (value: unknown) => {
180
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > max) {
181
+ throw new BadRequestException(`${key} must be an integer between 1 and ${max}.`);
182
+ }
183
+
184
+ return value;
185
+ };
186
+ }
187
+
188
+ function parseNonNegativeInt(key: string, max: number) {
189
+ return (value: unknown) => {
190
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > max) {
191
+ throw new BadRequestException(`${key} must be an integer between 0 and ${max}.`);
192
+ }
193
+
194
+ return value;
195
+ };
196
+ }
197
+
104
198
  function parseTimezone(value: unknown) {
105
199
  if (typeof value !== 'string') {
106
200
  throw new BadRequestException('timezone must be a string.');