@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.
- package/package.json +1 -1
- package/template/.env.example +28 -0
- package/template/README.md +51 -0
- package/template/_gitignore +6 -0
- package/template/_package.json +10 -1
- package/template/docs/FORMS.md +188 -0
- package/template/docs/FORMS_CHECKLIST.md +69 -0
- package/template/docs/REPORTS.md +255 -0
- package/template/docs/REPORTS_CHECKLIST.md +152 -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/migrations/20260616000000_add_form_outbox_claimed_by/migration.sql +5 -0
- package/template/prisma/schema.prisma +289 -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 +30 -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 +28 -0
- package/template/src/config/forms.config.ts +13 -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 +18 -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-delivery.transport.ts +319 -0
- package/template/src/modules/forms/application/services/handlers/webhook.handler.ts +89 -0
- package/template/src/modules/forms/application/services/outbox-dispatcher.service.ts +131 -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 +228 -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 +156 -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 +205 -0
- package/template/src/modules/reports/application/services/reports-exports.service.ts +78 -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 +92 -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-captcha.e2e-spec.ts +163 -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 +570 -0
- package/template/test/forms-permission-sync.spec.ts +27 -0
- package/template/test/forms-public.e2e-spec.ts +293 -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-throttling.e2e-spec.ts +146 -0
- package/template/test/forms-webhooks.e2e-spec.ts +403 -0
- package/template/test/jest-e2e.json +1 -0
- package/template/test/reports-advanced.e2e-spec.ts +381 -0
- package/template/test/reports-permission-sync.spec.ts +30 -0
- package/template/test/reports-query.e2e-spec.ts +402 -0
- package/template/test/reports-tiers.e2e-spec.ts +343 -0
- package/template/test/route-registry.validator.spec.ts +22 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
ConflictException,
|
|
4
|
+
ForbiddenException,
|
|
5
|
+
GoneException,
|
|
6
|
+
NotFoundException,
|
|
7
|
+
RequestTimeoutException,
|
|
8
|
+
UnauthorizedException,
|
|
9
|
+
UnprocessableEntityException,
|
|
10
|
+
} from '@nestjs/common';
|
|
11
|
+
import {
|
|
12
|
+
ReportAuthzDeniedError,
|
|
13
|
+
ReportConflictError,
|
|
14
|
+
ReportCursorError,
|
|
15
|
+
ReportDriftError,
|
|
16
|
+
ReportExportError,
|
|
17
|
+
ReportLintError,
|
|
18
|
+
ReportNotFoundError,
|
|
19
|
+
ReportQueryBudgetError,
|
|
20
|
+
ReportSpecError,
|
|
21
|
+
ReportStateError,
|
|
22
|
+
ReportTokenError,
|
|
23
|
+
} from '@ftisindia/report-builder';
|
|
24
|
+
import { Prisma } from '@prisma/client';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Maps the engine's typed errors (ecosystem guide §2.1, report design §10)
|
|
28
|
+
* onto the app's HTTP error envelope. Object bodies survive the global
|
|
29
|
+
* HttpExceptionFilter, which lifts every non-message key into `error.details`
|
|
30
|
+
* — so 422 lint responses carry the engine's `issues` array (plus the §8
|
|
31
|
+
* `suggestionSql` migration snippet) verbatim, and 409 drift responses carry
|
|
32
|
+
* both counts so the client can re-confirm with fresh facts.
|
|
33
|
+
*/
|
|
34
|
+
export function mapReportsError(error: unknown): never {
|
|
35
|
+
if (error instanceof ReportSpecError || error instanceof ReportCursorError) {
|
|
36
|
+
throw new BadRequestException({ message: error.message, code: error.code });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (error instanceof ReportTokenError) {
|
|
40
|
+
if (error.expired) {
|
|
41
|
+
throw new GoneException(error.message);
|
|
42
|
+
}
|
|
43
|
+
throw new UnauthorizedException(error.message);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (error instanceof ReportLintError) {
|
|
47
|
+
throw new UnprocessableEntityException({
|
|
48
|
+
message: error.message,
|
|
49
|
+
issues: error.issues,
|
|
50
|
+
...(error.suggestionSql === undefined ? {} : { suggestionSql: error.suggestionSql }),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (error instanceof ReportDriftError) {
|
|
55
|
+
throw new ConflictException({
|
|
56
|
+
message: error.message,
|
|
57
|
+
expectedCount: error.expectedCount,
|
|
58
|
+
currentCount: error.currentCount,
|
|
59
|
+
code: 'REPORTS_SELECTION_DRIFT',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (error instanceof ReportAuthzDeniedError) {
|
|
64
|
+
throw new ForbiddenException(error.message);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (error instanceof ReportNotFoundError) {
|
|
68
|
+
throw new NotFoundException(error.message);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (error instanceof ReportConflictError || error instanceof ReportStateError) {
|
|
72
|
+
throw new ConflictException(error.message);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (error instanceof ReportQueryBudgetError) {
|
|
76
|
+
throw new RequestTimeoutException(error.message);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (error instanceof ReportExportError) {
|
|
80
|
+
throw new UnprocessableEntityException(error.message);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
|
|
84
|
+
throw new ConflictException('A report record with these unique fields already exists.');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Awaits an engine call and maps any engine-typed failure onto HTTP errors. */
|
|
91
|
+
export async function withReportsErrorMapping<T>(fn: () => Promise<T>): Promise<T> {
|
|
92
|
+
try {
|
|
93
|
+
return await fn();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
mapReportsError(error);
|
|
96
|
+
}
|
|
97
|
+
}
|