@ftisindia/create-app 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.env.example +25 -0
- package/template/README.md +51 -0
- package/template/_gitignore +6 -0
- package/template/_package.json +6 -0
- package/template/docs/FORMS.md +169 -0
- package/template/docs/FORMS_CHECKLIST.md +61 -0
- package/template/docs/REPORTS.md +246 -0
- package/template/docs/REPORTS_CHECKLIST.md +97 -0
- package/template/prisma/migrations/20260612000000_add_form_builder/migration.sql +147 -0
- package/template/prisma/migrations/20260613000000_add_report_builder/migration.sql +129 -0
- package/template/prisma/schema.prisma +285 -0
- package/template/scripts/export-openapi.ts +85 -0
- package/template/scripts/gen-form.mjs +149 -0
- package/template/scripts/push-form.ts +124 -0
- package/template/src/app.module.ts +29 -8
- package/template/src/common/dto/membership-response.dto.ts +1 -0
- package/template/src/common/dto/role-summary.dto.ts +3 -3
- package/template/src/common/dto/user-summary.dto.ts +3 -3
- package/template/src/config/env.validation.ts +25 -0
- package/template/src/config/forms.config.ts +12 -0
- package/template/src/config/index.ts +2 -0
- package/template/src/config/openapi.ts +12 -0
- package/template/src/config/reports-secret.ts +15 -0
- package/template/src/config/reports.config.ts +16 -0
- package/template/src/main.ts +3 -12
- package/template/src/modules/access-control/dto/access-control-response.dto.ts +3 -0
- package/template/src/modules/access-control/dto/current-access-control-response.dto.ts +5 -1
- package/template/src/modules/access-control/types/permission-key.ts +27 -0
- package/template/src/modules/access-control/types/route-permission-registry.ts +183 -0
- package/template/src/modules/audit/dto/audit-response.dto.ts +7 -3
- package/template/src/modules/auth/auth.module.ts +3 -1
- package/template/src/modules/auth/dto/auth-response.dto.ts +1 -1
- package/template/src/modules/forms/application/services/file-gc.service.ts +85 -0
- package/template/src/modules/forms/application/services/forms-definitions.service.ts +137 -0
- package/template/src/modules/forms/application/services/forms-error.mapper.ts +64 -0
- package/template/src/modules/forms/application/services/forms-export.service.ts +210 -0
- package/template/src/modules/forms/application/services/forms-files.service.ts +164 -0
- package/template/src/modules/forms/application/services/forms-public.service.ts +49 -0
- package/template/src/modules/forms/application/services/forms-settings-reader.service.ts +53 -0
- package/template/src/modules/forms/application/services/forms-submissions.service.ts +103 -0
- package/template/src/modules/forms/application/services/handlers/authenticate.action.ts +37 -0
- package/template/src/modules/forms/application/services/handlers/logging-email.handler.ts +22 -0
- package/template/src/modules/forms/application/services/handlers/send-confirmation-email.action.ts +40 -0
- package/template/src/modules/forms/application/services/handlers/webhook.handler.ts +41 -0
- package/template/src/modules/forms/application/services/outbox-dispatcher.service.ts +109 -0
- package/template/src/modules/forms/dto/create-form-definition.dto.ts +12 -0
- package/template/src/modules/forms/dto/data-source-response.dto.ts +19 -0
- package/template/src/modules/forms/dto/export-submissions-query.dto.ts +33 -0
- package/template/src/modules/forms/dto/file-upload-response.dto.ts +24 -0
- package/template/src/modules/forms/dto/form-definition-response.dto.ts +50 -0
- package/template/src/modules/forms/dto/form-render-response.dto.ts +17 -0
- package/template/src/modules/forms/dto/list-form-definitions-query.dto.ts +10 -0
- package/template/src/modules/forms/dto/list-submissions-query.dto.ts +10 -0
- package/template/src/modules/forms/dto/public-submit-form.dto.ts +24 -0
- package/template/src/modules/forms/dto/set-public-access.dto.ts +8 -0
- package/template/src/modules/forms/dto/submission-response.dto.ts +99 -0
- package/template/src/modules/forms/dto/submit-form.dto.ts +50 -0
- package/template/src/modules/forms/dto/update-form-definition.dto.ts +12 -0
- package/template/src/modules/forms/dto/upload-file-query.dto.ts +33 -0
- package/template/src/modules/forms/dto/validate-submission.dto.ts +22 -0
- package/template/src/modules/forms/examples/abstract-submission.form.json +80 -0
- package/template/src/modules/forms/examples/login.form.json +24 -0
- package/template/src/modules/forms/examples/registration.form.json +44 -0
- package/template/src/modules/forms/forms.module.ts +226 -0
- package/template/src/modules/forms/forms.tokens.ts +6 -0
- package/template/src/modules/forms/infrastructure/audit-sink.adapter.ts +30 -0
- package/template/src/modules/forms/infrastructure/casl-forms-authorization.ts +31 -0
- package/template/src/modules/forms/infrastructure/prisma-tx-runner.ts +17 -0
- package/template/src/modules/forms/infrastructure/registry/form-extension.decorators.ts +17 -0
- package/template/src/modules/forms/infrastructure/registry/registry-bootstrap.service.ts +82 -0
- package/template/src/modules/forms/infrastructure/request-forms-context.ts +60 -0
- package/template/src/modules/forms/infrastructure/schema-check/forms-schema-check.service.ts +76 -0
- package/template/src/modules/forms/infrastructure/storage/local-disk-storage.adapter.ts +43 -0
- package/template/src/modules/forms/infrastructure/stores/index.ts +5 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-action-log.store.ts +37 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-file.store.ts +108 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-form-definition.store.ts +147 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-outbox.store.ts +133 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-submission.store.ts +164 -0
- package/template/src/modules/forms/presentation/forms-data-sources.controller.ts +58 -0
- package/template/src/modules/forms/presentation/forms-definitions.controller.ts +191 -0
- package/template/src/modules/forms/presentation/forms-files.controller.ts +79 -0
- package/template/src/modules/forms/presentation/forms-submissions.controller.ts +154 -0
- package/template/src/modules/forms/presentation/forms-upload.interceptor.ts +33 -0
- package/template/src/modules/forms/presentation/public-forms.controller.ts +51 -0
- package/template/src/modules/invitations/dto/invitation-response.dto.ts +4 -0
- package/template/src/modules/organisations/dto/organisation-response.dto.ts +1 -0
- package/template/src/modules/reports/application/services/reports-actions.service.ts +54 -0
- package/template/src/modules/reports/application/services/reports-definitions.service.ts +66 -0
- package/template/src/modules/reports/application/services/reports-error.mapper.ts +97 -0
- package/template/src/modules/reports/application/services/reports-export-dispatcher.service.ts +124 -0
- package/template/src/modules/reports/application/services/reports-exports.service.ts +74 -0
- package/template/src/modules/reports/application/services/reports-queries.service.ts +35 -0
- package/template/src/modules/reports/application/services/reports-settings-reader.service.ts +49 -0
- package/template/src/modules/reports/application/services/reports-views.service.ts +79 -0
- package/template/src/modules/reports/dto/action-result-response.dto.ts +21 -0
- package/template/src/modules/reports/dto/create-report-definition.dto.ts +86 -0
- package/template/src/modules/reports/dto/create-saved-view.dto.ts +26 -0
- package/template/src/modules/reports/dto/execute-action.dto.ts +71 -0
- package/template/src/modules/reports/dto/export-job-response.dto.ts +60 -0
- package/template/src/modules/reports/dto/export-request.dto.ts +34 -0
- package/template/src/modules/reports/dto/list-reports-query.dto.ts +10 -0
- package/template/src/modules/reports/dto/list-views-query.dto.ts +17 -0
- package/template/src/modules/reports/dto/prepare-action-response.dto.ts +14 -0
- package/template/src/modules/reports/dto/prepare-action.dto.ts +27 -0
- package/template/src/modules/reports/dto/query-response.dto.ts +64 -0
- package/template/src/modules/reports/dto/query-spec.dto.ts +120 -0
- package/template/src/modules/reports/dto/report-definition-response.dto.ts +64 -0
- package/template/src/modules/reports/dto/report-meta-query.dto.ts +16 -0
- package/template/src/modules/reports/dto/report-meta-response.dto.ts +113 -0
- package/template/src/modules/reports/dto/saved-view-response.dto.ts +66 -0
- package/template/src/modules/reports/dto/update-report-definition.dto.ts +9 -0
- package/template/src/modules/reports/dto/update-saved-view.dto.ts +27 -0
- package/template/src/modules/reports/examples/abstract-review-board.report.json +54 -0
- package/template/src/modules/reports/examples/org-members.report.json +55 -0
- package/template/src/modules/reports/infrastructure/audit-sink.adapter.ts +31 -0
- package/template/src/modules/reports/infrastructure/casl-reports-authorization.ts +39 -0
- package/template/src/modules/reports/infrastructure/forms-adapter/form-report-source.adapter.ts +292 -0
- package/template/src/modules/reports/infrastructure/forms-adapter/form-row-actions.ts +171 -0
- package/template/src/modules/reports/infrastructure/forms-adapter/forms-bridge-bootstrap.service.ts +32 -0
- package/template/src/modules/reports/infrastructure/prisma-catalog.adapter.ts +95 -0
- package/template/src/modules/reports/infrastructure/prisma-query-executor.ts +103 -0
- package/template/src/modules/reports/infrastructure/prisma-snapshot-runner.ts +47 -0
- package/template/src/modules/reports/infrastructure/prisma-tx-runner.ts +18 -0
- package/template/src/modules/reports/infrastructure/registry/registry-bootstrap.service.ts +61 -0
- package/template/src/modules/reports/infrastructure/registry/report-extension.decorators.ts +14 -0
- package/template/src/modules/reports/infrastructure/reports-job-queue.adapter.ts +28 -0
- package/template/src/modules/reports/infrastructure/request-reports-context.ts +42 -0
- package/template/src/modules/reports/infrastructure/schema-check/reports-schema-check.service.ts +116 -0
- package/template/src/modules/reports/infrastructure/storage/local-disk-export-storage.adapter.ts +79 -0
- package/template/src/modules/reports/infrastructure/stores/index.ts +5 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-bulk-action-run.store.ts +89 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-export-job.store.ts +93 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-report-definition.store.ts +171 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-row-tag.store.ts +110 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-saved-view.store.ts +144 -0
- package/template/src/modules/reports/presentation/reports-actions.controller.ts +83 -0
- package/template/src/modules/reports/presentation/reports-definitions.controller.ts +156 -0
- package/template/src/modules/reports/presentation/reports-export-jobs.controller.ts +61 -0
- package/template/src/modules/reports/presentation/reports-export.controller.ts +76 -0
- package/template/src/modules/reports/presentation/reports-query.controller.ts +52 -0
- package/template/src/modules/reports/presentation/reports-views.controller.ts +140 -0
- package/template/src/modules/reports/reports-forms.module.ts +33 -0
- package/template/src/modules/reports/reports.module.ts +335 -0
- package/template/src/modules/reports/reports.tokens.ts +11 -0
- package/template/src/modules/reports/sources/org-members.source.ts +112 -0
- package/template/src/modules/settings/types/setting-definitions.ts +94 -0
- package/template/test/forms-definitions.e2e-spec.ts +394 -0
- package/template/test/forms-export.e2e-spec.ts +390 -0
- package/template/test/forms-files.e2e-spec.ts +345 -0
- package/template/test/forms-outbox.e2e-spec.ts +309 -0
- package/template/test/forms-permission-sync.spec.ts +27 -0
- package/template/test/forms-public.e2e-spec.ts +269 -0
- package/template/test/forms-schema-check.e2e-spec.ts +65 -0
- package/template/test/forms-submissions.e2e-spec.ts +500 -0
- package/template/test/forms-webhooks.e2e-spec.ts +261 -0
- package/template/test/reports-advanced.e2e-spec.ts +368 -0
- package/template/test/reports-permission-sync.spec.ts +30 -0
- package/template/test/reports-query.e2e-spec.ts +350 -0
- package/template/test/reports-tiers.e2e-spec.ts +257 -0
- package/template/test/route-registry.validator.spec.ts +22 -0
|
@@ -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,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { ReportActionService } from '@ftisindia/report-builder';
|
|
3
|
+
import type { Selection } from '@ftisindia/report-builder';
|
|
4
|
+
import { RequestContextService } from '../../../request-context/application/services/request-context.service';
|
|
5
|
+
import { ExecuteActionDto } from '../../dto/execute-action.dto';
|
|
6
|
+
import { PrepareActionDto } from '../../dto/prepare-action.dto';
|
|
7
|
+
import { RequestReportsContext } from '../../infrastructure/request-reports-context';
|
|
8
|
+
import { withReportsErrorMapping } from './reports-error.mapper';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Thin HTTP-facing wrapper over the engine's ReportActionService. The §6.3
|
|
12
|
+
* protocol — token verification, drift detection, keyset batching, the
|
|
13
|
+
* idempotency ledger — and the per-action permission checks (§6.1's
|
|
14
|
+
* execute-time layer) all live in the engine.
|
|
15
|
+
*/
|
|
16
|
+
@Injectable()
|
|
17
|
+
export class ReportsActionsService {
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly engine: ReportActionService,
|
|
20
|
+
private readonly reportsContext: RequestReportsContext,
|
|
21
|
+
private readonly requestContext: RequestContextService,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
async prepare(orgId: string, key: string, actionName: string, dto: PrepareActionDto) {
|
|
25
|
+
this.requestContext.assertOrgScope(orgId);
|
|
26
|
+
return withReportsErrorMapping(() =>
|
|
27
|
+
this.engine.prepare(
|
|
28
|
+
orgId,
|
|
29
|
+
key,
|
|
30
|
+
actionName,
|
|
31
|
+
dto.selection as unknown as Selection,
|
|
32
|
+
this.reportsContext,
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async execute(orgId: string, key: string, actionName: string, dto: ExecuteActionDto) {
|
|
38
|
+
this.requestContext.assertOrgScope(orgId);
|
|
39
|
+
return withReportsErrorMapping(() =>
|
|
40
|
+
this.engine.execute(
|
|
41
|
+
orgId,
|
|
42
|
+
key,
|
|
43
|
+
actionName,
|
|
44
|
+
{
|
|
45
|
+
selection: dto.selection as unknown as Selection | undefined,
|
|
46
|
+
actionToken: dto.actionToken,
|
|
47
|
+
input: dto.input,
|
|
48
|
+
idempotencyKey: dto.idempotencyKey,
|
|
49
|
+
},
|
|
50
|
+
this.reportsContext,
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { ReportDefinitionService } from '@ftisindia/report-builder';
|
|
3
|
+
import { resolvePageLimit, toPage } from '../../../../common/dto/pagination-query.dto';
|
|
4
|
+
import { RequestContextService } from '../../../request-context/application/services/request-context.service';
|
|
5
|
+
import { CreateReportDefinitionDto } from '../../dto/create-report-definition.dto';
|
|
6
|
+
import { ListReportsQueryDto } from '../../dto/list-reports-query.dto';
|
|
7
|
+
import { UpdateReportDefinitionDto } from '../../dto/update-report-definition.dto';
|
|
8
|
+
import { RequestReportsContext } from '../../infrastructure/request-reports-context';
|
|
9
|
+
import { withReportsErrorMapping } from './reports-error.mapper';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Thin HTTP-facing wrapper over the engine's ReportDefinitionService:
|
|
13
|
+
* org-scope assertion first, engine call through the ReportsContext seam,
|
|
14
|
+
* engine-typed errors mapped onto the app's HTTP envelope. No business logic
|
|
15
|
+
* lives here — save/publish linting, tier gates, and state transitions all
|
|
16
|
+
* belong to the engine (design §2/§8/§10).
|
|
17
|
+
*/
|
|
18
|
+
@Injectable()
|
|
19
|
+
export class ReportsDefinitionsService {
|
|
20
|
+
constructor(
|
|
21
|
+
private readonly engine: ReportDefinitionService,
|
|
22
|
+
private readonly reportsContext: RequestReportsContext,
|
|
23
|
+
private readonly requestContext: RequestContextService,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
async list(orgId: string, query: ListReportsQueryDto) {
|
|
27
|
+
this.requestContext.assertOrgScope(orgId);
|
|
28
|
+
const limit = resolvePageLimit(query.limit);
|
|
29
|
+
|
|
30
|
+
const records = await withReportsErrorMapping(() =>
|
|
31
|
+
this.engine.list(orgId, this.reportsContext, {
|
|
32
|
+
status: query.status,
|
|
33
|
+
cursor: query.cursor,
|
|
34
|
+
limit: limit + 1,
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return toPage(records, limit);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async create(orgId: string, dto: CreateReportDefinitionDto) {
|
|
42
|
+
this.requestContext.assertOrgScope(orgId);
|
|
43
|
+
return withReportsErrorMapping(() => this.engine.create(orgId, dto, this.reportsContext));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async getLatest(orgId: string, key: string) {
|
|
47
|
+
this.requestContext.assertOrgScope(orgId);
|
|
48
|
+
return withReportsErrorMapping(() => this.engine.get(orgId, key, this.reportsContext));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async update(orgId: string, key: string, dto: UpdateReportDefinitionDto) {
|
|
52
|
+
this.requestContext.assertOrgScope(orgId);
|
|
53
|
+
return withReportsErrorMapping(() => this.engine.update(orgId, key, dto, this.reportsContext));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async publish(orgId: string, key: string) {
|
|
57
|
+
this.requestContext.assertOrgScope(orgId);
|
|
58
|
+
return withReportsErrorMapping(() => this.engine.publish(orgId, key, this.reportsContext));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async archive(orgId: string, key: string) {
|
|
62
|
+
this.requestContext.assertOrgScope(orgId);
|
|
63
|
+
await withReportsErrorMapping(() => this.engine.archive(orgId, key, this.reportsContext));
|
|
64
|
+
return { archived: true };
|
|
65
|
+
}
|
|
66
|
+
}
|