@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
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { Prisma, ReportSavedView as ReportSavedViewRow } from '@prisma/client';
|
|
3
|
+
import {
|
|
4
|
+
ReportConflictError,
|
|
5
|
+
ReportNotFoundError,
|
|
6
|
+
type EngineTx,
|
|
7
|
+
type NewSavedViewRecord,
|
|
8
|
+
type QuerySpec,
|
|
9
|
+
type SavedViewRecord,
|
|
10
|
+
type SavedViewStore,
|
|
11
|
+
} from '@ftisindia/report-builder';
|
|
12
|
+
import { PrismaService } from '../../../../database/prisma/prisma.service';
|
|
13
|
+
|
|
14
|
+
@Injectable()
|
|
15
|
+
export class PrismaSavedViewStore implements SavedViewStore {
|
|
16
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
17
|
+
|
|
18
|
+
private client(tx?: EngineTx) {
|
|
19
|
+
return tx ? (tx as unknown as Prisma.TransactionClient) : this.prisma;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async create(record: NewSavedViewRecord, tx?: EngineTx): Promise<SavedViewRecord> {
|
|
23
|
+
try {
|
|
24
|
+
const row = await this.client(tx).reportSavedView.create({
|
|
25
|
+
data: {
|
|
26
|
+
orgId: record.orgId,
|
|
27
|
+
reportKey: record.reportKey,
|
|
28
|
+
reportVersion: record.reportVersion,
|
|
29
|
+
name: record.name,
|
|
30
|
+
spec: record.spec as unknown as Prisma.InputJsonValue,
|
|
31
|
+
ownerId: record.ownerId,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return this.toRecord(row);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (this.isDuplicateViewName(error)) {
|
|
38
|
+
throw new ReportConflictError(
|
|
39
|
+
`A view named "${record.name}" already exists for this report.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async update(
|
|
47
|
+
orgId: string,
|
|
48
|
+
id: string,
|
|
49
|
+
patch: { name?: string; spec?: QuerySpec; reportVersion?: number },
|
|
50
|
+
tx?: EngineTx,
|
|
51
|
+
): Promise<SavedViewRecord> {
|
|
52
|
+
const client = this.client(tx);
|
|
53
|
+
|
|
54
|
+
const existing = await client.reportSavedView.findFirst({
|
|
55
|
+
where: { id, orgId },
|
|
56
|
+
select: { id: true },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!existing) {
|
|
60
|
+
throw new ReportNotFoundError(`Saved view ${id} was not found in this organisation.`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const row = await client.reportSavedView.update({
|
|
65
|
+
where: { id: existing.id },
|
|
66
|
+
data: {
|
|
67
|
+
...(patch.name !== undefined ? { name: patch.name } : {}),
|
|
68
|
+
...(patch.spec !== undefined
|
|
69
|
+
? { spec: patch.spec as unknown as Prisma.InputJsonValue }
|
|
70
|
+
: {}),
|
|
71
|
+
...(patch.reportVersion !== undefined ? { reportVersion: patch.reportVersion } : {}),
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return this.toRecord(row);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (this.isDuplicateViewName(error)) {
|
|
78
|
+
throw new ReportConflictError(
|
|
79
|
+
patch.name === undefined
|
|
80
|
+
? 'A view with this name already exists for this report.'
|
|
81
|
+
: `A view named "${patch.name}" already exists for this report.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async delete(orgId: string, id: string, tx?: EngineTx): Promise<void> {
|
|
89
|
+
await this.client(tx).reportSavedView.deleteMany({
|
|
90
|
+
where: { id, orgId },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async findById(orgId: string, id: string): Promise<SavedViewRecord | null> {
|
|
95
|
+
const row = await this.client().reportSavedView.findFirst({
|
|
96
|
+
where: { id, orgId },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return row ? this.toRecord(row) : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async listByReport(
|
|
103
|
+
orgId: string,
|
|
104
|
+
reportKey: string,
|
|
105
|
+
userId: string,
|
|
106
|
+
): Promise<SavedViewRecord[]> {
|
|
107
|
+
const rows = await this.client().reportSavedView.findMany({
|
|
108
|
+
// The caller's PERSONAL views plus SHARED views (ownerId null) — never
|
|
109
|
+
// another user's personal views (report design §4/§12).
|
|
110
|
+
where: { orgId, reportKey, OR: [{ ownerId: userId }, { ownerId: null }] },
|
|
111
|
+
orderBy: [{ name: 'asc' }, { createdAt: 'asc' }],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return rows.map((row) => this.toRecord(row));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* View-name uniqueness lives in PARTIAL UNIQUE INDEXES shipped as raw SQL
|
|
119
|
+
* (report design §12, finding #5 — Postgres treats NULL ownerIds as
|
|
120
|
+
* distinct, so a Prisma @@unique cannot express it). Prisma maps the
|
|
121
|
+
* underlying 23505 to P2002 when it can; a raw partial-unique violation may
|
|
122
|
+
* also surface as an unknown request error, so match the index-name prefix
|
|
123
|
+
* ("report_view_shared_uq" / "report_view_personal_uq") as the fallback.
|
|
124
|
+
*/
|
|
125
|
+
private isDuplicateViewName(error: unknown): boolean {
|
|
126
|
+
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return error instanceof Error && error.message.includes('report_view_');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private toRecord(row: ReportSavedViewRow): SavedViewRecord {
|
|
133
|
+
return {
|
|
134
|
+
id: row.id,
|
|
135
|
+
orgId: row.orgId,
|
|
136
|
+
reportKey: row.reportKey,
|
|
137
|
+
reportVersion: row.reportVersion,
|
|
138
|
+
name: row.name,
|
|
139
|
+
spec: row.spec as unknown as QuerySpec,
|
|
140
|
+
ownerId: row.ownerId,
|
|
141
|
+
createdAt: row.createdAt,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Body, Controller, HttpCode, Param, Post, UseGuards } from '@nestjs/common';
|
|
2
|
+
import {
|
|
3
|
+
ApiBearerAuth,
|
|
4
|
+
ApiOkResponse,
|
|
5
|
+
ApiOperation,
|
|
6
|
+
ApiParam,
|
|
7
|
+
ApiTags,
|
|
8
|
+
ApiUnprocessableEntityResponse,
|
|
9
|
+
} from '@nestjs/swagger';
|
|
10
|
+
import { ErrorResponseDto } from '../../../common/dto/error-response.dto';
|
|
11
|
+
import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
|
|
12
|
+
import { PermissionGuard } from '../../access-control/application/services/permission.guard';
|
|
13
|
+
import { RequirePermissions } from '../../access-control/presentation/permissions.decorator';
|
|
14
|
+
import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
|
|
15
|
+
import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
|
|
16
|
+
import { ReportsActionsService } from '../application/services/reports-actions.service';
|
|
17
|
+
import { ActionResultResponseDto } from '../dto/action-result-response.dto';
|
|
18
|
+
import { ExecuteActionDto } from '../dto/execute-action.dto';
|
|
19
|
+
import { PrepareActionDto } from '../dto/prepare-action.dto';
|
|
20
|
+
import { PrepareActionResponseDto } from '../dto/prepare-action-response.dto';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Row actions (design §6) — verbs that DELEGATE, never a second write path.
|
|
24
|
+
* The route-level key is reports.read (the grid entry point); each action's
|
|
25
|
+
* own permission keys (action.requiredPermissions, e.g.
|
|
26
|
+
* 'formSubmissions.update') are enforced INSIDE the engine at execute-time —
|
|
27
|
+
* the second of §6.1's two layers (attach-time ran at definition save).
|
|
28
|
+
*/
|
|
29
|
+
@ApiTags('Reports')
|
|
30
|
+
@ApiBearerAuth()
|
|
31
|
+
@ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
|
|
32
|
+
@ApiParam({ name: 'key', description: 'Report definition key.' })
|
|
33
|
+
@ApiParam({ name: 'name', description: 'Registered row-action name.' })
|
|
34
|
+
@ApiProtectedErrorResponses(404, 409, 410)
|
|
35
|
+
@ApiUnprocessableEntityResponse({
|
|
36
|
+
description: 'The action input failed validation.',
|
|
37
|
+
type: ErrorResponseDto,
|
|
38
|
+
})
|
|
39
|
+
@Controller('organisations/:orgId/reports')
|
|
40
|
+
@UseGuards(JwtAuthGuard, OrgScopeGuard, PermissionGuard)
|
|
41
|
+
export class ReportsActionsController {
|
|
42
|
+
constructor(private readonly reportsActionsService: ReportsActionsService) {}
|
|
43
|
+
|
|
44
|
+
@Post(':key/actions/:name/prepare')
|
|
45
|
+
@HttpCode(200)
|
|
46
|
+
@RequirePermissions('reports.read')
|
|
47
|
+
@ApiOperation({
|
|
48
|
+
summary:
|
|
49
|
+
'Prepare a byFilter bulk action: resolve the selection NOW and sign what was seen (§6.3 step 1).',
|
|
50
|
+
})
|
|
51
|
+
@ApiOkResponse({
|
|
52
|
+
description: 'Expected count plus the signed, short-TTL action token.',
|
|
53
|
+
type: PrepareActionResponseDto,
|
|
54
|
+
})
|
|
55
|
+
prepare(
|
|
56
|
+
@Param('orgId') orgId: string,
|
|
57
|
+
@Param('key') key: string,
|
|
58
|
+
@Param('name') name: string,
|
|
59
|
+
@Body() dto: PrepareActionDto,
|
|
60
|
+
) {
|
|
61
|
+
return this.reportsActionsService.prepare(orgId, key, name, dto);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Post(':key/actions/:name')
|
|
65
|
+
@HttpCode(200)
|
|
66
|
+
@RequirePermissions('reports.read')
|
|
67
|
+
@ApiOperation({
|
|
68
|
+
summary:
|
|
69
|
+
'Execute a row/bulk action: byIds directly, byFilter via the prepared token with drift detection and the idempotency ledger (§6.3).',
|
|
70
|
+
})
|
|
71
|
+
@ApiOkResponse({
|
|
72
|
+
description: 'Execution outcome.',
|
|
73
|
+
type: ActionResultResponseDto,
|
|
74
|
+
})
|
|
75
|
+
execute(
|
|
76
|
+
@Param('orgId') orgId: string,
|
|
77
|
+
@Param('key') key: string,
|
|
78
|
+
@Param('name') name: string,
|
|
79
|
+
@Body() dto: ExecuteActionDto,
|
|
80
|
+
) {
|
|
81
|
+
return this.reportsActionsService.execute(orgId, key, name, dto);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Body,
|
|
3
|
+
Controller,
|
|
4
|
+
Get,
|
|
5
|
+
HttpCode,
|
|
6
|
+
Param,
|
|
7
|
+
Patch,
|
|
8
|
+
Post,
|
|
9
|
+
Query,
|
|
10
|
+
UseGuards,
|
|
11
|
+
} from '@nestjs/common';
|
|
12
|
+
import {
|
|
13
|
+
ApiBearerAuth,
|
|
14
|
+
ApiCreatedResponse,
|
|
15
|
+
ApiOkResponse,
|
|
16
|
+
ApiOperation,
|
|
17
|
+
ApiParam,
|
|
18
|
+
ApiTags,
|
|
19
|
+
ApiUnprocessableEntityResponse,
|
|
20
|
+
} from '@nestjs/swagger';
|
|
21
|
+
import { ErrorResponseDto } from '../../../common/dto/error-response.dto';
|
|
22
|
+
import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
|
|
23
|
+
import { PermissionGuard } from '../../access-control/application/services/permission.guard';
|
|
24
|
+
import { RequirePermissions } from '../../access-control/presentation/permissions.decorator';
|
|
25
|
+
import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
|
|
26
|
+
import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
|
|
27
|
+
import { ReportsDefinitionsService } from '../application/services/reports-definitions.service';
|
|
28
|
+
import { ReportsQueriesService } from '../application/services/reports-queries.service';
|
|
29
|
+
import { CreateReportDefinitionDto } from '../dto/create-report-definition.dto';
|
|
30
|
+
import { ListReportsQueryDto } from '../dto/list-reports-query.dto';
|
|
31
|
+
import {
|
|
32
|
+
ReportArchivedResponseDto,
|
|
33
|
+
ReportDefinitionListResponseDto,
|
|
34
|
+
ReportDefinitionResponseDto,
|
|
35
|
+
} from '../dto/report-definition-response.dto';
|
|
36
|
+
import { ReportMetaQueryDto } from '../dto/report-meta-query.dto';
|
|
37
|
+
import { ReportMetaResponseDto } from '../dto/report-meta-response.dto';
|
|
38
|
+
import { UpdateReportDefinitionDto } from '../dto/update-report-definition.dto';
|
|
39
|
+
|
|
40
|
+
@ApiTags('Reports')
|
|
41
|
+
@ApiBearerAuth()
|
|
42
|
+
@ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
|
|
43
|
+
@ApiProtectedErrorResponses(404, 409)
|
|
44
|
+
@ApiUnprocessableEntityResponse({
|
|
45
|
+
description: 'The report definition failed engine validation or linting (§5.2/§8).',
|
|
46
|
+
type: ErrorResponseDto,
|
|
47
|
+
})
|
|
48
|
+
@Controller('organisations/:orgId/reports')
|
|
49
|
+
@UseGuards(JwtAuthGuard, OrgScopeGuard, PermissionGuard)
|
|
50
|
+
export class ReportsDefinitionsController {
|
|
51
|
+
constructor(
|
|
52
|
+
private readonly reportsDefinitionsService: ReportsDefinitionsService,
|
|
53
|
+
private readonly reportsQueriesService: ReportsQueriesService,
|
|
54
|
+
) {}
|
|
55
|
+
|
|
56
|
+
@Get()
|
|
57
|
+
@RequirePermissions('reports.read')
|
|
58
|
+
@ApiOperation({ summary: 'List report definitions.' })
|
|
59
|
+
@ApiOkResponse({
|
|
60
|
+
description: 'Report definitions.',
|
|
61
|
+
type: ReportDefinitionListResponseDto,
|
|
62
|
+
})
|
|
63
|
+
list(@Param('orgId') orgId: string, @Query() query: ListReportsQueryDto) {
|
|
64
|
+
return this.reportsDefinitionsService.list(orgId, query);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Post()
|
|
68
|
+
@RequirePermissions('reports.create')
|
|
69
|
+
@ApiOperation({ summary: 'Create a new draft report definition version.' })
|
|
70
|
+
@ApiCreatedResponse({
|
|
71
|
+
description: 'Created draft definition.',
|
|
72
|
+
type: ReportDefinitionResponseDto,
|
|
73
|
+
})
|
|
74
|
+
create(@Param('orgId') orgId: string, @Body() dto: CreateReportDefinitionDto) {
|
|
75
|
+
return this.reportsDefinitionsService.create(orgId, dto);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Get(':key')
|
|
79
|
+
@RequirePermissions('reports.read')
|
|
80
|
+
@ApiParam({ name: 'key', description: 'Report definition key.' })
|
|
81
|
+
@ApiOperation({ summary: 'Return the latest version of a report definition.' })
|
|
82
|
+
@ApiOkResponse({
|
|
83
|
+
description: 'Latest definition version.',
|
|
84
|
+
type: ReportDefinitionResponseDto,
|
|
85
|
+
})
|
|
86
|
+
getLatest(@Param('orgId') orgId: string, @Param('key') key: string) {
|
|
87
|
+
return this.reportsDefinitionsService.getLatest(orgId, key);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@Patch(':key')
|
|
91
|
+
@RequirePermissions('reports.update')
|
|
92
|
+
@ApiParam({ name: 'key', description: 'Report definition key.' })
|
|
93
|
+
@ApiOperation({
|
|
94
|
+
summary:
|
|
95
|
+
'Edit the draft. Editing a published version opens a new draft version (published rows are immutable, §2).',
|
|
96
|
+
})
|
|
97
|
+
@ApiOkResponse({
|
|
98
|
+
description: 'Updated draft definition.',
|
|
99
|
+
type: ReportDefinitionResponseDto,
|
|
100
|
+
})
|
|
101
|
+
update(
|
|
102
|
+
@Param('orgId') orgId: string,
|
|
103
|
+
@Param('key') key: string,
|
|
104
|
+
@Body() dto: UpdateReportDefinitionDto,
|
|
105
|
+
) {
|
|
106
|
+
return this.reportsDefinitionsService.update(orgId, key, dto);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Post(':key/publish')
|
|
110
|
+
@HttpCode(200)
|
|
111
|
+
@RequirePermissions('reports.publish')
|
|
112
|
+
@ApiParam({ name: 'key', description: 'Report definition key.' })
|
|
113
|
+
@ApiOperation({
|
|
114
|
+
summary:
|
|
115
|
+
'Publish the latest draft: the §5.2 layered lint runs here, failing constructively with the suggested migration SQL (§8).',
|
|
116
|
+
})
|
|
117
|
+
@ApiOkResponse({
|
|
118
|
+
description: 'Published definition.',
|
|
119
|
+
type: ReportDefinitionResponseDto,
|
|
120
|
+
})
|
|
121
|
+
publish(@Param('orgId') orgId: string, @Param('key') key: string) {
|
|
122
|
+
return this.reportsDefinitionsService.publish(orgId, key);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@Post(':key/archive')
|
|
126
|
+
@HttpCode(200)
|
|
127
|
+
@RequirePermissions('reports.archive')
|
|
128
|
+
@ApiParam({ name: 'key', description: 'Report definition key.' })
|
|
129
|
+
@ApiOperation({ summary: 'Archive every version of a report definition.' })
|
|
130
|
+
@ApiOkResponse({
|
|
131
|
+
description: 'Archive confirmation.',
|
|
132
|
+
type: ReportArchivedResponseDto,
|
|
133
|
+
})
|
|
134
|
+
archive(@Param('orgId') orgId: string, @Param('key') key: string) {
|
|
135
|
+
return this.reportsDefinitionsService.archive(orgId, key);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@Get(':key/meta')
|
|
139
|
+
@RequirePermissions('reports.read')
|
|
140
|
+
@ApiParam({ name: 'key', description: 'Report definition key.' })
|
|
141
|
+
@ApiOperation({
|
|
142
|
+
summary:
|
|
143
|
+
'Return the self-describing grid descriptor: columns, typed operators, actions, export config (§4).',
|
|
144
|
+
})
|
|
145
|
+
@ApiOkResponse({
|
|
146
|
+
description: 'Report metadata descriptor.',
|
|
147
|
+
type: ReportMetaResponseDto,
|
|
148
|
+
})
|
|
149
|
+
meta(
|
|
150
|
+
@Param('orgId') orgId: string,
|
|
151
|
+
@Param('key') key: string,
|
|
152
|
+
@Query() query: ReportMetaQueryDto,
|
|
153
|
+
) {
|
|
154
|
+
return this.reportsQueriesService.meta(orgId, key, query.version ?? null);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Controller, Get, Param, ParseUUIDPipe, StreamableFile, UseGuards } from '@nestjs/common';
|
|
2
|
+
import {
|
|
3
|
+
ApiBearerAuth,
|
|
4
|
+
ApiOkResponse,
|
|
5
|
+
ApiOperation,
|
|
6
|
+
ApiParam,
|
|
7
|
+
ApiProduces,
|
|
8
|
+
ApiTags,
|
|
9
|
+
} from '@nestjs/swagger';
|
|
10
|
+
import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
|
|
11
|
+
import { PermissionGuard } from '../../access-control/application/services/permission.guard';
|
|
12
|
+
import { RequirePermissions } from '../../access-control/presentation/permissions.decorator';
|
|
13
|
+
import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
|
|
14
|
+
import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
|
|
15
|
+
import { ReportsExportsService } from '../application/services/reports-exports.service';
|
|
16
|
+
import { ExportJobResponseDto } from '../dto/export-job-response.dto';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* MUST be registered BEFORE ReportsDefinitionsController in the module's
|
|
20
|
+
* controllers array: both controllers share the 'organisations/:orgId/reports'
|
|
21
|
+
* base path, and the literal 'exports' segment has to win over the ':key'
|
|
22
|
+
* parameter route ('exports' is a reserved report key for the same reason).
|
|
23
|
+
*/
|
|
24
|
+
@ApiTags('Reports')
|
|
25
|
+
@ApiBearerAuth()
|
|
26
|
+
@ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
|
|
27
|
+
@ApiProtectedErrorResponses(404)
|
|
28
|
+
@Controller('organisations/:orgId/reports/exports')
|
|
29
|
+
@UseGuards(JwtAuthGuard, OrgScopeGuard, PermissionGuard)
|
|
30
|
+
export class ReportsExportJobsController {
|
|
31
|
+
constructor(private readonly reportsExportsService: ReportsExportsService) {}
|
|
32
|
+
|
|
33
|
+
@Get(':jobId')
|
|
34
|
+
@RequirePermissions('reports.export')
|
|
35
|
+
@ApiParam({ name: 'jobId', description: 'Export job ID.', format: 'uuid' })
|
|
36
|
+
@ApiOperation({ summary: 'Return the status of an async export job (§9/§10).' })
|
|
37
|
+
@ApiOkResponse({
|
|
38
|
+
description: 'Export job status.',
|
|
39
|
+
type: ExportJobResponseDto,
|
|
40
|
+
})
|
|
41
|
+
getJob(@Param('orgId') orgId: string, @Param('jobId', ParseUUIDPipe) jobId: string) {
|
|
42
|
+
return this.reportsExportsService.getJob(orgId, jobId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Get(':jobId/download')
|
|
46
|
+
@RequirePermissions('reports.export')
|
|
47
|
+
@ApiParam({ name: 'jobId', description: 'Export job ID.', format: 'uuid' })
|
|
48
|
+
@ApiOperation({ summary: 'Download a finished async export file (§9).' })
|
|
49
|
+
@ApiProduces('text/csv', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
|
50
|
+
@ApiOkResponse({ description: 'The export file stream.' })
|
|
51
|
+
async download(
|
|
52
|
+
@Param('orgId') orgId: string,
|
|
53
|
+
@Param('jobId', ParseUUIDPipe) jobId: string,
|
|
54
|
+
): Promise<StreamableFile> {
|
|
55
|
+
const file = await this.reportsExportsService.download(orgId, jobId);
|
|
56
|
+
return new StreamableFile(file.stream, {
|
|
57
|
+
type: file.mimeType,
|
|
58
|
+
disposition: `attachment; filename="${file.fileName}"`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Body,
|
|
3
|
+
Controller,
|
|
4
|
+
Header,
|
|
5
|
+
HttpCode,
|
|
6
|
+
Param,
|
|
7
|
+
Post,
|
|
8
|
+
Res,
|
|
9
|
+
StreamableFile,
|
|
10
|
+
UseGuards,
|
|
11
|
+
} from '@nestjs/common';
|
|
12
|
+
import { Readable } from 'node:stream';
|
|
13
|
+
import type { Response } from 'express';
|
|
14
|
+
import {
|
|
15
|
+
ApiAcceptedResponse,
|
|
16
|
+
ApiBearerAuth,
|
|
17
|
+
ApiOkResponse,
|
|
18
|
+
ApiOperation,
|
|
19
|
+
ApiParam,
|
|
20
|
+
ApiTags,
|
|
21
|
+
ApiUnprocessableEntityResponse,
|
|
22
|
+
} from '@nestjs/swagger';
|
|
23
|
+
import { ErrorResponseDto } from '../../../common/dto/error-response.dto';
|
|
24
|
+
import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
|
|
25
|
+
import { PermissionGuard } from '../../access-control/application/services/permission.guard';
|
|
26
|
+
import { RequirePermissions } from '../../access-control/presentation/permissions.decorator';
|
|
27
|
+
import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
|
|
28
|
+
import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
|
|
29
|
+
import { ReportsExportsService } from '../application/services/reports-exports.service';
|
|
30
|
+
import { ExportJobResponseDto } from '../dto/export-job-response.dto';
|
|
31
|
+
import { ExportRequestDto } from '../dto/export-request.dto';
|
|
32
|
+
|
|
33
|
+
@ApiTags('Reports')
|
|
34
|
+
@ApiBearerAuth()
|
|
35
|
+
@ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
|
|
36
|
+
@ApiProtectedErrorResponses(404)
|
|
37
|
+
@ApiUnprocessableEntityResponse({
|
|
38
|
+
description: 'The export was rejected (snapshot bound exceeded, no exportable columns, §9).',
|
|
39
|
+
type: ErrorResponseDto,
|
|
40
|
+
})
|
|
41
|
+
@Controller('organisations/:orgId/reports')
|
|
42
|
+
@UseGuards(JwtAuthGuard, OrgScopeGuard, PermissionGuard)
|
|
43
|
+
export class ReportsExportController {
|
|
44
|
+
constructor(private readonly reportsExportsService: ReportsExportsService) {}
|
|
45
|
+
|
|
46
|
+
@Post(':key/export')
|
|
47
|
+
@HttpCode(200)
|
|
48
|
+
@Header('Cache-Control', 'no-store')
|
|
49
|
+
@RequirePermissions('reports.export')
|
|
50
|
+
@ApiParam({ name: 'key', description: 'Report definition key.' })
|
|
51
|
+
@ApiOperation({
|
|
52
|
+
summary:
|
|
53
|
+
'Export report rows (PII egress — separately permissioned, always audited): sync stream up to the cap, 202 job above it (§9).',
|
|
54
|
+
})
|
|
55
|
+
@ApiOkResponse({ description: 'The export file stream (CSV or XLSX).' })
|
|
56
|
+
@ApiAcceptedResponse({
|
|
57
|
+
description: 'The selection exceeds the sync cap; an async export job was queued.',
|
|
58
|
+
type: ExportJobResponseDto,
|
|
59
|
+
})
|
|
60
|
+
async export(
|
|
61
|
+
@Param('orgId') orgId: string,
|
|
62
|
+
@Param('key') key: string,
|
|
63
|
+
@Body() dto: ExportRequestDto,
|
|
64
|
+
@Res({ passthrough: true }) res: Response,
|
|
65
|
+
) {
|
|
66
|
+
const outcome = await this.reportsExportsService.requestExport(orgId, key, dto);
|
|
67
|
+
if (outcome.kind === 'stream') {
|
|
68
|
+
return new StreamableFile(Readable.from(outcome.stream), {
|
|
69
|
+
type: outcome.mimeType,
|
|
70
|
+
disposition: `attachment; filename="${outcome.fileName}"`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
res.status(202);
|
|
74
|
+
return outcome.job;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Body, Controller, HttpCode, Param, Post, UseGuards } from '@nestjs/common';
|
|
2
|
+
import {
|
|
3
|
+
ApiBearerAuth,
|
|
4
|
+
ApiOkResponse,
|
|
5
|
+
ApiOperation,
|
|
6
|
+
ApiParam,
|
|
7
|
+
ApiRequestTimeoutResponse,
|
|
8
|
+
ApiTags,
|
|
9
|
+
} from '@nestjs/swagger';
|
|
10
|
+
import { ErrorResponseDto } from '../../../common/dto/error-response.dto';
|
|
11
|
+
import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
|
|
12
|
+
import { PermissionGuard } from '../../access-control/application/services/permission.guard';
|
|
13
|
+
import { RequirePermissions } from '../../access-control/presentation/permissions.decorator';
|
|
14
|
+
import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
|
|
15
|
+
import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
|
|
16
|
+
import { ReportsQueriesService } from '../application/services/reports-queries.service';
|
|
17
|
+
import { QueryResponseDto } from '../dto/query-response.dto';
|
|
18
|
+
import { QuerySpecDto } from '../dto/query-spec.dto';
|
|
19
|
+
|
|
20
|
+
@ApiTags('Reports')
|
|
21
|
+
@ApiBearerAuth()
|
|
22
|
+
@ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
|
|
23
|
+
@ApiProtectedErrorResponses(404)
|
|
24
|
+
@ApiRequestTimeoutResponse({
|
|
25
|
+
description: 'The report query exceeded its statement budget (§5.2).',
|
|
26
|
+
type: ErrorResponseDto,
|
|
27
|
+
})
|
|
28
|
+
@Controller('organisations/:orgId/reports')
|
|
29
|
+
@UseGuards(JwtAuthGuard, OrgScopeGuard, PermissionGuard)
|
|
30
|
+
export class ReportsQueryController {
|
|
31
|
+
constructor(private readonly reportsQueriesService: ReportsQueriesService) {}
|
|
32
|
+
|
|
33
|
+
@Post(':key/query')
|
|
34
|
+
@HttpCode(200)
|
|
35
|
+
@RequirePermissions('reports.read')
|
|
36
|
+
@ApiParam({ name: 'key', description: 'Report definition key.' })
|
|
37
|
+
@ApiOperation({
|
|
38
|
+
summary:
|
|
39
|
+
'THE grid endpoint (§4): one declarative QuerySpec for filters, search, sort, keyset paging, counts, and projection.',
|
|
40
|
+
})
|
|
41
|
+
@ApiOkResponse({
|
|
42
|
+
description: 'Query result page.',
|
|
43
|
+
type: QueryResponseDto,
|
|
44
|
+
})
|
|
45
|
+
query(
|
|
46
|
+
@Param('orgId') orgId: string,
|
|
47
|
+
@Param('key') key: string,
|
|
48
|
+
@Body() spec: QuerySpecDto,
|
|
49
|
+
) {
|
|
50
|
+
return this.reportsQueriesService.query(orgId, key, spec);
|
|
51
|
+
}
|
|
52
|
+
}
|