@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,156 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { Injectable } from '@nestjs/common';
|
|
3
|
+
import { FormOutboxJob as FormOutboxJobRow, Prisma } from '@prisma/client';
|
|
4
|
+
import type {
|
|
5
|
+
EngineTx,
|
|
6
|
+
OutboxJobInput,
|
|
7
|
+
OutboxJobRecord,
|
|
8
|
+
OutboxStatus,
|
|
9
|
+
OutboxStore,
|
|
10
|
+
} from '@ftisindia/form-builder';
|
|
11
|
+
import { PrismaService } from '../../../../database/prisma/prisma.service';
|
|
12
|
+
|
|
13
|
+
const PROCESSING_LEASE_MS = 5 * 60 * 1000;
|
|
14
|
+
|
|
15
|
+
@Injectable()
|
|
16
|
+
export class PrismaOutboxStore implements OutboxStore {
|
|
17
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
18
|
+
|
|
19
|
+
private client(tx?: EngineTx) {
|
|
20
|
+
return tx ? (tx as unknown as Prisma.TransactionClient) : this.prisma;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async enqueue(job: OutboxJobInput, tx: EngineTx): Promise<void> {
|
|
24
|
+
await this.client(tx).formOutboxJob.createMany({
|
|
25
|
+
data: [
|
|
26
|
+
{
|
|
27
|
+
type: job.type,
|
|
28
|
+
payload: job.payload as unknown as Prisma.InputJsonValue,
|
|
29
|
+
status: 'PENDING',
|
|
30
|
+
attempts: 0,
|
|
31
|
+
maxAttempts: job.maxAttempts ?? 8,
|
|
32
|
+
idempotencyKey: job.idempotencyKey ?? null,
|
|
33
|
+
runAfter: job.runAfter ?? null,
|
|
34
|
+
orgId: job.orgId ?? null,
|
|
35
|
+
actorUserId: job.actorUserId ?? null,
|
|
36
|
+
originRequestId: job.originRequestId ?? null,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
skipDuplicates: true,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Claims due PENDING jobs and stale PROCESSING jobs with row-level locks so
|
|
45
|
+
* multiple app instances can poll without double-delivering the same job.
|
|
46
|
+
*/
|
|
47
|
+
async claimDue(now: Date, batchSize: number): Promise<OutboxJobRecord[]> {
|
|
48
|
+
if (batchSize <= 0) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const staleProcessingBefore = new Date(now.getTime() - PROCESSING_LEASE_MS);
|
|
53
|
+
const claimedBy = randomUUID();
|
|
54
|
+
const claimed = await this.prisma.$transaction(
|
|
55
|
+
(tx) =>
|
|
56
|
+
tx.$queryRaw<FormOutboxJobRow[]>`
|
|
57
|
+
WITH due AS (
|
|
58
|
+
SELECT "id"
|
|
59
|
+
FROM "FormOutboxJob"
|
|
60
|
+
WHERE (
|
|
61
|
+
"status" = 'PENDING'
|
|
62
|
+
AND ("runAfter" IS NULL OR "runAfter" <= (${now}::timestamptz AT TIME ZONE 'UTC'))
|
|
63
|
+
)
|
|
64
|
+
OR (
|
|
65
|
+
"status" = 'PROCESSING'
|
|
66
|
+
AND "updatedAt" <= (${staleProcessingBefore}::timestamptz AT TIME ZONE 'UTC')
|
|
67
|
+
)
|
|
68
|
+
ORDER BY "createdAt" ASC
|
|
69
|
+
LIMIT ${batchSize}
|
|
70
|
+
FOR UPDATE SKIP LOCKED
|
|
71
|
+
)
|
|
72
|
+
UPDATE "FormOutboxJob" AS job
|
|
73
|
+
SET "status" = 'PROCESSING',
|
|
74
|
+
"claimedBy" = ${claimedBy},
|
|
75
|
+
"updatedAt" = (CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
|
|
76
|
+
FROM due
|
|
77
|
+
WHERE job."id" = due."id"
|
|
78
|
+
RETURNING job.*
|
|
79
|
+
`,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return claimed.map((row) => this.toRecord(row));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async markDone(id: string, claimedBy: string): Promise<boolean> {
|
|
86
|
+
const result = await this.client().formOutboxJob.updateMany({
|
|
87
|
+
where: { id, status: 'PROCESSING', claimedBy },
|
|
88
|
+
data: { status: 'DONE', claimedBy: null },
|
|
89
|
+
});
|
|
90
|
+
return result.count === 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async markRetry(
|
|
94
|
+
id: string,
|
|
95
|
+
claimedBy: string,
|
|
96
|
+
attempts: number,
|
|
97
|
+
runAfter: Date,
|
|
98
|
+
lastError: string,
|
|
99
|
+
): Promise<boolean> {
|
|
100
|
+
const result = await this.client().formOutboxJob.updateMany({
|
|
101
|
+
where: { id, status: 'PROCESSING', claimedBy },
|
|
102
|
+
data: {
|
|
103
|
+
status: 'PENDING',
|
|
104
|
+
claimedBy: null,
|
|
105
|
+
attempts,
|
|
106
|
+
runAfter,
|
|
107
|
+
lastError,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
return result.count === 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async markFailed(id: string, claimedBy: string, attempts: number, lastError: string): Promise<boolean> {
|
|
114
|
+
const result = await this.client().formOutboxJob.updateMany({
|
|
115
|
+
where: { id, status: 'PROCESSING', claimedBy },
|
|
116
|
+
data: {
|
|
117
|
+
status: 'FAILED',
|
|
118
|
+
claimedBy: null,
|
|
119
|
+
attempts,
|
|
120
|
+
lastError,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
return result.count === 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async touchProcessing(id: string, claimedBy: string): Promise<boolean> {
|
|
127
|
+
const touched = await this.prisma.$executeRaw`
|
|
128
|
+
UPDATE "FormOutboxJob"
|
|
129
|
+
SET "updatedAt" = (CURRENT_TIMESTAMP AT TIME ZONE 'UTC')
|
|
130
|
+
WHERE "id" = ${id}
|
|
131
|
+
AND "status" = 'PROCESSING'
|
|
132
|
+
AND "claimedBy" = ${claimedBy}
|
|
133
|
+
`;
|
|
134
|
+
return touched === 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private toRecord(row: FormOutboxJobRow): OutboxJobRecord {
|
|
138
|
+
return {
|
|
139
|
+
id: row.id,
|
|
140
|
+
type: row.type,
|
|
141
|
+
payload: row.payload as unknown as Record<string, unknown>,
|
|
142
|
+
status: row.status as OutboxStatus,
|
|
143
|
+
claimedBy: row.claimedBy,
|
|
144
|
+
attempts: row.attempts,
|
|
145
|
+
maxAttempts: row.maxAttempts,
|
|
146
|
+
idempotencyKey: row.idempotencyKey,
|
|
147
|
+
lastError: row.lastError,
|
|
148
|
+
runAfter: row.runAfter,
|
|
149
|
+
orgId: row.orgId,
|
|
150
|
+
actorUserId: row.actorUserId,
|
|
151
|
+
originRequestId: row.originRequestId,
|
|
152
|
+
createdAt: row.createdAt,
|
|
153
|
+
updatedAt: row.updatedAt,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { FormSubmission as FormSubmissionRow, Prisma } from '@prisma/client';
|
|
3
|
+
import type {
|
|
4
|
+
EngineTx,
|
|
5
|
+
NewSubmissionRecord,
|
|
6
|
+
SubmissionListFilters,
|
|
7
|
+
SubmissionPatch,
|
|
8
|
+
SubmissionRecord,
|
|
9
|
+
SubmissionStatus,
|
|
10
|
+
SubmissionStore,
|
|
11
|
+
} from '@ftisindia/form-builder';
|
|
12
|
+
import { PrismaService } from '../../../../database/prisma/prisma.service';
|
|
13
|
+
|
|
14
|
+
@Injectable()
|
|
15
|
+
export class PrismaSubmissionStore implements SubmissionStore {
|
|
16
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
17
|
+
|
|
18
|
+
private client(tx?: EngineTx) {
|
|
19
|
+
return tx ? (tx as unknown as Prisma.TransactionClient) : this.prisma;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async create(record: NewSubmissionRecord, tx?: EngineTx): Promise<SubmissionRecord> {
|
|
23
|
+
const row = await this.client(tx).formSubmission.create({
|
|
24
|
+
data: {
|
|
25
|
+
orgId: record.orgId,
|
|
26
|
+
formKey: record.formKey,
|
|
27
|
+
formVersion: record.formVersion,
|
|
28
|
+
data: record.data as unknown as Prisma.InputJsonValue,
|
|
29
|
+
status: record.status,
|
|
30
|
+
createdBy: record.createdBy ?? null,
|
|
31
|
+
ipAddress: record.ipAddress ?? null,
|
|
32
|
+
userAgent: record.userAgent ?? null,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return this.toRecord(row);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async update(
|
|
40
|
+
orgId: string,
|
|
41
|
+
id: string,
|
|
42
|
+
patch: SubmissionPatch,
|
|
43
|
+
tx?: EngineTx,
|
|
44
|
+
): Promise<SubmissionRecord> {
|
|
45
|
+
const client = this.client(tx);
|
|
46
|
+
|
|
47
|
+
const existing = await client.formSubmission.findFirst({
|
|
48
|
+
where: { id, orgId },
|
|
49
|
+
select: { id: true },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!existing) {
|
|
53
|
+
throw new Error(`Form submission ${id} was not found in this organisation.`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const row = await client.formSubmission.update({
|
|
57
|
+
where: { id: existing.id },
|
|
58
|
+
data: {
|
|
59
|
+
...(patch.data !== undefined
|
|
60
|
+
? { data: patch.data as unknown as Prisma.InputJsonValue }
|
|
61
|
+
: {}),
|
|
62
|
+
...(patch.status !== undefined ? { status: patch.status } : {}),
|
|
63
|
+
...(patch.locked !== undefined ? { locked: patch.locked } : {}),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return this.toRecord(row);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async findById(orgId: string, id: string, tx?: EngineTx): Promise<SubmissionRecord | null> {
|
|
71
|
+
const row = await this.client(tx).formSubmission.findFirst({
|
|
72
|
+
where: { id, orgId },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return row ? this.toRecord(row) : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async list(
|
|
79
|
+
orgId: string,
|
|
80
|
+
formKey: string,
|
|
81
|
+
filters: SubmissionListFilters,
|
|
82
|
+
): Promise<SubmissionRecord[]> {
|
|
83
|
+
const rows = await this.client().formSubmission.findMany({
|
|
84
|
+
where: this.filterWhere(orgId, formKey, filters),
|
|
85
|
+
orderBy: { createdAt: 'desc' },
|
|
86
|
+
take: filters.limit,
|
|
87
|
+
...(filters.cursor
|
|
88
|
+
? {
|
|
89
|
+
cursor: { id: filters.cursor },
|
|
90
|
+
skip: 1,
|
|
91
|
+
}
|
|
92
|
+
: {}),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return rows.map((row) => this.toRecord(row));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async countByIpSince(
|
|
99
|
+
orgId: string,
|
|
100
|
+
formKey: string,
|
|
101
|
+
ipAddress: string,
|
|
102
|
+
since: Date,
|
|
103
|
+
): Promise<number> {
|
|
104
|
+
return this.client().formSubmission.count({
|
|
105
|
+
where: {
|
|
106
|
+
orgId,
|
|
107
|
+
formKey,
|
|
108
|
+
ipAddress,
|
|
109
|
+
createdAt: { gte: since },
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async listForExport(
|
|
115
|
+
orgId: string,
|
|
116
|
+
formKey: string,
|
|
117
|
+
filters: Omit<SubmissionListFilters, 'cursor' | 'limit'>,
|
|
118
|
+
): Promise<SubmissionRecord[]> {
|
|
119
|
+
const rows = await this.client().formSubmission.findMany({
|
|
120
|
+
where: this.filterWhere(orgId, formKey, filters),
|
|
121
|
+
orderBy: { createdAt: 'asc' },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return rows.map((row) => this.toRecord(row));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private filterWhere(
|
|
128
|
+
orgId: string,
|
|
129
|
+
formKey: string,
|
|
130
|
+
filters: Omit<SubmissionListFilters, 'cursor' | 'limit'>,
|
|
131
|
+
): Prisma.FormSubmissionWhereInput {
|
|
132
|
+
return {
|
|
133
|
+
orgId,
|
|
134
|
+
formKey,
|
|
135
|
+
...(filters.status ? { status: filters.status } : {}),
|
|
136
|
+
...(filters.createdBy ? { createdBy: filters.createdBy } : {}),
|
|
137
|
+
...(filters.createdFrom || filters.createdTo
|
|
138
|
+
? {
|
|
139
|
+
createdAt: {
|
|
140
|
+
...(filters.createdFrom ? { gte: filters.createdFrom } : {}),
|
|
141
|
+
...(filters.createdTo ? { lte: filters.createdTo } : {}),
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
: {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private toRecord(row: FormSubmissionRow): SubmissionRecord {
|
|
149
|
+
return {
|
|
150
|
+
id: row.id,
|
|
151
|
+
orgId: row.orgId,
|
|
152
|
+
formKey: row.formKey,
|
|
153
|
+
formVersion: row.formVersion,
|
|
154
|
+
data: row.data as unknown as Record<string, unknown>,
|
|
155
|
+
status: row.status as SubmissionStatus,
|
|
156
|
+
locked: row.locked,
|
|
157
|
+
createdBy: row.createdBy,
|
|
158
|
+
ipAddress: row.ipAddress,
|
|
159
|
+
userAgent: row.userAgent,
|
|
160
|
+
createdAt: row.createdAt,
|
|
161
|
+
updatedAt: row.updatedAt,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
|
2
|
+
import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
|
|
3
|
+
import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
|
|
4
|
+
import { PermissionGuard } from '../../access-control/application/services/permission.guard';
|
|
5
|
+
import { RequirePermissions } from '../../access-control/presentation/permissions.decorator';
|
|
6
|
+
import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
|
|
7
|
+
import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
|
|
8
|
+
import { FormsDefinitionsService } from '../application/services/forms-definitions.service';
|
|
9
|
+
import {
|
|
10
|
+
DataSourceKeysResponseDto,
|
|
11
|
+
DataSourceOptionsResponseDto,
|
|
12
|
+
} from '../dto/data-source-response.dto';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* MUST be registered BEFORE FormsDefinitionsController in the module's
|
|
16
|
+
* controllers array: both controllers share the 'organisations/:orgId/forms'
|
|
17
|
+
* base path, and the literal 'data-sources' segment has to win over the
|
|
18
|
+
* ':formKey' parameter route.
|
|
19
|
+
*/
|
|
20
|
+
@ApiTags('Forms')
|
|
21
|
+
@ApiBearerAuth()
|
|
22
|
+
@ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
|
|
23
|
+
@ApiProtectedErrorResponses(404)
|
|
24
|
+
@Controller('organisations/:orgId/forms')
|
|
25
|
+
@UseGuards(JwtAuthGuard, OrgScopeGuard, PermissionGuard)
|
|
26
|
+
export class FormsDataSourcesController {
|
|
27
|
+
constructor(private readonly formsDefinitionsService: FormsDefinitionsService) {}
|
|
28
|
+
|
|
29
|
+
@Get('data-sources')
|
|
30
|
+
@RequirePermissions('formDataSources.manage')
|
|
31
|
+
@ApiOperation({ summary: 'List registered data source keys.' })
|
|
32
|
+
@ApiOkResponse({
|
|
33
|
+
description: 'Registered data source keys.',
|
|
34
|
+
type: DataSourceKeysResponseDto,
|
|
35
|
+
})
|
|
36
|
+
listDataSourceKeys(@Param('orgId') orgId: string) {
|
|
37
|
+
return this.formsDefinitionsService.listDataSourceKeys(orgId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Get(':formKey/data-sources/:dataSourceKey/options')
|
|
41
|
+
@RequirePermissions('formSubmissions.create')
|
|
42
|
+
@ApiParam({ name: 'formKey', description: 'Form definition key.' })
|
|
43
|
+
@ApiParam({ name: 'dataSourceKey', description: 'Registered data source key.' })
|
|
44
|
+
@ApiOperation({
|
|
45
|
+
summary: 'Resolve options for a data source bound to fields of a published form.',
|
|
46
|
+
})
|
|
47
|
+
@ApiOkResponse({
|
|
48
|
+
description: 'Resolved data-source options.',
|
|
49
|
+
type: DataSourceOptionsResponseDto,
|
|
50
|
+
})
|
|
51
|
+
dataSourceOptions(
|
|
52
|
+
@Param('orgId') orgId: string,
|
|
53
|
+
@Param('formKey') formKey: string,
|
|
54
|
+
@Param('dataSourceKey') dataSourceKey: string,
|
|
55
|
+
) {
|
|
56
|
+
return this.formsDefinitionsService.dataSourceOptions(orgId, formKey, dataSourceKey);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Body,
|
|
3
|
+
Controller,
|
|
4
|
+
Get,
|
|
5
|
+
HttpCode,
|
|
6
|
+
Param,
|
|
7
|
+
ParseIntPipe,
|
|
8
|
+
Patch,
|
|
9
|
+
Post,
|
|
10
|
+
Query,
|
|
11
|
+
UseGuards,
|
|
12
|
+
} from '@nestjs/common';
|
|
13
|
+
import {
|
|
14
|
+
ApiBearerAuth,
|
|
15
|
+
ApiCreatedResponse,
|
|
16
|
+
ApiOkResponse,
|
|
17
|
+
ApiOperation,
|
|
18
|
+
ApiParam,
|
|
19
|
+
ApiTags,
|
|
20
|
+
ApiUnprocessableEntityResponse,
|
|
21
|
+
} from '@nestjs/swagger';
|
|
22
|
+
import { ErrorResponseDto } from '../../../common/dto/error-response.dto';
|
|
23
|
+
import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
|
|
24
|
+
import { PermissionGuard } from '../../access-control/application/services/permission.guard';
|
|
25
|
+
import { RequirePermissions } from '../../access-control/presentation/permissions.decorator';
|
|
26
|
+
import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
|
|
27
|
+
import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
|
|
28
|
+
import { FormsDefinitionsService } from '../application/services/forms-definitions.service';
|
|
29
|
+
import { CreateFormDefinitionDto } from '../dto/create-form-definition.dto';
|
|
30
|
+
import {
|
|
31
|
+
FormDefinitionListResponseDto,
|
|
32
|
+
FormDefinitionResponseDto,
|
|
33
|
+
} from '../dto/form-definition-response.dto';
|
|
34
|
+
import { FormRenderResponseDto } from '../dto/form-render-response.dto';
|
|
35
|
+
import { ListFormDefinitionsQueryDto } from '../dto/list-form-definitions-query.dto';
|
|
36
|
+
import { SetPublicAccessDto } from '../dto/set-public-access.dto';
|
|
37
|
+
import { UpdateFormDefinitionDto } from '../dto/update-form-definition.dto';
|
|
38
|
+
|
|
39
|
+
@ApiTags('Forms')
|
|
40
|
+
@ApiBearerAuth()
|
|
41
|
+
@ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
|
|
42
|
+
@ApiProtectedErrorResponses(404, 409)
|
|
43
|
+
@ApiUnprocessableEntityResponse({
|
|
44
|
+
description: 'The form definition failed engine validation or linting.',
|
|
45
|
+
type: ErrorResponseDto,
|
|
46
|
+
})
|
|
47
|
+
@Controller('organisations/:orgId/forms')
|
|
48
|
+
@UseGuards(JwtAuthGuard, OrgScopeGuard, PermissionGuard)
|
|
49
|
+
export class FormsDefinitionsController {
|
|
50
|
+
constructor(private readonly formsDefinitionsService: FormsDefinitionsService) {}
|
|
51
|
+
|
|
52
|
+
@Get()
|
|
53
|
+
@RequirePermissions('forms.read')
|
|
54
|
+
@ApiOperation({ summary: 'List form definitions.' })
|
|
55
|
+
@ApiOkResponse({
|
|
56
|
+
description: 'Form definitions.',
|
|
57
|
+
type: FormDefinitionListResponseDto,
|
|
58
|
+
})
|
|
59
|
+
list(@Param('orgId') orgId: string, @Query() query: ListFormDefinitionsQueryDto) {
|
|
60
|
+
return this.formsDefinitionsService.list(orgId, query);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Post()
|
|
64
|
+
@RequirePermissions('forms.create')
|
|
65
|
+
@ApiOperation({ summary: 'Create a new draft form definition version.' })
|
|
66
|
+
@ApiCreatedResponse({
|
|
67
|
+
description: 'Created draft definition.',
|
|
68
|
+
type: FormDefinitionResponseDto,
|
|
69
|
+
})
|
|
70
|
+
create(@Param('orgId') orgId: string, @Body() dto: CreateFormDefinitionDto) {
|
|
71
|
+
return this.formsDefinitionsService.create(orgId, dto);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Get(':formKey')
|
|
75
|
+
@RequirePermissions('forms.read')
|
|
76
|
+
@ApiParam({ name: 'formKey', description: 'Form definition key.' })
|
|
77
|
+
@ApiOperation({ summary: 'Return the latest version of a form definition.' })
|
|
78
|
+
@ApiOkResponse({
|
|
79
|
+
description: 'Latest definition version.',
|
|
80
|
+
type: FormDefinitionResponseDto,
|
|
81
|
+
})
|
|
82
|
+
getLatest(@Param('orgId') orgId: string, @Param('formKey') formKey: string) {
|
|
83
|
+
return this.formsDefinitionsService.getLatest(orgId, formKey);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Get(':formKey/render')
|
|
87
|
+
@RequirePermissions('formSubmissions.create')
|
|
88
|
+
@ApiParam({ name: 'formKey', description: 'Form definition key.' })
|
|
89
|
+
@ApiOperation({
|
|
90
|
+
summary: 'Return the published definition plus resolved data-source options.',
|
|
91
|
+
})
|
|
92
|
+
@ApiOkResponse({
|
|
93
|
+
description: 'Renderable definition.',
|
|
94
|
+
type: FormRenderResponseDto,
|
|
95
|
+
})
|
|
96
|
+
render(@Param('orgId') orgId: string, @Param('formKey') formKey: string) {
|
|
97
|
+
return this.formsDefinitionsService.render(orgId, formKey);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@Get(':formKey/versions/:version')
|
|
101
|
+
@RequirePermissions('forms.read')
|
|
102
|
+
@ApiParam({ name: 'formKey', description: 'Form definition key.' })
|
|
103
|
+
@ApiParam({ name: 'version', description: 'Definition version number.', type: Number })
|
|
104
|
+
@ApiOperation({ summary: 'Return a specific form definition version.' })
|
|
105
|
+
@ApiOkResponse({
|
|
106
|
+
description: 'Definition version.',
|
|
107
|
+
type: FormDefinitionResponseDto,
|
|
108
|
+
})
|
|
109
|
+
getVersion(
|
|
110
|
+
@Param('orgId') orgId: string,
|
|
111
|
+
@Param('formKey') formKey: string,
|
|
112
|
+
@Param('version', ParseIntPipe) version: number,
|
|
113
|
+
) {
|
|
114
|
+
return this.formsDefinitionsService.getVersion(orgId, formKey, version);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@Patch(':formKey/versions/:version')
|
|
118
|
+
@RequirePermissions('forms.update')
|
|
119
|
+
@ApiParam({ name: 'formKey', description: 'Form definition key.' })
|
|
120
|
+
@ApiParam({ name: 'version', description: 'Definition version number.', type: Number })
|
|
121
|
+
@ApiOperation({ summary: 'Update a draft form definition version.' })
|
|
122
|
+
@ApiOkResponse({
|
|
123
|
+
description: 'Updated draft definition.',
|
|
124
|
+
type: FormDefinitionResponseDto,
|
|
125
|
+
})
|
|
126
|
+
update(
|
|
127
|
+
@Param('orgId') orgId: string,
|
|
128
|
+
@Param('formKey') formKey: string,
|
|
129
|
+
@Param('version', ParseIntPipe) version: number,
|
|
130
|
+
@Body() dto: UpdateFormDefinitionDto,
|
|
131
|
+
) {
|
|
132
|
+
return this.formsDefinitionsService.update(orgId, formKey, version, dto);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@Post(':formKey/versions/:version/publish')
|
|
136
|
+
@HttpCode(200)
|
|
137
|
+
@RequirePermissions('forms.publish')
|
|
138
|
+
@ApiParam({ name: 'formKey', description: 'Form definition key.' })
|
|
139
|
+
@ApiParam({ name: 'version', description: 'Definition version number.', type: Number })
|
|
140
|
+
@ApiOperation({ summary: 'Publish a draft form definition version.' })
|
|
141
|
+
@ApiOkResponse({
|
|
142
|
+
description: 'Published definition.',
|
|
143
|
+
type: FormDefinitionResponseDto,
|
|
144
|
+
})
|
|
145
|
+
publish(
|
|
146
|
+
@Param('orgId') orgId: string,
|
|
147
|
+
@Param('formKey') formKey: string,
|
|
148
|
+
@Param('version', ParseIntPipe) version: number,
|
|
149
|
+
) {
|
|
150
|
+
return this.formsDefinitionsService.publish(orgId, formKey, version);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@Post(':formKey/versions/:version/archive')
|
|
154
|
+
@HttpCode(200)
|
|
155
|
+
@RequirePermissions('forms.archive')
|
|
156
|
+
@ApiParam({ name: 'formKey', description: 'Form definition key.' })
|
|
157
|
+
@ApiParam({ name: 'version', description: 'Definition version number.', type: Number })
|
|
158
|
+
@ApiOperation({ summary: 'Archive a form definition version.' })
|
|
159
|
+
@ApiOkResponse({
|
|
160
|
+
description: 'Archived definition.',
|
|
161
|
+
type: FormDefinitionResponseDto,
|
|
162
|
+
})
|
|
163
|
+
archive(
|
|
164
|
+
@Param('orgId') orgId: string,
|
|
165
|
+
@Param('formKey') formKey: string,
|
|
166
|
+
@Param('version', ParseIntPipe) version: number,
|
|
167
|
+
) {
|
|
168
|
+
return this.formsDefinitionsService.archive(orgId, formKey, version);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@Post(':formKey/versions/:version/public-access')
|
|
172
|
+
@HttpCode(200)
|
|
173
|
+
@RequirePermissions('forms.managePublicAccess')
|
|
174
|
+
@ApiParam({ name: 'formKey', description: 'Form definition key.' })
|
|
175
|
+
@ApiParam({ name: 'version', description: 'Definition version number.', type: Number })
|
|
176
|
+
@ApiOperation({
|
|
177
|
+
summary: 'Flip the public/private access setting on a form definition version.',
|
|
178
|
+
})
|
|
179
|
+
@ApiOkResponse({
|
|
180
|
+
description: 'Updated definition.',
|
|
181
|
+
type: FormDefinitionResponseDto,
|
|
182
|
+
})
|
|
183
|
+
setPublicAccess(
|
|
184
|
+
@Param('orgId') orgId: string,
|
|
185
|
+
@Param('formKey') formKey: string,
|
|
186
|
+
@Param('version', ParseIntPipe) version: number,
|
|
187
|
+
@Body() dto: SetPublicAccessDto,
|
|
188
|
+
) {
|
|
189
|
+
return this.formsDefinitionsService.setPublicAccess(orgId, formKey, version, dto);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Controller,
|
|
3
|
+
Get,
|
|
4
|
+
Param,
|
|
5
|
+
ParseUUIDPipe,
|
|
6
|
+
Post,
|
|
7
|
+
Query,
|
|
8
|
+
UploadedFile,
|
|
9
|
+
UseGuards,
|
|
10
|
+
UseInterceptors,
|
|
11
|
+
} from '@nestjs/common';
|
|
12
|
+
import {
|
|
13
|
+
ApiBearerAuth,
|
|
14
|
+
ApiBody,
|
|
15
|
+
ApiConsumes,
|
|
16
|
+
ApiOkResponse,
|
|
17
|
+
ApiOperation,
|
|
18
|
+
ApiParam,
|
|
19
|
+
ApiTags,
|
|
20
|
+
} from '@nestjs/swagger';
|
|
21
|
+
import { ApiProtectedErrorResponses } from '../../../common/swagger/api-error-responses';
|
|
22
|
+
import { CurrentUser } from '../../auth/presentation/current-user.decorator';
|
|
23
|
+
import { JwtAuthGuard } from '../../auth/infrastructure/passport/jwt-auth.guard';
|
|
24
|
+
import { AuthenticatedUser } from '../../auth/types/authenticated-user';
|
|
25
|
+
import { PermissionGuard } from '../../access-control/application/services/permission.guard';
|
|
26
|
+
import { RequirePermissions } from '../../access-control/presentation/permissions.decorator';
|
|
27
|
+
import { OrgScopeGuard } from '../../request-context/presentation/org-scope.guard';
|
|
28
|
+
import { FileUploadResponseDto } from '../dto/file-upload-response.dto';
|
|
29
|
+
import { UploadFileQueryDto } from '../dto/upload-file-query.dto';
|
|
30
|
+
import { FormsFilesService } from '../application/services/forms-files.service';
|
|
31
|
+
import { FormsUploadInterceptor } from './forms-upload.interceptor';
|
|
32
|
+
|
|
33
|
+
@ApiTags('Forms')
|
|
34
|
+
@ApiBearerAuth()
|
|
35
|
+
@ApiParam({ name: 'orgId', description: 'Organisation ID.', format: 'uuid' })
|
|
36
|
+
@ApiParam({ name: 'formKey', description: 'Form definition key.' })
|
|
37
|
+
@ApiProtectedErrorResponses(404, 409)
|
|
38
|
+
@Controller('organisations/:orgId/forms/:formKey/files')
|
|
39
|
+
@UseGuards(JwtAuthGuard, OrgScopeGuard, PermissionGuard)
|
|
40
|
+
export class FormsFilesController {
|
|
41
|
+
constructor(private readonly filesService: FormsFilesService) {}
|
|
42
|
+
|
|
43
|
+
@Post()
|
|
44
|
+
@RequirePermissions('formSubmissions.create')
|
|
45
|
+
@UseInterceptors(FormsUploadInterceptor)
|
|
46
|
+
@ApiConsumes('multipart/form-data')
|
|
47
|
+
@ApiBody({
|
|
48
|
+
schema: {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: { file: { type: 'string', format: 'binary' } },
|
|
51
|
+
required: ['file'],
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
@ApiOperation({
|
|
55
|
+
summary:
|
|
56
|
+
'Upload a file for a form field (before submit). The submission then references the returned fileId.',
|
|
57
|
+
})
|
|
58
|
+
@ApiOkResponse({ description: 'Uploaded file reference.', type: FileUploadResponseDto })
|
|
59
|
+
upload(
|
|
60
|
+
@CurrentUser() user: AuthenticatedUser,
|
|
61
|
+
@Param('orgId') orgId: string,
|
|
62
|
+
@Param('formKey') formKey: string,
|
|
63
|
+
@Query() query: UploadFileQueryDto,
|
|
64
|
+
@UploadedFile() file: { originalname: string; size: number; buffer: Buffer } | undefined,
|
|
65
|
+
) {
|
|
66
|
+
return this.filesService.upload(orgId, formKey, query, file, user);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Get(':fileId')
|
|
70
|
+
@RequirePermissions('formSubmissions.read')
|
|
71
|
+
@ApiOperation({ summary: 'Return metadata for an uploaded file.' })
|
|
72
|
+
@ApiOkResponse({ description: 'Uploaded file metadata.', type: FileUploadResponseDto })
|
|
73
|
+
getMeta(
|
|
74
|
+
@Param('orgId') orgId: string,
|
|
75
|
+
@Param('fileId', ParseUUIDPipe) fileId: string,
|
|
76
|
+
) {
|
|
77
|
+
return this.filesService.getMeta(orgId, fileId);
|
|
78
|
+
}
|
|
79
|
+
}
|