@form-engine-ts/core 2.5.1 → 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 +11 -5
- package/dist/index.cjs +211 -12
- package/dist/index.d.cts +113 -15
- package/dist/index.d.ts +113 -15
- package/dist/index.js +204 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,22 +76,28 @@ Results are ordered by `submittedAt`, then submission ID. Both boundaries are in
|
|
|
76
76
|
|
|
77
77
|
## Versioning, incremental analytics, and paged storage
|
|
78
78
|
|
|
79
|
-
`cloneVersionToDraft`, `publishDraft`, and `deleteDraft` implement revision-checked version transitions as
|
|
79
|
+
`cloneVersionToDraft`, asynchronous `publishDraft`, and `deleteDraft` implement revision-checked version transitions as
|
|
80
|
+
pure functions. `createCloneTransitionPlan`, `createPublishTransitionPlan`, and `createDeleteDraftTransitionPlan` produce
|
|
81
|
+
complete persistence plans with the next state, affected records, and immutable audit events.
|
|
80
82
|
Clone/delete operations accept `expectedRevision`; cloning rejects non-published sources, publish validation failures are
|
|
81
|
-
returned as typed `validation_failed` issues, and successful publishing
|
|
82
|
-
|
|
83
|
+
returned as typed `validation_failed` issues, and successful publishing archives only a supplied actual published record,
|
|
84
|
+
preserving its schema and metadata. `createPublishTransitionPlan` returns complete records plus expected/next revisions for
|
|
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.
|
|
83
87
|
`createResponseAccumulator` incrementally counts choices, answered/unanswered values, and numeric summaries without retaining
|
|
84
88
|
free-text bodies. In lenient mode, mismatched responses are skipped and exposed by `addMany()` and `getReport()` instead of
|
|
85
89
|
being included silently. Independent accumulators for the same schema can be merged, and `finalize()` matches
|
|
86
90
|
`aggregateResponses`.
|
|
87
91
|
|
|
88
92
|
`exportResponsesToCsvStream` accepts an `AsyncIterable`, emits the BOM/header and one chunk per response, and supports
|
|
89
|
-
custom `CsvColumnDef` columns. Custom getters receive the submission, form version, and schema. Use
|
|
93
|
+
custom `CsvColumnDef` columns. Custom getters may be asynchronous and receive the submission, form version, and schema. Use
|
|
90
94
|
`pipeResponsesToCsvStream` to write to a Web `WritableStream` or Node-compatible writable while honoring backpressure.
|
|
91
95
|
Formula-injection neutralization applies to both default and custom columns.
|
|
92
96
|
|
|
93
97
|
Adapters implementing `PagedSubmissionStorageAdapter` expose `listSubmissionPage(formId, options)`. The opaque Base64
|
|
94
98
|
cursor combines `submittedAt` and response ID, so equal timestamps do not produce gaps or duplicates. `metadataFilters`
|
|
95
|
-
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.
|
|
96
102
|
|
|
97
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,17 +30,24 @@ __export(index_exports, {
|
|
|
30
30
|
calculatePageVisibility: () => calculatePageVisibility,
|
|
31
31
|
cloneVersionToDraft: () => cloneVersionToDraft,
|
|
32
32
|
collectSchemaLocales: () => collectSchemaLocales,
|
|
33
|
+
createCloneTransitionPlan: () => createCloneTransitionPlan,
|
|
34
|
+
createDeleteDraftTransitionPlan: () => createDeleteDraftTransitionPlan,
|
|
35
|
+
createPublishTransitionPlan: () => createPublishTransitionPlan,
|
|
33
36
|
createResponseAccumulator: () => createResponseAccumulator,
|
|
34
37
|
createSubmission: () => createSubmission,
|
|
35
38
|
decodeSubmissionCursor: () => decodeSubmissionCursor,
|
|
39
|
+
decodeTextAnswerCursor: () => decodeTextAnswerCursor,
|
|
36
40
|
deleteDraft: () => deleteDraft,
|
|
37
41
|
dispatchWebhook: () => dispatchWebhook,
|
|
38
42
|
encodeSubmissionCursor: () => encodeSubmissionCursor,
|
|
43
|
+
encodeTextAnswerCursor: () => encodeTextAnswerCursor,
|
|
39
44
|
escapeCsvCell: () => escapeCsvCell,
|
|
40
45
|
exportResponsesToCsv: () => exportResponsesToCsv,
|
|
41
46
|
exportResponsesToCsvStream: () => exportResponsesToCsvStream,
|
|
42
47
|
isDisplayConditionSatisfied: () => isDisplayConditionSatisfied,
|
|
43
48
|
isQuestionVisible: () => isQuestionVisible,
|
|
49
|
+
jsonValuesEqual: () => jsonValuesEqual,
|
|
50
|
+
matchesSubmissionFilter: () => matchesSubmissionFilter,
|
|
44
51
|
matchesSubmissionPageFilters: () => matchesSubmissionPageFilters,
|
|
45
52
|
normalizeSubmissionPageSize: () => normalizeSubmissionPageSize,
|
|
46
53
|
pipeResponsesToCsvStream: () => pipeResponsesToCsvStream,
|
|
@@ -1346,7 +1353,7 @@ async function* exportResponsesToCsvStream(schema, submissions, options = {}) {
|
|
|
1346
1353
|
formVersion: response.formVersion ?? schema.version,
|
|
1347
1354
|
schema
|
|
1348
1355
|
};
|
|
1349
|
-
const customCells = customColumns.map((column) => column.getValue(context));
|
|
1356
|
+
const customCells = await Promise.all(customColumns.map((column) => column.getValue(context)));
|
|
1350
1357
|
yield `\r
|
|
1351
1358
|
${[...defaultCells, ...customCells].map((value) => escapeCsvCell(value, neutralizeFormulas)).join(",")}`;
|
|
1352
1359
|
}
|
|
@@ -1564,6 +1571,24 @@ function decodeSubmissionCursor(cursor) {
|
|
|
1564
1571
|
throw new TypeError("cursor must be a valid form-engine cursor.", { cause });
|
|
1565
1572
|
}
|
|
1566
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
|
+
}
|
|
1567
1592
|
function normalizeSubmissionPageSize(pageSize, fallback = 100) {
|
|
1568
1593
|
const value = pageSize ?? fallback;
|
|
1569
1594
|
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("pageSize must be a positive safe integer.");
|
|
@@ -1588,8 +1613,39 @@ function jsonValuesEqual(left, right) {
|
|
|
1588
1613
|
const rightKeys = Object.keys(right);
|
|
1589
1614
|
return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.hasOwn(right, key) && jsonValuesEqual(left[key], right[key]));
|
|
1590
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
|
+
}
|
|
1591
1645
|
function matchesSubmissionPageFilters(submission, options) {
|
|
1592
|
-
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
|
+
}
|
|
1593
1649
|
if (options.metadataFilters === void 0) return true;
|
|
1594
1650
|
return Object.entries(options.metadataFilters).every(
|
|
1595
1651
|
([key, value]) => jsonValuesEqual(submission.metadata?.[key], value)
|
|
@@ -2048,6 +2104,21 @@ function validateState(state) {
|
|
|
2048
2104
|
throw new TypeError("revision must be a non-negative safe integer.");
|
|
2049
2105
|
}
|
|
2050
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
|
+
}
|
|
2051
2122
|
function cloneVersionToDraft(state, sourceSchema, options = {}) {
|
|
2052
2123
|
validateState(state);
|
|
2053
2124
|
if (sourceSchema.id !== state.formId) throw new TypeError("sourceSchema.id must match state.formId.");
|
|
@@ -2091,31 +2162,77 @@ function cloneVersionToDraft(state, sourceSchema, options = {}) {
|
|
|
2091
2162
|
}
|
|
2092
2163
|
};
|
|
2093
2164
|
}
|
|
2094
|
-
function
|
|
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
|
+
}
|
|
2205
|
+
function validatePublishedRecord(state, record) {
|
|
2206
|
+
if (record === void 0) return;
|
|
2207
|
+
if (record.formId !== state.formId || record.version !== state.publishedVersion || record.status !== "published" || record.schema.id !== record.formId || record.schema.version !== record.version) {
|
|
2208
|
+
throw new TypeError("currentPublishedRecord must match the state's published version.");
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
function transitionTimestamp(options) {
|
|
2212
|
+
return requireTimestamp(options.publishedAt ?? options.timestamp, "publishedAt");
|
|
2213
|
+
}
|
|
2214
|
+
async function publishDraft(state, draftSchema, options = {}) {
|
|
2095
2215
|
validateState(state);
|
|
2096
2216
|
const conflict = revisionConflict(state, options.expectedRevision);
|
|
2097
2217
|
if (conflict !== void 0) return conflict;
|
|
2098
2218
|
if (state.draftVersion === void 0 || draftSchema.id !== state.formId || draftSchema.version !== state.draftVersion) {
|
|
2099
2219
|
return { success: false, error: { type: "draft_not_found" } };
|
|
2100
2220
|
}
|
|
2101
|
-
|
|
2221
|
+
validatePublishedRecord(state, options.currentPublishedRecord);
|
|
2222
|
+
const validation = await options.validate?.(draftSchema);
|
|
2102
2223
|
if (validation === false || Array.isArray(validation) && validation.length > 0) {
|
|
2103
2224
|
return {
|
|
2104
2225
|
success: false,
|
|
2105
2226
|
error: { type: "validation_failed", issues: Array.isArray(validation) ? validation : [] }
|
|
2106
2227
|
};
|
|
2107
2228
|
}
|
|
2108
|
-
const timestamp = options
|
|
2109
|
-
if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError("timestamp must be a valid date string.");
|
|
2229
|
+
const timestamp = transitionTimestamp(options);
|
|
2110
2230
|
const archivedVersion = state.publishedVersion;
|
|
2111
|
-
const archivedRecords =
|
|
2231
|
+
const archivedRecords = options.currentPublishedRecord === void 0 ? [] : [
|
|
2112
2232
|
{
|
|
2113
|
-
|
|
2114
|
-
version: archivedVersion,
|
|
2233
|
+
...options.currentPublishedRecord,
|
|
2115
2234
|
status: "archived",
|
|
2116
|
-
|
|
2117
|
-
createdAt: timestamp,
|
|
2118
|
-
publishedAt: timestamp,
|
|
2235
|
+
revision: options.currentPublishedRecord.revision + 1,
|
|
2119
2236
|
archivedAt: timestamp
|
|
2120
2237
|
}
|
|
2121
2238
|
];
|
|
@@ -2133,6 +2250,8 @@ function publishDraft(state, draftSchema, options = {}) {
|
|
|
2133
2250
|
version: draftSchema.version,
|
|
2134
2251
|
status: "published",
|
|
2135
2252
|
schema: draftSchema,
|
|
2253
|
+
revision: 1,
|
|
2254
|
+
...archivedVersion === void 0 ? {} : { createdFromVersion: archivedVersion },
|
|
2136
2255
|
createdAt: timestamp,
|
|
2137
2256
|
publishedAt: timestamp
|
|
2138
2257
|
},
|
|
@@ -2141,6 +2260,56 @@ function publishDraft(state, draftSchema, options = {}) {
|
|
|
2141
2260
|
}
|
|
2142
2261
|
};
|
|
2143
2262
|
}
|
|
2263
|
+
async function createPublishTransitionPlan(state, draftRecord, options = {}) {
|
|
2264
|
+
if (draftRecord.formId !== state.formId || draftRecord.version !== state.draftVersion || draftRecord.schema.id !== draftRecord.formId || draftRecord.schema.version !== draftRecord.version) {
|
|
2265
|
+
return { success: false, error: { type: "draft_not_found" } };
|
|
2266
|
+
}
|
|
2267
|
+
if (draftRecord.status !== "draft") {
|
|
2268
|
+
return { success: false, error: { type: "version_immutable", status: draftRecord.status } };
|
|
2269
|
+
}
|
|
2270
|
+
const expectedRevision = options.expectedRevision ?? state.revision;
|
|
2271
|
+
const timestamp = transitionTimestamp(options);
|
|
2272
|
+
const result = await publishDraft(state, draftRecord.schema, {
|
|
2273
|
+
...options,
|
|
2274
|
+
expectedRevision,
|
|
2275
|
+
publishedAt: timestamp
|
|
2276
|
+
});
|
|
2277
|
+
if (!result.success) return result;
|
|
2278
|
+
const publishedRecordToSave = {
|
|
2279
|
+
...draftRecord,
|
|
2280
|
+
status: "published",
|
|
2281
|
+
revision: draftRecord.revision + 1,
|
|
2282
|
+
publishedAt: timestamp
|
|
2283
|
+
};
|
|
2284
|
+
return {
|
|
2285
|
+
success: true,
|
|
2286
|
+
value: {
|
|
2287
|
+
nextState: result.value.nextState,
|
|
2288
|
+
plan: {
|
|
2289
|
+
formId: state.formId,
|
|
2290
|
+
expectedRevision,
|
|
2291
|
+
nextRevision: result.value.nextState.revision,
|
|
2292
|
+
draftToDeleteVersion: draftRecord.version,
|
|
2293
|
+
publishedRecordToSave,
|
|
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,
|
|
2308
|
+
timestamp
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2144
2313
|
function deleteDraft(state, options = {}) {
|
|
2145
2314
|
validateState(state);
|
|
2146
2315
|
const conflict = revisionConflict(state, options.expectedRevision);
|
|
@@ -2152,6 +2321,29 @@ function deleteDraft(state, options = {}) {
|
|
|
2152
2321
|
value: { nextState: { ...stateWithoutDraft, revision: state.revision + 1 } }
|
|
2153
2322
|
};
|
|
2154
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
|
+
}
|
|
2155
2347
|
function assertVersionMutable(status) {
|
|
2156
2348
|
if (status !== "draft") {
|
|
2157
2349
|
const error = { type: "version_immutable", status };
|
|
@@ -2170,17 +2362,24 @@ function assertVersionMutable(status) {
|
|
|
2170
2362
|
calculatePageVisibility,
|
|
2171
2363
|
cloneVersionToDraft,
|
|
2172
2364
|
collectSchemaLocales,
|
|
2365
|
+
createCloneTransitionPlan,
|
|
2366
|
+
createDeleteDraftTransitionPlan,
|
|
2367
|
+
createPublishTransitionPlan,
|
|
2173
2368
|
createResponseAccumulator,
|
|
2174
2369
|
createSubmission,
|
|
2175
2370
|
decodeSubmissionCursor,
|
|
2371
|
+
decodeTextAnswerCursor,
|
|
2176
2372
|
deleteDraft,
|
|
2177
2373
|
dispatchWebhook,
|
|
2178
2374
|
encodeSubmissionCursor,
|
|
2375
|
+
encodeTextAnswerCursor,
|
|
2179
2376
|
escapeCsvCell,
|
|
2180
2377
|
exportResponsesToCsv,
|
|
2181
2378
|
exportResponsesToCsvStream,
|
|
2182
2379
|
isDisplayConditionSatisfied,
|
|
2183
2380
|
isQuestionVisible,
|
|
2381
|
+
jsonValuesEqual,
|
|
2382
|
+
matchesSubmissionFilter,
|
|
2184
2383
|
matchesSubmissionPageFilters,
|
|
2185
2384
|
normalizeSubmissionPageSize,
|
|
2186
2385
|
pipeResponsesToCsvStream,
|
package/dist/index.d.cts
CHANGED
|
@@ -6,11 +6,13 @@ type Result<T, E> = {
|
|
|
6
6
|
readonly error: E;
|
|
7
7
|
};
|
|
8
8
|
type FormVersionStatus = "draft" | "published" | "archived";
|
|
9
|
-
interface FormVersionRecord {
|
|
9
|
+
interface FormVersionRecord extends ExtensibleNode {
|
|
10
10
|
readonly formId: string;
|
|
11
11
|
readonly version: number;
|
|
12
12
|
readonly status: FormVersionStatus;
|
|
13
13
|
readonly schema: FormSchema;
|
|
14
|
+
readonly revision: number;
|
|
15
|
+
readonly createdFromVersion?: number;
|
|
14
16
|
readonly createdAt: string;
|
|
15
17
|
readonly publishedAt?: string;
|
|
16
18
|
readonly archivedAt?: string;
|
|
@@ -22,6 +24,14 @@ interface FormVersionState {
|
|
|
22
24
|
readonly nextVersion: number;
|
|
23
25
|
readonly revision: number;
|
|
24
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
|
+
}
|
|
25
35
|
type VersionTransitionError = {
|
|
26
36
|
readonly type: "draft_already_exists";
|
|
27
37
|
readonly currentDraftVersion: number;
|
|
@@ -48,17 +58,22 @@ type VersionTransitionError = {
|
|
|
48
58
|
interface CloneVersionOptions {
|
|
49
59
|
readonly maxVersions?: number;
|
|
50
60
|
readonly expectedRevision?: number;
|
|
61
|
+
readonly clonedAt?: string;
|
|
62
|
+
readonly metadata?: Readonly<Record<string, JsonValue>>;
|
|
51
63
|
/** Additional known published versions that may be used as a clone source. */
|
|
52
64
|
readonly allowedSourceVersions?: readonly number[];
|
|
53
65
|
}
|
|
54
66
|
interface PublishDraftOptions {
|
|
55
67
|
readonly expectedRevision?: number;
|
|
56
|
-
readonly
|
|
57
|
-
|
|
68
|
+
readonly currentPublishedRecord?: FormVersionRecord;
|
|
69
|
+
readonly validate?: (schema: FormSchema) => boolean | Promise<boolean> | readonly SchemaIssue[] | Promise<readonly SchemaIssue[]>;
|
|
70
|
+
readonly publishedAt?: string;
|
|
71
|
+
/** @deprecated Use publishedAt. */
|
|
58
72
|
readonly timestamp?: string;
|
|
59
73
|
}
|
|
60
74
|
interface DeleteDraftOptions {
|
|
61
75
|
readonly expectedRevision?: number;
|
|
76
|
+
readonly deletedAt?: string;
|
|
62
77
|
}
|
|
63
78
|
interface PublishDraftResult {
|
|
64
79
|
readonly nextState: FormVersionState;
|
|
@@ -71,10 +86,22 @@ declare function cloneVersionToDraft(state: FormVersionState, sourceSchema: Form
|
|
|
71
86
|
readonly nextState: FormVersionState;
|
|
72
87
|
readonly draftSchema: FormSchema;
|
|
73
88
|
}, VersionTransitionError>;
|
|
74
|
-
declare function
|
|
89
|
+
declare function createCloneTransitionPlan(state: FormVersionState, sourceRecord: FormVersionRecord, options?: CloneVersionOptions): Result<{
|
|
90
|
+
readonly nextState: FormVersionState;
|
|
91
|
+
readonly plan: VersionTransitionPlan;
|
|
92
|
+
}, VersionTransitionError>;
|
|
93
|
+
declare function publishDraft(state: FormVersionState, draftSchema: FormSchema, options?: PublishDraftOptions): Promise<Result<PublishDraftResult, VersionTransitionError>>;
|
|
94
|
+
declare function createPublishTransitionPlan(state: FormVersionState, draftRecord: FormVersionRecord, options?: PublishDraftOptions): Promise<Result<{
|
|
95
|
+
readonly nextState: FormVersionState;
|
|
96
|
+
readonly plan: VersionTransitionPlan;
|
|
97
|
+
}, VersionTransitionError>>;
|
|
75
98
|
declare function deleteDraft(state: FormVersionState, options?: DeleteDraftOptions): Result<{
|
|
76
99
|
readonly nextState: FormVersionState;
|
|
77
100
|
}, VersionTransitionError>;
|
|
101
|
+
declare function createDeleteDraftTransitionPlan(state: FormVersionState, draftRecord: FormVersionRecord, options?: DeleteDraftOptions): Result<{
|
|
102
|
+
readonly nextState: FormVersionState;
|
|
103
|
+
readonly plan: VersionTransitionPlan;
|
|
104
|
+
}, VersionTransitionError>;
|
|
78
105
|
declare function assertVersionMutable(status: FormVersionStatus): void;
|
|
79
106
|
|
|
80
107
|
type FieldType = "text" | "textarea" | "number" | "rating" | "select" | "multi-select" | "checkbox" | "radio";
|
|
@@ -249,9 +276,34 @@ interface SubmissionPageQueryOptions {
|
|
|
249
276
|
readonly since?: string;
|
|
250
277
|
readonly until?: string;
|
|
251
278
|
readonly locale?: string;
|
|
252
|
-
readonly filter?: (submission: FormSubmission) => boolean;
|
|
279
|
+
readonly filter?: SubmissionFilter | ((submission: FormSubmission) => boolean);
|
|
280
|
+
/** @deprecated Prefer the generic filter AST. */
|
|
253
281
|
readonly metadataFilters?: Readonly<Record<string, JsonValue>>;
|
|
254
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
|
+
};
|
|
255
307
|
interface SubmissionPage {
|
|
256
308
|
readonly items: readonly FormSubmission[];
|
|
257
309
|
readonly nextCursor?: string;
|
|
@@ -259,19 +311,57 @@ interface SubmissionPage {
|
|
|
259
311
|
}
|
|
260
312
|
interface PagedSubmissionStorageAdapter extends FormStorageAdapter {
|
|
261
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;
|
|
262
330
|
}
|
|
263
331
|
interface VersionTransitionPlan {
|
|
264
|
-
readonly
|
|
332
|
+
readonly formId: string;
|
|
265
333
|
readonly expectedRevision: number;
|
|
266
|
-
readonly
|
|
267
|
-
readonly
|
|
268
|
-
readonly
|
|
334
|
+
readonly nextRevision: number;
|
|
335
|
+
readonly draftToCreate?: FormVersionRecord;
|
|
336
|
+
readonly draftToDeleteVersion?: number;
|
|
337
|
+
readonly publishedRecordToSave?: FormVersionRecord;
|
|
338
|
+
readonly archivedRecordsToSave?: readonly FormVersionRecord[];
|
|
339
|
+
readonly events: readonly VersionTransitionEvent[];
|
|
340
|
+
/** The complete next state value used by persistent adapters. */
|
|
341
|
+
readonly nextVersion?: number;
|
|
342
|
+
readonly timestamp: string;
|
|
269
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
|
+
};
|
|
270
358
|
interface VersionedFormStorageAdapter extends FormStorageAdapter {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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>>;
|
|
275
365
|
}
|
|
276
366
|
interface BaseQuestionAggregate {
|
|
277
367
|
readonly fieldId: string;
|
|
@@ -377,7 +467,7 @@ interface CsvExportOptions {
|
|
|
377
467
|
}
|
|
378
468
|
interface CsvColumnDef {
|
|
379
469
|
readonly header: string;
|
|
380
|
-
readonly getValue: (context: CsvColumnContext) => string | number | boolean | null | undefined
|
|
470
|
+
readonly getValue: (context: CsvColumnContext) => string | number | boolean | null | undefined | Promise<string | number | boolean | null | undefined>;
|
|
381
471
|
}
|
|
382
472
|
interface CsvColumnContext extends FormResponse {
|
|
383
473
|
readonly submission: FormResponse;
|
|
@@ -431,9 +521,17 @@ interface SubmissionCursorValue {
|
|
|
431
521
|
readonly submittedAt: string;
|
|
432
522
|
readonly responseId: string;
|
|
433
523
|
}
|
|
524
|
+
interface TextAnswerCursorValue {
|
|
525
|
+
readonly responseId: string;
|
|
526
|
+
readonly fieldId: string;
|
|
527
|
+
}
|
|
434
528
|
declare function encodeSubmissionCursor(value: SubmissionCursorValue): string;
|
|
435
529
|
declare function decodeSubmissionCursor(cursor: string): SubmissionCursorValue;
|
|
530
|
+
declare function encodeTextAnswerCursor(value: TextAnswerCursorValue): string;
|
|
531
|
+
declare function decodeTextAnswerCursor(cursor: string): TextAnswerCursorValue;
|
|
436
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;
|
|
437
535
|
declare function matchesSubmissionPageFilters(submission: FormSubmission, options: Pick<SubmissionPageQueryOptions, "filter" | "metadataFilters">): boolean;
|
|
438
536
|
|
|
439
537
|
interface CollectedLocales {
|
|
@@ -509,4 +607,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
|
|
|
509
607
|
declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
|
|
510
608
|
declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
|
|
511
609
|
|
|
512
|
-
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, 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
|
@@ -6,11 +6,13 @@ type Result<T, E> = {
|
|
|
6
6
|
readonly error: E;
|
|
7
7
|
};
|
|
8
8
|
type FormVersionStatus = "draft" | "published" | "archived";
|
|
9
|
-
interface FormVersionRecord {
|
|
9
|
+
interface FormVersionRecord extends ExtensibleNode {
|
|
10
10
|
readonly formId: string;
|
|
11
11
|
readonly version: number;
|
|
12
12
|
readonly status: FormVersionStatus;
|
|
13
13
|
readonly schema: FormSchema;
|
|
14
|
+
readonly revision: number;
|
|
15
|
+
readonly createdFromVersion?: number;
|
|
14
16
|
readonly createdAt: string;
|
|
15
17
|
readonly publishedAt?: string;
|
|
16
18
|
readonly archivedAt?: string;
|
|
@@ -22,6 +24,14 @@ interface FormVersionState {
|
|
|
22
24
|
readonly nextVersion: number;
|
|
23
25
|
readonly revision: number;
|
|
24
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
|
+
}
|
|
25
35
|
type VersionTransitionError = {
|
|
26
36
|
readonly type: "draft_already_exists";
|
|
27
37
|
readonly currentDraftVersion: number;
|
|
@@ -48,17 +58,22 @@ type VersionTransitionError = {
|
|
|
48
58
|
interface CloneVersionOptions {
|
|
49
59
|
readonly maxVersions?: number;
|
|
50
60
|
readonly expectedRevision?: number;
|
|
61
|
+
readonly clonedAt?: string;
|
|
62
|
+
readonly metadata?: Readonly<Record<string, JsonValue>>;
|
|
51
63
|
/** Additional known published versions that may be used as a clone source. */
|
|
52
64
|
readonly allowedSourceVersions?: readonly number[];
|
|
53
65
|
}
|
|
54
66
|
interface PublishDraftOptions {
|
|
55
67
|
readonly expectedRevision?: number;
|
|
56
|
-
readonly
|
|
57
|
-
|
|
68
|
+
readonly currentPublishedRecord?: FormVersionRecord;
|
|
69
|
+
readonly validate?: (schema: FormSchema) => boolean | Promise<boolean> | readonly SchemaIssue[] | Promise<readonly SchemaIssue[]>;
|
|
70
|
+
readonly publishedAt?: string;
|
|
71
|
+
/** @deprecated Use publishedAt. */
|
|
58
72
|
readonly timestamp?: string;
|
|
59
73
|
}
|
|
60
74
|
interface DeleteDraftOptions {
|
|
61
75
|
readonly expectedRevision?: number;
|
|
76
|
+
readonly deletedAt?: string;
|
|
62
77
|
}
|
|
63
78
|
interface PublishDraftResult {
|
|
64
79
|
readonly nextState: FormVersionState;
|
|
@@ -71,10 +86,22 @@ declare function cloneVersionToDraft(state: FormVersionState, sourceSchema: Form
|
|
|
71
86
|
readonly nextState: FormVersionState;
|
|
72
87
|
readonly draftSchema: FormSchema;
|
|
73
88
|
}, VersionTransitionError>;
|
|
74
|
-
declare function
|
|
89
|
+
declare function createCloneTransitionPlan(state: FormVersionState, sourceRecord: FormVersionRecord, options?: CloneVersionOptions): Result<{
|
|
90
|
+
readonly nextState: FormVersionState;
|
|
91
|
+
readonly plan: VersionTransitionPlan;
|
|
92
|
+
}, VersionTransitionError>;
|
|
93
|
+
declare function publishDraft(state: FormVersionState, draftSchema: FormSchema, options?: PublishDraftOptions): Promise<Result<PublishDraftResult, VersionTransitionError>>;
|
|
94
|
+
declare function createPublishTransitionPlan(state: FormVersionState, draftRecord: FormVersionRecord, options?: PublishDraftOptions): Promise<Result<{
|
|
95
|
+
readonly nextState: FormVersionState;
|
|
96
|
+
readonly plan: VersionTransitionPlan;
|
|
97
|
+
}, VersionTransitionError>>;
|
|
75
98
|
declare function deleteDraft(state: FormVersionState, options?: DeleteDraftOptions): Result<{
|
|
76
99
|
readonly nextState: FormVersionState;
|
|
77
100
|
}, VersionTransitionError>;
|
|
101
|
+
declare function createDeleteDraftTransitionPlan(state: FormVersionState, draftRecord: FormVersionRecord, options?: DeleteDraftOptions): Result<{
|
|
102
|
+
readonly nextState: FormVersionState;
|
|
103
|
+
readonly plan: VersionTransitionPlan;
|
|
104
|
+
}, VersionTransitionError>;
|
|
78
105
|
declare function assertVersionMutable(status: FormVersionStatus): void;
|
|
79
106
|
|
|
80
107
|
type FieldType = "text" | "textarea" | "number" | "rating" | "select" | "multi-select" | "checkbox" | "radio";
|
|
@@ -249,9 +276,34 @@ interface SubmissionPageQueryOptions {
|
|
|
249
276
|
readonly since?: string;
|
|
250
277
|
readonly until?: string;
|
|
251
278
|
readonly locale?: string;
|
|
252
|
-
readonly filter?: (submission: FormSubmission) => boolean;
|
|
279
|
+
readonly filter?: SubmissionFilter | ((submission: FormSubmission) => boolean);
|
|
280
|
+
/** @deprecated Prefer the generic filter AST. */
|
|
253
281
|
readonly metadataFilters?: Readonly<Record<string, JsonValue>>;
|
|
254
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
|
+
};
|
|
255
307
|
interface SubmissionPage {
|
|
256
308
|
readonly items: readonly FormSubmission[];
|
|
257
309
|
readonly nextCursor?: string;
|
|
@@ -259,19 +311,57 @@ interface SubmissionPage {
|
|
|
259
311
|
}
|
|
260
312
|
interface PagedSubmissionStorageAdapter extends FormStorageAdapter {
|
|
261
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;
|
|
262
330
|
}
|
|
263
331
|
interface VersionTransitionPlan {
|
|
264
|
-
readonly
|
|
332
|
+
readonly formId: string;
|
|
265
333
|
readonly expectedRevision: number;
|
|
266
|
-
readonly
|
|
267
|
-
readonly
|
|
268
|
-
readonly
|
|
334
|
+
readonly nextRevision: number;
|
|
335
|
+
readonly draftToCreate?: FormVersionRecord;
|
|
336
|
+
readonly draftToDeleteVersion?: number;
|
|
337
|
+
readonly publishedRecordToSave?: FormVersionRecord;
|
|
338
|
+
readonly archivedRecordsToSave?: readonly FormVersionRecord[];
|
|
339
|
+
readonly events: readonly VersionTransitionEvent[];
|
|
340
|
+
/** The complete next state value used by persistent adapters. */
|
|
341
|
+
readonly nextVersion?: number;
|
|
342
|
+
readonly timestamp: string;
|
|
269
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
|
+
};
|
|
270
358
|
interface VersionedFormStorageAdapter extends FormStorageAdapter {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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>>;
|
|
275
365
|
}
|
|
276
366
|
interface BaseQuestionAggregate {
|
|
277
367
|
readonly fieldId: string;
|
|
@@ -377,7 +467,7 @@ interface CsvExportOptions {
|
|
|
377
467
|
}
|
|
378
468
|
interface CsvColumnDef {
|
|
379
469
|
readonly header: string;
|
|
380
|
-
readonly getValue: (context: CsvColumnContext) => string | number | boolean | null | undefined
|
|
470
|
+
readonly getValue: (context: CsvColumnContext) => string | number | boolean | null | undefined | Promise<string | number | boolean | null | undefined>;
|
|
381
471
|
}
|
|
382
472
|
interface CsvColumnContext extends FormResponse {
|
|
383
473
|
readonly submission: FormResponse;
|
|
@@ -431,9 +521,17 @@ interface SubmissionCursorValue {
|
|
|
431
521
|
readonly submittedAt: string;
|
|
432
522
|
readonly responseId: string;
|
|
433
523
|
}
|
|
524
|
+
interface TextAnswerCursorValue {
|
|
525
|
+
readonly responseId: string;
|
|
526
|
+
readonly fieldId: string;
|
|
527
|
+
}
|
|
434
528
|
declare function encodeSubmissionCursor(value: SubmissionCursorValue): string;
|
|
435
529
|
declare function decodeSubmissionCursor(cursor: string): SubmissionCursorValue;
|
|
530
|
+
declare function encodeTextAnswerCursor(value: TextAnswerCursorValue): string;
|
|
531
|
+
declare function decodeTextAnswerCursor(cursor: string): TextAnswerCursorValue;
|
|
436
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;
|
|
437
535
|
declare function matchesSubmissionPageFilters(submission: FormSubmission, options: Pick<SubmissionPageQueryOptions, "filter" | "metadataFilters">): boolean;
|
|
438
536
|
|
|
439
537
|
interface CollectedLocales {
|
|
@@ -509,4 +607,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
|
|
|
509
607
|
declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
|
|
510
608
|
declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
|
|
511
609
|
|
|
512
|
-
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, 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
|
@@ -1286,7 +1286,7 @@ async function* exportResponsesToCsvStream(schema, submissions, options = {}) {
|
|
|
1286
1286
|
formVersion: response.formVersion ?? schema.version,
|
|
1287
1287
|
schema
|
|
1288
1288
|
};
|
|
1289
|
-
const customCells = customColumns.map((column) => column.getValue(context));
|
|
1289
|
+
const customCells = await Promise.all(customColumns.map((column) => column.getValue(context)));
|
|
1290
1290
|
yield `\r
|
|
1291
1291
|
${[...defaultCells, ...customCells].map((value) => escapeCsvCell(value, neutralizeFormulas)).join(",")}`;
|
|
1292
1292
|
}
|
|
@@ -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,31 +2095,77 @@ function cloneVersionToDraft(state, sourceSchema, options = {}) {
|
|
|
2031
2095
|
}
|
|
2032
2096
|
};
|
|
2033
2097
|
}
|
|
2034
|
-
function
|
|
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
|
+
}
|
|
2138
|
+
function validatePublishedRecord(state, record) {
|
|
2139
|
+
if (record === void 0) return;
|
|
2140
|
+
if (record.formId !== state.formId || record.version !== state.publishedVersion || record.status !== "published" || record.schema.id !== record.formId || record.schema.version !== record.version) {
|
|
2141
|
+
throw new TypeError("currentPublishedRecord must match the state's published version.");
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
function transitionTimestamp(options) {
|
|
2145
|
+
return requireTimestamp(options.publishedAt ?? options.timestamp, "publishedAt");
|
|
2146
|
+
}
|
|
2147
|
+
async function publishDraft(state, draftSchema, options = {}) {
|
|
2035
2148
|
validateState(state);
|
|
2036
2149
|
const conflict = revisionConflict(state, options.expectedRevision);
|
|
2037
2150
|
if (conflict !== void 0) return conflict;
|
|
2038
2151
|
if (state.draftVersion === void 0 || draftSchema.id !== state.formId || draftSchema.version !== state.draftVersion) {
|
|
2039
2152
|
return { success: false, error: { type: "draft_not_found" } };
|
|
2040
2153
|
}
|
|
2041
|
-
|
|
2154
|
+
validatePublishedRecord(state, options.currentPublishedRecord);
|
|
2155
|
+
const validation = await options.validate?.(draftSchema);
|
|
2042
2156
|
if (validation === false || Array.isArray(validation) && validation.length > 0) {
|
|
2043
2157
|
return {
|
|
2044
2158
|
success: false,
|
|
2045
2159
|
error: { type: "validation_failed", issues: Array.isArray(validation) ? validation : [] }
|
|
2046
2160
|
};
|
|
2047
2161
|
}
|
|
2048
|
-
const timestamp = options
|
|
2049
|
-
if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError("timestamp must be a valid date string.");
|
|
2162
|
+
const timestamp = transitionTimestamp(options);
|
|
2050
2163
|
const archivedVersion = state.publishedVersion;
|
|
2051
|
-
const archivedRecords =
|
|
2164
|
+
const archivedRecords = options.currentPublishedRecord === void 0 ? [] : [
|
|
2052
2165
|
{
|
|
2053
|
-
|
|
2054
|
-
version: archivedVersion,
|
|
2166
|
+
...options.currentPublishedRecord,
|
|
2055
2167
|
status: "archived",
|
|
2056
|
-
|
|
2057
|
-
createdAt: timestamp,
|
|
2058
|
-
publishedAt: timestamp,
|
|
2168
|
+
revision: options.currentPublishedRecord.revision + 1,
|
|
2059
2169
|
archivedAt: timestamp
|
|
2060
2170
|
}
|
|
2061
2171
|
];
|
|
@@ -2073,6 +2183,8 @@ function publishDraft(state, draftSchema, options = {}) {
|
|
|
2073
2183
|
version: draftSchema.version,
|
|
2074
2184
|
status: "published",
|
|
2075
2185
|
schema: draftSchema,
|
|
2186
|
+
revision: 1,
|
|
2187
|
+
...archivedVersion === void 0 ? {} : { createdFromVersion: archivedVersion },
|
|
2076
2188
|
createdAt: timestamp,
|
|
2077
2189
|
publishedAt: timestamp
|
|
2078
2190
|
},
|
|
@@ -2081,6 +2193,56 @@ function publishDraft(state, draftSchema, options = {}) {
|
|
|
2081
2193
|
}
|
|
2082
2194
|
};
|
|
2083
2195
|
}
|
|
2196
|
+
async function createPublishTransitionPlan(state, draftRecord, options = {}) {
|
|
2197
|
+
if (draftRecord.formId !== state.formId || draftRecord.version !== state.draftVersion || draftRecord.schema.id !== draftRecord.formId || draftRecord.schema.version !== draftRecord.version) {
|
|
2198
|
+
return { success: false, error: { type: "draft_not_found" } };
|
|
2199
|
+
}
|
|
2200
|
+
if (draftRecord.status !== "draft") {
|
|
2201
|
+
return { success: false, error: { type: "version_immutable", status: draftRecord.status } };
|
|
2202
|
+
}
|
|
2203
|
+
const expectedRevision = options.expectedRevision ?? state.revision;
|
|
2204
|
+
const timestamp = transitionTimestamp(options);
|
|
2205
|
+
const result = await publishDraft(state, draftRecord.schema, {
|
|
2206
|
+
...options,
|
|
2207
|
+
expectedRevision,
|
|
2208
|
+
publishedAt: timestamp
|
|
2209
|
+
});
|
|
2210
|
+
if (!result.success) return result;
|
|
2211
|
+
const publishedRecordToSave = {
|
|
2212
|
+
...draftRecord,
|
|
2213
|
+
status: "published",
|
|
2214
|
+
revision: draftRecord.revision + 1,
|
|
2215
|
+
publishedAt: timestamp
|
|
2216
|
+
};
|
|
2217
|
+
return {
|
|
2218
|
+
success: true,
|
|
2219
|
+
value: {
|
|
2220
|
+
nextState: result.value.nextState,
|
|
2221
|
+
plan: {
|
|
2222
|
+
formId: state.formId,
|
|
2223
|
+
expectedRevision,
|
|
2224
|
+
nextRevision: result.value.nextState.revision,
|
|
2225
|
+
draftToDeleteVersion: draftRecord.version,
|
|
2226
|
+
publishedRecordToSave,
|
|
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,
|
|
2241
|
+
timestamp
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2084
2246
|
function deleteDraft(state, options = {}) {
|
|
2085
2247
|
validateState(state);
|
|
2086
2248
|
const conflict = revisionConflict(state, options.expectedRevision);
|
|
@@ -2092,6 +2254,29 @@ function deleteDraft(state, options = {}) {
|
|
|
2092
2254
|
value: { nextState: { ...stateWithoutDraft, revision: state.revision + 1 } }
|
|
2093
2255
|
};
|
|
2094
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
|
+
}
|
|
2095
2280
|
function assertVersionMutable(status) {
|
|
2096
2281
|
if (status !== "draft") {
|
|
2097
2282
|
const error = { type: "version_immutable", status };
|
|
@@ -2109,17 +2294,24 @@ export {
|
|
|
2109
2294
|
calculatePageVisibility,
|
|
2110
2295
|
cloneVersionToDraft,
|
|
2111
2296
|
collectSchemaLocales,
|
|
2297
|
+
createCloneTransitionPlan,
|
|
2298
|
+
createDeleteDraftTransitionPlan,
|
|
2299
|
+
createPublishTransitionPlan,
|
|
2112
2300
|
createResponseAccumulator,
|
|
2113
2301
|
createSubmission,
|
|
2114
2302
|
decodeSubmissionCursor,
|
|
2303
|
+
decodeTextAnswerCursor,
|
|
2115
2304
|
deleteDraft,
|
|
2116
2305
|
dispatchWebhook,
|
|
2117
2306
|
encodeSubmissionCursor,
|
|
2307
|
+
encodeTextAnswerCursor,
|
|
2118
2308
|
escapeCsvCell,
|
|
2119
2309
|
exportResponsesToCsv,
|
|
2120
2310
|
exportResponsesToCsvStream,
|
|
2121
2311
|
isDisplayConditionSatisfied,
|
|
2122
2312
|
isQuestionVisible,
|
|
2313
|
+
jsonValuesEqual,
|
|
2314
|
+
matchesSubmissionFilter,
|
|
2123
2315
|
matchesSubmissionPageFilters,
|
|
2124
2316
|
normalizeSubmissionPageSize,
|
|
2125
2317
|
pipeResponsesToCsvStream,
|