@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,129 @@
|
|
|
1
|
+
-- CreateEnum
|
|
2
|
+
CREATE TYPE "ReportDefinitionStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED');
|
|
3
|
+
|
|
4
|
+
-- CreateEnum
|
|
5
|
+
CREATE TYPE "ReportExportStatus" AS ENUM ('PENDING', 'RUNNING', 'DONE', 'FAILED');
|
|
6
|
+
|
|
7
|
+
-- CreateEnum
|
|
8
|
+
CREATE TYPE "ReportActionRunStatus" AS ENUM ('RUNNING', 'DONE', 'FAILED');
|
|
9
|
+
|
|
10
|
+
-- CreateTable
|
|
11
|
+
CREATE TABLE "ReportDefinition" (
|
|
12
|
+
"id" TEXT NOT NULL,
|
|
13
|
+
"orgId" TEXT NOT NULL,
|
|
14
|
+
"key" TEXT NOT NULL,
|
|
15
|
+
"version" INTEGER NOT NULL,
|
|
16
|
+
"status" "ReportDefinitionStatus" NOT NULL DEFAULT 'DRAFT',
|
|
17
|
+
"schema" JSONB NOT NULL,
|
|
18
|
+
"compiled" JSONB,
|
|
19
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
20
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
21
|
+
|
|
22
|
+
CONSTRAINT "ReportDefinition_pkey" PRIMARY KEY ("id")
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
-- CreateTable
|
|
26
|
+
CREATE TABLE "ReportSavedView" (
|
|
27
|
+
"id" TEXT NOT NULL,
|
|
28
|
+
"orgId" TEXT NOT NULL,
|
|
29
|
+
"reportKey" TEXT NOT NULL,
|
|
30
|
+
"reportVersion" INTEGER NOT NULL,
|
|
31
|
+
"name" TEXT NOT NULL,
|
|
32
|
+
"spec" JSONB NOT NULL,
|
|
33
|
+
"ownerId" TEXT,
|
|
34
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
35
|
+
|
|
36
|
+
CONSTRAINT "ReportSavedView_pkey" PRIMARY KEY ("id")
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
-- CreateTable
|
|
40
|
+
CREATE TABLE "ReportExportJob" (
|
|
41
|
+
"id" TEXT NOT NULL,
|
|
42
|
+
"orgId" TEXT NOT NULL,
|
|
43
|
+
"reportKey" TEXT NOT NULL,
|
|
44
|
+
"reportVersion" INTEGER NOT NULL,
|
|
45
|
+
"spec" JSONB NOT NULL,
|
|
46
|
+
"asOf" TIMESTAMP(3),
|
|
47
|
+
"status" "ReportExportStatus" NOT NULL DEFAULT 'PENDING',
|
|
48
|
+
"fileId" TEXT,
|
|
49
|
+
"rowCount" INTEGER,
|
|
50
|
+
"error" TEXT,
|
|
51
|
+
"requestedBy" TEXT NOT NULL,
|
|
52
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
53
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
54
|
+
|
|
55
|
+
CONSTRAINT "ReportExportJob_pkey" PRIMARY KEY ("id")
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
-- CreateTable
|
|
59
|
+
CREATE TABLE "ReportBulkActionRun" (
|
|
60
|
+
"id" TEXT NOT NULL,
|
|
61
|
+
"orgId" TEXT NOT NULL,
|
|
62
|
+
"reportKey" TEXT NOT NULL,
|
|
63
|
+
"reportVersion" INTEGER NOT NULL,
|
|
64
|
+
"action" TEXT NOT NULL,
|
|
65
|
+
"idempotencyKey" TEXT NOT NULL,
|
|
66
|
+
"status" "ReportActionRunStatus" NOT NULL DEFAULT 'RUNNING',
|
|
67
|
+
"result" JSONB,
|
|
68
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
69
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
70
|
+
|
|
71
|
+
CONSTRAINT "ReportBulkActionRun_pkey" PRIMARY KEY ("id")
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
-- CreateTable
|
|
75
|
+
CREATE TABLE "ReportRowTag" (
|
|
76
|
+
"id" TEXT NOT NULL,
|
|
77
|
+
"orgId" TEXT NOT NULL,
|
|
78
|
+
"sourceKind" TEXT NOT NULL,
|
|
79
|
+
"sourceKey" TEXT NOT NULL,
|
|
80
|
+
"rowId" TEXT NOT NULL,
|
|
81
|
+
"tag" TEXT NOT NULL,
|
|
82
|
+
"createdBy" TEXT NOT NULL,
|
|
83
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
84
|
+
|
|
85
|
+
CONSTRAINT "ReportRowTag_pkey" PRIMARY KEY ("id")
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
-- CreateIndex
|
|
89
|
+
CREATE UNIQUE INDEX "ReportDefinition_orgId_key_version_key" ON "ReportDefinition"("orgId", "key", "version");
|
|
90
|
+
|
|
91
|
+
-- CreateIndex
|
|
92
|
+
CREATE INDEX "ReportDefinition_orgId_idx" ON "ReportDefinition"("orgId");
|
|
93
|
+
|
|
94
|
+
-- CreateIndex
|
|
95
|
+
CREATE INDEX "ReportDefinition_orgId_status_idx" ON "ReportDefinition"("orgId", "status");
|
|
96
|
+
|
|
97
|
+
-- CreateIndex
|
|
98
|
+
CREATE INDEX "ReportSavedView_orgId_reportKey_idx" ON "ReportSavedView"("orgId", "reportKey");
|
|
99
|
+
|
|
100
|
+
-- CreateIndex
|
|
101
|
+
CREATE INDEX "ReportExportJob_orgId_status_idx" ON "ReportExportJob"("orgId", "status");
|
|
102
|
+
|
|
103
|
+
-- CreateIndex
|
|
104
|
+
CREATE INDEX "ReportExportJob_orgId_createdAt_idx" ON "ReportExportJob"("orgId", "createdAt");
|
|
105
|
+
|
|
106
|
+
-- CreateIndex
|
|
107
|
+
CREATE UNIQUE INDEX "ReportBulkActionRun_orgId_idempotencyKey_key" ON "ReportBulkActionRun"("orgId", "idempotencyKey");
|
|
108
|
+
|
|
109
|
+
-- CreateIndex
|
|
110
|
+
CREATE INDEX "ReportBulkActionRun_orgId_reportKey_action_idx" ON "ReportBulkActionRun"("orgId", "reportKey", "action");
|
|
111
|
+
|
|
112
|
+
-- CreateIndex
|
|
113
|
+
CREATE UNIQUE INDEX "ReportRowTag_orgId_sourceKind_sourceKey_rowId_tag_key" ON "ReportRowTag"("orgId", "sourceKind", "sourceKey", "rowId", "tag");
|
|
114
|
+
|
|
115
|
+
-- CreateIndex
|
|
116
|
+
CREATE INDEX "ReportRowTag_orgId_sourceKind_sourceKey_tag_idx" ON "ReportRowTag"("orgId", "sourceKind", "sourceKey", "tag");
|
|
117
|
+
|
|
118
|
+
-- CreateIndex
|
|
119
|
+
CREATE INDEX "ReportRowTag_orgId_sourceKind_sourceKey_rowId_idx" ON "ReportRowTag"("orgId", "sourceKind", "sourceKey", "rowId");
|
|
120
|
+
|
|
121
|
+
-- Partial unique indexes for saved-view names (report design §12, finding #5).
|
|
122
|
+
-- Postgres treats NULLs as distinct, so a plain unique over nullable "ownerId"
|
|
123
|
+
-- would not deduplicate SHARED views. Prisma cannot express partial indexes:
|
|
124
|
+
-- if a later `prisma migrate dev` proposes dropping these, DELETE those DROP
|
|
125
|
+
-- lines — the reports boot check fails fast when they are missing.
|
|
126
|
+
CREATE UNIQUE INDEX "report_view_shared_uq"
|
|
127
|
+
ON "ReportSavedView" ("orgId", "reportKey", "name") WHERE "ownerId" IS NULL;
|
|
128
|
+
CREATE UNIQUE INDEX "report_view_personal_uq"
|
|
129
|
+
ON "ReportSavedView" ("orgId", "reportKey", "ownerId", "name") WHERE "ownerId" IS NOT NULL;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- Add an opaque lease token for outbox claim ownership. Dispatchers must match
|
|
2
|
+
-- this token when heartbeating or settling PROCESSING jobs.
|
|
3
|
+
ALTER TABLE "FormOutboxJob" ADD COLUMN "claimedBy" TEXT;
|
|
4
|
+
|
|
5
|
+
CREATE INDEX "FormOutboxJob_status_claimedBy_idx" ON "FormOutboxJob"("status", "claimedBy");
|
|
@@ -297,3 +297,292 @@ model AuditLog {
|
|
|
297
297
|
@@index([targetType, targetId])
|
|
298
298
|
@@index([createdAt])
|
|
299
299
|
}
|
|
300
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
301
|
+
// @ftisindia/form-builder — canonical Prisma model snippet (ENGINE_SCHEMA_VERSION 1)
|
|
302
|
+
//
|
|
303
|
+
// The APP owns schema.prisma and the migration history (ecosystem guide §10.1).
|
|
304
|
+
// Copy these models into your app's prisma/schema.prisma verbatim and run
|
|
305
|
+
// `prisma migrate dev`. The glue module's boot-time check verifies the live
|
|
306
|
+
// database matches this snippet's tables/columns and fails fast otherwise.
|
|
307
|
+
// Conventions follow the FTIS template: uuid() ids, UPPER_CASE enum values,
|
|
308
|
+
// org-leading hot indexes, explicit orgId on every engine table.
|
|
309
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
enum FormDefinitionStatus {
|
|
312
|
+
DRAFT
|
|
313
|
+
PUBLISHED
|
|
314
|
+
ARCHIVED
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
enum SubmissionStatus {
|
|
318
|
+
DRAFT
|
|
319
|
+
SUBMITTED
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
enum FormActionStatus {
|
|
323
|
+
OK
|
|
324
|
+
ERROR
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
enum OutboxStatus {
|
|
328
|
+
PENDING
|
|
329
|
+
PROCESSING
|
|
330
|
+
DONE
|
|
331
|
+
FAILED
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
enum FileStatus {
|
|
335
|
+
TEMPORARY
|
|
336
|
+
SCANNING
|
|
337
|
+
CLEAN
|
|
338
|
+
INFECTED
|
|
339
|
+
LINKED
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
model FormDefinition {
|
|
343
|
+
id String @id @default(uuid())
|
|
344
|
+
orgId String
|
|
345
|
+
key String
|
|
346
|
+
// Bumped on every published change; published rows are immutable.
|
|
347
|
+
version Int
|
|
348
|
+
status FormDefinitionStatus @default(DRAFT)
|
|
349
|
+
// The full FormDefinition document (JSONB).
|
|
350
|
+
schema Json
|
|
351
|
+
|
|
352
|
+
createdAt DateTime @default(now())
|
|
353
|
+
updatedAt DateTime @updatedAt
|
|
354
|
+
|
|
355
|
+
@@unique([orgId, key, version])
|
|
356
|
+
@@index([orgId])
|
|
357
|
+
@@index([orgId, status])
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
model FormSubmission {
|
|
361
|
+
id String @id @default(uuid())
|
|
362
|
+
orgId String
|
|
363
|
+
formKey String
|
|
364
|
+
// The exact definition version the data was captured under. Non-negotiable.
|
|
365
|
+
formVersion Int
|
|
366
|
+
// Nested submission payload (JSONB) — submissions are documents, not rows.
|
|
367
|
+
data Json
|
|
368
|
+
status SubmissionStatus @default(DRAFT)
|
|
369
|
+
// Set by the lockEditing action; locked submissions reject further edits.
|
|
370
|
+
locked Boolean @default(false)
|
|
371
|
+
|
|
372
|
+
createdBy String?
|
|
373
|
+
// Stamped only on anonymous/public submissions (abuse-control Tier 1).
|
|
374
|
+
ipAddress String?
|
|
375
|
+
userAgent String?
|
|
376
|
+
|
|
377
|
+
createdAt DateTime @default(now())
|
|
378
|
+
updatedAt DateTime @updatedAt
|
|
379
|
+
|
|
380
|
+
@@index([orgId, formKey, status])
|
|
381
|
+
@@index([orgId, createdAt])
|
|
382
|
+
@@index([orgId, formKey, ipAddress, createdAt])
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
model FormActionLog {
|
|
386
|
+
id String @id @default(uuid())
|
|
387
|
+
orgId String
|
|
388
|
+
submissionId String?
|
|
389
|
+
formKey String
|
|
390
|
+
formVersion Int?
|
|
391
|
+
action String
|
|
392
|
+
status FormActionStatus
|
|
393
|
+
// Redacted by the engine before write — sensitive fields never land here.
|
|
394
|
+
input Json?
|
|
395
|
+
output Json?
|
|
396
|
+
errorCode String?
|
|
397
|
+
errorMessage String?
|
|
398
|
+
actorId String?
|
|
399
|
+
requestId String?
|
|
400
|
+
durationMs Int?
|
|
401
|
+
createdAt DateTime @default(now())
|
|
402
|
+
|
|
403
|
+
@@index([orgId, createdAt])
|
|
404
|
+
@@index([orgId, formKey, action, status])
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
model FormOutboxJob {
|
|
408
|
+
id String @id @default(uuid())
|
|
409
|
+
type String
|
|
410
|
+
payload Json
|
|
411
|
+
status OutboxStatus @default(PENDING)
|
|
412
|
+
|
|
413
|
+
attempts Int @default(0)
|
|
414
|
+
maxAttempts Int @default(8)
|
|
415
|
+
// Prevents double-send on retry (e.g. submissionId:type:action).
|
|
416
|
+
idempotencyKey String? @unique
|
|
417
|
+
lastError String?
|
|
418
|
+
runAfter DateTime?
|
|
419
|
+
// Opaque worker lease token. Heartbeats and terminal transitions must match
|
|
420
|
+
// this value so a stale worker cannot finish another worker's reclaimed job.
|
|
421
|
+
claimedBy String?
|
|
422
|
+
|
|
423
|
+
// Worker-context restoration + audit correlation (ecosystem guide §3).
|
|
424
|
+
orgId String?
|
|
425
|
+
actorUserId String?
|
|
426
|
+
originRequestId String?
|
|
427
|
+
|
|
428
|
+
createdAt DateTime @default(now())
|
|
429
|
+
updatedAt DateTime @updatedAt
|
|
430
|
+
|
|
431
|
+
@@index([status, runAfter])
|
|
432
|
+
@@index([status, claimedBy])
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
model UploadedFile {
|
|
436
|
+
id String @id @default(uuid())
|
|
437
|
+
storageKey String @unique
|
|
438
|
+
originalName String
|
|
439
|
+
// SERVER-VERIFIED (sniffed) MIME type, never the client-sent Content-Type.
|
|
440
|
+
mimeType String
|
|
441
|
+
size Int
|
|
442
|
+
// sha256 hex.
|
|
443
|
+
checksum String
|
|
444
|
+
// Two-factor ownership: uploader identity AND tenant scope (§10).
|
|
445
|
+
ownerId String
|
|
446
|
+
orgId String
|
|
447
|
+
|
|
448
|
+
status FileStatus @default(TEMPORARY)
|
|
449
|
+
submissionId String?
|
|
450
|
+
|
|
451
|
+
createdAt DateTime @default(now())
|
|
452
|
+
|
|
453
|
+
@@index([orgId, status])
|
|
454
|
+
@@index([ownerId, status])
|
|
455
|
+
@@index([status, createdAt])
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
459
|
+
// Report builder (@ftisindia/report-builder) — engine tables
|
|
460
|
+
// (REPORTS_ENGINE_SCHEMA_VERSION 1). Copied from the engine's canonical
|
|
461
|
+
// snippet (@ftisindia/report-builder/prisma/reports.prisma); the glue
|
|
462
|
+
// module's boot check verifies the live database matches it. Sources stay
|
|
463
|
+
// where they live — these are the engine's OWN tables only (report design §12).
|
|
464
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
enum ReportDefinitionStatus {
|
|
467
|
+
DRAFT
|
|
468
|
+
PUBLISHED
|
|
469
|
+
ARCHIVED
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
enum ReportExportStatus {
|
|
473
|
+
PENDING
|
|
474
|
+
RUNNING
|
|
475
|
+
DONE
|
|
476
|
+
FAILED
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
enum ReportActionRunStatus {
|
|
480
|
+
RUNNING
|
|
481
|
+
DONE
|
|
482
|
+
FAILED
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
model ReportDefinition {
|
|
486
|
+
id String @id @default(uuid())
|
|
487
|
+
orgId String
|
|
488
|
+
key String
|
|
489
|
+
// Bumped on every published change; published rows are immutable.
|
|
490
|
+
version Int
|
|
491
|
+
status ReportDefinitionStatus @default(DRAFT)
|
|
492
|
+
// The full ReportDefinition document (JSONB).
|
|
493
|
+
schema Json
|
|
494
|
+
// Set at publish: logical→physical column map + plan hashes (report design §5.2/§8.1).
|
|
495
|
+
compiled Json?
|
|
496
|
+
|
|
497
|
+
createdAt DateTime @default(now())
|
|
498
|
+
updatedAt DateTime @updatedAt
|
|
499
|
+
|
|
500
|
+
@@unique([orgId, key, version])
|
|
501
|
+
@@index([orgId])
|
|
502
|
+
@@index([orgId, status])
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
model ReportSavedView {
|
|
506
|
+
id String @id @default(uuid())
|
|
507
|
+
orgId String
|
|
508
|
+
reportKey String
|
|
509
|
+
// The definition version the view was authored against (report design §4).
|
|
510
|
+
reportVersion Int
|
|
511
|
+
name String
|
|
512
|
+
// The saved QuerySpec (JSONB).
|
|
513
|
+
spec Json
|
|
514
|
+
// null ⇒ shared with the whole org (creation gated by reports.update).
|
|
515
|
+
ownerId String?
|
|
516
|
+
createdAt DateTime @default(now())
|
|
517
|
+
|
|
518
|
+
@@index([orgId, reportKey])
|
|
519
|
+
// Uniqueness lives in PARTIAL UNIQUE INDEXES shipped as raw SQL inside the
|
|
520
|
+
// add_report_builder migration — Postgres treats NULLs as distinct, so a
|
|
521
|
+
// @@unique over nullable ownerId would NOT deduplicate shared views (report
|
|
522
|
+
// design §12, finding #5). If a later `prisma migrate dev` proposes
|
|
523
|
+
// DROP INDEX "report_view_shared_uq" / "report_view_personal_uq", DELETE
|
|
524
|
+
// those lines — the reports boot check fails fast if they go missing.
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
model ReportExportJob {
|
|
528
|
+
id String @id @default(uuid())
|
|
529
|
+
orgId String
|
|
530
|
+
reportKey String
|
|
531
|
+
// The version the export ran against (report design §9, finding #1).
|
|
532
|
+
reportVersion Int
|
|
533
|
+
// FULL export spec {spec, format, columns} — replayable, auditable (not a hash).
|
|
534
|
+
spec Json
|
|
535
|
+
// REPEATABLE READ snapshot timestamp, set when the job runs (finding #8).
|
|
536
|
+
asOf DateTime?
|
|
537
|
+
status ReportExportStatus @default(PENDING)
|
|
538
|
+
// Reports-owned storage key once the async export lands in file storage.
|
|
539
|
+
fileId String?
|
|
540
|
+
rowCount Int?
|
|
541
|
+
error String?
|
|
542
|
+
requestedBy String
|
|
543
|
+
|
|
544
|
+
createdAt DateTime @default(now())
|
|
545
|
+
updatedAt DateTime @updatedAt
|
|
546
|
+
|
|
547
|
+
@@index([orgId, status])
|
|
548
|
+
@@index([orgId, createdAt])
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
model ReportBulkActionRun {
|
|
552
|
+
id String @id @default(uuid())
|
|
553
|
+
orgId String
|
|
554
|
+
reportKey String
|
|
555
|
+
reportVersion Int
|
|
556
|
+
action String
|
|
557
|
+
// Client-supplied; a retried execute with the same key returns the recorded
|
|
558
|
+
// outcome instead of re-running (report design §6.3, finding #7).
|
|
559
|
+
idempotencyKey String
|
|
560
|
+
status ReportActionRunStatus @default(RUNNING)
|
|
561
|
+
result Json?
|
|
562
|
+
|
|
563
|
+
createdAt DateTime @default(now())
|
|
564
|
+
updatedAt DateTime @updatedAt
|
|
565
|
+
|
|
566
|
+
@@unique([orgId, idempotencyKey])
|
|
567
|
+
@@index([orgId, reportKey, action])
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
model ReportRowTag {
|
|
571
|
+
id String @id @default(uuid())
|
|
572
|
+
orgId String
|
|
573
|
+
// 'custom' | 'form' | any registered source-provider kind.
|
|
574
|
+
sourceKind String
|
|
575
|
+
// Custom source key or form key.
|
|
576
|
+
sourceKey String
|
|
577
|
+
// The source's declared, STABLE row identity (report design §7).
|
|
578
|
+
rowId String
|
|
579
|
+
// Normalized: lowercased, trimmed. Labels are single-valued by convention
|
|
580
|
+
// ("label:approved") — one table, one mechanism.
|
|
581
|
+
tag String
|
|
582
|
+
createdBy String
|
|
583
|
+
createdAt DateTime @default(now())
|
|
584
|
+
|
|
585
|
+
@@unique([orgId, sourceKind, sourceKey, rowId, tag])
|
|
586
|
+
@@index([orgId, sourceKind, sourceKey, tag])
|
|
587
|
+
@@index([orgId, sourceKind, sourceKey, rowId])
|
|
588
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { writeFile } from 'fs/promises';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
loadEnvFile();
|
|
6
|
+
ensurePlaceholderEnv();
|
|
7
|
+
|
|
8
|
+
main().catch((error: unknown) => {
|
|
9
|
+
console.error(error);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
// Imported after env preparation so config validation sees the placeholders.
|
|
15
|
+
const { NestFactory } = await import('@nestjs/core');
|
|
16
|
+
const { SwaggerModule } = await import('@nestjs/swagger');
|
|
17
|
+
const { AppModule } = await import('../src/app.module');
|
|
18
|
+
const { buildOpenApiConfig } = await import('../src/config/openapi');
|
|
19
|
+
|
|
20
|
+
const app = await NestFactory.create(AppModule, {
|
|
21
|
+
logger: false,
|
|
22
|
+
abortOnError: false,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const document = SwaggerModule.createDocument(app, buildOpenApiConfig());
|
|
27
|
+
const outputPath = resolve(process.cwd(), 'docs-json.json');
|
|
28
|
+
await writeFile(outputPath, `${JSON.stringify(document, null, 2)}\n`);
|
|
29
|
+
console.log(`OpenAPI document written to ${outputPath}`);
|
|
30
|
+
} finally {
|
|
31
|
+
await app.close().catch(() => undefined);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The document is built from route metadata only; no server starts and no
|
|
36
|
+
// database connection is made, so missing secrets get safe placeholders.
|
|
37
|
+
function ensurePlaceholderEnv() {
|
|
38
|
+
if (!process.env.DATABASE_URL) {
|
|
39
|
+
process.env.DATABASE_URL =
|
|
40
|
+
'postgresql://placeholder:placeholder@localhost:5432/placeholder';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!process.env.JWT_SECRET) {
|
|
44
|
+
process.env.JWT_SECRET = 'openapi-export-placeholder-secret-0123456789';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function loadEnvFile() {
|
|
49
|
+
const envPath = resolve(process.cwd(), '.env');
|
|
50
|
+
if (!existsSync(envPath)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const lines = readFileSync(envPath, 'utf8').split(/\r?\n/);
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const equalsIndex = trimmed.indexOf('=');
|
|
62
|
+
if (equalsIndex === -1) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const key = trimmed.slice(0, equalsIndex).trim();
|
|
67
|
+
const rawValue = trimmed.slice(equalsIndex + 1).trim();
|
|
68
|
+
if (!key || process.env[key] !== undefined) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
process.env[key] = stripQuotes(rawValue);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function stripQuotes(value: string) {
|
|
77
|
+
if (
|
|
78
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
79
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
80
|
+
) {
|
|
81
|
+
return value.slice(1, -1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Scaffold a form definition + a DB-free lint spec (definitions-as-code).
|
|
2
|
+
//
|
|
3
|
+
// npm run gen:form -- customer-feedback
|
|
4
|
+
// npm run gen:form -- customer-feedback --title "Customer feedback"
|
|
5
|
+
//
|
|
6
|
+
// Generates:
|
|
7
|
+
// src/modules/forms/definitions/<key>.form.json (starter definition)
|
|
8
|
+
// test/<key>.form.spec.ts (meta-schema + publish lint, runs in `npm test`)
|
|
9
|
+
//
|
|
10
|
+
// Load it into an organisation with:
|
|
11
|
+
// npm run forms:push -- --file src/modules/forms/definitions/<key>.form.json --org <org> --user <email> --publish
|
|
12
|
+
|
|
13
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
14
|
+
import { dirname, join } from 'path';
|
|
15
|
+
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const key = args[0];
|
|
18
|
+
if (!key || key.startsWith('--')) {
|
|
19
|
+
throw new Error('Usage: npm run gen:form -- <form-key> [--title "Display title"]');
|
|
20
|
+
}
|
|
21
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(key) || key.length > 100) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Form key "${key}" is invalid. Use lowercase letters, digits, and hyphens (must start with a letter or digit).`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const titleFlag = args.indexOf('--title');
|
|
28
|
+
const title =
|
|
29
|
+
titleFlag !== -1 && args[titleFlag + 1]
|
|
30
|
+
? args[titleFlag + 1]
|
|
31
|
+
: key
|
|
32
|
+
.split('-')
|
|
33
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
34
|
+
.join(' ');
|
|
35
|
+
|
|
36
|
+
const definitionPath = join('src', 'modules', 'forms', 'definitions', `${key}.form.json`);
|
|
37
|
+
const specPath = join('test', `${key}.form.spec.ts`);
|
|
38
|
+
|
|
39
|
+
for (const path of [definitionPath, specPath]) {
|
|
40
|
+
if (existsSync(path)) {
|
|
41
|
+
throw new Error(`Refusing to overwrite existing file: ${path}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const definition = {
|
|
46
|
+
key,
|
|
47
|
+
version: 1,
|
|
48
|
+
title,
|
|
49
|
+
fields: [
|
|
50
|
+
{
|
|
51
|
+
type: 'text',
|
|
52
|
+
name: 'title',
|
|
53
|
+
label: 'Title',
|
|
54
|
+
validators: { required: true, maxLength: 200 },
|
|
55
|
+
reportable: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'text',
|
|
59
|
+
name: 'notes',
|
|
60
|
+
label: 'Notes',
|
|
61
|
+
validators: { maxLength: 2000 },
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
actions: {
|
|
65
|
+
submit: ['validateAll', 'persist'],
|
|
66
|
+
saveDraft: ['persistDraft'],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const spec = `import { readFileSync } from 'node:fs';
|
|
71
|
+
import { join } from 'node:path';
|
|
72
|
+
import {
|
|
73
|
+
ActionRegistry,
|
|
74
|
+
DEFAULT_ORG_FORMS_POLICY,
|
|
75
|
+
DEFAULT_RULE_LIMITS,
|
|
76
|
+
DataSourceRegistry,
|
|
77
|
+
FieldTypeRegistry,
|
|
78
|
+
lintDefinition,
|
|
79
|
+
lintRules,
|
|
80
|
+
registerBuiltinActions,
|
|
81
|
+
registerCoreFieldTypes,
|
|
82
|
+
validateDefinitionShape,
|
|
83
|
+
} from '@ftisindia/form-builder';
|
|
84
|
+
import type { FormDefinition, SubmissionStore } from '@ftisindia/form-builder';
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* DB-free definition lint for ${key}.form.json — fails the unit-test run the
|
|
88
|
+
* moment the definition would be rejected at save/publish time. If your
|
|
89
|
+
* definition references custom actions or data sources, register matching
|
|
90
|
+
* stubs below (same names/kinds as the real providers).
|
|
91
|
+
*/
|
|
92
|
+
const definition = JSON.parse(
|
|
93
|
+
readFileSync(
|
|
94
|
+
join(__dirname, '..', 'src', 'modules', 'forms', 'definitions', '${key}.form.json'),
|
|
95
|
+
'utf8',
|
|
96
|
+
),
|
|
97
|
+
) as FormDefinition;
|
|
98
|
+
|
|
99
|
+
describe('${key} form definition', () => {
|
|
100
|
+
const fieldTypes = new FieldTypeRegistry();
|
|
101
|
+
registerCoreFieldTypes(fieldTypes);
|
|
102
|
+
|
|
103
|
+
const actions = new ActionRegistry();
|
|
104
|
+
registerBuiltinActions(actions, {
|
|
105
|
+
validate: () => ({ valid: true, errors: [] }),
|
|
106
|
+
submissions: {} as SubmissionStore,
|
|
107
|
+
});
|
|
108
|
+
actions.register({ name: 'sendConfirmationEmail', kind: 'post-commit', execute: async () => ({}) });
|
|
109
|
+
actions.register({
|
|
110
|
+
name: 'authenticate',
|
|
111
|
+
kind: 'transactional',
|
|
112
|
+
dangerous: true,
|
|
113
|
+
execute: async () => ({}),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('passes the engine meta-schema', () => {
|
|
117
|
+
expect(validateDefinitionShape(definition)).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('passes publish-stage linting', () => {
|
|
121
|
+
expect(
|
|
122
|
+
lintDefinition(definition, {
|
|
123
|
+
fieldTypes,
|
|
124
|
+
actions,
|
|
125
|
+
dataSources: new DataSourceRegistry(),
|
|
126
|
+
policy: { ...DEFAULT_ORG_FORMS_POLICY, allowedDangerousActions: ['authenticate'] },
|
|
127
|
+
ruleLimits: DEFAULT_RULE_LIMITS,
|
|
128
|
+
lintRules,
|
|
129
|
+
stage: 'publish',
|
|
130
|
+
canWireDangerous: true,
|
|
131
|
+
}),
|
|
132
|
+
).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
mkdirSync(dirname(definitionPath), { recursive: true });
|
|
138
|
+
writeFileSync(definitionPath, `${JSON.stringify(definition, null, 2)}\n`);
|
|
139
|
+
writeFileSync(specPath, spec);
|
|
140
|
+
|
|
141
|
+
console.log(`Created ${definitionPath}`);
|
|
142
|
+
console.log(`Created ${specPath}`);
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log('Next steps:');
|
|
145
|
+
console.log(' 1. Edit the definition (fields, rules, actions, settings).');
|
|
146
|
+
console.log(' 2. npm test # lint spec runs without a database');
|
|
147
|
+
console.log(
|
|
148
|
+
` 3. npm run forms:push -- --file ${definitionPath.replaceAll('\\', '/')} --org <org-id-or-slug> --user <owner-email> --publish`,
|
|
149
|
+
);
|