@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 +11 -3
- package/dist/index.cjs +176 -22
- package/dist/index.d.cts +124 -66
- package/dist/index.d.ts +124 -66
- package/dist/index.js +174 -22
- package/package.json +1 -1
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 ${
|
|
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
|
|
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
|
|
1144
|
-
if (
|
|
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
|
|
1294
|
-
if (
|
|
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
|
|
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
|
-
|
|
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>):
|
|
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: (
|
|
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>):
|
|
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: (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 ${
|
|
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
|
|
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
|
|
1086
|
-
if (
|
|
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
|
|
1236
|
-
if (
|
|
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
|
|
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
|
-
|
|
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,
|