@form-engine-ts/core 2.3.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1042,6 +1042,188 @@ function aggregateResponses(schema, submissions) {
1042
1042
  questions: schema.fields.map((field) => aggregateField(schema, field, submissions))
1043
1043
  };
1044
1044
  }
1045
+ function responseValues(submission) {
1046
+ if (typeof submission !== "object" || submission === null) return void 0;
1047
+ if ("values" in submission) return submission.values;
1048
+ return "answers" in submission ? submission.answers : void 0;
1049
+ }
1050
+ function responseIdentifier(submission) {
1051
+ if (typeof submission !== "object" || submission === null) return "<unknown>";
1052
+ if ("id" in submission && typeof submission.id === "string" && submission.id.length > 0) return submission.id;
1053
+ if ("responseId" in submission && typeof submission.responseId === "string" && submission.responseId.length > 0) {
1054
+ return submission.responseId;
1055
+ }
1056
+ return "<unknown>";
1057
+ }
1058
+ function isAnswerRecord(value) {
1059
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1060
+ }
1061
+ function responseProblem(schema, submission) {
1062
+ if (typeof submission !== "object" || submission === null) {
1063
+ return { reason: "invalid_structure", error: "Submission structure is invalid." };
1064
+ }
1065
+ const identifier = responseIdentifier(submission);
1066
+ const values = responseValues(submission);
1067
+ if (typeof identifier !== "string" || identifier.length === 0 || typeof submission.formId !== "string" || typeof submission.submittedAt !== "string" || !isAnswerRecord(values)) {
1068
+ return { reason: "invalid_structure", error: `Submission ${String(identifier)} structure is invalid.` };
1069
+ }
1070
+ if (submission.formId !== schema.id) {
1071
+ return { reason: "form_id_mismatch", error: `Submission ${identifier} does not match form ${schema.id}.` };
1072
+ }
1073
+ if ("formVersion" in submission && submission.formVersion !== void 0 && submission.formVersion !== schema.version) {
1074
+ return {
1075
+ reason: "version_mismatch",
1076
+ error: `Submission ${identifier} does not match ${schema.id}@${schema.version}.`
1077
+ };
1078
+ }
1079
+ return void 0;
1080
+ }
1081
+ var IncrementalResponseAccumulator = class _IncrementalResponseAccumulator {
1082
+ #schema;
1083
+ #mode;
1084
+ #fields;
1085
+ #skipReasons = [];
1086
+ #submissionCount = 0;
1087
+ constructor(schema, options) {
1088
+ assertValidFormSchema(schema);
1089
+ this.#schema = JSON.parse(JSON.stringify(schema));
1090
+ this.#mode = options.mode ?? "strict";
1091
+ this.#fields = new Map(
1092
+ schema.fields.map((field) => [
1093
+ field.id,
1094
+ {
1095
+ answeredCount: 0,
1096
+ total: 0,
1097
+ minimum: null,
1098
+ maximum: null,
1099
+ trueCount: 0,
1100
+ falseCount: 0,
1101
+ optionCounts: new Map("options" in field ? field.options.map((option) => [option.id, 0]) : [])
1102
+ }
1103
+ ])
1104
+ );
1105
+ }
1106
+ add(submission) {
1107
+ const problem = responseProblem(this.#schema, submission);
1108
+ if (problem !== void 0) {
1109
+ if (this.#mode === "strict") return { success: false, error: problem.error };
1110
+ this.#skipReasons.push({ responseId: responseIdentifier(submission), reason: problem.reason });
1111
+ return { success: true, skipped: true };
1112
+ }
1113
+ const values = responseValues(submission);
1114
+ if (!isAnswerRecord(values)) throw new Error("Validated response answers are unavailable.");
1115
+ const visibility = calculateFieldVisibility(this.#schema, values);
1116
+ for (const field of this.#schema.fields) {
1117
+ const accumulator = this.#fields.get(field.id);
1118
+ if (accumulator === void 0) throw new Error(`Accumulator for ${field.id} is unavailable.`);
1119
+ const candidate = values[field.id];
1120
+ if (visibility[field.id] !== true || !valueIsValid(field, candidate)) continue;
1121
+ accumulator.answeredCount += 1;
1122
+ if ((field.type === "number" || field.type === "rating") && typeof candidate === "number") {
1123
+ accumulator.total += candidate;
1124
+ accumulator.minimum = accumulator.minimum === null ? candidate : Math.min(accumulator.minimum, candidate);
1125
+ accumulator.maximum = accumulator.maximum === null ? candidate : Math.max(accumulator.maximum, candidate);
1126
+ } else if (field.type === "checkbox") {
1127
+ if (candidate === true) accumulator.trueCount += 1;
1128
+ if (candidate === false) accumulator.falseCount += 1;
1129
+ } else if ("options" in field) {
1130
+ const selections = Array.isArray(candidate) ? candidate : typeof candidate === "string" ? [candidate] : [];
1131
+ for (const selection of selections) {
1132
+ accumulator.optionCounts.set(selection, (accumulator.optionCounts.get(selection) ?? 0) + 1);
1133
+ }
1134
+ }
1135
+ }
1136
+ this.#submissionCount += 1;
1137
+ return { success: true };
1138
+ }
1139
+ addMany(submissions) {
1140
+ for (const submission of submissions) {
1141
+ const result = this.add(submission);
1142
+ if (!result.success) throw new TypeError(result.error ?? "Submission could not be accumulated.");
1143
+ }
1144
+ return this.getReport();
1145
+ }
1146
+ merge(other) {
1147
+ if (!(other instanceof _IncrementalResponseAccumulator)) {
1148
+ throw new TypeError("Only form-engine response accumulators can be merged.");
1149
+ }
1150
+ if (other.#schema.id !== this.#schema.id || other.#schema.version !== this.#schema.version || JSON.stringify(other.#schema.fields) !== JSON.stringify(this.#schema.fields)) {
1151
+ throw new TypeError("Response accumulators must use the same schema.");
1152
+ }
1153
+ this.#submissionCount += other.#submissionCount;
1154
+ this.#skipReasons.push(...other.#skipReasons);
1155
+ for (const [fieldId, source] of other.#fields) {
1156
+ const target = this.#fields.get(fieldId);
1157
+ if (target === void 0) throw new Error(`Accumulator for ${fieldId} is unavailable.`);
1158
+ target.answeredCount += source.answeredCount;
1159
+ target.total += source.total;
1160
+ target.minimum = target.minimum === null ? source.minimum : source.minimum === null ? target.minimum : Math.min(target.minimum, source.minimum);
1161
+ target.maximum = target.maximum === null ? source.maximum : source.maximum === null ? target.maximum : Math.max(target.maximum, source.maximum);
1162
+ target.trueCount += source.trueCount;
1163
+ target.falseCount += source.falseCount;
1164
+ for (const [optionId, count] of source.optionCounts) {
1165
+ target.optionCounts.set(optionId, (target.optionCounts.get(optionId) ?? 0) + count);
1166
+ }
1167
+ }
1168
+ return this;
1169
+ }
1170
+ getReport() {
1171
+ return {
1172
+ processedCount: this.#submissionCount,
1173
+ skippedCount: this.#skipReasons.length,
1174
+ skipReasons: this.#skipReasons.map((reason) => ({ ...reason }))
1175
+ };
1176
+ }
1177
+ finalize() {
1178
+ return {
1179
+ formId: this.#schema.id,
1180
+ formVersion: this.#schema.version,
1181
+ submissionCount: this.#submissionCount,
1182
+ questions: this.#schema.fields.map((field) => {
1183
+ const accumulator = this.#fields.get(field.id);
1184
+ if (accumulator === void 0) throw new Error(`Accumulator for ${field.id} is unavailable.`);
1185
+ const base = {
1186
+ fieldId: field.id,
1187
+ answeredCount: accumulator.answeredCount,
1188
+ unansweredCount: this.#submissionCount - accumulator.answeredCount
1189
+ };
1190
+ if (field.type === "text" || field.type === "textarea") return { ...base, kind: field.type };
1191
+ if (field.type === "number" || field.type === "rating") {
1192
+ return {
1193
+ ...base,
1194
+ kind: field.type,
1195
+ minimum: accumulator.minimum,
1196
+ maximum: accumulator.maximum,
1197
+ average: accumulator.answeredCount === 0 ? null : accumulator.total / accumulator.answeredCount,
1198
+ total: accumulator.total
1199
+ };
1200
+ }
1201
+ if (field.type === "checkbox") {
1202
+ return {
1203
+ ...base,
1204
+ kind: "checkbox",
1205
+ trueCount: accumulator.trueCount,
1206
+ falseCount: accumulator.falseCount,
1207
+ truePercentageOfSubmissions: percentage(accumulator.trueCount, this.#submissionCount),
1208
+ falsePercentageOfSubmissions: percentage(accumulator.falseCount, this.#submissionCount)
1209
+ };
1210
+ }
1211
+ if (!("options" in field)) throw new TypeError(`Field ${field.id} cannot be aggregated.`);
1212
+ return {
1213
+ ...base,
1214
+ kind: field.type,
1215
+ options: field.options.map((option) => {
1216
+ const count = accumulator.optionCounts.get(option.id) ?? 0;
1217
+ return { id: option.id, count, percentageOfSubmissions: percentage(count, this.#submissionCount) };
1218
+ })
1219
+ };
1220
+ })
1221
+ };
1222
+ }
1223
+ };
1224
+ function createResponseAccumulator(schema, options = {}) {
1225
+ return new IncrementalResponseAccumulator(schema, options);
1226
+ }
1045
1227
  function escapeCsvCell(value, neutralizeFormulas = true) {
1046
1228
  if (value === null || value === void 0) return "";
1047
1229
  let stringValue = String(value);
@@ -1057,6 +1239,105 @@ function serializeValue(value) {
1057
1239
  if (value === void 0) return "";
1058
1240
  return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value);
1059
1241
  }
1242
+ function asFormResponse(submission) {
1243
+ if (!("values" in submission)) return submission;
1244
+ return {
1245
+ responseId: submission.id,
1246
+ formId: submission.formId,
1247
+ sourceLocale: submission.locale,
1248
+ formVersion: submission.formVersion,
1249
+ answers: submission.values,
1250
+ submittedAt: submission.submittedAt,
1251
+ ...submission.metadata === void 0 ? {} : { metadata: submission.metadata },
1252
+ ...submission.translationMetadata === void 0 ? {} : { translationMetadata: submission.translationMetadata }
1253
+ };
1254
+ }
1255
+ function serializeUnknown(value) {
1256
+ if (value === null || value === void 0) return "";
1257
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
1258
+ return JSON.stringify(value);
1259
+ }
1260
+ async function* exportResponsesToCsvStream(schema, submissions, options = {}) {
1261
+ assertValidFormSchema(schema);
1262
+ const includeDefaultColumns = options.includeDefaultColumns ?? true;
1263
+ const customColumns = options.columns ?? [];
1264
+ const headers = [
1265
+ ...includeDefaultColumns ? ["submissionId", "submittedAt", "locale", ...schema.fields.map((field) => field.id)] : [],
1266
+ ...customColumns.map((column) => column.header)
1267
+ ];
1268
+ const neutralizeFormulas = options.neutralizeFormulas ?? true;
1269
+ const header = headers.map((value) => escapeCsvCell(value, neutralizeFormulas)).join(",");
1270
+ yield `${options.withBom ?? true ? "\uFEFF" : ""}${header}`;
1271
+ for await (const submission of submissions) {
1272
+ const problem = responseProblem(schema, submission);
1273
+ if (problem !== void 0) throw new TypeError(problem.error);
1274
+ const response = asFormResponse(submission);
1275
+ const answers = response.answers;
1276
+ const visible = selectVisibleAnswers(schema, answers);
1277
+ const defaultCells = includeDefaultColumns ? [
1278
+ response.responseId,
1279
+ response.submittedAt,
1280
+ response.sourceLocale ?? "",
1281
+ ...schema.fields.map((field) => serializeUnknown(visible[field.id]))
1282
+ ] : [];
1283
+ const context = {
1284
+ ...response,
1285
+ submission: response,
1286
+ formVersion: response.formVersion ?? schema.version,
1287
+ schema
1288
+ };
1289
+ const customCells = customColumns.map((column) => column.getValue(context));
1290
+ yield `\r
1291
+ ${[...defaultCells, ...customCells].map((value) => escapeCsvCell(value, neutralizeFormulas)).join(",")}`;
1292
+ }
1293
+ }
1294
+ function isWebWritableStream(writable) {
1295
+ return "getWriter" in writable && typeof writable.getWriter === "function";
1296
+ }
1297
+ async function writeNodeChunk(writable, chunk, streamError) {
1298
+ if (writable.write(chunk)) return;
1299
+ let onDrain;
1300
+ const drain = new Promise((resolve) => {
1301
+ onDrain = resolve;
1302
+ writable.once("drain", resolve);
1303
+ });
1304
+ try {
1305
+ await Promise.race([drain, streamError]);
1306
+ } finally {
1307
+ if (onDrain !== void 0) writable.removeListener("drain", onDrain);
1308
+ }
1309
+ }
1310
+ async function pipeResponsesToCsvStream(schema, submissions, writable, options = {}) {
1311
+ const encoder = new TextEncoder();
1312
+ if (isWebWritableStream(writable)) {
1313
+ const writer = writable.getWriter();
1314
+ try {
1315
+ for await (const chunk of exportResponsesToCsvStream(schema, submissions, options)) {
1316
+ await writer.write(encoder.encode(chunk));
1317
+ }
1318
+ await writer.close();
1319
+ } catch (cause) {
1320
+ await writer.abort(cause);
1321
+ throw cause;
1322
+ } finally {
1323
+ writer.releaseLock();
1324
+ }
1325
+ return;
1326
+ }
1327
+ let onStreamError;
1328
+ const streamError = new Promise((_resolve, reject) => {
1329
+ onStreamError = reject;
1330
+ writable.once("error", reject);
1331
+ });
1332
+ try {
1333
+ for await (const chunk of exportResponsesToCsvStream(schema, submissions, options)) {
1334
+ await writeNodeChunk(writable, encoder.encode(chunk), streamError);
1335
+ }
1336
+ await Promise.race([new Promise((resolve) => writable.end(resolve)), streamError]);
1337
+ } finally {
1338
+ if (onStreamError !== void 0) writable.removeListener("error", onStreamError);
1339
+ }
1340
+ }
1060
1341
  function exportResponsesToCsv(schema, responses, options = {}) {
1061
1342
  assertValidFormSchema(schema);
1062
1343
  for (const response of responses) {
@@ -1174,6 +1455,87 @@ function transformFieldType(field, nextType) {
1174
1455
  return { ...common, type: nextType, options };
1175
1456
  }
1176
1457
 
1458
+ // src/pagination.ts
1459
+ var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1460
+ function encodeBase64(bytes) {
1461
+ let result = "";
1462
+ for (let index = 0; index < bytes.length; index += 3) {
1463
+ const first = bytes[index] ?? 0;
1464
+ const second = bytes[index + 1] ?? 0;
1465
+ const third = bytes[index + 2] ?? 0;
1466
+ const combined = first << 16 | second << 8 | third;
1467
+ result += BASE64_ALPHABET[combined >> 18 & 63] ?? "";
1468
+ result += BASE64_ALPHABET[combined >> 12 & 63] ?? "";
1469
+ result += index + 1 < bytes.length ? BASE64_ALPHABET[combined >> 6 & 63] ?? "" : "=";
1470
+ result += index + 2 < bytes.length ? BASE64_ALPHABET[combined & 63] ?? "" : "=";
1471
+ }
1472
+ return result;
1473
+ }
1474
+ function decodeBase64(value) {
1475
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
1476
+ throw new TypeError("cursor must be a valid Base64 token.");
1477
+ }
1478
+ const bytes = [];
1479
+ for (let index = 0; index < value.length; index += 4) {
1480
+ const characters = value.slice(index, index + 4);
1481
+ const sextets = [...characters].map((character) => character === "=" ? 0 : BASE64_ALPHABET.indexOf(character));
1482
+ const combined = (sextets[0] ?? 0) << 18 | (sextets[1] ?? 0) << 12 | (sextets[2] ?? 0) << 6 | (sextets[3] ?? 0);
1483
+ bytes.push(combined >> 16 & 255);
1484
+ if (characters[2] !== "=") bytes.push(combined >> 8 & 255);
1485
+ if (characters[3] !== "=") bytes.push(combined & 255);
1486
+ }
1487
+ return new Uint8Array(bytes);
1488
+ }
1489
+ function encodeSubmissionCursor(value) {
1490
+ if (value.submittedAt.length === 0 || value.responseId.length === 0) {
1491
+ throw new TypeError("Cursor values must not be empty.");
1492
+ }
1493
+ return encodeBase64(new TextEncoder().encode(JSON.stringify(value)));
1494
+ }
1495
+ function decodeSubmissionCursor(cursor) {
1496
+ try {
1497
+ const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(decodeBase64(cursor)));
1498
+ if (typeof parsed !== "object" || parsed === null || !("submittedAt" in parsed) || typeof parsed.submittedAt !== "string" || parsed.submittedAt.length === 0 || !("responseId" in parsed) || typeof parsed.responseId !== "string" || parsed.responseId.length === 0) {
1499
+ throw new TypeError("cursor payload is invalid.");
1500
+ }
1501
+ return { submittedAt: parsed.submittedAt, responseId: parsed.responseId };
1502
+ } catch (cause) {
1503
+ if (cause instanceof TypeError && cause.message === "cursor payload is invalid.") throw cause;
1504
+ throw new TypeError("cursor must be a valid form-engine cursor.", { cause });
1505
+ }
1506
+ }
1507
+ function normalizeSubmissionPageSize(pageSize, fallback = 100) {
1508
+ const value = pageSize ?? fallback;
1509
+ if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("pageSize must be a positive safe integer.");
1510
+ return value;
1511
+ }
1512
+ function isJsonArray(value) {
1513
+ return Array.isArray(value);
1514
+ }
1515
+ function isJsonObject(value) {
1516
+ return typeof value === "object" && value !== null && !isJsonArray(value);
1517
+ }
1518
+ function jsonValuesEqual(left, right) {
1519
+ if (left === right) return true;
1520
+ if (left === void 0 || left === null || right === void 0 || right === null || typeof left !== typeof right) {
1521
+ return false;
1522
+ }
1523
+ if (isJsonArray(left) || isJsonArray(right)) {
1524
+ return isJsonArray(left) && isJsonArray(right) && left.length === right.length && left.every((value, index) => jsonValuesEqual(value, right[index]));
1525
+ }
1526
+ if (!isJsonObject(left) || !isJsonObject(right)) return false;
1527
+ const leftKeys = Object.keys(left);
1528
+ const rightKeys = Object.keys(right);
1529
+ return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.hasOwn(right, key) && jsonValuesEqual(left[key], right[key]));
1530
+ }
1531
+ function matchesSubmissionPageFilters(submission, options) {
1532
+ if (options.filter !== void 0 && !options.filter(submission)) return false;
1533
+ if (options.metadataFilters === void 0) return true;
1534
+ return Object.entries(options.metadataFilters).every(
1535
+ ([key, value]) => jsonValuesEqual(submission.metadata?.[key], value)
1536
+ );
1537
+ }
1538
+
1177
1539
  // src/validation.ts
1178
1540
  var DEFAULT_MESSAGES = {
1179
1541
  required: "validation.required",
@@ -1609,22 +1971,160 @@ async function resolveFormTranslation(schema, adapter, targetLocale, sourceLocal
1609
1971
  );
1610
1972
  return resolveLocalizedSchema(populated.schema, targetLocale);
1611
1973
  }
1974
+
1975
+ // src/versioning.ts
1976
+ function revisionConflict(state, expectedRevision) {
1977
+ return expectedRevision === void 0 || expectedRevision === state.revision ? void 0 : {
1978
+ success: false,
1979
+ error: { type: "revision_conflict", expectedRevision, actualRevision: state.revision }
1980
+ };
1981
+ }
1982
+ function validateState(state) {
1983
+ if (state.formId.trim().length === 0) throw new TypeError("formId must not be empty.");
1984
+ if (!Number.isSafeInteger(state.nextVersion) || state.nextVersion < 1) {
1985
+ throw new TypeError("nextVersion must be a positive safe integer.");
1986
+ }
1987
+ if (!Number.isSafeInteger(state.revision) || state.revision < 0) {
1988
+ throw new TypeError("revision must be a non-negative safe integer.");
1989
+ }
1990
+ }
1991
+ function cloneVersionToDraft(state, sourceSchema, options = {}) {
1992
+ validateState(state);
1993
+ if (sourceSchema.id !== state.formId) throw new TypeError("sourceSchema.id must match state.formId.");
1994
+ const conflict = revisionConflict(state, options.expectedRevision);
1995
+ if (conflict !== void 0) return conflict;
1996
+ if (state.draftVersion !== void 0) {
1997
+ return { success: false, error: { type: "draft_already_exists", currentDraftVersion: state.draftVersion } };
1998
+ }
1999
+ const allowedSourceVersions = /* @__PURE__ */ new Set([
2000
+ ...state.publishedVersion === void 0 ? [] : [state.publishedVersion],
2001
+ ...options.allowedSourceVersions ?? []
2002
+ ]);
2003
+ if (!allowedSourceVersions.has(sourceSchema.version)) {
2004
+ return {
2005
+ success: false,
2006
+ error: {
2007
+ type: "invalid_source_version",
2008
+ requestedVersion: sourceSchema.version,
2009
+ ...state.publishedVersion === void 0 ? {} : { publishedVersion: state.publishedVersion }
2010
+ }
2011
+ };
2012
+ }
2013
+ const maxVersions = options.maxVersions ?? Number.MAX_SAFE_INTEGER;
2014
+ if (!Number.isSafeInteger(maxVersions) || maxVersions < 1) {
2015
+ throw new TypeError("maxVersions must be a positive safe integer.");
2016
+ }
2017
+ if (state.nextVersion > maxVersions) {
2018
+ return { success: false, error: { type: "max_version_exceeded", max: maxVersions } };
2019
+ }
2020
+ const version = state.nextVersion;
2021
+ return {
2022
+ success: true,
2023
+ value: {
2024
+ nextState: {
2025
+ ...state,
2026
+ draftVersion: version,
2027
+ nextVersion: version + 1,
2028
+ revision: state.revision + 1
2029
+ },
2030
+ draftSchema: { ...sourceSchema, version }
2031
+ }
2032
+ };
2033
+ }
2034
+ function publishDraft(state, draftSchema, options = {}) {
2035
+ validateState(state);
2036
+ const conflict = revisionConflict(state, options.expectedRevision);
2037
+ if (conflict !== void 0) return conflict;
2038
+ if (state.draftVersion === void 0 || draftSchema.id !== state.formId || draftSchema.version !== state.draftVersion) {
2039
+ return { success: false, error: { type: "draft_not_found" } };
2040
+ }
2041
+ const validation = options.validate?.(draftSchema);
2042
+ if (validation === false || Array.isArray(validation) && validation.length > 0) {
2043
+ return {
2044
+ success: false,
2045
+ error: { type: "validation_failed", issues: Array.isArray(validation) ? validation : [] }
2046
+ };
2047
+ }
2048
+ const timestamp = options.timestamp ?? "1970-01-01T00:00:00.000Z";
2049
+ if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError("timestamp must be a valid date string.");
2050
+ const archivedVersion = state.publishedVersion;
2051
+ const archivedRecords = archivedVersion === void 0 ? [] : [
2052
+ {
2053
+ formId: state.formId,
2054
+ version: archivedVersion,
2055
+ status: "archived",
2056
+ schema: { ...draftSchema, version: archivedVersion },
2057
+ createdAt: timestamp,
2058
+ publishedAt: timestamp,
2059
+ archivedAt: timestamp
2060
+ }
2061
+ ];
2062
+ const { draftVersion: _draftVersion, ...stateWithoutDraft } = state;
2063
+ return {
2064
+ success: true,
2065
+ value: {
2066
+ nextState: {
2067
+ ...stateWithoutDraft,
2068
+ publishedVersion: draftSchema.version,
2069
+ revision: state.revision + 1
2070
+ },
2071
+ publishedRecord: {
2072
+ formId: state.formId,
2073
+ version: draftSchema.version,
2074
+ status: "published",
2075
+ schema: draftSchema,
2076
+ createdAt: timestamp,
2077
+ publishedAt: timestamp
2078
+ },
2079
+ archivedRecords,
2080
+ ...archivedVersion === void 0 ? {} : { archivedVersion }
2081
+ }
2082
+ };
2083
+ }
2084
+ function deleteDraft(state, options = {}) {
2085
+ validateState(state);
2086
+ const conflict = revisionConflict(state, options.expectedRevision);
2087
+ if (conflict !== void 0) return conflict;
2088
+ if (state.draftVersion === void 0) return { success: false, error: { type: "draft_not_found" } };
2089
+ const { draftVersion: _draftVersion, ...stateWithoutDraft } = state;
2090
+ return {
2091
+ success: true,
2092
+ value: { nextState: { ...stateWithoutDraft, revision: state.revision + 1 } }
2093
+ };
2094
+ }
2095
+ function assertVersionMutable(status) {
2096
+ if (status !== "draft") {
2097
+ const error = { type: "version_immutable", status };
2098
+ throw new TypeError(`A ${status} form version is immutable.`, { cause: error });
2099
+ }
2100
+ }
1612
2101
  export {
1613
2102
  aggregateResponses,
1614
2103
  assertValidFormSchema,
2104
+ assertVersionMutable,
1615
2105
  calculateChoiceDistribution,
1616
2106
  calculateCrossTabulation,
1617
2107
  calculateFieldVisibility,
1618
2108
  calculateNumericSummary,
1619
2109
  calculatePageVisibility,
2110
+ cloneVersionToDraft,
1620
2111
  collectSchemaLocales,
2112
+ createResponseAccumulator,
1621
2113
  createSubmission,
2114
+ decodeSubmissionCursor,
2115
+ deleteDraft,
1622
2116
  dispatchWebhook,
2117
+ encodeSubmissionCursor,
1623
2118
  escapeCsvCell,
1624
2119
  exportResponsesToCsv,
2120
+ exportResponsesToCsvStream,
1625
2121
  isDisplayConditionSatisfied,
1626
2122
  isQuestionVisible,
2123
+ matchesSubmissionPageFilters,
2124
+ normalizeSubmissionPageSize,
2125
+ pipeResponsesToCsvStream,
1627
2126
  populateSchemaTranslations,
2127
+ publishDraft,
1628
2128
  resolveFormTranslation,
1629
2129
  resolveLocalizedSchema,
1630
2130
  sanitizeSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/core",
3
- "version": "2.3.0",
3
+ "version": "2.5.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },