@lssm/lib.contracts-runtime-core 3.0.1
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/README.md +110 -0
- package/dist/approval.d.ts +13 -0
- package/dist/approval.js +2 -0
- package/dist/approval.test.d.ts +1 -0
- package/dist/browser/approval.js +1 -0
- package/dist/browser/collaboration.js +0 -0
- package/dist/browser/contracts-runtime-core.feature.js +1 -0
- package/dist/browser/engine/backoff.js +1 -0
- package/dist/browser/engine/cache-store.js +1 -0
- package/dist/browser/engine/conflict-registry.js +1 -0
- package/dist/browser/engine/conflict-resolution.js +1 -0
- package/dist/browser/engine/data-engine-host.js +1 -0
- package/dist/browser/engine/data-engine-test-helpers.js +1 -0
- package/dist/browser/engine/data-engine.js +1 -0
- package/dist/browser/engine/dedup.js +1 -0
- package/dist/browser/engine/events.js +1 -0
- package/dist/browser/engine/index.js +1 -0
- package/dist/browser/engine/maintenance.js +1 -0
- package/dist/browser/engine/mutation-engine.js +1 -0
- package/dist/browser/engine/mutation-queue.js +1 -0
- package/dist/browser/engine/mutation-types.js +0 -0
- package/dist/browser/engine/no-cache-store.js +1 -0
- package/dist/browser/engine/online-only-data-engine.js +1 -0
- package/dist/browser/engine/online-only-mutation-engine.js +1 -0
- package/dist/browser/engine/query-engine.js +1 -0
- package/dist/browser/engine/state.js +1 -0
- package/dist/browser/engine/types.js +0 -0
- package/dist/browser/forms-platform-csv-columns.js +1 -0
- package/dist/browser/forms-platform-csv.js +2 -0
- package/dist/browser/forms-platform-management.js +1 -0
- package/dist/browser/forms-platform-navigation.js +1 -0
- package/dist/browser/forms-platform-submission.js +1 -0
- package/dist/browser/forms-platform-types.js +1 -0
- package/dist/browser/forms-platform-utils.js +1 -0
- package/dist/browser/forms-platform.js +2 -0
- package/dist/browser/index.js +3 -0
- package/dist/browser/local-offline.js +1 -0
- package/dist/browser/observability.js +1 -0
- package/dist/browser/reachability.js +1 -0
- package/dist/browser/redaction.js +1 -0
- package/dist/browser/storage/indexeddb.web.js +1 -0
- package/dist/browser/storage/keyed-store.js +1 -0
- package/dist/browser/storage/memory.js +1 -0
- package/dist/browser/storage/port.js +1 -0
- package/dist/browser/transport.js +2 -0
- package/dist/collaboration.d.ts +57 -0
- package/dist/collaboration.js +1 -0
- package/dist/contracts-runtime-core.feature.d.ts +1 -0
- package/dist/contracts-runtime-core.feature.js +2 -0
- package/dist/engine/backoff.d.ts +16 -0
- package/dist/engine/backoff.js +2 -0
- package/dist/engine/backoff.test.d.ts +1 -0
- package/dist/engine/cache-store.d.ts +19 -0
- package/dist/engine/cache-store.js +2 -0
- package/dist/engine/conflict-registry.d.ts +10 -0
- package/dist/engine/conflict-registry.js +2 -0
- package/dist/engine/conflict-registry.test.d.ts +1 -0
- package/dist/engine/conflict-resolution.d.ts +24 -0
- package/dist/engine/conflict-resolution.js +2 -0
- package/dist/engine/conflict-resolution.test.d.ts +1 -0
- package/dist/engine/data-engine-host.d.ts +30 -0
- package/dist/engine/data-engine-host.js +2 -0
- package/dist/engine/data-engine-local-offline.test.d.ts +1 -0
- package/dist/engine/data-engine-test-helpers.d.ts +30 -0
- package/dist/engine/data-engine-test-helpers.js +2 -0
- package/dist/engine/data-engine.d.ts +46 -0
- package/dist/engine/data-engine.js +2 -0
- package/dist/engine/data-engine.test.d.ts +1 -0
- package/dist/engine/dedup.d.ts +9 -0
- package/dist/engine/dedup.js +2 -0
- package/dist/engine/events.d.ts +76 -0
- package/dist/engine/events.economic-evidence.test.d.ts +1 -0
- package/dist/engine/events.js +2 -0
- package/dist/engine/index.d.ts +17 -0
- package/dist/engine/index.js +2 -0
- package/dist/engine/maintenance.d.ts +18 -0
- package/dist/engine/maintenance.js +2 -0
- package/dist/engine/maintenance.test.d.ts +1 -0
- package/dist/engine/mutation-engine.d.ts +2 -0
- package/dist/engine/mutation-engine.js +2 -0
- package/dist/engine/mutation-queue.d.ts +16 -0
- package/dist/engine/mutation-queue.js +2 -0
- package/dist/engine/mutation-queue.test.d.ts +1 -0
- package/dist/engine/mutation-types.d.ts +118 -0
- package/dist/engine/mutation-types.js +1 -0
- package/dist/engine/no-cache-store.d.ts +3 -0
- package/dist/engine/no-cache-store.js +2 -0
- package/dist/engine/online-only-data-engine.d.ts +9 -0
- package/dist/engine/online-only-data-engine.js +2 -0
- package/dist/engine/online-only-mutation-engine.d.ts +3 -0
- package/dist/engine/online-only-mutation-engine.js +2 -0
- package/dist/engine/query-engine.d.ts +7 -0
- package/dist/engine/query-engine.js +2 -0
- package/dist/engine/query-engine.test.d.ts +1 -0
- package/dist/engine/state.d.ts +37 -0
- package/dist/engine/state.js +2 -0
- package/dist/engine/types.d.ts +70 -0
- package/dist/engine/types.js +1 -0
- package/dist/forms-platform-choice.test.d.ts +1 -0
- package/dist/forms-platform-csv-columns.d.ts +7 -0
- package/dist/forms-platform-csv-columns.js +2 -0
- package/dist/forms-platform-csv.d.ts +12 -0
- package/dist/forms-platform-csv.js +3 -0
- package/dist/forms-platform-management.d.ts +12 -0
- package/dist/forms-platform-management.js +2 -0
- package/dist/forms-platform-navigation.d.ts +2 -0
- package/dist/forms-platform-navigation.js +2 -0
- package/dist/forms-platform-submission.d.ts +7 -0
- package/dist/forms-platform-submission.js +2 -0
- package/dist/forms-platform-types.d.ts +51 -0
- package/dist/forms-platform-types.js +2 -0
- package/dist/forms-platform-utils.d.ts +6 -0
- package/dist/forms-platform-utils.js +2 -0
- package/dist/forms-platform-utils.test.d.ts +1 -0
- package/dist/forms-platform.d.ts +19 -0
- package/dist/forms-platform.js +3 -0
- package/dist/forms-platform.test.d.ts +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +4 -0
- package/dist/local-offline.d.ts +17 -0
- package/dist/local-offline.js +2 -0
- package/dist/local-offline.test.d.ts +1 -0
- package/dist/native/approval.js +130 -0
- package/dist/native/collaboration.js +0 -0
- package/dist/native/contracts-runtime-core.feature.js +17 -0
- package/dist/native/engine/backoff.js +16 -0
- package/dist/native/engine/cache-store.js +49 -0
- package/dist/native/engine/conflict-registry.js +51 -0
- package/dist/native/engine/conflict-resolution.js +81 -0
- package/dist/native/engine/data-engine-host.js +1119 -0
- package/dist/native/engine/data-engine-test-helpers.js +117 -0
- package/dist/native/engine/data-engine.js +1106 -0
- package/dist/native/engine/dedup.js +22 -0
- package/dist/native/engine/events.js +19 -0
- package/dist/native/engine/index.js +1189 -0
- package/dist/native/engine/maintenance.js +36 -0
- package/dist/native/engine/mutation-engine.js +360 -0
- package/dist/native/engine/mutation-queue.js +26 -0
- package/dist/native/engine/mutation-types.js +0 -0
- package/dist/native/engine/no-cache-store.js +15 -0
- package/dist/native/engine/online-only-data-engine.js +422 -0
- package/dist/native/engine/online-only-mutation-engine.js +70 -0
- package/dist/native/engine/query-engine.js +360 -0
- package/dist/native/engine/state.js +116 -0
- package/dist/native/engine/types.js +0 -0
- package/dist/native/forms-platform-csv-columns.js +31 -0
- package/dist/native/forms-platform-csv.js +150 -0
- package/dist/native/forms-platform-management.js +179 -0
- package/dist/native/forms-platform-navigation.js +27 -0
- package/dist/native/forms-platform-submission.js +207 -0
- package/dist/native/forms-platform-types.js +12 -0
- package/dist/native/forms-platform-utils.js +69 -0
- package/dist/native/forms-platform.js +429 -0
- package/dist/native/index.js +1879 -0
- package/dist/native/local-offline.js +81 -0
- package/dist/native/observability.js +182 -0
- package/dist/native/reachability.js +28 -0
- package/dist/native/redaction.js +44 -0
- package/dist/native/storage/async-storage.native.js +91 -0
- package/dist/native/storage/keyed-store.js +74 -0
- package/dist/native/storage/memory.js +26 -0
- package/dist/native/storage/port.js +16 -0
- package/dist/native/transport.js +27 -0
- package/dist/node/approval.js +1 -0
- package/dist/node/collaboration.js +0 -0
- package/dist/node/contracts-runtime-core.feature.js +1 -0
- package/dist/node/engine/backoff.js +1 -0
- package/dist/node/engine/cache-store.js +1 -0
- package/dist/node/engine/conflict-registry.js +1 -0
- package/dist/node/engine/conflict-resolution.js +1 -0
- package/dist/node/engine/data-engine-host.js +1 -0
- package/dist/node/engine/data-engine-test-helpers.js +1 -0
- package/dist/node/engine/data-engine.js +1 -0
- package/dist/node/engine/dedup.js +1 -0
- package/dist/node/engine/events.js +1 -0
- package/dist/node/engine/index.js +1 -0
- package/dist/node/engine/maintenance.js +1 -0
- package/dist/node/engine/mutation-engine.js +1 -0
- package/dist/node/engine/mutation-queue.js +1 -0
- package/dist/node/engine/mutation-types.js +0 -0
- package/dist/node/engine/no-cache-store.js +1 -0
- package/dist/node/engine/online-only-data-engine.js +1 -0
- package/dist/node/engine/online-only-mutation-engine.js +1 -0
- package/dist/node/engine/query-engine.js +1 -0
- package/dist/node/engine/state.js +1 -0
- package/dist/node/engine/types.js +0 -0
- package/dist/node/forms-platform-csv-columns.js +1 -0
- package/dist/node/forms-platform-csv.js +2 -0
- package/dist/node/forms-platform-management.js +1 -0
- package/dist/node/forms-platform-navigation.js +1 -0
- package/dist/node/forms-platform-submission.js +1 -0
- package/dist/node/forms-platform-types.js +1 -0
- package/dist/node/forms-platform-utils.js +1 -0
- package/dist/node/forms-platform.js +2 -0
- package/dist/node/index.js +3 -0
- package/dist/node/local-offline.js +1 -0
- package/dist/node/observability.js +1 -0
- package/dist/node/reachability.js +1 -0
- package/dist/node/redaction.js +1 -0
- package/dist/node/storage/keyed-store.js +1 -0
- package/dist/node/storage/memory.js +1 -0
- package/dist/node/storage/port.js +1 -0
- package/dist/node/transport.js +2 -0
- package/dist/observability.d.ts +34 -0
- package/dist/observability.js +2 -0
- package/dist/observability.test.d.ts +1 -0
- package/dist/reachability.d.ts +63 -0
- package/dist/reachability.js +2 -0
- package/dist/reachability.test.d.ts +1 -0
- package/dist/redaction.d.ts +34 -0
- package/dist/redaction.js +2 -0
- package/dist/redaction.test.d.ts +1 -0
- package/dist/storage/async-storage.native.d.ts +23 -0
- package/dist/storage/async-storage.native.test.d.ts +1 -0
- package/dist/storage/indexeddb.web.d.ts +13 -0
- package/dist/storage/indexeddb.web.js +2 -0
- package/dist/storage/keyed-store.d.ts +25 -0
- package/dist/storage/keyed-store.js +2 -0
- package/dist/storage/keyed-store.test.d.ts +1 -0
- package/dist/storage/memory.d.ts +11 -0
- package/dist/storage/memory.js +2 -0
- package/dist/storage/memory.test.d.ts +1 -0
- package/dist/storage/port.d.ts +34 -0
- package/dist/storage/port.js +2 -0
- package/dist/transport.d.ts +53 -0
- package/dist/transport.js +3 -0
- package/package.json +219 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
// src/forms-platform-types.ts
|
|
2
|
+
class FormsPlatformError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = "FormsPlatformError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/forms-platform-utils.ts
|
|
12
|
+
import {
|
|
13
|
+
compileFormDocument,
|
|
14
|
+
validateFormDocument
|
|
15
|
+
} from "@lssm/lib.contracts-spec/forms";
|
|
16
|
+
function requireFormDefinition(definition, scope) {
|
|
17
|
+
const matches = definition?.owner.product === scope.product && definition.owner.tenantRef === scope.tenantRef && definition.owner.workspaceRef === scope.workspaceRef && definition.owner.ownerRef === scope.ownerRef;
|
|
18
|
+
if (!definition || !matches) {
|
|
19
|
+
throw new FormsPlatformError("FORM_NOT_FOUND", "Form definition was not found in the caller scope");
|
|
20
|
+
}
|
|
21
|
+
return definition;
|
|
22
|
+
}
|
|
23
|
+
function validateRuntimeFormDocument(document) {
|
|
24
|
+
const issues = validateFormDocument(document);
|
|
25
|
+
if (issues.length) {
|
|
26
|
+
throw new FormsPlatformError("INVALID_FORM_DOCUMENT", issues.map((issue) => issue.message).join("; "));
|
|
27
|
+
}
|
|
28
|
+
compileFormDocument(document);
|
|
29
|
+
}
|
|
30
|
+
function formAssuranceMeetsRequirement(assurance, requirement) {
|
|
31
|
+
if (requirement === "none")
|
|
32
|
+
return true;
|
|
33
|
+
if (requirement === "either")
|
|
34
|
+
return assurance !== "anonymous";
|
|
35
|
+
if (requirement === "both")
|
|
36
|
+
return assurance === "both";
|
|
37
|
+
return assurance === requirement || assurance === "both";
|
|
38
|
+
}
|
|
39
|
+
function resolveFormResponseStatus(document, assurance) {
|
|
40
|
+
if (formAssuranceMeetsRequirement(assurance, document.verification.requirement)) {
|
|
41
|
+
return "completed";
|
|
42
|
+
}
|
|
43
|
+
return document.verification.unverifiedDisposition === "pending-hidden" ? "pending-verification" : "completed-unverified";
|
|
44
|
+
}
|
|
45
|
+
function resolveFormLocale(document, candidates) {
|
|
46
|
+
const supported = new Set(document.content.enabledLocales.map((locale) => locale.toLowerCase()));
|
|
47
|
+
for (const candidate of candidates) {
|
|
48
|
+
if (!candidate)
|
|
49
|
+
continue;
|
|
50
|
+
const normalized = candidate.trim().toLowerCase();
|
|
51
|
+
const exact = document.content.enabledLocales.find((locale) => locale.toLowerCase() === normalized);
|
|
52
|
+
if (supported.has(normalized) && exact)
|
|
53
|
+
return exact;
|
|
54
|
+
const language = normalized.split("-")[0];
|
|
55
|
+
const match = document.content.enabledLocales.find((locale) => locale.toLowerCase().split("-")[0] === language);
|
|
56
|
+
if (match)
|
|
57
|
+
return match;
|
|
58
|
+
}
|
|
59
|
+
return document.content.defaultLocale;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/forms-platform-management.ts
|
|
63
|
+
function createFormsManagementService(dependencies) {
|
|
64
|
+
const emit = async (key, payload) => dependencies.emit?.(key, payload);
|
|
65
|
+
return {
|
|
66
|
+
async create(scope, document) {
|
|
67
|
+
validateRuntimeFormDocument(document);
|
|
68
|
+
const now = dependencies.now();
|
|
69
|
+
const definition = {
|
|
70
|
+
id: dependencies.id("definition"),
|
|
71
|
+
owner: scope,
|
|
72
|
+
status: "draft",
|
|
73
|
+
draft: document,
|
|
74
|
+
draftVersion: 1,
|
|
75
|
+
createdAt: now,
|
|
76
|
+
updatedAt: now
|
|
77
|
+
};
|
|
78
|
+
await dependencies.store.insertDefinition(definition);
|
|
79
|
+
await emit("forms.definition.created", {
|
|
80
|
+
definitionId: definition.id,
|
|
81
|
+
occurredAt: now
|
|
82
|
+
});
|
|
83
|
+
return definition;
|
|
84
|
+
},
|
|
85
|
+
async saveDraft(scope, definitionId, expectedVersion, document) {
|
|
86
|
+
validateRuntimeFormDocument(document);
|
|
87
|
+
const current = requireFormDefinition(await dependencies.store.getDefinition(scope, definitionId), scope);
|
|
88
|
+
if (current.status === "archived") {
|
|
89
|
+
throw new FormsPlatformError("FORM_ARCHIVED", "Archived forms cannot be edited");
|
|
90
|
+
}
|
|
91
|
+
const next = {
|
|
92
|
+
...current,
|
|
93
|
+
draft: document,
|
|
94
|
+
draftVersion: expectedVersion + 1,
|
|
95
|
+
updatedAt: dependencies.now()
|
|
96
|
+
};
|
|
97
|
+
if (!await dependencies.store.updateDefinition(next, expectedVersion)) {
|
|
98
|
+
throw new FormsPlatformError("DRAFT_CONFLICT", "The form draft changed; reload before saving");
|
|
99
|
+
}
|
|
100
|
+
await emit("forms.draft.saved", {
|
|
101
|
+
definitionId,
|
|
102
|
+
occurredAt: next.updatedAt
|
|
103
|
+
});
|
|
104
|
+
return next;
|
|
105
|
+
},
|
|
106
|
+
async publish(scope, definitionId, expectedVersion) {
|
|
107
|
+
return dependencies.store.transaction(async (store) => {
|
|
108
|
+
const current = requireFormDefinition(await store.getDefinition(scope, definitionId), scope);
|
|
109
|
+
if (current.draftVersion !== expectedVersion) {
|
|
110
|
+
throw new FormsPlatformError("DRAFT_CONFLICT", "The form draft changed before publish");
|
|
111
|
+
}
|
|
112
|
+
validateRuntimeFormDocument(current.draft);
|
|
113
|
+
const previous = current.publishedRevisionId ? await store.getRevision(current.publishedRevisionId) : undefined;
|
|
114
|
+
const publishedAt = dependencies.now();
|
|
115
|
+
const revision = {
|
|
116
|
+
id: dependencies.id("revision"),
|
|
117
|
+
definitionId,
|
|
118
|
+
revision: (previous?.revision ?? 0) + 1,
|
|
119
|
+
document: structuredClone(current.draft),
|
|
120
|
+
digest: await dependencies.digest(JSON.stringify(current.draft)),
|
|
121
|
+
publishedAt
|
|
122
|
+
};
|
|
123
|
+
const definition = {
|
|
124
|
+
...current,
|
|
125
|
+
status: "published",
|
|
126
|
+
publishedRevisionId: revision.id,
|
|
127
|
+
publicToken: current.publicToken ?? dependencies.publicToken(),
|
|
128
|
+
updatedAt: publishedAt
|
|
129
|
+
};
|
|
130
|
+
await store.insertRevision(revision);
|
|
131
|
+
if (!await store.updateDefinition(definition, expectedVersion)) {
|
|
132
|
+
throw new FormsPlatformError("DRAFT_CONFLICT", "The form changed during publish");
|
|
133
|
+
}
|
|
134
|
+
await emit("forms.definition.published", {
|
|
135
|
+
definitionId,
|
|
136
|
+
revisionId: revision.id,
|
|
137
|
+
occurredAt: publishedAt
|
|
138
|
+
});
|
|
139
|
+
return { definition, revision };
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
async close(scope, definitionId) {
|
|
143
|
+
const current = requireFormDefinition(await dependencies.store.getDefinition(scope, definitionId), scope);
|
|
144
|
+
if (current.status !== "published") {
|
|
145
|
+
throw new FormsPlatformError("FORM_NOT_PUBLISHED", "Only a published form can be closed");
|
|
146
|
+
}
|
|
147
|
+
const next = {
|
|
148
|
+
...current,
|
|
149
|
+
status: "closed",
|
|
150
|
+
updatedAt: dependencies.now()
|
|
151
|
+
};
|
|
152
|
+
await dependencies.store.updateDefinition(next);
|
|
153
|
+
await emit("forms.definition.closed", {
|
|
154
|
+
definitionId,
|
|
155
|
+
occurredAt: next.updatedAt
|
|
156
|
+
});
|
|
157
|
+
return next;
|
|
158
|
+
},
|
|
159
|
+
async archive(scope, definitionId) {
|
|
160
|
+
const current = requireFormDefinition(await dependencies.store.getDefinition(scope, definitionId), scope);
|
|
161
|
+
if (current.status === "archived")
|
|
162
|
+
return current;
|
|
163
|
+
const next = {
|
|
164
|
+
...current,
|
|
165
|
+
status: "archived",
|
|
166
|
+
updatedAt: dependencies.now()
|
|
167
|
+
};
|
|
168
|
+
await dependencies.store.updateDefinition(next);
|
|
169
|
+
await emit("forms.definition.archived", {
|
|
170
|
+
definitionId,
|
|
171
|
+
occurredAt: next.updatedAt
|
|
172
|
+
});
|
|
173
|
+
return next;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/forms-platform-submission.ts
|
|
179
|
+
import {
|
|
180
|
+
buildZodWithRelations,
|
|
181
|
+
compileFormDocument as compileFormDocument2,
|
|
182
|
+
FORM_AI_FULL_DATA_CONSENT_NOTICE_VERSION
|
|
183
|
+
} from "@lssm/lib.contracts-spec/forms";
|
|
184
|
+
function validateCanonicalFormAnswers(document, answers) {
|
|
185
|
+
const questionsById = new Map(document.questions.map((question) => [question.id, question]));
|
|
186
|
+
const portableFieldNames = new Set(Object.keys(document.formSpec?.modelFields ?? {}));
|
|
187
|
+
const unknownIds = Object.keys(answers).filter((id) => !questionsById.has(id) && !portableFieldNames.has(id));
|
|
188
|
+
if (unknownIds.length) {
|
|
189
|
+
throw new FormsPlatformError("INVALID_RESPONSE", `Unknown question IDs: ${unknownIds.join(", ")}`);
|
|
190
|
+
}
|
|
191
|
+
for (const question of document.questions) {
|
|
192
|
+
if (!["single-choice", "multi-select", "dropdown"].includes(question.kind))
|
|
193
|
+
continue;
|
|
194
|
+
const value = answers[question.id];
|
|
195
|
+
if (value == null)
|
|
196
|
+
continue;
|
|
197
|
+
const allowed = new Set(question.options?.map((option) => option.value));
|
|
198
|
+
const valid = question.kind === "multi-select" ? Array.isArray(value) && value.every((item) => typeof item === "string" && allowed.has(item)) : typeof value === "string" && allowed.has(value);
|
|
199
|
+
if (!valid)
|
|
200
|
+
throw new FormsPlatformError("INVALID_RESPONSE", `Answer for ${question.id} is not a declared option`);
|
|
201
|
+
}
|
|
202
|
+
const runtimeAnswers = Object.fromEntries(Object.entries(answers).map(([id, value]) => [
|
|
203
|
+
questionsById.get(id)?.name ?? id,
|
|
204
|
+
value
|
|
205
|
+
]));
|
|
206
|
+
const result = buildZodWithRelations(compileFormDocument2(document)).safeParse(runtimeAnswers);
|
|
207
|
+
if (!result.success) {
|
|
208
|
+
throw new FormsPlatformError("INVALID_RESPONSE", `Response does not match the published form: ${String(result.error)}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function publicDocument(document) {
|
|
212
|
+
const {
|
|
213
|
+
kind,
|
|
214
|
+
version,
|
|
215
|
+
id,
|
|
216
|
+
titleKey,
|
|
217
|
+
descriptionKey,
|
|
218
|
+
content,
|
|
219
|
+
questions,
|
|
220
|
+
pages,
|
|
221
|
+
verification,
|
|
222
|
+
formSpec,
|
|
223
|
+
ai,
|
|
224
|
+
beforeDestination,
|
|
225
|
+
afterDestination
|
|
226
|
+
} = document;
|
|
227
|
+
return structuredClone({
|
|
228
|
+
kind,
|
|
229
|
+
version,
|
|
230
|
+
id,
|
|
231
|
+
titleKey,
|
|
232
|
+
descriptionKey,
|
|
233
|
+
content,
|
|
234
|
+
questions,
|
|
235
|
+
pages,
|
|
236
|
+
verification,
|
|
237
|
+
formSpec,
|
|
238
|
+
ai: {
|
|
239
|
+
mode: ai.mode,
|
|
240
|
+
dataMode: ai.dataMode,
|
|
241
|
+
...ai.dataMode === "full-with-consent" ? {
|
|
242
|
+
consentNoticeVersion: FORM_AI_FULL_DATA_CONSENT_NOTICE_VERSION
|
|
243
|
+
} : {}
|
|
244
|
+
},
|
|
245
|
+
beforeDestination,
|
|
246
|
+
afterDestination
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
async function resolveAssurance(dependencies, evidenceRef, definitionId, publicToken) {
|
|
250
|
+
if (!evidenceRef)
|
|
251
|
+
return "anonymous";
|
|
252
|
+
if (!dependencies.resolveAssurance) {
|
|
253
|
+
throw new FormsPlatformError("VERIFICATION_UNAVAILABLE", "Verification evidence cannot be resolved");
|
|
254
|
+
}
|
|
255
|
+
return dependencies.resolveAssurance(evidenceRef, definitionId, publicToken);
|
|
256
|
+
}
|
|
257
|
+
function createFormsSubmissionService(dependencies) {
|
|
258
|
+
return {
|
|
259
|
+
async getPublic(publicToken, localeCandidates) {
|
|
260
|
+
const definition = await dependencies.store.getDefinitionByPublicToken(publicToken);
|
|
261
|
+
if (!definition || definition.status !== "published" || !definition.publishedRevisionId) {
|
|
262
|
+
throw new FormsPlatformError("FORM_NOT_FOUND", "Published form was not found");
|
|
263
|
+
}
|
|
264
|
+
const revision = await dependencies.store.getRevision(definition.publishedRevisionId, { publicToken });
|
|
265
|
+
if (!revision) {
|
|
266
|
+
throw new FormsPlatformError("REVISION_NOT_FOUND", "Published form revision was not found");
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
definitionId: definition.id,
|
|
270
|
+
revisionId: revision.id,
|
|
271
|
+
revision: revision.revision,
|
|
272
|
+
document: publicDocument(revision.document),
|
|
273
|
+
locale: resolveFormLocale(revision.document, localeCandidates)
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
async submit(input) {
|
|
277
|
+
const definition = await dependencies.store.getDefinitionByPublicToken(input.publicToken);
|
|
278
|
+
if (!definition || definition.status !== "published") {
|
|
279
|
+
throw new FormsPlatformError("FORM_NOT_FOUND", "Form is not accepting responses");
|
|
280
|
+
}
|
|
281
|
+
const revision = await dependencies.store.getRevision(input.revisionId, {
|
|
282
|
+
publicToken: input.publicToken
|
|
283
|
+
});
|
|
284
|
+
if (!revision || revision.definitionId !== definition.id) {
|
|
285
|
+
throw new FormsPlatformError("REVISION_MISMATCH", "Response revision does not match the public session");
|
|
286
|
+
}
|
|
287
|
+
validateCanonicalFormAnswers(revision.document, input.answers);
|
|
288
|
+
const assurance = await resolveAssurance(dependencies, input.verificationEvidenceRef, definition.id, input.publicToken);
|
|
289
|
+
if (revision.document.verification.timing === "before" && !formAssuranceMeetsRequirement(assurance, revision.document.verification.requirement)) {
|
|
290
|
+
throw new FormsPlatformError("VERIFICATION_REQUIRED", "Verification is required before submission");
|
|
291
|
+
}
|
|
292
|
+
const now = dependencies.now();
|
|
293
|
+
const response = {
|
|
294
|
+
id: dependencies.id("response"),
|
|
295
|
+
definitionId: definition.id,
|
|
296
|
+
revisionId: revision.id,
|
|
297
|
+
locale: resolveFormLocale(revision.document, [input.locale]),
|
|
298
|
+
status: resolveFormResponseStatus(revision.document, assurance),
|
|
299
|
+
authorAssurance: assurance,
|
|
300
|
+
answers: structuredClone(input.answers),
|
|
301
|
+
createdAt: now,
|
|
302
|
+
updatedAt: now,
|
|
303
|
+
submittedAt: now
|
|
304
|
+
};
|
|
305
|
+
const stored = await dependencies.store.insertResponseIdempotently(response, input.publicToken, input.idempotencyKey);
|
|
306
|
+
if (!stored.inserted)
|
|
307
|
+
return stored.response;
|
|
308
|
+
await dependencies.emit?.("forms.response.submitted", {
|
|
309
|
+
definitionId: response.definitionId,
|
|
310
|
+
revisionId: response.revisionId,
|
|
311
|
+
responseId: response.id,
|
|
312
|
+
occurredAt: now
|
|
313
|
+
});
|
|
314
|
+
return stored.response;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/forms-platform-csv-columns.ts
|
|
320
|
+
import { flattenPortableFormFields } from "@lssm/lib.contracts-spec/forms";
|
|
321
|
+
function formCsvColumns(document) {
|
|
322
|
+
const questionNames = new Set(document.questions.map(({ name }) => name));
|
|
323
|
+
const questions = document.questions.map(questionColumn);
|
|
324
|
+
const portableSensitive = new Set(document.formSpec?.policy?.pii ?? []);
|
|
325
|
+
const allFields = flattenPortableFormFields(document.formSpec?.fields ?? []);
|
|
326
|
+
const portable = Object.entries(document.formSpec?.modelFields ?? {}).flatMap(([name, modelField]) => {
|
|
327
|
+
if (questionNames.has(name))
|
|
328
|
+
return [];
|
|
329
|
+
const field = allFields.find((candidate) => candidate.name === name);
|
|
330
|
+
return [
|
|
331
|
+
{
|
|
332
|
+
id: name,
|
|
333
|
+
labelKey: field?.labelI18n ?? name,
|
|
334
|
+
sensitive: portableSensitive.has(name) || [...portableSensitive].some((path) => path.startsWith(`${name}.`)) || field?.kind === "email" || field?.kind === "phone" || modelField.type === "email" || modelField.type === "phone"
|
|
335
|
+
}
|
|
336
|
+
];
|
|
337
|
+
});
|
|
338
|
+
return [...questions, ...portable];
|
|
339
|
+
}
|
|
340
|
+
function questionColumn(question) {
|
|
341
|
+
return {
|
|
342
|
+
id: question.id,
|
|
343
|
+
labelKey: question.labelKey,
|
|
344
|
+
sensitive: question.sensitive === true || question.kind === "email" || question.kind === "phone"
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/forms-platform-csv.ts
|
|
349
|
+
var RESPONSE_EXTENSIONS_COLUMN = "response_extensions:AI follow-ups";
|
|
350
|
+
function csvCell(value) {
|
|
351
|
+
const raw = Array.isArray(value) ? value.every((item) => typeof item !== "object") ? value.join(";") : JSON.stringify(value) : value == null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
352
|
+
const protectedValue = /^[\s]*[=+\-@]/.test(raw) ? `'${raw}` : raw;
|
|
353
|
+
return `"${protectedValue.replaceAll('"', '""')}"`;
|
|
354
|
+
}
|
|
355
|
+
function csvRow(values) {
|
|
356
|
+
return `${values.map(csvCell).join(",")}\r
|
|
357
|
+
`;
|
|
358
|
+
}
|
|
359
|
+
function responseRow(revision, response, options) {
|
|
360
|
+
if (response.revisionId !== revision.id) {
|
|
361
|
+
throw new Error("CSV responses must belong to the exported revision");
|
|
362
|
+
}
|
|
363
|
+
const columns = formCsvColumns(revision.document);
|
|
364
|
+
const baseIds = new Set(columns.map(({ id }) => id));
|
|
365
|
+
const extensions = Object.fromEntries(Object.entries(response.answers).filter(([id]) => !baseIds.has(id)));
|
|
366
|
+
return [
|
|
367
|
+
response.id,
|
|
368
|
+
response.status,
|
|
369
|
+
response.locale,
|
|
370
|
+
response.submittedAt ?? "",
|
|
371
|
+
...columns.map((column) => {
|
|
372
|
+
const mayIncludeSensitive = options.includeSensitive && options.sensitiveGrant?.authorized;
|
|
373
|
+
if (!mayIncludeSensitive && column.sensitive) {
|
|
374
|
+
return "[REDACTED]";
|
|
375
|
+
}
|
|
376
|
+
return response.answers[column.id];
|
|
377
|
+
}),
|
|
378
|
+
options.includeSensitive && options.sensitiveGrant?.authorized ? extensions : "[REDACTED]"
|
|
379
|
+
];
|
|
380
|
+
}
|
|
381
|
+
function csvHeaders(revision, locale) {
|
|
382
|
+
const resolved = resolveFormLocale(revision.document, [locale]);
|
|
383
|
+
const catalog = revision.document.content.catalogs[resolved] ?? {};
|
|
384
|
+
return [
|
|
385
|
+
"response_id",
|
|
386
|
+
"status",
|
|
387
|
+
"locale",
|
|
388
|
+
"submitted_at",
|
|
389
|
+
...formCsvColumns(revision.document).map((column) => `${column.id}:${catalog[column.labelKey] ?? column.labelKey}`),
|
|
390
|
+
RESPONSE_EXTENSIONS_COLUMN
|
|
391
|
+
];
|
|
392
|
+
}
|
|
393
|
+
function serializeFormResponsesCsv(revision, responses, options = {}) {
|
|
394
|
+
return `\uFEFF${csvRow(csvHeaders(revision, options.locale))}${responses.map((response) => csvRow(responseRow(revision, response, options))).join("")}`;
|
|
395
|
+
}
|
|
396
|
+
async function* streamFormResponsesCsv(revision, responses, options = {}) {
|
|
397
|
+
yield `\uFEFF${csvRow(csvHeaders(revision, options.locale))}`;
|
|
398
|
+
for await (const response of responses) {
|
|
399
|
+
yield csvRow(responseRow(revision, response, options));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// src/forms-platform-navigation.ts
|
|
403
|
+
import {
|
|
404
|
+
evalPredicate
|
|
405
|
+
} from "@lssm/lib.contracts-spec/forms";
|
|
406
|
+
function resolveNextFormPage(document, currentPageId, answers) {
|
|
407
|
+
const currentIndex = document.pages.findIndex((page) => page.id === currentPageId);
|
|
408
|
+
if (currentIndex < 0) {
|
|
409
|
+
throw new FormsPlatformError("PAGE_NOT_FOUND", "Current form page does not exist");
|
|
410
|
+
}
|
|
411
|
+
const rule = document.pages[currentIndex]?.nextPageRules?.find((candidate) => evalPredicate(answers, candidate.when));
|
|
412
|
+
return rule?.nextPageId ?? document.pages[currentIndex]?.defaultNextPageId ?? document.pages[currentIndex + 1]?.id;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/forms-platform.ts
|
|
416
|
+
function createFormsPlatformService(dependencies) {
|
|
417
|
+
return {
|
|
418
|
+
...createFormsManagementService(dependencies),
|
|
419
|
+
...createFormsSubmissionService(dependencies)
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
export {
|
|
423
|
+
FormsPlatformError,
|
|
424
|
+
createFormsPlatformService,
|
|
425
|
+
resolveFormLocale,
|
|
426
|
+
resolveNextFormPage,
|
|
427
|
+
serializeFormResponsesCsv,
|
|
428
|
+
streamFormResponsesCsv
|
|
429
|
+
};
|