@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,113 @@
|
|
|
1
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
|
|
3
|
+
/** Mirrors the engine's ReportMetaDescriptor (§4/§10) — the grid UI is generic. */
|
|
4
|
+
export class ReportColumnMetaDto {
|
|
5
|
+
@ApiProperty({ example: 'status' })
|
|
6
|
+
id!: string;
|
|
7
|
+
|
|
8
|
+
@ApiProperty({ example: 'Status' })
|
|
9
|
+
header!: string;
|
|
10
|
+
|
|
11
|
+
@ApiProperty({ example: 'enum' })
|
|
12
|
+
type!: string;
|
|
13
|
+
|
|
14
|
+
@ApiProperty({ example: true })
|
|
15
|
+
sortable!: boolean;
|
|
16
|
+
|
|
17
|
+
@ApiProperty({ example: true })
|
|
18
|
+
filterable!: boolean;
|
|
19
|
+
|
|
20
|
+
@ApiProperty({ example: false })
|
|
21
|
+
searchable!: boolean;
|
|
22
|
+
|
|
23
|
+
@ApiProperty({
|
|
24
|
+
type: [String],
|
|
25
|
+
example: ['eq', 'in'],
|
|
26
|
+
description: 'The exact operator set this column accepts.',
|
|
27
|
+
})
|
|
28
|
+
operators!: string[];
|
|
29
|
+
|
|
30
|
+
@ApiPropertyOptional({ type: [String] })
|
|
31
|
+
enum?: string[];
|
|
32
|
+
|
|
33
|
+
@ApiPropertyOptional({ type: 'object', additionalProperties: true })
|
|
34
|
+
lookup?: Record<string, unknown>;
|
|
35
|
+
|
|
36
|
+
@ApiProperty({ example: true })
|
|
37
|
+
exportable!: boolean;
|
|
38
|
+
|
|
39
|
+
@ApiPropertyOptional({
|
|
40
|
+
type: 'object',
|
|
41
|
+
additionalProperties: true,
|
|
42
|
+
description: 'Opaque UI hints — never interpreted by the backend.',
|
|
43
|
+
})
|
|
44
|
+
ui?: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class ReportRowActionMetaDto {
|
|
48
|
+
@ApiProperty({ example: 'markReviewed' })
|
|
49
|
+
name!: string;
|
|
50
|
+
|
|
51
|
+
@ApiProperty({ enum: ['transactional', 'post-commit'], example: 'transactional' })
|
|
52
|
+
kind!: string;
|
|
53
|
+
|
|
54
|
+
@ApiPropertyOptional({
|
|
55
|
+
type: 'object',
|
|
56
|
+
additionalProperties: true,
|
|
57
|
+
description: 'JSON Schema for the action input, when the action declares one.',
|
|
58
|
+
})
|
|
59
|
+
inputSchema?: Record<string, unknown>;
|
|
60
|
+
|
|
61
|
+
@ApiProperty({ example: false })
|
|
62
|
+
destructive!: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class ReportExportMetaDto {
|
|
66
|
+
@ApiProperty({ type: [String], example: ['csv', 'xlsx'] })
|
|
67
|
+
formats!: string[];
|
|
68
|
+
|
|
69
|
+
@ApiProperty({ example: 10000 })
|
|
70
|
+
maxRowsSync!: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class ReportMetaResponseDto {
|
|
74
|
+
@ApiProperty({ example: 'abstract-review-board' })
|
|
75
|
+
reportKey!: string;
|
|
76
|
+
|
|
77
|
+
@ApiProperty({ example: 4 })
|
|
78
|
+
reportVersion!: number;
|
|
79
|
+
|
|
80
|
+
@ApiPropertyOptional({ example: 'Abstract review board' })
|
|
81
|
+
title?: string;
|
|
82
|
+
|
|
83
|
+
@ApiPropertyOptional({ example: 'All submitted abstracts with review status.' })
|
|
84
|
+
description?: string;
|
|
85
|
+
|
|
86
|
+
@ApiProperty({ type: [ReportColumnMetaDto] })
|
|
87
|
+
columns!: ReportColumnMetaDto[];
|
|
88
|
+
|
|
89
|
+
@ApiProperty({
|
|
90
|
+
type: 'array',
|
|
91
|
+
items: { type: 'object', additionalProperties: true },
|
|
92
|
+
example: [{ column: 'createdAt', dir: 'desc' }],
|
|
93
|
+
})
|
|
94
|
+
defaultSort!: Record<string, unknown>[];
|
|
95
|
+
|
|
96
|
+
@ApiProperty({ example: true })
|
|
97
|
+
searchEnabled!: boolean;
|
|
98
|
+
|
|
99
|
+
@ApiProperty({ type: [ReportRowActionMetaDto] })
|
|
100
|
+
rowActions!: ReportRowActionMetaDto[];
|
|
101
|
+
|
|
102
|
+
@ApiProperty({ type: ReportExportMetaDto })
|
|
103
|
+
export!: ReportExportMetaDto;
|
|
104
|
+
|
|
105
|
+
@ApiProperty({ example: 'indexed' })
|
|
106
|
+
performanceTier!: string;
|
|
107
|
+
|
|
108
|
+
@ApiPropertyOptional({
|
|
109
|
+
example: 900,
|
|
110
|
+
description: 'Declared staleness bound for materialized-tier reports, in seconds (§8).',
|
|
111
|
+
})
|
|
112
|
+
stalenessSeconds?: number;
|
|
113
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
|
|
3
|
+
export class ViewCompatibilityDto {
|
|
4
|
+
@ApiProperty({
|
|
5
|
+
enum: ['ok', 'degraded', 'incompatible'],
|
|
6
|
+
description: 'Verdict against the requested report version (§4, finding #1).',
|
|
7
|
+
})
|
|
8
|
+
status!: 'ok' | 'degraded' | 'incompatible';
|
|
9
|
+
|
|
10
|
+
@ApiProperty({ type: [String] })
|
|
11
|
+
missingColumns!: string[];
|
|
12
|
+
|
|
13
|
+
@ApiProperty({ type: [String] })
|
|
14
|
+
retypedColumns!: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class SavedViewResponseDto {
|
|
18
|
+
@ApiProperty({
|
|
19
|
+
example: '0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3',
|
|
20
|
+
format: 'uuid',
|
|
21
|
+
})
|
|
22
|
+
id!: string;
|
|
23
|
+
|
|
24
|
+
@ApiProperty({
|
|
25
|
+
example: '2c67399d-670c-4025-a5fd-1ea9a211891e',
|
|
26
|
+
format: 'uuid',
|
|
27
|
+
})
|
|
28
|
+
orgId!: string;
|
|
29
|
+
|
|
30
|
+
@ApiProperty({ example: 'abstract-review-board' })
|
|
31
|
+
reportKey!: string;
|
|
32
|
+
|
|
33
|
+
@ApiProperty({
|
|
34
|
+
example: 4,
|
|
35
|
+
description: 'The version the view was authored against (finding #1).',
|
|
36
|
+
})
|
|
37
|
+
reportVersion!: number;
|
|
38
|
+
|
|
39
|
+
@ApiProperty({ example: 'My ML shortlist' })
|
|
40
|
+
name!: string;
|
|
41
|
+
|
|
42
|
+
@ApiProperty({ type: 'object', additionalProperties: true })
|
|
43
|
+
spec!: Record<string, unknown>;
|
|
44
|
+
|
|
45
|
+
@ApiProperty({
|
|
46
|
+
type: String,
|
|
47
|
+
format: 'uuid',
|
|
48
|
+
nullable: true,
|
|
49
|
+
description: 'null ⇒ shared with the org (creation gated by reports.update, §10).',
|
|
50
|
+
})
|
|
51
|
+
ownerId!: string | null;
|
|
52
|
+
|
|
53
|
+
@ApiProperty({ example: '2026-06-01T10:30:00.000Z', format: 'date-time' })
|
|
54
|
+
createdAt!: string;
|
|
55
|
+
|
|
56
|
+
@ApiPropertyOptional({
|
|
57
|
+
type: ViewCompatibilityDto,
|
|
58
|
+
description: 'Present on list responses only.',
|
|
59
|
+
})
|
|
60
|
+
compatibility?: ViewCompatibilityDto;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class SavedViewListResponseDto {
|
|
64
|
+
@ApiProperty({ type: [SavedViewResponseDto] })
|
|
65
|
+
items!: SavedViewResponseDto[];
|
|
66
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CreateReportDefinitionDto } from './create-report-definition.dto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Same envelope as create — the body IS the full ReportDefinition document.
|
|
5
|
+
* The engine enforces the rest: the document key must match the route's
|
|
6
|
+
* report, drafts mutate in place, and editing a published version opens a new
|
|
7
|
+
* draft (report design §2/§10).
|
|
8
|
+
*/
|
|
9
|
+
export class UpdateReportDefinitionDto extends CreateReportDefinitionDto {}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
import { Type } from 'class-transformer';
|
|
3
|
+
import {
|
|
4
|
+
IsObject,
|
|
5
|
+
IsOptional,
|
|
6
|
+
IsString,
|
|
7
|
+
MaxLength,
|
|
8
|
+
MinLength,
|
|
9
|
+
ValidateNested,
|
|
10
|
+
} from 'class-validator';
|
|
11
|
+
import { QuerySpecDto } from './query-spec.dto';
|
|
12
|
+
|
|
13
|
+
export class UpdateSavedViewDto {
|
|
14
|
+
@ApiPropertyOptional({ example: 'My ML shortlist', minLength: 1, maxLength: 120 })
|
|
15
|
+
@IsOptional()
|
|
16
|
+
@IsString()
|
|
17
|
+
@MinLength(1)
|
|
18
|
+
@MaxLength(120)
|
|
19
|
+
name?: string;
|
|
20
|
+
|
|
21
|
+
@ApiPropertyOptional({ type: QuerySpecDto })
|
|
22
|
+
@IsOptional()
|
|
23
|
+
@IsObject()
|
|
24
|
+
@ValidateNested()
|
|
25
|
+
@Type(() => QuerySpecDto)
|
|
26
|
+
spec?: QuerySpecDto;
|
|
27
|
+
}
|
|
@@ -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
|
+
}
|