@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,179 @@
|
|
|
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
|
+
export {
|
|
178
|
+
createFormsManagementService
|
|
179
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/forms-platform-navigation.ts
|
|
2
|
+
import {
|
|
3
|
+
evalPredicate
|
|
4
|
+
} from "@lssm/lib.contracts-spec/forms";
|
|
5
|
+
|
|
6
|
+
// src/forms-platform-types.ts
|
|
7
|
+
class FormsPlatformError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.name = "FormsPlatformError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/forms-platform-navigation.ts
|
|
17
|
+
function resolveNextFormPage(document, currentPageId, answers) {
|
|
18
|
+
const currentIndex = document.pages.findIndex((page) => page.id === currentPageId);
|
|
19
|
+
if (currentIndex < 0) {
|
|
20
|
+
throw new FormsPlatformError("PAGE_NOT_FOUND", "Current form page does not exist");
|
|
21
|
+
}
|
|
22
|
+
const rule = document.pages[currentIndex]?.nextPageRules?.find((candidate) => evalPredicate(answers, candidate.when));
|
|
23
|
+
return rule?.nextPageId ?? document.pages[currentIndex]?.defaultNextPageId ?? document.pages[currentIndex + 1]?.id;
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
resolveNextFormPage
|
|
27
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// src/forms-platform-submission.ts
|
|
2
|
+
import {
|
|
3
|
+
buildZodWithRelations,
|
|
4
|
+
compileFormDocument as compileFormDocument2,
|
|
5
|
+
FORM_AI_FULL_DATA_CONSENT_NOTICE_VERSION
|
|
6
|
+
} from "@lssm/lib.contracts-spec/forms";
|
|
7
|
+
|
|
8
|
+
// src/forms-platform-types.ts
|
|
9
|
+
class FormsPlatformError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
constructor(code, message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.name = "FormsPlatformError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/forms-platform-utils.ts
|
|
19
|
+
import {
|
|
20
|
+
compileFormDocument,
|
|
21
|
+
validateFormDocument
|
|
22
|
+
} from "@lssm/lib.contracts-spec/forms";
|
|
23
|
+
function requireFormDefinition(definition, scope) {
|
|
24
|
+
const matches = definition?.owner.product === scope.product && definition.owner.tenantRef === scope.tenantRef && definition.owner.workspaceRef === scope.workspaceRef && definition.owner.ownerRef === scope.ownerRef;
|
|
25
|
+
if (!definition || !matches) {
|
|
26
|
+
throw new FormsPlatformError("FORM_NOT_FOUND", "Form definition was not found in the caller scope");
|
|
27
|
+
}
|
|
28
|
+
return definition;
|
|
29
|
+
}
|
|
30
|
+
function validateRuntimeFormDocument(document) {
|
|
31
|
+
const issues = validateFormDocument(document);
|
|
32
|
+
if (issues.length) {
|
|
33
|
+
throw new FormsPlatformError("INVALID_FORM_DOCUMENT", issues.map((issue) => issue.message).join("; "));
|
|
34
|
+
}
|
|
35
|
+
compileFormDocument(document);
|
|
36
|
+
}
|
|
37
|
+
function formAssuranceMeetsRequirement(assurance, requirement) {
|
|
38
|
+
if (requirement === "none")
|
|
39
|
+
return true;
|
|
40
|
+
if (requirement === "either")
|
|
41
|
+
return assurance !== "anonymous";
|
|
42
|
+
if (requirement === "both")
|
|
43
|
+
return assurance === "both";
|
|
44
|
+
return assurance === requirement || assurance === "both";
|
|
45
|
+
}
|
|
46
|
+
function resolveFormResponseStatus(document, assurance) {
|
|
47
|
+
if (formAssuranceMeetsRequirement(assurance, document.verification.requirement)) {
|
|
48
|
+
return "completed";
|
|
49
|
+
}
|
|
50
|
+
return document.verification.unverifiedDisposition === "pending-hidden" ? "pending-verification" : "completed-unverified";
|
|
51
|
+
}
|
|
52
|
+
function resolveFormLocale(document, candidates) {
|
|
53
|
+
const supported = new Set(document.content.enabledLocales.map((locale) => locale.toLowerCase()));
|
|
54
|
+
for (const candidate of candidates) {
|
|
55
|
+
if (!candidate)
|
|
56
|
+
continue;
|
|
57
|
+
const normalized = candidate.trim().toLowerCase();
|
|
58
|
+
const exact = document.content.enabledLocales.find((locale) => locale.toLowerCase() === normalized);
|
|
59
|
+
if (supported.has(normalized) && exact)
|
|
60
|
+
return exact;
|
|
61
|
+
const language = normalized.split("-")[0];
|
|
62
|
+
const match = document.content.enabledLocales.find((locale) => locale.toLowerCase().split("-")[0] === language);
|
|
63
|
+
if (match)
|
|
64
|
+
return match;
|
|
65
|
+
}
|
|
66
|
+
return document.content.defaultLocale;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/forms-platform-submission.ts
|
|
70
|
+
function validateCanonicalFormAnswers(document, answers) {
|
|
71
|
+
const questionsById = new Map(document.questions.map((question) => [question.id, question]));
|
|
72
|
+
const portableFieldNames = new Set(Object.keys(document.formSpec?.modelFields ?? {}));
|
|
73
|
+
const unknownIds = Object.keys(answers).filter((id) => !questionsById.has(id) && !portableFieldNames.has(id));
|
|
74
|
+
if (unknownIds.length) {
|
|
75
|
+
throw new FormsPlatformError("INVALID_RESPONSE", `Unknown question IDs: ${unknownIds.join(", ")}`);
|
|
76
|
+
}
|
|
77
|
+
for (const question of document.questions) {
|
|
78
|
+
if (!["single-choice", "multi-select", "dropdown"].includes(question.kind))
|
|
79
|
+
continue;
|
|
80
|
+
const value = answers[question.id];
|
|
81
|
+
if (value == null)
|
|
82
|
+
continue;
|
|
83
|
+
const allowed = new Set(question.options?.map((option) => option.value));
|
|
84
|
+
const valid = question.kind === "multi-select" ? Array.isArray(value) && value.every((item) => typeof item === "string" && allowed.has(item)) : typeof value === "string" && allowed.has(value);
|
|
85
|
+
if (!valid)
|
|
86
|
+
throw new FormsPlatformError("INVALID_RESPONSE", `Answer for ${question.id} is not a declared option`);
|
|
87
|
+
}
|
|
88
|
+
const runtimeAnswers = Object.fromEntries(Object.entries(answers).map(([id, value]) => [
|
|
89
|
+
questionsById.get(id)?.name ?? id,
|
|
90
|
+
value
|
|
91
|
+
]));
|
|
92
|
+
const result = buildZodWithRelations(compileFormDocument2(document)).safeParse(runtimeAnswers);
|
|
93
|
+
if (!result.success) {
|
|
94
|
+
throw new FormsPlatformError("INVALID_RESPONSE", `Response does not match the published form: ${String(result.error)}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function publicDocument(document) {
|
|
98
|
+
const {
|
|
99
|
+
kind,
|
|
100
|
+
version,
|
|
101
|
+
id,
|
|
102
|
+
titleKey,
|
|
103
|
+
descriptionKey,
|
|
104
|
+
content,
|
|
105
|
+
questions,
|
|
106
|
+
pages,
|
|
107
|
+
verification,
|
|
108
|
+
formSpec,
|
|
109
|
+
ai,
|
|
110
|
+
beforeDestination,
|
|
111
|
+
afterDestination
|
|
112
|
+
} = document;
|
|
113
|
+
return structuredClone({
|
|
114
|
+
kind,
|
|
115
|
+
version,
|
|
116
|
+
id,
|
|
117
|
+
titleKey,
|
|
118
|
+
descriptionKey,
|
|
119
|
+
content,
|
|
120
|
+
questions,
|
|
121
|
+
pages,
|
|
122
|
+
verification,
|
|
123
|
+
formSpec,
|
|
124
|
+
ai: {
|
|
125
|
+
mode: ai.mode,
|
|
126
|
+
dataMode: ai.dataMode,
|
|
127
|
+
...ai.dataMode === "full-with-consent" ? {
|
|
128
|
+
consentNoticeVersion: FORM_AI_FULL_DATA_CONSENT_NOTICE_VERSION
|
|
129
|
+
} : {}
|
|
130
|
+
},
|
|
131
|
+
beforeDestination,
|
|
132
|
+
afterDestination
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async function resolveAssurance(dependencies, evidenceRef, definitionId, publicToken) {
|
|
136
|
+
if (!evidenceRef)
|
|
137
|
+
return "anonymous";
|
|
138
|
+
if (!dependencies.resolveAssurance) {
|
|
139
|
+
throw new FormsPlatformError("VERIFICATION_UNAVAILABLE", "Verification evidence cannot be resolved");
|
|
140
|
+
}
|
|
141
|
+
return dependencies.resolveAssurance(evidenceRef, definitionId, publicToken);
|
|
142
|
+
}
|
|
143
|
+
function createFormsSubmissionService(dependencies) {
|
|
144
|
+
return {
|
|
145
|
+
async getPublic(publicToken, localeCandidates) {
|
|
146
|
+
const definition = await dependencies.store.getDefinitionByPublicToken(publicToken);
|
|
147
|
+
if (!definition || definition.status !== "published" || !definition.publishedRevisionId) {
|
|
148
|
+
throw new FormsPlatformError("FORM_NOT_FOUND", "Published form was not found");
|
|
149
|
+
}
|
|
150
|
+
const revision = await dependencies.store.getRevision(definition.publishedRevisionId, { publicToken });
|
|
151
|
+
if (!revision) {
|
|
152
|
+
throw new FormsPlatformError("REVISION_NOT_FOUND", "Published form revision was not found");
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
definitionId: definition.id,
|
|
156
|
+
revisionId: revision.id,
|
|
157
|
+
revision: revision.revision,
|
|
158
|
+
document: publicDocument(revision.document),
|
|
159
|
+
locale: resolveFormLocale(revision.document, localeCandidates)
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
async submit(input) {
|
|
163
|
+
const definition = await dependencies.store.getDefinitionByPublicToken(input.publicToken);
|
|
164
|
+
if (!definition || definition.status !== "published") {
|
|
165
|
+
throw new FormsPlatformError("FORM_NOT_FOUND", "Form is not accepting responses");
|
|
166
|
+
}
|
|
167
|
+
const revision = await dependencies.store.getRevision(input.revisionId, {
|
|
168
|
+
publicToken: input.publicToken
|
|
169
|
+
});
|
|
170
|
+
if (!revision || revision.definitionId !== definition.id) {
|
|
171
|
+
throw new FormsPlatformError("REVISION_MISMATCH", "Response revision does not match the public session");
|
|
172
|
+
}
|
|
173
|
+
validateCanonicalFormAnswers(revision.document, input.answers);
|
|
174
|
+
const assurance = await resolveAssurance(dependencies, input.verificationEvidenceRef, definition.id, input.publicToken);
|
|
175
|
+
if (revision.document.verification.timing === "before" && !formAssuranceMeetsRequirement(assurance, revision.document.verification.requirement)) {
|
|
176
|
+
throw new FormsPlatformError("VERIFICATION_REQUIRED", "Verification is required before submission");
|
|
177
|
+
}
|
|
178
|
+
const now = dependencies.now();
|
|
179
|
+
const response = {
|
|
180
|
+
id: dependencies.id("response"),
|
|
181
|
+
definitionId: definition.id,
|
|
182
|
+
revisionId: revision.id,
|
|
183
|
+
locale: resolveFormLocale(revision.document, [input.locale]),
|
|
184
|
+
status: resolveFormResponseStatus(revision.document, assurance),
|
|
185
|
+
authorAssurance: assurance,
|
|
186
|
+
answers: structuredClone(input.answers),
|
|
187
|
+
createdAt: now,
|
|
188
|
+
updatedAt: now,
|
|
189
|
+
submittedAt: now
|
|
190
|
+
};
|
|
191
|
+
const stored = await dependencies.store.insertResponseIdempotently(response, input.publicToken, input.idempotencyKey);
|
|
192
|
+
if (!stored.inserted)
|
|
193
|
+
return stored.response;
|
|
194
|
+
await dependencies.emit?.("forms.response.submitted", {
|
|
195
|
+
definitionId: response.definitionId,
|
|
196
|
+
revisionId: response.revisionId,
|
|
197
|
+
responseId: response.id,
|
|
198
|
+
occurredAt: now
|
|
199
|
+
});
|
|
200
|
+
return stored.response;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
export {
|
|
205
|
+
createFormsSubmissionService,
|
|
206
|
+
validateCanonicalFormAnswers
|
|
207
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/forms-platform-utils.ts
|
|
2
|
+
import {
|
|
3
|
+
compileFormDocument,
|
|
4
|
+
validateFormDocument
|
|
5
|
+
} from "@lssm/lib.contracts-spec/forms";
|
|
6
|
+
|
|
7
|
+
// src/forms-platform-types.ts
|
|
8
|
+
class FormsPlatformError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
constructor(code, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.name = "FormsPlatformError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/forms-platform-utils.ts
|
|
18
|
+
function requireFormDefinition(definition, scope) {
|
|
19
|
+
const matches = definition?.owner.product === scope.product && definition.owner.tenantRef === scope.tenantRef && definition.owner.workspaceRef === scope.workspaceRef && definition.owner.ownerRef === scope.ownerRef;
|
|
20
|
+
if (!definition || !matches) {
|
|
21
|
+
throw new FormsPlatformError("FORM_NOT_FOUND", "Form definition was not found in the caller scope");
|
|
22
|
+
}
|
|
23
|
+
return definition;
|
|
24
|
+
}
|
|
25
|
+
function validateRuntimeFormDocument(document) {
|
|
26
|
+
const issues = validateFormDocument(document);
|
|
27
|
+
if (issues.length) {
|
|
28
|
+
throw new FormsPlatformError("INVALID_FORM_DOCUMENT", issues.map((issue) => issue.message).join("; "));
|
|
29
|
+
}
|
|
30
|
+
compileFormDocument(document);
|
|
31
|
+
}
|
|
32
|
+
function formAssuranceMeetsRequirement(assurance, requirement) {
|
|
33
|
+
if (requirement === "none")
|
|
34
|
+
return true;
|
|
35
|
+
if (requirement === "either")
|
|
36
|
+
return assurance !== "anonymous";
|
|
37
|
+
if (requirement === "both")
|
|
38
|
+
return assurance === "both";
|
|
39
|
+
return assurance === requirement || assurance === "both";
|
|
40
|
+
}
|
|
41
|
+
function resolveFormResponseStatus(document, assurance) {
|
|
42
|
+
if (formAssuranceMeetsRequirement(assurance, document.verification.requirement)) {
|
|
43
|
+
return "completed";
|
|
44
|
+
}
|
|
45
|
+
return document.verification.unverifiedDisposition === "pending-hidden" ? "pending-verification" : "completed-unverified";
|
|
46
|
+
}
|
|
47
|
+
function resolveFormLocale(document, candidates) {
|
|
48
|
+
const supported = new Set(document.content.enabledLocales.map((locale) => locale.toLowerCase()));
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
if (!candidate)
|
|
51
|
+
continue;
|
|
52
|
+
const normalized = candidate.trim().toLowerCase();
|
|
53
|
+
const exact = document.content.enabledLocales.find((locale) => locale.toLowerCase() === normalized);
|
|
54
|
+
if (supported.has(normalized) && exact)
|
|
55
|
+
return exact;
|
|
56
|
+
const language = normalized.split("-")[0];
|
|
57
|
+
const match = document.content.enabledLocales.find((locale) => locale.toLowerCase().split("-")[0] === language);
|
|
58
|
+
if (match)
|
|
59
|
+
return match;
|
|
60
|
+
}
|
|
61
|
+
return document.content.defaultLocale;
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
formAssuranceMeetsRequirement,
|
|
65
|
+
requireFormDefinition,
|
|
66
|
+
resolveFormLocale,
|
|
67
|
+
resolveFormResponseStatus,
|
|
68
|
+
validateRuntimeFormDocument
|
|
69
|
+
};
|