@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,390 @@
1
+ import { INestApplication, ValidationPipe } from '@nestjs/common';
2
+ import { Test } from '@nestjs/testing';
3
+ import { FieldTypeRegistry } from '@ftisindia/form-builder';
4
+ import request from 'supertest';
5
+ import { HttpExceptionFilter } from '../src/common/filters/http-exception.filter';
6
+ import { PrismaService } from '../src/database/prisma/prisma.service';
7
+
8
+ describe('Forms submissions export (e2e)', () => {
9
+ let app: INestApplication;
10
+ let prisma: PrismaService;
11
+ let owner: { accessToken: string; userId: string; orgId: string };
12
+ let formKey: string;
13
+ const seededEmails = ['export-one@example.com', 'export-two@example.com'];
14
+
15
+ beforeAll(async () => {
16
+ process.env.DATABASE_URL = process.env.TEST_DATABASE_URL ?? process.env.DATABASE_URL;
17
+ process.env.JWT_SECRET ||= 'test-jwt-secret-at-least-16-chars';
18
+ process.env.AUTH_GOOGLE_ENABLED ||= 'false';
19
+ process.env.AUTH_EMAIL_PASSWORD_ENABLED ||= 'true';
20
+ process.env.NODE_ENV = 'test';
21
+ process.env.FORMS_OUTBOX_ENABLED = 'false';
22
+ process.env.FORMS_FILE_STORAGE_DIR = './var/test-uploads';
23
+
24
+ const { AppModule } = await import('../src/app.module');
25
+ const moduleRef = await Test.createTestingModule({
26
+ imports: [AppModule],
27
+ }).compile();
28
+
29
+ app = moduleRef.createNestApplication();
30
+ app.useGlobalFilters(new HttpExceptionFilter());
31
+ app.useGlobalPipes(
32
+ new ValidationPipe({
33
+ forbidNonWhitelisted: true,
34
+ transform: true,
35
+ whitelist: true,
36
+ }),
37
+ );
38
+ await app.init();
39
+ prisma = app.get(PrismaService);
40
+ app.get(FieldTypeRegistry).register({
41
+ type: 'api-secret',
42
+ alwaysSensitive: true,
43
+ buildValidator: () => ({ type: 'string' }),
44
+ });
45
+
46
+ owner = await createUserAndOrg('export-owner');
47
+ formKey = await publishDefinition(registrationDefinition(uniqueFormKey('export')));
48
+
49
+ for (const [index, email] of seededEmails.entries()) {
50
+ await request(app.getHttpServer())
51
+ .post(`/organisations/${owner.orgId}/forms/${formKey}/submissions`)
52
+ .set('Authorization', `Bearer ${owner.accessToken}`)
53
+ .send({
54
+ mode: 'submit',
55
+ data: { fullName: `Export Person ${index}`, email, ticketType: 'standard' },
56
+ })
57
+ .expect(200);
58
+ }
59
+ });
60
+
61
+ afterAll(async () => {
62
+ await app.close();
63
+ });
64
+
65
+ it('denies export to members holding only formSubmissions.read', async () => {
66
+ const member = await createMemberInOrg(owner.orgId, 'export-reader', [
67
+ 'formSubmissions.read',
68
+ ]);
69
+
70
+ await request(app.getHttpServer())
71
+ .get(`/organisations/${owner.orgId}/forms/${formKey}/submissions/export`)
72
+ .set('Authorization', `Bearer ${member.accessToken}`)
73
+ .expect(403);
74
+ });
75
+
76
+ it('exports reportable fields as JSON and audits every export, empty windows included', async () => {
77
+ const response = await request(app.getHttpServer())
78
+ .get(`/organisations/${owner.orgId}/forms/${formKey}/submissions/export`)
79
+ .query({ format: 'json' })
80
+ .set('Authorization', `Bearer ${owner.accessToken}`)
81
+ .expect(200);
82
+
83
+ const columns = response.body.columns as string[];
84
+ const rows = response.body.rows as Array<Record<string, unknown>>;
85
+ expect(columns).toEqual(expect.arrayContaining(['fullName', 'email', 'ticketType']));
86
+ expect(rows).toHaveLength(2);
87
+ for (const email of seededEmails) {
88
+ expect(rows.some((row) => row.email === email)).toBe(true);
89
+ }
90
+
91
+ // Empty filter window: still audited, rowCount 0.
92
+ const empty = await request(app.getHttpServer())
93
+ .get(`/organisations/${owner.orgId}/forms/${formKey}/submissions/export`)
94
+ .query({
95
+ format: 'json',
96
+ from: '2000-01-01T00:00:00.000Z',
97
+ to: '2000-01-02T00:00:00.000Z',
98
+ })
99
+ .set('Authorization', `Bearer ${owner.accessToken}`)
100
+ .expect(200);
101
+ expect(empty.body.rows).toHaveLength(0);
102
+
103
+ const auditRows = await prisma.auditLog.findMany({
104
+ where: { orgId: owner.orgId, action: 'formSubmissions.export' },
105
+ orderBy: { createdAt: 'asc' },
106
+ });
107
+ const metadatas = auditRows.map(
108
+ (row) => row.metadata as { rowCount?: number; fields?: string[] },
109
+ );
110
+ expect(metadatas.some((metadata) => metadata.rowCount === 2)).toBe(true);
111
+ expect(metadatas.some((metadata) => metadata.rowCount === 0)).toBe(true);
112
+ expect(
113
+ metadatas.some((metadata) => (metadata.fields ?? []).includes('fullName')),
114
+ ).toBe(true);
115
+ });
116
+
117
+ it('exports CSV with a header line and a text/csv content type', async () => {
118
+ const response = await request(app.getHttpServer())
119
+ .get(`/organisations/${owner.orgId}/forms/${formKey}/submissions/export`)
120
+ .query({ format: 'csv' })
121
+ .set('Authorization', `Bearer ${owner.accessToken}`)
122
+ .expect(200);
123
+
124
+ expect(response.headers['content-type']).toContain('text/csv');
125
+ const lines = (response.text as string).split(/\r\n/);
126
+ expect(lines[0]).toContain('id');
127
+ expect(lines[0]).toContain('formVersion');
128
+ expect(lines[0]).toContain('fullName');
129
+ expect(lines.length).toBeGreaterThanOrEqual(3);
130
+ });
131
+
132
+ it('refuses explicitly requested sensitive fields with 403', async () => {
133
+ const sensitiveKey = uniqueFormKey('export-sensitive');
134
+ // Draft is enough — export resolves the latest definition of any status.
135
+ await request(app.getHttpServer())
136
+ .post(`/organisations/${owner.orgId}/forms`)
137
+ .set('Authorization', `Bearer ${owner.accessToken}`)
138
+ .send({ definition: credentialsDefinition(sensitiveKey) })
139
+ .expect(201);
140
+
141
+ await request(app.getHttpServer())
142
+ .get(`/organisations/${owner.orgId}/forms/${sensitiveKey}/submissions/export`)
143
+ .query({ format: 'json', fields: 'password' })
144
+ .set('Authorization', `Bearer ${owner.accessToken}`)
145
+ .expect(403);
146
+ });
147
+
148
+ it('treats custom alwaysSensitive field types as non-exportable', async () => {
149
+ const customKey = uniqueFormKey('export-custom-sensitive');
150
+ await request(app.getHttpServer())
151
+ .post(`/organisations/${owner.orgId}/forms`)
152
+ .set('Authorization', `Bearer ${owner.accessToken}`)
153
+ .send({ definition: customSensitiveDefinition(customKey) })
154
+ .expect(201);
155
+
156
+ await request(app.getHttpServer())
157
+ .get(`/organisations/${owner.orgId}/forms/${customKey}/submissions/export`)
158
+ .query({ format: 'json', fields: 'apiKey' })
159
+ .set('Authorization', `Bearer ${owner.accessToken}`)
160
+ .expect(403);
161
+
162
+ const exported = await request(app.getHttpServer())
163
+ .get(`/organisations/${owner.orgId}/forms/${customKey}/submissions/export`)
164
+ .query({ format: 'json' })
165
+ .set('Authorization', `Bearer ${owner.accessToken}`)
166
+ .expect(200);
167
+ expect(exported.body.columns).not.toContain('apiKey');
168
+ });
169
+
170
+ it('refuses reportable parent objects that contain sensitive children', async () => {
171
+ const nestedKey = uniqueFormKey('export-nested-sensitive');
172
+ await request(app.getHttpServer())
173
+ .post(`/organisations/${owner.orgId}/forms`)
174
+ .set('Authorization', `Bearer ${owner.accessToken}`)
175
+ .send({ definition: nestedSensitiveDefinition(nestedKey) })
176
+ .expect(201);
177
+
178
+ await request(app.getHttpServer())
179
+ .get(`/organisations/${owner.orgId}/forms/${nestedKey}/submissions/export`)
180
+ .query({ format: 'json', fields: 'profile' })
181
+ .set('Authorization', `Bearer ${owner.accessToken}`)
182
+ .expect(403);
183
+
184
+ const exported = await request(app.getHttpServer())
185
+ .get(`/organisations/${owner.orgId}/forms/${nestedKey}/submissions/export`)
186
+ .query({ format: 'json' })
187
+ .set('Authorization', `Bearer ${owner.accessToken}`)
188
+ .expect(200);
189
+ expect(exported.body.columns).not.toContain('profile');
190
+ });
191
+
192
+ // ── Helpers ────────────────────────────────────────────────────────────────
193
+
194
+ function uniqueSuffix() {
195
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
196
+ }
197
+
198
+ function uniqueFormKey(label: string) {
199
+ return `${label}-${uniqueSuffix()}`.toLowerCase();
200
+ }
201
+
202
+ function registrationDefinition(key: string): Record<string, unknown> {
203
+ return {
204
+ key,
205
+ version: 1,
206
+ title: 'Event registration',
207
+ fields: [
208
+ {
209
+ type: 'text',
210
+ name: 'fullName',
211
+ label: 'Full name',
212
+ validators: { required: true, maxLength: 120 },
213
+ reportable: true,
214
+ },
215
+ {
216
+ type: 'email',
217
+ name: 'email',
218
+ label: 'Email address',
219
+ validators: { required: true },
220
+ reportable: true,
221
+ },
222
+ {
223
+ type: 'select',
224
+ name: 'ticketType',
225
+ label: 'Ticket type',
226
+ validators: { required: true, options: ['standard', 'student', 'vip'] },
227
+ reportable: true,
228
+ },
229
+ ],
230
+ actions: {
231
+ submit: ['validateAll', 'persist'],
232
+ saveDraft: ['persistDraft'],
233
+ },
234
+ };
235
+ }
236
+
237
+ function credentialsDefinition(key: string): Record<string, unknown> {
238
+ return {
239
+ key,
240
+ version: 1,
241
+ title: 'Account credentials',
242
+ fields: [
243
+ {
244
+ type: 'text',
245
+ name: 'username',
246
+ label: 'Username',
247
+ validators: { required: true },
248
+ reportable: true,
249
+ },
250
+ {
251
+ type: 'password',
252
+ name: 'password',
253
+ label: 'Password',
254
+ validators: { required: true, minLength: 8 },
255
+ },
256
+ ],
257
+ actions: {
258
+ submit: ['validateAll', 'persist'],
259
+ },
260
+ };
261
+ }
262
+
263
+ function customSensitiveDefinition(key: string): Record<string, unknown> {
264
+ return {
265
+ key,
266
+ version: 1,
267
+ title: 'Custom sensitive field',
268
+ fields: [
269
+ {
270
+ type: 'api-secret',
271
+ name: 'apiKey',
272
+ label: 'API key',
273
+ reportable: true,
274
+ },
275
+ ],
276
+ actions: {
277
+ submit: ['validateAll', 'persist'],
278
+ },
279
+ };
280
+ }
281
+
282
+ function nestedSensitiveDefinition(key: string): Record<string, unknown> {
283
+ return {
284
+ key,
285
+ version: 1,
286
+ title: 'Nested sensitive field',
287
+ fields: [
288
+ {
289
+ type: 'group',
290
+ name: 'profile',
291
+ label: 'Profile',
292
+ reportable: true,
293
+ fields: [
294
+ { type: 'text', name: 'displayName', reportable: true },
295
+ { type: 'password', name: 'secretCode' },
296
+ ],
297
+ },
298
+ ],
299
+ actions: {
300
+ submit: ['validateAll', 'persist'],
301
+ },
302
+ };
303
+ }
304
+
305
+ async function publishDefinition(definition: Record<string, unknown>): Promise<string> {
306
+ const created = await request(app.getHttpServer())
307
+ .post(`/organisations/${owner.orgId}/forms`)
308
+ .set('Authorization', `Bearer ${owner.accessToken}`)
309
+ .send({ definition })
310
+ .expect(201);
311
+
312
+ await request(app.getHttpServer())
313
+ .post(
314
+ `/organisations/${owner.orgId}/forms/${created.body.key as string}/versions/${created.body.version as number}/publish`,
315
+ )
316
+ .set('Authorization', `Bearer ${owner.accessToken}`)
317
+ .expect(200);
318
+
319
+ return created.body.key as string;
320
+ }
321
+
322
+ async function createUser(label: string) {
323
+ const suffix = uniqueSuffix();
324
+ const signup = await request(app.getHttpServer())
325
+ .post('/auth/signup')
326
+ .send({
327
+ email: `${label}-${suffix}@example.com`,
328
+ password: 'test-password-123',
329
+ displayName: label,
330
+ })
331
+ .expect(201);
332
+
333
+ return {
334
+ accessToken: signup.body.accessToken as string,
335
+ userId: signup.body.user.id as string,
336
+ };
337
+ }
338
+
339
+ async function createUserAndOrg(label: string) {
340
+ const user = await createUser(label);
341
+ const suffix = uniqueSuffix();
342
+ const org = await request(app.getHttpServer())
343
+ .post('/organisations')
344
+ .set('Authorization', `Bearer ${user.accessToken}`)
345
+ .send({
346
+ name: `${label} ${suffix}`,
347
+ slug: `${label}-${suffix}`,
348
+ })
349
+ .expect(201);
350
+
351
+ return {
352
+ ...user,
353
+ orgId: org.body.organisation.id as string,
354
+ };
355
+ }
356
+
357
+ async function createMemberInOrg(orgId: string, label: string, permissionKeys: string[]) {
358
+ const user = await createUser(label);
359
+ const permissions = await prisma.permission.findMany({
360
+ where: { key: { in: permissionKeys } },
361
+ select: { id: true },
362
+ });
363
+ expect(permissions).toHaveLength(permissionKeys.length);
364
+
365
+ const role = await prisma.role.create({
366
+ data: {
367
+ orgId,
368
+ name: `${label}-${uniqueSuffix()}`,
369
+ permissions: {
370
+ create: permissions.map((permission) => ({ permissionId: permission.id })),
371
+ },
372
+ },
373
+ select: { id: true },
374
+ });
375
+ const membership = await prisma.membership.create({
376
+ data: {
377
+ userId: user.userId,
378
+ orgId,
379
+ roleId: role.id,
380
+ },
381
+ select: { id: true, roleId: true },
382
+ });
383
+
384
+ return {
385
+ ...user,
386
+ membershipId: membership.id,
387
+ roleId: membership.roleId,
388
+ };
389
+ }
390
+ });