@ftisindia/create-app 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.env.example +28 -0
- package/template/README.md +51 -0
- package/template/_gitignore +6 -0
- package/template/_package.json +10 -1
- package/template/docs/FORMS.md +188 -0
- package/template/docs/FORMS_CHECKLIST.md +69 -0
- package/template/docs/REPORTS.md +255 -0
- package/template/docs/REPORTS_CHECKLIST.md +152 -0
- package/template/prisma/migrations/20260612000000_add_form_builder/migration.sql +147 -0
- package/template/prisma/migrations/20260613000000_add_report_builder/migration.sql +129 -0
- package/template/prisma/migrations/20260616000000_add_form_outbox_claimed_by/migration.sql +5 -0
- package/template/prisma/schema.prisma +289 -0
- package/template/scripts/export-openapi.ts +85 -0
- package/template/scripts/gen-form.mjs +149 -0
- package/template/scripts/push-form.ts +124 -0
- package/template/src/app.module.ts +30 -8
- package/template/src/common/dto/membership-response.dto.ts +1 -0
- package/template/src/common/dto/role-summary.dto.ts +3 -3
- package/template/src/common/dto/user-summary.dto.ts +3 -3
- package/template/src/config/env.validation.ts +28 -0
- package/template/src/config/forms.config.ts +13 -0
- package/template/src/config/index.ts +2 -0
- package/template/src/config/openapi.ts +12 -0
- package/template/src/config/reports-secret.ts +15 -0
- package/template/src/config/reports.config.ts +18 -0
- package/template/src/main.ts +3 -12
- package/template/src/modules/access-control/dto/access-control-response.dto.ts +3 -0
- package/template/src/modules/access-control/dto/current-access-control-response.dto.ts +5 -1
- package/template/src/modules/access-control/types/permission-key.ts +27 -0
- package/template/src/modules/access-control/types/route-permission-registry.ts +183 -0
- package/template/src/modules/audit/dto/audit-response.dto.ts +7 -3
- package/template/src/modules/auth/auth.module.ts +3 -1
- package/template/src/modules/auth/dto/auth-response.dto.ts +1 -1
- package/template/src/modules/forms/application/services/file-gc.service.ts +85 -0
- package/template/src/modules/forms/application/services/forms-definitions.service.ts +137 -0
- package/template/src/modules/forms/application/services/forms-error.mapper.ts +64 -0
- package/template/src/modules/forms/application/services/forms-export.service.ts +210 -0
- package/template/src/modules/forms/application/services/forms-files.service.ts +164 -0
- package/template/src/modules/forms/application/services/forms-public.service.ts +49 -0
- package/template/src/modules/forms/application/services/forms-settings-reader.service.ts +53 -0
- package/template/src/modules/forms/application/services/forms-submissions.service.ts +103 -0
- package/template/src/modules/forms/application/services/handlers/authenticate.action.ts +37 -0
- package/template/src/modules/forms/application/services/handlers/logging-email.handler.ts +22 -0
- package/template/src/modules/forms/application/services/handlers/send-confirmation-email.action.ts +40 -0
- package/template/src/modules/forms/application/services/handlers/webhook-delivery.transport.ts +319 -0
- package/template/src/modules/forms/application/services/handlers/webhook.handler.ts +89 -0
- package/template/src/modules/forms/application/services/outbox-dispatcher.service.ts +131 -0
- package/template/src/modules/forms/dto/create-form-definition.dto.ts +12 -0
- package/template/src/modules/forms/dto/data-source-response.dto.ts +19 -0
- package/template/src/modules/forms/dto/export-submissions-query.dto.ts +33 -0
- package/template/src/modules/forms/dto/file-upload-response.dto.ts +24 -0
- package/template/src/modules/forms/dto/form-definition-response.dto.ts +50 -0
- package/template/src/modules/forms/dto/form-render-response.dto.ts +17 -0
- package/template/src/modules/forms/dto/list-form-definitions-query.dto.ts +10 -0
- package/template/src/modules/forms/dto/list-submissions-query.dto.ts +10 -0
- package/template/src/modules/forms/dto/public-submit-form.dto.ts +24 -0
- package/template/src/modules/forms/dto/set-public-access.dto.ts +8 -0
- package/template/src/modules/forms/dto/submission-response.dto.ts +99 -0
- package/template/src/modules/forms/dto/submit-form.dto.ts +50 -0
- package/template/src/modules/forms/dto/update-form-definition.dto.ts +12 -0
- package/template/src/modules/forms/dto/upload-file-query.dto.ts +33 -0
- package/template/src/modules/forms/dto/validate-submission.dto.ts +22 -0
- package/template/src/modules/forms/examples/abstract-submission.form.json +80 -0
- package/template/src/modules/forms/examples/login.form.json +24 -0
- package/template/src/modules/forms/examples/registration.form.json +44 -0
- package/template/src/modules/forms/forms.module.ts +228 -0
- package/template/src/modules/forms/forms.tokens.ts +6 -0
- package/template/src/modules/forms/infrastructure/audit-sink.adapter.ts +30 -0
- package/template/src/modules/forms/infrastructure/casl-forms-authorization.ts +31 -0
- package/template/src/modules/forms/infrastructure/prisma-tx-runner.ts +17 -0
- package/template/src/modules/forms/infrastructure/registry/form-extension.decorators.ts +17 -0
- package/template/src/modules/forms/infrastructure/registry/registry-bootstrap.service.ts +82 -0
- package/template/src/modules/forms/infrastructure/request-forms-context.ts +60 -0
- package/template/src/modules/forms/infrastructure/schema-check/forms-schema-check.service.ts +76 -0
- package/template/src/modules/forms/infrastructure/storage/local-disk-storage.adapter.ts +43 -0
- package/template/src/modules/forms/infrastructure/stores/index.ts +5 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-action-log.store.ts +37 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-file.store.ts +108 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-form-definition.store.ts +147 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-outbox.store.ts +156 -0
- package/template/src/modules/forms/infrastructure/stores/prisma-submission.store.ts +164 -0
- package/template/src/modules/forms/presentation/forms-data-sources.controller.ts +58 -0
- package/template/src/modules/forms/presentation/forms-definitions.controller.ts +191 -0
- package/template/src/modules/forms/presentation/forms-files.controller.ts +79 -0
- package/template/src/modules/forms/presentation/forms-submissions.controller.ts +154 -0
- package/template/src/modules/forms/presentation/forms-upload.interceptor.ts +33 -0
- package/template/src/modules/forms/presentation/public-forms.controller.ts +51 -0
- package/template/src/modules/invitations/dto/invitation-response.dto.ts +4 -0
- package/template/src/modules/organisations/dto/organisation-response.dto.ts +1 -0
- package/template/src/modules/reports/application/services/reports-actions.service.ts +54 -0
- package/template/src/modules/reports/application/services/reports-definitions.service.ts +66 -0
- package/template/src/modules/reports/application/services/reports-error.mapper.ts +97 -0
- package/template/src/modules/reports/application/services/reports-export-dispatcher.service.ts +205 -0
- package/template/src/modules/reports/application/services/reports-exports.service.ts +78 -0
- package/template/src/modules/reports/application/services/reports-queries.service.ts +35 -0
- package/template/src/modules/reports/application/services/reports-settings-reader.service.ts +49 -0
- package/template/src/modules/reports/application/services/reports-views.service.ts +79 -0
- package/template/src/modules/reports/dto/action-result-response.dto.ts +21 -0
- package/template/src/modules/reports/dto/create-report-definition.dto.ts +86 -0
- package/template/src/modules/reports/dto/create-saved-view.dto.ts +26 -0
- package/template/src/modules/reports/dto/execute-action.dto.ts +71 -0
- package/template/src/modules/reports/dto/export-job-response.dto.ts +60 -0
- package/template/src/modules/reports/dto/export-request.dto.ts +34 -0
- package/template/src/modules/reports/dto/list-reports-query.dto.ts +10 -0
- package/template/src/modules/reports/dto/list-views-query.dto.ts +17 -0
- package/template/src/modules/reports/dto/prepare-action-response.dto.ts +14 -0
- package/template/src/modules/reports/dto/prepare-action.dto.ts +27 -0
- package/template/src/modules/reports/dto/query-response.dto.ts +64 -0
- package/template/src/modules/reports/dto/query-spec.dto.ts +120 -0
- package/template/src/modules/reports/dto/report-definition-response.dto.ts +64 -0
- package/template/src/modules/reports/dto/report-meta-query.dto.ts +16 -0
- package/template/src/modules/reports/dto/report-meta-response.dto.ts +113 -0
- package/template/src/modules/reports/dto/saved-view-response.dto.ts +66 -0
- package/template/src/modules/reports/dto/update-report-definition.dto.ts +9 -0
- package/template/src/modules/reports/dto/update-saved-view.dto.ts +27 -0
- package/template/src/modules/reports/examples/abstract-review-board.report.json +54 -0
- package/template/src/modules/reports/examples/org-members.report.json +55 -0
- package/template/src/modules/reports/infrastructure/audit-sink.adapter.ts +31 -0
- package/template/src/modules/reports/infrastructure/casl-reports-authorization.ts +39 -0
- package/template/src/modules/reports/infrastructure/forms-adapter/form-report-source.adapter.ts +292 -0
- package/template/src/modules/reports/infrastructure/forms-adapter/form-row-actions.ts +171 -0
- package/template/src/modules/reports/infrastructure/forms-adapter/forms-bridge-bootstrap.service.ts +32 -0
- package/template/src/modules/reports/infrastructure/prisma-catalog.adapter.ts +95 -0
- package/template/src/modules/reports/infrastructure/prisma-query-executor.ts +103 -0
- package/template/src/modules/reports/infrastructure/prisma-snapshot-runner.ts +47 -0
- package/template/src/modules/reports/infrastructure/prisma-tx-runner.ts +18 -0
- package/template/src/modules/reports/infrastructure/registry/registry-bootstrap.service.ts +61 -0
- package/template/src/modules/reports/infrastructure/registry/report-extension.decorators.ts +14 -0
- package/template/src/modules/reports/infrastructure/reports-job-queue.adapter.ts +28 -0
- package/template/src/modules/reports/infrastructure/request-reports-context.ts +42 -0
- package/template/src/modules/reports/infrastructure/schema-check/reports-schema-check.service.ts +116 -0
- package/template/src/modules/reports/infrastructure/storage/local-disk-export-storage.adapter.ts +92 -0
- package/template/src/modules/reports/infrastructure/stores/index.ts +5 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-bulk-action-run.store.ts +89 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-export-job.store.ts +93 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-report-definition.store.ts +171 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-row-tag.store.ts +110 -0
- package/template/src/modules/reports/infrastructure/stores/prisma-saved-view.store.ts +144 -0
- package/template/src/modules/reports/presentation/reports-actions.controller.ts +83 -0
- package/template/src/modules/reports/presentation/reports-definitions.controller.ts +156 -0
- package/template/src/modules/reports/presentation/reports-export-jobs.controller.ts +61 -0
- package/template/src/modules/reports/presentation/reports-export.controller.ts +76 -0
- package/template/src/modules/reports/presentation/reports-query.controller.ts +52 -0
- package/template/src/modules/reports/presentation/reports-views.controller.ts +140 -0
- package/template/src/modules/reports/reports-forms.module.ts +33 -0
- package/template/src/modules/reports/reports.module.ts +335 -0
- package/template/src/modules/reports/reports.tokens.ts +11 -0
- package/template/src/modules/reports/sources/org-members.source.ts +112 -0
- package/template/src/modules/settings/types/setting-definitions.ts +94 -0
- package/template/test/forms-captcha.e2e-spec.ts +163 -0
- package/template/test/forms-definitions.e2e-spec.ts +394 -0
- package/template/test/forms-export.e2e-spec.ts +390 -0
- package/template/test/forms-files.e2e-spec.ts +345 -0
- package/template/test/forms-outbox.e2e-spec.ts +570 -0
- package/template/test/forms-permission-sync.spec.ts +27 -0
- package/template/test/forms-public.e2e-spec.ts +293 -0
- package/template/test/forms-schema-check.e2e-spec.ts +65 -0
- package/template/test/forms-submissions.e2e-spec.ts +500 -0
- package/template/test/forms-throttling.e2e-spec.ts +146 -0
- package/template/test/forms-webhooks.e2e-spec.ts +403 -0
- package/template/test/jest-e2e.json +1 -0
- package/template/test/reports-advanced.e2e-spec.ts +381 -0
- package/template/test/reports-permission-sync.spec.ts +30 -0
- package/template/test/reports-query.e2e-spec.ts +402 -0
- package/template/test/reports-tiers.e2e-spec.ts +343 -0
- package/template/test/route-registry.validator.spec.ts +22 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"key": "abstract-review-board",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"status": "PUBLISHED",
|
|
5
|
+
"title": "Abstract submissions — review board",
|
|
6
|
+
"description": "Review grid over the abstract-submission form. Column ids follow the form-adapter convention (field path with dots replaced by underscores); $row.* references the physical FormSubmission columns the adapter allowlists. Only indexHint fields are sortable; the indexed tier's publish lint demands their generated columns and indexes.",
|
|
7
|
+
"source": { "kind": "form", "key": "abstract-submission" },
|
|
8
|
+
"columns": [
|
|
9
|
+
{
|
|
10
|
+
"id": "title",
|
|
11
|
+
"header": "Title",
|
|
12
|
+
"path": "title",
|
|
13
|
+
"type": "text",
|
|
14
|
+
"filterable": true
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "track",
|
|
18
|
+
"header": "Track",
|
|
19
|
+
"path": "track",
|
|
20
|
+
"type": "lookup",
|
|
21
|
+
"sortable": true,
|
|
22
|
+
"filterable": true,
|
|
23
|
+
"lookup": { "dataSource": "conference-tracks" }
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "status",
|
|
27
|
+
"header": "Status",
|
|
28
|
+
"path": "$row.status",
|
|
29
|
+
"type": "enum",
|
|
30
|
+
"sortable": true,
|
|
31
|
+
"filterable": true,
|
|
32
|
+
"enum": ["DRAFT", "SUBMITTED"]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "createdAt",
|
|
36
|
+
"header": "Submitted",
|
|
37
|
+
"path": "$row.createdAt",
|
|
38
|
+
"type": "datetime",
|
|
39
|
+
"sortable": true,
|
|
40
|
+
"filterable": true
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "createdBy",
|
|
44
|
+
"header": "Submitted by",
|
|
45
|
+
"path": "$row.createdBy",
|
|
46
|
+
"type": "text",
|
|
47
|
+
"filterable": true
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"defaultSort": [{ "column": "createdAt", "dir": "desc" }],
|
|
51
|
+
"rowActions": ["updateStatus", "editSubmission", "manageTags"],
|
|
52
|
+
"export": { "formats": ["csv", "xlsx"] },
|
|
53
|
+
"performanceTier": "indexed"
|
|
54
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"key": "org-members",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"status": "PUBLISHED",
|
|
5
|
+
"title": "Organisation members",
|
|
6
|
+
"description": "Every membership in the active organisation with user, role, and status — the Phase-1 standalone example: a custom source with no form-builder involvement.",
|
|
7
|
+
"source": { "kind": "custom", "key": "org-members" },
|
|
8
|
+
"columns": [
|
|
9
|
+
{
|
|
10
|
+
"id": "displayName",
|
|
11
|
+
"header": "Name",
|
|
12
|
+
"columnId": "displayName",
|
|
13
|
+
"type": "text",
|
|
14
|
+
"sortable": true,
|
|
15
|
+
"filterable": true,
|
|
16
|
+
"searchable": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "email",
|
|
20
|
+
"header": "Email",
|
|
21
|
+
"columnId": "email",
|
|
22
|
+
"type": "text",
|
|
23
|
+
"sortable": true,
|
|
24
|
+
"filterable": true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "role",
|
|
28
|
+
"header": "Role",
|
|
29
|
+
"columnId": "role",
|
|
30
|
+
"type": "text",
|
|
31
|
+
"sortable": true,
|
|
32
|
+
"filterable": true
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "status",
|
|
36
|
+
"header": "Status",
|
|
37
|
+
"columnId": "status",
|
|
38
|
+
"type": "enum",
|
|
39
|
+
"filterable": true,
|
|
40
|
+
"enum": ["ACTIVE", "SUSPENDED", "REVOKED"]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "joinedAt",
|
|
44
|
+
"header": "Joined",
|
|
45
|
+
"columnId": "joinedAt",
|
|
46
|
+
"type": "datetime",
|
|
47
|
+
"sortable": true
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"defaultSort": [{ "column": "joinedAt", "dir": "desc" }],
|
|
51
|
+
"search": { "columns": ["displayName"] },
|
|
52
|
+
"rowActions": ["manageTags"],
|
|
53
|
+
"export": { "formats": ["csv", "xlsx"] },
|
|
54
|
+
"performanceTier": "live"
|
|
55
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { Prisma } from '@prisma/client';
|
|
3
|
+
import type { AuditEntry, EngineTx, ReportsAuditSink } from '@ftisindia/report-builder';
|
|
4
|
+
import { PrismaService } from '../../../database/prisma/prisma.service';
|
|
5
|
+
import { AuditService } from '../../audit/application/services/audit.service';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Routes engine audit entries through the template's AuditService. When the
|
|
9
|
+
* engine supplies a transaction, the audit row is written through it so it
|
|
10
|
+
* commits atomically with the change it describes (ecosystem guide §7) —
|
|
11
|
+
* exports and bulk actions are audited as the PII egress they are (§9).
|
|
12
|
+
*/
|
|
13
|
+
@Injectable()
|
|
14
|
+
export class PrismaReportsAuditSink implements ReportsAuditSink {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly audit: AuditService,
|
|
17
|
+
private readonly prisma: PrismaService,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
async write(entry: AuditEntry, tx?: EngineTx): Promise<void> {
|
|
21
|
+
const client = tx ? (tx as unknown as Prisma.TransactionClient) : this.prisma;
|
|
22
|
+
await this.audit.write(client, {
|
|
23
|
+
orgId: entry.orgId ?? null,
|
|
24
|
+
actorUserId: entry.actorUserId ?? null,
|
|
25
|
+
action: entry.action,
|
|
26
|
+
targetType: entry.targetType,
|
|
27
|
+
targetId: entry.targetId ?? null,
|
|
28
|
+
metadata: entry.metadata as Prisma.InputJsonValue | undefined,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import type { ReportsAuthorization, ReportsContext } from '@ftisindia/report-builder';
|
|
3
|
+
import { AbilityFactory } from '../../access-control/application/services/ability.factory';
|
|
4
|
+
import { permissionKeyToRule } from '../../access-control/types/permission-key';
|
|
5
|
+
import { RequestContextService } from '../../request-context/application/services/request-context.service';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The engine's internal authorization seam over CASL — mirrors
|
|
9
|
+
* PermissionGuard's own logic and fails closed when no RBAC context exists
|
|
10
|
+
* (anonymous/public requests). Route guards remain the first enforcement
|
|
11
|
+
* layer; this is the defense-in-depth second layer (ecosystem guide §5.3).
|
|
12
|
+
*
|
|
13
|
+
* Required keys are plain two-segment strings by design: row actions declare
|
|
14
|
+
* keys from ANY domain (e.g. 'formSubmissions.update'), not just 'reports.*'
|
|
15
|
+
* (report design §6.1) — permissionKeyToRule splits them generically.
|
|
16
|
+
*/
|
|
17
|
+
@Injectable()
|
|
18
|
+
export class CaslReportsAuthorization implements ReportsAuthorization {
|
|
19
|
+
constructor(
|
|
20
|
+
private readonly abilities: AbilityFactory,
|
|
21
|
+
private readonly ctx: RequestContextService,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
can(required: readonly string[], ctx: ReportsContext): boolean {
|
|
25
|
+
// Fail closed: no authenticated user inside an active org ⇒ no capability.
|
|
26
|
+
if (!ctx.userId() || !ctx.orgId()) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const rbac = this.ctx.getRbacContext();
|
|
30
|
+
if (!rbac) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const ability = this.abilities.createForContext(rbac);
|
|
34
|
+
return required.every((key) => {
|
|
35
|
+
const { action, subject } = permissionKeyToRule(key);
|
|
36
|
+
return ability.can(action, subject);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
package/template/src/modules/reports/infrastructure/forms-adapter/form-report-source.adapter.ts
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import {
|
|
3
|
+
generatedColumnComment,
|
|
4
|
+
generatedColumnExpression,
|
|
5
|
+
generatedColumnIndexes,
|
|
6
|
+
generatedColumnName,
|
|
7
|
+
} from '@ftisindia/report-builder';
|
|
8
|
+
import type {
|
|
9
|
+
ColumnType,
|
|
10
|
+
GeneratedIndexPlan,
|
|
11
|
+
IndexSpec,
|
|
12
|
+
ManifestColumn,
|
|
13
|
+
ReportsContext,
|
|
14
|
+
ResolvedSource,
|
|
15
|
+
SourceBinding,
|
|
16
|
+
SourceManifest,
|
|
17
|
+
SourceProvider,
|
|
18
|
+
SourceQuery,
|
|
19
|
+
} from '@ftisindia/report-builder';
|
|
20
|
+
import { walkFields } from '@ftisindia/form-builder';
|
|
21
|
+
import type { FieldDef, FormDefinition } from '@ftisindia/form-builder';
|
|
22
|
+
import { PrismaFormDefinitionStore } from '../../../forms/infrastructure/stores';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `kind: "form"` source provider (report design §3.1) — the quick path. Maps a
|
|
26
|
+
* published FormDefinition onto the same SourceManifest interface every custom
|
|
27
|
+
* source implements, so the compiler never knows forms exist.
|
|
28
|
+
*
|
|
29
|
+
* THE STANDALONE GUARANTEE, INVERTED (§3.2): the report-builder core contains
|
|
30
|
+
* zero knowledge of the form builder, and this file is the ONLY place the two
|
|
31
|
+
* engines meet. Remove this provider (and the row actions beside it) and the
|
|
32
|
+
* report module still ships full reporting over custom sources.
|
|
33
|
+
*
|
|
34
|
+
* COLUMN-ID CONVENTION: each reportable field becomes one manifest column
|
|
35
|
+
* whose id is the field's dot path with dots replaced by '_' (e.g.
|
|
36
|
+
* `contact.email` → `contact_email`). Definitions over form sources reference
|
|
37
|
+
* fields via plain-path columns, and plain-path allowlist resolution maps by
|
|
38
|
+
* the DEFINITION column's own id — so a definition column over a form source
|
|
39
|
+
* MUST use this id convention (`{ "id": "contact_email", "path":
|
|
40
|
+
* "contact.email" }`) or it will not resolve against the manifest.
|
|
41
|
+
*
|
|
42
|
+
* VERSION NOTE (§3.1): the design's renamed-field `CASE ON "formVersion"` map
|
|
43
|
+
* requires rename metadata that FieldDef does not carry today, so this adapter
|
|
44
|
+
* resolves against the LATEST PUBLISHED definition version only. Submissions
|
|
45
|
+
* stamp `formVersion`, which is exactly what a future version-aware upgrade
|
|
46
|
+
* needs — nothing here forecloses it.
|
|
47
|
+
*/
|
|
48
|
+
@Injectable()
|
|
49
|
+
export class FormReportSourceProvider implements SourceProvider {
|
|
50
|
+
readonly kind = 'form';
|
|
51
|
+
|
|
52
|
+
constructor(private readonly definitions: PrismaFormDefinitionStore) {}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve a `{ kind: 'form', key: <formKey> }` binding to the manifest +
|
|
56
|
+
* base query the compiler consumes. Null (→ ReportNotFoundError upstream)
|
|
57
|
+
* when no org scope is active or the form has no published version.
|
|
58
|
+
*/
|
|
59
|
+
async resolve(binding: SourceBinding, ctx: ReportsContext): Promise<ResolvedSource | null> {
|
|
60
|
+
const orgId = ctx.orgId();
|
|
61
|
+
if (orgId === undefined) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const record = await this.definitions.findLatest(orgId, binding.key, 'PUBLISHED');
|
|
65
|
+
if (record === null) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const manifest: SourceManifest = {
|
|
70
|
+
// The submission row id — what tags, row actions, and the keyset
|
|
71
|
+
// tiebreaker bind to (§7).
|
|
72
|
+
rowId: '"id"',
|
|
73
|
+
orgScoped: true,
|
|
74
|
+
columns: [...this.autoColumns(binding.key, record.schema), ...physicalRowColumns()],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const baseQuery: SourceQuery = {
|
|
78
|
+
from: '"FormSubmission"',
|
|
79
|
+
orgColumn: '"orgId"',
|
|
80
|
+
primaryTable: 'FormSubmission',
|
|
81
|
+
// Code-owned discriminator — multiple forms share one physical table.
|
|
82
|
+
where: [{ sql: '"formKey" = ?', params: [binding.key] }],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return { kind: this.kind, key: binding.key, manifest, baseQuery };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Auto-columns from form fields (§3.1): every `reportable: true` field with
|
|
90
|
+
* a mappable type becomes a manifest column over the JSONB extraction
|
|
91
|
+
* expression (the live tier reads it directly; the indexed tier's generated
|
|
92
|
+
* column uses the SAME expression, §8.1).
|
|
93
|
+
*
|
|
94
|
+
* Skipped on purpose:
|
|
95
|
+
* - `sensitive` fields — their values never reach action logs, audit
|
|
96
|
+
* metadata, or exports (form engine contract), so they never enter a
|
|
97
|
+
* report manifest either;
|
|
98
|
+
* - file/password fields (and any type outside the map) — files are
|
|
99
|
+
* references, passwords are always-sensitive; neither is tabular data;
|
|
100
|
+
* - repeatable groups and every field inside one — a repeatable group is an
|
|
101
|
+
* array of objects, so one submission row maps to MANY values: a scalar
|
|
102
|
+
* JSONB extraction would return NULL or require flattening the row set,
|
|
103
|
+
* which is a materialization concern (§8), not a column.
|
|
104
|
+
*/
|
|
105
|
+
private autoColumns(formKey: string, definition: FormDefinition): ManifestColumn[] {
|
|
106
|
+
const columns: ManifestColumn[] = [];
|
|
107
|
+
const reserved = new Set(physicalRowColumns().map((column) => column.id));
|
|
108
|
+
const repeatableRoots: string[] = [];
|
|
109
|
+
|
|
110
|
+
walkFields(definition.fields, (field, segments) => {
|
|
111
|
+
const path = segments.join('.');
|
|
112
|
+
// walkFields is depth-first pre-order: a repeatable ancestor is always
|
|
113
|
+
// recorded before its children are visited.
|
|
114
|
+
if (repeatableRoots.some((root) => path === root || path.startsWith(`${root}.`))) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (field.repeatable === true) {
|
|
118
|
+
repeatableRoots.push(path);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (field.reportable !== true || field.sensitive === true) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const type = mapFieldType(field);
|
|
125
|
+
if (type === undefined) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const id = path.replace(/\./g, '_');
|
|
129
|
+
if (reserved.has(id)) {
|
|
130
|
+
// A field named like a physical $row column would make the allowlist
|
|
131
|
+
// ambiguous (§5.1) — the physical column wins; rename the field.
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
columns.push(this.buildColumn(formKey, path, id, type, field));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return columns;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private buildColumn(
|
|
141
|
+
formKey: string,
|
|
142
|
+
path: string,
|
|
143
|
+
id: string,
|
|
144
|
+
type: ColumnType,
|
|
145
|
+
field: FieldDef,
|
|
146
|
+
): ManifestColumn {
|
|
147
|
+
const extraction = generatedColumnExpression(path, type);
|
|
148
|
+
const hot = field.indexHint === true;
|
|
149
|
+
const searchable = hot && type === 'text';
|
|
150
|
+
// SORTABILITY needs non-null keys (§5.3): indexHint fields get the
|
|
151
|
+
// COALESCE-wrapped expression — in BOTH the queryable sql and the
|
|
152
|
+
// generated column, so the index is built on exactly what is compared —
|
|
153
|
+
// and are declared non-nullable. Fields without the hint stay un-wrapped,
|
|
154
|
+
// filterable-only, nullable.
|
|
155
|
+
const expression = hot ? `COALESCE(${extraction}, ${nullFallback(type)})` : extraction;
|
|
156
|
+
const name = generatedColumnName(formKey, path, type);
|
|
157
|
+
const plans = generatedColumnIndexes(name, type, { table: 'FormSubmission', searchable });
|
|
158
|
+
const index = plans.length > 0 ? toIndexSpec(plans[0]) : undefined;
|
|
159
|
+
const trgmPlan = plans.find((plan) => plan.using === 'gin');
|
|
160
|
+
const searchIndex = searchable && trgmPlan ? toIndexSpec(trgmPlan) : undefined;
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
id,
|
|
164
|
+
sql: expression,
|
|
165
|
+
type,
|
|
166
|
+
sortable: hot,
|
|
167
|
+
filterable: true,
|
|
168
|
+
...(searchable ? { searchable: true, search: { mode: 'trgm' as const } } : {}),
|
|
169
|
+
nullable: !hot,
|
|
170
|
+
// The §8.1 storage trick: dates are canonical ISO-8601 UTC TEXT, so the
|
|
171
|
+
// lookup binding for `select` fields lives in the DEFINITION column
|
|
172
|
+
// (lookup.dataSource); the manifest only types it 'lookup'.
|
|
173
|
+
...(type === 'datetime' ? { valueKind: 'isoText' as const } : {}),
|
|
174
|
+
// The physical column the indexed tier publishes against (§8.1); the
|
|
175
|
+
// publish lint verifies it exists and suggests the exact ALTER TABLE.
|
|
176
|
+
generated: {
|
|
177
|
+
table: 'FormSubmission',
|
|
178
|
+
name,
|
|
179
|
+
expression,
|
|
180
|
+
comment: generatedColumnComment(formKey, path, type),
|
|
181
|
+
sqlType: generatedSqlType(type),
|
|
182
|
+
},
|
|
183
|
+
...(index ? { index } : {}),
|
|
184
|
+
...(searchIndex ? { searchIndex } : {}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** §3.1 field-type map — anything outside it is skipped, by design. */
|
|
190
|
+
function mapFieldType(field: FieldDef): ColumnType | undefined {
|
|
191
|
+
switch (field.type) {
|
|
192
|
+
case 'text':
|
|
193
|
+
case 'email':
|
|
194
|
+
return 'text';
|
|
195
|
+
case 'number':
|
|
196
|
+
return 'number';
|
|
197
|
+
case 'boolean':
|
|
198
|
+
return 'boolean';
|
|
199
|
+
case 'select':
|
|
200
|
+
return 'lookup';
|
|
201
|
+
case 'date':
|
|
202
|
+
return 'datetime';
|
|
203
|
+
default:
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Typed COALESCE fallback for indexHint columns (§5.3 non-null sort keys). */
|
|
209
|
+
function nullFallback(type: ColumnType): string {
|
|
210
|
+
switch (type) {
|
|
211
|
+
case 'number':
|
|
212
|
+
return '0';
|
|
213
|
+
case 'boolean':
|
|
214
|
+
return 'false';
|
|
215
|
+
default:
|
|
216
|
+
// text/enum/lookup — and datetime, which is ISO-8601 UTC TEXT (§8.1).
|
|
217
|
+
return "''";
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Stored SQL type of the generated column (the §8.1 casting table). */
|
|
222
|
+
function generatedSqlType(type: ColumnType): string {
|
|
223
|
+
switch (type) {
|
|
224
|
+
case 'number':
|
|
225
|
+
return 'numeric';
|
|
226
|
+
case 'boolean':
|
|
227
|
+
return 'boolean';
|
|
228
|
+
default:
|
|
229
|
+
return 'text';
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function toIndexSpec(plan: GeneratedIndexPlan): IndexSpec {
|
|
234
|
+
return { name: plan.name, kind: plan.using, table: 'FormSubmission', expr: plan.expr };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The §2.1 allowlist of PHYSICAL FormSubmission columns, referenced as
|
|
239
|
+
* `$row.<id>` in definitions. Index specs name the indexes the template's
|
|
240
|
+
* Prisma schema already creates — declared capabilities stay honest (§5.2).
|
|
241
|
+
*/
|
|
242
|
+
function physicalRowColumns(): ManifestColumn[] {
|
|
243
|
+
return [
|
|
244
|
+
{
|
|
245
|
+
id: 'status',
|
|
246
|
+
sql: '"status"::text',
|
|
247
|
+
type: 'enum',
|
|
248
|
+
filterable: true,
|
|
249
|
+
sortable: true,
|
|
250
|
+
nullable: false,
|
|
251
|
+
index: {
|
|
252
|
+
name: 'FormSubmission_orgId_formKey_status_idx',
|
|
253
|
+
kind: 'btree',
|
|
254
|
+
table: 'FormSubmission',
|
|
255
|
+
expr: '"orgId", "formKey", "status"',
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: 'createdAt',
|
|
260
|
+
sql: '"createdAt"',
|
|
261
|
+
type: 'datetime',
|
|
262
|
+
valueKind: 'native',
|
|
263
|
+
sortable: true,
|
|
264
|
+
filterable: true,
|
|
265
|
+
nullable: false,
|
|
266
|
+
index: {
|
|
267
|
+
name: 'FormSubmission_orgId_createdAt_idx',
|
|
268
|
+
kind: 'btree',
|
|
269
|
+
table: 'FormSubmission',
|
|
270
|
+
expr: '"orgId", "createdAt"',
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: 'updatedAt',
|
|
275
|
+
sql: '"updatedAt"',
|
|
276
|
+
type: 'datetime',
|
|
277
|
+
valueKind: 'native',
|
|
278
|
+
sortable: true,
|
|
279
|
+
nullable: false,
|
|
280
|
+
// No index declared: sortable on the live tier only until an app
|
|
281
|
+
// publishes an indexed report over it.
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: 'createdBy',
|
|
285
|
+
sql: '"createdBy"',
|
|
286
|
+
type: 'text',
|
|
287
|
+
filterable: true,
|
|
288
|
+
// Null for anonymous/public submissions — filterable only.
|
|
289
|
+
nullable: true,
|
|
290
|
+
},
|
|
291
|
+
];
|
|
292
|
+
}
|
|
@@ -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
|
+
}
|