@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,335 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
3
|
+
import { DiscoveryModule } from '@nestjs/core';
|
|
4
|
+
import {
|
|
5
|
+
createInMemoryResultCache,
|
|
6
|
+
ReportActionService,
|
|
7
|
+
ReportDefinitionService,
|
|
8
|
+
ReportExportService,
|
|
9
|
+
ReportQueryService,
|
|
10
|
+
ReportRowActionRegistry,
|
|
11
|
+
ReportSourceRegistry,
|
|
12
|
+
ReportTagService,
|
|
13
|
+
ReportViewService,
|
|
14
|
+
SourceProviderRegistry,
|
|
15
|
+
type ResultCache,
|
|
16
|
+
} from '@ftisindia/report-builder';
|
|
17
|
+
import { AuthModule } from '../auth/auth.module';
|
|
18
|
+
import { ReportsActionsService } from './application/services/reports-actions.service';
|
|
19
|
+
import { ReportsDefinitionsService } from './application/services/reports-definitions.service';
|
|
20
|
+
import { ReportsExportsService } from './application/services/reports-exports.service';
|
|
21
|
+
import { ReportsExportDispatcherService } from './application/services/reports-export-dispatcher.service';
|
|
22
|
+
import { ReportsQueriesService } from './application/services/reports-queries.service';
|
|
23
|
+
import { ReportsSettingsReader } from './application/services/reports-settings-reader.service';
|
|
24
|
+
import { ReportsViewsService } from './application/services/reports-views.service';
|
|
25
|
+
import { CaslReportsAuthorization } from './infrastructure/casl-reports-authorization';
|
|
26
|
+
import { PrismaReportsAuditSink } from './infrastructure/audit-sink.adapter';
|
|
27
|
+
import { ReportsPrismaTxRunner } from './infrastructure/prisma-tx-runner';
|
|
28
|
+
import { PrismaQueryExecutor } from './infrastructure/prisma-query-executor';
|
|
29
|
+
import { PrismaSnapshotRunner } from './infrastructure/prisma-snapshot-runner';
|
|
30
|
+
import { PrismaReportsCatalog } from './infrastructure/prisma-catalog.adapter';
|
|
31
|
+
import { ReportsJobQueueNoop } from './infrastructure/reports-job-queue.adapter';
|
|
32
|
+
import { LocalDiskReportExportStorage } from './infrastructure/storage/local-disk-export-storage.adapter';
|
|
33
|
+
import { RequestReportsContext } from './infrastructure/request-reports-context';
|
|
34
|
+
import { ReportsRegistryBootstrapService } from './infrastructure/registry/registry-bootstrap.service';
|
|
35
|
+
import { ReportsSchemaCheckService } from './infrastructure/schema-check/reports-schema-check.service';
|
|
36
|
+
import {
|
|
37
|
+
PrismaBulkActionRunStore,
|
|
38
|
+
PrismaExportJobStore,
|
|
39
|
+
PrismaReportDefinitionStore,
|
|
40
|
+
PrismaRowTagStore,
|
|
41
|
+
PrismaSavedViewStore,
|
|
42
|
+
} from './infrastructure/stores';
|
|
43
|
+
import { ReportsActionsController } from './presentation/reports-actions.controller';
|
|
44
|
+
import { ReportsDefinitionsController } from './presentation/reports-definitions.controller';
|
|
45
|
+
import { ReportsExportController } from './presentation/reports-export.controller';
|
|
46
|
+
import { ReportsExportJobsController } from './presentation/reports-export-jobs.controller';
|
|
47
|
+
import { ReportsQueryController } from './presentation/reports-query.controller';
|
|
48
|
+
import { ReportsViewsController } from './presentation/reports-views.controller';
|
|
49
|
+
import { OrgMembersReportSource } from './sources/org-members.source';
|
|
50
|
+
import { REPORTS_RESULT_CACHE, REPORTS_TOKEN_SECRET } from './reports.tokens';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The reports glue module — binds the framework-free @ftisindia/report-builder
|
|
54
|
+
* engine to this app's services (request context, CASL RBAC, AuditService,
|
|
55
|
+
* Prisma) and ships reports-OWNED async-export infrastructure (its own worker
|
|
56
|
+
* over ReportExportJob + org-scoped file storage). It imports NO FormsModule:
|
|
57
|
+
* form-backed sources and the delegated grid verbs live in the optional
|
|
58
|
+
* ReportsFormsModule bridge (§3.2 standalone guarantee). Engine services are
|
|
59
|
+
* composed here via factories over the adapters and stores; app extensions
|
|
60
|
+
* register through the @ReportSource/@ReportRowAction decorators.
|
|
61
|
+
*
|
|
62
|
+
* The engine core knows nothing about the form builder (the standalone
|
|
63
|
+
* guarantee, report design §3.2); the forms adapter wired below is what makes
|
|
64
|
+
* `kind: "form"` sources exist in THIS app.
|
|
65
|
+
*/
|
|
66
|
+
@Module({
|
|
67
|
+
imports: [AuthModule, DiscoveryModule],
|
|
68
|
+
controllers: [
|
|
69
|
+
// Listed before the definitions controller so the literal 'exports'
|
|
70
|
+
// segment wins over the ':key' parameter on the shared base path.
|
|
71
|
+
ReportsExportJobsController,
|
|
72
|
+
ReportsDefinitionsController,
|
|
73
|
+
ReportsQueryController,
|
|
74
|
+
ReportsViewsController,
|
|
75
|
+
ReportsActionsController,
|
|
76
|
+
ReportsExportController,
|
|
77
|
+
],
|
|
78
|
+
providers: [
|
|
79
|
+
// Adapters over the ecosystem seams.
|
|
80
|
+
RequestReportsContext,
|
|
81
|
+
CaslReportsAuthorization,
|
|
82
|
+
PrismaReportsAuditSink,
|
|
83
|
+
ReportsPrismaTxRunner,
|
|
84
|
+
PrismaQueryExecutor,
|
|
85
|
+
PrismaSnapshotRunner,
|
|
86
|
+
PrismaReportsCatalog,
|
|
87
|
+
// Reports-OWNED async-export infrastructure — no forms outbox / UploadedFile.
|
|
88
|
+
ReportsJobQueueNoop,
|
|
89
|
+
LocalDiskReportExportStorage,
|
|
90
|
+
ReportsExportDispatcherService,
|
|
91
|
+
// Prisma store implementations of the engine ports.
|
|
92
|
+
PrismaReportDefinitionStore,
|
|
93
|
+
PrismaSavedViewStore,
|
|
94
|
+
PrismaExportJobStore,
|
|
95
|
+
PrismaBulkActionRunStore,
|
|
96
|
+
PrismaRowTagStore,
|
|
97
|
+
ReportsSettingsReader,
|
|
98
|
+
// Engine registries — singletons; extensions land via the bootstrap scan.
|
|
99
|
+
{ provide: ReportSourceRegistry, useFactory: () => new ReportSourceRegistry() },
|
|
100
|
+
{ provide: SourceProviderRegistry, useFactory: () => new SourceProviderRegistry() },
|
|
101
|
+
{ provide: ReportRowActionRegistry, useFactory: () => new ReportRowActionRegistry() },
|
|
102
|
+
// Result cache (§5.7) — in-memory by default; rebind the token for Redis.
|
|
103
|
+
{ provide: REPORTS_RESULT_CACHE, useFactory: () => createInMemoryResultCache() },
|
|
104
|
+
// HMAC key bytes for cursors and action tokens (resolved in reports.config).
|
|
105
|
+
{
|
|
106
|
+
provide: REPORTS_TOKEN_SECRET,
|
|
107
|
+
useFactory: (config: ConfigService) => {
|
|
108
|
+
const secret = config.get<Uint8Array>('reports.tokenSecret');
|
|
109
|
+
if (secret === undefined || secret.length === 0) {
|
|
110
|
+
throw new Error('reports.tokenSecret failed to resolve — check reports.config.ts.');
|
|
111
|
+
}
|
|
112
|
+
return secret;
|
|
113
|
+
},
|
|
114
|
+
inject: [ConfigService],
|
|
115
|
+
},
|
|
116
|
+
// Engine orchestration services, composed over the seams above.
|
|
117
|
+
{
|
|
118
|
+
provide: ReportDefinitionService,
|
|
119
|
+
useFactory: (
|
|
120
|
+
store: PrismaReportDefinitionStore,
|
|
121
|
+
providers: SourceProviderRegistry,
|
|
122
|
+
rowActions: ReportRowActionRegistry,
|
|
123
|
+
authz: CaslReportsAuthorization,
|
|
124
|
+
audit: PrismaReportsAuditSink,
|
|
125
|
+
txRunner: ReportsPrismaTxRunner,
|
|
126
|
+
catalog: PrismaReportsCatalog,
|
|
127
|
+
executor: PrismaQueryExecutor,
|
|
128
|
+
settings: ReportsSettingsReader,
|
|
129
|
+
secret: Uint8Array,
|
|
130
|
+
) =>
|
|
131
|
+
new ReportDefinitionService({
|
|
132
|
+
store,
|
|
133
|
+
providers,
|
|
134
|
+
rowActions,
|
|
135
|
+
authz,
|
|
136
|
+
audit,
|
|
137
|
+
txRunner,
|
|
138
|
+
catalog,
|
|
139
|
+
executor,
|
|
140
|
+
policyFor: (orgId) => settings.policyFor(orgId),
|
|
141
|
+
secret,
|
|
142
|
+
}),
|
|
143
|
+
inject: [
|
|
144
|
+
PrismaReportDefinitionStore,
|
|
145
|
+
SourceProviderRegistry,
|
|
146
|
+
ReportRowActionRegistry,
|
|
147
|
+
CaslReportsAuthorization,
|
|
148
|
+
PrismaReportsAuditSink,
|
|
149
|
+
ReportsPrismaTxRunner,
|
|
150
|
+
PrismaReportsCatalog,
|
|
151
|
+
PrismaQueryExecutor,
|
|
152
|
+
ReportsSettingsReader,
|
|
153
|
+
REPORTS_TOKEN_SECRET,
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
provide: ReportQueryService,
|
|
158
|
+
useFactory: (
|
|
159
|
+
store: PrismaReportDefinitionStore,
|
|
160
|
+
providers: SourceProviderRegistry,
|
|
161
|
+
rowActions: ReportRowActionRegistry,
|
|
162
|
+
tags: PrismaRowTagStore,
|
|
163
|
+
executor: PrismaQueryExecutor,
|
|
164
|
+
authz: CaslReportsAuthorization,
|
|
165
|
+
cache: ResultCache,
|
|
166
|
+
settings: ReportsSettingsReader,
|
|
167
|
+
secret: Uint8Array,
|
|
168
|
+
) =>
|
|
169
|
+
new ReportQueryService({
|
|
170
|
+
store,
|
|
171
|
+
providers,
|
|
172
|
+
rowActions,
|
|
173
|
+
tags,
|
|
174
|
+
executor,
|
|
175
|
+
authz,
|
|
176
|
+
cache,
|
|
177
|
+
policyFor: (orgId) => settings.policyFor(orgId),
|
|
178
|
+
secret,
|
|
179
|
+
}),
|
|
180
|
+
inject: [
|
|
181
|
+
PrismaReportDefinitionStore,
|
|
182
|
+
SourceProviderRegistry,
|
|
183
|
+
ReportRowActionRegistry,
|
|
184
|
+
PrismaRowTagStore,
|
|
185
|
+
PrismaQueryExecutor,
|
|
186
|
+
CaslReportsAuthorization,
|
|
187
|
+
REPORTS_RESULT_CACHE,
|
|
188
|
+
ReportsSettingsReader,
|
|
189
|
+
REPORTS_TOKEN_SECRET,
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
provide: ReportViewService,
|
|
194
|
+
useFactory: (
|
|
195
|
+
views: PrismaSavedViewStore,
|
|
196
|
+
definitions: PrismaReportDefinitionStore,
|
|
197
|
+
authz: CaslReportsAuthorization,
|
|
198
|
+
audit: PrismaReportsAuditSink,
|
|
199
|
+
txRunner: ReportsPrismaTxRunner,
|
|
200
|
+
) => new ReportViewService({ views, definitions, authz, audit, txRunner }),
|
|
201
|
+
inject: [
|
|
202
|
+
PrismaSavedViewStore,
|
|
203
|
+
PrismaReportDefinitionStore,
|
|
204
|
+
CaslReportsAuthorization,
|
|
205
|
+
PrismaReportsAuditSink,
|
|
206
|
+
ReportsPrismaTxRunner,
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
provide: ReportTagService,
|
|
211
|
+
useFactory: (
|
|
212
|
+
tags: PrismaRowTagStore,
|
|
213
|
+
authz: CaslReportsAuthorization,
|
|
214
|
+
audit: PrismaReportsAuditSink,
|
|
215
|
+
settings: ReportsSettingsReader,
|
|
216
|
+
) =>
|
|
217
|
+
new ReportTagService({
|
|
218
|
+
tags,
|
|
219
|
+
authz,
|
|
220
|
+
audit,
|
|
221
|
+
policyFor: (orgId) => settings.policyFor(orgId),
|
|
222
|
+
}),
|
|
223
|
+
inject: [
|
|
224
|
+
PrismaRowTagStore,
|
|
225
|
+
CaslReportsAuthorization,
|
|
226
|
+
PrismaReportsAuditSink,
|
|
227
|
+
ReportsSettingsReader,
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
provide: ReportActionService,
|
|
232
|
+
useFactory: (
|
|
233
|
+
definitions: PrismaReportDefinitionStore,
|
|
234
|
+
providers: SourceProviderRegistry,
|
|
235
|
+
actions: ReportRowActionRegistry,
|
|
236
|
+
runs: PrismaBulkActionRunStore,
|
|
237
|
+
executor: PrismaQueryExecutor,
|
|
238
|
+
authz: CaslReportsAuthorization,
|
|
239
|
+
audit: PrismaReportsAuditSink,
|
|
240
|
+
txRunner: ReportsPrismaTxRunner,
|
|
241
|
+
cache: ResultCache,
|
|
242
|
+
settings: ReportsSettingsReader,
|
|
243
|
+
secret: Uint8Array,
|
|
244
|
+
) =>
|
|
245
|
+
new ReportActionService({
|
|
246
|
+
definitions,
|
|
247
|
+
providers,
|
|
248
|
+
actions,
|
|
249
|
+
runs,
|
|
250
|
+
executor,
|
|
251
|
+
authz,
|
|
252
|
+
audit,
|
|
253
|
+
txRunner,
|
|
254
|
+
cache,
|
|
255
|
+
policyFor: (orgId) => settings.policyFor(orgId),
|
|
256
|
+
secret,
|
|
257
|
+
// Glue row actions are Nest providers with their own injection —
|
|
258
|
+
// the engine's service-locator seam stays empty in this app.
|
|
259
|
+
services: {},
|
|
260
|
+
}),
|
|
261
|
+
inject: [
|
|
262
|
+
PrismaReportDefinitionStore,
|
|
263
|
+
SourceProviderRegistry,
|
|
264
|
+
ReportRowActionRegistry,
|
|
265
|
+
PrismaBulkActionRunStore,
|
|
266
|
+
PrismaQueryExecutor,
|
|
267
|
+
CaslReportsAuthorization,
|
|
268
|
+
PrismaReportsAuditSink,
|
|
269
|
+
ReportsPrismaTxRunner,
|
|
270
|
+
REPORTS_RESULT_CACHE,
|
|
271
|
+
ReportsSettingsReader,
|
|
272
|
+
REPORTS_TOKEN_SECRET,
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
provide: ReportExportService,
|
|
277
|
+
useFactory: (
|
|
278
|
+
definitions: PrismaReportDefinitionStore,
|
|
279
|
+
providers: SourceProviderRegistry,
|
|
280
|
+
jobs: PrismaExportJobStore,
|
|
281
|
+
queue: ReportsJobQueueNoop,
|
|
282
|
+
sink: LocalDiskReportExportStorage,
|
|
283
|
+
snapshot: PrismaSnapshotRunner,
|
|
284
|
+
executor: PrismaQueryExecutor,
|
|
285
|
+
authz: CaslReportsAuthorization,
|
|
286
|
+
audit: PrismaReportsAuditSink,
|
|
287
|
+
txRunner: ReportsPrismaTxRunner,
|
|
288
|
+
settings: ReportsSettingsReader,
|
|
289
|
+
secret: Uint8Array,
|
|
290
|
+
) =>
|
|
291
|
+
new ReportExportService({
|
|
292
|
+
definitions,
|
|
293
|
+
providers,
|
|
294
|
+
jobs,
|
|
295
|
+
queue,
|
|
296
|
+
sink,
|
|
297
|
+
snapshot,
|
|
298
|
+
executor,
|
|
299
|
+
authz,
|
|
300
|
+
audit,
|
|
301
|
+
txRunner,
|
|
302
|
+
policyFor: (orgId) => settings.policyFor(orgId),
|
|
303
|
+
secret,
|
|
304
|
+
}),
|
|
305
|
+
inject: [
|
|
306
|
+
PrismaReportDefinitionStore,
|
|
307
|
+
SourceProviderRegistry,
|
|
308
|
+
PrismaExportJobStore,
|
|
309
|
+
ReportsJobQueueNoop,
|
|
310
|
+
LocalDiskReportExportStorage,
|
|
311
|
+
PrismaSnapshotRunner,
|
|
312
|
+
PrismaQueryExecutor,
|
|
313
|
+
CaslReportsAuthorization,
|
|
314
|
+
PrismaReportsAuditSink,
|
|
315
|
+
ReportsPrismaTxRunner,
|
|
316
|
+
ReportsSettingsReader,
|
|
317
|
+
REPORTS_TOKEN_SECRET,
|
|
318
|
+
],
|
|
319
|
+
},
|
|
320
|
+
// The example custom source (form-backed sources + delegated form verbs
|
|
321
|
+
// live in the optional ReportsFormsModule, not here — §3.2 standalone).
|
|
322
|
+
OrgMembersReportSource,
|
|
323
|
+
// Startup wiring + background work.
|
|
324
|
+
ReportsRegistryBootstrapService,
|
|
325
|
+
ReportsSchemaCheckService,
|
|
326
|
+
// Thin HTTP-facing services.
|
|
327
|
+
ReportsDefinitionsService,
|
|
328
|
+
ReportsQueriesService,
|
|
329
|
+
ReportsViewsService,
|
|
330
|
+
ReportsActionsService,
|
|
331
|
+
ReportsExportsService,
|
|
332
|
+
],
|
|
333
|
+
exports: [ReportSourceRegistry, SourceProviderRegistry, ReportRowActionRegistry],
|
|
334
|
+
})
|
|
335
|
+
export class ReportsModule {}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DI tokens for swappable reports seams. The defaults are bound in
|
|
3
|
+
* reports.module.ts; apps override them with custom providers (e.g. a
|
|
4
|
+
* Redis-backed ResultCache) without touching the module internals.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Binds the engine's ResultCache port (report design §5.7). */
|
|
8
|
+
export const REPORTS_RESULT_CACHE = Symbol('REPORTS_RESULT_CACHE');
|
|
9
|
+
|
|
10
|
+
/** Key bytes for cursor/action-token HMACs (report design §5.3/§6.3). */
|
|
11
|
+
export const REPORTS_TOKEN_SECRET = Symbol('REPORTS_TOKEN_SECRET');
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import type { ReportSourceDef, SourceManifest, SourceQuery } from '@ftisindia/report-builder';
|
|
3
|
+
import { ReportSource } from '../infrastructure/registry/report-extension.decorators';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `kind: "custom"` example source (report design §3.2) over the template's own
|
|
7
|
+
* Membership/User/Role tables — the Phase-1 milestone proof (§14): a complete
|
|
8
|
+
* report with NO form-builder involvement, demonstrating the standalone
|
|
9
|
+
* guarantee before form integration exists.
|
|
10
|
+
*
|
|
11
|
+
* The contract split (§3): this class declares STRUCTURE only — the compiler
|
|
12
|
+
* owns query construction. It never sees a QuerySpec, so it cannot be injected
|
|
13
|
+
* and cannot bypass the allowlist.
|
|
14
|
+
*
|
|
15
|
+
* Nullability is read off prisma/schema.prisma, not assumed: User.displayName
|
|
16
|
+
* and User.email are nullable (String?), so their sortable columns are
|
|
17
|
+
* COALESCE-wrapped to '' and declared nullable: false — NULL poisons keyset
|
|
18
|
+
* predicates (§5.3). Role.name and the Membership columns are NOT NULL.
|
|
19
|
+
*
|
|
20
|
+
* Index specs are honest documentation (§5.2): they name the indexes an app
|
|
21
|
+
* WOULD create to publish this source on the indexed tier (org-leading btrees
|
|
22
|
+
* are impossible for User-side columns — User carries no orgId — so those
|
|
23
|
+
* specs live on "User" without the org prefix). The shipped example report
|
|
24
|
+
* runs on the LIVE tier, where the catalog lint does not require them.
|
|
25
|
+
*/
|
|
26
|
+
@Injectable()
|
|
27
|
+
@ReportSource()
|
|
28
|
+
export class OrgMembersReportSource implements ReportSourceDef {
|
|
29
|
+
readonly key = 'org-members';
|
|
30
|
+
|
|
31
|
+
manifest(): SourceManifest {
|
|
32
|
+
return {
|
|
33
|
+
// Membership id — the stable row identity tags and actions bind to (§7).
|
|
34
|
+
rowId: 'm."id"',
|
|
35
|
+
orgScoped: true,
|
|
36
|
+
columns: [
|
|
37
|
+
{
|
|
38
|
+
id: 'displayName',
|
|
39
|
+
sql: `COALESCE(u."displayName", '')`,
|
|
40
|
+
type: 'text',
|
|
41
|
+
sortable: true,
|
|
42
|
+
filterable: true,
|
|
43
|
+
searchable: true,
|
|
44
|
+
search: { mode: 'trgm' },
|
|
45
|
+
nullable: false,
|
|
46
|
+
collation: 'C',
|
|
47
|
+
index: {
|
|
48
|
+
name: 'idx_rb_member_display',
|
|
49
|
+
kind: 'btree',
|
|
50
|
+
table: 'User',
|
|
51
|
+
expr: `COALESCE("displayName", '') COLLATE "C"`,
|
|
52
|
+
},
|
|
53
|
+
// A btree cannot carry gin_trgm_ops — search gets its own index.
|
|
54
|
+
searchIndex: {
|
|
55
|
+
name: 'idx_rb_member_display_trgm',
|
|
56
|
+
kind: 'gin',
|
|
57
|
+
table: 'User',
|
|
58
|
+
expr: `COALESCE("displayName", '') gin_trgm_ops`,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'email',
|
|
63
|
+
// User.email is String? in schema.prisma — surrogate '' keeps the
|
|
64
|
+
// sort key non-null (§5.3).
|
|
65
|
+
sql: `COALESCE(u."email", '')`,
|
|
66
|
+
type: 'text',
|
|
67
|
+
sortable: true,
|
|
68
|
+
filterable: true,
|
|
69
|
+
nullable: false,
|
|
70
|
+
collation: 'C',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'role',
|
|
74
|
+
sql: 'r."name"',
|
|
75
|
+
type: 'text',
|
|
76
|
+
sortable: true,
|
|
77
|
+
filterable: true,
|
|
78
|
+
nullable: false,
|
|
79
|
+
collation: 'C',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'status',
|
|
83
|
+
// MembershipStatus enum → text for binds and projection.
|
|
84
|
+
sql: 'm."status"::text',
|
|
85
|
+
type: 'enum',
|
|
86
|
+
filterable: true,
|
|
87
|
+
nullable: false,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'joinedAt',
|
|
91
|
+
sql: 'm."createdAt"',
|
|
92
|
+
type: 'datetime',
|
|
93
|
+
valueKind: 'native',
|
|
94
|
+
sortable: true,
|
|
95
|
+
nullable: false,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** FROM/JOIN only — WHERE/ORDER/LIMIT belong to the compiler (§3). */
|
|
102
|
+
baseQuery(): SourceQuery {
|
|
103
|
+
return {
|
|
104
|
+
from:
|
|
105
|
+
'"Membership" m' +
|
|
106
|
+
' JOIN "User" u ON u."id" = m."userId"' +
|
|
107
|
+
' JOIN "Role" r ON r."id" = m."roleId"',
|
|
108
|
+
orgColumn: 'm."orgId"',
|
|
109
|
+
primaryTable: 'Membership',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -35,6 +35,66 @@ export const settingDefinitions = {
|
|
|
35
35
|
defaultValue: '',
|
|
36
36
|
parse: parseOptionalUrl,
|
|
37
37
|
},
|
|
38
|
+
// Form builder (@ftisindia/form-builder) — org-variable policy lives here so
|
|
39
|
+
// it is typed, validated, and audited like every other org setting.
|
|
40
|
+
'forms.allowedDangerousActions': {
|
|
41
|
+
defaultValue: [],
|
|
42
|
+
parse: parseStringArray('forms.allowedDangerousActions', 50),
|
|
43
|
+
},
|
|
44
|
+
'forms.maxFileSizeMb': {
|
|
45
|
+
defaultValue: 10,
|
|
46
|
+
parse: parsePositiveInt('forms.maxFileSizeMb', 1024),
|
|
47
|
+
},
|
|
48
|
+
'forms.enableRuleIteration': {
|
|
49
|
+
defaultValue: false,
|
|
50
|
+
parse: parseBoolean,
|
|
51
|
+
},
|
|
52
|
+
'forms.virusScanRequired': {
|
|
53
|
+
defaultValue: false,
|
|
54
|
+
parse: parseBoolean,
|
|
55
|
+
},
|
|
56
|
+
'forms.maxSubmissionsPerIpPerDay': {
|
|
57
|
+
defaultValue: 50,
|
|
58
|
+
parse: parsePositiveInt('forms.maxSubmissionsPerIpPerDay', 100000),
|
|
59
|
+
},
|
|
60
|
+
// SSRF gate for definition webhooks: empty list = webhooks disabled until
|
|
61
|
+
// an admin explicitly allows destination hosts (changes are audited).
|
|
62
|
+
'forms.webhookAllowedHosts': {
|
|
63
|
+
defaultValue: [],
|
|
64
|
+
parse: parseStringArray('forms.webhookAllowedHosts', 50),
|
|
65
|
+
},
|
|
66
|
+
// Report builder (@ftisindia/report-builder) — runtime budgets and caps
|
|
67
|
+
// (report design §5.2/§8/§9). Typed, validated, audited like every setting.
|
|
68
|
+
'reports.statementTimeoutMs': {
|
|
69
|
+
defaultValue: 5000,
|
|
70
|
+
parse: parsePositiveInt('reports.statementTimeoutMs', 60000),
|
|
71
|
+
},
|
|
72
|
+
'reports.exportMaxSnapshotSeconds': {
|
|
73
|
+
defaultValue: 120,
|
|
74
|
+
parse: parsePositiveInt('reports.exportMaxSnapshotSeconds', 3600),
|
|
75
|
+
},
|
|
76
|
+
'reports.maxRowsSync': {
|
|
77
|
+
defaultValue: 10000,
|
|
78
|
+
parse: parsePositiveInt('reports.maxRowsSync', 100000),
|
|
79
|
+
},
|
|
80
|
+
'reports.countCap': {
|
|
81
|
+
defaultValue: 10000,
|
|
82
|
+
parse: parsePositiveInt('reports.countCap', 1000000),
|
|
83
|
+
},
|
|
84
|
+
'reports.liveTierMaxRows': {
|
|
85
|
+
defaultValue: 50000,
|
|
86
|
+
parse: parsePositiveInt('reports.liveTierMaxRows', 10000000),
|
|
87
|
+
},
|
|
88
|
+
// 0 disables the short-TTL result cache (report design §5.7).
|
|
89
|
+
'reports.resultCacheTtlMs': {
|
|
90
|
+
defaultValue: 0,
|
|
91
|
+
parse: parseNonNegativeInt('reports.resultCacheTtlMs', 300000),
|
|
92
|
+
},
|
|
93
|
+
// Curated tag vocabulary; empty = free-form tags (report design §7).
|
|
94
|
+
'reports.tagVocabulary': {
|
|
95
|
+
defaultValue: [],
|
|
96
|
+
parse: parseStringArray('reports.tagVocabulary', 200),
|
|
97
|
+
},
|
|
38
98
|
timezone: {
|
|
39
99
|
defaultValue: 'UTC',
|
|
40
100
|
parse: parseTimezone,
|
|
@@ -101,6 +161,40 @@ function parseOptionalUrl(value: unknown) {
|
|
|
101
161
|
}
|
|
102
162
|
}
|
|
103
163
|
|
|
164
|
+
function parseStringArray(key: string, maxItems: number) {
|
|
165
|
+
return (value: unknown) => {
|
|
166
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
|
|
167
|
+
throw new BadRequestException(`${key} must be an array of strings.`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (value.length > maxItems) {
|
|
171
|
+
throw new BadRequestException(`${key} must not contain more than ${maxItems} items.`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return value.map((item) => (item as string).trim()).filter((item) => item.length > 0);
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parsePositiveInt(key: string, max: number) {
|
|
179
|
+
return (value: unknown) => {
|
|
180
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > max) {
|
|
181
|
+
throw new BadRequestException(`${key} must be an integer between 1 and ${max}.`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return value;
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function parseNonNegativeInt(key: string, max: number) {
|
|
189
|
+
return (value: unknown) => {
|
|
190
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > max) {
|
|
191
|
+
throw new BadRequestException(`${key} must be an integer between 0 and ${max}.`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return value;
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
104
198
|
function parseTimezone(value: unknown) {
|
|
105
199
|
if (typeof value !== 'string') {
|
|
106
200
|
throw new BadRequestException('timezone must be a string.');
|