@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,171 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { SubmissionService } from '@ftisindia/form-builder';
|
|
3
|
+
import type { SubmissionRecord, SubmissionStatus, SubmitArgs } from '@ftisindia/form-builder';
|
|
4
|
+
import { ReportAuthzDeniedError } from '@ftisindia/report-builder';
|
|
5
|
+
import type {
|
|
6
|
+
ReportActionContext,
|
|
7
|
+
ReportRowActionDef,
|
|
8
|
+
ResolvedRow,
|
|
9
|
+
RowActionKind,
|
|
10
|
+
} from '@ftisindia/report-builder';
|
|
11
|
+
import { withFormsErrorMapping } from '../../../forms/application/services/forms-error.mapper';
|
|
12
|
+
import { RequestFormsContext } from '../../../forms/infrastructure/request-forms-context';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Form-backed row actions (report design §6.1) — verbs that DELEGATE, never a
|
|
16
|
+
* second write path. The report engine resolves rows and checks org +
|
|
17
|
+
* permissions; these handlers hand each row to the form engine's
|
|
18
|
+
* SubmissionService, so grid verbs run the SAME validation and action pipeline
|
|
19
|
+
* as the form itself. No Prisma access here, ever.
|
|
20
|
+
*
|
|
21
|
+
* Call path mirrors FormsSubmissionsService: the engine is called through the
|
|
22
|
+
* forms context seam (RequestFormsContext — the same AsyncLocalStorage request
|
|
23
|
+
* context the report engine's own seam reads), and every call is wrapped in
|
|
24
|
+
* withFormsErrorMapping so a form validation failure surfaces as the app's
|
|
25
|
+
* 422 envelope with the engine's `errors` array — not an opaque 500.
|
|
26
|
+
*
|
|
27
|
+
* Engine API constraint (read from SubmissionService): the only public update
|
|
28
|
+
* surface is submit/saveDraft with `submissionId`, and the built-in persist
|
|
29
|
+
* action rejects rows that are locked or already SUBMITTED. So edits apply to
|
|
30
|
+
* DRAFT rows; a locked/submitted row surfaces the engine's FormsStateError
|
|
31
|
+
* (409) untouched. The forms engine also runs its OWN transaction per
|
|
32
|
+
* delegated call — its public API takes no external tx — so a multi-row batch
|
|
33
|
+
* is one report-engine transaction wrapping N form-engine transactions, and a
|
|
34
|
+
* failed row stops the batch without rolling back rows already committed by
|
|
35
|
+
* the form engine. Documented seam, not a bug: the alternative would be a
|
|
36
|
+
* second write path (§6).
|
|
37
|
+
*
|
|
38
|
+
* NO generic softDelete is registered: §6.1 makes delete "a status transition
|
|
39
|
+
* the source declares", and the form engine declares no deleted status —
|
|
40
|
+
* SubmissionStatus is DRAFT | SUBMITTED. When the form engine grows one, a
|
|
41
|
+
* delegated action lands here beside these two.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** The submission statuses the form engine exposes (SubmissionStatus). */
|
|
45
|
+
const SUBMISSION_STATUSES: readonly SubmissionStatus[] = ['DRAFT', 'SUBMITTED'];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `editSubmission` (§6.1 "Edit"): patches a submission through the form
|
|
49
|
+
* engine, re-validating against the submission's STAMPED `formVersion`
|
|
50
|
+
* definition in submit mode — an edit can never be validated against the
|
|
51
|
+
* wrong schema generation. The patch is shallow-merged over the stored data,
|
|
52
|
+
* then the whole document rides the form's own submit pipeline.
|
|
53
|
+
*/
|
|
54
|
+
@Injectable()
|
|
55
|
+
export class EditSubmissionRowAction implements ReportRowActionDef {
|
|
56
|
+
readonly name = 'editSubmission';
|
|
57
|
+
readonly kind: RowActionKind = 'transactional';
|
|
58
|
+
readonly requiredPermissions = ['formSubmissions.update'] as const;
|
|
59
|
+
readonly inputSchema = {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: { data: { type: 'object' } },
|
|
62
|
+
required: ['data'],
|
|
63
|
+
additionalProperties: false,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
constructor(
|
|
67
|
+
private readonly submissions: SubmissionService,
|
|
68
|
+
private readonly formsContext: RequestFormsContext,
|
|
69
|
+
) {}
|
|
70
|
+
|
|
71
|
+
async execute(
|
|
72
|
+
rows: ResolvedRow[],
|
|
73
|
+
input: Record<string, unknown> | undefined,
|
|
74
|
+
actionCtx: ReportActionContext,
|
|
75
|
+
): Promise<unknown> {
|
|
76
|
+
const orgId = requireOrgId(actionCtx);
|
|
77
|
+
// The action service ajv-validates inputSchema before execute (§6).
|
|
78
|
+
const patch = (input?.data ?? {}) as Record<string, unknown>;
|
|
79
|
+
|
|
80
|
+
let updated = 0;
|
|
81
|
+
for (const row of rows) {
|
|
82
|
+
const record = await this.loadRecord(orgId, row);
|
|
83
|
+
const args: SubmitArgs = {
|
|
84
|
+
orgId,
|
|
85
|
+
formKey: record.formKey,
|
|
86
|
+
// Re-validate against the stamped version, never the latest (§6.1).
|
|
87
|
+
version: record.formVersion,
|
|
88
|
+
data: { ...record.data, ...patch },
|
|
89
|
+
submissionId: record.id,
|
|
90
|
+
};
|
|
91
|
+
await withFormsErrorMapping(() => this.submissions.submit(args, this.formsContext));
|
|
92
|
+
updated += 1;
|
|
93
|
+
}
|
|
94
|
+
return { updated };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async loadRecord(orgId: string, row: ResolvedRow): Promise<SubmissionRecord> {
|
|
98
|
+
// rows carry sourceRef.rowId = the FormSubmission id; the engine's get()
|
|
99
|
+
// re-checks org scope and formSubmissions.read through the forms seam.
|
|
100
|
+
return withFormsErrorMapping(() =>
|
|
101
|
+
this.submissions.get({ orgId, submissionId: row.sourceRef.rowId }, this.formsContext),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* `updateStatus` (§6.1 "Status update"): a delegated transactional action.
|
|
108
|
+
* Status is not patched directly — the form engine couples status to its
|
|
109
|
+
* submit/saveDraft verbs, so SUBMITTED delegates to submit() (full pipeline,
|
|
110
|
+
* stamped-version validation) and DRAFT delegates to saveDraft(). Demoting an
|
|
111
|
+
* already-SUBMITTED row is rejected by the engine's persist action and
|
|
112
|
+
* surfaces as a conflict — the source, not the report layer, owns the legal
|
|
113
|
+
* transitions.
|
|
114
|
+
*/
|
|
115
|
+
@Injectable()
|
|
116
|
+
export class UpdateSubmissionStatusRowAction implements ReportRowActionDef {
|
|
117
|
+
readonly name = 'updateStatus';
|
|
118
|
+
readonly kind: RowActionKind = 'transactional';
|
|
119
|
+
readonly requiredPermissions = ['formSubmissions.update'] as const;
|
|
120
|
+
readonly inputSchema = {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: { status: { enum: [...SUBMISSION_STATUSES] } },
|
|
123
|
+
required: ['status'],
|
|
124
|
+
additionalProperties: false,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
constructor(
|
|
128
|
+
private readonly submissions: SubmissionService,
|
|
129
|
+
private readonly formsContext: RequestFormsContext,
|
|
130
|
+
) {}
|
|
131
|
+
|
|
132
|
+
async execute(
|
|
133
|
+
rows: ResolvedRow[],
|
|
134
|
+
input: Record<string, unknown> | undefined,
|
|
135
|
+
actionCtx: ReportActionContext,
|
|
136
|
+
): Promise<unknown> {
|
|
137
|
+
const orgId = requireOrgId(actionCtx);
|
|
138
|
+
// ajv-validated against inputSchema before execute (§6).
|
|
139
|
+
const status = input?.status as SubmissionStatus;
|
|
140
|
+
|
|
141
|
+
let updated = 0;
|
|
142
|
+
for (const row of rows) {
|
|
143
|
+
const record = await withFormsErrorMapping(() =>
|
|
144
|
+
this.submissions.get({ orgId, submissionId: row.sourceRef.rowId }, this.formsContext),
|
|
145
|
+
);
|
|
146
|
+
const args: SubmitArgs = {
|
|
147
|
+
orgId,
|
|
148
|
+
formKey: record.formKey,
|
|
149
|
+
version: record.formVersion,
|
|
150
|
+
data: record.data,
|
|
151
|
+
submissionId: record.id,
|
|
152
|
+
};
|
|
153
|
+
await withFormsErrorMapping(() =>
|
|
154
|
+
status === 'SUBMITTED'
|
|
155
|
+
? this.submissions.submit(args, this.formsContext)
|
|
156
|
+
: this.submissions.saveDraft(args, this.formsContext),
|
|
157
|
+
);
|
|
158
|
+
updated += 1;
|
|
159
|
+
}
|
|
160
|
+
return { updated };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Mirrors the engine's own manageTags guard: no org scope, no action. */
|
|
165
|
+
function requireOrgId(actionCtx: ReportActionContext): string {
|
|
166
|
+
const orgId = actionCtx.ctx.orgId();
|
|
167
|
+
if (orgId === undefined) {
|
|
168
|
+
throw new ReportAuthzDeniedError('No organisation scope is active.');
|
|
169
|
+
}
|
|
170
|
+
return orgId;
|
|
171
|
+
}
|
package/template/src/modules/reports/infrastructure/forms-adapter/forms-bridge-bootstrap.service.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Injectable, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { ReportRowActionRegistry, SourceProviderRegistry } from '@ftisindia/report-builder';
|
|
3
|
+
import { FormReportSourceProvider } from './form-report-source.adapter';
|
|
4
|
+
import {
|
|
5
|
+
EditSubmissionRowAction,
|
|
6
|
+
UpdateSubmissionStatusRowAction,
|
|
7
|
+
} from './form-row-actions';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The ONLY place the two engines meet (report design §3.2/§13). It registers
|
|
11
|
+
* the form-backed bits into the report engine's shared registries — the
|
|
12
|
+
* 'form' source provider and the delegated editSubmission/updateStatus verbs
|
|
13
|
+
* — only because THIS app ships the form builder. Removing
|
|
14
|
+
* ReportsFormsModule from the app leaves ReportsModule fully functional over
|
|
15
|
+
* custom sources, with no dangling form dependency.
|
|
16
|
+
*/
|
|
17
|
+
@Injectable()
|
|
18
|
+
export class ReportsFormsBridgeBootstrap implements OnModuleInit {
|
|
19
|
+
constructor(
|
|
20
|
+
private readonly sourceProviders: SourceProviderRegistry,
|
|
21
|
+
private readonly actions: ReportRowActionRegistry,
|
|
22
|
+
private readonly formSourceProvider: FormReportSourceProvider,
|
|
23
|
+
private readonly editSubmission: EditSubmissionRowAction,
|
|
24
|
+
private readonly updateStatus: UpdateSubmissionStatusRowAction,
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
onModuleInit(): void {
|
|
28
|
+
this.sourceProviders.register(this.formSourceProvider);
|
|
29
|
+
this.actions.register(this.editSubmission);
|
|
30
|
+
this.actions.register(this.updateStatus);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import type { CatalogColumn, CatalogIndex, CatalogPort } from '@ftisindia/report-builder';
|
|
3
|
+
import { PrismaService } from '../../../database/prisma/prisma.service';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Catalog introspection for the publish-time lints (report design §5.2/§8):
|
|
7
|
+
* declared indexes are verified against pg_indexes, generated columns against
|
|
8
|
+
* information_schema, tier caps against the planner's pg_class estimates.
|
|
9
|
+
* Every lookup is scoped to current_schema() so other schemas (extensions,
|
|
10
|
+
* shadow databases) cannot shadow or leak into lint verdicts.
|
|
11
|
+
*/
|
|
12
|
+
@Injectable()
|
|
13
|
+
export class PrismaReportsCatalog implements CatalogPort {
|
|
14
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
15
|
+
|
|
16
|
+
async indexesFor(table: string): Promise<CatalogIndex[]> {
|
|
17
|
+
const rows = await this.prisma.$queryRawUnsafe<
|
|
18
|
+
Array<{ indexname: string; tablename: string; indexdef: string }>
|
|
19
|
+
>(
|
|
20
|
+
`SELECT indexname, tablename, indexdef
|
|
21
|
+
FROM pg_indexes
|
|
22
|
+
WHERE schemaname = current_schema()
|
|
23
|
+
AND tablename = $1`,
|
|
24
|
+
table,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return rows.map((row) => ({
|
|
28
|
+
name: row.indexname,
|
|
29
|
+
table: row.tablename,
|
|
30
|
+
indexDef: row.indexdef,
|
|
31
|
+
isUnique: row.indexdef.includes('UNIQUE'),
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async columnsFor(table: string): Promise<CatalogColumn[]> {
|
|
36
|
+
const rows = await this.prisma.$queryRawUnsafe<
|
|
37
|
+
Array<{ column_name: string; data_type: string; is_generated: string }>
|
|
38
|
+
>(
|
|
39
|
+
`SELECT column_name, data_type, is_generated
|
|
40
|
+
FROM information_schema.columns
|
|
41
|
+
WHERE table_schema = current_schema()
|
|
42
|
+
AND table_name = $1`,
|
|
43
|
+
table,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return rows.map((row) => ({
|
|
47
|
+
name: row.column_name,
|
|
48
|
+
dataType: row.data_type,
|
|
49
|
+
isGenerated: row.is_generated === 'ALWAYS',
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async hasExtension(name: string): Promise<boolean> {
|
|
54
|
+
const rows = await this.prisma.$queryRawUnsafe<Array<{ found: number }>>(
|
|
55
|
+
`SELECT 1 AS found FROM pg_extension WHERE extname = $1`,
|
|
56
|
+
name,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return rows.length > 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async estimatedRowCount(table: string): Promise<number> {
|
|
63
|
+
const rows = await this.prisma.$queryRawUnsafe<Array<{ reltuples: unknown }>>(
|
|
64
|
+
`SELECT c.reltuples
|
|
65
|
+
FROM pg_class c
|
|
66
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
67
|
+
WHERE n.nspname = current_schema()
|
|
68
|
+
AND c.relname = $1
|
|
69
|
+
AND c.relkind IN ('r', 'p')`,
|
|
70
|
+
table,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (rows.length === 0) {
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
const estimate = Number(rows[0].reltuples);
|
|
77
|
+
return Number.isFinite(estimate) ? Math.max(0, estimate) : 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async relationExists(name: string): Promise<boolean> {
|
|
81
|
+
// Tables, views, materialized views, and partitioned tables all count —
|
|
82
|
+
// the materialized tier reads from any of them (report design §8).
|
|
83
|
+
const rows = await this.prisma.$queryRawUnsafe<Array<{ found: number }>>(
|
|
84
|
+
`SELECT 1 AS found
|
|
85
|
+
FROM pg_class c
|
|
86
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
87
|
+
WHERE n.nspname = current_schema()
|
|
88
|
+
AND c.relname = $1
|
|
89
|
+
AND c.relkind IN ('r', 'v', 'm', 'p')`,
|
|
90
|
+
name,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return rows.length > 0;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { Prisma } from '@prisma/client';
|
|
3
|
+
import { ReportQueryBudgetError, type QueryExecutor, type SqlRow } from '@ftisindia/report-builder';
|
|
4
|
+
import { PrismaService } from '../../../database/prisma/prisma.service';
|
|
5
|
+
|
|
6
|
+
/** Hard ceiling on the per-statement budget — matches Prisma's own tx limits. */
|
|
7
|
+
const MAX_STATEMENT_TIMEOUT_MS = 600_000;
|
|
8
|
+
|
|
9
|
+
function assertValidStatementTimeout(ms: number): void {
|
|
10
|
+
if (!Number.isInteger(ms) || ms < 1 || ms > MAX_STATEMENT_TIMEOUT_MS) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`Invalid report statement timeout: ${ms} (expected an integer between 1 and ${MAX_STATEMENT_TIMEOUT_MS} ms).`,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Postgres 57014 — 'canceling statement due to statement timeout'. */
|
|
18
|
+
function isStatementTimeout(error: unknown): boolean {
|
|
19
|
+
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
20
|
+
const meta = error.meta as { code?: unknown } | undefined;
|
|
21
|
+
if (meta?.code === '57014') {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const message = error instanceof Error ? error.message : '';
|
|
26
|
+
return message.includes('57014') || message.includes('canceling statement due to statement timeout');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Runs one compiler-built statement under a SET LOCAL statement_timeout on an
|
|
31
|
+
* open transaction client. SQL identifiers come only from code-owned manifests
|
|
32
|
+
* and every VALUE travels as a positional $1..$n bind; SET LOCAL cannot take
|
|
33
|
+
* binds, so the timeout — a VALIDATED INTEGER — is the one sanctioned
|
|
34
|
+
* interpolation exception (report design §2.1/§5.2).
|
|
35
|
+
*
|
|
36
|
+
* Shared between PrismaQueryExecutor (one-off transactions) and
|
|
37
|
+
* PrismaSnapshotRunner (the export's REPEATABLE READ transaction).
|
|
38
|
+
*/
|
|
39
|
+
export async function runBoundedQuery(
|
|
40
|
+
client: Prisma.TransactionClient,
|
|
41
|
+
sql: string,
|
|
42
|
+
params: readonly unknown[],
|
|
43
|
+
opts: { statementTimeoutMs: number },
|
|
44
|
+
): Promise<SqlRow[]> {
|
|
45
|
+
assertValidStatementTimeout(opts.statementTimeoutMs);
|
|
46
|
+
try {
|
|
47
|
+
await client.$executeRawUnsafe(`SET LOCAL statement_timeout = ${opts.statementTimeoutMs}`);
|
|
48
|
+
return await client.$queryRawUnsafe<SqlRow[]>(sql, ...params);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (isStatementTimeout(error)) {
|
|
51
|
+
// The runtime backstop (§5.2): a pathological plan fails fast, typed.
|
|
52
|
+
throw new ReportQueryBudgetError();
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* EXPLAIN (FORMAT JSON) for the plan lint and estimated counts (§5.2/§5.4).
|
|
60
|
+
* Postgres returns one row shaped [{ "QUERY PLAN": <document> }]; drivers may
|
|
61
|
+
* surface the json column already parsed or as a string — the engine expects
|
|
62
|
+
* the parsed array-root document either way.
|
|
63
|
+
*/
|
|
64
|
+
export async function runExplain(
|
|
65
|
+
client: Prisma.TransactionClient,
|
|
66
|
+
sql: string,
|
|
67
|
+
params: readonly unknown[],
|
|
68
|
+
): Promise<unknown> {
|
|
69
|
+
const rows = await client.$queryRawUnsafe<Array<Record<string, unknown>>>(
|
|
70
|
+
`EXPLAIN (FORMAT JSON) ${sql}`,
|
|
71
|
+
...params,
|
|
72
|
+
);
|
|
73
|
+
const plan = rows[0]?.['QUERY PLAN'];
|
|
74
|
+
return typeof plan === 'string' ? (JSON.parse(plan) as unknown) : plan;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The engine's SQL seam (report design §5) over Prisma. Each call runs inside
|
|
79
|
+
* its own short transaction because SET LOCAL is transaction-scoped — outside
|
|
80
|
+
* one it would be a silent no-op and the statement budget would not exist.
|
|
81
|
+
*/
|
|
82
|
+
@Injectable()
|
|
83
|
+
export class PrismaQueryExecutor implements QueryExecutor {
|
|
84
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
85
|
+
|
|
86
|
+
query(
|
|
87
|
+
sql: string,
|
|
88
|
+
params: readonly unknown[],
|
|
89
|
+
opts: { statementTimeoutMs: number },
|
|
90
|
+
): Promise<SqlRow[]> {
|
|
91
|
+
assertValidStatementTimeout(opts.statementTimeoutMs);
|
|
92
|
+
return this.prisma.$transaction((tx) => runBoundedQuery(tx, sql, params, opts), {
|
|
93
|
+
// Keep Prisma's transaction window above the statement budget so the
|
|
94
|
+
// budget — not Prisma's 5s interactive-tx default — is what fires.
|
|
95
|
+
timeout: opts.statementTimeoutMs + 1000,
|
|
96
|
+
maxWait: 5000,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
explain(sql: string, params: readonly unknown[]): Promise<unknown> {
|
|
101
|
+
return this.prisma.$transaction((tx) => runExplain(tx, sql, params));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { Prisma } from '@prisma/client';
|
|
3
|
+
import type { QueryExecutor, SnapshotRunner } from '@ftisindia/report-builder';
|
|
4
|
+
import { PrismaService } from '../../../database/prisma/prisma.service';
|
|
5
|
+
import { runBoundedQuery, runExplain } from './prisma-query-executor';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One REPEATABLE READ transaction per export (report design §9, finding #8):
|
|
9
|
+
* every keyset batch reads the same MVCC snapshot, so rows moving under the
|
|
10
|
+
* export can neither be skipped nor duplicated. The transaction is
|
|
11
|
+
* duration-bounded — an export that cannot finish inside the bound aborts
|
|
12
|
+
* (and the engine maps that to guidance: tighten filters or move the report
|
|
13
|
+
* to the materialized tier).
|
|
14
|
+
*/
|
|
15
|
+
@Injectable()
|
|
16
|
+
export class PrismaSnapshotRunner implements SnapshotRunner {
|
|
17
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
18
|
+
|
|
19
|
+
withSnapshot<T>(
|
|
20
|
+
opts: { maxDurationMs: number },
|
|
21
|
+
fn: (exec: QueryExecutor, asOf: Date) => Promise<T>,
|
|
22
|
+
): Promise<T> {
|
|
23
|
+
return this.prisma.$transaction(
|
|
24
|
+
async (tx) => {
|
|
25
|
+
// Under REPEATABLE READ now() is the transaction (= snapshot) start
|
|
26
|
+
// time — the export's `asOf`, stamped into the job and file metadata.
|
|
27
|
+
const rows = await tx.$queryRawUnsafe<Array<{ now: unknown }>>('SELECT now() AS now');
|
|
28
|
+
const now = rows[0]?.now;
|
|
29
|
+
const asOf = now instanceof Date ? now : new Date();
|
|
30
|
+
|
|
31
|
+
// Executor bound to THIS transaction: same SET LOCAL budget + EXPLAIN
|
|
32
|
+
// logic as PrismaQueryExecutor, valid only while `fn` runs.
|
|
33
|
+
const exec: QueryExecutor = {
|
|
34
|
+
query: (sql, params, queryOpts) => runBoundedQuery(tx, sql, params, queryOpts),
|
|
35
|
+
explain: (sql, params) => runExplain(tx, sql, params),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return fn(exec, asOf);
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead,
|
|
42
|
+
timeout: opts.maxDurationMs,
|
|
43
|
+
maxWait: 5000,
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import type { EngineTx, TxRunner } from '@ftisindia/report-builder';
|
|
3
|
+
import { PrismaService } from '../../../database/prisma/prisma.service';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The engine's transaction seam over PrismaService.$transaction. The opaque
|
|
7
|
+
* EngineTx brand is a Prisma.TransactionClient underneath; the stores cast it
|
|
8
|
+
* back. One TxRunner.run call is the engine's atomicity boundary (export job
|
|
9
|
+
* row + outbox enqueue, bulk-action ledger + audit — report design §6.3/§9).
|
|
10
|
+
*/
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class ReportsPrismaTxRunner implements TxRunner {
|
|
13
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
14
|
+
|
|
15
|
+
run<T>(fn: (tx: EngineTx) => Promise<T>): Promise<T> {
|
|
16
|
+
return this.prisma.$transaction((tx) => fn(tx as unknown as EngineTx));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Injectable, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { DiscoveryService, Reflector } from '@nestjs/core';
|
|
3
|
+
import {
|
|
4
|
+
ReportRowActionRegistry,
|
|
5
|
+
ReportSourceRegistry,
|
|
6
|
+
ReportTagService,
|
|
7
|
+
SourceProviderRegistry,
|
|
8
|
+
createCustomSourceProvider,
|
|
9
|
+
createManageTagsAction,
|
|
10
|
+
} from '@ftisindia/report-builder';
|
|
11
|
+
import type { ReportRowActionDef, ReportSourceDef } from '@ftisindia/report-builder';
|
|
12
|
+
import {
|
|
13
|
+
REPORT_ROW_ACTION_METADATA,
|
|
14
|
+
REPORT_SOURCE_METADATA,
|
|
15
|
+
} from './report-extension.decorators';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Populates the engine registries at startup — WITHOUT any form-builder
|
|
19
|
+
* dependency (the standalone guarantee extends to the glue, report design
|
|
20
|
+
* §3.2): the built-in manageTags action (§7), the 'custom' source provider
|
|
21
|
+
* over the code-owned source registry (the one provider the core ships), and
|
|
22
|
+
* a discovery scan for every app provider decorated with @ReportSource /
|
|
23
|
+
* @ReportRowAction (e.g. the org-members example source).
|
|
24
|
+
*
|
|
25
|
+
* Form-backed sources and the delegated editSubmission/updateStatus verbs are
|
|
26
|
+
* added by the OPTIONAL ReportsFormsModule, which registers the 'form'
|
|
27
|
+
* provider + actions into these same registries only when the app ships forms.
|
|
28
|
+
*/
|
|
29
|
+
@Injectable()
|
|
30
|
+
export class ReportsRegistryBootstrapService implements OnModuleInit {
|
|
31
|
+
constructor(
|
|
32
|
+
private readonly discovery: DiscoveryService,
|
|
33
|
+
private readonly reflector: Reflector,
|
|
34
|
+
private readonly sourceRegistry: ReportSourceRegistry,
|
|
35
|
+
private readonly sourceProviders: SourceProviderRegistry,
|
|
36
|
+
private readonly actionRegistry: ReportRowActionRegistry,
|
|
37
|
+
private readonly tagService: ReportTagService,
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
onModuleInit(): void {
|
|
41
|
+
this.actionRegistry.register(createManageTagsAction(this.tagService));
|
|
42
|
+
this.sourceProviders.register(createCustomSourceProvider(this.sourceRegistry));
|
|
43
|
+
|
|
44
|
+
for (const wrapper of this.discovery.getProviders()) {
|
|
45
|
+
const instance: unknown = wrapper.instance;
|
|
46
|
+
if (!instance || typeof instance !== 'object') {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const ctor = instance.constructor as object | undefined;
|
|
50
|
+
if (!ctor || ctor === Object) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (this.reflector.get<boolean>(REPORT_SOURCE_METADATA, ctor as never)) {
|
|
54
|
+
this.sourceRegistry.register(instance as ReportSourceDef);
|
|
55
|
+
}
|
|
56
|
+
if (this.reflector.get<boolean>(REPORT_ROW_ACTION_METADATA, ctor as never)) {
|
|
57
|
+
this.actionRegistry.register(instance as ReportRowActionDef);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SetMetadata } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
export const REPORT_SOURCE_METADATA = 'ftis:reports:source';
|
|
4
|
+
export const REPORT_ROW_ACTION_METADATA = 'ftis:reports:row-action';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The standardized extension story (report design §3.2/§6): implement the
|
|
8
|
+
* engine interface (ReportSourceDef / ReportRowActionDef), decorate the
|
|
9
|
+
* @Injectable class, list it as a provider — ReportsRegistryBootstrapService
|
|
10
|
+
* discovers and registers it at startup. Same three steps as the form
|
|
11
|
+
* registries.
|
|
12
|
+
*/
|
|
13
|
+
export const ReportSource = () => SetMetadata(REPORT_SOURCE_METADATA, true);
|
|
14
|
+
export const ReportRowAction = () => SetMetadata(REPORT_ROW_ACTION_METADATA, true);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import type { EngineTx, ReportsJobQueue } from '@ftisindia/report-builder';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The async-export job queue (report design §9). The durable signal IS the
|
|
6
|
+
* `ReportExportJob` row the engine creates in the same transaction — the
|
|
7
|
+
* reports-owned ReportsExportDispatcherService polls that table for PENDING
|
|
8
|
+
* jobs. So `enqueue` is intentionally a no-op: there is no separate queue to
|
|
9
|
+
* write, and reports stays independent of the forms outbox.
|
|
10
|
+
*
|
|
11
|
+
* (An app that prefers an external queue can bind a different ReportsJobQueue
|
|
12
|
+
* — e.g. one that publishes to SQS — without changing the engine.)
|
|
13
|
+
*/
|
|
14
|
+
@Injectable()
|
|
15
|
+
export class ReportsJobQueueNoop implements ReportsJobQueue {
|
|
16
|
+
async enqueue(
|
|
17
|
+
_job: {
|
|
18
|
+
type: string;
|
|
19
|
+
payload: Record<string, unknown>;
|
|
20
|
+
orgId: string;
|
|
21
|
+
actorUserId?: string | null;
|
|
22
|
+
idempotencyKey?: string;
|
|
23
|
+
},
|
|
24
|
+
_tx: EngineTx,
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
// The ReportExportJob row (committed with this tx) is the queue entry.
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import type { ReportsContext } from '@ftisindia/report-builder';
|
|
3
|
+
import { RequestContextService } from '../../request-context/application/services/request-context.service';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Binds the engine's ReportsContext seam to the app's AsyncLocalStorage
|
|
7
|
+
* request context (report design §13). Worker contexts (async export jobs)
|
|
8
|
+
* are restored by the forms outbox dispatcher before the engine runs, so the
|
|
9
|
+
* same adapter serves HTTP and worker callers (ecosystem guide §3).
|
|
10
|
+
*/
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class RequestReportsContext implements ReportsContext {
|
|
13
|
+
constructor(private readonly ctx: RequestContextService) {}
|
|
14
|
+
|
|
15
|
+
requestId() {
|
|
16
|
+
return this.ctx.getRequestId();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
source(): 'http' | 'worker' {
|
|
20
|
+
return this.ctx.get()?.source ?? 'worker';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
userId() {
|
|
24
|
+
return this.ctx.getUserId();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
orgId() {
|
|
28
|
+
return this.ctx.getOrgId();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
permissionKeys() {
|
|
32
|
+
return this.ctx.getPermissions();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
isOwner() {
|
|
36
|
+
return this.ctx.getRbacContext()?.isOwner ?? false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
assertOrgScope(orgId: string) {
|
|
40
|
+
this.ctx.assertOrgScope(orgId);
|
|
41
|
+
}
|
|
42
|
+
}
|