@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,191 @@
1
+ import {
2
+ Body,
3
+ Controller,
4
+ Get,
5
+ HttpCode,
6
+ Param,
7
+ ParseIntPipe,
8
+ Patch,
9
+ Post,
10
+ Query,
11
+ UseGuards,
12
+ } from '@nestjs/common';
13
+ import {
14
+ ApiBearerAuth,
15
+ ApiCreatedResponse,
16
+ ApiOkResponse,
17
+ ApiOperation,
18
+ ApiParam,
19
+ ApiTags,
20
+ ApiUnprocessableEntityResponse,
21
+ } from '@nestjs/swagger';
22
+ import { ErrorResponseDto } from '../../../common/dto/error-response.dto';
23
+ import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
24
+ import { PermissionGuard } from '../../access-control/application/services/permission.guard';
25
+ import { RequirePermissions } from '../../access-control/presentation/permissions.decorator';
26
+ import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
27
+ import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
28
+ import { FormsDefinitionsService } from '../application/services/forms-definitions.service';
29
+ import { CreateFormDefinitionDto } from '../dto/create-form-definition.dto';
30
+ import {
31
+ FormDefinitionListResponseDto,
32
+ FormDefinitionResponseDto,
33
+ } from '../dto/form-definition-response.dto';
34
+ import { FormRenderResponseDto } from '../dto/form-render-response.dto';
35
+ import { ListFormDefinitionsQueryDto } from '../dto/list-form-definitions-query.dto';
36
+ import { SetPublicAccessDto } from '../dto/set-public-access.dto';
37
+ import { UpdateFormDefinitionDto } from '../dto/update-form-definition.dto';
38
+
39
+ @ApiTags('Forms')
40
+ @ApiBearerAuth()
41
+ @ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
42
+ @ApiProtectedErrorResponses(404, 409)
43
+ @ApiUnprocessableEntityResponse({
44
+ description: 'The form definition failed engine validation or linting.',
45
+ type: ErrorResponseDto,
46
+ })
47
+ @Controller('organisations/:orgId/forms')
48
+ @UseGuards(JwtAuthGuard, OrgScopeGuard, PermissionGuard)
49
+ export class FormsDefinitionsController {
50
+ constructor(private readonly formsDefinitionsService: FormsDefinitionsService) {}
51
+
52
+ @Get()
53
+ @RequirePermissions('forms.read')
54
+ @ApiOperation({ summary: 'List form definitions.' })
55
+ @ApiOkResponse({
56
+ description: 'Form definitions.',
57
+ type: FormDefinitionListResponseDto,
58
+ })
59
+ list(@Param('orgId') orgId: string, @Query() query: ListFormDefinitionsQueryDto) {
60
+ return this.formsDefinitionsService.list(orgId, query);
61
+ }
62
+
63
+ @Post()
64
+ @RequirePermissions('forms.create')
65
+ @ApiOperation({ summary: 'Create a new draft form definition version.' })
66
+ @ApiCreatedResponse({
67
+ description: 'Created draft definition.',
68
+ type: FormDefinitionResponseDto,
69
+ })
70
+ create(@Param('orgId') orgId: string, @Body() dto: CreateFormDefinitionDto) {
71
+ return this.formsDefinitionsService.create(orgId, dto);
72
+ }
73
+
74
+ @Get(':formKey')
75
+ @RequirePermissions('forms.read')
76
+ @ApiParam({ name: 'formKey', description: 'Form definition key.' })
77
+ @ApiOperation({ summary: 'Return the latest version of a form definition.' })
78
+ @ApiOkResponse({
79
+ description: 'Latest definition version.',
80
+ type: FormDefinitionResponseDto,
81
+ })
82
+ getLatest(@Param('orgId') orgId: string, @Param('formKey') formKey: string) {
83
+ return this.formsDefinitionsService.getLatest(orgId, formKey);
84
+ }
85
+
86
+ @Get(':formKey/render')
87
+ @RequirePermissions('formSubmissions.create')
88
+ @ApiParam({ name: 'formKey', description: 'Form definition key.' })
89
+ @ApiOperation({
90
+ summary: 'Return the published definition plus resolved data-source options.',
91
+ })
92
+ @ApiOkResponse({
93
+ description: 'Renderable definition.',
94
+ type: FormRenderResponseDto,
95
+ })
96
+ render(@Param('orgId') orgId: string, @Param('formKey') formKey: string) {
97
+ return this.formsDefinitionsService.render(orgId, formKey);
98
+ }
99
+
100
+ @Get(':formKey/versions/:version')
101
+ @RequirePermissions('forms.read')
102
+ @ApiParam({ name: 'formKey', description: 'Form definition key.' })
103
+ @ApiParam({ name: 'version', description: 'Definition version number.', type: Number })
104
+ @ApiOperation({ summary: 'Return a specific form definition version.' })
105
+ @ApiOkResponse({
106
+ description: 'Definition version.',
107
+ type: FormDefinitionResponseDto,
108
+ })
109
+ getVersion(
110
+ @Param('orgId') orgId: string,
111
+ @Param('formKey') formKey: string,
112
+ @Param('version', ParseIntPipe) version: number,
113
+ ) {
114
+ return this.formsDefinitionsService.getVersion(orgId, formKey, version);
115
+ }
116
+
117
+ @Patch(':formKey/versions/:version')
118
+ @RequirePermissions('forms.update')
119
+ @ApiParam({ name: 'formKey', description: 'Form definition key.' })
120
+ @ApiParam({ name: 'version', description: 'Definition version number.', type: Number })
121
+ @ApiOperation({ summary: 'Update a draft form definition version.' })
122
+ @ApiOkResponse({
123
+ description: 'Updated draft definition.',
124
+ type: FormDefinitionResponseDto,
125
+ })
126
+ update(
127
+ @Param('orgId') orgId: string,
128
+ @Param('formKey') formKey: string,
129
+ @Param('version', ParseIntPipe) version: number,
130
+ @Body() dto: UpdateFormDefinitionDto,
131
+ ) {
132
+ return this.formsDefinitionsService.update(orgId, formKey, version, dto);
133
+ }
134
+
135
+ @Post(':formKey/versions/:version/publish')
136
+ @HttpCode(200)
137
+ @RequirePermissions('forms.publish')
138
+ @ApiParam({ name: 'formKey', description: 'Form definition key.' })
139
+ @ApiParam({ name: 'version', description: 'Definition version number.', type: Number })
140
+ @ApiOperation({ summary: 'Publish a draft form definition version.' })
141
+ @ApiOkResponse({
142
+ description: 'Published definition.',
143
+ type: FormDefinitionResponseDto,
144
+ })
145
+ publish(
146
+ @Param('orgId') orgId: string,
147
+ @Param('formKey') formKey: string,
148
+ @Param('version', ParseIntPipe) version: number,
149
+ ) {
150
+ return this.formsDefinitionsService.publish(orgId, formKey, version);
151
+ }
152
+
153
+ @Post(':formKey/versions/:version/archive')
154
+ @HttpCode(200)
155
+ @RequirePermissions('forms.archive')
156
+ @ApiParam({ name: 'formKey', description: 'Form definition key.' })
157
+ @ApiParam({ name: 'version', description: 'Definition version number.', type: Number })
158
+ @ApiOperation({ summary: 'Archive a form definition version.' })
159
+ @ApiOkResponse({
160
+ description: 'Archived definition.',
161
+ type: FormDefinitionResponseDto,
162
+ })
163
+ archive(
164
+ @Param('orgId') orgId: string,
165
+ @Param('formKey') formKey: string,
166
+ @Param('version', ParseIntPipe) version: number,
167
+ ) {
168
+ return this.formsDefinitionsService.archive(orgId, formKey, version);
169
+ }
170
+
171
+ @Post(':formKey/versions/:version/public-access')
172
+ @HttpCode(200)
173
+ @RequirePermissions('forms.managePublicAccess')
174
+ @ApiParam({ name: 'formKey', description: 'Form definition key.' })
175
+ @ApiParam({ name: 'version', description: 'Definition version number.', type: Number })
176
+ @ApiOperation({
177
+ summary: 'Flip the public/private access setting on a form definition version.',
178
+ })
179
+ @ApiOkResponse({
180
+ description: 'Updated definition.',
181
+ type: FormDefinitionResponseDto,
182
+ })
183
+ setPublicAccess(
184
+ @Param('orgId') orgId: string,
185
+ @Param('formKey') formKey: string,
186
+ @Param('version', ParseIntPipe) version: number,
187
+ @Body() dto: SetPublicAccessDto,
188
+ ) {
189
+ return this.formsDefinitionsService.setPublicAccess(orgId, formKey, version, dto);
190
+ }
191
+ }
@@ -0,0 +1,79 @@
1
+ import {
2
+ Controller,
3
+ Get,
4
+ Param,
5
+ ParseUUIDPipe,
6
+ Post,
7
+ Query,
8
+ UploadedFile,
9
+ UseGuards,
10
+ UseInterceptors,
11
+ } from '@nestjs/common';
12
+ import {
13
+ ApiBearerAuth,
14
+ ApiBody,
15
+ ApiConsumes,
16
+ ApiOkResponse,
17
+ ApiOperation,
18
+ ApiParam,
19
+ ApiTags,
20
+ } from '@nestjs/swagger';
21
+ import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
22
+ import { CurrentUser } from '../../auth/presentation/current-user.decorator';
23
+ import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
24
+ import { AuthenticatedUser } from '../../auth/types/authenticated-user';
25
+ import { PermissionGuard } from '../../access-control/application/services/permission.guard';
26
+ import { RequirePermissions } from '../../access-control/presentation/permissions.decorator';
27
+ import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
28
+ import { FileUploadResponseDto } from '../dto/file-upload-response.dto';
29
+ import { UploadFileQueryDto } from '../dto/upload-file-query.dto';
30
+ import { FormsFilesService } from '../application/services/forms-files.service';
31
+ import { FormsUploadInterceptor } from './forms-upload.interceptor';
32
+
33
+ @ApiTags('Forms')
34
+ @ApiBearerAuth()
35
+ @ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
36
+ @ApiParam({ name: 'formKey', description: 'Form definition key.' })
37
+ @ApiProtectedErrorResponses(404, 409)
38
+ @Controller('organisations/:orgId/forms/:formKey/files')
39
+ @UseGuards(JwtAuthGuard, OrgScopeGuard, PermissionGuard)
40
+ export class FormsFilesController {
41
+ constructor(private readonly filesService: FormsFilesService) {}
42
+
43
+ @Post()
44
+ @RequirePermissions('formSubmissions.create')
45
+ @UseInterceptors(FormsUploadInterceptor)
46
+ @ApiConsumes('multipart/form-data')
47
+ @ApiBody({
48
+ schema: {
49
+ type: 'object',
50
+ properties: { file: { type: 'string', format: 'binary' } },
51
+ required: ['file'],
52
+ },
53
+ })
54
+ @ApiOperation({
55
+ summary:
56
+ 'Upload a file for a form field (before submit). The submission then references the returned fileId.',
57
+ })
58
+ @ApiOkResponse({ description: 'Uploaded file reference.', type: FileUploadResponseDto })
59
+ upload(
60
+ @CurrentUser() user: AuthenticatedUser,
61
+ @Param('orgId') orgId: string,
62
+ @Param('formKey') formKey: string,
63
+ @Query() query: UploadFileQueryDto,
64
+ @UploadedFile() file: { originalname: string; size: number; buffer: Buffer } | undefined,
65
+ ) {
66
+ return this.filesService.upload(orgId, formKey, query, file, user);
67
+ }
68
+
69
+ @Get(':fileId')
70
+ @RequirePermissions('formSubmissions.read')
71
+ @ApiOperation({ summary: 'Return metadata for an uploaded file.' })
72
+ @ApiOkResponse({ description: 'Uploaded file metadata.', type: FileUploadResponseDto })
73
+ getMeta(
74
+ @Param('orgId') orgId: string,
75
+ @Param('fileId', ParseUUIDPipe) fileId: string,
76
+ ) {
77
+ return this.filesService.getMeta(orgId, fileId);
78
+ }
79
+ }
@@ -0,0 +1,154 @@
1
+ import {
2
+ Body,
3
+ Controller,
4
+ Get,
5
+ Header,
6
+ HttpCode,
7
+ Param,
8
+ ParseUUIDPipe,
9
+ Post,
10
+ Query,
11
+ Res,
12
+ UseGuards,
13
+ } from '@nestjs/common';
14
+ import type { Response } from 'express';
15
+ import {
16
+ ApiBearerAuth,
17
+ ApiOkResponse,
18
+ ApiOperation,
19
+ ApiParam,
20
+ ApiTags,
21
+ ApiUnprocessableEntityResponse,
22
+ } from '@nestjs/swagger';
23
+ import { Throttle } from '@nestjs/throttler';
24
+ import { ErrorResponseDto } from '../../../common/dto/error-response.dto';
25
+ import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
26
+ import { PermissionGuard } from '../../access-control/application/services/permission.guard';
27
+ import { RequirePermissions } from '../../access-control/presentation/permissions.decorator';
28
+ import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
29
+ import { CurrentUser } from '../../auth/presentation/current-user.decorator';
30
+ import { AuthenticatedUser } from '../../auth/types/authenticated-user';
31
+ import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
32
+ import { FormsExportService } from '../application/services/forms-export.service';
33
+ import { FormsSubmissionsService } from '../application/services/forms-submissions.service';
34
+ import { ExportSubmissionsQueryDto } from '../dto/export-submissions-query.dto';
35
+ import { ListSubmissionsQueryDto } from '../dto/list-submissions-query.dto';
36
+ import {
37
+ SubmissionListResponseDto,
38
+ SubmissionResponseDto,
39
+ SubmitResultResponseDto,
40
+ ValidationResultResponseDto,
41
+ } from '../dto/submission-response.dto';
42
+ import { SubmitFormDto } from '../dto/submit-form.dto';
43
+ import { ValidateSubmissionDto } from '../dto/validate-submission.dto';
44
+
45
+ @ApiTags('Forms')
46
+ @ApiBearerAuth()
47
+ @ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
48
+ @ApiParam({ name: 'formKey', description: 'Form definition key.' })
49
+ @ApiProtectedErrorResponses(404, 409)
50
+ @ApiUnprocessableEntityResponse({
51
+ description: 'The submission data failed engine validation.',
52
+ type: ErrorResponseDto,
53
+ })
54
+ @Controller('organisations/:orgId/forms/:formKey/submissions')
55
+ @UseGuards(JwtAuthGuard, OrgScopeGuard, PermissionGuard)
56
+ export class FormsSubmissionsController {
57
+ constructor(
58
+ private readonly formsSubmissionsService: FormsSubmissionsService,
59
+ private readonly formsExportService: FormsExportService,
60
+ ) {}
61
+
62
+ @Post()
63
+ @HttpCode(200)
64
+ @Throttle({ default: { limit: 30, ttl: 60_000 } })
65
+ @RequirePermissions('formSubmissions.create')
66
+ @ApiOperation({ summary: 'Submit form data or save a draft submission.' })
67
+ @ApiOkResponse({
68
+ description: 'Pipeline result.',
69
+ type: SubmitResultResponseDto,
70
+ })
71
+ submit(
72
+ @CurrentUser() user: AuthenticatedUser,
73
+ @Param('orgId') orgId: string,
74
+ @Param('formKey') formKey: string,
75
+ @Body() dto: SubmitFormDto,
76
+ ) {
77
+ return this.formsSubmissionsService.submit(orgId, formKey, dto, user);
78
+ }
79
+
80
+ @Post('validate')
81
+ @HttpCode(200)
82
+ @RequirePermissions('formSubmissions.create')
83
+ @ApiOperation({ summary: 'Validate submission data without side effects.' })
84
+ @ApiOkResponse({
85
+ description: 'Validation result.',
86
+ type: ValidationResultResponseDto,
87
+ })
88
+ validate(
89
+ @Param('orgId') orgId: string,
90
+ @Param('formKey') formKey: string,
91
+ @Body() dto: ValidateSubmissionDto,
92
+ ) {
93
+ return this.formsSubmissionsService.validate(orgId, formKey, dto);
94
+ }
95
+
96
+ @Get()
97
+ @RequirePermissions('formSubmissions.read')
98
+ @ApiOperation({ summary: 'List submissions for a form.' })
99
+ @ApiOkResponse({
100
+ description: 'Form submissions.',
101
+ type: SubmissionListResponseDto,
102
+ })
103
+ list(
104
+ @Param('orgId') orgId: string,
105
+ @Param('formKey') formKey: string,
106
+ @Query() query: ListSubmissionsQueryDto,
107
+ ) {
108
+ return this.formsSubmissionsService.list(orgId, formKey, query);
109
+ }
110
+
111
+ // Declared before GET :submissionId so the literal segment wins.
112
+ @Get('export')
113
+ @RequirePermissions('formSubmissions.export')
114
+ @Header('Cache-Control', 'no-store')
115
+ @ApiOperation({
116
+ summary:
117
+ 'Export submissions (PII egress — separately permissioned and always audited). Defaults to reportable fields.',
118
+ })
119
+ @ApiOkResponse({ description: 'Export payload (JSON object or CSV body).' })
120
+ async export(
121
+ @CurrentUser() user: AuthenticatedUser,
122
+ @Param('orgId') orgId: string,
123
+ @Param('formKey') formKey: string,
124
+ @Query() query: ExportSubmissionsQueryDto,
125
+ @Res({ passthrough: true }) res: Response,
126
+ ) {
127
+ const result = await this.formsExportService.export(orgId, formKey, query, user);
128
+ if (result.format === 'csv') {
129
+ res.setHeader('Content-Type', 'text/csv; charset=utf-8');
130
+ res.setHeader(
131
+ 'Content-Disposition',
132
+ `attachment; filename="${formKey}-submissions.csv"`,
133
+ );
134
+ return result.csv;
135
+ }
136
+ return { columns: result.columns, rows: result.rows };
137
+ }
138
+
139
+ @Get(':submissionId')
140
+ @RequirePermissions('formSubmissions.read')
141
+ @ApiParam({
142
+ name: 'submissionId',
143
+ description: 'Submission ID.',
144
+ format: 'uuid',
145
+ })
146
+ @ApiOperation({ summary: 'Return a single submission.' })
147
+ @ApiOkResponse({
148
+ description: 'Submission.',
149
+ type: SubmissionResponseDto,
150
+ })
151
+ get(@Param('orgId') orgId: string, @Param('submissionId', ParseUUIDPipe) submissionId: string) {
152
+ return this.formsSubmissionsService.get(orgId, submissionId);
153
+ }
154
+ }
@@ -0,0 +1,33 @@
1
+ import {
2
+ CallHandler,
3
+ ExecutionContext,
4
+ Injectable,
5
+ NestInterceptor,
6
+ } from '@nestjs/common';
7
+ import { ConfigService } from '@nestjs/config';
8
+ import { FileInterceptor } from '@nestjs/platform-express';
9
+ import { memoryStorage } from 'multer';
10
+ import type { Observable } from 'rxjs';
11
+
12
+ @Injectable()
13
+ export class FormsUploadInterceptor implements NestInterceptor {
14
+ private readonly delegate: NestInterceptor;
15
+
16
+ constructor(config: ConfigService) {
17
+ const maxUploadMb = config.get<number>('forms.maxUploadMb') ?? 25;
18
+ const Interceptor = FileInterceptor('file', {
19
+ storage: memoryStorage(),
20
+ limits: { fileSize: maxUploadMb * 1024 * 1024 },
21
+ });
22
+ this.delegate = new Interceptor();
23
+ }
24
+
25
+ intercept(
26
+ context: ExecutionContext,
27
+ next: CallHandler,
28
+ ): Observable<unknown> | Promise<Observable<unknown>> {
29
+ return this.delegate.intercept(context, next) as
30
+ | Observable<unknown>
31
+ | Promise<Observable<unknown>>;
32
+ }
33
+ }
@@ -0,0 +1,51 @@
1
+ import { Body, Controller, Get, HttpCode, Param, Post, UseGuards } from '@nestjs/common';
2
+ import { ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
3
+ import { Throttle } from '@nestjs/throttler';
4
+ import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
5
+ import { Public } from '../../access-control/presentation/public.decorator';
6
+ import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
7
+ import { FormsPublicService } from '../application/services/forms-public.service';
8
+ import { FormRenderResponseDto } from '../dto/form-render-response.dto';
9
+ import { PublicSubmitFormDto } from '../dto/public-submit-form.dto';
10
+ import { SubmitResultResponseDto } from '../dto/submission-response.dto';
11
+
12
+ /**
13
+ * Anonymous endpoints for definitions whose settings.access === 'public'
14
+ * (flipping that setting is gated by forms.managePublicAccess and audited).
15
+ * Like the auth routes, these carry no JwtAuthGuard/PermissionGuard and are
16
+ * NOT in the route-permission registry; they are throttled tighter than the
17
+ * app default, and the engine enforces the remaining Tier-1 abuse controls
18
+ * (per-IP/day caps, captcha, ip/ua stamping).
19
+ */
20
+ @ApiTags('Forms (public)')
21
+ @ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
22
+ @ApiParam({ name: 'formKey', description: 'Form definition key.' })
23
+ @ApiProtectedErrorResponses(404, 429)
24
+ @Controller('public/organisations/:orgId/forms/:formKey')
25
+ @UseGuards(OrgScopeGuard)
26
+ export class PublicFormsController {
27
+ constructor(private readonly publicService: FormsPublicService) {}
28
+
29
+ @Get('render')
30
+ @Public()
31
+ @Throttle({ default: { limit: 30, ttl: 60_000 } })
32
+ @ApiOperation({ summary: 'Render a public form (definition + resolved options).' })
33
+ @ApiOkResponse({ description: 'Renderable definition.', type: FormRenderResponseDto })
34
+ render(@Param('orgId') orgId: string, @Param('formKey') formKey: string) {
35
+ return this.publicService.render(orgId, formKey);
36
+ }
37
+
38
+ @Post('submissions')
39
+ @Public()
40
+ @HttpCode(200)
41
+ @Throttle({ default: { limit: 10, ttl: 60_000 } })
42
+ @ApiOperation({ summary: 'Submit a public form anonymously.' })
43
+ @ApiOkResponse({ description: 'Submission result.', type: SubmitResultResponseDto })
44
+ submit(
45
+ @Param('orgId') orgId: string,
46
+ @Param('formKey') formKey: string,
47
+ @Body() dto: PublicSubmitFormDto,
48
+ ) {
49
+ return this.publicService.submit(orgId, formKey, dto);
50
+ }
51
+ }
@@ -48,6 +48,7 @@ export class InvitationResponseDto {
48
48
  invitedByUserId!: string;
49
49
 
50
50
  @ApiPropertyOptional({
51
+ type: String,
51
52
  example: '7efd9a26-fbec-4511-ae37-7fb1c35e5215',
52
53
  format: 'uuid',
53
54
  nullable: true,
@@ -67,6 +68,7 @@ export class InvitationResponseDto {
67
68
  createdAt!: string;
68
69
 
69
70
  @ApiPropertyOptional({
71
+ type: String,
70
72
  example: '2026-06-01T10:35:00.000Z',
71
73
  format: 'date-time',
72
74
  nullable: true,
@@ -74,6 +76,7 @@ export class InvitationResponseDto {
74
76
  acceptedAt?: string | null;
75
77
 
76
78
  @ApiPropertyOptional({
79
+ type: String,
77
80
  example: '2026-06-01T10:40:00.000Z',
78
81
  format: 'date-time',
79
82
  nullable: true,
@@ -97,6 +100,7 @@ export class InvitationListResponseDto {
97
100
  items!: InvitationResponseDto[];
98
101
 
99
102
  @ApiPropertyOptional({
103
+ type: String,
100
104
  example: '17a21ad7-bd1b-42d8-b809-0a3892bb60c3',
101
105
  format: 'uuid',
102
106
  nullable: true,
@@ -1,5 +1,10 @@
1
1
  import { ConflictException, Injectable } from '@nestjs/common';
2
- import { Prisma } from '@prisma/client';
2
+ import { MembershipStatus, OrganisationStatus, Prisma } from '@prisma/client';
3
+ import {
4
+ PaginationQueryDto,
5
+ resolvePageLimit,
6
+ toPage,
7
+ } from '../../../../common/dto/pagination-query.dto';
3
8
  import { PrismaService } from '../../../../database/prisma/prisma.service';
4
9
  import { RequestContextService } from '../../../request-context/application/services/request-context.service';
5
10
  import { AuthenticatedUser } from '../../../auth/types/authenticated-user';
@@ -18,6 +23,67 @@ export class OrganisationsService {
18
23
  private readonly requestContext: RequestContextService,
19
24
  ) {}
20
25
 
26
+ async listMine(currentUser: AuthenticatedUser, query: PaginationQueryDto) {
27
+ const limit = resolvePageLimit(query.limit);
28
+
29
+ const rows = await this.prisma.membership.findMany({
30
+ where: {
31
+ userId: currentUser.id,
32
+ status: MembershipStatus.ACTIVE,
33
+ organisation: {
34
+ status: OrganisationStatus.ACTIVE,
35
+ },
36
+ },
37
+ select: {
38
+ id: true,
39
+ roleId: true,
40
+ isOwner: true,
41
+ isBillingContact: true,
42
+ organisation: {
43
+ select: {
44
+ id: true,
45
+ name: true,
46
+ slug: true,
47
+ status: true,
48
+ },
49
+ },
50
+ role: {
51
+ select: {
52
+ id: true,
53
+ name: true,
54
+ description: true,
55
+ isSystemSeeded: true,
56
+ },
57
+ },
58
+ },
59
+ orderBy: [{ isOwner: 'desc' }, { createdAt: 'asc' }, { id: 'asc' }],
60
+ take: limit + 1,
61
+ ...(query.cursor
62
+ ? {
63
+ cursor: { id: query.cursor },
64
+ skip: 1,
65
+ }
66
+ : {}),
67
+ });
68
+
69
+ const page = toPage(rows, limit);
70
+
71
+ return {
72
+ items: page.items.map((membership) => ({
73
+ id: membership.organisation.id,
74
+ name: membership.organisation.name,
75
+ slug: membership.organisation.slug,
76
+ status: membership.organisation.status,
77
+ membershipId: membership.id,
78
+ roleId: membership.roleId,
79
+ role: membership.role,
80
+ isOwner: membership.isOwner,
81
+ isBillingContact: membership.isBillingContact,
82
+ })),
83
+ nextCursor: page.nextCursor,
84
+ };
85
+ }
86
+
21
87
  async createOrganisation(currentUser: AuthenticatedUser, dto: CreateOrganisationDto) {
22
88
  const name = dto.name.trim();
23
89
  const slug = dto.slug?.trim() || (await this.createAvailableSlug(name));
@@ -1,6 +1,7 @@
1
1
  import { ApiProperty } from '@nestjs/swagger';
2
2
  import { OrganisationStatus } from '@prisma/client';
3
3
  import { MembershipResponseDto } from '../../../common/dto/membership-response.dto';
4
+ import { RoleSummaryDto } from '../../../common/dto/role-summary.dto';
4
5
 
5
6
  export class OrganisationResponseDto {
6
7
  @ApiProperty({
@@ -60,3 +61,54 @@ export class CreateOrganisationResponseDto {
60
61
  @ApiProperty({ type: [SeededOrganisationSettingDto] })
61
62
  settings!: SeededOrganisationSettingDto[];
62
63
  }
64
+
65
+ export class MyOrganisationResponseDto {
66
+ @ApiProperty({
67
+ example: '2c67399d-670c-4025-a5fd-1ea9a211891e',
68
+ format: 'uuid',
69
+ })
70
+ id!: string;
71
+
72
+ @ApiProperty({ example: 'Acme Operations' })
73
+ name!: string;
74
+
75
+ @ApiProperty({ example: 'acme-operations' })
76
+ slug!: string;
77
+
78
+ @ApiProperty({ enum: OrganisationStatus, example: OrganisationStatus.ACTIVE })
79
+ status!: OrganisationStatus;
80
+
81
+ @ApiProperty({
82
+ example: '0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3',
83
+ format: 'uuid',
84
+ })
85
+ membershipId!: string;
86
+
87
+ @ApiProperty({
88
+ example: 'f602c057-04f4-4ef8-8c84-1b7c62fbf8c5',
89
+ format: 'uuid',
90
+ })
91
+ roleId!: string;
92
+
93
+ @ApiProperty({ type: RoleSummaryDto })
94
+ role!: RoleSummaryDto;
95
+
96
+ @ApiProperty({ example: true })
97
+ isOwner!: boolean;
98
+
99
+ @ApiProperty({ example: true })
100
+ isBillingContact!: boolean;
101
+ }
102
+
103
+ export class MyOrganisationListResponseDto {
104
+ @ApiProperty({ type: [MyOrganisationResponseDto] })
105
+ items!: MyOrganisationResponseDto[];
106
+
107
+ @ApiProperty({
108
+ type: String,
109
+ example: '2c67399d-670c-4025-a5fd-1ea9a211891e',
110
+ format: 'uuid',
111
+ nullable: true,
112
+ })
113
+ nextCursor!: string | null;
114
+ }