@form-engine-ts/core 2.5.0 → 2.6.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 +14 -4
- package/dist/index.cjs +228 -25
- package/dist/index.d.cts +135 -66
- package/dist/index.d.ts +135 -66
- package/dist/index.js +225 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,14 +76,24 @@ 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.
|
|
81
|
+
Clone/delete operations accept `expectedRevision`; cloning rejects non-published sources, publish validation failures are
|
|
82
|
+
returned as typed `validation_failed` issues, and successful publishing archives only a supplied actual published record,
|
|
83
|
+
preserving its schema and metadata. `createPublishTransitionPlan` returns complete records plus expected/next revisions for
|
|
84
|
+
storage adapters implementing `VersionedFormStorageAdapter` to commit atomically.
|
|
80
85
|
`createResponseAccumulator` incrementally counts choices, answered/unanswered values, and numeric summaries without retaining
|
|
81
|
-
free-text bodies.
|
|
86
|
+
free-text bodies. In lenient mode, mismatched responses are skipped and exposed by `addMany()` and `getReport()` instead of
|
|
87
|
+
being included silently. Independent accumulators for the same schema can be merged, and `finalize()` matches
|
|
88
|
+
`aggregateResponses`.
|
|
82
89
|
|
|
83
90
|
`exportResponsesToCsvStream` accepts an `AsyncIterable`, emits the BOM/header and one chunk per response, and supports
|
|
84
|
-
custom `CsvColumnDef` columns.
|
|
91
|
+
custom `CsvColumnDef` columns. Custom getters may be asynchronous and receive the submission, form version, and schema. Use
|
|
92
|
+
`pipeResponsesToCsvStream` to write to a Web `WritableStream` or Node-compatible writable while honoring backpressure.
|
|
93
|
+
Formula-injection neutralization applies to both default and custom columns.
|
|
85
94
|
|
|
86
95
|
Adapters implementing `PagedSubmissionStorageAdapter` expose `listSubmissionPage(formId, options)`. The opaque Base64
|
|
87
|
-
cursor combines `submittedAt` and response ID, so equal timestamps do not produce gaps or duplicates.
|
|
96
|
+
cursor combines `submittedAt` and response ID, so equal timestamps do not produce gaps or duplicates. `metadataFilters`
|
|
97
|
+
and `filter` are applied before page sizing.
|
|
88
98
|
|
|
89
99
|
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,6 +30,7 @@ __export(index_exports, {
|
|
|
30
30
|
calculatePageVisibility: () => calculatePageVisibility,
|
|
31
31
|
cloneVersionToDraft: () => cloneVersionToDraft,
|
|
32
32
|
collectSchemaLocales: () => collectSchemaLocales,
|
|
33
|
+
createPublishTransitionPlan: () => createPublishTransitionPlan,
|
|
33
34
|
createResponseAccumulator: () => createResponseAccumulator,
|
|
34
35
|
createSubmission: () => createSubmission,
|
|
35
36
|
decodeSubmissionCursor: () => decodeSubmissionCursor,
|
|
@@ -41,7 +42,9 @@ __export(index_exports, {
|
|
|
41
42
|
exportResponsesToCsvStream: () => exportResponsesToCsvStream,
|
|
42
43
|
isDisplayConditionSatisfied: () => isDisplayConditionSatisfied,
|
|
43
44
|
isQuestionVisible: () => isQuestionVisible,
|
|
45
|
+
matchesSubmissionPageFilters: () => matchesSubmissionPageFilters,
|
|
44
46
|
normalizeSubmissionPageSize: () => normalizeSubmissionPageSize,
|
|
47
|
+
pipeResponsesToCsvStream: () => pipeResponsesToCsvStream,
|
|
45
48
|
populateSchemaTranslations: () => populateSchemaTranslations,
|
|
46
49
|
publishDraft: () => publishDraft,
|
|
47
50
|
resolveFormTranslation: () => resolveFormTranslation,
|
|
@@ -1101,17 +1104,38 @@ function aggregateResponses(schema, submissions) {
|
|
|
1101
1104
|
};
|
|
1102
1105
|
}
|
|
1103
1106
|
function responseValues(submission) {
|
|
1104
|
-
|
|
1107
|
+
if (typeof submission !== "object" || submission === null) return void 0;
|
|
1108
|
+
if ("values" in submission) return submission.values;
|
|
1109
|
+
return "answers" in submission ? submission.answers : void 0;
|
|
1105
1110
|
}
|
|
1106
1111
|
function responseIdentifier(submission) {
|
|
1107
|
-
|
|
1112
|
+
if (typeof submission !== "object" || submission === null) return "<unknown>";
|
|
1113
|
+
if ("id" in submission && typeof submission.id === "string" && submission.id.length > 0) return submission.id;
|
|
1114
|
+
if ("responseId" in submission && typeof submission.responseId === "string" && submission.responseId.length > 0) {
|
|
1115
|
+
return submission.responseId;
|
|
1116
|
+
}
|
|
1117
|
+
return "<unknown>";
|
|
1118
|
+
}
|
|
1119
|
+
function isAnswerRecord(value) {
|
|
1120
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1108
1121
|
}
|
|
1109
|
-
function
|
|
1122
|
+
function responseProblem(schema, submission) {
|
|
1123
|
+
if (typeof submission !== "object" || submission === null) {
|
|
1124
|
+
return { reason: "invalid_structure", error: "Submission structure is invalid." };
|
|
1125
|
+
}
|
|
1126
|
+
const identifier = responseIdentifier(submission);
|
|
1127
|
+
const values = responseValues(submission);
|
|
1128
|
+
if (typeof identifier !== "string" || identifier.length === 0 || typeof submission.formId !== "string" || typeof submission.submittedAt !== "string" || !isAnswerRecord(values)) {
|
|
1129
|
+
return { reason: "invalid_structure", error: `Submission ${String(identifier)} structure is invalid.` };
|
|
1130
|
+
}
|
|
1110
1131
|
if (submission.formId !== schema.id) {
|
|
1111
|
-
return `Submission ${
|
|
1132
|
+
return { reason: "form_id_mismatch", error: `Submission ${identifier} does not match form ${schema.id}.` };
|
|
1112
1133
|
}
|
|
1113
|
-
if ("formVersion" in submission && submission.formVersion !== schema.version) {
|
|
1114
|
-
return
|
|
1134
|
+
if ("formVersion" in submission && submission.formVersion !== void 0 && submission.formVersion !== schema.version) {
|
|
1135
|
+
return {
|
|
1136
|
+
reason: "version_mismatch",
|
|
1137
|
+
error: `Submission ${identifier} does not match ${schema.id}@${schema.version}.`
|
|
1138
|
+
};
|
|
1115
1139
|
}
|
|
1116
1140
|
return void 0;
|
|
1117
1141
|
}
|
|
@@ -1119,6 +1143,7 @@ var IncrementalResponseAccumulator = class _IncrementalResponseAccumulator {
|
|
|
1119
1143
|
#schema;
|
|
1120
1144
|
#mode;
|
|
1121
1145
|
#fields;
|
|
1146
|
+
#skipReasons = [];
|
|
1122
1147
|
#submissionCount = 0;
|
|
1123
1148
|
constructor(schema, options) {
|
|
1124
1149
|
assertValidFormSchema(schema);
|
|
@@ -1140,9 +1165,14 @@ var IncrementalResponseAccumulator = class _IncrementalResponseAccumulator {
|
|
|
1140
1165
|
);
|
|
1141
1166
|
}
|
|
1142
1167
|
add(submission) {
|
|
1143
|
-
const
|
|
1144
|
-
if (
|
|
1168
|
+
const problem = responseProblem(this.#schema, submission);
|
|
1169
|
+
if (problem !== void 0) {
|
|
1170
|
+
if (this.#mode === "strict") return { success: false, error: problem.error };
|
|
1171
|
+
this.#skipReasons.push({ responseId: responseIdentifier(submission), reason: problem.reason });
|
|
1172
|
+
return { success: true, skipped: true };
|
|
1173
|
+
}
|
|
1145
1174
|
const values = responseValues(submission);
|
|
1175
|
+
if (!isAnswerRecord(values)) throw new Error("Validated response answers are unavailable.");
|
|
1146
1176
|
const visibility = calculateFieldVisibility(this.#schema, values);
|
|
1147
1177
|
for (const field of this.#schema.fields) {
|
|
1148
1178
|
const accumulator = this.#fields.get(field.id);
|
|
@@ -1172,6 +1202,7 @@ var IncrementalResponseAccumulator = class _IncrementalResponseAccumulator {
|
|
|
1172
1202
|
const result = this.add(submission);
|
|
1173
1203
|
if (!result.success) throw new TypeError(result.error ?? "Submission could not be accumulated.");
|
|
1174
1204
|
}
|
|
1205
|
+
return this.getReport();
|
|
1175
1206
|
}
|
|
1176
1207
|
merge(other) {
|
|
1177
1208
|
if (!(other instanceof _IncrementalResponseAccumulator)) {
|
|
@@ -1181,6 +1212,7 @@ var IncrementalResponseAccumulator = class _IncrementalResponseAccumulator {
|
|
|
1181
1212
|
throw new TypeError("Response accumulators must use the same schema.");
|
|
1182
1213
|
}
|
|
1183
1214
|
this.#submissionCount += other.#submissionCount;
|
|
1215
|
+
this.#skipReasons.push(...other.#skipReasons);
|
|
1184
1216
|
for (const [fieldId, source] of other.#fields) {
|
|
1185
1217
|
const target = this.#fields.get(fieldId);
|
|
1186
1218
|
if (target === void 0) throw new Error(`Accumulator for ${fieldId} is unavailable.`);
|
|
@@ -1196,6 +1228,13 @@ var IncrementalResponseAccumulator = class _IncrementalResponseAccumulator {
|
|
|
1196
1228
|
}
|
|
1197
1229
|
return this;
|
|
1198
1230
|
}
|
|
1231
|
+
getReport() {
|
|
1232
|
+
return {
|
|
1233
|
+
processedCount: this.#submissionCount,
|
|
1234
|
+
skippedCount: this.#skipReasons.length,
|
|
1235
|
+
skipReasons: this.#skipReasons.map((reason) => ({ ...reason }))
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1199
1238
|
finalize() {
|
|
1200
1239
|
return {
|
|
1201
1240
|
formId: this.#schema.id,
|
|
@@ -1267,6 +1306,7 @@ function asFormResponse(submission) {
|
|
|
1267
1306
|
responseId: submission.id,
|
|
1268
1307
|
formId: submission.formId,
|
|
1269
1308
|
sourceLocale: submission.locale,
|
|
1309
|
+
formVersion: submission.formVersion,
|
|
1270
1310
|
answers: submission.values,
|
|
1271
1311
|
submittedAt: submission.submittedAt,
|
|
1272
1312
|
...submission.metadata === void 0 ? {} : { metadata: submission.metadata },
|
|
@@ -1290,8 +1330,8 @@ async function* exportResponsesToCsvStream(schema, submissions, options = {}) {
|
|
|
1290
1330
|
const header = headers.map((value) => escapeCsvCell(value, neutralizeFormulas)).join(",");
|
|
1291
1331
|
yield `${options.withBom ?? true ? "\uFEFF" : ""}${header}`;
|
|
1292
1332
|
for await (const submission of submissions) {
|
|
1293
|
-
const
|
|
1294
|
-
if (
|
|
1333
|
+
const problem = responseProblem(schema, submission);
|
|
1334
|
+
if (problem !== void 0) throw new TypeError(problem.error);
|
|
1295
1335
|
const response = asFormResponse(submission);
|
|
1296
1336
|
const answers = response.answers;
|
|
1297
1337
|
const visible = selectVisibleAnswers(schema, answers);
|
|
@@ -1301,11 +1341,64 @@ async function* exportResponsesToCsvStream(schema, submissions, options = {}) {
|
|
|
1301
1341
|
response.sourceLocale ?? "",
|
|
1302
1342
|
...schema.fields.map((field) => serializeUnknown(visible[field.id]))
|
|
1303
1343
|
] : [];
|
|
1304
|
-
const
|
|
1344
|
+
const context = {
|
|
1345
|
+
...response,
|
|
1346
|
+
submission: response,
|
|
1347
|
+
formVersion: response.formVersion ?? schema.version,
|
|
1348
|
+
schema
|
|
1349
|
+
};
|
|
1350
|
+
const customCells = await Promise.all(customColumns.map((column) => column.getValue(context)));
|
|
1305
1351
|
yield `\r
|
|
1306
1352
|
${[...defaultCells, ...customCells].map((value) => escapeCsvCell(value, neutralizeFormulas)).join(",")}`;
|
|
1307
1353
|
}
|
|
1308
1354
|
}
|
|
1355
|
+
function isWebWritableStream(writable) {
|
|
1356
|
+
return "getWriter" in writable && typeof writable.getWriter === "function";
|
|
1357
|
+
}
|
|
1358
|
+
async function writeNodeChunk(writable, chunk, streamError) {
|
|
1359
|
+
if (writable.write(chunk)) return;
|
|
1360
|
+
let onDrain;
|
|
1361
|
+
const drain = new Promise((resolve) => {
|
|
1362
|
+
onDrain = resolve;
|
|
1363
|
+
writable.once("drain", resolve);
|
|
1364
|
+
});
|
|
1365
|
+
try {
|
|
1366
|
+
await Promise.race([drain, streamError]);
|
|
1367
|
+
} finally {
|
|
1368
|
+
if (onDrain !== void 0) writable.removeListener("drain", onDrain);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
async function pipeResponsesToCsvStream(schema, submissions, writable, options = {}) {
|
|
1372
|
+
const encoder = new TextEncoder();
|
|
1373
|
+
if (isWebWritableStream(writable)) {
|
|
1374
|
+
const writer = writable.getWriter();
|
|
1375
|
+
try {
|
|
1376
|
+
for await (const chunk of exportResponsesToCsvStream(schema, submissions, options)) {
|
|
1377
|
+
await writer.write(encoder.encode(chunk));
|
|
1378
|
+
}
|
|
1379
|
+
await writer.close();
|
|
1380
|
+
} catch (cause) {
|
|
1381
|
+
await writer.abort(cause);
|
|
1382
|
+
throw cause;
|
|
1383
|
+
} finally {
|
|
1384
|
+
writer.releaseLock();
|
|
1385
|
+
}
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
let onStreamError;
|
|
1389
|
+
const streamError = new Promise((_resolve, reject) => {
|
|
1390
|
+
onStreamError = reject;
|
|
1391
|
+
writable.once("error", reject);
|
|
1392
|
+
});
|
|
1393
|
+
try {
|
|
1394
|
+
for await (const chunk of exportResponsesToCsvStream(schema, submissions, options)) {
|
|
1395
|
+
await writeNodeChunk(writable, encoder.encode(chunk), streamError);
|
|
1396
|
+
}
|
|
1397
|
+
await Promise.race([new Promise((resolve) => writable.end(resolve)), streamError]);
|
|
1398
|
+
} finally {
|
|
1399
|
+
if (onStreamError !== void 0) writable.removeListener("error", onStreamError);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1309
1402
|
function exportResponsesToCsv(schema, responses, options = {}) {
|
|
1310
1403
|
assertValidFormSchema(schema);
|
|
1311
1404
|
for (const response of responses) {
|
|
@@ -1477,6 +1570,32 @@ function normalizeSubmissionPageSize(pageSize, fallback = 100) {
|
|
|
1477
1570
|
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("pageSize must be a positive safe integer.");
|
|
1478
1571
|
return value;
|
|
1479
1572
|
}
|
|
1573
|
+
function isJsonArray(value) {
|
|
1574
|
+
return Array.isArray(value);
|
|
1575
|
+
}
|
|
1576
|
+
function isJsonObject(value) {
|
|
1577
|
+
return typeof value === "object" && value !== null && !isJsonArray(value);
|
|
1578
|
+
}
|
|
1579
|
+
function jsonValuesEqual(left, right) {
|
|
1580
|
+
if (left === right) return true;
|
|
1581
|
+
if (left === void 0 || left === null || right === void 0 || right === null || typeof left !== typeof right) {
|
|
1582
|
+
return false;
|
|
1583
|
+
}
|
|
1584
|
+
if (isJsonArray(left) || isJsonArray(right)) {
|
|
1585
|
+
return isJsonArray(left) && isJsonArray(right) && left.length === right.length && left.every((value, index) => jsonValuesEqual(value, right[index]));
|
|
1586
|
+
}
|
|
1587
|
+
if (!isJsonObject(left) || !isJsonObject(right)) return false;
|
|
1588
|
+
const leftKeys = Object.keys(left);
|
|
1589
|
+
const rightKeys = Object.keys(right);
|
|
1590
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.hasOwn(right, key) && jsonValuesEqual(left[key], right[key]));
|
|
1591
|
+
}
|
|
1592
|
+
function matchesSubmissionPageFilters(submission, options) {
|
|
1593
|
+
if (options.filter !== void 0 && !options.filter(submission)) return false;
|
|
1594
|
+
if (options.metadataFilters === void 0) return true;
|
|
1595
|
+
return Object.entries(options.metadataFilters).every(
|
|
1596
|
+
([key, value]) => jsonValuesEqual(submission.metadata?.[key], value)
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1480
1599
|
|
|
1481
1600
|
// src/validation.ts
|
|
1482
1601
|
var DEFAULT_MESSAGES = {
|
|
@@ -1915,6 +2034,12 @@ async function resolveFormTranslation(schema, adapter, targetLocale, sourceLocal
|
|
|
1915
2034
|
}
|
|
1916
2035
|
|
|
1917
2036
|
// src/versioning.ts
|
|
2037
|
+
function revisionConflict(state, expectedRevision) {
|
|
2038
|
+
return expectedRevision === void 0 || expectedRevision === state.revision ? void 0 : {
|
|
2039
|
+
success: false,
|
|
2040
|
+
error: { type: "revision_conflict", expectedRevision, actualRevision: state.revision }
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
1918
2043
|
function validateState(state) {
|
|
1919
2044
|
if (state.formId.trim().length === 0) throw new TypeError("formId must not be empty.");
|
|
1920
2045
|
if (!Number.isSafeInteger(state.nextVersion) || state.nextVersion < 1) {
|
|
@@ -1927,9 +2052,25 @@ function validateState(state) {
|
|
|
1927
2052
|
function cloneVersionToDraft(state, sourceSchema, options = {}) {
|
|
1928
2053
|
validateState(state);
|
|
1929
2054
|
if (sourceSchema.id !== state.formId) throw new TypeError("sourceSchema.id must match state.formId.");
|
|
2055
|
+
const conflict = revisionConflict(state, options.expectedRevision);
|
|
2056
|
+
if (conflict !== void 0) return conflict;
|
|
1930
2057
|
if (state.draftVersion !== void 0) {
|
|
1931
2058
|
return { success: false, error: { type: "draft_already_exists", currentDraftVersion: state.draftVersion } };
|
|
1932
2059
|
}
|
|
2060
|
+
const allowedSourceVersions = /* @__PURE__ */ new Set([
|
|
2061
|
+
...state.publishedVersion === void 0 ? [] : [state.publishedVersion],
|
|
2062
|
+
...options.allowedSourceVersions ?? []
|
|
2063
|
+
]);
|
|
2064
|
+
if (!allowedSourceVersions.has(sourceSchema.version)) {
|
|
2065
|
+
return {
|
|
2066
|
+
success: false,
|
|
2067
|
+
error: {
|
|
2068
|
+
type: "invalid_source_version",
|
|
2069
|
+
requestedVersion: sourceSchema.version,
|
|
2070
|
+
...state.publishedVersion === void 0 ? {} : { publishedVersion: state.publishedVersion }
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
1933
2074
|
const maxVersions = options.maxVersions ?? Number.MAX_SAFE_INTEGER;
|
|
1934
2075
|
if (!Number.isSafeInteger(maxVersions) || maxVersions < 1) {
|
|
1935
2076
|
throw new TypeError("maxVersions must be a positive safe integer.");
|
|
@@ -1951,25 +2092,42 @@ function cloneVersionToDraft(state, sourceSchema, options = {}) {
|
|
|
1951
2092
|
}
|
|
1952
2093
|
};
|
|
1953
2094
|
}
|
|
1954
|
-
function
|
|
2095
|
+
function validatePublishedRecord(state, record) {
|
|
2096
|
+
if (record === void 0) return;
|
|
2097
|
+
if (record.formId !== state.formId || record.version !== state.publishedVersion || record.status !== "published" || record.schema.id !== record.formId || record.schema.version !== record.version) {
|
|
2098
|
+
throw new TypeError("currentPublishedRecord must match the state's published version.");
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
function transitionTimestamp(options) {
|
|
2102
|
+
const timestamp = options.publishedAt ?? options.timestamp ?? "1970-01-01T00:00:00.000Z";
|
|
2103
|
+
if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError("publishedAt must be a valid date string.");
|
|
2104
|
+
return timestamp;
|
|
2105
|
+
}
|
|
2106
|
+
async function publishDraft(state, draftSchema, options = {}) {
|
|
1955
2107
|
validateState(state);
|
|
1956
|
-
|
|
2108
|
+
const conflict = revisionConflict(state, options.expectedRevision);
|
|
2109
|
+
if (conflict !== void 0) return conflict;
|
|
2110
|
+
if (state.draftVersion === void 0 || draftSchema.id !== state.formId || draftSchema.version !== state.draftVersion) {
|
|
2111
|
+
return { success: false, error: { type: "draft_not_found" } };
|
|
2112
|
+
}
|
|
2113
|
+
validatePublishedRecord(state, options.currentPublishedRecord);
|
|
2114
|
+
const validation = await options.validate?.(draftSchema);
|
|
2115
|
+
if (validation === false || Array.isArray(validation) && validation.length > 0) {
|
|
1957
2116
|
return {
|
|
1958
2117
|
success: false,
|
|
1959
|
-
error: {
|
|
1960
|
-
type: "revision_conflict",
|
|
1961
|
-
expectedRevision: options.expectedRevision,
|
|
1962
|
-
actualRevision: state.revision
|
|
1963
|
-
}
|
|
2118
|
+
error: { type: "validation_failed", issues: Array.isArray(validation) ? validation : [] }
|
|
1964
2119
|
};
|
|
1965
2120
|
}
|
|
1966
|
-
|
|
1967
|
-
return { success: false, error: { type: "draft_not_found" } };
|
|
1968
|
-
}
|
|
1969
|
-
if (options.validate?.(draftSchema) === false) throw new TypeError("Draft schema validation failed.");
|
|
1970
|
-
const timestamp = options.timestamp ?? "1970-01-01T00:00:00.000Z";
|
|
1971
|
-
if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError("timestamp must be a valid date string.");
|
|
2121
|
+
const timestamp = transitionTimestamp(options);
|
|
1972
2122
|
const archivedVersion = state.publishedVersion;
|
|
2123
|
+
const archivedRecords = options.currentPublishedRecord === void 0 ? [] : [
|
|
2124
|
+
{
|
|
2125
|
+
...options.currentPublishedRecord,
|
|
2126
|
+
status: "archived",
|
|
2127
|
+
revision: options.currentPublishedRecord.revision + 1,
|
|
2128
|
+
archivedAt: timestamp
|
|
2129
|
+
}
|
|
2130
|
+
];
|
|
1973
2131
|
const { draftVersion: _draftVersion, ...stateWithoutDraft } = state;
|
|
1974
2132
|
return {
|
|
1975
2133
|
success: true,
|
|
@@ -1984,15 +2142,57 @@ function publishDraft(state, draftSchema, options = {}) {
|
|
|
1984
2142
|
version: draftSchema.version,
|
|
1985
2143
|
status: "published",
|
|
1986
2144
|
schema: draftSchema,
|
|
2145
|
+
revision: 1,
|
|
2146
|
+
...archivedVersion === void 0 ? {} : { createdFromVersion: archivedVersion },
|
|
1987
2147
|
createdAt: timestamp,
|
|
1988
2148
|
publishedAt: timestamp
|
|
1989
2149
|
},
|
|
2150
|
+
archivedRecords,
|
|
1990
2151
|
...archivedVersion === void 0 ? {} : { archivedVersion }
|
|
1991
2152
|
}
|
|
1992
2153
|
};
|
|
1993
2154
|
}
|
|
1994
|
-
function
|
|
2155
|
+
async function createPublishTransitionPlan(state, draftRecord, options = {}) {
|
|
2156
|
+
if (draftRecord.formId !== state.formId || draftRecord.version !== state.draftVersion || draftRecord.schema.id !== draftRecord.formId || draftRecord.schema.version !== draftRecord.version) {
|
|
2157
|
+
return { success: false, error: { type: "draft_not_found" } };
|
|
2158
|
+
}
|
|
2159
|
+
if (draftRecord.status !== "draft") {
|
|
2160
|
+
return { success: false, error: { type: "version_immutable", status: draftRecord.status } };
|
|
2161
|
+
}
|
|
2162
|
+
const expectedRevision = options.expectedRevision ?? state.revision;
|
|
2163
|
+
const timestamp = transitionTimestamp(options);
|
|
2164
|
+
const result = await publishDraft(state, draftRecord.schema, {
|
|
2165
|
+
...options,
|
|
2166
|
+
expectedRevision,
|
|
2167
|
+
publishedAt: timestamp
|
|
2168
|
+
});
|
|
2169
|
+
if (!result.success) return result;
|
|
2170
|
+
const publishedRecordToSave = {
|
|
2171
|
+
...draftRecord,
|
|
2172
|
+
status: "published",
|
|
2173
|
+
revision: draftRecord.revision + 1,
|
|
2174
|
+
publishedAt: timestamp
|
|
2175
|
+
};
|
|
2176
|
+
return {
|
|
2177
|
+
success: true,
|
|
2178
|
+
value: {
|
|
2179
|
+
nextState: result.value.nextState,
|
|
2180
|
+
plan: {
|
|
2181
|
+
formId: state.formId,
|
|
2182
|
+
expectedRevision,
|
|
2183
|
+
nextRevision: result.value.nextState.revision,
|
|
2184
|
+
draftToDeleteVersion: draftRecord.version,
|
|
2185
|
+
publishedRecordToSave,
|
|
2186
|
+
archivedRecordsToSave: result.value.archivedRecords,
|
|
2187
|
+
timestamp
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
function deleteDraft(state, options = {}) {
|
|
1995
2193
|
validateState(state);
|
|
2194
|
+
const conflict = revisionConflict(state, options.expectedRevision);
|
|
2195
|
+
if (conflict !== void 0) return conflict;
|
|
1996
2196
|
if (state.draftVersion === void 0) return { success: false, error: { type: "draft_not_found" } };
|
|
1997
2197
|
const { draftVersion: _draftVersion, ...stateWithoutDraft } = state;
|
|
1998
2198
|
return {
|
|
@@ -2018,6 +2218,7 @@ function assertVersionMutable(status) {
|
|
|
2018
2218
|
calculatePageVisibility,
|
|
2019
2219
|
cloneVersionToDraft,
|
|
2020
2220
|
collectSchemaLocales,
|
|
2221
|
+
createPublishTransitionPlan,
|
|
2021
2222
|
createResponseAccumulator,
|
|
2022
2223
|
createSubmission,
|
|
2023
2224
|
decodeSubmissionCursor,
|
|
@@ -2029,7 +2230,9 @@ function assertVersionMutable(status) {
|
|
|
2029
2230
|
exportResponsesToCsvStream,
|
|
2030
2231
|
isDisplayConditionSatisfied,
|
|
2031
2232
|
isQuestionVisible,
|
|
2233
|
+
matchesSubmissionPageFilters,
|
|
2032
2234
|
normalizeSubmissionPageSize,
|
|
2235
|
+
pipeResponsesToCsvStream,
|
|
2033
2236
|
populateSchemaTranslations,
|
|
2034
2237
|
publishDraft,
|
|
2035
2238
|
resolveFormTranslation,
|