@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,156 @@
1
+ import {
2
+ Body,
3
+ Controller,
4
+ Get,
5
+ HttpCode,
6
+ Param,
7
+ Patch,
8
+ Post,
9
+ Query,
10
+ UseGuards,
11
+ } from '@nestjs/common';
12
+ import {
13
+ ApiBearerAuth,
14
+ ApiCreatedResponse,
15
+ ApiOkResponse,
16
+ ApiOperation,
17
+ ApiParam,
18
+ ApiTags,
19
+ ApiUnprocessableEntityResponse,
20
+ } from '@nestjs/swagger';
21
+ import { ErrorResponseDto } from '../../../common/dto/error-response.dto';
22
+ import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
23
+ import { PermissionGuard } from '../../access-control/application/services/permission.guard';
24
+ import { RequirePermissions } from '../../access-control/presentation/permissions.decorator';
25
+ import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
26
+ import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
27
+ import { ReportsDefinitionsService } from '../application/services/reports-definitions.service';
28
+ import { ReportsQueriesService } from '../application/services/reports-queries.service';
29
+ import { CreateReportDefinitionDto } from '../dto/create-report-definition.dto';
30
+ import { ListReportsQueryDto } from '../dto/list-reports-query.dto';
31
+ import {
32
+ ReportArchivedResponseDto,
33
+ ReportDefinitionListResponseDto,
34
+ ReportDefinitionResponseDto,
35
+ } from '../dto/report-definition-response.dto';
36
+ import { ReportMetaQueryDto } from '../dto/report-meta-query.dto';
37
+ import { ReportMetaResponseDto } from '../dto/report-meta-response.dto';
38
+ import { UpdateReportDefinitionDto } from '../dto/update-report-definition.dto';
39
+
40
+ @ApiTags('Reports')
41
+ @ApiBearerAuth()
42
+ @ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
43
+ @ApiProtectedErrorResponses(404, 409)
44
+ @ApiUnprocessableEntityResponse({
45
+ description: 'The report definition failed engine validation or linting (§5.2/§8).',
46
+ type: ErrorResponseDto,
47
+ })
48
+ @Controller('organisations/:orgId/reports')
49
+ @UseGuards(JwtAuthGuard, OrgScopeGuard, PermissionGuard)
50
+ export class ReportsDefinitionsController {
51
+ constructor(
52
+ private readonly reportsDefinitionsService: ReportsDefinitionsService,
53
+ private readonly reportsQueriesService: ReportsQueriesService,
54
+ ) {}
55
+
56
+ @Get()
57
+ @RequirePermissions('reports.read')
58
+ @ApiOperation({ summary: 'List report definitions.' })
59
+ @ApiOkResponse({
60
+ description: 'Report definitions.',
61
+ type: ReportDefinitionListResponseDto,
62
+ })
63
+ list(@Param('orgId') orgId: string, @Query() query: ListReportsQueryDto) {
64
+ return this.reportsDefinitionsService.list(orgId, query);
65
+ }
66
+
67
+ @Post()
68
+ @RequirePermissions('reports.create')
69
+ @ApiOperation({ summary: 'Create a new draft report definition version.' })
70
+ @ApiCreatedResponse({
71
+ description: 'Created draft definition.',
72
+ type: ReportDefinitionResponseDto,
73
+ })
74
+ create(@Param('orgId') orgId: string, @Body() dto: CreateReportDefinitionDto) {
75
+ return this.reportsDefinitionsService.create(orgId, dto);
76
+ }
77
+
78
+ @Get(':key')
79
+ @RequirePermissions('reports.read')
80
+ @ApiParam({ name: 'key', description: 'Report definition key.' })
81
+ @ApiOperation({ summary: 'Return the latest version of a report definition.' })
82
+ @ApiOkResponse({
83
+ description: 'Latest definition version.',
84
+ type: ReportDefinitionResponseDto,
85
+ })
86
+ getLatest(@Param('orgId') orgId: string, @Param('key') key: string) {
87
+ return this.reportsDefinitionsService.getLatest(orgId, key);
88
+ }
89
+
90
+ @Patch(':key')
91
+ @RequirePermissions('reports.update')
92
+ @ApiParam({ name: 'key', description: 'Report definition key.' })
93
+ @ApiOperation({
94
+ summary:
95
+ 'Edit the draft. Editing a published version opens a new draft version (published rows are immutable, §2).',
96
+ })
97
+ @ApiOkResponse({
98
+ description: 'Updated draft definition.',
99
+ type: ReportDefinitionResponseDto,
100
+ })
101
+ update(
102
+ @Param('orgId') orgId: string,
103
+ @Param('key') key: string,
104
+ @Body() dto: UpdateReportDefinitionDto,
105
+ ) {
106
+ return this.reportsDefinitionsService.update(orgId, key, dto);
107
+ }
108
+
109
+ @Post(':key/publish')
110
+ @HttpCode(200)
111
+ @RequirePermissions('reports.publish')
112
+ @ApiParam({ name: 'key', description: 'Report definition key.' })
113
+ @ApiOperation({
114
+ summary:
115
+ 'Publish the latest draft: the §5.2 layered lint runs here, failing constructively with the suggested migration SQL (§8).',
116
+ })
117
+ @ApiOkResponse({
118
+ description: 'Published definition.',
119
+ type: ReportDefinitionResponseDto,
120
+ })
121
+ publish(@Param('orgId') orgId: string, @Param('key') key: string) {
122
+ return this.reportsDefinitionsService.publish(orgId, key);
123
+ }
124
+
125
+ @Post(':key/archive')
126
+ @HttpCode(200)
127
+ @RequirePermissions('reports.archive')
128
+ @ApiParam({ name: 'key', description: 'Report definition key.' })
129
+ @ApiOperation({ summary: 'Archive every version of a report definition.' })
130
+ @ApiOkResponse({
131
+ description: 'Archive confirmation.',
132
+ type: ReportArchivedResponseDto,
133
+ })
134
+ archive(@Param('orgId') orgId: string, @Param('key') key: string) {
135
+ return this.reportsDefinitionsService.archive(orgId, key);
136
+ }
137
+
138
+ @Get(':key/meta')
139
+ @RequirePermissions('reports.read')
140
+ @ApiParam({ name: 'key', description: 'Report definition key.' })
141
+ @ApiOperation({
142
+ summary:
143
+ 'Return the self-describing grid descriptor: columns, typed operators, actions, export config (§4).',
144
+ })
145
+ @ApiOkResponse({
146
+ description: 'Report metadata descriptor.',
147
+ type: ReportMetaResponseDto,
148
+ })
149
+ meta(
150
+ @Param('orgId') orgId: string,
151
+ @Param('key') key: string,
152
+ @Query() query: ReportMetaQueryDto,
153
+ ) {
154
+ return this.reportsQueriesService.meta(orgId, key, query.version ?? null);
155
+ }
156
+ }
@@ -0,0 +1,61 @@
1
+ import { Controller, Get, Param, ParseUUIDPipe, StreamableFile, UseGuards } from '@nestjs/common';
2
+ import {
3
+ ApiBearerAuth,
4
+ ApiOkResponse,
5
+ ApiOperation,
6
+ ApiParam,
7
+ ApiProduces,
8
+ ApiTags,
9
+ } from '@nestjs/swagger';
10
+ import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
11
+ import { PermissionGuard } from '../../access-control/application/services/permission.guard';
12
+ import { RequirePermissions } from '../../access-control/presentation/permissions.decorator';
13
+ import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
14
+ import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
15
+ import { ReportsExportsService } from '../application/services/reports-exports.service';
16
+ import { ExportJobResponseDto } from '../dto/export-job-response.dto';
17
+
18
+ /**
19
+ * MUST be registered BEFORE ReportsDefinitionsController in the module's
20
+ * controllers array: both controllers share the 'organisations/:orgId/reports'
21
+ * base path, and the literal 'exports' segment has to win over the ':key'
22
+ * parameter route ('exports' is a reserved report key for the same reason).
23
+ */
24
+ @ApiTags('Reports')
25
+ @ApiBearerAuth()
26
+ @ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
27
+ @ApiProtectedErrorResponses(404)
28
+ @Controller('organisations/:orgId/reports/exports')
29
+ @UseGuards(JwtAuthGuard, OrgScopeGuard, PermissionGuard)
30
+ export class ReportsExportJobsController {
31
+ constructor(private readonly reportsExportsService: ReportsExportsService) {}
32
+
33
+ @Get(':jobId')
34
+ @RequirePermissions('reports.export')
35
+ @ApiParam({ name: 'jobId', description: 'Export job ID.', format: 'uuid' })
36
+ @ApiOperation({ summary: 'Return the status of an async export job (§9/§10).' })
37
+ @ApiOkResponse({
38
+ description: 'Export job status.',
39
+ type: ExportJobResponseDto,
40
+ })
41
+ getJob(@Param('orgId') orgId: string, @Param('jobId', ParseUUIDPipe) jobId: string) {
42
+ return this.reportsExportsService.getJob(orgId, jobId);
43
+ }
44
+
45
+ @Get(':jobId/download')
46
+ @RequirePermissions('reports.export')
47
+ @ApiParam({ name: 'jobId', description: 'Export job ID.', format: 'uuid' })
48
+ @ApiOperation({ summary: 'Download a finished async export file (§9).' })
49
+ @ApiProduces('text/csv', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
50
+ @ApiOkResponse({ description: 'The export file stream.' })
51
+ async download(
52
+ @Param('orgId') orgId: string,
53
+ @Param('jobId', ParseUUIDPipe) jobId: string,
54
+ ): Promise<StreamableFile> {
55
+ const file = await this.reportsExportsService.download(orgId, jobId);
56
+ return new StreamableFile(file.stream, {
57
+ type: file.mimeType,
58
+ disposition: `attachment; filename="${file.fileName}"`,
59
+ });
60
+ }
61
+ }
@@ -0,0 +1,76 @@
1
+ import {
2
+ Body,
3
+ Controller,
4
+ Header,
5
+ HttpCode,
6
+ Param,
7
+ Post,
8
+ Res,
9
+ StreamableFile,
10
+ UseGuards,
11
+ } from '@nestjs/common';
12
+ import { Readable } from 'node:stream';
13
+ import type { Response } from 'express';
14
+ import {
15
+ ApiAcceptedResponse,
16
+ ApiBearerAuth,
17
+ ApiOkResponse,
18
+ ApiOperation,
19
+ ApiParam,
20
+ ApiTags,
21
+ ApiUnprocessableEntityResponse,
22
+ } from '@nestjs/swagger';
23
+ import { ErrorResponseDto } from '../../../common/dto/error-response.dto';
24
+ import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
25
+ import { PermissionGuard } from '../../access-control/application/services/permission.guard';
26
+ import { RequirePermissions } from '../../access-control/presentation/permissions.decorator';
27
+ import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
28
+ import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
29
+ import { ReportsExportsService } from '../application/services/reports-exports.service';
30
+ import { ExportJobResponseDto } from '../dto/export-job-response.dto';
31
+ import { ExportRequestDto } from '../dto/export-request.dto';
32
+
33
+ @ApiTags('Reports')
34
+ @ApiBearerAuth()
35
+ @ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
36
+ @ApiProtectedErrorResponses(404)
37
+ @ApiUnprocessableEntityResponse({
38
+ description: 'The export was rejected (snapshot bound exceeded, no exportable columns, §9).',
39
+ type: ErrorResponseDto,
40
+ })
41
+ @Controller('organisations/:orgId/reports')
42
+ @UseGuards(JwtAuthGuard, OrgScopeGuard, PermissionGuard)
43
+ export class ReportsExportController {
44
+ constructor(private readonly reportsExportsService: ReportsExportsService) {}
45
+
46
+ @Post(':key/export')
47
+ @HttpCode(200)
48
+ @Header('Cache-Control', 'no-store')
49
+ @RequirePermissions('reports.export')
50
+ @ApiParam({ name: 'key', description: 'Report definition key.' })
51
+ @ApiOperation({
52
+ summary:
53
+ 'Export report rows (PII egress — separately permissioned, always audited): sync stream up to the cap, 202 job above it (§9).',
54
+ })
55
+ @ApiOkResponse({ description: 'The export file stream (CSV or XLSX).' })
56
+ @ApiAcceptedResponse({
57
+ description: 'The selection exceeds the sync cap; an async export job was queued.',
58
+ type: ExportJobResponseDto,
59
+ })
60
+ async export(
61
+ @Param('orgId') orgId: string,
62
+ @Param('key') key: string,
63
+ @Body() dto: ExportRequestDto,
64
+ @Res({ passthrough: true }) res: Response,
65
+ ) {
66
+ const outcome = await this.reportsExportsService.requestExport(orgId, key, dto);
67
+ if (outcome.kind === 'stream') {
68
+ return new StreamableFile(Readable.from(outcome.stream), {
69
+ type: outcome.mimeType,
70
+ disposition: `attachment; filename="${outcome.fileName}"`,
71
+ });
72
+ }
73
+ res.status(202);
74
+ return outcome.job;
75
+ }
76
+ }
@@ -0,0 +1,52 @@
1
+ import { Body, Controller, HttpCode, Param, Post, UseGuards } from '@nestjs/common';
2
+ import {
3
+ ApiBearerAuth,
4
+ ApiOkResponse,
5
+ ApiOperation,
6
+ ApiParam,
7
+ ApiRequestTimeoutResponse,
8
+ ApiTags,
9
+ } from '@nestjs/swagger';
10
+ import { ErrorResponseDto } from '../../../common/dto/error-response.dto';
11
+ import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
12
+ import { PermissionGuard } from '../../access-control/application/services/permission.guard';
13
+ import { RequirePermissions } from '../../access-control/presentation/permissions.decorator';
14
+ import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
15
+ import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
16
+ import { ReportsQueriesService } from '../application/services/reports-queries.service';
17
+ import { QueryResponseDto } from '../dto/query-response.dto';
18
+ import { QuerySpecDto } from '../dto/query-spec.dto';
19
+
20
+ @ApiTags('Reports')
21
+ @ApiBearerAuth()
22
+ @ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
23
+ @ApiProtectedErrorResponses(404)
24
+ @ApiRequestTimeoutResponse({
25
+ description: 'The report query exceeded its statement budget (§5.2).',
26
+ type: ErrorResponseDto,
27
+ })
28
+ @Controller('organisations/:orgId/reports')
29
+ @UseGuards(JwtAuthGuard, OrgScopeGuard, PermissionGuard)
30
+ export class ReportsQueryController {
31
+ constructor(private readonly reportsQueriesService: ReportsQueriesService) {}
32
+
33
+ @Post(':key/query')
34
+ @HttpCode(200)
35
+ @RequirePermissions('reports.read')
36
+ @ApiParam({ name: 'key', description: 'Report definition key.' })
37
+ @ApiOperation({
38
+ summary:
39
+ 'THE grid endpoint (§4): one declarative QuerySpec for filters, search, sort, keyset paging, counts, and projection.',
40
+ })
41
+ @ApiOkResponse({
42
+ description: 'Query result page.',
43
+ type: QueryResponseDto,
44
+ })
45
+ query(
46
+ @Param('orgId') orgId: string,
47
+ @Param('key') key: string,
48
+ @Body() spec: QuerySpecDto,
49
+ ) {
50
+ return this.reportsQueriesService.query(orgId, key, spec);
51
+ }
52
+ }
@@ -0,0 +1,140 @@
1
+ import {
2
+ Body,
3
+ Controller,
4
+ Delete,
5
+ Get,
6
+ Param,
7
+ ParseUUIDPipe,
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
+ } from '@nestjs/swagger';
21
+ import { DeletedResponseDto } from '../../../common/dto/mutation-response.dto';
22
+ import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
23
+ import { PermissionGuard } from '../../access-control/application/services/permission.guard';
24
+ import { RequirePermissions } from '../../access-control/presentation/permissions.decorator';
25
+ import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
26
+ import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
27
+ import { ReportsViewsService } from '../application/services/reports-views.service';
28
+ import { CreateSavedViewDto } from '../dto/create-saved-view.dto';
29
+ import { ListViewsQueryDto } from '../dto/list-views-query.dto';
30
+ import {
31
+ SavedViewListResponseDto,
32
+ SavedViewResponseDto,
33
+ } from '../dto/saved-view-response.dto';
34
+ import { UpdateSavedViewDto } from '../dto/update-saved-view.dto';
35
+
36
+ /**
37
+ * Saved views (design §4, finding #9): personal views need reports.read;
38
+ * creating a SHARED view (ownerId null) needs reports.update — and the engine
39
+ * re-checks reports.update on every mutation of a shared view, whatever the
40
+ * route-level key says.
41
+ */
42
+ @ApiTags('Reports')
43
+ @ApiBearerAuth()
44
+ @ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
45
+ @ApiProtectedErrorResponses(404, 409)
46
+ @Controller('organisations/:orgId/reports')
47
+ @UseGuards(JwtAuthGuard, OrgScopeGuard, PermissionGuard)
48
+ export class ReportsViewsController {
49
+ constructor(private readonly reportsViewsService: ReportsViewsService) {}
50
+
51
+ @Get(':key/views')
52
+ @RequirePermissions('reports.read')
53
+ @ApiParam({ name: 'key', description: 'Report definition key.' })
54
+ @ApiOperation({
55
+ summary:
56
+ 'List saved views (personal + shared), each with a compatibility verdict against the requested version (§4).',
57
+ })
58
+ @ApiOkResponse({
59
+ description: 'Saved views with compatibility verdicts.',
60
+ type: SavedViewListResponseDto,
61
+ })
62
+ list(
63
+ @Param('orgId') orgId: string,
64
+ @Param('key') key: string,
65
+ @Query() query: ListViewsQueryDto,
66
+ ) {
67
+ return this.reportsViewsService.list(orgId, key, query);
68
+ }
69
+
70
+ @Post(':key/views')
71
+ @RequirePermissions('reports.read')
72
+ @ApiParam({ name: 'key', description: 'Report definition key.' })
73
+ @ApiOperation({ summary: 'Create a PERSONAL saved view.' })
74
+ @ApiCreatedResponse({
75
+ description: 'Created view.',
76
+ type: SavedViewResponseDto,
77
+ })
78
+ createPersonal(
79
+ @Param('orgId') orgId: string,
80
+ @Param('key') key: string,
81
+ @Body() dto: CreateSavedViewDto,
82
+ ) {
83
+ return this.reportsViewsService.createPersonal(orgId, key, dto);
84
+ }
85
+
86
+ @Post(':key/views/shared')
87
+ @RequirePermissions('reports.update')
88
+ @ApiParam({ name: 'key', description: 'Report definition key.' })
89
+ @ApiOperation({ summary: 'Create a SHARED (org-wide) saved view (finding #9).' })
90
+ @ApiCreatedResponse({
91
+ description: 'Created shared view.',
92
+ type: SavedViewResponseDto,
93
+ })
94
+ createShared(
95
+ @Param('orgId') orgId: string,
96
+ @Param('key') key: string,
97
+ @Body() dto: CreateSavedViewDto,
98
+ ) {
99
+ return this.reportsViewsService.createShared(orgId, key, dto);
100
+ }
101
+
102
+ @Patch(':key/views/:viewId')
103
+ @RequirePermissions('reports.read')
104
+ @ApiParam({ name: 'key', description: 'Report definition key.' })
105
+ @ApiParam({ name: 'viewId', description: 'Saved view ID.', format: 'uuid' })
106
+ @ApiOperation({
107
+ summary: 'Rename / respec a saved view. Personal: owner only. Shared: reports.update.',
108
+ })
109
+ @ApiOkResponse({
110
+ description: 'Updated view.',
111
+ type: SavedViewResponseDto,
112
+ })
113
+ update(
114
+ @Param('orgId') orgId: string,
115
+ @Param('key') key: string,
116
+ @Param('viewId', ParseUUIDPipe) viewId: string,
117
+ @Body() dto: UpdateSavedViewDto,
118
+ ) {
119
+ return this.reportsViewsService.update(orgId, key, viewId, dto);
120
+ }
121
+
122
+ @Delete(':key/views/:viewId')
123
+ @RequirePermissions('reports.read')
124
+ @ApiParam({ name: 'key', description: 'Report definition key.' })
125
+ @ApiParam({ name: 'viewId', description: 'Saved view ID.', format: 'uuid' })
126
+ @ApiOperation({
127
+ summary: 'Delete a saved view. Personal: owner only. Shared: reports.update.',
128
+ })
129
+ @ApiOkResponse({
130
+ description: 'Delete confirmation.',
131
+ type: DeletedResponseDto,
132
+ })
133
+ delete(
134
+ @Param('orgId') orgId: string,
135
+ @Param('key') key: string,
136
+ @Param('viewId', ParseUUIDPipe) viewId: string,
137
+ ) {
138
+ return this.reportsViewsService.delete(orgId, key, viewId);
139
+ }
140
+ }
@@ -0,0 +1,33 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { FormsModule } from '../forms/forms.module';
3
+ import { ReportsModule } from './reports.module';
4
+ import { FormReportSourceProvider } from './infrastructure/forms-adapter/form-report-source.adapter';
5
+ import {
6
+ EditSubmissionRowAction,
7
+ UpdateSubmissionStatusRowAction,
8
+ } from './infrastructure/forms-adapter/form-row-actions';
9
+ import { ReportsFormsBridgeBootstrap } from './infrastructure/forms-adapter/forms-bridge-bootstrap.service';
10
+
11
+ /**
12
+ * OPTIONAL bridge between the report and form engines (report design
13
+ * §3.2/§13). Importing it adds form-backed reports (`source.kind: "form"`) and
14
+ * the delegated editSubmission/updateStatus grid verbs to the running app.
15
+ *
16
+ * It is the single artifact aware of BOTH products: it imports FormsModule
17
+ * (for SubmissionService, the form definition store, and the forms request
18
+ * context) and ReportsModule (for the engine's shared source-provider and
19
+ * row-action registries), and wires the form adapter into them at boot.
20
+ *
21
+ * Drop this import from AppModule and ReportsModule keeps working over custom
22
+ * sources alone — the standalone guarantee, made structural at the glue layer.
23
+ */
24
+ @Module({
25
+ imports: [FormsModule, ReportsModule],
26
+ providers: [
27
+ FormReportSourceProvider,
28
+ EditSubmissionRowAction,
29
+ UpdateSubmissionStatusRowAction,
30
+ ReportsFormsBridgeBootstrap,
31
+ ],
32
+ })
33
+ export class ReportsFormsModule {}