@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,343 @@
1
+ import { INestApplication, ValidationPipe } from '@nestjs/common';
2
+ import { Test } from '@nestjs/testing';
3
+ import request from 'supertest';
4
+ import { ReportSourceRegistry } from '@ftisindia/report-builder';
5
+ import type { ReportSourceDef } from '@ftisindia/report-builder';
6
+ import { HttpExceptionFilter } from '../src/common/filters/http-exception.filter';
7
+ import { PrismaService } from '../src/database/prisma/prisma.service';
8
+ import { ReportsSchemaCheckService } from '../src/modules/reports/infrastructure/schema-check/reports-schema-check.service';
9
+
10
+ /**
11
+ * Reports e2e for the production-critical surfaces the basic specs do not
12
+ * reach: the indexed-tier publish → apply-migration → republish ROUND TRIP,
13
+ * the materialized tier over a real materialized view, and the boot
14
+ * schema-check FAILURE mode. All against real Postgres; test-only sources are
15
+ * registered into the engine's source registry at runtime (no shipped
16
+ * production code needed to exercise them).
17
+ */
18
+ jest.setTimeout(60_000);
19
+
20
+ describe('Reports performance tiers + schema check (e2e)', () => {
21
+ let app: INestApplication;
22
+ let prisma: PrismaService;
23
+ let sources: ReportSourceRegistry;
24
+
25
+ beforeAll(async () => {
26
+ process.env.DATABASE_URL = process.env.TEST_DATABASE_URL ?? process.env.DATABASE_URL;
27
+ process.env.JWT_SECRET ||= 'test-jwt-secret-at-least-32-characters-long';
28
+ process.env.AUTH_GOOGLE_ENABLED ||= 'false';
29
+ process.env.NODE_ENV = 'test';
30
+ process.env.FORMS_OUTBOX_ENABLED = 'false';
31
+ process.env.REPORTS_EXPORT_WORKER_ENABLED = 'false';
32
+
33
+ const { AppModule } = await import('../src/app.module');
34
+ const moduleRef = await Test.createTestingModule({
35
+ imports: [AppModule],
36
+ }).compile();
37
+
38
+ app = moduleRef.createNestApplication();
39
+ app.useGlobalFilters(new HttpExceptionFilter());
40
+ app.useGlobalPipes(
41
+ new ValidationPipe({
42
+ forbidNonWhitelisted: true,
43
+ transform: true,
44
+ whitelist: true,
45
+ }),
46
+ );
47
+ await app.init();
48
+ prisma = app.get(PrismaService);
49
+ sources = app.get(ReportSourceRegistry);
50
+ });
51
+
52
+ afterAll(async () => {
53
+ await app.close();
54
+ });
55
+
56
+ it('indexed tier: publish fails with the migration snippet, then SUCCEEDS after applying it', async () => {
57
+ const { accessToken, orgId } = await createUserAndOrg('rep-idxrt');
58
+ const tag = uniqueTag();
59
+ const sourceKey = `idx-test-${tag}`;
60
+ const statusIdx = `idx_rb_test_status_${tag}`;
61
+ const joinedIdx = `idx_rb_test_joined_${tag}`;
62
+ sources.register(indexedTestSource(sourceKey, statusIdx, joinedIdx));
63
+
64
+ const key = uniqueKey('idxrt');
65
+ const def = indexedDefinition(key, sourceKey);
66
+
67
+ try {
68
+ await api()
69
+ .post(`/organisations/${orgId}/reports`)
70
+ .set(auth(accessToken))
71
+ .send(def)
72
+ .expect(201);
73
+
74
+ // Publish runs the catalog lint: the declared indexes do not exist yet,
75
+ // so it fails CONSTRUCTIVELY with the exact CREATE INDEX SQL (§8).
76
+ const failed = await api()
77
+ .post(`/organisations/${orgId}/reports/${key}/publish`)
78
+ .set(auth(accessToken))
79
+ .expect(422);
80
+ expect(JSON.stringify(failed.body)).toContain('CREATE INDEX');
81
+ expect(JSON.stringify(failed.body)).toContain(statusIdx);
82
+
83
+ // Apply the suggested indexes (org-leading btrees on the physical columns).
84
+ await prisma.$executeRawUnsafe(
85
+ `CREATE INDEX "${statusIdx}" ON "Membership" ("orgId", "status")`,
86
+ );
87
+ await prisma.$executeRawUnsafe(
88
+ `CREATE INDEX "${joinedIdx}" ON "Membership" ("orgId", "createdAt")`,
89
+ );
90
+
91
+ // The SAME draft now publishes: catalog + plan lint pass.
92
+ await api()
93
+ .post(`/organisations/${orgId}/reports/${key}/publish`)
94
+ .set(auth(accessToken))
95
+ .expect(200);
96
+
97
+ const result = await api()
98
+ .post(`/organisations/${orgId}/reports/${key}/query`)
99
+ .set(auth(accessToken))
100
+ .send({ sort: [{ column: 'joinedAt', dir: 'desc' }] })
101
+ .expect(200);
102
+ expect(result.body.rows.length).toBeGreaterThanOrEqual(1);
103
+ } finally {
104
+ await prisma.$executeRawUnsafe(`DROP INDEX IF EXISTS "${statusIdx}"`);
105
+ await prisma.$executeRawUnsafe(`DROP INDEX IF EXISTS "${joinedIdx}"`);
106
+ }
107
+ });
108
+
109
+ it('materialized tier: publishes against a materialized view and reports freshAsOf', async () => {
110
+ const { accessToken, orgId } = await createUserAndOrg('rep-matrt');
111
+ const tag = uniqueTag();
112
+ const mv = `rb_test_mv_${tag}`;
113
+
114
+ // Create the matview AFTER the org exists so it captures the owner row.
115
+ await prisma.$executeRawUnsafe(
116
+ `CREATE MATERIALIZED VIEW ${mv} AS ` +
117
+ `SELECT m."id" AS "rowId", m."orgId" AS "orgId", m."status"::text AS "status", ` +
118
+ `m."createdAt" AS "joinedAt" FROM "Membership" m`,
119
+ );
120
+ try {
121
+ const sourceKey = `mat-test-${tag}`;
122
+ sources.register(materializedTestSource(sourceKey, mv));
123
+
124
+ const key = uniqueKey('matrt');
125
+ const def = {
126
+ key,
127
+ title: 'Member stats (materialized)',
128
+ source: { kind: 'custom', key: sourceKey },
129
+ columns: [
130
+ {
131
+ id: 'status',
132
+ header: 'Status',
133
+ columnId: 'status',
134
+ type: 'enum',
135
+ filterable: true,
136
+ enum: ['ACTIVE', 'SUSPENDED', 'REVOKED'],
137
+ },
138
+ {
139
+ id: 'joinedAt',
140
+ header: 'Joined',
141
+ columnId: 'joinedAt',
142
+ type: 'datetime',
143
+ sortable: true,
144
+ },
145
+ ],
146
+ defaultSort: [{ column: 'joinedAt', dir: 'desc' }],
147
+ performanceTier: 'materialized',
148
+ };
149
+
150
+ await api()
151
+ .post(`/organisations/${orgId}/reports`)
152
+ .set(auth(accessToken))
153
+ .send(def)
154
+ .expect(201);
155
+ await api()
156
+ .post(`/organisations/${orgId}/reports/${key}/publish`)
157
+ .set(auth(accessToken))
158
+ .expect(200);
159
+
160
+ const result = await api()
161
+ .post(`/organisations/${orgId}/reports/${key}/query`)
162
+ .set(auth(accessToken))
163
+ .send({})
164
+ .expect(200);
165
+ expect(result.body.rows.length).toBeGreaterThanOrEqual(1);
166
+ expect(result.body.meta.freshAsOf).not.toBeNull();
167
+ } finally {
168
+ await prisma.$executeRawUnsafe(`DROP MATERIALIZED VIEW IF EXISTS ${mv}`);
169
+ }
170
+ });
171
+
172
+ it('boot schema check fails loudly when a required partial unique index is missing', async () => {
173
+ const check = app.get(ReportsSchemaCheckService);
174
+ // A freshly migrated DB passes.
175
+ await expect(check.check()).resolves.toBeUndefined();
176
+
177
+ await prisma.$executeRawUnsafe('DROP INDEX "report_view_shared_uq"');
178
+ try {
179
+ await expect(check.check()).rejects.toThrow(/report_view_shared_uq|CREATE UNIQUE INDEX/);
180
+ } finally {
181
+ // Restore so the rest of the suite (and the DB) stays intact.
182
+ await prisma.$executeRawUnsafe(
183
+ 'CREATE UNIQUE INDEX "report_view_shared_uq" ON "ReportSavedView" ("orgId", "reportKey", "name") WHERE "ownerId" IS NULL',
184
+ );
185
+ }
186
+ // Green again after restore.
187
+ await expect(check.check()).resolves.toBeUndefined();
188
+ });
189
+
190
+ // ── test-only sources ───────────────────────────────────────────────────────
191
+
192
+ function indexedDefinition(key: string, sourceKey: string): Record<string, unknown> {
193
+ return {
194
+ key,
195
+ title: 'Indexed members',
196
+ source: { kind: 'custom', key: sourceKey },
197
+ columns: [
198
+ {
199
+ id: 'status',
200
+ header: 'Status',
201
+ columnId: 'status',
202
+ type: 'enum',
203
+ filterable: true,
204
+ sortable: true,
205
+ enum: ['ACTIVE', 'SUSPENDED', 'REVOKED'],
206
+ },
207
+ {
208
+ id: 'joinedAt',
209
+ header: 'Joined',
210
+ columnId: 'joinedAt',
211
+ type: 'datetime',
212
+ sortable: true,
213
+ },
214
+ ],
215
+ defaultSort: [{ column: 'joinedAt', dir: 'desc' }],
216
+ performanceTier: 'indexed',
217
+ };
218
+ }
219
+
220
+ function indexedTestSource(key: string, statusIdx: string, joinedIdx: string): ReportSourceDef {
221
+ return {
222
+ key,
223
+ manifest: () => ({
224
+ rowId: 'm."id"',
225
+ orgScoped: true,
226
+ columns: [
227
+ {
228
+ id: 'status',
229
+ sql: 'm."status"::text',
230
+ type: 'enum',
231
+ filterable: true,
232
+ sortable: true,
233
+ nullable: false,
234
+ index: {
235
+ name: statusIdx,
236
+ kind: 'btree',
237
+ table: 'Membership',
238
+ expr: '"orgId", "status"',
239
+ },
240
+ },
241
+ {
242
+ id: 'joinedAt',
243
+ sql: 'm."createdAt"',
244
+ type: 'datetime',
245
+ valueKind: 'native',
246
+ sortable: true,
247
+ nullable: false,
248
+ index: {
249
+ name: joinedIdx,
250
+ kind: 'btree',
251
+ table: 'Membership',
252
+ expr: '"orgId", "createdAt"',
253
+ },
254
+ },
255
+ ],
256
+ }),
257
+ baseQuery: () => ({
258
+ from: '"Membership" m',
259
+ orgColumn: 'm."orgId"',
260
+ primaryTable: 'Membership',
261
+ }),
262
+ };
263
+ }
264
+
265
+ function materializedTestSource(key: string, mv: string): ReportSourceDef {
266
+ return {
267
+ key,
268
+ manifest: () => ({
269
+ rowId: 'm."id"',
270
+ orgScoped: true,
271
+ columns: [
272
+ {
273
+ id: 'status',
274
+ sql: '"status"',
275
+ type: 'enum',
276
+ filterable: true,
277
+ nullable: false,
278
+ },
279
+ {
280
+ id: 'joinedAt',
281
+ sql: '"joinedAt"',
282
+ type: 'datetime',
283
+ valueKind: 'native',
284
+ sortable: true,
285
+ nullable: false,
286
+ },
287
+ ],
288
+ materialization: {
289
+ relation: mv,
290
+ orgColumn: '"orgId"',
291
+ rowIdColumn: 'rowId',
292
+ stalenessSeconds: 300,
293
+ },
294
+ }),
295
+ baseQuery: () => ({ from: mv, orgColumn: '"orgId"', primaryTable: mv }),
296
+ freshAsOf: () => Promise.resolve(new Date()),
297
+ };
298
+ }
299
+
300
+ // ── shared harness ──────────────────────────────────────────────────────────
301
+
302
+ function api() {
303
+ return request(app.getHttpServer());
304
+ }
305
+
306
+ function auth(token: string) {
307
+ return { Authorization: `Bearer ${token}` };
308
+ }
309
+
310
+ function uniqueSuffix() {
311
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
312
+ }
313
+
314
+ /** A lowercase alphanumeric tag safe for SQL identifiers and report keys. */
315
+ function uniqueTag() {
316
+ return uniqueSuffix()
317
+ .replace(/[^a-z0-9]/gi, '')
318
+ .toLowerCase();
319
+ }
320
+
321
+ function uniqueKey(label: string) {
322
+ return `${label}-${uniqueTag()}`;
323
+ }
324
+
325
+ async function createUserAndOrg(label: string) {
326
+ const suffix = uniqueSuffix();
327
+ const signup = await api()
328
+ .post('/auth/signup')
329
+ .send({
330
+ email: `${label}-${suffix}@example.com`,
331
+ password: 'test-password-123',
332
+ displayName: label,
333
+ })
334
+ .expect(201);
335
+ const accessToken = signup.body.accessToken as string;
336
+ const org = await api()
337
+ .post('/organisations')
338
+ .set(auth(accessToken))
339
+ .send({ name: `${label} ${suffix}`, slug: `${label}-${suffix}` })
340
+ .expect(201);
341
+ return { accessToken, orgId: org.body.organisation.id as string };
342
+ }
343
+ });
@@ -8,9 +8,20 @@ import { PermissionKey } from '../src/modules/access-control/types/permission-ke
8
8
  import { routePermissionRegistry } from '../src/modules/access-control/types/route-permission-registry';
9
9
  import { AuditController } from '../src/modules/audit/presentation/audit.controller';
10
10
  import { AuthController } from '../src/modules/auth/presentation/auth.controller';
11
+ import { FormsDataSourcesController } from '../src/modules/forms/presentation/forms-data-sources.controller';
12
+ import { FormsDefinitionsController } from '../src/modules/forms/presentation/forms-definitions.controller';
13
+ import { FormsFilesController } from '../src/modules/forms/presentation/forms-files.controller';
14
+ import { FormsSubmissionsController } from '../src/modules/forms/presentation/forms-submissions.controller';
15
+ import { PublicFormsController } from '../src/modules/forms/presentation/public-forms.controller';
11
16
  import { InvitationsController } from '../src/modules/invitations/presentation/invitations.controller';
12
17
  import { MembershipsController } from '../src/modules/memberships/presentation/memberships.controller';
13
18
  import { OrganisationsController } from '../src/modules/organisations/presentation/organisations.controller';
19
+ import { ReportsActionsController } from '../src/modules/reports/presentation/reports-actions.controller';
20
+ import { ReportsDefinitionsController } from '../src/modules/reports/presentation/reports-definitions.controller';
21
+ import { ReportsExportController } from '../src/modules/reports/presentation/reports-export.controller';
22
+ import { ReportsExportJobsController } from '../src/modules/reports/presentation/reports-export-jobs.controller';
23
+ import { ReportsQueryController } from '../src/modules/reports/presentation/reports-query.controller';
24
+ import { ReportsViewsController } from '../src/modules/reports/presentation/reports-views.controller';
14
25
  import { SampleController } from '../src/modules/sample/presentation/sample.controller';
15
26
  import { SettingsController } from '../src/modules/settings/presentation/settings.controller';
16
27
 
@@ -19,9 +30,20 @@ const controllers = [
19
30
  CurrentAccessControlController,
20
31
  AuditController,
21
32
  AuthController,
33
+ FormsDataSourcesController,
34
+ FormsDefinitionsController,
35
+ FormsFilesController,
36
+ FormsSubmissionsController,
37
+ PublicFormsController,
22
38
  InvitationsController,
23
39
  MembershipsController,
24
40
  OrganisationsController,
41
+ ReportsActionsController,
42
+ ReportsDefinitionsController,
43
+ ReportsExportController,
44
+ ReportsExportJobsController,
45
+ ReportsQueryController,
46
+ ReportsViewsController,
25
47
  SampleController,
26
48
  SettingsController,
27
49
  ];