@form-engine-ts/core 2.6.0 → 2.7.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/README.md +7 -3
- package/dist/index.cjs +154 -4
- package/dist/index.d.cts +93 -6
- package/dist/index.d.ts +93 -6
- package/dist/index.js +148 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,11 +77,13 @@ Results are ordered by `submittedAt`, then submission ID. Both boundaries are in
|
|
|
77
77
|
## Versioning, incremental analytics, and paged storage
|
|
78
78
|
|
|
79
79
|
`cloneVersionToDraft`, asynchronous `publishDraft`, and `deleteDraft` implement revision-checked version transitions as
|
|
80
|
-
pure functions.
|
|
80
|
+
pure functions. `createCloneTransitionPlan`, `createPublishTransitionPlan`, and `createDeleteDraftTransitionPlan` produce
|
|
81
|
+
complete persistence plans with the next state, affected records, and immutable audit events.
|
|
81
82
|
Clone/delete operations accept `expectedRevision`; cloning rejects non-published sources, publish validation failures are
|
|
82
83
|
returned as typed `validation_failed` issues, and successful publishing archives only a supplied actual published record,
|
|
83
84
|
preserving its schema and metadata. `createPublishTransitionPlan` returns complete records plus expected/next revisions for
|
|
84
|
-
storage adapters implementing `VersionedFormStorageAdapter` to commit atomically.
|
|
85
|
+
storage adapters implementing `VersionedFormStorageAdapter` to commit atomically. Versioned adapters expose state/record
|
|
86
|
+
reads and return a typed `Result` from `commitVersionTransition`, including the actual revision on concurrency conflicts.
|
|
85
87
|
`createResponseAccumulator` incrementally counts choices, answered/unanswered values, and numeric summaries without retaining
|
|
86
88
|
free-text bodies. In lenient mode, mismatched responses are skipped and exposed by `addMany()` and `getReport()` instead of
|
|
87
89
|
being included silently. Independent accumulators for the same schema can be merged, and `finalize()` matches
|
|
@@ -94,6 +96,8 @@ Formula-injection neutralization applies to both default and custom columns.
|
|
|
94
96
|
|
|
95
97
|
Adapters implementing `PagedSubmissionStorageAdapter` expose `listSubmissionPage(formId, options)`. The opaque Base64
|
|
96
98
|
cursor combines `submittedAt` and response ID, so equal timestamps do not produce gaps or duplicates. `metadataFilters`
|
|
97
|
-
and `filter` are applied before page sizing.
|
|
99
|
+
and `filter` are applied before page sizing. `filter` accepts a composable `eq`/`in`/`range`/`exists` and/or AST; adapters
|
|
100
|
+
may push supported nodes to their native query language while preserving identical client-side semantics. Adapters that
|
|
101
|
+
implement `listTextAnswerPage` expose stable cursor pagination over individual text answers.
|
|
98
102
|
|
|
99
103
|
See the [project documentation](https://github.com/nitta-a/form-engine-ts#readme) for the complete schema and API guide.
|
package/dist/index.cjs
CHANGED
|
@@ -30,18 +30,24 @@ __export(index_exports, {
|
|
|
30
30
|
calculatePageVisibility: () => calculatePageVisibility,
|
|
31
31
|
cloneVersionToDraft: () => cloneVersionToDraft,
|
|
32
32
|
collectSchemaLocales: () => collectSchemaLocales,
|
|
33
|
+
createCloneTransitionPlan: () => createCloneTransitionPlan,
|
|
34
|
+
createDeleteDraftTransitionPlan: () => createDeleteDraftTransitionPlan,
|
|
33
35
|
createPublishTransitionPlan: () => createPublishTransitionPlan,
|
|
34
36
|
createResponseAccumulator: () => createResponseAccumulator,
|
|
35
37
|
createSubmission: () => createSubmission,
|
|
36
38
|
decodeSubmissionCursor: () => decodeSubmissionCursor,
|
|
39
|
+
decodeTextAnswerCursor: () => decodeTextAnswerCursor,
|
|
37
40
|
deleteDraft: () => deleteDraft,
|
|
38
41
|
dispatchWebhook: () => dispatchWebhook,
|
|
39
42
|
encodeSubmissionCursor: () => encodeSubmissionCursor,
|
|
43
|
+
encodeTextAnswerCursor: () => encodeTextAnswerCursor,
|
|
40
44
|
escapeCsvCell: () => escapeCsvCell,
|
|
41
45
|
exportResponsesToCsv: () => exportResponsesToCsv,
|
|
42
46
|
exportResponsesToCsvStream: () => exportResponsesToCsvStream,
|
|
43
47
|
isDisplayConditionSatisfied: () => isDisplayConditionSatisfied,
|
|
44
48
|
isQuestionVisible: () => isQuestionVisible,
|
|
49
|
+
jsonValuesEqual: () => jsonValuesEqual,
|
|
50
|
+
matchesSubmissionFilter: () => matchesSubmissionFilter,
|
|
45
51
|
matchesSubmissionPageFilters: () => matchesSubmissionPageFilters,
|
|
46
52
|
normalizeSubmissionPageSize: () => normalizeSubmissionPageSize,
|
|
47
53
|
pipeResponsesToCsvStream: () => pipeResponsesToCsvStream,
|
|
@@ -1565,6 +1571,24 @@ function decodeSubmissionCursor(cursor) {
|
|
|
1565
1571
|
throw new TypeError("cursor must be a valid form-engine cursor.", { cause });
|
|
1566
1572
|
}
|
|
1567
1573
|
}
|
|
1574
|
+
function encodeTextAnswerCursor(value) {
|
|
1575
|
+
if (value.responseId.length === 0 || value.fieldId.length === 0) {
|
|
1576
|
+
throw new TypeError("Text answer cursor values must not be empty.");
|
|
1577
|
+
}
|
|
1578
|
+
return encodeBase64(new TextEncoder().encode(JSON.stringify(value)));
|
|
1579
|
+
}
|
|
1580
|
+
function decodeTextAnswerCursor(cursor) {
|
|
1581
|
+
try {
|
|
1582
|
+
const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(decodeBase64(cursor)));
|
|
1583
|
+
if (typeof parsed !== "object" || parsed === null || !("responseId" in parsed) || typeof parsed.responseId !== "string" || parsed.responseId.length === 0 || !("fieldId" in parsed) || typeof parsed.fieldId !== "string" || parsed.fieldId.length === 0) {
|
|
1584
|
+
throw new TypeError("text answer cursor payload is invalid.");
|
|
1585
|
+
}
|
|
1586
|
+
return { responseId: parsed.responseId, fieldId: parsed.fieldId };
|
|
1587
|
+
} catch (cause) {
|
|
1588
|
+
if (cause instanceof TypeError && cause.message === "text answer cursor payload is invalid.") throw cause;
|
|
1589
|
+
throw new TypeError("cursor must be a valid text answer cursor.", { cause });
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1568
1592
|
function normalizeSubmissionPageSize(pageSize, fallback = 100) {
|
|
1569
1593
|
const value = pageSize ?? fallback;
|
|
1570
1594
|
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("pageSize must be a positive safe integer.");
|
|
@@ -1589,8 +1613,39 @@ function jsonValuesEqual(left, right) {
|
|
|
1589
1613
|
const rightKeys = Object.keys(right);
|
|
1590
1614
|
return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.hasOwn(right, key) && jsonValuesEqual(left[key], right[key]));
|
|
1591
1615
|
}
|
|
1616
|
+
function readSubmissionPath(submission, path) {
|
|
1617
|
+
if (path.length === 0 || path.split(".").some((part) => part.length === 0 || part === "__proto__" || part === "constructor")) {
|
|
1618
|
+
return void 0;
|
|
1619
|
+
}
|
|
1620
|
+
let current = submission;
|
|
1621
|
+
for (const part of path.split(".")) {
|
|
1622
|
+
if (typeof current !== "object" || current === null || !Object.hasOwn(current, part)) return void 0;
|
|
1623
|
+
current = current[part];
|
|
1624
|
+
}
|
|
1625
|
+
return current;
|
|
1626
|
+
}
|
|
1627
|
+
function compareRangeValue(value, boundary, direction) {
|
|
1628
|
+
if (typeof value === "number" && typeof boundary === "number") {
|
|
1629
|
+
return direction === "from" ? value >= boundary : value <= boundary;
|
|
1630
|
+
}
|
|
1631
|
+
if (typeof value === "string" && typeof boundary === "string") {
|
|
1632
|
+
return direction === "from" ? value >= boundary : value <= boundary;
|
|
1633
|
+
}
|
|
1634
|
+
return false;
|
|
1635
|
+
}
|
|
1636
|
+
function matchesSubmissionFilter(submission, filter) {
|
|
1637
|
+
if (filter.op === "and") return filter.filters.every((item) => matchesSubmissionFilter(submission, item));
|
|
1638
|
+
if (filter.op === "or") return filter.filters.some((item) => matchesSubmissionFilter(submission, item));
|
|
1639
|
+
const value = readSubmissionPath(submission, filter.path);
|
|
1640
|
+
if (filter.op === "eq") return jsonValuesEqual(value, filter.value);
|
|
1641
|
+
if (filter.op === "in") return filter.values.some((candidate) => jsonValuesEqual(value, candidate));
|
|
1642
|
+
if (filter.op === "exists") return filter.value ? value !== void 0 : value === void 0;
|
|
1643
|
+
return (filter.from === void 0 || compareRangeValue(value, filter.from, "from")) && (filter.to === void 0 || compareRangeValue(value, filter.to, "to"));
|
|
1644
|
+
}
|
|
1592
1645
|
function matchesSubmissionPageFilters(submission, options) {
|
|
1593
|
-
if (options.filter !== void 0 && !options.filter(submission))
|
|
1646
|
+
if (options.filter !== void 0 && !(typeof options.filter === "function" ? options.filter(submission) : matchesSubmissionFilter(submission, options.filter))) {
|
|
1647
|
+
return false;
|
|
1648
|
+
}
|
|
1594
1649
|
if (options.metadataFilters === void 0) return true;
|
|
1595
1650
|
return Object.entries(options.metadataFilters).every(
|
|
1596
1651
|
([key, value]) => jsonValuesEqual(submission.metadata?.[key], value)
|
|
@@ -2049,6 +2104,21 @@ function validateState(state) {
|
|
|
2049
2104
|
throw new TypeError("revision must be a non-negative safe integer.");
|
|
2050
2105
|
}
|
|
2051
2106
|
}
|
|
2107
|
+
function requireTimestamp(value, name) {
|
|
2108
|
+
const timestamp = value ?? "1970-01-01T00:00:00.000Z";
|
|
2109
|
+
if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError(`${name} must be a valid date string.`);
|
|
2110
|
+
return timestamp;
|
|
2111
|
+
}
|
|
2112
|
+
function transitionEvent(type, state, nextState, affectedVersions, occurredAt) {
|
|
2113
|
+
return {
|
|
2114
|
+
type,
|
|
2115
|
+
formId: state.formId,
|
|
2116
|
+
fromRevision: state.revision,
|
|
2117
|
+
toRevision: nextState.revision,
|
|
2118
|
+
affectedVersions,
|
|
2119
|
+
occurredAt
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2052
2122
|
function cloneVersionToDraft(state, sourceSchema, options = {}) {
|
|
2053
2123
|
validateState(state);
|
|
2054
2124
|
if (sourceSchema.id !== state.formId) throw new TypeError("sourceSchema.id must match state.formId.");
|
|
@@ -2092,6 +2162,46 @@ function cloneVersionToDraft(state, sourceSchema, options = {}) {
|
|
|
2092
2162
|
}
|
|
2093
2163
|
};
|
|
2094
2164
|
}
|
|
2165
|
+
function createCloneTransitionPlan(state, sourceRecord, options = {}) {
|
|
2166
|
+
if (sourceRecord.formId !== state.formId || sourceRecord.schema.id !== sourceRecord.formId || sourceRecord.schema.version !== sourceRecord.version || sourceRecord.status !== "published" && !options.allowedSourceVersions?.includes(sourceRecord.version)) {
|
|
2167
|
+
return {
|
|
2168
|
+
success: false,
|
|
2169
|
+
error: {
|
|
2170
|
+
type: "invalid_source_version",
|
|
2171
|
+
requestedVersion: sourceRecord.version,
|
|
2172
|
+
...state.publishedVersion === void 0 ? {} : { publishedVersion: state.publishedVersion }
|
|
2173
|
+
}
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
const result = cloneVersionToDraft(state, sourceRecord.schema, options);
|
|
2177
|
+
if (!result.success) return result;
|
|
2178
|
+
const timestamp = requireTimestamp(options.clonedAt, "clonedAt");
|
|
2179
|
+
const draftRecord = {
|
|
2180
|
+
formId: state.formId,
|
|
2181
|
+
version: result.value.draftSchema.version,
|
|
2182
|
+
status: "draft",
|
|
2183
|
+
schema: result.value.draftSchema,
|
|
2184
|
+
revision: 1,
|
|
2185
|
+
createdFromVersion: sourceRecord.version,
|
|
2186
|
+
createdAt: timestamp,
|
|
2187
|
+
...options.metadata === void 0 ? {} : { metadata: options.metadata }
|
|
2188
|
+
};
|
|
2189
|
+
return {
|
|
2190
|
+
success: true,
|
|
2191
|
+
value: {
|
|
2192
|
+
nextState: result.value.nextState,
|
|
2193
|
+
plan: {
|
|
2194
|
+
formId: state.formId,
|
|
2195
|
+
expectedRevision: options.expectedRevision ?? state.revision,
|
|
2196
|
+
nextRevision: result.value.nextState.revision,
|
|
2197
|
+
draftToCreate: draftRecord,
|
|
2198
|
+
events: [transitionEvent("draft.created", state, result.value.nextState, [draftRecord.version], timestamp)],
|
|
2199
|
+
nextVersion: result.value.nextState.nextVersion,
|
|
2200
|
+
timestamp
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2095
2205
|
function validatePublishedRecord(state, record) {
|
|
2096
2206
|
if (record === void 0) return;
|
|
2097
2207
|
if (record.formId !== state.formId || record.version !== state.publishedVersion || record.status !== "published" || record.schema.id !== record.formId || record.schema.version !== record.version) {
|
|
@@ -2099,9 +2209,7 @@ function validatePublishedRecord(state, record) {
|
|
|
2099
2209
|
}
|
|
2100
2210
|
}
|
|
2101
2211
|
function transitionTimestamp(options) {
|
|
2102
|
-
|
|
2103
|
-
if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError("publishedAt must be a valid date string.");
|
|
2104
|
-
return timestamp;
|
|
2212
|
+
return requireTimestamp(options.publishedAt ?? options.timestamp, "publishedAt");
|
|
2105
2213
|
}
|
|
2106
2214
|
async function publishDraft(state, draftSchema, options = {}) {
|
|
2107
2215
|
validateState(state);
|
|
@@ -2184,6 +2292,19 @@ async function createPublishTransitionPlan(state, draftRecord, options = {}) {
|
|
|
2184
2292
|
draftToDeleteVersion: draftRecord.version,
|
|
2185
2293
|
publishedRecordToSave,
|
|
2186
2294
|
archivedRecordsToSave: result.value.archivedRecords,
|
|
2295
|
+
events: [
|
|
2296
|
+
...result.value.archivedRecords.length === 0 ? [] : [
|
|
2297
|
+
transitionEvent(
|
|
2298
|
+
"version.archived",
|
|
2299
|
+
state,
|
|
2300
|
+
result.value.nextState,
|
|
2301
|
+
result.value.archivedRecords.map((record) => record.version),
|
|
2302
|
+
timestamp
|
|
2303
|
+
)
|
|
2304
|
+
],
|
|
2305
|
+
transitionEvent("version.published", state, result.value.nextState, [draftRecord.version], timestamp)
|
|
2306
|
+
],
|
|
2307
|
+
nextVersion: result.value.nextState.nextVersion,
|
|
2187
2308
|
timestamp
|
|
2188
2309
|
}
|
|
2189
2310
|
}
|
|
@@ -2200,6 +2321,29 @@ function deleteDraft(state, options = {}) {
|
|
|
2200
2321
|
value: { nextState: { ...stateWithoutDraft, revision: state.revision + 1 } }
|
|
2201
2322
|
};
|
|
2202
2323
|
}
|
|
2324
|
+
function createDeleteDraftTransitionPlan(state, draftRecord, options = {}) {
|
|
2325
|
+
if (draftRecord.formId !== state.formId || draftRecord.version !== state.draftVersion || draftRecord.status !== "draft" || draftRecord.schema.id !== state.formId || draftRecord.schema.version !== draftRecord.version) {
|
|
2326
|
+
return { success: false, error: { type: "draft_not_found" } };
|
|
2327
|
+
}
|
|
2328
|
+
const result = deleteDraft(state, options);
|
|
2329
|
+
if (!result.success) return result;
|
|
2330
|
+
const timestamp = requireTimestamp(options.deletedAt, "deletedAt");
|
|
2331
|
+
return {
|
|
2332
|
+
success: true,
|
|
2333
|
+
value: {
|
|
2334
|
+
nextState: result.value.nextState,
|
|
2335
|
+
plan: {
|
|
2336
|
+
formId: state.formId,
|
|
2337
|
+
expectedRevision: options.expectedRevision ?? state.revision,
|
|
2338
|
+
nextRevision: result.value.nextState.revision,
|
|
2339
|
+
draftToDeleteVersion: draftRecord.version,
|
|
2340
|
+
events: [transitionEvent("draft.deleted", state, result.value.nextState, [draftRecord.version], timestamp)],
|
|
2341
|
+
nextVersion: result.value.nextState.nextVersion,
|
|
2342
|
+
timestamp
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2203
2347
|
function assertVersionMutable(status) {
|
|
2204
2348
|
if (status !== "draft") {
|
|
2205
2349
|
const error = { type: "version_immutable", status };
|
|
@@ -2218,18 +2362,24 @@ function assertVersionMutable(status) {
|
|
|
2218
2362
|
calculatePageVisibility,
|
|
2219
2363
|
cloneVersionToDraft,
|
|
2220
2364
|
collectSchemaLocales,
|
|
2365
|
+
createCloneTransitionPlan,
|
|
2366
|
+
createDeleteDraftTransitionPlan,
|
|
2221
2367
|
createPublishTransitionPlan,
|
|
2222
2368
|
createResponseAccumulator,
|
|
2223
2369
|
createSubmission,
|
|
2224
2370
|
decodeSubmissionCursor,
|
|
2371
|
+
decodeTextAnswerCursor,
|
|
2225
2372
|
deleteDraft,
|
|
2226
2373
|
dispatchWebhook,
|
|
2227
2374
|
encodeSubmissionCursor,
|
|
2375
|
+
encodeTextAnswerCursor,
|
|
2228
2376
|
escapeCsvCell,
|
|
2229
2377
|
exportResponsesToCsv,
|
|
2230
2378
|
exportResponsesToCsvStream,
|
|
2231
2379
|
isDisplayConditionSatisfied,
|
|
2232
2380
|
isQuestionVisible,
|
|
2381
|
+
jsonValuesEqual,
|
|
2382
|
+
matchesSubmissionFilter,
|
|
2233
2383
|
matchesSubmissionPageFilters,
|
|
2234
2384
|
normalizeSubmissionPageSize,
|
|
2235
2385
|
pipeResponsesToCsvStream,
|
package/dist/index.d.cts
CHANGED
|
@@ -24,6 +24,14 @@ interface FormVersionState {
|
|
|
24
24
|
readonly nextVersion: number;
|
|
25
25
|
readonly revision: number;
|
|
26
26
|
}
|
|
27
|
+
interface VersionTransitionEvent {
|
|
28
|
+
readonly type: "draft.created" | "draft.deleted" | "version.published" | "version.archived";
|
|
29
|
+
readonly formId: string;
|
|
30
|
+
readonly fromRevision: number;
|
|
31
|
+
readonly toRevision: number;
|
|
32
|
+
readonly affectedVersions: readonly number[];
|
|
33
|
+
readonly occurredAt: string;
|
|
34
|
+
}
|
|
27
35
|
type VersionTransitionError = {
|
|
28
36
|
readonly type: "draft_already_exists";
|
|
29
37
|
readonly currentDraftVersion: number;
|
|
@@ -50,6 +58,8 @@ type VersionTransitionError = {
|
|
|
50
58
|
interface CloneVersionOptions {
|
|
51
59
|
readonly maxVersions?: number;
|
|
52
60
|
readonly expectedRevision?: number;
|
|
61
|
+
readonly clonedAt?: string;
|
|
62
|
+
readonly metadata?: Readonly<Record<string, JsonValue>>;
|
|
53
63
|
/** Additional known published versions that may be used as a clone source. */
|
|
54
64
|
readonly allowedSourceVersions?: readonly number[];
|
|
55
65
|
}
|
|
@@ -63,6 +73,7 @@ interface PublishDraftOptions {
|
|
|
63
73
|
}
|
|
64
74
|
interface DeleteDraftOptions {
|
|
65
75
|
readonly expectedRevision?: number;
|
|
76
|
+
readonly deletedAt?: string;
|
|
66
77
|
}
|
|
67
78
|
interface PublishDraftResult {
|
|
68
79
|
readonly nextState: FormVersionState;
|
|
@@ -75,6 +86,10 @@ declare function cloneVersionToDraft(state: FormVersionState, sourceSchema: Form
|
|
|
75
86
|
readonly nextState: FormVersionState;
|
|
76
87
|
readonly draftSchema: FormSchema;
|
|
77
88
|
}, VersionTransitionError>;
|
|
89
|
+
declare function createCloneTransitionPlan(state: FormVersionState, sourceRecord: FormVersionRecord, options?: CloneVersionOptions): Result<{
|
|
90
|
+
readonly nextState: FormVersionState;
|
|
91
|
+
readonly plan: VersionTransitionPlan;
|
|
92
|
+
}, VersionTransitionError>;
|
|
78
93
|
declare function publishDraft(state: FormVersionState, draftSchema: FormSchema, options?: PublishDraftOptions): Promise<Result<PublishDraftResult, VersionTransitionError>>;
|
|
79
94
|
declare function createPublishTransitionPlan(state: FormVersionState, draftRecord: FormVersionRecord, options?: PublishDraftOptions): Promise<Result<{
|
|
80
95
|
readonly nextState: FormVersionState;
|
|
@@ -83,6 +98,10 @@ declare function createPublishTransitionPlan(state: FormVersionState, draftRecor
|
|
|
83
98
|
declare function deleteDraft(state: FormVersionState, options?: DeleteDraftOptions): Result<{
|
|
84
99
|
readonly nextState: FormVersionState;
|
|
85
100
|
}, VersionTransitionError>;
|
|
101
|
+
declare function createDeleteDraftTransitionPlan(state: FormVersionState, draftRecord: FormVersionRecord, options?: DeleteDraftOptions): Result<{
|
|
102
|
+
readonly nextState: FormVersionState;
|
|
103
|
+
readonly plan: VersionTransitionPlan;
|
|
104
|
+
}, VersionTransitionError>;
|
|
86
105
|
declare function assertVersionMutable(status: FormVersionStatus): void;
|
|
87
106
|
|
|
88
107
|
type FieldType = "text" | "textarea" | "number" | "rating" | "select" | "multi-select" | "checkbox" | "radio";
|
|
@@ -257,9 +276,34 @@ interface SubmissionPageQueryOptions {
|
|
|
257
276
|
readonly since?: string;
|
|
258
277
|
readonly until?: string;
|
|
259
278
|
readonly locale?: string;
|
|
260
|
-
readonly filter?: (submission: FormSubmission) => boolean;
|
|
279
|
+
readonly filter?: SubmissionFilter | ((submission: FormSubmission) => boolean);
|
|
280
|
+
/** @deprecated Prefer the generic filter AST. */
|
|
261
281
|
readonly metadataFilters?: Readonly<Record<string, JsonValue>>;
|
|
262
282
|
}
|
|
283
|
+
type SubmissionFilter = {
|
|
284
|
+
readonly op: "eq";
|
|
285
|
+
readonly path: string;
|
|
286
|
+
readonly value: JsonValue;
|
|
287
|
+
} | {
|
|
288
|
+
readonly op: "in";
|
|
289
|
+
readonly path: string;
|
|
290
|
+
readonly values: readonly JsonValue[];
|
|
291
|
+
} | {
|
|
292
|
+
readonly op: "range";
|
|
293
|
+
readonly path: string;
|
|
294
|
+
readonly from?: JsonValue;
|
|
295
|
+
readonly to?: JsonValue;
|
|
296
|
+
} | {
|
|
297
|
+
readonly op: "exists";
|
|
298
|
+
readonly path: string;
|
|
299
|
+
readonly value: boolean;
|
|
300
|
+
} | {
|
|
301
|
+
readonly op: "and";
|
|
302
|
+
readonly filters: readonly SubmissionFilter[];
|
|
303
|
+
} | {
|
|
304
|
+
readonly op: "or";
|
|
305
|
+
readonly filters: readonly SubmissionFilter[];
|
|
306
|
+
};
|
|
263
307
|
interface SubmissionPage {
|
|
264
308
|
readonly items: readonly FormSubmission[];
|
|
265
309
|
readonly nextCursor?: string;
|
|
@@ -267,6 +311,22 @@ interface SubmissionPage {
|
|
|
267
311
|
}
|
|
268
312
|
interface PagedSubmissionStorageAdapter extends FormStorageAdapter {
|
|
269
313
|
listSubmissionPage(formId: string, options?: SubmissionPageQueryOptions): Promise<SubmissionPage>;
|
|
314
|
+
listTextAnswerPage?(formId: string, fieldId: string, options?: SubmissionPageQueryOptions): Promise<TextAnswerPage>;
|
|
315
|
+
}
|
|
316
|
+
interface TextAnswerItem {
|
|
317
|
+
readonly responseId: string;
|
|
318
|
+
readonly formId: string;
|
|
319
|
+
readonly formVersion: number;
|
|
320
|
+
readonly fieldId: string;
|
|
321
|
+
readonly text: string;
|
|
322
|
+
readonly locale?: string;
|
|
323
|
+
readonly submittedAt: string;
|
|
324
|
+
readonly metadata?: Readonly<Record<string, JsonValue>>;
|
|
325
|
+
}
|
|
326
|
+
interface TextAnswerPage {
|
|
327
|
+
readonly items: readonly TextAnswerItem[];
|
|
328
|
+
readonly nextCursor?: string;
|
|
329
|
+
readonly hasMore: boolean;
|
|
270
330
|
}
|
|
271
331
|
interface VersionTransitionPlan {
|
|
272
332
|
readonly formId: string;
|
|
@@ -276,13 +336,32 @@ interface VersionTransitionPlan {
|
|
|
276
336
|
readonly draftToDeleteVersion?: number;
|
|
277
337
|
readonly publishedRecordToSave?: FormVersionRecord;
|
|
278
338
|
readonly archivedRecordsToSave?: readonly FormVersionRecord[];
|
|
339
|
+
readonly events: readonly VersionTransitionEvent[];
|
|
340
|
+
/** The complete next state value used by persistent adapters. */
|
|
341
|
+
readonly nextVersion?: number;
|
|
279
342
|
readonly timestamp: string;
|
|
280
343
|
}
|
|
344
|
+
type StorageCommitError = {
|
|
345
|
+
readonly type: "revision_conflict";
|
|
346
|
+
readonly expectedRevision: number;
|
|
347
|
+
readonly actualRevision?: number;
|
|
348
|
+
} | {
|
|
349
|
+
readonly type: "draft_already_exists";
|
|
350
|
+
readonly currentDraftVersion: number;
|
|
351
|
+
} | {
|
|
352
|
+
readonly type: "invalid_transition";
|
|
353
|
+
readonly message: string;
|
|
354
|
+
} | {
|
|
355
|
+
readonly type: "storage_error";
|
|
356
|
+
readonly cause: unknown;
|
|
357
|
+
};
|
|
281
358
|
interface VersionedFormStorageAdapter extends FormStorageAdapter {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
359
|
+
getVersionState(formId: string): Promise<FormVersionState | null>;
|
|
360
|
+
getVersionRecord(formId: string, version: number): Promise<FormVersionRecord | null>;
|
|
361
|
+
listVersionRecords(formId: string): Promise<readonly FormVersionRecord[]>;
|
|
362
|
+
commitVersionTransition(plan: VersionTransitionPlan): Promise<Result<{
|
|
363
|
+
readonly success: true;
|
|
364
|
+
}, StorageCommitError>>;
|
|
286
365
|
}
|
|
287
366
|
interface BaseQuestionAggregate {
|
|
288
367
|
readonly fieldId: string;
|
|
@@ -442,9 +521,17 @@ interface SubmissionCursorValue {
|
|
|
442
521
|
readonly submittedAt: string;
|
|
443
522
|
readonly responseId: string;
|
|
444
523
|
}
|
|
524
|
+
interface TextAnswerCursorValue {
|
|
525
|
+
readonly responseId: string;
|
|
526
|
+
readonly fieldId: string;
|
|
527
|
+
}
|
|
445
528
|
declare function encodeSubmissionCursor(value: SubmissionCursorValue): string;
|
|
446
529
|
declare function decodeSubmissionCursor(cursor: string): SubmissionCursorValue;
|
|
530
|
+
declare function encodeTextAnswerCursor(value: TextAnswerCursorValue): string;
|
|
531
|
+
declare function decodeTextAnswerCursor(cursor: string): TextAnswerCursorValue;
|
|
447
532
|
declare function normalizeSubmissionPageSize(pageSize: number | undefined, fallback?: number): number;
|
|
533
|
+
declare function jsonValuesEqual(left: JsonValue | undefined, right: JsonValue | undefined): boolean;
|
|
534
|
+
declare function matchesSubmissionFilter(submission: FormSubmission, filter: SubmissionFilter): boolean;
|
|
448
535
|
declare function matchesSubmissionPageFilters(submission: FormSubmission, options: Pick<SubmissionPageQueryOptions, "filter" | "metadataFilters">): boolean;
|
|
449
536
|
|
|
450
537
|
interface CollectedLocales {
|
|
@@ -520,4 +607,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
|
|
|
520
607
|
declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
|
|
521
608
|
declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
|
|
522
609
|
|
|
523
|
-
export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, type DeleteDraftOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LocalizedText, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PopulateTranslationOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, createPublishTransitionPlan, createResponseAccumulator, createSubmission, decodeSubmissionCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, isDisplayConditionSatisfied, isQuestionVisible, matchesSubmissionPageFilters, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
|
610
|
+
export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, type DeleteDraftOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LocalizedText, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PopulateTranslationOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StorageCommitError, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionFilter, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextAnswerCursorValue, type TextAnswerItem, type TextAnswerPage, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionEvent, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, createCloneTransitionPlan, createDeleteDraftTransitionPlan, createPublishTransitionPlan, createResponseAccumulator, createSubmission, decodeSubmissionCursor, decodeTextAnswerCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, encodeTextAnswerCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, isDisplayConditionSatisfied, isQuestionVisible, jsonValuesEqual, matchesSubmissionFilter, matchesSubmissionPageFilters, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,14 @@ interface FormVersionState {
|
|
|
24
24
|
readonly nextVersion: number;
|
|
25
25
|
readonly revision: number;
|
|
26
26
|
}
|
|
27
|
+
interface VersionTransitionEvent {
|
|
28
|
+
readonly type: "draft.created" | "draft.deleted" | "version.published" | "version.archived";
|
|
29
|
+
readonly formId: string;
|
|
30
|
+
readonly fromRevision: number;
|
|
31
|
+
readonly toRevision: number;
|
|
32
|
+
readonly affectedVersions: readonly number[];
|
|
33
|
+
readonly occurredAt: string;
|
|
34
|
+
}
|
|
27
35
|
type VersionTransitionError = {
|
|
28
36
|
readonly type: "draft_already_exists";
|
|
29
37
|
readonly currentDraftVersion: number;
|
|
@@ -50,6 +58,8 @@ type VersionTransitionError = {
|
|
|
50
58
|
interface CloneVersionOptions {
|
|
51
59
|
readonly maxVersions?: number;
|
|
52
60
|
readonly expectedRevision?: number;
|
|
61
|
+
readonly clonedAt?: string;
|
|
62
|
+
readonly metadata?: Readonly<Record<string, JsonValue>>;
|
|
53
63
|
/** Additional known published versions that may be used as a clone source. */
|
|
54
64
|
readonly allowedSourceVersions?: readonly number[];
|
|
55
65
|
}
|
|
@@ -63,6 +73,7 @@ interface PublishDraftOptions {
|
|
|
63
73
|
}
|
|
64
74
|
interface DeleteDraftOptions {
|
|
65
75
|
readonly expectedRevision?: number;
|
|
76
|
+
readonly deletedAt?: string;
|
|
66
77
|
}
|
|
67
78
|
interface PublishDraftResult {
|
|
68
79
|
readonly nextState: FormVersionState;
|
|
@@ -75,6 +86,10 @@ declare function cloneVersionToDraft(state: FormVersionState, sourceSchema: Form
|
|
|
75
86
|
readonly nextState: FormVersionState;
|
|
76
87
|
readonly draftSchema: FormSchema;
|
|
77
88
|
}, VersionTransitionError>;
|
|
89
|
+
declare function createCloneTransitionPlan(state: FormVersionState, sourceRecord: FormVersionRecord, options?: CloneVersionOptions): Result<{
|
|
90
|
+
readonly nextState: FormVersionState;
|
|
91
|
+
readonly plan: VersionTransitionPlan;
|
|
92
|
+
}, VersionTransitionError>;
|
|
78
93
|
declare function publishDraft(state: FormVersionState, draftSchema: FormSchema, options?: PublishDraftOptions): Promise<Result<PublishDraftResult, VersionTransitionError>>;
|
|
79
94
|
declare function createPublishTransitionPlan(state: FormVersionState, draftRecord: FormVersionRecord, options?: PublishDraftOptions): Promise<Result<{
|
|
80
95
|
readonly nextState: FormVersionState;
|
|
@@ -83,6 +98,10 @@ declare function createPublishTransitionPlan(state: FormVersionState, draftRecor
|
|
|
83
98
|
declare function deleteDraft(state: FormVersionState, options?: DeleteDraftOptions): Result<{
|
|
84
99
|
readonly nextState: FormVersionState;
|
|
85
100
|
}, VersionTransitionError>;
|
|
101
|
+
declare function createDeleteDraftTransitionPlan(state: FormVersionState, draftRecord: FormVersionRecord, options?: DeleteDraftOptions): Result<{
|
|
102
|
+
readonly nextState: FormVersionState;
|
|
103
|
+
readonly plan: VersionTransitionPlan;
|
|
104
|
+
}, VersionTransitionError>;
|
|
86
105
|
declare function assertVersionMutable(status: FormVersionStatus): void;
|
|
87
106
|
|
|
88
107
|
type FieldType = "text" | "textarea" | "number" | "rating" | "select" | "multi-select" | "checkbox" | "radio";
|
|
@@ -257,9 +276,34 @@ interface SubmissionPageQueryOptions {
|
|
|
257
276
|
readonly since?: string;
|
|
258
277
|
readonly until?: string;
|
|
259
278
|
readonly locale?: string;
|
|
260
|
-
readonly filter?: (submission: FormSubmission) => boolean;
|
|
279
|
+
readonly filter?: SubmissionFilter | ((submission: FormSubmission) => boolean);
|
|
280
|
+
/** @deprecated Prefer the generic filter AST. */
|
|
261
281
|
readonly metadataFilters?: Readonly<Record<string, JsonValue>>;
|
|
262
282
|
}
|
|
283
|
+
type SubmissionFilter = {
|
|
284
|
+
readonly op: "eq";
|
|
285
|
+
readonly path: string;
|
|
286
|
+
readonly value: JsonValue;
|
|
287
|
+
} | {
|
|
288
|
+
readonly op: "in";
|
|
289
|
+
readonly path: string;
|
|
290
|
+
readonly values: readonly JsonValue[];
|
|
291
|
+
} | {
|
|
292
|
+
readonly op: "range";
|
|
293
|
+
readonly path: string;
|
|
294
|
+
readonly from?: JsonValue;
|
|
295
|
+
readonly to?: JsonValue;
|
|
296
|
+
} | {
|
|
297
|
+
readonly op: "exists";
|
|
298
|
+
readonly path: string;
|
|
299
|
+
readonly value: boolean;
|
|
300
|
+
} | {
|
|
301
|
+
readonly op: "and";
|
|
302
|
+
readonly filters: readonly SubmissionFilter[];
|
|
303
|
+
} | {
|
|
304
|
+
readonly op: "or";
|
|
305
|
+
readonly filters: readonly SubmissionFilter[];
|
|
306
|
+
};
|
|
263
307
|
interface SubmissionPage {
|
|
264
308
|
readonly items: readonly FormSubmission[];
|
|
265
309
|
readonly nextCursor?: string;
|
|
@@ -267,6 +311,22 @@ interface SubmissionPage {
|
|
|
267
311
|
}
|
|
268
312
|
interface PagedSubmissionStorageAdapter extends FormStorageAdapter {
|
|
269
313
|
listSubmissionPage(formId: string, options?: SubmissionPageQueryOptions): Promise<SubmissionPage>;
|
|
314
|
+
listTextAnswerPage?(formId: string, fieldId: string, options?: SubmissionPageQueryOptions): Promise<TextAnswerPage>;
|
|
315
|
+
}
|
|
316
|
+
interface TextAnswerItem {
|
|
317
|
+
readonly responseId: string;
|
|
318
|
+
readonly formId: string;
|
|
319
|
+
readonly formVersion: number;
|
|
320
|
+
readonly fieldId: string;
|
|
321
|
+
readonly text: string;
|
|
322
|
+
readonly locale?: string;
|
|
323
|
+
readonly submittedAt: string;
|
|
324
|
+
readonly metadata?: Readonly<Record<string, JsonValue>>;
|
|
325
|
+
}
|
|
326
|
+
interface TextAnswerPage {
|
|
327
|
+
readonly items: readonly TextAnswerItem[];
|
|
328
|
+
readonly nextCursor?: string;
|
|
329
|
+
readonly hasMore: boolean;
|
|
270
330
|
}
|
|
271
331
|
interface VersionTransitionPlan {
|
|
272
332
|
readonly formId: string;
|
|
@@ -276,13 +336,32 @@ interface VersionTransitionPlan {
|
|
|
276
336
|
readonly draftToDeleteVersion?: number;
|
|
277
337
|
readonly publishedRecordToSave?: FormVersionRecord;
|
|
278
338
|
readonly archivedRecordsToSave?: readonly FormVersionRecord[];
|
|
339
|
+
readonly events: readonly VersionTransitionEvent[];
|
|
340
|
+
/** The complete next state value used by persistent adapters. */
|
|
341
|
+
readonly nextVersion?: number;
|
|
279
342
|
readonly timestamp: string;
|
|
280
343
|
}
|
|
344
|
+
type StorageCommitError = {
|
|
345
|
+
readonly type: "revision_conflict";
|
|
346
|
+
readonly expectedRevision: number;
|
|
347
|
+
readonly actualRevision?: number;
|
|
348
|
+
} | {
|
|
349
|
+
readonly type: "draft_already_exists";
|
|
350
|
+
readonly currentDraftVersion: number;
|
|
351
|
+
} | {
|
|
352
|
+
readonly type: "invalid_transition";
|
|
353
|
+
readonly message: string;
|
|
354
|
+
} | {
|
|
355
|
+
readonly type: "storage_error";
|
|
356
|
+
readonly cause: unknown;
|
|
357
|
+
};
|
|
281
358
|
interface VersionedFormStorageAdapter extends FormStorageAdapter {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
359
|
+
getVersionState(formId: string): Promise<FormVersionState | null>;
|
|
360
|
+
getVersionRecord(formId: string, version: number): Promise<FormVersionRecord | null>;
|
|
361
|
+
listVersionRecords(formId: string): Promise<readonly FormVersionRecord[]>;
|
|
362
|
+
commitVersionTransition(plan: VersionTransitionPlan): Promise<Result<{
|
|
363
|
+
readonly success: true;
|
|
364
|
+
}, StorageCommitError>>;
|
|
286
365
|
}
|
|
287
366
|
interface BaseQuestionAggregate {
|
|
288
367
|
readonly fieldId: string;
|
|
@@ -442,9 +521,17 @@ interface SubmissionCursorValue {
|
|
|
442
521
|
readonly submittedAt: string;
|
|
443
522
|
readonly responseId: string;
|
|
444
523
|
}
|
|
524
|
+
interface TextAnswerCursorValue {
|
|
525
|
+
readonly responseId: string;
|
|
526
|
+
readonly fieldId: string;
|
|
527
|
+
}
|
|
445
528
|
declare function encodeSubmissionCursor(value: SubmissionCursorValue): string;
|
|
446
529
|
declare function decodeSubmissionCursor(cursor: string): SubmissionCursorValue;
|
|
530
|
+
declare function encodeTextAnswerCursor(value: TextAnswerCursorValue): string;
|
|
531
|
+
declare function decodeTextAnswerCursor(cursor: string): TextAnswerCursorValue;
|
|
447
532
|
declare function normalizeSubmissionPageSize(pageSize: number | undefined, fallback?: number): number;
|
|
533
|
+
declare function jsonValuesEqual(left: JsonValue | undefined, right: JsonValue | undefined): boolean;
|
|
534
|
+
declare function matchesSubmissionFilter(submission: FormSubmission, filter: SubmissionFilter): boolean;
|
|
448
535
|
declare function matchesSubmissionPageFilters(submission: FormSubmission, options: Pick<SubmissionPageQueryOptions, "filter" | "metadataFilters">): boolean;
|
|
449
536
|
|
|
450
537
|
interface CollectedLocales {
|
|
@@ -520,4 +607,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
|
|
|
520
607
|
declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
|
|
521
608
|
declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
|
|
522
609
|
|
|
523
|
-
export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, type DeleteDraftOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LocalizedText, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PopulateTranslationOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, createPublishTransitionPlan, createResponseAccumulator, createSubmission, decodeSubmissionCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, isDisplayConditionSatisfied, isQuestionVisible, matchesSubmissionPageFilters, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
|
610
|
+
export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, type DeleteDraftOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LocalizedText, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PopulateTranslationOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StorageCommitError, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionFilter, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextAnswerCursorValue, type TextAnswerItem, type TextAnswerPage, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionEvent, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, createCloneTransitionPlan, createDeleteDraftTransitionPlan, createPublishTransitionPlan, createResponseAccumulator, createSubmission, decodeSubmissionCursor, decodeTextAnswerCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, encodeTextAnswerCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, isDisplayConditionSatisfied, isQuestionVisible, jsonValuesEqual, matchesSubmissionFilter, matchesSubmissionPageFilters, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
package/dist/index.js
CHANGED
|
@@ -1504,6 +1504,24 @@ function decodeSubmissionCursor(cursor) {
|
|
|
1504
1504
|
throw new TypeError("cursor must be a valid form-engine cursor.", { cause });
|
|
1505
1505
|
}
|
|
1506
1506
|
}
|
|
1507
|
+
function encodeTextAnswerCursor(value) {
|
|
1508
|
+
if (value.responseId.length === 0 || value.fieldId.length === 0) {
|
|
1509
|
+
throw new TypeError("Text answer cursor values must not be empty.");
|
|
1510
|
+
}
|
|
1511
|
+
return encodeBase64(new TextEncoder().encode(JSON.stringify(value)));
|
|
1512
|
+
}
|
|
1513
|
+
function decodeTextAnswerCursor(cursor) {
|
|
1514
|
+
try {
|
|
1515
|
+
const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(decodeBase64(cursor)));
|
|
1516
|
+
if (typeof parsed !== "object" || parsed === null || !("responseId" in parsed) || typeof parsed.responseId !== "string" || parsed.responseId.length === 0 || !("fieldId" in parsed) || typeof parsed.fieldId !== "string" || parsed.fieldId.length === 0) {
|
|
1517
|
+
throw new TypeError("text answer cursor payload is invalid.");
|
|
1518
|
+
}
|
|
1519
|
+
return { responseId: parsed.responseId, fieldId: parsed.fieldId };
|
|
1520
|
+
} catch (cause) {
|
|
1521
|
+
if (cause instanceof TypeError && cause.message === "text answer cursor payload is invalid.") throw cause;
|
|
1522
|
+
throw new TypeError("cursor must be a valid text answer cursor.", { cause });
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1507
1525
|
function normalizeSubmissionPageSize(pageSize, fallback = 100) {
|
|
1508
1526
|
const value = pageSize ?? fallback;
|
|
1509
1527
|
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("pageSize must be a positive safe integer.");
|
|
@@ -1528,8 +1546,39 @@ function jsonValuesEqual(left, right) {
|
|
|
1528
1546
|
const rightKeys = Object.keys(right);
|
|
1529
1547
|
return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.hasOwn(right, key) && jsonValuesEqual(left[key], right[key]));
|
|
1530
1548
|
}
|
|
1549
|
+
function readSubmissionPath(submission, path) {
|
|
1550
|
+
if (path.length === 0 || path.split(".").some((part) => part.length === 0 || part === "__proto__" || part === "constructor")) {
|
|
1551
|
+
return void 0;
|
|
1552
|
+
}
|
|
1553
|
+
let current = submission;
|
|
1554
|
+
for (const part of path.split(".")) {
|
|
1555
|
+
if (typeof current !== "object" || current === null || !Object.hasOwn(current, part)) return void 0;
|
|
1556
|
+
current = current[part];
|
|
1557
|
+
}
|
|
1558
|
+
return current;
|
|
1559
|
+
}
|
|
1560
|
+
function compareRangeValue(value, boundary, direction) {
|
|
1561
|
+
if (typeof value === "number" && typeof boundary === "number") {
|
|
1562
|
+
return direction === "from" ? value >= boundary : value <= boundary;
|
|
1563
|
+
}
|
|
1564
|
+
if (typeof value === "string" && typeof boundary === "string") {
|
|
1565
|
+
return direction === "from" ? value >= boundary : value <= boundary;
|
|
1566
|
+
}
|
|
1567
|
+
return false;
|
|
1568
|
+
}
|
|
1569
|
+
function matchesSubmissionFilter(submission, filter) {
|
|
1570
|
+
if (filter.op === "and") return filter.filters.every((item) => matchesSubmissionFilter(submission, item));
|
|
1571
|
+
if (filter.op === "or") return filter.filters.some((item) => matchesSubmissionFilter(submission, item));
|
|
1572
|
+
const value = readSubmissionPath(submission, filter.path);
|
|
1573
|
+
if (filter.op === "eq") return jsonValuesEqual(value, filter.value);
|
|
1574
|
+
if (filter.op === "in") return filter.values.some((candidate) => jsonValuesEqual(value, candidate));
|
|
1575
|
+
if (filter.op === "exists") return filter.value ? value !== void 0 : value === void 0;
|
|
1576
|
+
return (filter.from === void 0 || compareRangeValue(value, filter.from, "from")) && (filter.to === void 0 || compareRangeValue(value, filter.to, "to"));
|
|
1577
|
+
}
|
|
1531
1578
|
function matchesSubmissionPageFilters(submission, options) {
|
|
1532
|
-
if (options.filter !== void 0 && !options.filter(submission))
|
|
1579
|
+
if (options.filter !== void 0 && !(typeof options.filter === "function" ? options.filter(submission) : matchesSubmissionFilter(submission, options.filter))) {
|
|
1580
|
+
return false;
|
|
1581
|
+
}
|
|
1533
1582
|
if (options.metadataFilters === void 0) return true;
|
|
1534
1583
|
return Object.entries(options.metadataFilters).every(
|
|
1535
1584
|
([key, value]) => jsonValuesEqual(submission.metadata?.[key], value)
|
|
@@ -1988,6 +2037,21 @@ function validateState(state) {
|
|
|
1988
2037
|
throw new TypeError("revision must be a non-negative safe integer.");
|
|
1989
2038
|
}
|
|
1990
2039
|
}
|
|
2040
|
+
function requireTimestamp(value, name) {
|
|
2041
|
+
const timestamp = value ?? "1970-01-01T00:00:00.000Z";
|
|
2042
|
+
if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError(`${name} must be a valid date string.`);
|
|
2043
|
+
return timestamp;
|
|
2044
|
+
}
|
|
2045
|
+
function transitionEvent(type, state, nextState, affectedVersions, occurredAt) {
|
|
2046
|
+
return {
|
|
2047
|
+
type,
|
|
2048
|
+
formId: state.formId,
|
|
2049
|
+
fromRevision: state.revision,
|
|
2050
|
+
toRevision: nextState.revision,
|
|
2051
|
+
affectedVersions,
|
|
2052
|
+
occurredAt
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
1991
2055
|
function cloneVersionToDraft(state, sourceSchema, options = {}) {
|
|
1992
2056
|
validateState(state);
|
|
1993
2057
|
if (sourceSchema.id !== state.formId) throw new TypeError("sourceSchema.id must match state.formId.");
|
|
@@ -2031,6 +2095,46 @@ function cloneVersionToDraft(state, sourceSchema, options = {}) {
|
|
|
2031
2095
|
}
|
|
2032
2096
|
};
|
|
2033
2097
|
}
|
|
2098
|
+
function createCloneTransitionPlan(state, sourceRecord, options = {}) {
|
|
2099
|
+
if (sourceRecord.formId !== state.formId || sourceRecord.schema.id !== sourceRecord.formId || sourceRecord.schema.version !== sourceRecord.version || sourceRecord.status !== "published" && !options.allowedSourceVersions?.includes(sourceRecord.version)) {
|
|
2100
|
+
return {
|
|
2101
|
+
success: false,
|
|
2102
|
+
error: {
|
|
2103
|
+
type: "invalid_source_version",
|
|
2104
|
+
requestedVersion: sourceRecord.version,
|
|
2105
|
+
...state.publishedVersion === void 0 ? {} : { publishedVersion: state.publishedVersion }
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
const result = cloneVersionToDraft(state, sourceRecord.schema, options);
|
|
2110
|
+
if (!result.success) return result;
|
|
2111
|
+
const timestamp = requireTimestamp(options.clonedAt, "clonedAt");
|
|
2112
|
+
const draftRecord = {
|
|
2113
|
+
formId: state.formId,
|
|
2114
|
+
version: result.value.draftSchema.version,
|
|
2115
|
+
status: "draft",
|
|
2116
|
+
schema: result.value.draftSchema,
|
|
2117
|
+
revision: 1,
|
|
2118
|
+
createdFromVersion: sourceRecord.version,
|
|
2119
|
+
createdAt: timestamp,
|
|
2120
|
+
...options.metadata === void 0 ? {} : { metadata: options.metadata }
|
|
2121
|
+
};
|
|
2122
|
+
return {
|
|
2123
|
+
success: true,
|
|
2124
|
+
value: {
|
|
2125
|
+
nextState: result.value.nextState,
|
|
2126
|
+
plan: {
|
|
2127
|
+
formId: state.formId,
|
|
2128
|
+
expectedRevision: options.expectedRevision ?? state.revision,
|
|
2129
|
+
nextRevision: result.value.nextState.revision,
|
|
2130
|
+
draftToCreate: draftRecord,
|
|
2131
|
+
events: [transitionEvent("draft.created", state, result.value.nextState, [draftRecord.version], timestamp)],
|
|
2132
|
+
nextVersion: result.value.nextState.nextVersion,
|
|
2133
|
+
timestamp
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2034
2138
|
function validatePublishedRecord(state, record) {
|
|
2035
2139
|
if (record === void 0) return;
|
|
2036
2140
|
if (record.formId !== state.formId || record.version !== state.publishedVersion || record.status !== "published" || record.schema.id !== record.formId || record.schema.version !== record.version) {
|
|
@@ -2038,9 +2142,7 @@ function validatePublishedRecord(state, record) {
|
|
|
2038
2142
|
}
|
|
2039
2143
|
}
|
|
2040
2144
|
function transitionTimestamp(options) {
|
|
2041
|
-
|
|
2042
|
-
if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError("publishedAt must be a valid date string.");
|
|
2043
|
-
return timestamp;
|
|
2145
|
+
return requireTimestamp(options.publishedAt ?? options.timestamp, "publishedAt");
|
|
2044
2146
|
}
|
|
2045
2147
|
async function publishDraft(state, draftSchema, options = {}) {
|
|
2046
2148
|
validateState(state);
|
|
@@ -2123,6 +2225,19 @@ async function createPublishTransitionPlan(state, draftRecord, options = {}) {
|
|
|
2123
2225
|
draftToDeleteVersion: draftRecord.version,
|
|
2124
2226
|
publishedRecordToSave,
|
|
2125
2227
|
archivedRecordsToSave: result.value.archivedRecords,
|
|
2228
|
+
events: [
|
|
2229
|
+
...result.value.archivedRecords.length === 0 ? [] : [
|
|
2230
|
+
transitionEvent(
|
|
2231
|
+
"version.archived",
|
|
2232
|
+
state,
|
|
2233
|
+
result.value.nextState,
|
|
2234
|
+
result.value.archivedRecords.map((record) => record.version),
|
|
2235
|
+
timestamp
|
|
2236
|
+
)
|
|
2237
|
+
],
|
|
2238
|
+
transitionEvent("version.published", state, result.value.nextState, [draftRecord.version], timestamp)
|
|
2239
|
+
],
|
|
2240
|
+
nextVersion: result.value.nextState.nextVersion,
|
|
2126
2241
|
timestamp
|
|
2127
2242
|
}
|
|
2128
2243
|
}
|
|
@@ -2139,6 +2254,29 @@ function deleteDraft(state, options = {}) {
|
|
|
2139
2254
|
value: { nextState: { ...stateWithoutDraft, revision: state.revision + 1 } }
|
|
2140
2255
|
};
|
|
2141
2256
|
}
|
|
2257
|
+
function createDeleteDraftTransitionPlan(state, draftRecord, options = {}) {
|
|
2258
|
+
if (draftRecord.formId !== state.formId || draftRecord.version !== state.draftVersion || draftRecord.status !== "draft" || draftRecord.schema.id !== state.formId || draftRecord.schema.version !== draftRecord.version) {
|
|
2259
|
+
return { success: false, error: { type: "draft_not_found" } };
|
|
2260
|
+
}
|
|
2261
|
+
const result = deleteDraft(state, options);
|
|
2262
|
+
if (!result.success) return result;
|
|
2263
|
+
const timestamp = requireTimestamp(options.deletedAt, "deletedAt");
|
|
2264
|
+
return {
|
|
2265
|
+
success: true,
|
|
2266
|
+
value: {
|
|
2267
|
+
nextState: result.value.nextState,
|
|
2268
|
+
plan: {
|
|
2269
|
+
formId: state.formId,
|
|
2270
|
+
expectedRevision: options.expectedRevision ?? state.revision,
|
|
2271
|
+
nextRevision: result.value.nextState.revision,
|
|
2272
|
+
draftToDeleteVersion: draftRecord.version,
|
|
2273
|
+
events: [transitionEvent("draft.deleted", state, result.value.nextState, [draftRecord.version], timestamp)],
|
|
2274
|
+
nextVersion: result.value.nextState.nextVersion,
|
|
2275
|
+
timestamp
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2142
2280
|
function assertVersionMutable(status) {
|
|
2143
2281
|
if (status !== "draft") {
|
|
2144
2282
|
const error = { type: "version_immutable", status };
|
|
@@ -2156,18 +2294,24 @@ export {
|
|
|
2156
2294
|
calculatePageVisibility,
|
|
2157
2295
|
cloneVersionToDraft,
|
|
2158
2296
|
collectSchemaLocales,
|
|
2297
|
+
createCloneTransitionPlan,
|
|
2298
|
+
createDeleteDraftTransitionPlan,
|
|
2159
2299
|
createPublishTransitionPlan,
|
|
2160
2300
|
createResponseAccumulator,
|
|
2161
2301
|
createSubmission,
|
|
2162
2302
|
decodeSubmissionCursor,
|
|
2303
|
+
decodeTextAnswerCursor,
|
|
2163
2304
|
deleteDraft,
|
|
2164
2305
|
dispatchWebhook,
|
|
2165
2306
|
encodeSubmissionCursor,
|
|
2307
|
+
encodeTextAnswerCursor,
|
|
2166
2308
|
escapeCsvCell,
|
|
2167
2309
|
exportResponsesToCsv,
|
|
2168
2310
|
exportResponsesToCsvStream,
|
|
2169
2311
|
isDisplayConditionSatisfied,
|
|
2170
2312
|
isQuestionVisible,
|
|
2313
|
+
jsonValuesEqual,
|
|
2314
|
+
matchesSubmissionFilter,
|
|
2171
2315
|
matchesSubmissionPageFilters,
|
|
2172
2316
|
normalizeSubmissionPageSize,
|
|
2173
2317
|
pipeResponsesToCsvStream,
|