@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
|
@@ -1,12 +1,22 @@
|
|
|
1
|
-
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
|
|
2
|
-
import {
|
|
1
|
+
import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common';
|
|
2
|
+
import {
|
|
3
|
+
ApiBearerAuth,
|
|
4
|
+
ApiCreatedResponse,
|
|
5
|
+
ApiOkResponse,
|
|
6
|
+
ApiOperation,
|
|
7
|
+
ApiTags,
|
|
8
|
+
} from '@nestjs/swagger';
|
|
9
|
+
import { PaginationQueryDto } from '../../../common/dto/pagination-query.dto';
|
|
3
10
|
import { ApiErrorResponses } from '../../../common/swagger/api-error-responses';
|
|
4
11
|
import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
|
|
5
12
|
import { CurrentUser } from '../../auth/presentation/current-user.decorator';
|
|
6
13
|
import { AuthenticatedUser } from '../../auth/types/authenticated-user';
|
|
7
14
|
import { OrganisationsService } from '../application/services/organisations.service';
|
|
8
15
|
import { CreateOrganisationDto } from '../dto/create-organisation.dto';
|
|
9
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
CreateOrganisationResponseDto,
|
|
18
|
+
MyOrganisationListResponseDto,
|
|
19
|
+
} from '../dto/organisation-response.dto';
|
|
10
20
|
|
|
11
21
|
@ApiTags('Organisations')
|
|
12
22
|
@ApiBearerAuth()
|
|
@@ -14,6 +24,18 @@ import { CreateOrganisationResponseDto } from '../dto/organisation-response.dto'
|
|
|
14
24
|
export class OrganisationsController {
|
|
15
25
|
constructor(private readonly organisationsService: OrganisationsService) {}
|
|
16
26
|
|
|
27
|
+
@Get('mine')
|
|
28
|
+
@UseGuards(JwtAuthGuard)
|
|
29
|
+
@ApiOperation({ summary: 'List active organisations for the current user.' })
|
|
30
|
+
@ApiOkResponse({
|
|
31
|
+
description: 'Current user organisations.',
|
|
32
|
+
type: MyOrganisationListResponseDto,
|
|
33
|
+
})
|
|
34
|
+
@ApiErrorResponses(400, 401)
|
|
35
|
+
mine(@CurrentUser() user: AuthenticatedUser, @Query() query: PaginationQueryDto) {
|
|
36
|
+
return this.organisationsService.listMine(user, query);
|
|
37
|
+
}
|
|
38
|
+
|
|
17
39
|
@Post()
|
|
18
40
|
@UseGuards(JwtAuthGuard)
|
|
19
41
|
@ApiOperation({ summary: 'Create an organisation for the current user.' })
|
|
@@ -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
|
+
}
|
package/template/src/modules/reports/application/services/reports-export-dispatcher.service.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Injectable,
|
|
3
|
+
Logger,
|
|
4
|
+
OnApplicationBootstrap,
|
|
5
|
+
OnModuleDestroy,
|
|
6
|
+
} from '@nestjs/common';
|
|
7
|
+
import { ConfigService } from '@nestjs/config';
|
|
8
|
+
import { ReportExportService } from '@ftisindia/report-builder';
|
|
9
|
+
import { PrismaService } from '../../../../database/prisma/prisma.service';
|
|
10
|
+
import { RequestContextService } from '../../../request-context/application/services/request-context.service';
|
|
11
|
+
import { RequestReportsContext } from '../../infrastructure/request-reports-context';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reports-OWNED async export worker (report design §9). Instead of riding the
|
|
15
|
+
* forms transactional outbox, it polls the engine's own `ReportExportJob`
|
|
16
|
+
* table for PENDING rows — so the reports module needs no FormsModule. Each
|
|
17
|
+
* job is claimed atomically (PENDING → RUNNING) to prevent double-processing,
|
|
18
|
+
* then run inside a restored worker request context (source: 'worker', orgId,
|
|
19
|
+
* userId) exactly as the forms dispatcher restores context for its handlers.
|
|
20
|
+
*
|
|
21
|
+
* The engine's ReportExportService.runJob streams from one REPEATABLE READ
|
|
22
|
+
* snapshot into the reports export storage and marks the row DONE/FAILED.
|
|
23
|
+
*/
|
|
24
|
+
@Injectable()
|
|
25
|
+
export class ReportsExportDispatcherService implements OnApplicationBootstrap, OnModuleDestroy {
|
|
26
|
+
private readonly logger = new Logger('ReportsExportWorker');
|
|
27
|
+
private timer?: NodeJS.Timeout;
|
|
28
|
+
private ticking = false;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly config: ConfigService,
|
|
32
|
+
private readonly prisma: PrismaService,
|
|
33
|
+
private readonly exports: ReportExportService,
|
|
34
|
+
private readonly requestContext: RequestContextService,
|
|
35
|
+
private readonly reportsContext: RequestReportsContext,
|
|
36
|
+
) {}
|
|
37
|
+
|
|
38
|
+
onApplicationBootstrap(): void {
|
|
39
|
+
if (this.config.get<boolean>('reports.exportWorkerEnabled') === false) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const pollMs = this.config.get<number>('reports.exportPollMs') ?? 5000;
|
|
43
|
+
this.timer = setInterval(() => {
|
|
44
|
+
void this.tick();
|
|
45
|
+
}, pollMs);
|
|
46
|
+
this.timer.unref?.();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
onModuleDestroy(): void {
|
|
50
|
+
if (this.timer) {
|
|
51
|
+
clearInterval(this.timer);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One poll cycle — exposed for tests / manual draining. */
|
|
56
|
+
async runOnce(batchSize = 10): Promise<{ claimed: number; done: number; failed: number }> {
|
|
57
|
+
const pending = await this.prisma.reportExportJob.findMany({
|
|
58
|
+
where: { status: 'PENDING' },
|
|
59
|
+
orderBy: { createdAt: 'asc' },
|
|
60
|
+
take: batchSize,
|
|
61
|
+
select: { id: true, orgId: true, requestedBy: true },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
let claimed = 0;
|
|
65
|
+
let done = 0;
|
|
66
|
+
let failed = 0;
|
|
67
|
+
for (const job of pending) {
|
|
68
|
+
// Atomic claim: only the worker that flips PENDING → RUNNING runs it.
|
|
69
|
+
const claim = await this.prisma.reportExportJob.updateMany({
|
|
70
|
+
where: { id: job.id, orgId: job.orgId, status: 'PENDING' },
|
|
71
|
+
data: { status: 'RUNNING' },
|
|
72
|
+
});
|
|
73
|
+
if (claim.count !== 1) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
claimed += 1;
|
|
77
|
+
|
|
78
|
+
const outcome = await this.requestContext.run(
|
|
79
|
+
{
|
|
80
|
+
source: 'worker',
|
|
81
|
+
orgId: job.orgId,
|
|
82
|
+
userId: job.requestedBy,
|
|
83
|
+
},
|
|
84
|
+
async () => {
|
|
85
|
+
try {
|
|
86
|
+
await this.exports.runJob(job.orgId, job.id, this.reportsContext);
|
|
87
|
+
return 'done' as const;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
this.logger.error(
|
|
90
|
+
`Export job ${job.id} failed.`,
|
|
91
|
+
error instanceof Error ? error.stack : String(error),
|
|
92
|
+
);
|
|
93
|
+
return 'failed' as const;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
if (outcome === 'done') {
|
|
98
|
+
done += 1;
|
|
99
|
+
} else {
|
|
100
|
+
failed += 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { claimed, done, failed };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private async tick(): Promise<void> {
|
|
107
|
+
if (this.ticking) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
this.ticking = true;
|
|
111
|
+
try {
|
|
112
|
+
const result = await this.runOnce();
|
|
113
|
+
if (result.claimed > 0) {
|
|
114
|
+
this.logger.log(
|
|
115
|
+
`Export cycle: claimed ${result.claimed}, done ${result.done}, failed ${result.failed}.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
this.logger.error('Export cycle failed.', error instanceof Error ? error.stack : String(error));
|
|
120
|
+
} finally {
|
|
121
|
+
this.ticking = false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
2
|
+
import type { Readable } from 'node:stream';
|
|
3
|
+
import { ReportExportService } from '@ftisindia/report-builder';
|
|
4
|
+
import type { ExportOutcome, QuerySpec } from '@ftisindia/report-builder';
|
|
5
|
+
import { RequestContextService } from '../../../request-context/application/services/request-context.service';
|
|
6
|
+
import { ExportRequestDto } from '../../dto/export-request.dto';
|
|
7
|
+
import { RequestReportsContext } from '../../infrastructure/request-reports-context';
|
|
8
|
+
import { LocalDiskReportExportStorage } from '../../infrastructure/storage/local-disk-export-storage.adapter';
|
|
9
|
+
import { withReportsErrorMapping } from './reports-error.mapper';
|
|
10
|
+
|
|
11
|
+
const FORMAT_MIME: Record<string, string> = {
|
|
12
|
+
csv: 'text/csv',
|
|
13
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Thin HTTP-facing wrapper over the engine's ReportExportService. The sync-vs-
|
|
18
|
+
* async split by ACTUAL size, snapshot consistency, exportable:false stripping,
|
|
19
|
+
* and the PII-egress audit all live in the engine (design §9). The controller
|
|
20
|
+
* turns the ExportOutcome union into a StreamableFile or a 202 job response.
|
|
21
|
+
*
|
|
22
|
+
* Async export files land in the reports-OWNED export storage (no dependency
|
|
23
|
+
* on the forms file machinery); `download` streams a finished job's file back.
|
|
24
|
+
*/
|
|
25
|
+
@Injectable()
|
|
26
|
+
export class ReportsExportsService {
|
|
27
|
+
constructor(
|
|
28
|
+
private readonly engine: ReportExportService,
|
|
29
|
+
private readonly reportsContext: RequestReportsContext,
|
|
30
|
+
private readonly requestContext: RequestContextService,
|
|
31
|
+
private readonly storage: LocalDiskReportExportStorage,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
async requestExport(orgId: string, key: string, dto: ExportRequestDto): Promise<ExportOutcome> {
|
|
35
|
+
this.requestContext.assertOrgScope(orgId);
|
|
36
|
+
return withReportsErrorMapping(() =>
|
|
37
|
+
this.engine.requestExport(
|
|
38
|
+
orgId,
|
|
39
|
+
key,
|
|
40
|
+
{
|
|
41
|
+
format: dto.format,
|
|
42
|
+
spec: dto.spec as unknown as QuerySpec | undefined,
|
|
43
|
+
columns: dto.columns,
|
|
44
|
+
},
|
|
45
|
+
this.reportsContext,
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async getJob(orgId: string, jobId: string) {
|
|
51
|
+
this.requestContext.assertOrgScope(orgId);
|
|
52
|
+
return withReportsErrorMapping(() => this.engine.getJob(orgId, jobId, this.reportsContext));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Stream a finished async-export file (the job's fileId is its storage key). */
|
|
56
|
+
async download(
|
|
57
|
+
orgId: string,
|
|
58
|
+
jobId: string,
|
|
59
|
+
): Promise<{ stream: Readable; fileName: string; mimeType: string }> {
|
|
60
|
+
this.requestContext.assertOrgScope(orgId);
|
|
61
|
+
const job = await withReportsErrorMapping(() =>
|
|
62
|
+
this.engine.getJob(orgId, jobId, this.reportsContext),
|
|
63
|
+
);
|
|
64
|
+
if (job.status !== 'DONE' || job.fileId === null) {
|
|
65
|
+
throw new NotFoundException('The export is not ready for download.');
|
|
66
|
+
}
|
|
67
|
+
const format = job.spec.format;
|
|
68
|
+
return {
|
|
69
|
+
stream: this.storage.read(job.fileId),
|
|
70
|
+
fileName: `${job.reportKey}-v${job.reportVersion}.${format}`,
|
|
71
|
+
mimeType: FORMAT_MIME[format] ?? 'application/octet-stream',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { ReportQueryService } from '@ftisindia/report-builder';
|
|
3
|
+
import type { QuerySpec } from '@ftisindia/report-builder';
|
|
4
|
+
import { RequestContextService } from '../../../request-context/application/services/request-context.service';
|
|
5
|
+
import { QuerySpecDto } from '../../dto/query-spec.dto';
|
|
6
|
+
import { RequestReportsContext } from '../../infrastructure/request-reports-context';
|
|
7
|
+
import { withReportsErrorMapping } from './reports-error.mapper';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Thin HTTP-facing wrapper over the engine's ReportQueryService — THE grid
|
|
11
|
+
* endpoint plus the self-describing meta descriptor (design §4). The DTO
|
|
12
|
+
* validated the envelope; the engine's compiler validates the semantics.
|
|
13
|
+
*/
|
|
14
|
+
@Injectable()
|
|
15
|
+
export class ReportsQueriesService {
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly engine: ReportQueryService,
|
|
18
|
+
private readonly reportsContext: RequestReportsContext,
|
|
19
|
+
private readonly requestContext: RequestContextService,
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
async query(orgId: string, key: string, spec: QuerySpecDto) {
|
|
23
|
+
this.requestContext.assertOrgScope(orgId);
|
|
24
|
+
return withReportsErrorMapping(() =>
|
|
25
|
+
this.engine.query(orgId, key, spec as unknown as QuerySpec, this.reportsContext),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async meta(orgId: string, key: string, version: number | null) {
|
|
30
|
+
this.requestContext.assertOrgScope(orgId);
|
|
31
|
+
return withReportsErrorMapping(() =>
|
|
32
|
+
this.engine.meta(orgId, key, version, this.reportsContext),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { DEFAULT_REPORTS_POLICY } from '@ftisindia/report-builder';
|
|
3
|
+
import type { ReportsPolicy } from '@ftisindia/report-builder';
|
|
4
|
+
import { PrismaService } from '../../../../database/prisma/prisma.service';
|
|
5
|
+
import { settingDefinitions } from '../../../settings/types/setting-definitions';
|
|
6
|
+
|
|
7
|
+
const REPORTS_SETTING_KEYS = [
|
|
8
|
+
'reports.statementTimeoutMs',
|
|
9
|
+
'reports.exportMaxSnapshotSeconds',
|
|
10
|
+
'reports.maxRowsSync',
|
|
11
|
+
'reports.countCap',
|
|
12
|
+
'reports.liveTierMaxRows',
|
|
13
|
+
'reports.resultCacheTtlMs',
|
|
14
|
+
'reports.tagVocabulary',
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Reads the org's reports policy from typed settings, falling back to the
|
|
19
|
+
* setting definitions' defaults for unset keys (the SettingsService 404s on
|
|
20
|
+
* unset keys, which is wrong for policy reads). Keys without a setting (page
|
|
21
|
+
* sizes, batch sizes, token TTLs, ...) keep the engine's DEFAULT_REPORTS_POLICY
|
|
22
|
+
* values. The result feeds every engine call that takes a ReportsPolicy —
|
|
23
|
+
* statement budgets, count caps, tier gates, export bounds (design §5.2/§8/§9).
|
|
24
|
+
*/
|
|
25
|
+
@Injectable()
|
|
26
|
+
export class ReportsSettingsReader {
|
|
27
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
28
|
+
|
|
29
|
+
async policyFor(orgId: string): Promise<ReportsPolicy> {
|
|
30
|
+
const rows = await this.prisma.organisationSetting.findMany({
|
|
31
|
+
where: { orgId, key: { in: [...REPORTS_SETTING_KEYS] } },
|
|
32
|
+
select: { key: true, value: true },
|
|
33
|
+
});
|
|
34
|
+
const values = new Map(rows.map((row) => [row.key, row.value as unknown]));
|
|
35
|
+
const read = <T>(key: (typeof REPORTS_SETTING_KEYS)[number]): T =>
|
|
36
|
+
(values.has(key) ? values.get(key) : settingDefinitions[key].defaultValue) as T;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
...DEFAULT_REPORTS_POLICY,
|
|
40
|
+
statementTimeoutMs: read<number>('reports.statementTimeoutMs'),
|
|
41
|
+
exportMaxSnapshotSeconds: read<number>('reports.exportMaxSnapshotSeconds'),
|
|
42
|
+
maxRowsSync: read<number>('reports.maxRowsSync'),
|
|
43
|
+
countCap: read<number>('reports.countCap'),
|
|
44
|
+
liveTierMaxRows: read<number>('reports.liveTierMaxRows'),
|
|
45
|
+
resultCacheTtlMs: read<number>('reports.resultCacheTtlMs'),
|
|
46
|
+
tagVocabulary: read<string[]>('reports.tagVocabulary'),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { ReportViewService } from '@ftisindia/report-builder';
|
|
3
|
+
import type { QuerySpec } from '@ftisindia/report-builder';
|
|
4
|
+
import { RequestContextService } from '../../../request-context/application/services/request-context.service';
|
|
5
|
+
import { CreateSavedViewDto } from '../../dto/create-saved-view.dto';
|
|
6
|
+
import { ListViewsQueryDto } from '../../dto/list-views-query.dto';
|
|
7
|
+
import { UpdateSavedViewDto } from '../../dto/update-saved-view.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 ReportViewService. Personal vs
|
|
13
|
+
* shared semantics (ownerId null ⇒ shared, gated by reports.update) and the
|
|
14
|
+
* per-view compatibility verdicts live in the engine (design §4, finding #9).
|
|
15
|
+
*/
|
|
16
|
+
@Injectable()
|
|
17
|
+
export class ReportsViewsService {
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly engine: ReportViewService,
|
|
20
|
+
private readonly reportsContext: RequestReportsContext,
|
|
21
|
+
private readonly requestContext: RequestContextService,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
async list(orgId: string, key: string, query: ListViewsQueryDto) {
|
|
25
|
+
this.requestContext.assertOrgScope(orgId);
|
|
26
|
+
const items = await withReportsErrorMapping(() =>
|
|
27
|
+
this.engine.list(orgId, key, query.version ?? null, this.reportsContext),
|
|
28
|
+
);
|
|
29
|
+
return { items };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async createPersonal(orgId: string, key: string, dto: CreateSavedViewDto) {
|
|
33
|
+
this.requestContext.assertOrgScope(orgId);
|
|
34
|
+
return withReportsErrorMapping(() =>
|
|
35
|
+
this.engine.createPersonal(
|
|
36
|
+
orgId,
|
|
37
|
+
key,
|
|
38
|
+
{ name: dto.name, spec: dto.spec as unknown as QuerySpec },
|
|
39
|
+
this.reportsContext,
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async createShared(orgId: string, key: string, dto: CreateSavedViewDto) {
|
|
45
|
+
this.requestContext.assertOrgScope(orgId);
|
|
46
|
+
return withReportsErrorMapping(() =>
|
|
47
|
+
this.engine.createShared(
|
|
48
|
+
orgId,
|
|
49
|
+
key,
|
|
50
|
+
{ name: dto.name, spec: dto.spec as unknown as QuerySpec },
|
|
51
|
+
this.reportsContext,
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async update(orgId: string, key: string, viewId: string, dto: UpdateSavedViewDto) {
|
|
57
|
+
this.requestContext.assertOrgScope(orgId);
|
|
58
|
+
return withReportsErrorMapping(() =>
|
|
59
|
+
this.engine.update(
|
|
60
|
+
orgId,
|
|
61
|
+
key,
|
|
62
|
+
viewId,
|
|
63
|
+
{
|
|
64
|
+
...(dto.name === undefined ? {} : { name: dto.name }),
|
|
65
|
+
...(dto.spec === undefined ? {} : { spec: dto.spec as unknown as QuerySpec }),
|
|
66
|
+
},
|
|
67
|
+
this.reportsContext,
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async delete(orgId: string, key: string, viewId: string) {
|
|
73
|
+
this.requestContext.assertOrgScope(orgId);
|
|
74
|
+
await withReportsErrorMapping(() =>
|
|
75
|
+
this.engine.delete(orgId, key, viewId, this.reportsContext),
|
|
76
|
+
);
|
|
77
|
+
return { deleted: true };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
2
|
+
|
|
3
|
+
export class ActionResultResponseDto {
|
|
4
|
+
@ApiProperty({ enum: ['DONE'], example: 'DONE' })
|
|
5
|
+
status!: 'DONE';
|
|
6
|
+
|
|
7
|
+
@ApiProperty({ example: 240 })
|
|
8
|
+
affectedRows!: number;
|
|
9
|
+
|
|
10
|
+
@ApiProperty({
|
|
11
|
+
nullable: true,
|
|
12
|
+
description: 'Whatever the delegated handler returned (e.g. { updated: 12 }).',
|
|
13
|
+
})
|
|
14
|
+
result!: unknown;
|
|
15
|
+
|
|
16
|
+
@ApiProperty({
|
|
17
|
+
example: false,
|
|
18
|
+
description: 'True when this call replayed a previously recorded idempotent run (§6.3).',
|
|
19
|
+
})
|
|
20
|
+
replayed!: boolean;
|
|
21
|
+
}
|