@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.
- package/package.json +1 -1
- package/template/.env.example +31 -0
- package/template/README.md +61 -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/app.config.ts +6 -1
- package/template/src/config/env.validation.ts +45 -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 +16 -12
- package/template/src/modules/access-control/access-control.module.ts +2 -1
- 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 +35 -0
- package/template/src/modules/access-control/presentation/current-access-control.controller.ts +40 -0
- 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/application/services/organisations.service.ts +67 -1
- package/template/src/modules/organisations/dto/organisation-response.dto.ts +52 -0
- package/template/src/modules/organisations/presentation/organisations.controller.ts +25 -3
- 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/frontend-bootstrap.spec.ts +181 -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 +34 -0
- package/template/test/security.e2e-spec.ts +134 -2
|
@@ -20,6 +20,7 @@ export class PermissionResponseDto {
|
|
|
20
20
|
subject!: string;
|
|
21
21
|
|
|
22
22
|
@ApiPropertyOptional({
|
|
23
|
+
type: String,
|
|
23
24
|
example: 'Read organisation memberships.',
|
|
24
25
|
nullable: true,
|
|
25
26
|
})
|
|
@@ -57,6 +58,7 @@ export class RoleResponseDto {
|
|
|
57
58
|
name!: string;
|
|
58
59
|
|
|
59
60
|
@ApiPropertyOptional({
|
|
61
|
+
type: String,
|
|
60
62
|
example: 'Can review support-facing records.',
|
|
61
63
|
nullable: true,
|
|
62
64
|
})
|
|
@@ -83,6 +85,7 @@ export class RoleListResponseDto {
|
|
|
83
85
|
items!: RoleResponseDto[];
|
|
84
86
|
|
|
85
87
|
@ApiPropertyOptional({
|
|
88
|
+
type: String,
|
|
86
89
|
example: 'f602c057-04f4-4ef8-8c84-1b7c62fbf8c5',
|
|
87
90
|
format: 'uuid',
|
|
88
91
|
nullable: true,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
+
import { PermissionKey, permissionKeys } from '../types/permission-key';
|
|
3
|
+
|
|
4
|
+
export class CurrentAccessControlResponseDto {
|
|
5
|
+
@ApiProperty({
|
|
6
|
+
example: '2c67399d-670c-4025-a5fd-1ea9a211891e',
|
|
7
|
+
format: 'uuid',
|
|
8
|
+
})
|
|
9
|
+
orgId!: string;
|
|
10
|
+
|
|
11
|
+
@ApiProperty({
|
|
12
|
+
example: '0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3',
|
|
13
|
+
format: 'uuid',
|
|
14
|
+
})
|
|
15
|
+
membershipId!: string;
|
|
16
|
+
|
|
17
|
+
@ApiProperty({
|
|
18
|
+
example: 'f602c057-04f4-4ef8-8c84-1b7c62fbf8c5',
|
|
19
|
+
format: 'uuid',
|
|
20
|
+
})
|
|
21
|
+
roleId!: string;
|
|
22
|
+
|
|
23
|
+
@ApiProperty({ example: true })
|
|
24
|
+
isOwner!: boolean;
|
|
25
|
+
|
|
26
|
+
@ApiProperty({ example: true })
|
|
27
|
+
isBillingContact!: boolean;
|
|
28
|
+
|
|
29
|
+
@ApiProperty({
|
|
30
|
+
enum: permissionKeys,
|
|
31
|
+
isArray: true,
|
|
32
|
+
example: ['organisations.read', 'settings.read'],
|
|
33
|
+
})
|
|
34
|
+
permissionKeys!: PermissionKey[];
|
|
35
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
|
2
|
+
import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
|
|
3
|
+
import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
|
|
4
|
+
import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
|
|
5
|
+
import { CurrentUser } from '../../auth/presentation/current-user.decorator';
|
|
6
|
+
import { AuthenticatedUser } from '../../auth/types/authenticated-user';
|
|
7
|
+
import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
|
|
8
|
+
import { RbacCacheService } from '../application/services/rbac-cache.service';
|
|
9
|
+
import { CurrentAccessControlResponseDto } from '../dto/current-access-control-response.dto';
|
|
10
|
+
|
|
11
|
+
@ApiTags('Access control')
|
|
12
|
+
@ApiBearerAuth()
|
|
13
|
+
@ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
|
|
14
|
+
@ApiProtectedErrorResponses()
|
|
15
|
+
@Controller('organisations/:orgId/access-control')
|
|
16
|
+
@UseGuards(JwtAuthGuard, OrgScopeGuard)
|
|
17
|
+
export class CurrentAccessControlController {
|
|
18
|
+
constructor(private readonly rbacCache: RbacCacheService) {}
|
|
19
|
+
|
|
20
|
+
@Get('me')
|
|
21
|
+
@ApiOperation({
|
|
22
|
+
summary: "Return the current user's effective RBAC context for the organisation.",
|
|
23
|
+
})
|
|
24
|
+
@ApiOkResponse({
|
|
25
|
+
description: 'Effective access-control context.',
|
|
26
|
+
type: CurrentAccessControlResponseDto,
|
|
27
|
+
})
|
|
28
|
+
async me(@CurrentUser() user: AuthenticatedUser, @Param('orgId') orgId: string) {
|
|
29
|
+
const context = await this.rbacCache.getContext(user.id, orgId);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
orgId: context.orgId,
|
|
33
|
+
membershipId: context.membershipId,
|
|
34
|
+
roleId: context.roleId,
|
|
35
|
+
isOwner: context.isOwner,
|
|
36
|
+
isBillingContact: context.isBillingContact,
|
|
37
|
+
permissionKeys: context.permissionKeys,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -17,6 +17,33 @@ export const permissionKeys = [
|
|
|
17
17
|
'settings.update',
|
|
18
18
|
'audit.read',
|
|
19
19
|
'platform.admin',
|
|
20
|
+
// Form builder (@ftisindia/form-builder). Kept as literals so the as-const
|
|
21
|
+
// tuple stays narrow; test/forms-permission-sync.spec.ts asserts these match
|
|
22
|
+
// the engine's FORM_PERMISSION_KEYS export exactly.
|
|
23
|
+
'forms.read',
|
|
24
|
+
'forms.create',
|
|
25
|
+
'forms.update',
|
|
26
|
+
'forms.publish',
|
|
27
|
+
'forms.archive',
|
|
28
|
+
'forms.wireDangerous',
|
|
29
|
+
'forms.managePublicAccess',
|
|
30
|
+
'formSubmissions.read',
|
|
31
|
+
'formSubmissions.create',
|
|
32
|
+
// Row-level edits delegated from report grids (report design §6.1) — and any
|
|
33
|
+
// other surface that patches submissions through the form engine.
|
|
34
|
+
'formSubmissions.update',
|
|
35
|
+
'formSubmissions.export',
|
|
36
|
+
'formDataSources.manage',
|
|
37
|
+
// Report builder (@ftisindia/report-builder). Kept as literals so the
|
|
38
|
+
// as-const tuple stays narrow; test/reports-permission-sync.spec.ts asserts
|
|
39
|
+
// these match the engine's REPORT_PERMISSION_KEYS export exactly.
|
|
40
|
+
'reports.read',
|
|
41
|
+
'reports.create',
|
|
42
|
+
'reports.update',
|
|
43
|
+
'reports.publish',
|
|
44
|
+
'reports.archive',
|
|
45
|
+
'reports.export',
|
|
46
|
+
'reportTags.manage',
|
|
20
47
|
] as const;
|
|
21
48
|
|
|
22
49
|
export type PermissionKey = (typeof permissionKeys)[number];
|
|
@@ -126,4 +126,187 @@ export const routePermissionRegistry: RoutePermissionEntry[] = [
|
|
|
126
126
|
path: '/organisations/:orgId/sample/echo',
|
|
127
127
|
permissions: ['organisations.update'],
|
|
128
128
|
},
|
|
129
|
+
{
|
|
130
|
+
method: 'GET',
|
|
131
|
+
path: '/organisations/:orgId/forms',
|
|
132
|
+
permissions: ['forms.read'],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
method: 'POST',
|
|
136
|
+
path: '/organisations/:orgId/forms',
|
|
137
|
+
permissions: ['forms.create'],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
method: 'GET',
|
|
141
|
+
path: '/organisations/:orgId/forms/:formKey',
|
|
142
|
+
permissions: ['forms.read'],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
method: 'GET',
|
|
146
|
+
path: '/organisations/:orgId/forms/:formKey/render',
|
|
147
|
+
permissions: ['formSubmissions.create'],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
method: 'GET',
|
|
151
|
+
path: '/organisations/:orgId/forms/:formKey/versions/:version',
|
|
152
|
+
permissions: ['forms.read'],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
method: 'PATCH',
|
|
156
|
+
path: '/organisations/:orgId/forms/:formKey/versions/:version',
|
|
157
|
+
permissions: ['forms.update'],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
method: 'POST',
|
|
161
|
+
path: '/organisations/:orgId/forms/:formKey/versions/:version/publish',
|
|
162
|
+
permissions: ['forms.publish'],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
method: 'POST',
|
|
166
|
+
path: '/organisations/:orgId/forms/:formKey/versions/:version/archive',
|
|
167
|
+
permissions: ['forms.archive'],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
method: 'POST',
|
|
171
|
+
path: '/organisations/:orgId/forms/:formKey/versions/:version/public-access',
|
|
172
|
+
permissions: ['forms.managePublicAccess'],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
method: 'POST',
|
|
176
|
+
path: '/organisations/:orgId/forms/:formKey/submissions',
|
|
177
|
+
permissions: ['formSubmissions.create'],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
method: 'POST',
|
|
181
|
+
path: '/organisations/:orgId/forms/:formKey/submissions/validate',
|
|
182
|
+
permissions: ['formSubmissions.create'],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
method: 'GET',
|
|
186
|
+
path: '/organisations/:orgId/forms/:formKey/submissions',
|
|
187
|
+
permissions: ['formSubmissions.read'],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
method: 'GET',
|
|
191
|
+
path: '/organisations/:orgId/forms/:formKey/submissions/:submissionId',
|
|
192
|
+
permissions: ['formSubmissions.read'],
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
method: 'GET',
|
|
196
|
+
path: '/organisations/:orgId/forms/data-sources',
|
|
197
|
+
permissions: ['formDataSources.manage'],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
method: 'GET',
|
|
201
|
+
path: '/organisations/:orgId/forms/:formKey/data-sources/:dataSourceKey/options',
|
|
202
|
+
permissions: ['formSubmissions.create'],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
method: 'POST',
|
|
206
|
+
path: '/organisations/:orgId/forms/:formKey/files',
|
|
207
|
+
permissions: ['formSubmissions.create'],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
method: 'GET',
|
|
211
|
+
path: '/organisations/:orgId/forms/:formKey/files/:fileId',
|
|
212
|
+
permissions: ['formSubmissions.read'],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
method: 'GET',
|
|
216
|
+
path: '/organisations/:orgId/forms/:formKey/submissions/export',
|
|
217
|
+
permissions: ['formSubmissions.export'],
|
|
218
|
+
},
|
|
219
|
+
// Report builder (@ftisindia/report-builder) — report design §10. Row
|
|
220
|
+
// actions carry an additional per-action permission check inside the engine
|
|
221
|
+
// (attach-time and execute-time, §6.1); the route gate below is the base.
|
|
222
|
+
{
|
|
223
|
+
method: 'GET',
|
|
224
|
+
path: '/organisations/:orgId/reports',
|
|
225
|
+
permissions: ['reports.read'],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
method: 'POST',
|
|
229
|
+
path: '/organisations/:orgId/reports',
|
|
230
|
+
permissions: ['reports.create'],
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
method: 'GET',
|
|
234
|
+
path: '/organisations/:orgId/reports/exports/:jobId',
|
|
235
|
+
permissions: ['reports.export'],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
method: 'GET',
|
|
239
|
+
path: '/organisations/:orgId/reports/exports/:jobId/download',
|
|
240
|
+
permissions: ['reports.export'],
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
method: 'GET',
|
|
244
|
+
path: '/organisations/:orgId/reports/:key',
|
|
245
|
+
permissions: ['reports.read'],
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
method: 'PATCH',
|
|
249
|
+
path: '/organisations/:orgId/reports/:key',
|
|
250
|
+
permissions: ['reports.update'],
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
method: 'POST',
|
|
254
|
+
path: '/organisations/:orgId/reports/:key/publish',
|
|
255
|
+
permissions: ['reports.publish'],
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
method: 'POST',
|
|
259
|
+
path: '/organisations/:orgId/reports/:key/archive',
|
|
260
|
+
permissions: ['reports.archive'],
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
method: 'GET',
|
|
264
|
+
path: '/organisations/:orgId/reports/:key/meta',
|
|
265
|
+
permissions: ['reports.read'],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
method: 'POST',
|
|
269
|
+
path: '/organisations/:orgId/reports/:key/query',
|
|
270
|
+
permissions: ['reports.read'],
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
method: 'GET',
|
|
274
|
+
path: '/organisations/:orgId/reports/:key/views',
|
|
275
|
+
permissions: ['reports.read'],
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
method: 'POST',
|
|
279
|
+
path: '/organisations/:orgId/reports/:key/views',
|
|
280
|
+
permissions: ['reports.read'],
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
method: 'POST',
|
|
284
|
+
path: '/organisations/:orgId/reports/:key/views/shared',
|
|
285
|
+
permissions: ['reports.update'],
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
method: 'PATCH',
|
|
289
|
+
path: '/organisations/:orgId/reports/:key/views/:viewId',
|
|
290
|
+
permissions: ['reports.read'],
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
method: 'DELETE',
|
|
294
|
+
path: '/organisations/:orgId/reports/:key/views/:viewId',
|
|
295
|
+
permissions: ['reports.read'],
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
method: 'POST',
|
|
299
|
+
path: '/organisations/:orgId/reports/:key/actions/:name/prepare',
|
|
300
|
+
permissions: ['reports.read'],
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
method: 'POST',
|
|
304
|
+
path: '/organisations/:orgId/reports/:key/actions/:name',
|
|
305
|
+
permissions: ['reports.read'],
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
method: 'POST',
|
|
309
|
+
path: '/organisations/:orgId/reports/:key/export',
|
|
310
|
+
permissions: ['reports.export'],
|
|
311
|
+
},
|
|
129
312
|
];
|
|
@@ -7,7 +7,7 @@ class AuditActorResponseDto {
|
|
|
7
7
|
})
|
|
8
8
|
id!: string;
|
|
9
9
|
|
|
10
|
-
@ApiPropertyOptional({ example: 'Starter Owner', nullable: true })
|
|
10
|
+
@ApiPropertyOptional({ type: String, example: 'Starter Owner', nullable: true })
|
|
11
11
|
displayName?: string | null;
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -19,6 +19,7 @@ export class AuditLogResponseDto {
|
|
|
19
19
|
id!: string;
|
|
20
20
|
|
|
21
21
|
@ApiPropertyOptional({
|
|
22
|
+
type: String,
|
|
22
23
|
example: '2c67399d-670c-4025-a5fd-1ea9a211891e',
|
|
23
24
|
format: 'uuid',
|
|
24
25
|
nullable: true,
|
|
@@ -26,6 +27,7 @@ export class AuditLogResponseDto {
|
|
|
26
27
|
orgId?: string | null;
|
|
27
28
|
|
|
28
29
|
@ApiPropertyOptional({
|
|
30
|
+
type: String,
|
|
29
31
|
example: '4a4f0d8a-4bd2-469f-a6a9-3e1cb6a2b456',
|
|
30
32
|
format: 'uuid',
|
|
31
33
|
nullable: true,
|
|
@@ -39,6 +41,7 @@ export class AuditLogResponseDto {
|
|
|
39
41
|
targetType!: string;
|
|
40
42
|
|
|
41
43
|
@ApiPropertyOptional({
|
|
44
|
+
type: String,
|
|
42
45
|
example: '0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3',
|
|
43
46
|
nullable: true,
|
|
44
47
|
})
|
|
@@ -50,10 +53,10 @@ export class AuditLogResponseDto {
|
|
|
50
53
|
})
|
|
51
54
|
metadata?: Record<string, unknown> | null;
|
|
52
55
|
|
|
53
|
-
@ApiPropertyOptional({ example: '127.0.0.1', nullable: true })
|
|
56
|
+
@ApiPropertyOptional({ type: String, example: '127.0.0.1', nullable: true })
|
|
54
57
|
ipAddress?: string | null;
|
|
55
58
|
|
|
56
|
-
@ApiPropertyOptional({ example: 'Mozilla/5.0', nullable: true })
|
|
59
|
+
@ApiPropertyOptional({ type: String, example: 'Mozilla/5.0', nullable: true })
|
|
57
60
|
userAgent?: string | null;
|
|
58
61
|
|
|
59
62
|
@ApiProperty({ example: '2026-06-01T10:30:00.000Z', format: 'date-time' })
|
|
@@ -68,6 +71,7 @@ export class AuditLogListResponseDto {
|
|
|
68
71
|
items!: AuditLogResponseDto[];
|
|
69
72
|
|
|
70
73
|
@ApiPropertyOptional({
|
|
74
|
+
type: String,
|
|
71
75
|
example: 'df6537c4-f58b-452e-a67e-18ec528d0f0f',
|
|
72
76
|
nullable: true,
|
|
73
77
|
})
|
|
@@ -41,6 +41,8 @@ import { GoogleOAuthExceptionFilter } from './presentation/google-oauth-exceptio
|
|
|
41
41
|
PasswordService,
|
|
42
42
|
TokenService,
|
|
43
43
|
],
|
|
44
|
-
|
|
44
|
+
// AuthService is exported for the forms module's delegating `authenticate`
|
|
45
|
+
// action (login-as-a-form rides the real auth stack, never reimplements it).
|
|
46
|
+
exports: [JwtAuthGuard, PasswordService, AuthService],
|
|
45
47
|
})
|
|
46
48
|
export class AuthModule {}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Injectable, Logger, OnApplicationBootstrap, OnModuleDestroy } from '@nestjs/common';
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
3
|
+
import { isGcEligible } from '@ftisindia/form-builder';
|
|
4
|
+
import { PrismaFileStore } from '../../infrastructure/stores/prisma-file.store';
|
|
5
|
+
import { LocalDiskStorageAdapter } from '../../infrastructure/storage/local-disk-storage.adapter';
|
|
6
|
+
|
|
7
|
+
const GC_BATCH_SIZE = 100;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sweeps orphaned TEMPORARY uploads (design doc §10.3): files uploaded but
|
|
11
|
+
* never linked to a submission within FORMS_FILE_TEMP_TTL_HOURS are deleted
|
|
12
|
+
* — bytes first, then the row. LINKED files are exempt by construction.
|
|
13
|
+
* runOnce() makes tests deterministic; the timer mirrors the outbox poller.
|
|
14
|
+
*/
|
|
15
|
+
@Injectable()
|
|
16
|
+
export class FileGcService implements OnApplicationBootstrap, OnModuleDestroy {
|
|
17
|
+
private readonly logger = new Logger('FormsFileGc');
|
|
18
|
+
private timer?: NodeJS.Timeout;
|
|
19
|
+
private sweeping = false;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly config: ConfigService,
|
|
23
|
+
private readonly files: PrismaFileStore,
|
|
24
|
+
private readonly storage: LocalDiskStorageAdapter,
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
onApplicationBootstrap(): void {
|
|
28
|
+
if (this.config.get<boolean>('forms.outboxEnabled') === false) {
|
|
29
|
+
// Test environments disable background work with one switch.
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const intervalMs = this.config.get<number>('forms.fileGcIntervalMs') ?? 3_600_000;
|
|
33
|
+
this.timer = setInterval(() => {
|
|
34
|
+
void this.sweep();
|
|
35
|
+
}, intervalMs);
|
|
36
|
+
this.timer.unref?.();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
onModuleDestroy(): void {
|
|
40
|
+
if (this.timer) {
|
|
41
|
+
clearInterval(this.timer);
|
|
42
|
+
this.timer = undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async runOnce(now = new Date()): Promise<number> {
|
|
47
|
+
const ttlMs =
|
|
48
|
+
(this.config.get<number>('forms.fileTempTtlHours') ?? 24) * 60 * 60 * 1000;
|
|
49
|
+
const cutoff = new Date(now.getTime() - ttlMs);
|
|
50
|
+
const candidates = await this.files.listGcEligible(cutoff, GC_BATCH_SIZE);
|
|
51
|
+
let removed = 0;
|
|
52
|
+
for (const file of candidates) {
|
|
53
|
+
if (!isGcEligible(file, now, ttlMs)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
await this.storage.delete(file.storageKey);
|
|
58
|
+
await this.files.delete(file.id);
|
|
59
|
+
removed += 1;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
this.logger.warn(
|
|
62
|
+
`Failed to garbage-collect file ${file.id}: ${error instanceof Error ? error.message : String(error)}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return removed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private async sweep(): Promise<void> {
|
|
70
|
+
if (this.sweeping) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.sweeping = true;
|
|
74
|
+
try {
|
|
75
|
+
const removed = await this.runOnce();
|
|
76
|
+
if (removed > 0) {
|
|
77
|
+
this.logger.log(`Garbage-collected ${removed} orphaned temporary file(s).`);
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
this.logger.error('File GC sweep failed.', error instanceof Error ? error.stack : error);
|
|
81
|
+
} finally {
|
|
82
|
+
this.sweeping = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
2
|
+
import { DataSourceRegistry, DefinitionService, walkFields } from '@ftisindia/form-builder';
|
|
3
|
+
import type { DataSourceOption, FormDefinition } from '@ftisindia/form-builder';
|
|
4
|
+
import { resolvePageLimit, toPage } from '../../../../common/dto/pagination-query.dto';
|
|
5
|
+
import { RequestContextService } from '../../../request-context/application/services/request-context.service';
|
|
6
|
+
import { CreateFormDefinitionDto } from '../../dto/create-form-definition.dto';
|
|
7
|
+
import { ListFormDefinitionsQueryDto } from '../../dto/list-form-definitions-query.dto';
|
|
8
|
+
import { SetPublicAccessDto } from '../../dto/set-public-access.dto';
|
|
9
|
+
import { UpdateFormDefinitionDto } from '../../dto/update-form-definition.dto';
|
|
10
|
+
import { RequestFormsContext } from '../../infrastructure/request-forms-context';
|
|
11
|
+
import { withFormsErrorMapping } from './forms-error.mapper';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Thin HTTP-facing wrapper over the engine's DefinitionService: org-scope
|
|
15
|
+
* assertion first, engine call through the FormsContext seam, engine-typed
|
|
16
|
+
* errors mapped onto the app's HTTP envelope. No business logic lives here.
|
|
17
|
+
*/
|
|
18
|
+
@Injectable()
|
|
19
|
+
export class FormsDefinitionsService {
|
|
20
|
+
constructor(
|
|
21
|
+
private readonly engine: DefinitionService,
|
|
22
|
+
private readonly dataSources: DataSourceRegistry,
|
|
23
|
+
private readonly formsContext: RequestFormsContext,
|
|
24
|
+
private readonly requestContext: RequestContextService,
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
async list(orgId: string, query: ListFormDefinitionsQueryDto) {
|
|
28
|
+
this.requestContext.assertOrgScope(orgId);
|
|
29
|
+
const limit = resolvePageLimit(query.limit);
|
|
30
|
+
|
|
31
|
+
const records = await withFormsErrorMapping(() =>
|
|
32
|
+
this.engine.list(
|
|
33
|
+
{ orgId, status: query.status, cursor: query.cursor, limit: limit + 1 },
|
|
34
|
+
this.formsContext,
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return toPage(records, limit);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async create(orgId: string, dto: CreateFormDefinitionDto) {
|
|
42
|
+
this.requestContext.assertOrgScope(orgId);
|
|
43
|
+
return withFormsErrorMapping(() =>
|
|
44
|
+
this.engine.createDraft(
|
|
45
|
+
{ orgId, definition: dto.definition as unknown as FormDefinition },
|
|
46
|
+
this.formsContext,
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async getLatest(orgId: string, formKey: string) {
|
|
52
|
+
this.requestContext.assertOrgScope(orgId);
|
|
53
|
+
return withFormsErrorMapping(() =>
|
|
54
|
+
this.engine.getLatest({ orgId, key: formKey }, this.formsContext),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async getVersion(orgId: string, formKey: string, version: number) {
|
|
59
|
+
this.requestContext.assertOrgScope(orgId);
|
|
60
|
+
return withFormsErrorMapping(() =>
|
|
61
|
+
this.engine.getByKeyVersion({ orgId, key: formKey, version }, this.formsContext),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async update(orgId: string, formKey: string, version: number, dto: UpdateFormDefinitionDto) {
|
|
66
|
+
this.requestContext.assertOrgScope(orgId);
|
|
67
|
+
return withFormsErrorMapping(() =>
|
|
68
|
+
this.engine.updateDraft(
|
|
69
|
+
{
|
|
70
|
+
orgId,
|
|
71
|
+
key: formKey,
|
|
72
|
+
version,
|
|
73
|
+
definition: dto.definition as unknown as FormDefinition,
|
|
74
|
+
},
|
|
75
|
+
this.formsContext,
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async publish(orgId: string, formKey: string, version: number) {
|
|
81
|
+
this.requestContext.assertOrgScope(orgId);
|
|
82
|
+
return withFormsErrorMapping(() =>
|
|
83
|
+
this.engine.publish({ orgId, key: formKey, version }, this.formsContext),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async archive(orgId: string, formKey: string, version: number) {
|
|
88
|
+
this.requestContext.assertOrgScope(orgId);
|
|
89
|
+
return withFormsErrorMapping(() =>
|
|
90
|
+
this.engine.archive({ orgId, key: formKey, version }, this.formsContext),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async setPublicAccess(orgId: string, formKey: string, version: number, dto: SetPublicAccessDto) {
|
|
95
|
+
this.requestContext.assertOrgScope(orgId);
|
|
96
|
+
return withFormsErrorMapping(() =>
|
|
97
|
+
this.engine.setPublicAccess(
|
|
98
|
+
{ orgId, key: formKey, version, access: dto.access },
|
|
99
|
+
this.formsContext,
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async render(orgId: string, formKey: string) {
|
|
105
|
+
this.requestContext.assertOrgScope(orgId);
|
|
106
|
+
const result = await withFormsErrorMapping(() =>
|
|
107
|
+
this.engine.getForRender({ orgId, key: formKey }, this.formsContext),
|
|
108
|
+
);
|
|
109
|
+
return { definition: result.definition, options: result.options };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async dataSourceOptions(orgId: string, formKey: string, dataSourceKey: string) {
|
|
113
|
+
this.requestContext.assertOrgScope(orgId);
|
|
114
|
+
const render = await withFormsErrorMapping(() =>
|
|
115
|
+
this.engine.getForRender({ orgId, key: formKey }, this.formsContext),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const paths: string[] = [];
|
|
119
|
+
walkFields(render.definition.fields, (field, path) => {
|
|
120
|
+
if (field.dataSource?.key === dataSourceKey) {
|
|
121
|
+
paths.push(path.join('.'));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (paths.length === 0) {
|
|
126
|
+
throw new NotFoundException('No field on this form uses the requested data source.');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const items: DataSourceOption[] = paths.flatMap((path) => render.options[path] ?? []);
|
|
130
|
+
return { items };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
listDataSourceKeys(orgId: string) {
|
|
134
|
+
this.requestContext.assertOrgScope(orgId);
|
|
135
|
+
return { items: this.dataSources.keys() };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConflictException,
|
|
3
|
+
ForbiddenException,
|
|
4
|
+
NotFoundException,
|
|
5
|
+
UnprocessableEntityException,
|
|
6
|
+
} from '@nestjs/common';
|
|
7
|
+
import {
|
|
8
|
+
FormsAuthzDeniedError,
|
|
9
|
+
FormsConflictError,
|
|
10
|
+
FormsDefinitionLintError,
|
|
11
|
+
FormsNotFoundError,
|
|
12
|
+
FormsStateError,
|
|
13
|
+
FormsValidationError,
|
|
14
|
+
} from '@ftisindia/form-builder';
|
|
15
|
+
import { Prisma } from '@prisma/client';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Maps the engine's typed errors (ecosystem guide §2.1) onto the app's HTTP
|
|
19
|
+
* error envelope. Object bodies survive the global HttpExceptionFilter, which
|
|
20
|
+
* lifts every non-message key into `error.details` — so 422 responses carry
|
|
21
|
+
* the engine's `errors` / `issues` arrays verbatim.
|
|
22
|
+
*/
|
|
23
|
+
export function mapFormsError(error: unknown): never {
|
|
24
|
+
if (error instanceof FormsValidationError) {
|
|
25
|
+
throw new UnprocessableEntityException({
|
|
26
|
+
message: 'Validation failed.',
|
|
27
|
+
errors: error.errors,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (error instanceof FormsDefinitionLintError) {
|
|
32
|
+
throw new UnprocessableEntityException({
|
|
33
|
+
message: 'Form definition is invalid.',
|
|
34
|
+
issues: error.issues,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (error instanceof FormsAuthzDeniedError) {
|
|
39
|
+
throw new ForbiddenException(error.message);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (error instanceof FormsNotFoundError) {
|
|
43
|
+
throw new NotFoundException(error.message);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (error instanceof FormsConflictError || error instanceof FormsStateError) {
|
|
47
|
+
throw new ConflictException(error.message);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
|
|
51
|
+
throw new ConflictException('A form definition with this key and version already exists.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Awaits an engine call and maps any engine-typed failure onto HTTP errors. */
|
|
58
|
+
export async function withFormsErrorMapping<T>(fn: () => Promise<T>): Promise<T> {
|
|
59
|
+
try {
|
|
60
|
+
return await fn();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
mapFormsError(error);
|
|
63
|
+
}
|
|
64
|
+
}
|