@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.
Files changed (169) hide show
  1. package/package.json +1 -1
  2. package/template/.env.example +31 -0
  3. package/template/README.md +61 -0
  4. package/template/_gitignore +6 -0
  5. package/template/_package.json +6 -0
  6. package/template/docs/FORMS.md +169 -0
  7. package/template/docs/FORMS_CHECKLIST.md +61 -0
  8. package/template/docs/REPORTS.md +246 -0
  9. package/template/docs/REPORTS_CHECKLIST.md +97 -0
  10. package/template/prisma/migrations/20260612000000_add_form_builder/migration.sql +147 -0
  11. package/template/prisma/migrations/20260613000000_add_report_builder/migration.sql +129 -0
  12. package/template/prisma/schema.prisma +285 -0
  13. package/template/scripts/export-openapi.ts +85 -0
  14. package/template/scripts/gen-form.mjs +149 -0
  15. package/template/scripts/push-form.ts +124 -0
  16. package/template/src/app.module.ts +29 -8
  17. package/template/src/common/dto/membership-response.dto.ts +1 -0
  18. package/template/src/common/dto/role-summary.dto.ts +3 -3
  19. package/template/src/common/dto/user-summary.dto.ts +3 -3
  20. package/template/src/config/app.config.ts +6 -1
  21. package/template/src/config/env.validation.ts +45 -0
  22. package/template/src/config/forms.config.ts +12 -0
  23. package/template/src/config/index.ts +2 -0
  24. package/template/src/config/openapi.ts +12 -0
  25. package/template/src/config/reports-secret.ts +15 -0
  26. package/template/src/config/reports.config.ts +16 -0
  27. package/template/src/main.ts +16 -12
  28. package/template/src/modules/access-control/access-control.module.ts +2 -1
  29. package/template/src/modules/access-control/dto/access-control-response.dto.ts +3 -0
  30. package/template/src/modules/access-control/dto/current-access-control-response.dto.ts +35 -0
  31. package/template/src/modules/access-control/presentation/current-access-control.controller.ts +40 -0
  32. package/template/src/modules/access-control/types/permission-key.ts +27 -0
  33. package/template/src/modules/access-control/types/route-permission-registry.ts +183 -0
  34. package/template/src/modules/audit/dto/audit-response.dto.ts +7 -3
  35. package/template/src/modules/auth/auth.module.ts +3 -1
  36. package/template/src/modules/auth/dto/auth-response.dto.ts +1 -1
  37. package/template/src/modules/forms/application/services/file-gc.service.ts +85 -0
  38. package/template/src/modules/forms/application/services/forms-definitions.service.ts +137 -0
  39. package/template/src/modules/forms/application/services/forms-error.mapper.ts +64 -0
  40. package/template/src/modules/forms/application/services/forms-export.service.ts +210 -0
  41. package/template/src/modules/forms/application/services/forms-files.service.ts +164 -0
  42. package/template/src/modules/forms/application/services/forms-public.service.ts +49 -0
  43. package/template/src/modules/forms/application/services/forms-settings-reader.service.ts +53 -0
  44. package/template/src/modules/forms/application/services/forms-submissions.service.ts +103 -0
  45. package/template/src/modules/forms/application/services/handlers/authenticate.action.ts +37 -0
  46. package/template/src/modules/forms/application/services/handlers/logging-email.handler.ts +22 -0
  47. package/template/src/modules/forms/application/services/handlers/send-confirmation-email.action.ts +40 -0
  48. package/template/src/modules/forms/application/services/handlers/webhook.handler.ts +41 -0
  49. package/template/src/modules/forms/application/services/outbox-dispatcher.service.ts +109 -0
  50. package/template/src/modules/forms/dto/create-form-definition.dto.ts +12 -0
  51. package/template/src/modules/forms/dto/data-source-response.dto.ts +19 -0
  52. package/template/src/modules/forms/dto/export-submissions-query.dto.ts +33 -0
  53. package/template/src/modules/forms/dto/file-upload-response.dto.ts +24 -0
  54. package/template/src/modules/forms/dto/form-definition-response.dto.ts +50 -0
  55. package/template/src/modules/forms/dto/form-render-response.dto.ts +17 -0
  56. package/template/src/modules/forms/dto/list-form-definitions-query.dto.ts +10 -0
  57. package/template/src/modules/forms/dto/list-submissions-query.dto.ts +10 -0
  58. package/template/src/modules/forms/dto/public-submit-form.dto.ts +24 -0
  59. package/template/src/modules/forms/dto/set-public-access.dto.ts +8 -0
  60. package/template/src/modules/forms/dto/submission-response.dto.ts +99 -0
  61. package/template/src/modules/forms/dto/submit-form.dto.ts +50 -0
  62. package/template/src/modules/forms/dto/update-form-definition.dto.ts +12 -0
  63. package/template/src/modules/forms/dto/upload-file-query.dto.ts +33 -0
  64. package/template/src/modules/forms/dto/validate-submission.dto.ts +22 -0
  65. package/template/src/modules/forms/examples/abstract-submission.form.json +80 -0
  66. package/template/src/modules/forms/examples/login.form.json +24 -0
  67. package/template/src/modules/forms/examples/registration.form.json +44 -0
  68. package/template/src/modules/forms/forms.module.ts +226 -0
  69. package/template/src/modules/forms/forms.tokens.ts +6 -0
  70. package/template/src/modules/forms/infrastructure/audit-sink.adapter.ts +30 -0
  71. package/template/src/modules/forms/infrastructure/casl-forms-authorization.ts +31 -0
  72. package/template/src/modules/forms/infrastructure/prisma-tx-runner.ts +17 -0
  73. package/template/src/modules/forms/infrastructure/registry/form-extension.decorators.ts +17 -0
  74. package/template/src/modules/forms/infrastructure/registry/registry-bootstrap.service.ts +82 -0
  75. package/template/src/modules/forms/infrastructure/request-forms-context.ts +60 -0
  76. package/template/src/modules/forms/infrastructure/schema-check/forms-schema-check.service.ts +76 -0
  77. package/template/src/modules/forms/infrastructure/storage/local-disk-storage.adapter.ts +43 -0
  78. package/template/src/modules/forms/infrastructure/stores/index.ts +5 -0
  79. package/template/src/modules/forms/infrastructure/stores/prisma-action-log.store.ts +37 -0
  80. package/template/src/modules/forms/infrastructure/stores/prisma-file.store.ts +108 -0
  81. package/template/src/modules/forms/infrastructure/stores/prisma-form-definition.store.ts +147 -0
  82. package/template/src/modules/forms/infrastructure/stores/prisma-outbox.store.ts +133 -0
  83. package/template/src/modules/forms/infrastructure/stores/prisma-submission.store.ts +164 -0
  84. package/template/src/modules/forms/presentation/forms-data-sources.controller.ts +58 -0
  85. package/template/src/modules/forms/presentation/forms-definitions.controller.ts +191 -0
  86. package/template/src/modules/forms/presentation/forms-files.controller.ts +79 -0
  87. package/template/src/modules/forms/presentation/forms-submissions.controller.ts +154 -0
  88. package/template/src/modules/forms/presentation/forms-upload.interceptor.ts +33 -0
  89. package/template/src/modules/forms/presentation/public-forms.controller.ts +51 -0
  90. package/template/src/modules/invitations/dto/invitation-response.dto.ts +4 -0
  91. package/template/src/modules/organisations/application/services/organisations.service.ts +67 -1
  92. package/template/src/modules/organisations/dto/organisation-response.dto.ts +52 -0
  93. package/template/src/modules/organisations/presentation/organisations.controller.ts +25 -3
  94. package/template/src/modules/reports/application/services/reports-actions.service.ts +54 -0
  95. package/template/src/modules/reports/application/services/reports-definitions.service.ts +66 -0
  96. package/template/src/modules/reports/application/services/reports-error.mapper.ts +97 -0
  97. package/template/src/modules/reports/application/services/reports-export-dispatcher.service.ts +124 -0
  98. package/template/src/modules/reports/application/services/reports-exports.service.ts +74 -0
  99. package/template/src/modules/reports/application/services/reports-queries.service.ts +35 -0
  100. package/template/src/modules/reports/application/services/reports-settings-reader.service.ts +49 -0
  101. package/template/src/modules/reports/application/services/reports-views.service.ts +79 -0
  102. package/template/src/modules/reports/dto/action-result-response.dto.ts +21 -0
  103. package/template/src/modules/reports/dto/create-report-definition.dto.ts +86 -0
  104. package/template/src/modules/reports/dto/create-saved-view.dto.ts +26 -0
  105. package/template/src/modules/reports/dto/execute-action.dto.ts +71 -0
  106. package/template/src/modules/reports/dto/export-job-response.dto.ts +60 -0
  107. package/template/src/modules/reports/dto/export-request.dto.ts +34 -0
  108. package/template/src/modules/reports/dto/list-reports-query.dto.ts +10 -0
  109. package/template/src/modules/reports/dto/list-views-query.dto.ts +17 -0
  110. package/template/src/modules/reports/dto/prepare-action-response.dto.ts +14 -0
  111. package/template/src/modules/reports/dto/prepare-action.dto.ts +27 -0
  112. package/template/src/modules/reports/dto/query-response.dto.ts +64 -0
  113. package/template/src/modules/reports/dto/query-spec.dto.ts +120 -0
  114. package/template/src/modules/reports/dto/report-definition-response.dto.ts +64 -0
  115. package/template/src/modules/reports/dto/report-meta-query.dto.ts +16 -0
  116. package/template/src/modules/reports/dto/report-meta-response.dto.ts +113 -0
  117. package/template/src/modules/reports/dto/saved-view-response.dto.ts +66 -0
  118. package/template/src/modules/reports/dto/update-report-definition.dto.ts +9 -0
  119. package/template/src/modules/reports/dto/update-saved-view.dto.ts +27 -0
  120. package/template/src/modules/reports/examples/abstract-review-board.report.json +54 -0
  121. package/template/src/modules/reports/examples/org-members.report.json +55 -0
  122. package/template/src/modules/reports/infrastructure/audit-sink.adapter.ts +31 -0
  123. package/template/src/modules/reports/infrastructure/casl-reports-authorization.ts +39 -0
  124. package/template/src/modules/reports/infrastructure/forms-adapter/form-report-source.adapter.ts +292 -0
  125. package/template/src/modules/reports/infrastructure/forms-adapter/form-row-actions.ts +171 -0
  126. package/template/src/modules/reports/infrastructure/forms-adapter/forms-bridge-bootstrap.service.ts +32 -0
  127. package/template/src/modules/reports/infrastructure/prisma-catalog.adapter.ts +95 -0
  128. package/template/src/modules/reports/infrastructure/prisma-query-executor.ts +103 -0
  129. package/template/src/modules/reports/infrastructure/prisma-snapshot-runner.ts +47 -0
  130. package/template/src/modules/reports/infrastructure/prisma-tx-runner.ts +18 -0
  131. package/template/src/modules/reports/infrastructure/registry/registry-bootstrap.service.ts +61 -0
  132. package/template/src/modules/reports/infrastructure/registry/report-extension.decorators.ts +14 -0
  133. package/template/src/modules/reports/infrastructure/reports-job-queue.adapter.ts +28 -0
  134. package/template/src/modules/reports/infrastructure/request-reports-context.ts +42 -0
  135. package/template/src/modules/reports/infrastructure/schema-check/reports-schema-check.service.ts +116 -0
  136. package/template/src/modules/reports/infrastructure/storage/local-disk-export-storage.adapter.ts +79 -0
  137. package/template/src/modules/reports/infrastructure/stores/index.ts +5 -0
  138. package/template/src/modules/reports/infrastructure/stores/prisma-bulk-action-run.store.ts +89 -0
  139. package/template/src/modules/reports/infrastructure/stores/prisma-export-job.store.ts +93 -0
  140. package/template/src/modules/reports/infrastructure/stores/prisma-report-definition.store.ts +171 -0
  141. package/template/src/modules/reports/infrastructure/stores/prisma-row-tag.store.ts +110 -0
  142. package/template/src/modules/reports/infrastructure/stores/prisma-saved-view.store.ts +144 -0
  143. package/template/src/modules/reports/presentation/reports-actions.controller.ts +83 -0
  144. package/template/src/modules/reports/presentation/reports-definitions.controller.ts +156 -0
  145. package/template/src/modules/reports/presentation/reports-export-jobs.controller.ts +61 -0
  146. package/template/src/modules/reports/presentation/reports-export.controller.ts +76 -0
  147. package/template/src/modules/reports/presentation/reports-query.controller.ts +52 -0
  148. package/template/src/modules/reports/presentation/reports-views.controller.ts +140 -0
  149. package/template/src/modules/reports/reports-forms.module.ts +33 -0
  150. package/template/src/modules/reports/reports.module.ts +335 -0
  151. package/template/src/modules/reports/reports.tokens.ts +11 -0
  152. package/template/src/modules/reports/sources/org-members.source.ts +112 -0
  153. package/template/src/modules/settings/types/setting-definitions.ts +94 -0
  154. package/template/test/forms-definitions.e2e-spec.ts +394 -0
  155. package/template/test/forms-export.e2e-spec.ts +390 -0
  156. package/template/test/forms-files.e2e-spec.ts +345 -0
  157. package/template/test/forms-outbox.e2e-spec.ts +309 -0
  158. package/template/test/forms-permission-sync.spec.ts +27 -0
  159. package/template/test/forms-public.e2e-spec.ts +269 -0
  160. package/template/test/forms-schema-check.e2e-spec.ts +65 -0
  161. package/template/test/forms-submissions.e2e-spec.ts +500 -0
  162. package/template/test/forms-webhooks.e2e-spec.ts +261 -0
  163. package/template/test/frontend-bootstrap.spec.ts +181 -0
  164. package/template/test/reports-advanced.e2e-spec.ts +368 -0
  165. package/template/test/reports-permission-sync.spec.ts +30 -0
  166. package/template/test/reports-query.e2e-spec.ts +350 -0
  167. package/template/test/reports-tiers.e2e-spec.ts +257 -0
  168. package/template/test/route-registry.validator.spec.ts +34 -0
  169. package/template/test/security.e2e-spec.ts +134 -2
@@ -0,0 +1,41 @@
1
+ import { createHmac } from 'node:crypto';
2
+ import { Injectable } from '@nestjs/common';
3
+ import type { OutboxJobHandler, OutboxJobRecord } from '@ftisindia/form-builder';
4
+
5
+ /**
6
+ * Generic webhook delivery. Payload contract: { url: string, body?: object,
7
+ * secret?: string }. When a secret is set, the request carries
8
+ * `x-forms-signature: sha256=<hmac-sha256(rawBody)>` so receivers can verify
9
+ * authenticity. Non-2xx responses and timeouts throw, which the dispatcher
10
+ * turns into a retry with backoff (then parks as FAILED after max attempts).
11
+ */
12
+ @Injectable()
13
+ export class WebhookOutboxHandler implements OutboxJobHandler {
14
+ readonly type = 'webhook';
15
+
16
+ async handle(job: OutboxJobRecord): Promise<void> {
17
+ const { url, body, secret } = job.payload as {
18
+ url?: unknown;
19
+ body?: unknown;
20
+ secret?: unknown;
21
+ };
22
+ if (typeof url !== 'string' || url.length === 0) {
23
+ throw new Error('Webhook job payload requires a "url" string.');
24
+ }
25
+ const rawBody = JSON.stringify(body ?? {});
26
+ const headers: Record<string, string> = { 'content-type': 'application/json' };
27
+ if (typeof secret === 'string' && secret.length > 0) {
28
+ headers['x-forms-signature'] =
29
+ 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
30
+ }
31
+ const response = await fetch(url, {
32
+ method: 'POST',
33
+ headers,
34
+ body: rawBody,
35
+ signal: AbortSignal.timeout(10_000),
36
+ });
37
+ if (!response.ok) {
38
+ throw new Error(`Webhook endpoint responded with status ${response.status}.`);
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,109 @@
1
+ import { Injectable, Logger, OnApplicationBootstrap, OnModuleDestroy } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { OutboxHandlerRegistry, runDispatchCycle } from '@ftisindia/form-builder';
4
+ import type { DispatchCycleResult } from '@ftisindia/form-builder';
5
+ import { PrismaService } from '../../../../database/prisma/prisma.service';
6
+ import { AuditService } from '../../../audit/application/services/audit.service';
7
+ import { RequestContextService } from '../../../request-context/application/services/request-context.service';
8
+ import { PrismaOutboxStore } from '../../infrastructure/stores/prisma-outbox.store';
9
+
10
+ /**
11
+ * In-process outbox poller (ecosystem guide §3/§8.3). Every claimed job runs
12
+ * inside requestContext.run({ source: 'worker', jobId, orgId, userId,
13
+ * requestId: originRequestId }) so audit and logging keep working in
14
+ * background work and every external side effect stays traceable to its
15
+ * originating request. Successful delivery writes an audit row.
16
+ *
17
+ * Deterministic in tests: FORMS_OUTBOX_ENABLED=false disables the timer and
18
+ * runOnce() drives a single cycle by hand. Single-instance by design — a
19
+ * multi-node deployment should swap the store's claim for SKIP LOCKED or a
20
+ * real queue (BullMQ) without touching the engine.
21
+ */
22
+ @Injectable()
23
+ export class OutboxDispatcherService implements OnApplicationBootstrap, OnModuleDestroy {
24
+ private readonly logger = new Logger('FormsOutbox');
25
+ private timer?: NodeJS.Timeout;
26
+ private ticking = false;
27
+
28
+ constructor(
29
+ private readonly config: ConfigService,
30
+ private readonly store: PrismaOutboxStore,
31
+ private readonly handlers: OutboxHandlerRegistry,
32
+ private readonly requestContext: RequestContextService,
33
+ private readonly audit: AuditService,
34
+ private readonly prisma: PrismaService,
35
+ ) {}
36
+
37
+ onApplicationBootstrap(): void {
38
+ if (this.config.get<boolean>('forms.outboxEnabled') === false) {
39
+ return;
40
+ }
41
+ const pollMs = this.config.get<number>('forms.outboxPollMs') ?? 5000;
42
+ this.timer = setInterval(() => {
43
+ void this.tick();
44
+ }, pollMs);
45
+ this.timer.unref?.();
46
+ }
47
+
48
+ onModuleDestroy(): void {
49
+ if (this.timer) {
50
+ clearInterval(this.timer);
51
+ this.timer = undefined;
52
+ }
53
+ }
54
+
55
+ async runOnce(): Promise<DispatchCycleResult> {
56
+ return runDispatchCycle(this.store, this.handlers, {
57
+ wrapJob: (job, run) =>
58
+ this.requestContext.run(
59
+ {
60
+ source: 'worker',
61
+ jobId: job.id,
62
+ orgId: job.orgId ?? undefined,
63
+ userId: job.actorUserId ?? undefined,
64
+ requestId: job.originRequestId ?? undefined,
65
+ },
66
+ async () => {
67
+ const outcome = await run();
68
+ if (outcome === 'done') {
69
+ try {
70
+ await this.audit.write(this.prisma, {
71
+ orgId: job.orgId ?? null,
72
+ actorUserId: job.actorUserId ?? null,
73
+ action: 'forms.outbox.delivered',
74
+ targetType: 'FormOutboxJob',
75
+ targetId: job.id,
76
+ metadata: { type: job.type, attempts: job.attempts + 1 },
77
+ });
78
+ } catch (error) {
79
+ this.logger.warn(
80
+ `Outbox delivery audit failed for job ${job.id}.`,
81
+ error instanceof Error ? error.stack : String(error),
82
+ );
83
+ }
84
+ }
85
+ return outcome;
86
+ },
87
+ ),
88
+ });
89
+ }
90
+
91
+ private async tick(): Promise<void> {
92
+ if (this.ticking) {
93
+ return;
94
+ }
95
+ this.ticking = true;
96
+ try {
97
+ const result = await this.runOnce();
98
+ if (result.claimed > 0) {
99
+ this.logger.log(
100
+ `Outbox cycle: claimed ${result.claimed}, done ${result.done}, retried ${result.retried}, failed ${result.failed}.`,
101
+ );
102
+ }
103
+ } catch (error) {
104
+ this.logger.error('Outbox cycle failed.', error instanceof Error ? error.stack : error);
105
+ } finally {
106
+ this.ticking = false;
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,12 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsObject } from 'class-validator';
3
+
4
+ export class CreateFormDefinitionDto {
5
+ @ApiProperty({
6
+ type: 'object',
7
+ additionalProperties: true,
8
+ description: 'The FormDefinition document (validated against the engine meta-schema).',
9
+ })
10
+ @IsObject()
11
+ definition!: Record<string, unknown>;
12
+ }
@@ -0,0 +1,19 @@
1
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2
+
3
+ export class DataSourceKeysResponseDto {
4
+ @ApiProperty({ type: [String], example: ['countries', 'departments'] })
5
+ items!: string[];
6
+ }
7
+
8
+ export class DataSourceOptionDto {
9
+ @ApiProperty({ oneOf: [{ type: 'string' }, { type: 'number' }], example: 'IN' })
10
+ value!: string | number;
11
+
12
+ @ApiPropertyOptional({ example: 'India' })
13
+ label?: string;
14
+ }
15
+
16
+ export class DataSourceOptionsResponseDto {
17
+ @ApiProperty({ type: [DataSourceOptionDto] })
18
+ items!: DataSourceOptionDto[];
19
+ }
@@ -0,0 +1,33 @@
1
+ import { ApiPropertyOptional } from '@nestjs/swagger';
2
+ import { IsIn, IsISO8601, IsOptional, IsString, MaxLength } from 'class-validator';
3
+
4
+ export class ExportSubmissionsQueryDto {
5
+ @ApiPropertyOptional({ enum: ['csv', 'json'], default: 'json' })
6
+ @IsOptional()
7
+ @IsIn(['csv', 'json'])
8
+ format?: 'csv' | 'json';
9
+
10
+ @ApiPropertyOptional({ enum: ['DRAFT', 'SUBMITTED'] })
11
+ @IsOptional()
12
+ @IsIn(['DRAFT', 'SUBMITTED'])
13
+ status?: 'DRAFT' | 'SUBMITTED';
14
+
15
+ @ApiPropertyOptional({ format: 'date-time', description: 'Only submissions created at/after.' })
16
+ @IsOptional()
17
+ @IsISO8601()
18
+ from?: string;
19
+
20
+ @ApiPropertyOptional({ format: 'date-time', description: 'Only submissions created at/before.' })
21
+ @IsOptional()
22
+ @IsISO8601()
23
+ to?: string;
24
+
25
+ @ApiPropertyOptional({
26
+ description:
27
+ 'Comma-separated field paths to include. Defaults to the definitions’ reportable fields. Sensitive fields are always refused.',
28
+ })
29
+ @IsOptional()
30
+ @IsString()
31
+ @MaxLength(2000)
32
+ fields?: string;
33
+ }
@@ -0,0 +1,24 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+
3
+ export class FileUploadResponseDto {
4
+ @ApiProperty({ example: '0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3', format: 'uuid' })
5
+ fileId!: string;
6
+
7
+ @ApiProperty({ example: 'manuscript.pdf' })
8
+ originalName!: string;
9
+
10
+ @ApiProperty({ example: 'application/pdf', description: 'Server-sniffed MIME type.' })
11
+ mimeType!: string;
12
+
13
+ @ApiProperty({ example: 348160 })
14
+ size!: number;
15
+
16
+ @ApiProperty({ example: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' })
17
+ checksum!: string;
18
+
19
+ @ApiProperty({ enum: ['TEMPORARY', 'SCANNING', 'CLEAN', 'INFECTED', 'LINKED'] })
20
+ status!: string;
21
+
22
+ @ApiProperty({ example: '2026-06-12T10:30:00.000Z', format: 'date-time' })
23
+ createdAt!: Date;
24
+ }
@@ -0,0 +1,50 @@
1
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2
+
3
+ export class FormDefinitionResponseDto {
4
+ @ApiProperty({
5
+ example: '0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3',
6
+ format: 'uuid',
7
+ })
8
+ id!: string;
9
+
10
+ @ApiProperty({
11
+ example: '2c67399d-670c-4025-a5fd-1ea9a211891e',
12
+ format: 'uuid',
13
+ })
14
+ orgId!: string;
15
+
16
+ @ApiProperty({ example: 'abstract-submission' })
17
+ key!: string;
18
+
19
+ @ApiProperty({ example: 1 })
20
+ version!: number;
21
+
22
+ @ApiProperty({ enum: ['DRAFT', 'PUBLISHED', 'ARCHIVED'], example: 'DRAFT' })
23
+ status!: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
24
+
25
+ @ApiProperty({
26
+ type: 'object',
27
+ additionalProperties: true,
28
+ description: 'The full FormDefinition document.',
29
+ })
30
+ schema!: Record<string, unknown>;
31
+
32
+ @ApiProperty({ example: '2026-06-01T10:30:00.000Z', format: 'date-time' })
33
+ createdAt!: string;
34
+
35
+ @ApiProperty({ example: '2026-06-01T10:30:00.000Z', format: 'date-time' })
36
+ updatedAt!: string;
37
+ }
38
+
39
+ export class FormDefinitionListResponseDto {
40
+ @ApiProperty({ type: [FormDefinitionResponseDto] })
41
+ items!: FormDefinitionResponseDto[];
42
+
43
+ @ApiPropertyOptional({
44
+ type: String,
45
+ example: '0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3',
46
+ format: 'uuid',
47
+ nullable: true,
48
+ })
49
+ nextCursor!: string | null;
50
+ }
@@ -0,0 +1,17 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+
3
+ export class FormRenderResponseDto {
4
+ @ApiProperty({
5
+ type: 'object',
6
+ additionalProperties: true,
7
+ description: 'The published FormDefinition document.',
8
+ })
9
+ definition!: Record<string, unknown>;
10
+
11
+ @ApiProperty({
12
+ type: 'object',
13
+ additionalProperties: true,
14
+ description: 'Resolved data-source options keyed by dotted field path.',
15
+ })
16
+ options!: Record<string, unknown>;
17
+ }
@@ -0,0 +1,10 @@
1
+ import { ApiPropertyOptional } from '@nestjs/swagger';
2
+ import { IsIn, IsOptional } from 'class-validator';
3
+ import { PaginationQueryDto } from '../../../common/dto/pagination-query.dto';
4
+
5
+ export class ListFormDefinitionsQueryDto extends PaginationQueryDto {
6
+ @ApiPropertyOptional({ enum: ['DRAFT', 'PUBLISHED', 'ARCHIVED'] })
7
+ @IsOptional()
8
+ @IsIn(['DRAFT', 'PUBLISHED', 'ARCHIVED'])
9
+ status?: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
10
+ }
@@ -0,0 +1,10 @@
1
+ import { ApiPropertyOptional } from '@nestjs/swagger';
2
+ import { IsIn, IsOptional } from 'class-validator';
3
+ import { PaginationQueryDto } from '../../../common/dto/pagination-query.dto';
4
+
5
+ export class ListSubmissionsQueryDto extends PaginationQueryDto {
6
+ @ApiPropertyOptional({ enum: ['DRAFT', 'SUBMITTED'] })
7
+ @IsOptional()
8
+ @IsIn(['DRAFT', 'SUBMITTED'])
9
+ status?: 'DRAFT' | 'SUBMITTED';
10
+ }
@@ -0,0 +1,24 @@
1
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2
+ import { IsInt, IsObject, IsOptional, IsString, MaxLength, Min } from 'class-validator';
3
+
4
+ export class PublicSubmitFormDto {
5
+ @ApiProperty({
6
+ type: 'object',
7
+ additionalProperties: true,
8
+ description: 'The submission payload, validated against the form definition.',
9
+ })
10
+ @IsObject()
11
+ data!: Record<string, unknown>;
12
+
13
+ @ApiPropertyOptional({ description: 'Specific definition version; defaults to latest published.' })
14
+ @IsOptional()
15
+ @IsInt()
16
+ @Min(1)
17
+ version?: number;
18
+
19
+ @ApiPropertyOptional({ description: 'Captcha token, required when the form enables captcha.' })
20
+ @IsOptional()
21
+ @IsString()
22
+ @MaxLength(4096)
23
+ captchaToken?: string;
24
+ }
@@ -0,0 +1,8 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsIn } from 'class-validator';
3
+
4
+ export class SetPublicAccessDto {
5
+ @ApiProperty({ enum: ['public', 'private'], example: 'private' })
6
+ @IsIn(['public', 'private'])
7
+ access!: 'public' | 'private';
8
+ }
@@ -0,0 +1,99 @@
1
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2
+
3
+ export class SubmissionResponseDto {
4
+ @ApiProperty({
5
+ example: '0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3',
6
+ format: 'uuid',
7
+ })
8
+ id!: string;
9
+
10
+ @ApiProperty({
11
+ example: '2c67399d-670c-4025-a5fd-1ea9a211891e',
12
+ format: 'uuid',
13
+ })
14
+ orgId!: string;
15
+
16
+ @ApiProperty({ example: 'abstract-submission' })
17
+ formKey!: string;
18
+
19
+ @ApiProperty({ example: 1, description: 'The definition version the data was captured under.' })
20
+ formVersion!: number;
21
+
22
+ @ApiProperty({ type: 'object', additionalProperties: true })
23
+ data!: Record<string, unknown>;
24
+
25
+ @ApiProperty({ enum: ['DRAFT', 'SUBMITTED'], example: 'SUBMITTED' })
26
+ status!: 'DRAFT' | 'SUBMITTED';
27
+
28
+ @ApiProperty({ example: false })
29
+ locked!: boolean;
30
+
31
+ @ApiPropertyOptional({
32
+ type: String,
33
+ example: '4a4f0d8a-4bd2-469f-a6a9-3e1cb6a2b456',
34
+ format: 'uuid',
35
+ nullable: true,
36
+ })
37
+ createdBy?: string | null;
38
+
39
+ @ApiProperty({ example: '2026-06-01T10:30:00.000Z', format: 'date-time' })
40
+ createdAt!: string;
41
+
42
+ @ApiProperty({ example: '2026-06-01T10:30:00.000Z', format: 'date-time' })
43
+ updatedAt!: string;
44
+ }
45
+
46
+ export class SubmissionListResponseDto {
47
+ @ApiProperty({ type: [SubmissionResponseDto] })
48
+ items!: SubmissionResponseDto[];
49
+
50
+ @ApiPropertyOptional({
51
+ type: String,
52
+ example: '0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3',
53
+ format: 'uuid',
54
+ nullable: true,
55
+ })
56
+ nextCursor!: string | null;
57
+ }
58
+
59
+ export class SubmitResultResponseDto {
60
+ @ApiProperty({
61
+ type: String,
62
+ example: '0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3',
63
+ format: 'uuid',
64
+ nullable: true,
65
+ })
66
+ submissionId!: string | null;
67
+
68
+ @ApiProperty({
69
+ type: 'object',
70
+ additionalProperties: true,
71
+ description: 'Outputs of every executed action, keyed by action name.',
72
+ })
73
+ outputs!: Record<string, unknown>;
74
+ }
75
+
76
+ export class ValidationErrorItemDto {
77
+ @ApiProperty({
78
+ example: 'authors.0.email',
79
+ description: 'Dotted data path. Empty string means the whole document.',
80
+ })
81
+ path!: string;
82
+
83
+ @ApiProperty({ example: 'REQUIRED' })
84
+ code!: string;
85
+
86
+ @ApiProperty({ example: 'This field is required.' })
87
+ message!: string;
88
+
89
+ @ApiPropertyOptional({ example: 'require-corresponding-author' })
90
+ ruleId?: string;
91
+ }
92
+
93
+ export class ValidationResultResponseDto {
94
+ @ApiProperty({ example: false })
95
+ valid!: boolean;
96
+
97
+ @ApiProperty({ type: [ValidationErrorItemDto] })
98
+ errors!: ValidationErrorItemDto[];
99
+ }
@@ -0,0 +1,50 @@
1
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2
+ import {
3
+ IsIn,
4
+ IsInt,
5
+ IsObject,
6
+ IsOptional,
7
+ IsString,
8
+ IsUUID,
9
+ MaxLength,
10
+ Min,
11
+ } from 'class-validator';
12
+
13
+ export class SubmitFormDto {
14
+ @ApiProperty({ enum: ['draft', 'submit'], example: 'submit' })
15
+ @IsIn(['draft', 'submit'])
16
+ mode!: 'draft' | 'submit';
17
+
18
+ @ApiPropertyOptional({
19
+ example: 'submit',
20
+ maxLength: 64,
21
+ description: 'Button name in the definition actions map. Defaults to the mode.',
22
+ })
23
+ @IsOptional()
24
+ @IsString()
25
+ @MaxLength(64)
26
+ button?: string;
27
+
28
+ @ApiPropertyOptional({
29
+ example: 3,
30
+ minimum: 1,
31
+ description: 'Specific definition version; defaults to the latest published one.',
32
+ })
33
+ @IsOptional()
34
+ @IsInt()
35
+ @Min(1)
36
+ version?: number;
37
+
38
+ @ApiProperty({ type: 'object', additionalProperties: true })
39
+ @IsObject()
40
+ data!: Record<string, unknown>;
41
+
42
+ @ApiPropertyOptional({
43
+ example: '0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3',
44
+ format: 'uuid',
45
+ description: 'Present when editing an existing draft submission.',
46
+ })
47
+ @IsOptional()
48
+ @IsUUID()
49
+ submissionId?: string;
50
+ }
@@ -0,0 +1,12 @@
1
+ import { ApiProperty } from '@nestjs/swagger';
2
+ import { IsObject } from 'class-validator';
3
+
4
+ export class UpdateFormDefinitionDto {
5
+ @ApiProperty({
6
+ type: 'object',
7
+ additionalProperties: true,
8
+ description: 'The FormDefinition document (validated against the engine meta-schema).',
9
+ })
10
+ @IsObject()
11
+ definition!: Record<string, unknown>;
12
+ }
@@ -0,0 +1,33 @@
1
+ import { Type } from 'class-transformer';
2
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
3
+ import { IsInt, IsOptional, IsString, IsUUID, MaxLength, Min } from 'class-validator';
4
+
5
+ export class UploadFileQueryDto {
6
+ @ApiProperty({
7
+ description: 'File field name in the target form definition.',
8
+ example: 'manuscript',
9
+ })
10
+ @IsString()
11
+ @MaxLength(128)
12
+ field!: string;
13
+
14
+ @ApiPropertyOptional({
15
+ example: 3,
16
+ minimum: 1,
17
+ description: 'Specific definition version; defaults to the latest published one.',
18
+ })
19
+ @IsOptional()
20
+ @Type(() => Number)
21
+ @IsInt()
22
+ @Min(1)
23
+ version?: number;
24
+
25
+ @ApiPropertyOptional({
26
+ example: '0a57fb4a-95c6-4f7e-bd5a-f96dbe0599e3',
27
+ format: 'uuid',
28
+ description: 'Existing draft submission whose form version should be used.',
29
+ })
30
+ @IsOptional()
31
+ @IsUUID()
32
+ submissionId?: string;
33
+ }
@@ -0,0 +1,22 @@
1
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2
+ import { IsIn, IsInt, IsObject, IsOptional, Min } from 'class-validator';
3
+
4
+ export class ValidateSubmissionDto {
5
+ @ApiProperty({ enum: ['draft', 'submit'], example: 'submit' })
6
+ @IsIn(['draft', 'submit'])
7
+ mode!: 'draft' | 'submit';
8
+
9
+ @ApiPropertyOptional({
10
+ example: 3,
11
+ minimum: 1,
12
+ description: 'Specific definition version; defaults to the latest published one.',
13
+ })
14
+ @IsOptional()
15
+ @IsInt()
16
+ @Min(1)
17
+ version?: number;
18
+
19
+ @ApiProperty({ type: 'object', additionalProperties: true })
20
+ @IsObject()
21
+ data!: Record<string, unknown>;
22
+ }
@@ -0,0 +1,80 @@
1
+ {
2
+ "key": "abstract-submission",
3
+ "version": 1,
4
+ "title": "Scientific abstract submission",
5
+ "fields": [
6
+ {
7
+ "type": "text",
8
+ "name": "title",
9
+ "label": "Abstract title",
10
+ "validators": { "required": true, "maxLength": 200 },
11
+ "reportable": true
12
+ },
13
+ {
14
+ "type": "group",
15
+ "name": "authors",
16
+ "label": "Authors",
17
+ "repeatable": true,
18
+ "min": 1,
19
+ "max": 10,
20
+ "fields": [
21
+ { "type": "text", "name": "fullName", "validators": { "required": true } },
22
+ { "type": "email", "name": "email", "validators": { "required": true } },
23
+ { "type": "boolean", "name": "isPresenting" }
24
+ ]
25
+ },
26
+ {
27
+ "type": "select",
28
+ "name": "track",
29
+ "label": "Conference track",
30
+ "dataSource": { "key": "conference-tracks" },
31
+ "validators": { "required": true },
32
+ "reportable": true,
33
+ "indexHint": true
34
+ },
35
+ {
36
+ "type": "text",
37
+ "name": "body",
38
+ "label": "Abstract body",
39
+ "validators": { "required": true, "maxLength": 3000 },
40
+ "ui": { "widget": "richtext" }
41
+ },
42
+ {
43
+ "type": "file",
44
+ "name": "manuscript",
45
+ "label": "Full manuscript (PDF)",
46
+ "accept": ["application/pdf"],
47
+ "maxSizeMb": 20
48
+ },
49
+ {
50
+ "type": "boolean",
51
+ "name": "needsFunding",
52
+ "label": "Requesting funding?"
53
+ },
54
+ {
55
+ "type": "text",
56
+ "name": "fundingSource",
57
+ "label": "Funding source",
58
+ "ui": { "showWhen": "needsFunding" }
59
+ },
60
+ {
61
+ "type": "hidden",
62
+ "name": "submittedBy",
63
+ "source": "context",
64
+ "path": "user.id"
65
+ }
66
+ ],
67
+ "rules": [
68
+ {
69
+ "id": "funding-required",
70
+ "enforceOn": ["submit"],
71
+ "if": { "==": [{ "var": "data.needsFunding" }, true] },
72
+ "then": { "require": ["fundingSource"] },
73
+ "message": "Funding source is required when requesting funding."
74
+ }
75
+ ],
76
+ "actions": {
77
+ "submit": ["validateAll", "persist", "lockEditing", "sendConfirmationEmail"],
78
+ "saveDraft": ["persistDraft"]
79
+ }
80
+ }