@form-engine-ts/core 2.5.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/README.md CHANGED
@@ -77,13 +77,21 @@ Results are ordered by `submittedAt`, then submission ID. Both boundaries are in
77
77
  ## Versioning, incremental analytics, and paged storage
78
78
 
79
79
  `cloneVersionToDraft`, `publishDraft`, and `deleteDraft` implement revision-checked version transitions as pure functions.
80
+ Clone/delete operations accept `expectedRevision`; cloning rejects non-published sources, publish validation failures are
81
+ returned as typed `validation_failed` issues, and successful publishing reports every archived record. Storage adapters
82
+ that can commit the resulting changes atomically implement `VersionedFormStorageAdapter` and `VersionTransitionPlan`.
80
83
  `createResponseAccumulator` incrementally counts choices, answered/unanswered values, and numeric summaries without retaining
81
- free-text bodies. Independent accumulators for the same schema can be merged, and `finalize()` matches `aggregateResponses`.
84
+ free-text bodies. In lenient mode, mismatched responses are skipped and exposed by `addMany()` and `getReport()` instead of
85
+ being included silently. Independent accumulators for the same schema can be merged, and `finalize()` matches
86
+ `aggregateResponses`.
82
87
 
83
88
  `exportResponsesToCsvStream` accepts an `AsyncIterable`, emits the BOM/header and one chunk per response, and supports
84
- custom `CsvColumnDef` columns. Formula-injection neutralization applies to both default and custom columns.
89
+ custom `CsvColumnDef` columns. Custom getters receive the submission, form version, and schema. Use
90
+ `pipeResponsesToCsvStream` to write to a Web `WritableStream` or Node-compatible writable while honoring backpressure.
91
+ Formula-injection neutralization applies to both default and custom columns.
85
92
 
86
93
  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.
94
+ cursor combines `submittedAt` and response ID, so equal timestamps do not produce gaps or duplicates. `metadataFilters`
95
+ and `filter` are applied before page sizing.
88
96
 
89
97
  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
@@ -41,7 +41,9 @@ __export(index_exports, {
41
41
  exportResponsesToCsvStream: () => exportResponsesToCsvStream,
42
42
  isDisplayConditionSatisfied: () => isDisplayConditionSatisfied,
43
43
  isQuestionVisible: () => isQuestionVisible,
44
+ matchesSubmissionPageFilters: () => matchesSubmissionPageFilters,
44
45
  normalizeSubmissionPageSize: () => normalizeSubmissionPageSize,
46
+ pipeResponsesToCsvStream: () => pipeResponsesToCsvStream,
45
47
  populateSchemaTranslations: () => populateSchemaTranslations,
46
48
  publishDraft: () => publishDraft,
47
49
  resolveFormTranslation: () => resolveFormTranslation,
@@ -1101,17 +1103,38 @@ function aggregateResponses(schema, submissions) {
1101
1103
  };
1102
1104
  }
1103
1105
  function responseValues(submission) {
1104
- return "values" in submission ? submission.values : submission.answers;
1106
+ if (typeof submission !== "object" || submission === null) return void 0;
1107
+ if ("values" in submission) return submission.values;
1108
+ return "answers" in submission ? submission.answers : void 0;
1105
1109
  }
1106
1110
  function responseIdentifier(submission) {
1107
- return "id" in submission ? submission.id : submission.responseId;
1111
+ if (typeof submission !== "object" || submission === null) return "<unknown>";
1112
+ if ("id" in submission && typeof submission.id === "string" && submission.id.length > 0) return submission.id;
1113
+ if ("responseId" in submission && typeof submission.responseId === "string" && submission.responseId.length > 0) {
1114
+ return submission.responseId;
1115
+ }
1116
+ return "<unknown>";
1117
+ }
1118
+ function isAnswerRecord(value) {
1119
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1108
1120
  }
1109
- function responseMismatch(schema, submission) {
1121
+ function responseProblem(schema, submission) {
1122
+ if (typeof submission !== "object" || submission === null) {
1123
+ return { reason: "invalid_structure", error: "Submission structure is invalid." };
1124
+ }
1125
+ const identifier = responseIdentifier(submission);
1126
+ const values = responseValues(submission);
1127
+ if (typeof identifier !== "string" || identifier.length === 0 || typeof submission.formId !== "string" || typeof submission.submittedAt !== "string" || !isAnswerRecord(values)) {
1128
+ return { reason: "invalid_structure", error: `Submission ${String(identifier)} structure is invalid.` };
1129
+ }
1110
1130
  if (submission.formId !== schema.id) {
1111
- return `Submission ${responseIdentifier(submission)} does not match form ${schema.id}.`;
1131
+ return { reason: "form_id_mismatch", error: `Submission ${identifier} does not match form ${schema.id}.` };
1112
1132
  }
1113
- if ("formVersion" in submission && submission.formVersion !== schema.version) {
1114
- return `Submission ${responseIdentifier(submission)} does not match ${schema.id}@${schema.version}.`;
1133
+ if ("formVersion" in submission && submission.formVersion !== void 0 && submission.formVersion !== schema.version) {
1134
+ return {
1135
+ reason: "version_mismatch",
1136
+ error: `Submission ${identifier} does not match ${schema.id}@${schema.version}.`
1137
+ };
1115
1138
  }
1116
1139
  return void 0;
1117
1140
  }
@@ -1119,6 +1142,7 @@ var IncrementalResponseAccumulator = class _IncrementalResponseAccumulator {
1119
1142
  #schema;
1120
1143
  #mode;
1121
1144
  #fields;
1145
+ #skipReasons = [];
1122
1146
  #submissionCount = 0;
1123
1147
  constructor(schema, options) {
1124
1148
  assertValidFormSchema(schema);
@@ -1140,9 +1164,14 @@ var IncrementalResponseAccumulator = class _IncrementalResponseAccumulator {
1140
1164
  );
1141
1165
  }
1142
1166
  add(submission) {
1143
- const mismatch = responseMismatch(this.#schema, submission);
1144
- if (mismatch !== void 0 && this.#mode === "strict") return { success: false, error: mismatch };
1167
+ const problem = responseProblem(this.#schema, submission);
1168
+ if (problem !== void 0) {
1169
+ if (this.#mode === "strict") return { success: false, error: problem.error };
1170
+ this.#skipReasons.push({ responseId: responseIdentifier(submission), reason: problem.reason });
1171
+ return { success: true, skipped: true };
1172
+ }
1145
1173
  const values = responseValues(submission);
1174
+ if (!isAnswerRecord(values)) throw new Error("Validated response answers are unavailable.");
1146
1175
  const visibility = calculateFieldVisibility(this.#schema, values);
1147
1176
  for (const field of this.#schema.fields) {
1148
1177
  const accumulator = this.#fields.get(field.id);
@@ -1172,6 +1201,7 @@ var IncrementalResponseAccumulator = class _IncrementalResponseAccumulator {
1172
1201
  const result = this.add(submission);
1173
1202
  if (!result.success) throw new TypeError(result.error ?? "Submission could not be accumulated.");
1174
1203
  }
1204
+ return this.getReport();
1175
1205
  }
1176
1206
  merge(other) {
1177
1207
  if (!(other instanceof _IncrementalResponseAccumulator)) {
@@ -1181,6 +1211,7 @@ var IncrementalResponseAccumulator = class _IncrementalResponseAccumulator {
1181
1211
  throw new TypeError("Response accumulators must use the same schema.");
1182
1212
  }
1183
1213
  this.#submissionCount += other.#submissionCount;
1214
+ this.#skipReasons.push(...other.#skipReasons);
1184
1215
  for (const [fieldId, source] of other.#fields) {
1185
1216
  const target = this.#fields.get(fieldId);
1186
1217
  if (target === void 0) throw new Error(`Accumulator for ${fieldId} is unavailable.`);
@@ -1196,6 +1227,13 @@ var IncrementalResponseAccumulator = class _IncrementalResponseAccumulator {
1196
1227
  }
1197
1228
  return this;
1198
1229
  }
1230
+ getReport() {
1231
+ return {
1232
+ processedCount: this.#submissionCount,
1233
+ skippedCount: this.#skipReasons.length,
1234
+ skipReasons: this.#skipReasons.map((reason) => ({ ...reason }))
1235
+ };
1236
+ }
1199
1237
  finalize() {
1200
1238
  return {
1201
1239
  formId: this.#schema.id,
@@ -1267,6 +1305,7 @@ function asFormResponse(submission) {
1267
1305
  responseId: submission.id,
1268
1306
  formId: submission.formId,
1269
1307
  sourceLocale: submission.locale,
1308
+ formVersion: submission.formVersion,
1270
1309
  answers: submission.values,
1271
1310
  submittedAt: submission.submittedAt,
1272
1311
  ...submission.metadata === void 0 ? {} : { metadata: submission.metadata },
@@ -1290,8 +1329,8 @@ async function* exportResponsesToCsvStream(schema, submissions, options = {}) {
1290
1329
  const header = headers.map((value) => escapeCsvCell(value, neutralizeFormulas)).join(",");
1291
1330
  yield `${options.withBom ?? true ? "\uFEFF" : ""}${header}`;
1292
1331
  for await (const submission of submissions) {
1293
- const mismatch = responseMismatch(schema, submission);
1294
- if (mismatch !== void 0) throw new TypeError(mismatch);
1332
+ const problem = responseProblem(schema, submission);
1333
+ if (problem !== void 0) throw new TypeError(problem.error);
1295
1334
  const response = asFormResponse(submission);
1296
1335
  const answers = response.answers;
1297
1336
  const visible = selectVisibleAnswers(schema, answers);
@@ -1301,11 +1340,64 @@ async function* exportResponsesToCsvStream(schema, submissions, options = {}) {
1301
1340
  response.sourceLocale ?? "",
1302
1341
  ...schema.fields.map((field) => serializeUnknown(visible[field.id]))
1303
1342
  ] : [];
1304
- const customCells = customColumns.map((column) => column.getValue(response));
1343
+ const context = {
1344
+ ...response,
1345
+ submission: response,
1346
+ formVersion: response.formVersion ?? schema.version,
1347
+ schema
1348
+ };
1349
+ const customCells = customColumns.map((column) => column.getValue(context));
1305
1350
  yield `\r
1306
1351
  ${[...defaultCells, ...customCells].map((value) => escapeCsvCell(value, neutralizeFormulas)).join(",")}`;
1307
1352
  }
1308
1353
  }
1354
+ function isWebWritableStream(writable) {
1355
+ return "getWriter" in writable && typeof writable.getWriter === "function";
1356
+ }
1357
+ async function writeNodeChunk(writable, chunk, streamError) {
1358
+ if (writable.write(chunk)) return;
1359
+ let onDrain;
1360
+ const drain = new Promise((resolve) => {
1361
+ onDrain = resolve;
1362
+ writable.once("drain", resolve);
1363
+ });
1364
+ try {
1365
+ await Promise.race([drain, streamError]);
1366
+ } finally {
1367
+ if (onDrain !== void 0) writable.removeListener("drain", onDrain);
1368
+ }
1369
+ }
1370
+ async function pipeResponsesToCsvStream(schema, submissions, writable, options = {}) {
1371
+ const encoder = new TextEncoder();
1372
+ if (isWebWritableStream(writable)) {
1373
+ const writer = writable.getWriter();
1374
+ try {
1375
+ for await (const chunk of exportResponsesToCsvStream(schema, submissions, options)) {
1376
+ await writer.write(encoder.encode(chunk));
1377
+ }
1378
+ await writer.close();
1379
+ } catch (cause) {
1380
+ await writer.abort(cause);
1381
+ throw cause;
1382
+ } finally {
1383
+ writer.releaseLock();
1384
+ }
1385
+ return;
1386
+ }
1387
+ let onStreamError;
1388
+ const streamError = new Promise((_resolve, reject) => {
1389
+ onStreamError = reject;
1390
+ writable.once("error", reject);
1391
+ });
1392
+ try {
1393
+ for await (const chunk of exportResponsesToCsvStream(schema, submissions, options)) {
1394
+ await writeNodeChunk(writable, encoder.encode(chunk), streamError);
1395
+ }
1396
+ await Promise.race([new Promise((resolve) => writable.end(resolve)), streamError]);
1397
+ } finally {
1398
+ if (onStreamError !== void 0) writable.removeListener("error", onStreamError);
1399
+ }
1400
+ }
1309
1401
  function exportResponsesToCsv(schema, responses, options = {}) {
1310
1402
  assertValidFormSchema(schema);
1311
1403
  for (const response of responses) {
@@ -1477,6 +1569,32 @@ function normalizeSubmissionPageSize(pageSize, fallback = 100) {
1477
1569
  if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("pageSize must be a positive safe integer.");
1478
1570
  return value;
1479
1571
  }
1572
+ function isJsonArray(value) {
1573
+ return Array.isArray(value);
1574
+ }
1575
+ function isJsonObject(value) {
1576
+ return typeof value === "object" && value !== null && !isJsonArray(value);
1577
+ }
1578
+ function jsonValuesEqual(left, right) {
1579
+ if (left === right) return true;
1580
+ if (left === void 0 || left === null || right === void 0 || right === null || typeof left !== typeof right) {
1581
+ return false;
1582
+ }
1583
+ if (isJsonArray(left) || isJsonArray(right)) {
1584
+ return isJsonArray(left) && isJsonArray(right) && left.length === right.length && left.every((value, index) => jsonValuesEqual(value, right[index]));
1585
+ }
1586
+ if (!isJsonObject(left) || !isJsonObject(right)) return false;
1587
+ const leftKeys = Object.keys(left);
1588
+ const rightKeys = Object.keys(right);
1589
+ return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.hasOwn(right, key) && jsonValuesEqual(left[key], right[key]));
1590
+ }
1591
+ function matchesSubmissionPageFilters(submission, options) {
1592
+ if (options.filter !== void 0 && !options.filter(submission)) return false;
1593
+ if (options.metadataFilters === void 0) return true;
1594
+ return Object.entries(options.metadataFilters).every(
1595
+ ([key, value]) => jsonValuesEqual(submission.metadata?.[key], value)
1596
+ );
1597
+ }
1480
1598
 
1481
1599
  // src/validation.ts
1482
1600
  var DEFAULT_MESSAGES = {
@@ -1915,6 +2033,12 @@ async function resolveFormTranslation(schema, adapter, targetLocale, sourceLocal
1915
2033
  }
1916
2034
 
1917
2035
  // src/versioning.ts
2036
+ function revisionConflict(state, expectedRevision) {
2037
+ return expectedRevision === void 0 || expectedRevision === state.revision ? void 0 : {
2038
+ success: false,
2039
+ error: { type: "revision_conflict", expectedRevision, actualRevision: state.revision }
2040
+ };
2041
+ }
1918
2042
  function validateState(state) {
1919
2043
  if (state.formId.trim().length === 0) throw new TypeError("formId must not be empty.");
1920
2044
  if (!Number.isSafeInteger(state.nextVersion) || state.nextVersion < 1) {
@@ -1927,9 +2051,25 @@ function validateState(state) {
1927
2051
  function cloneVersionToDraft(state, sourceSchema, options = {}) {
1928
2052
  validateState(state);
1929
2053
  if (sourceSchema.id !== state.formId) throw new TypeError("sourceSchema.id must match state.formId.");
2054
+ const conflict = revisionConflict(state, options.expectedRevision);
2055
+ if (conflict !== void 0) return conflict;
1930
2056
  if (state.draftVersion !== void 0) {
1931
2057
  return { success: false, error: { type: "draft_already_exists", currentDraftVersion: state.draftVersion } };
1932
2058
  }
2059
+ const allowedSourceVersions = /* @__PURE__ */ new Set([
2060
+ ...state.publishedVersion === void 0 ? [] : [state.publishedVersion],
2061
+ ...options.allowedSourceVersions ?? []
2062
+ ]);
2063
+ if (!allowedSourceVersions.has(sourceSchema.version)) {
2064
+ return {
2065
+ success: false,
2066
+ error: {
2067
+ type: "invalid_source_version",
2068
+ requestedVersion: sourceSchema.version,
2069
+ ...state.publishedVersion === void 0 ? {} : { publishedVersion: state.publishedVersion }
2070
+ }
2071
+ };
2072
+ }
1933
2073
  const maxVersions = options.maxVersions ?? Number.MAX_SAFE_INTEGER;
1934
2074
  if (!Number.isSafeInteger(maxVersions) || maxVersions < 1) {
1935
2075
  throw new TypeError("maxVersions must be a positive safe integer.");
@@ -1953,23 +2093,32 @@ function cloneVersionToDraft(state, sourceSchema, options = {}) {
1953
2093
  }
1954
2094
  function publishDraft(state, draftSchema, options = {}) {
1955
2095
  validateState(state);
1956
- if (options.expectedRevision !== void 0 && options.expectedRevision !== state.revision) {
2096
+ const conflict = revisionConflict(state, options.expectedRevision);
2097
+ if (conflict !== void 0) return conflict;
2098
+ if (state.draftVersion === void 0 || draftSchema.id !== state.formId || draftSchema.version !== state.draftVersion) {
2099
+ return { success: false, error: { type: "draft_not_found" } };
2100
+ }
2101
+ const validation = options.validate?.(draftSchema);
2102
+ if (validation === false || Array.isArray(validation) && validation.length > 0) {
1957
2103
  return {
1958
2104
  success: false,
1959
- error: {
1960
- type: "revision_conflict",
1961
- expectedRevision: options.expectedRevision,
1962
- actualRevision: state.revision
1963
- }
2105
+ error: { type: "validation_failed", issues: Array.isArray(validation) ? validation : [] }
1964
2106
  };
1965
2107
  }
1966
- if (state.draftVersion === void 0 || draftSchema.id !== state.formId || draftSchema.version !== state.draftVersion) {
1967
- return { success: false, error: { type: "draft_not_found" } };
1968
- }
1969
- if (options.validate?.(draftSchema) === false) throw new TypeError("Draft schema validation failed.");
1970
2108
  const timestamp = options.timestamp ?? "1970-01-01T00:00:00.000Z";
1971
2109
  if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError("timestamp must be a valid date string.");
1972
2110
  const archivedVersion = state.publishedVersion;
2111
+ const archivedRecords = archivedVersion === void 0 ? [] : [
2112
+ {
2113
+ formId: state.formId,
2114
+ version: archivedVersion,
2115
+ status: "archived",
2116
+ schema: { ...draftSchema, version: archivedVersion },
2117
+ createdAt: timestamp,
2118
+ publishedAt: timestamp,
2119
+ archivedAt: timestamp
2120
+ }
2121
+ ];
1973
2122
  const { draftVersion: _draftVersion, ...stateWithoutDraft } = state;
1974
2123
  return {
1975
2124
  success: true,
@@ -1987,12 +2136,15 @@ function publishDraft(state, draftSchema, options = {}) {
1987
2136
  createdAt: timestamp,
1988
2137
  publishedAt: timestamp
1989
2138
  },
2139
+ archivedRecords,
1990
2140
  ...archivedVersion === void 0 ? {} : { archivedVersion }
1991
2141
  }
1992
2142
  };
1993
2143
  }
1994
- function deleteDraft(state) {
2144
+ function deleteDraft(state, options = {}) {
1995
2145
  validateState(state);
2146
+ const conflict = revisionConflict(state, options.expectedRevision);
2147
+ if (conflict !== void 0) return conflict;
1996
2148
  if (state.draftVersion === void 0) return { success: false, error: { type: "draft_not_found" } };
1997
2149
  const { draftVersion: _draftVersion, ...stateWithoutDraft } = state;
1998
2150
  return {
@@ -2029,7 +2181,9 @@ function assertVersionMutable(status) {
2029
2181
  exportResponsesToCsvStream,
2030
2182
  isDisplayConditionSatisfied,
2031
2183
  isQuestionVisible,
2184
+ matchesSubmissionPageFilters,
2032
2185
  normalizeSubmissionPageSize,
2186
+ pipeResponsesToCsvStream,
2033
2187
  populateSchemaTranslations,
2034
2188
  publishDraft,
2035
2189
  resolveFormTranslation,
package/dist/index.d.cts CHANGED
@@ -1,3 +1,82 @@
1
+ type Result<T, E> = {
2
+ readonly success: true;
3
+ readonly value: T;
4
+ } | {
5
+ readonly success: false;
6
+ readonly error: E;
7
+ };
8
+ type FormVersionStatus = "draft" | "published" | "archived";
9
+ interface FormVersionRecord {
10
+ readonly formId: string;
11
+ readonly version: number;
12
+ readonly status: FormVersionStatus;
13
+ readonly schema: FormSchema;
14
+ readonly createdAt: string;
15
+ readonly publishedAt?: string;
16
+ readonly archivedAt?: string;
17
+ }
18
+ interface FormVersionState {
19
+ readonly formId: string;
20
+ readonly draftVersion?: number;
21
+ readonly publishedVersion?: number;
22
+ readonly nextVersion: number;
23
+ readonly revision: number;
24
+ }
25
+ type VersionTransitionError = {
26
+ readonly type: "draft_already_exists";
27
+ readonly currentDraftVersion: number;
28
+ } | {
29
+ readonly type: "draft_not_found";
30
+ } | {
31
+ readonly type: "revision_conflict";
32
+ readonly expectedRevision: number;
33
+ readonly actualRevision: number;
34
+ } | {
35
+ readonly type: "invalid_source_version";
36
+ readonly requestedVersion: number;
37
+ readonly publishedVersion?: number;
38
+ } | {
39
+ readonly type: "version_immutable";
40
+ readonly status: FormVersionStatus;
41
+ } | {
42
+ readonly type: "max_version_exceeded";
43
+ readonly max: number;
44
+ } | {
45
+ readonly type: "validation_failed";
46
+ readonly issues: readonly SchemaIssue[];
47
+ };
48
+ interface CloneVersionOptions {
49
+ readonly maxVersions?: number;
50
+ readonly expectedRevision?: number;
51
+ /** Additional known published versions that may be used as a clone source. */
52
+ readonly allowedSourceVersions?: readonly number[];
53
+ }
54
+ interface PublishDraftOptions {
55
+ readonly expectedRevision?: number;
56
+ readonly validate?: (schema: FormSchema) => boolean | readonly SchemaIssue[];
57
+ /** Supplies deterministic record timestamps while keeping the transition pure. */
58
+ readonly timestamp?: string;
59
+ }
60
+ interface DeleteDraftOptions {
61
+ readonly expectedRevision?: number;
62
+ }
63
+ interface PublishDraftResult {
64
+ readonly nextState: FormVersionState;
65
+ readonly publishedRecord: FormVersionRecord;
66
+ readonly archivedRecords: readonly FormVersionRecord[];
67
+ /** @deprecated Read archivedRecords instead. */
68
+ readonly archivedVersion?: number;
69
+ }
70
+ declare function cloneVersionToDraft(state: FormVersionState, sourceSchema: FormSchema, options?: CloneVersionOptions): Result<{
71
+ readonly nextState: FormVersionState;
72
+ readonly draftSchema: FormSchema;
73
+ }, VersionTransitionError>;
74
+ declare function publishDraft(state: FormVersionState, draftSchema: FormSchema, options?: PublishDraftOptions): Result<PublishDraftResult, VersionTransitionError>;
75
+ declare function deleteDraft(state: FormVersionState, options?: DeleteDraftOptions): Result<{
76
+ readonly nextState: FormVersionState;
77
+ }, VersionTransitionError>;
78
+ declare function assertVersionMutable(status: FormVersionStatus): void;
79
+
1
80
  type FieldType = "text" | "textarea" | "number" | "rating" | "select" | "multi-select" | "checkbox" | "radio";
2
81
  interface FormPolicy {
3
82
  readonly allowedFieldTypes?: readonly FieldType[];
@@ -170,6 +249,8 @@ interface SubmissionPageQueryOptions {
170
249
  readonly since?: string;
171
250
  readonly until?: string;
172
251
  readonly locale?: string;
252
+ readonly filter?: (submission: FormSubmission) => boolean;
253
+ readonly metadataFilters?: Readonly<Record<string, JsonValue>>;
173
254
  }
174
255
  interface SubmissionPage {
175
256
  readonly items: readonly FormSubmission[];
@@ -179,6 +260,19 @@ interface SubmissionPage {
179
260
  interface PagedSubmissionStorageAdapter extends FormStorageAdapter {
180
261
  listSubmissionPage(formId: string, options?: SubmissionPageQueryOptions): Promise<SubmissionPage>;
181
262
  }
263
+ interface VersionTransitionPlan {
264
+ readonly state: FormVersionState;
265
+ readonly expectedRevision: number;
266
+ readonly draftToPublish?: FormSchema;
267
+ readonly versionsToArchive: readonly number[];
268
+ readonly versionsToDelete: readonly number[];
269
+ }
270
+ interface VersionedFormStorageAdapter extends FormStorageAdapter {
271
+ commitVersionTransition(plan: VersionTransitionPlan): Promise<{
272
+ readonly success: boolean;
273
+ readonly error?: string;
274
+ }>;
275
+ }
182
276
  interface BaseQuestionAggregate {
183
277
  readonly fieldId: string;
184
278
  readonly answeredCount: number;
@@ -223,6 +317,7 @@ type ChoiceOption = FieldOption;
223
317
  interface FormResponse extends ExtensibleNode {
224
318
  readonly responseId: string;
225
319
  readonly formId: string;
320
+ readonly formVersion?: number;
226
321
  readonly sourceLocale?: string;
227
322
  readonly answers: Readonly<Record<string, unknown>>;
228
323
  readonly submittedAt: string;
@@ -251,14 +346,25 @@ declare function calculateNumericSummary(responses: readonly FormSubmission[], q
251
346
  declare function calculateCrossTabulation(responses: readonly FormSubmission[], rowQuestionId: string, colQuestionId: string): CrossTabulationResult;
252
347
  declare function aggregateResponses(schema: FormSchema, submissions: readonly FormSubmission[]): FormAnalytics;
253
348
  type AccumulatorResponse = FormSubmission | FormResponse;
349
+ type AccumulatorSkipReason = "form_id_mismatch" | "version_mismatch" | "invalid_structure";
350
+ interface AccumulatorReport {
351
+ readonly processedCount: number;
352
+ readonly skippedCount: number;
353
+ readonly skipReasons: readonly {
354
+ readonly responseId: string;
355
+ readonly reason: AccumulatorSkipReason;
356
+ }[];
357
+ }
254
358
  interface ResponseAccumulator {
255
359
  add(submission: AccumulatorResponse): {
256
360
  readonly success: boolean;
361
+ readonly skipped?: boolean;
257
362
  readonly error?: string;
258
363
  };
259
- addMany(submissions: Iterable<AccumulatorResponse>): void;
364
+ addMany(submissions: Iterable<AccumulatorResponse>): AccumulatorReport;
260
365
  merge(other: ResponseAccumulator): ResponseAccumulator;
261
366
  finalize(): FormAnalytics;
367
+ getReport(): AccumulatorReport;
262
368
  }
263
369
  interface ResponseAccumulatorOptions {
264
370
  readonly mode?: "strict" | "lenient";
@@ -271,13 +377,27 @@ interface CsvExportOptions {
271
377
  }
272
378
  interface CsvColumnDef {
273
379
  readonly header: string;
274
- readonly getValue: (submission: FormResponse) => string | number | boolean | null | undefined;
380
+ readonly getValue: (context: CsvColumnContext) => string | number | boolean | null | undefined;
381
+ }
382
+ interface CsvColumnContext extends FormResponse {
383
+ readonly submission: FormResponse;
384
+ readonly formVersion: number;
385
+ readonly schema: FormSchema;
275
386
  }
276
387
  interface StreamCsvOptions extends CsvExportOptions {
277
388
  readonly columns?: readonly CsvColumnDef[];
278
389
  readonly includeDefaultColumns?: boolean;
279
390
  }
280
391
  declare function exportResponsesToCsvStream(schema: FormSchema, submissions: AsyncIterable<AccumulatorResponse>, options?: StreamCsvOptions): AsyncIterable<string>;
392
+ interface NodeWritableStream {
393
+ write(chunk: Uint8Array): boolean;
394
+ once(event: "drain", listener: () => void): unknown;
395
+ once(event: "error", listener: (error: Error) => void): unknown;
396
+ removeListener(event: "drain", listener: () => void): unknown;
397
+ removeListener(event: "error", listener: (error: Error) => void): unknown;
398
+ end(callback: () => void): unknown;
399
+ }
400
+ declare function pipeResponsesToCsvStream(schema: FormSchema, submissions: AsyncIterable<AccumulatorResponse>, writable: WritableStream<Uint8Array> | NodeWritableStream, options?: StreamCsvOptions): Promise<void>;
281
401
  declare function exportResponsesToCsv(schema: FormSchema, responses: readonly FormSubmission[], options?: CsvExportOptions): string;
282
402
 
283
403
  type FormEventType = "response.submitted" | "schema.updated";
@@ -314,6 +434,7 @@ interface SubmissionCursorValue {
314
434
  declare function encodeSubmissionCursor(value: SubmissionCursorValue): string;
315
435
  declare function decodeSubmissionCursor(cursor: string): SubmissionCursorValue;
316
436
  declare function normalizeSubmissionPageSize(pageSize: number | undefined, fallback?: number): number;
437
+ declare function matchesSubmissionPageFilters(submission: FormSubmission, options: Pick<SubmissionPageQueryOptions, "filter" | "metadataFilters">): boolean;
317
438
 
318
439
  interface CollectedLocales {
319
440
  readonly defaultLocale?: string;
@@ -382,73 +503,10 @@ declare function resolveFormTranslation(schema: FormSchema, adapter: AsyncTransl
382
503
  declare function validateAnswers(schema: FormSchema, values: FormValues): AnswerValidationResult;
383
504
  declare function validatePageAnswers(schema: FormSchema, pageIndex: number, values: FormValues): AnswerValidationResult;
384
505
 
385
- type Result<T, E> = {
386
- readonly success: true;
387
- readonly value: T;
388
- } | {
389
- readonly success: false;
390
- readonly error: E;
391
- };
392
- type FormVersionStatus = "draft" | "published" | "archived";
393
- interface FormVersionRecord {
394
- readonly formId: string;
395
- readonly version: number;
396
- readonly status: FormVersionStatus;
397
- readonly schema: FormSchema;
398
- readonly createdAt: string;
399
- readonly publishedAt?: string;
400
- readonly archivedAt?: string;
401
- }
402
- interface FormVersionState {
403
- readonly formId: string;
404
- readonly draftVersion?: number;
405
- readonly publishedVersion?: number;
406
- readonly nextVersion: number;
407
- readonly revision: number;
408
- }
409
- type VersionTransitionError = {
410
- readonly type: "draft_already_exists";
411
- readonly currentDraftVersion: number;
412
- } | {
413
- readonly type: "draft_not_found";
414
- } | {
415
- readonly type: "revision_conflict";
416
- readonly expectedRevision: number;
417
- readonly actualRevision: number;
418
- } | {
419
- readonly type: "version_immutable";
420
- readonly status: FormVersionStatus;
421
- } | {
422
- readonly type: "max_version_exceeded";
423
- readonly max: number;
424
- };
425
- interface CloneVersionOptions {
426
- readonly maxVersions?: number;
427
- }
428
- interface PublishDraftOptions {
429
- readonly expectedRevision?: number;
430
- readonly validate?: (schema: FormSchema) => boolean;
431
- /** Supplies deterministic record timestamps while keeping the transition pure. */
432
- readonly timestamp?: string;
433
- }
434
- declare function cloneVersionToDraft(state: FormVersionState, sourceSchema: FormSchema, options?: CloneVersionOptions): Result<{
435
- readonly nextState: FormVersionState;
436
- readonly draftSchema: FormSchema;
437
- }, VersionTransitionError>;
438
- declare function publishDraft(state: FormVersionState, draftSchema: FormSchema, options?: PublishDraftOptions): Result<{
439
- readonly nextState: FormVersionState;
440
- readonly publishedRecord: FormVersionRecord;
441
- readonly archivedVersion?: number;
442
- }, VersionTransitionError>;
443
- declare function deleteDraft(state: FormVersionState): Result<{
444
- readonly nextState: FormVersionState;
445
- }, VersionTransitionError>;
446
- declare function assertVersionMutable(status: FormVersionStatus): void;
447
-
448
506
  declare function isQuestionVisible(question: FormField, currentAnswers: Readonly<Record<string, unknown>>): boolean;
449
507
  declare function isDisplayConditionSatisfied(condition: DisplayCondition | undefined, currentAnswers: Readonly<Record<string, unknown>>): boolean;
450
508
  declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
451
509
  declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
452
510
  declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
453
511
 
454
- export { type AccumulatorResponse, 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 CsvColumnDef, type CsvExportOptions, 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 NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PopulateTranslationOptions, type PublishDraftOptions, 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 WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, createResponseAccumulator, createSubmission, decodeSubmissionCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, isDisplayConditionSatisfied, isQuestionVisible, normalizeSubmissionPageSize, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
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 };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,82 @@
1
+ type Result<T, E> = {
2
+ readonly success: true;
3
+ readonly value: T;
4
+ } | {
5
+ readonly success: false;
6
+ readonly error: E;
7
+ };
8
+ type FormVersionStatus = "draft" | "published" | "archived";
9
+ interface FormVersionRecord {
10
+ readonly formId: string;
11
+ readonly version: number;
12
+ readonly status: FormVersionStatus;
13
+ readonly schema: FormSchema;
14
+ readonly createdAt: string;
15
+ readonly publishedAt?: string;
16
+ readonly archivedAt?: string;
17
+ }
18
+ interface FormVersionState {
19
+ readonly formId: string;
20
+ readonly draftVersion?: number;
21
+ readonly publishedVersion?: number;
22
+ readonly nextVersion: number;
23
+ readonly revision: number;
24
+ }
25
+ type VersionTransitionError = {
26
+ readonly type: "draft_already_exists";
27
+ readonly currentDraftVersion: number;
28
+ } | {
29
+ readonly type: "draft_not_found";
30
+ } | {
31
+ readonly type: "revision_conflict";
32
+ readonly expectedRevision: number;
33
+ readonly actualRevision: number;
34
+ } | {
35
+ readonly type: "invalid_source_version";
36
+ readonly requestedVersion: number;
37
+ readonly publishedVersion?: number;
38
+ } | {
39
+ readonly type: "version_immutable";
40
+ readonly status: FormVersionStatus;
41
+ } | {
42
+ readonly type: "max_version_exceeded";
43
+ readonly max: number;
44
+ } | {
45
+ readonly type: "validation_failed";
46
+ readonly issues: readonly SchemaIssue[];
47
+ };
48
+ interface CloneVersionOptions {
49
+ readonly maxVersions?: number;
50
+ readonly expectedRevision?: number;
51
+ /** Additional known published versions that may be used as a clone source. */
52
+ readonly allowedSourceVersions?: readonly number[];
53
+ }
54
+ interface PublishDraftOptions {
55
+ readonly expectedRevision?: number;
56
+ readonly validate?: (schema: FormSchema) => boolean | readonly SchemaIssue[];
57
+ /** Supplies deterministic record timestamps while keeping the transition pure. */
58
+ readonly timestamp?: string;
59
+ }
60
+ interface DeleteDraftOptions {
61
+ readonly expectedRevision?: number;
62
+ }
63
+ interface PublishDraftResult {
64
+ readonly nextState: FormVersionState;
65
+ readonly publishedRecord: FormVersionRecord;
66
+ readonly archivedRecords: readonly FormVersionRecord[];
67
+ /** @deprecated Read archivedRecords instead. */
68
+ readonly archivedVersion?: number;
69
+ }
70
+ declare function cloneVersionToDraft(state: FormVersionState, sourceSchema: FormSchema, options?: CloneVersionOptions): Result<{
71
+ readonly nextState: FormVersionState;
72
+ readonly draftSchema: FormSchema;
73
+ }, VersionTransitionError>;
74
+ declare function publishDraft(state: FormVersionState, draftSchema: FormSchema, options?: PublishDraftOptions): Result<PublishDraftResult, VersionTransitionError>;
75
+ declare function deleteDraft(state: FormVersionState, options?: DeleteDraftOptions): Result<{
76
+ readonly nextState: FormVersionState;
77
+ }, VersionTransitionError>;
78
+ declare function assertVersionMutable(status: FormVersionStatus): void;
79
+
1
80
  type FieldType = "text" | "textarea" | "number" | "rating" | "select" | "multi-select" | "checkbox" | "radio";
2
81
  interface FormPolicy {
3
82
  readonly allowedFieldTypes?: readonly FieldType[];
@@ -170,6 +249,8 @@ interface SubmissionPageQueryOptions {
170
249
  readonly since?: string;
171
250
  readonly until?: string;
172
251
  readonly locale?: string;
252
+ readonly filter?: (submission: FormSubmission) => boolean;
253
+ readonly metadataFilters?: Readonly<Record<string, JsonValue>>;
173
254
  }
174
255
  interface SubmissionPage {
175
256
  readonly items: readonly FormSubmission[];
@@ -179,6 +260,19 @@ interface SubmissionPage {
179
260
  interface PagedSubmissionStorageAdapter extends FormStorageAdapter {
180
261
  listSubmissionPage(formId: string, options?: SubmissionPageQueryOptions): Promise<SubmissionPage>;
181
262
  }
263
+ interface VersionTransitionPlan {
264
+ readonly state: FormVersionState;
265
+ readonly expectedRevision: number;
266
+ readonly draftToPublish?: FormSchema;
267
+ readonly versionsToArchive: readonly number[];
268
+ readonly versionsToDelete: readonly number[];
269
+ }
270
+ interface VersionedFormStorageAdapter extends FormStorageAdapter {
271
+ commitVersionTransition(plan: VersionTransitionPlan): Promise<{
272
+ readonly success: boolean;
273
+ readonly error?: string;
274
+ }>;
275
+ }
182
276
  interface BaseQuestionAggregate {
183
277
  readonly fieldId: string;
184
278
  readonly answeredCount: number;
@@ -223,6 +317,7 @@ type ChoiceOption = FieldOption;
223
317
  interface FormResponse extends ExtensibleNode {
224
318
  readonly responseId: string;
225
319
  readonly formId: string;
320
+ readonly formVersion?: number;
226
321
  readonly sourceLocale?: string;
227
322
  readonly answers: Readonly<Record<string, unknown>>;
228
323
  readonly submittedAt: string;
@@ -251,14 +346,25 @@ declare function calculateNumericSummary(responses: readonly FormSubmission[], q
251
346
  declare function calculateCrossTabulation(responses: readonly FormSubmission[], rowQuestionId: string, colQuestionId: string): CrossTabulationResult;
252
347
  declare function aggregateResponses(schema: FormSchema, submissions: readonly FormSubmission[]): FormAnalytics;
253
348
  type AccumulatorResponse = FormSubmission | FormResponse;
349
+ type AccumulatorSkipReason = "form_id_mismatch" | "version_mismatch" | "invalid_structure";
350
+ interface AccumulatorReport {
351
+ readonly processedCount: number;
352
+ readonly skippedCount: number;
353
+ readonly skipReasons: readonly {
354
+ readonly responseId: string;
355
+ readonly reason: AccumulatorSkipReason;
356
+ }[];
357
+ }
254
358
  interface ResponseAccumulator {
255
359
  add(submission: AccumulatorResponse): {
256
360
  readonly success: boolean;
361
+ readonly skipped?: boolean;
257
362
  readonly error?: string;
258
363
  };
259
- addMany(submissions: Iterable<AccumulatorResponse>): void;
364
+ addMany(submissions: Iterable<AccumulatorResponse>): AccumulatorReport;
260
365
  merge(other: ResponseAccumulator): ResponseAccumulator;
261
366
  finalize(): FormAnalytics;
367
+ getReport(): AccumulatorReport;
262
368
  }
263
369
  interface ResponseAccumulatorOptions {
264
370
  readonly mode?: "strict" | "lenient";
@@ -271,13 +377,27 @@ interface CsvExportOptions {
271
377
  }
272
378
  interface CsvColumnDef {
273
379
  readonly header: string;
274
- readonly getValue: (submission: FormResponse) => string | number | boolean | null | undefined;
380
+ readonly getValue: (context: CsvColumnContext) => string | number | boolean | null | undefined;
381
+ }
382
+ interface CsvColumnContext extends FormResponse {
383
+ readonly submission: FormResponse;
384
+ readonly formVersion: number;
385
+ readonly schema: FormSchema;
275
386
  }
276
387
  interface StreamCsvOptions extends CsvExportOptions {
277
388
  readonly columns?: readonly CsvColumnDef[];
278
389
  readonly includeDefaultColumns?: boolean;
279
390
  }
280
391
  declare function exportResponsesToCsvStream(schema: FormSchema, submissions: AsyncIterable<AccumulatorResponse>, options?: StreamCsvOptions): AsyncIterable<string>;
392
+ interface NodeWritableStream {
393
+ write(chunk: Uint8Array): boolean;
394
+ once(event: "drain", listener: () => void): unknown;
395
+ once(event: "error", listener: (error: Error) => void): unknown;
396
+ removeListener(event: "drain", listener: () => void): unknown;
397
+ removeListener(event: "error", listener: (error: Error) => void): unknown;
398
+ end(callback: () => void): unknown;
399
+ }
400
+ declare function pipeResponsesToCsvStream(schema: FormSchema, submissions: AsyncIterable<AccumulatorResponse>, writable: WritableStream<Uint8Array> | NodeWritableStream, options?: StreamCsvOptions): Promise<void>;
281
401
  declare function exportResponsesToCsv(schema: FormSchema, responses: readonly FormSubmission[], options?: CsvExportOptions): string;
282
402
 
283
403
  type FormEventType = "response.submitted" | "schema.updated";
@@ -314,6 +434,7 @@ interface SubmissionCursorValue {
314
434
  declare function encodeSubmissionCursor(value: SubmissionCursorValue): string;
315
435
  declare function decodeSubmissionCursor(cursor: string): SubmissionCursorValue;
316
436
  declare function normalizeSubmissionPageSize(pageSize: number | undefined, fallback?: number): number;
437
+ declare function matchesSubmissionPageFilters(submission: FormSubmission, options: Pick<SubmissionPageQueryOptions, "filter" | "metadataFilters">): boolean;
317
438
 
318
439
  interface CollectedLocales {
319
440
  readonly defaultLocale?: string;
@@ -382,73 +503,10 @@ declare function resolveFormTranslation(schema: FormSchema, adapter: AsyncTransl
382
503
  declare function validateAnswers(schema: FormSchema, values: FormValues): AnswerValidationResult;
383
504
  declare function validatePageAnswers(schema: FormSchema, pageIndex: number, values: FormValues): AnswerValidationResult;
384
505
 
385
- type Result<T, E> = {
386
- readonly success: true;
387
- readonly value: T;
388
- } | {
389
- readonly success: false;
390
- readonly error: E;
391
- };
392
- type FormVersionStatus = "draft" | "published" | "archived";
393
- interface FormVersionRecord {
394
- readonly formId: string;
395
- readonly version: number;
396
- readonly status: FormVersionStatus;
397
- readonly schema: FormSchema;
398
- readonly createdAt: string;
399
- readonly publishedAt?: string;
400
- readonly archivedAt?: string;
401
- }
402
- interface FormVersionState {
403
- readonly formId: string;
404
- readonly draftVersion?: number;
405
- readonly publishedVersion?: number;
406
- readonly nextVersion: number;
407
- readonly revision: number;
408
- }
409
- type VersionTransitionError = {
410
- readonly type: "draft_already_exists";
411
- readonly currentDraftVersion: number;
412
- } | {
413
- readonly type: "draft_not_found";
414
- } | {
415
- readonly type: "revision_conflict";
416
- readonly expectedRevision: number;
417
- readonly actualRevision: number;
418
- } | {
419
- readonly type: "version_immutable";
420
- readonly status: FormVersionStatus;
421
- } | {
422
- readonly type: "max_version_exceeded";
423
- readonly max: number;
424
- };
425
- interface CloneVersionOptions {
426
- readonly maxVersions?: number;
427
- }
428
- interface PublishDraftOptions {
429
- readonly expectedRevision?: number;
430
- readonly validate?: (schema: FormSchema) => boolean;
431
- /** Supplies deterministic record timestamps while keeping the transition pure. */
432
- readonly timestamp?: string;
433
- }
434
- declare function cloneVersionToDraft(state: FormVersionState, sourceSchema: FormSchema, options?: CloneVersionOptions): Result<{
435
- readonly nextState: FormVersionState;
436
- readonly draftSchema: FormSchema;
437
- }, VersionTransitionError>;
438
- declare function publishDraft(state: FormVersionState, draftSchema: FormSchema, options?: PublishDraftOptions): Result<{
439
- readonly nextState: FormVersionState;
440
- readonly publishedRecord: FormVersionRecord;
441
- readonly archivedVersion?: number;
442
- }, VersionTransitionError>;
443
- declare function deleteDraft(state: FormVersionState): Result<{
444
- readonly nextState: FormVersionState;
445
- }, VersionTransitionError>;
446
- declare function assertVersionMutable(status: FormVersionStatus): void;
447
-
448
506
  declare function isQuestionVisible(question: FormField, currentAnswers: Readonly<Record<string, unknown>>): boolean;
449
507
  declare function isDisplayConditionSatisfied(condition: DisplayCondition | undefined, currentAnswers: Readonly<Record<string, unknown>>): boolean;
450
508
  declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
451
509
  declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
452
510
  declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
453
511
 
454
- export { type AccumulatorResponse, 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 CsvColumnDef, type CsvExportOptions, 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 NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PopulateTranslationOptions, type PublishDraftOptions, 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 WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, createResponseAccumulator, createSubmission, decodeSubmissionCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, isDisplayConditionSatisfied, isQuestionVisible, normalizeSubmissionPageSize, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
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 };
package/dist/index.js CHANGED
@@ -1043,17 +1043,38 @@ function aggregateResponses(schema, submissions) {
1043
1043
  };
1044
1044
  }
1045
1045
  function responseValues(submission) {
1046
- return "values" in submission ? submission.values : submission.answers;
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;
1047
1049
  }
1048
1050
  function responseIdentifier(submission) {
1049
- return "id" in submission ? submission.id : submission.responseId;
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);
1050
1060
  }
1051
- function responseMismatch(schema, submission) {
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
+ }
1052
1070
  if (submission.formId !== schema.id) {
1053
- return `Submission ${responseIdentifier(submission)} does not match form ${schema.id}.`;
1071
+ return { reason: "form_id_mismatch", error: `Submission ${identifier} does not match form ${schema.id}.` };
1054
1072
  }
1055
- if ("formVersion" in submission && submission.formVersion !== schema.version) {
1056
- return `Submission ${responseIdentifier(submission)} does not match ${schema.id}@${schema.version}.`;
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
+ };
1057
1078
  }
1058
1079
  return void 0;
1059
1080
  }
@@ -1061,6 +1082,7 @@ var IncrementalResponseAccumulator = class _IncrementalResponseAccumulator {
1061
1082
  #schema;
1062
1083
  #mode;
1063
1084
  #fields;
1085
+ #skipReasons = [];
1064
1086
  #submissionCount = 0;
1065
1087
  constructor(schema, options) {
1066
1088
  assertValidFormSchema(schema);
@@ -1082,9 +1104,14 @@ var IncrementalResponseAccumulator = class _IncrementalResponseAccumulator {
1082
1104
  );
1083
1105
  }
1084
1106
  add(submission) {
1085
- const mismatch = responseMismatch(this.#schema, submission);
1086
- if (mismatch !== void 0 && this.#mode === "strict") return { success: false, error: mismatch };
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
+ }
1087
1113
  const values = responseValues(submission);
1114
+ if (!isAnswerRecord(values)) throw new Error("Validated response answers are unavailable.");
1088
1115
  const visibility = calculateFieldVisibility(this.#schema, values);
1089
1116
  for (const field of this.#schema.fields) {
1090
1117
  const accumulator = this.#fields.get(field.id);
@@ -1114,6 +1141,7 @@ var IncrementalResponseAccumulator = class _IncrementalResponseAccumulator {
1114
1141
  const result = this.add(submission);
1115
1142
  if (!result.success) throw new TypeError(result.error ?? "Submission could not be accumulated.");
1116
1143
  }
1144
+ return this.getReport();
1117
1145
  }
1118
1146
  merge(other) {
1119
1147
  if (!(other instanceof _IncrementalResponseAccumulator)) {
@@ -1123,6 +1151,7 @@ var IncrementalResponseAccumulator = class _IncrementalResponseAccumulator {
1123
1151
  throw new TypeError("Response accumulators must use the same schema.");
1124
1152
  }
1125
1153
  this.#submissionCount += other.#submissionCount;
1154
+ this.#skipReasons.push(...other.#skipReasons);
1126
1155
  for (const [fieldId, source] of other.#fields) {
1127
1156
  const target = this.#fields.get(fieldId);
1128
1157
  if (target === void 0) throw new Error(`Accumulator for ${fieldId} is unavailable.`);
@@ -1138,6 +1167,13 @@ var IncrementalResponseAccumulator = class _IncrementalResponseAccumulator {
1138
1167
  }
1139
1168
  return this;
1140
1169
  }
1170
+ getReport() {
1171
+ return {
1172
+ processedCount: this.#submissionCount,
1173
+ skippedCount: this.#skipReasons.length,
1174
+ skipReasons: this.#skipReasons.map((reason) => ({ ...reason }))
1175
+ };
1176
+ }
1141
1177
  finalize() {
1142
1178
  return {
1143
1179
  formId: this.#schema.id,
@@ -1209,6 +1245,7 @@ function asFormResponse(submission) {
1209
1245
  responseId: submission.id,
1210
1246
  formId: submission.formId,
1211
1247
  sourceLocale: submission.locale,
1248
+ formVersion: submission.formVersion,
1212
1249
  answers: submission.values,
1213
1250
  submittedAt: submission.submittedAt,
1214
1251
  ...submission.metadata === void 0 ? {} : { metadata: submission.metadata },
@@ -1232,8 +1269,8 @@ async function* exportResponsesToCsvStream(schema, submissions, options = {}) {
1232
1269
  const header = headers.map((value) => escapeCsvCell(value, neutralizeFormulas)).join(",");
1233
1270
  yield `${options.withBom ?? true ? "\uFEFF" : ""}${header}`;
1234
1271
  for await (const submission of submissions) {
1235
- const mismatch = responseMismatch(schema, submission);
1236
- if (mismatch !== void 0) throw new TypeError(mismatch);
1272
+ const problem = responseProblem(schema, submission);
1273
+ if (problem !== void 0) throw new TypeError(problem.error);
1237
1274
  const response = asFormResponse(submission);
1238
1275
  const answers = response.answers;
1239
1276
  const visible = selectVisibleAnswers(schema, answers);
@@ -1243,11 +1280,64 @@ async function* exportResponsesToCsvStream(schema, submissions, options = {}) {
1243
1280
  response.sourceLocale ?? "",
1244
1281
  ...schema.fields.map((field) => serializeUnknown(visible[field.id]))
1245
1282
  ] : [];
1246
- const customCells = customColumns.map((column) => column.getValue(response));
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));
1247
1290
  yield `\r
1248
1291
  ${[...defaultCells, ...customCells].map((value) => escapeCsvCell(value, neutralizeFormulas)).join(",")}`;
1249
1292
  }
1250
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
+ }
1251
1341
  function exportResponsesToCsv(schema, responses, options = {}) {
1252
1342
  assertValidFormSchema(schema);
1253
1343
  for (const response of responses) {
@@ -1419,6 +1509,32 @@ function normalizeSubmissionPageSize(pageSize, fallback = 100) {
1419
1509
  if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("pageSize must be a positive safe integer.");
1420
1510
  return value;
1421
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
+ }
1422
1538
 
1423
1539
  // src/validation.ts
1424
1540
  var DEFAULT_MESSAGES = {
@@ -1857,6 +1973,12 @@ async function resolveFormTranslation(schema, adapter, targetLocale, sourceLocal
1857
1973
  }
1858
1974
 
1859
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
+ }
1860
1982
  function validateState(state) {
1861
1983
  if (state.formId.trim().length === 0) throw new TypeError("formId must not be empty.");
1862
1984
  if (!Number.isSafeInteger(state.nextVersion) || state.nextVersion < 1) {
@@ -1869,9 +1991,25 @@ function validateState(state) {
1869
1991
  function cloneVersionToDraft(state, sourceSchema, options = {}) {
1870
1992
  validateState(state);
1871
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;
1872
1996
  if (state.draftVersion !== void 0) {
1873
1997
  return { success: false, error: { type: "draft_already_exists", currentDraftVersion: state.draftVersion } };
1874
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
+ }
1875
2013
  const maxVersions = options.maxVersions ?? Number.MAX_SAFE_INTEGER;
1876
2014
  if (!Number.isSafeInteger(maxVersions) || maxVersions < 1) {
1877
2015
  throw new TypeError("maxVersions must be a positive safe integer.");
@@ -1895,23 +2033,32 @@ function cloneVersionToDraft(state, sourceSchema, options = {}) {
1895
2033
  }
1896
2034
  function publishDraft(state, draftSchema, options = {}) {
1897
2035
  validateState(state);
1898
- if (options.expectedRevision !== void 0 && options.expectedRevision !== state.revision) {
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) {
1899
2043
  return {
1900
2044
  success: false,
1901
- error: {
1902
- type: "revision_conflict",
1903
- expectedRevision: options.expectedRevision,
1904
- actualRevision: state.revision
1905
- }
2045
+ error: { type: "validation_failed", issues: Array.isArray(validation) ? validation : [] }
1906
2046
  };
1907
2047
  }
1908
- if (state.draftVersion === void 0 || draftSchema.id !== state.formId || draftSchema.version !== state.draftVersion) {
1909
- return { success: false, error: { type: "draft_not_found" } };
1910
- }
1911
- if (options.validate?.(draftSchema) === false) throw new TypeError("Draft schema validation failed.");
1912
2048
  const timestamp = options.timestamp ?? "1970-01-01T00:00:00.000Z";
1913
2049
  if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError("timestamp must be a valid date string.");
1914
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
+ ];
1915
2062
  const { draftVersion: _draftVersion, ...stateWithoutDraft } = state;
1916
2063
  return {
1917
2064
  success: true,
@@ -1929,12 +2076,15 @@ function publishDraft(state, draftSchema, options = {}) {
1929
2076
  createdAt: timestamp,
1930
2077
  publishedAt: timestamp
1931
2078
  },
2079
+ archivedRecords,
1932
2080
  ...archivedVersion === void 0 ? {} : { archivedVersion }
1933
2081
  }
1934
2082
  };
1935
2083
  }
1936
- function deleteDraft(state) {
2084
+ function deleteDraft(state, options = {}) {
1937
2085
  validateState(state);
2086
+ const conflict = revisionConflict(state, options.expectedRevision);
2087
+ if (conflict !== void 0) return conflict;
1938
2088
  if (state.draftVersion === void 0) return { success: false, error: { type: "draft_not_found" } };
1939
2089
  const { draftVersion: _draftVersion, ...stateWithoutDraft } = state;
1940
2090
  return {
@@ -1970,7 +2120,9 @@ export {
1970
2120
  exportResponsesToCsvStream,
1971
2121
  isDisplayConditionSatisfied,
1972
2122
  isQuestionVisible,
2123
+ matchesSubmissionPageFilters,
1973
2124
  normalizeSubmissionPageSize,
2125
+ pipeResponsesToCsvStream,
1974
2126
  populateSchemaTranslations,
1975
2127
  publishDraft,
1976
2128
  resolveFormTranslation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/core",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },