@form-engine-ts/core 2.6.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/dist/index.cjs +165 -7
- package/dist/index.d.cts +99 -6
- package/dist/index.d.ts +99 -6
- package/dist/index.js +159 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,11 +77,16 @@ 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`, asynchronous `publishDraft`, and `deleteDraft` implement revision-checked version transitions as
|
|
80
|
-
pure functions.
|
|
80
|
+
pure functions. `createCloneTransitionPlan`, `createPublishTransitionPlan`, and `createDeleteDraftTransitionPlan` produce
|
|
81
|
+
complete persistence plans with the next state, affected records, and immutable audit events.
|
|
82
|
+
Publishing when state already identifies a Published version requires its matching `currentPublishedRecord`; omission or
|
|
83
|
+
a version mismatch returns the typed `missing_published_record` error. The resulting archive preserves the original
|
|
84
|
+
schema, creation timestamp, and metadata.
|
|
81
85
|
Clone/delete operations accept `expectedRevision`; cloning rejects non-published sources, publish validation failures are
|
|
82
86
|
returned as typed `validation_failed` issues, and successful publishing archives only a supplied actual published record,
|
|
83
87
|
preserving its schema and metadata. `createPublishTransitionPlan` returns complete records plus expected/next revisions for
|
|
84
|
-
storage adapters implementing `VersionedFormStorageAdapter` to commit atomically.
|
|
88
|
+
storage adapters implementing `VersionedFormStorageAdapter` to commit atomically. Versioned adapters expose state/record
|
|
89
|
+
reads and return a typed `Result` from `commitVersionTransition`, including the actual revision on concurrency conflicts.
|
|
85
90
|
`createResponseAccumulator` incrementally counts choices, answered/unanswered values, and numeric summaries without retaining
|
|
86
91
|
free-text bodies. In lenient mode, mismatched responses are skipped and exposed by `addMany()` and `getReport()` instead of
|
|
87
92
|
being included silently. Independent accumulators for the same schema can be merged, and `finalize()` matches
|
|
@@ -94,6 +99,9 @@ Formula-injection neutralization applies to both default and custom columns.
|
|
|
94
99
|
|
|
95
100
|
Adapters implementing `PagedSubmissionStorageAdapter` expose `listSubmissionPage(formId, options)`. The opaque Base64
|
|
96
101
|
cursor combines `submittedAt` and response ID, so equal timestamps do not produce gaps or duplicates. `metadataFilters`
|
|
97
|
-
and `filter` are applied before page sizing.
|
|
102
|
+
and `filter` are applied before page sizing. `filter` accepts a composable `eq`/`in`/`range`/`exists` and/or AST; adapters
|
|
103
|
+
may push supported nodes to their native query language while preserving identical client-side semantics. Adapters that
|
|
104
|
+
implement `listTextAnswerPage` expose stable cursor pagination over individual text answers.
|
|
105
|
+
`TextAnswerPageQueryOptions.fieldIds` can select multiple free-text fields for item-level paging.
|
|
98
106
|
|
|
99
107
|
See the [project documentation](https://github.com/nitta-a/form-engine-ts#readme) for the complete schema and API guide.
|
package/dist/index.cjs
CHANGED
|
@@ -30,18 +30,24 @@ __export(index_exports, {
|
|
|
30
30
|
calculatePageVisibility: () => calculatePageVisibility,
|
|
31
31
|
cloneVersionToDraft: () => cloneVersionToDraft,
|
|
32
32
|
collectSchemaLocales: () => collectSchemaLocales,
|
|
33
|
+
createCloneTransitionPlan: () => createCloneTransitionPlan,
|
|
34
|
+
createDeleteDraftTransitionPlan: () => createDeleteDraftTransitionPlan,
|
|
33
35
|
createPublishTransitionPlan: () => createPublishTransitionPlan,
|
|
34
36
|
createResponseAccumulator: () => createResponseAccumulator,
|
|
35
37
|
createSubmission: () => createSubmission,
|
|
36
38
|
decodeSubmissionCursor: () => decodeSubmissionCursor,
|
|
39
|
+
decodeTextAnswerCursor: () => decodeTextAnswerCursor,
|
|
37
40
|
deleteDraft: () => deleteDraft,
|
|
38
41
|
dispatchWebhook: () => dispatchWebhook,
|
|
39
42
|
encodeSubmissionCursor: () => encodeSubmissionCursor,
|
|
43
|
+
encodeTextAnswerCursor: () => encodeTextAnswerCursor,
|
|
40
44
|
escapeCsvCell: () => escapeCsvCell,
|
|
41
45
|
exportResponsesToCsv: () => exportResponsesToCsv,
|
|
42
46
|
exportResponsesToCsvStream: () => exportResponsesToCsvStream,
|
|
43
47
|
isDisplayConditionSatisfied: () => isDisplayConditionSatisfied,
|
|
44
48
|
isQuestionVisible: () => isQuestionVisible,
|
|
49
|
+
jsonValuesEqual: () => jsonValuesEqual,
|
|
50
|
+
matchesSubmissionFilter: () => matchesSubmissionFilter,
|
|
45
51
|
matchesSubmissionPageFilters: () => matchesSubmissionPageFilters,
|
|
46
52
|
normalizeSubmissionPageSize: () => normalizeSubmissionPageSize,
|
|
47
53
|
pipeResponsesToCsvStream: () => pipeResponsesToCsvStream,
|
|
@@ -1565,6 +1571,24 @@ function decodeSubmissionCursor(cursor) {
|
|
|
1565
1571
|
throw new TypeError("cursor must be a valid form-engine cursor.", { cause });
|
|
1566
1572
|
}
|
|
1567
1573
|
}
|
|
1574
|
+
function encodeTextAnswerCursor(value) {
|
|
1575
|
+
if (value.responseId.length === 0 || value.fieldId.length === 0) {
|
|
1576
|
+
throw new TypeError("Text answer cursor values must not be empty.");
|
|
1577
|
+
}
|
|
1578
|
+
return encodeBase64(new TextEncoder().encode(JSON.stringify(value)));
|
|
1579
|
+
}
|
|
1580
|
+
function decodeTextAnswerCursor(cursor) {
|
|
1581
|
+
try {
|
|
1582
|
+
const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(decodeBase64(cursor)));
|
|
1583
|
+
if (typeof parsed !== "object" || parsed === null || !("responseId" in parsed) || typeof parsed.responseId !== "string" || parsed.responseId.length === 0 || !("fieldId" in parsed) || typeof parsed.fieldId !== "string" || parsed.fieldId.length === 0) {
|
|
1584
|
+
throw new TypeError("text answer cursor payload is invalid.");
|
|
1585
|
+
}
|
|
1586
|
+
return { responseId: parsed.responseId, fieldId: parsed.fieldId };
|
|
1587
|
+
} catch (cause) {
|
|
1588
|
+
if (cause instanceof TypeError && cause.message === "text answer cursor payload is invalid.") throw cause;
|
|
1589
|
+
throw new TypeError("cursor must be a valid text answer cursor.", { cause });
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1568
1592
|
function normalizeSubmissionPageSize(pageSize, fallback = 100) {
|
|
1569
1593
|
const value = pageSize ?? fallback;
|
|
1570
1594
|
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("pageSize must be a positive safe integer.");
|
|
@@ -1589,8 +1613,39 @@ function jsonValuesEqual(left, right) {
|
|
|
1589
1613
|
const rightKeys = Object.keys(right);
|
|
1590
1614
|
return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.hasOwn(right, key) && jsonValuesEqual(left[key], right[key]));
|
|
1591
1615
|
}
|
|
1616
|
+
function readSubmissionPath(submission, path) {
|
|
1617
|
+
if (path.length === 0 || path.split(".").some((part) => part.length === 0 || part === "__proto__" || part === "constructor")) {
|
|
1618
|
+
return void 0;
|
|
1619
|
+
}
|
|
1620
|
+
let current = submission;
|
|
1621
|
+
for (const part of path.split(".")) {
|
|
1622
|
+
if (typeof current !== "object" || current === null || !Object.hasOwn(current, part)) return void 0;
|
|
1623
|
+
current = current[part];
|
|
1624
|
+
}
|
|
1625
|
+
return current;
|
|
1626
|
+
}
|
|
1627
|
+
function compareRangeValue(value, boundary, direction) {
|
|
1628
|
+
if (typeof value === "number" && typeof boundary === "number") {
|
|
1629
|
+
return direction === "from" ? value >= boundary : value <= boundary;
|
|
1630
|
+
}
|
|
1631
|
+
if (typeof value === "string" && typeof boundary === "string") {
|
|
1632
|
+
return direction === "from" ? value >= boundary : value <= boundary;
|
|
1633
|
+
}
|
|
1634
|
+
return false;
|
|
1635
|
+
}
|
|
1636
|
+
function matchesSubmissionFilter(submission, filter) {
|
|
1637
|
+
if (filter.op === "and") return filter.filters.every((item) => matchesSubmissionFilter(submission, item));
|
|
1638
|
+
if (filter.op === "or") return filter.filters.some((item) => matchesSubmissionFilter(submission, item));
|
|
1639
|
+
const value = readSubmissionPath(submission, filter.path);
|
|
1640
|
+
if (filter.op === "eq") return jsonValuesEqual(value, filter.value);
|
|
1641
|
+
if (filter.op === "in") return filter.values.some((candidate) => jsonValuesEqual(value, candidate));
|
|
1642
|
+
if (filter.op === "exists") return filter.value ? value !== void 0 : value === void 0;
|
|
1643
|
+
return (filter.from === void 0 || compareRangeValue(value, filter.from, "from")) && (filter.to === void 0 || compareRangeValue(value, filter.to, "to"));
|
|
1644
|
+
}
|
|
1592
1645
|
function matchesSubmissionPageFilters(submission, options) {
|
|
1593
|
-
if (options.filter !== void 0 && !options.filter(submission))
|
|
1646
|
+
if (options.filter !== void 0 && !(typeof options.filter === "function" ? options.filter(submission) : matchesSubmissionFilter(submission, options.filter))) {
|
|
1647
|
+
return false;
|
|
1648
|
+
}
|
|
1594
1649
|
if (options.metadataFilters === void 0) return true;
|
|
1595
1650
|
return Object.entries(options.metadataFilters).every(
|
|
1596
1651
|
([key, value]) => jsonValuesEqual(submission.metadata?.[key], value)
|
|
@@ -2049,6 +2104,21 @@ function validateState(state) {
|
|
|
2049
2104
|
throw new TypeError("revision must be a non-negative safe integer.");
|
|
2050
2105
|
}
|
|
2051
2106
|
}
|
|
2107
|
+
function requireTimestamp(value, name) {
|
|
2108
|
+
const timestamp = value ?? "1970-01-01T00:00:00.000Z";
|
|
2109
|
+
if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError(`${name} must be a valid date string.`);
|
|
2110
|
+
return timestamp;
|
|
2111
|
+
}
|
|
2112
|
+
function transitionEvent(type, state, nextState, affectedVersions, occurredAt) {
|
|
2113
|
+
return {
|
|
2114
|
+
type,
|
|
2115
|
+
formId: state.formId,
|
|
2116
|
+
fromRevision: state.revision,
|
|
2117
|
+
toRevision: nextState.revision,
|
|
2118
|
+
affectedVersions,
|
|
2119
|
+
occurredAt
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2052
2122
|
function cloneVersionToDraft(state, sourceSchema, options = {}) {
|
|
2053
2123
|
validateState(state);
|
|
2054
2124
|
if (sourceSchema.id !== state.formId) throw new TypeError("sourceSchema.id must match state.formId.");
|
|
@@ -2092,16 +2162,61 @@ function cloneVersionToDraft(state, sourceSchema, options = {}) {
|
|
|
2092
2162
|
}
|
|
2093
2163
|
};
|
|
2094
2164
|
}
|
|
2165
|
+
function createCloneTransitionPlan(state, sourceRecord, options = {}) {
|
|
2166
|
+
if (sourceRecord.formId !== state.formId || sourceRecord.schema.id !== sourceRecord.formId || sourceRecord.schema.version !== sourceRecord.version || sourceRecord.status !== "published" && !options.allowedSourceVersions?.includes(sourceRecord.version)) {
|
|
2167
|
+
return {
|
|
2168
|
+
success: false,
|
|
2169
|
+
error: {
|
|
2170
|
+
type: "invalid_source_version",
|
|
2171
|
+
requestedVersion: sourceRecord.version,
|
|
2172
|
+
...state.publishedVersion === void 0 ? {} : { publishedVersion: state.publishedVersion }
|
|
2173
|
+
}
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
const result = cloneVersionToDraft(state, sourceRecord.schema, options);
|
|
2177
|
+
if (!result.success) return result;
|
|
2178
|
+
const timestamp = requireTimestamp(options.clonedAt, "clonedAt");
|
|
2179
|
+
const draftRecord = {
|
|
2180
|
+
formId: state.formId,
|
|
2181
|
+
version: result.value.draftSchema.version,
|
|
2182
|
+
status: "draft",
|
|
2183
|
+
schema: result.value.draftSchema,
|
|
2184
|
+
revision: 1,
|
|
2185
|
+
createdFromVersion: sourceRecord.version,
|
|
2186
|
+
createdAt: timestamp,
|
|
2187
|
+
...options.metadata === void 0 ? {} : { metadata: options.metadata }
|
|
2188
|
+
};
|
|
2189
|
+
return {
|
|
2190
|
+
success: true,
|
|
2191
|
+
value: {
|
|
2192
|
+
nextState: result.value.nextState,
|
|
2193
|
+
plan: {
|
|
2194
|
+
formId: state.formId,
|
|
2195
|
+
expectedRevision: options.expectedRevision ?? state.revision,
|
|
2196
|
+
nextRevision: result.value.nextState.revision,
|
|
2197
|
+
draftToCreate: draftRecord,
|
|
2198
|
+
events: [transitionEvent("draft.created", state, result.value.nextState, [draftRecord.version], timestamp)],
|
|
2199
|
+
nextVersion: result.value.nextState.nextVersion,
|
|
2200
|
+
timestamp
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2095
2205
|
function validatePublishedRecord(state, record) {
|
|
2096
|
-
if (
|
|
2097
|
-
|
|
2206
|
+
if (state.publishedVersion !== void 0 && record?.version !== state.publishedVersion) {
|
|
2207
|
+
return {
|
|
2208
|
+
success: false,
|
|
2209
|
+
error: { type: "missing_published_record", expectedVersion: state.publishedVersion }
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
if (record === void 0) return void 0;
|
|
2213
|
+
if (record.formId !== state.formId || record.status !== "published" || record.schema.id !== record.formId || record.schema.version !== record.version) {
|
|
2098
2214
|
throw new TypeError("currentPublishedRecord must match the state's published version.");
|
|
2099
2215
|
}
|
|
2216
|
+
return void 0;
|
|
2100
2217
|
}
|
|
2101
2218
|
function transitionTimestamp(options) {
|
|
2102
|
-
|
|
2103
|
-
if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError("publishedAt must be a valid date string.");
|
|
2104
|
-
return timestamp;
|
|
2219
|
+
return requireTimestamp(options.publishedAt ?? options.timestamp, "publishedAt");
|
|
2105
2220
|
}
|
|
2106
2221
|
async function publishDraft(state, draftSchema, options = {}) {
|
|
2107
2222
|
validateState(state);
|
|
@@ -2110,7 +2225,8 @@ async function publishDraft(state, draftSchema, options = {}) {
|
|
|
2110
2225
|
if (state.draftVersion === void 0 || draftSchema.id !== state.formId || draftSchema.version !== state.draftVersion) {
|
|
2111
2226
|
return { success: false, error: { type: "draft_not_found" } };
|
|
2112
2227
|
}
|
|
2113
|
-
validatePublishedRecord(state, options.currentPublishedRecord);
|
|
2228
|
+
const publishedRecordError = validatePublishedRecord(state, options.currentPublishedRecord);
|
|
2229
|
+
if (publishedRecordError !== void 0) return publishedRecordError;
|
|
2114
2230
|
const validation = await options.validate?.(draftSchema);
|
|
2115
2231
|
if (validation === false || Array.isArray(validation) && validation.length > 0) {
|
|
2116
2232
|
return {
|
|
@@ -2184,6 +2300,19 @@ async function createPublishTransitionPlan(state, draftRecord, options = {}) {
|
|
|
2184
2300
|
draftToDeleteVersion: draftRecord.version,
|
|
2185
2301
|
publishedRecordToSave,
|
|
2186
2302
|
archivedRecordsToSave: result.value.archivedRecords,
|
|
2303
|
+
events: [
|
|
2304
|
+
...result.value.archivedRecords.length === 0 ? [] : [
|
|
2305
|
+
transitionEvent(
|
|
2306
|
+
"version.archived",
|
|
2307
|
+
state,
|
|
2308
|
+
result.value.nextState,
|
|
2309
|
+
result.value.archivedRecords.map((record) => record.version),
|
|
2310
|
+
timestamp
|
|
2311
|
+
)
|
|
2312
|
+
],
|
|
2313
|
+
transitionEvent("version.published", state, result.value.nextState, [draftRecord.version], timestamp)
|
|
2314
|
+
],
|
|
2315
|
+
nextVersion: result.value.nextState.nextVersion,
|
|
2187
2316
|
timestamp
|
|
2188
2317
|
}
|
|
2189
2318
|
}
|
|
@@ -2200,6 +2329,29 @@ function deleteDraft(state, options = {}) {
|
|
|
2200
2329
|
value: { nextState: { ...stateWithoutDraft, revision: state.revision + 1 } }
|
|
2201
2330
|
};
|
|
2202
2331
|
}
|
|
2332
|
+
function createDeleteDraftTransitionPlan(state, draftRecord, options = {}) {
|
|
2333
|
+
if (draftRecord.formId !== state.formId || draftRecord.version !== state.draftVersion || draftRecord.status !== "draft" || draftRecord.schema.id !== state.formId || draftRecord.schema.version !== draftRecord.version) {
|
|
2334
|
+
return { success: false, error: { type: "draft_not_found" } };
|
|
2335
|
+
}
|
|
2336
|
+
const result = deleteDraft(state, options);
|
|
2337
|
+
if (!result.success) return result;
|
|
2338
|
+
const timestamp = requireTimestamp(options.deletedAt, "deletedAt");
|
|
2339
|
+
return {
|
|
2340
|
+
success: true,
|
|
2341
|
+
value: {
|
|
2342
|
+
nextState: result.value.nextState,
|
|
2343
|
+
plan: {
|
|
2344
|
+
formId: state.formId,
|
|
2345
|
+
expectedRevision: options.expectedRevision ?? state.revision,
|
|
2346
|
+
nextRevision: result.value.nextState.revision,
|
|
2347
|
+
draftToDeleteVersion: draftRecord.version,
|
|
2348
|
+
events: [transitionEvent("draft.deleted", state, result.value.nextState, [draftRecord.version], timestamp)],
|
|
2349
|
+
nextVersion: result.value.nextState.nextVersion,
|
|
2350
|
+
timestamp
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2203
2355
|
function assertVersionMutable(status) {
|
|
2204
2356
|
if (status !== "draft") {
|
|
2205
2357
|
const error = { type: "version_immutable", status };
|
|
@@ -2218,18 +2370,24 @@ function assertVersionMutable(status) {
|
|
|
2218
2370
|
calculatePageVisibility,
|
|
2219
2371
|
cloneVersionToDraft,
|
|
2220
2372
|
collectSchemaLocales,
|
|
2373
|
+
createCloneTransitionPlan,
|
|
2374
|
+
createDeleteDraftTransitionPlan,
|
|
2221
2375
|
createPublishTransitionPlan,
|
|
2222
2376
|
createResponseAccumulator,
|
|
2223
2377
|
createSubmission,
|
|
2224
2378
|
decodeSubmissionCursor,
|
|
2379
|
+
decodeTextAnswerCursor,
|
|
2225
2380
|
deleteDraft,
|
|
2226
2381
|
dispatchWebhook,
|
|
2227
2382
|
encodeSubmissionCursor,
|
|
2383
|
+
encodeTextAnswerCursor,
|
|
2228
2384
|
escapeCsvCell,
|
|
2229
2385
|
exportResponsesToCsv,
|
|
2230
2386
|
exportResponsesToCsvStream,
|
|
2231
2387
|
isDisplayConditionSatisfied,
|
|
2232
2388
|
isQuestionVisible,
|
|
2389
|
+
jsonValuesEqual,
|
|
2390
|
+
matchesSubmissionFilter,
|
|
2233
2391
|
matchesSubmissionPageFilters,
|
|
2234
2392
|
normalizeSubmissionPageSize,
|
|
2235
2393
|
pipeResponsesToCsvStream,
|
package/dist/index.d.cts
CHANGED
|
@@ -24,11 +24,22 @@ interface FormVersionState {
|
|
|
24
24
|
readonly nextVersion: number;
|
|
25
25
|
readonly revision: number;
|
|
26
26
|
}
|
|
27
|
+
interface VersionTransitionEvent {
|
|
28
|
+
readonly type: "draft.created" | "draft.deleted" | "version.published" | "version.archived";
|
|
29
|
+
readonly formId: string;
|
|
30
|
+
readonly fromRevision: number;
|
|
31
|
+
readonly toRevision: number;
|
|
32
|
+
readonly affectedVersions: readonly number[];
|
|
33
|
+
readonly occurredAt: string;
|
|
34
|
+
}
|
|
27
35
|
type VersionTransitionError = {
|
|
28
36
|
readonly type: "draft_already_exists";
|
|
29
37
|
readonly currentDraftVersion: number;
|
|
30
38
|
} | {
|
|
31
39
|
readonly type: "draft_not_found";
|
|
40
|
+
} | {
|
|
41
|
+
readonly type: "missing_published_record";
|
|
42
|
+
readonly expectedVersion: number;
|
|
32
43
|
} | {
|
|
33
44
|
readonly type: "revision_conflict";
|
|
34
45
|
readonly expectedRevision: number;
|
|
@@ -50,6 +61,8 @@ type VersionTransitionError = {
|
|
|
50
61
|
interface CloneVersionOptions {
|
|
51
62
|
readonly maxVersions?: number;
|
|
52
63
|
readonly expectedRevision?: number;
|
|
64
|
+
readonly clonedAt?: string;
|
|
65
|
+
readonly metadata?: Readonly<Record<string, JsonValue>>;
|
|
53
66
|
/** Additional known published versions that may be used as a clone source. */
|
|
54
67
|
readonly allowedSourceVersions?: readonly number[];
|
|
55
68
|
}
|
|
@@ -63,6 +76,7 @@ interface PublishDraftOptions {
|
|
|
63
76
|
}
|
|
64
77
|
interface DeleteDraftOptions {
|
|
65
78
|
readonly expectedRevision?: number;
|
|
79
|
+
readonly deletedAt?: string;
|
|
66
80
|
}
|
|
67
81
|
interface PublishDraftResult {
|
|
68
82
|
readonly nextState: FormVersionState;
|
|
@@ -75,6 +89,10 @@ declare function cloneVersionToDraft(state: FormVersionState, sourceSchema: Form
|
|
|
75
89
|
readonly nextState: FormVersionState;
|
|
76
90
|
readonly draftSchema: FormSchema;
|
|
77
91
|
}, VersionTransitionError>;
|
|
92
|
+
declare function createCloneTransitionPlan(state: FormVersionState, sourceRecord: FormVersionRecord, options?: CloneVersionOptions): Result<{
|
|
93
|
+
readonly nextState: FormVersionState;
|
|
94
|
+
readonly plan: VersionTransitionPlan;
|
|
95
|
+
}, VersionTransitionError>;
|
|
78
96
|
declare function publishDraft(state: FormVersionState, draftSchema: FormSchema, options?: PublishDraftOptions): Promise<Result<PublishDraftResult, VersionTransitionError>>;
|
|
79
97
|
declare function createPublishTransitionPlan(state: FormVersionState, draftRecord: FormVersionRecord, options?: PublishDraftOptions): Promise<Result<{
|
|
80
98
|
readonly nextState: FormVersionState;
|
|
@@ -83,6 +101,10 @@ declare function createPublishTransitionPlan(state: FormVersionState, draftRecor
|
|
|
83
101
|
declare function deleteDraft(state: FormVersionState, options?: DeleteDraftOptions): Result<{
|
|
84
102
|
readonly nextState: FormVersionState;
|
|
85
103
|
}, VersionTransitionError>;
|
|
104
|
+
declare function createDeleteDraftTransitionPlan(state: FormVersionState, draftRecord: FormVersionRecord, options?: DeleteDraftOptions): Result<{
|
|
105
|
+
readonly nextState: FormVersionState;
|
|
106
|
+
readonly plan: VersionTransitionPlan;
|
|
107
|
+
}, VersionTransitionError>;
|
|
86
108
|
declare function assertVersionMutable(status: FormVersionStatus): void;
|
|
87
109
|
|
|
88
110
|
type FieldType = "text" | "textarea" | "number" | "rating" | "select" | "multi-select" | "checkbox" | "radio";
|
|
@@ -257,9 +279,37 @@ interface SubmissionPageQueryOptions {
|
|
|
257
279
|
readonly since?: string;
|
|
258
280
|
readonly until?: string;
|
|
259
281
|
readonly locale?: string;
|
|
260
|
-
readonly filter?: (submission: FormSubmission) => boolean;
|
|
282
|
+
readonly filter?: SubmissionFilter | ((submission: FormSubmission) => boolean);
|
|
283
|
+
/** @deprecated Prefer the generic filter AST. */
|
|
261
284
|
readonly metadataFilters?: Readonly<Record<string, JsonValue>>;
|
|
262
285
|
}
|
|
286
|
+
interface TextAnswerPageQueryOptions extends SubmissionPageQueryOptions {
|
|
287
|
+
readonly fieldIds?: readonly string[];
|
|
288
|
+
}
|
|
289
|
+
type SubmissionFilter = {
|
|
290
|
+
readonly op: "eq";
|
|
291
|
+
readonly path: string;
|
|
292
|
+
readonly value: JsonValue;
|
|
293
|
+
} | {
|
|
294
|
+
readonly op: "in";
|
|
295
|
+
readonly path: string;
|
|
296
|
+
readonly values: readonly JsonValue[];
|
|
297
|
+
} | {
|
|
298
|
+
readonly op: "range";
|
|
299
|
+
readonly path: string;
|
|
300
|
+
readonly from?: JsonValue;
|
|
301
|
+
readonly to?: JsonValue;
|
|
302
|
+
} | {
|
|
303
|
+
readonly op: "exists";
|
|
304
|
+
readonly path: string;
|
|
305
|
+
readonly value: boolean;
|
|
306
|
+
} | {
|
|
307
|
+
readonly op: "and";
|
|
308
|
+
readonly filters: readonly SubmissionFilter[];
|
|
309
|
+
} | {
|
|
310
|
+
readonly op: "or";
|
|
311
|
+
readonly filters: readonly SubmissionFilter[];
|
|
312
|
+
};
|
|
263
313
|
interface SubmissionPage {
|
|
264
314
|
readonly items: readonly FormSubmission[];
|
|
265
315
|
readonly nextCursor?: string;
|
|
@@ -267,6 +317,22 @@ interface SubmissionPage {
|
|
|
267
317
|
}
|
|
268
318
|
interface PagedSubmissionStorageAdapter extends FormStorageAdapter {
|
|
269
319
|
listSubmissionPage(formId: string, options?: SubmissionPageQueryOptions): Promise<SubmissionPage>;
|
|
320
|
+
listTextAnswerPage?(formId: string, fieldIdOrOptions?: string | TextAnswerPageQueryOptions, options?: TextAnswerPageQueryOptions): Promise<TextAnswerPage>;
|
|
321
|
+
}
|
|
322
|
+
interface TextAnswerItem {
|
|
323
|
+
readonly responseId: string;
|
|
324
|
+
readonly formId: string;
|
|
325
|
+
readonly formVersion: number;
|
|
326
|
+
readonly fieldId: string;
|
|
327
|
+
readonly text: string;
|
|
328
|
+
readonly locale?: string;
|
|
329
|
+
readonly submittedAt: string;
|
|
330
|
+
readonly metadata?: Readonly<Record<string, JsonValue>>;
|
|
331
|
+
}
|
|
332
|
+
interface TextAnswerPage {
|
|
333
|
+
readonly items: readonly TextAnswerItem[];
|
|
334
|
+
readonly nextCursor?: string;
|
|
335
|
+
readonly hasMore: boolean;
|
|
270
336
|
}
|
|
271
337
|
interface VersionTransitionPlan {
|
|
272
338
|
readonly formId: string;
|
|
@@ -276,13 +342,32 @@ interface VersionTransitionPlan {
|
|
|
276
342
|
readonly draftToDeleteVersion?: number;
|
|
277
343
|
readonly publishedRecordToSave?: FormVersionRecord;
|
|
278
344
|
readonly archivedRecordsToSave?: readonly FormVersionRecord[];
|
|
345
|
+
readonly events: readonly VersionTransitionEvent[];
|
|
346
|
+
/** The complete next state value used by persistent adapters. */
|
|
347
|
+
readonly nextVersion?: number;
|
|
279
348
|
readonly timestamp: string;
|
|
280
349
|
}
|
|
350
|
+
type StorageCommitError = {
|
|
351
|
+
readonly type: "revision_conflict";
|
|
352
|
+
readonly expectedRevision: number;
|
|
353
|
+
readonly actualRevision?: number;
|
|
354
|
+
} | {
|
|
355
|
+
readonly type: "draft_already_exists";
|
|
356
|
+
readonly currentDraftVersion: number;
|
|
357
|
+
} | {
|
|
358
|
+
readonly type: "invalid_transition";
|
|
359
|
+
readonly message: string;
|
|
360
|
+
} | {
|
|
361
|
+
readonly type: "storage_error";
|
|
362
|
+
readonly cause: unknown;
|
|
363
|
+
};
|
|
281
364
|
interface VersionedFormStorageAdapter extends FormStorageAdapter {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
365
|
+
getVersionState(formId: string): Promise<FormVersionState | null>;
|
|
366
|
+
getVersionRecord(formId: string, version: number): Promise<FormVersionRecord | null>;
|
|
367
|
+
listVersionRecords(formId: string): Promise<readonly FormVersionRecord[]>;
|
|
368
|
+
commitVersionTransition(plan: VersionTransitionPlan): Promise<Result<{
|
|
369
|
+
readonly success: true;
|
|
370
|
+
}, StorageCommitError>>;
|
|
286
371
|
}
|
|
287
372
|
interface BaseQuestionAggregate {
|
|
288
373
|
readonly fieldId: string;
|
|
@@ -442,9 +527,17 @@ interface SubmissionCursorValue {
|
|
|
442
527
|
readonly submittedAt: string;
|
|
443
528
|
readonly responseId: string;
|
|
444
529
|
}
|
|
530
|
+
interface TextAnswerCursorValue {
|
|
531
|
+
readonly responseId: string;
|
|
532
|
+
readonly fieldId: string;
|
|
533
|
+
}
|
|
445
534
|
declare function encodeSubmissionCursor(value: SubmissionCursorValue): string;
|
|
446
535
|
declare function decodeSubmissionCursor(cursor: string): SubmissionCursorValue;
|
|
536
|
+
declare function encodeTextAnswerCursor(value: TextAnswerCursorValue): string;
|
|
537
|
+
declare function decodeTextAnswerCursor(cursor: string): TextAnswerCursorValue;
|
|
447
538
|
declare function normalizeSubmissionPageSize(pageSize: number | undefined, fallback?: number): number;
|
|
539
|
+
declare function jsonValuesEqual(left: JsonValue | undefined, right: JsonValue | undefined): boolean;
|
|
540
|
+
declare function matchesSubmissionFilter(submission: FormSubmission, filter: SubmissionFilter): boolean;
|
|
448
541
|
declare function matchesSubmissionPageFilters(submission: FormSubmission, options: Pick<SubmissionPageQueryOptions, "filter" | "metadataFilters">): boolean;
|
|
449
542
|
|
|
450
543
|
interface CollectedLocales {
|
|
@@ -520,4 +613,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
|
|
|
520
613
|
declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
|
|
521
614
|
declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
|
|
522
615
|
|
|
523
|
-
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, createPublishTransitionPlan, 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 };
|
|
616
|
+
export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, type DeleteDraftOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LocalizedText, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PopulateTranslationOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StorageCommitError, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionFilter, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextAnswerCursorValue, type TextAnswerItem, type TextAnswerPage, type TextAnswerPageQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionEvent, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, createCloneTransitionPlan, createDeleteDraftTransitionPlan, createPublishTransitionPlan, createResponseAccumulator, createSubmission, decodeSubmissionCursor, decodeTextAnswerCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, encodeTextAnswerCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, isDisplayConditionSatisfied, isQuestionVisible, jsonValuesEqual, matchesSubmissionFilter, matchesSubmissionPageFilters, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
package/dist/index.d.ts
CHANGED
|
@@ -24,11 +24,22 @@ interface FormVersionState {
|
|
|
24
24
|
readonly nextVersion: number;
|
|
25
25
|
readonly revision: number;
|
|
26
26
|
}
|
|
27
|
+
interface VersionTransitionEvent {
|
|
28
|
+
readonly type: "draft.created" | "draft.deleted" | "version.published" | "version.archived";
|
|
29
|
+
readonly formId: string;
|
|
30
|
+
readonly fromRevision: number;
|
|
31
|
+
readonly toRevision: number;
|
|
32
|
+
readonly affectedVersions: readonly number[];
|
|
33
|
+
readonly occurredAt: string;
|
|
34
|
+
}
|
|
27
35
|
type VersionTransitionError = {
|
|
28
36
|
readonly type: "draft_already_exists";
|
|
29
37
|
readonly currentDraftVersion: number;
|
|
30
38
|
} | {
|
|
31
39
|
readonly type: "draft_not_found";
|
|
40
|
+
} | {
|
|
41
|
+
readonly type: "missing_published_record";
|
|
42
|
+
readonly expectedVersion: number;
|
|
32
43
|
} | {
|
|
33
44
|
readonly type: "revision_conflict";
|
|
34
45
|
readonly expectedRevision: number;
|
|
@@ -50,6 +61,8 @@ type VersionTransitionError = {
|
|
|
50
61
|
interface CloneVersionOptions {
|
|
51
62
|
readonly maxVersions?: number;
|
|
52
63
|
readonly expectedRevision?: number;
|
|
64
|
+
readonly clonedAt?: string;
|
|
65
|
+
readonly metadata?: Readonly<Record<string, JsonValue>>;
|
|
53
66
|
/** Additional known published versions that may be used as a clone source. */
|
|
54
67
|
readonly allowedSourceVersions?: readonly number[];
|
|
55
68
|
}
|
|
@@ -63,6 +76,7 @@ interface PublishDraftOptions {
|
|
|
63
76
|
}
|
|
64
77
|
interface DeleteDraftOptions {
|
|
65
78
|
readonly expectedRevision?: number;
|
|
79
|
+
readonly deletedAt?: string;
|
|
66
80
|
}
|
|
67
81
|
interface PublishDraftResult {
|
|
68
82
|
readonly nextState: FormVersionState;
|
|
@@ -75,6 +89,10 @@ declare function cloneVersionToDraft(state: FormVersionState, sourceSchema: Form
|
|
|
75
89
|
readonly nextState: FormVersionState;
|
|
76
90
|
readonly draftSchema: FormSchema;
|
|
77
91
|
}, VersionTransitionError>;
|
|
92
|
+
declare function createCloneTransitionPlan(state: FormVersionState, sourceRecord: FormVersionRecord, options?: CloneVersionOptions): Result<{
|
|
93
|
+
readonly nextState: FormVersionState;
|
|
94
|
+
readonly plan: VersionTransitionPlan;
|
|
95
|
+
}, VersionTransitionError>;
|
|
78
96
|
declare function publishDraft(state: FormVersionState, draftSchema: FormSchema, options?: PublishDraftOptions): Promise<Result<PublishDraftResult, VersionTransitionError>>;
|
|
79
97
|
declare function createPublishTransitionPlan(state: FormVersionState, draftRecord: FormVersionRecord, options?: PublishDraftOptions): Promise<Result<{
|
|
80
98
|
readonly nextState: FormVersionState;
|
|
@@ -83,6 +101,10 @@ declare function createPublishTransitionPlan(state: FormVersionState, draftRecor
|
|
|
83
101
|
declare function deleteDraft(state: FormVersionState, options?: DeleteDraftOptions): Result<{
|
|
84
102
|
readonly nextState: FormVersionState;
|
|
85
103
|
}, VersionTransitionError>;
|
|
104
|
+
declare function createDeleteDraftTransitionPlan(state: FormVersionState, draftRecord: FormVersionRecord, options?: DeleteDraftOptions): Result<{
|
|
105
|
+
readonly nextState: FormVersionState;
|
|
106
|
+
readonly plan: VersionTransitionPlan;
|
|
107
|
+
}, VersionTransitionError>;
|
|
86
108
|
declare function assertVersionMutable(status: FormVersionStatus): void;
|
|
87
109
|
|
|
88
110
|
type FieldType = "text" | "textarea" | "number" | "rating" | "select" | "multi-select" | "checkbox" | "radio";
|
|
@@ -257,9 +279,37 @@ interface SubmissionPageQueryOptions {
|
|
|
257
279
|
readonly since?: string;
|
|
258
280
|
readonly until?: string;
|
|
259
281
|
readonly locale?: string;
|
|
260
|
-
readonly filter?: (submission: FormSubmission) => boolean;
|
|
282
|
+
readonly filter?: SubmissionFilter | ((submission: FormSubmission) => boolean);
|
|
283
|
+
/** @deprecated Prefer the generic filter AST. */
|
|
261
284
|
readonly metadataFilters?: Readonly<Record<string, JsonValue>>;
|
|
262
285
|
}
|
|
286
|
+
interface TextAnswerPageQueryOptions extends SubmissionPageQueryOptions {
|
|
287
|
+
readonly fieldIds?: readonly string[];
|
|
288
|
+
}
|
|
289
|
+
type SubmissionFilter = {
|
|
290
|
+
readonly op: "eq";
|
|
291
|
+
readonly path: string;
|
|
292
|
+
readonly value: JsonValue;
|
|
293
|
+
} | {
|
|
294
|
+
readonly op: "in";
|
|
295
|
+
readonly path: string;
|
|
296
|
+
readonly values: readonly JsonValue[];
|
|
297
|
+
} | {
|
|
298
|
+
readonly op: "range";
|
|
299
|
+
readonly path: string;
|
|
300
|
+
readonly from?: JsonValue;
|
|
301
|
+
readonly to?: JsonValue;
|
|
302
|
+
} | {
|
|
303
|
+
readonly op: "exists";
|
|
304
|
+
readonly path: string;
|
|
305
|
+
readonly value: boolean;
|
|
306
|
+
} | {
|
|
307
|
+
readonly op: "and";
|
|
308
|
+
readonly filters: readonly SubmissionFilter[];
|
|
309
|
+
} | {
|
|
310
|
+
readonly op: "or";
|
|
311
|
+
readonly filters: readonly SubmissionFilter[];
|
|
312
|
+
};
|
|
263
313
|
interface SubmissionPage {
|
|
264
314
|
readonly items: readonly FormSubmission[];
|
|
265
315
|
readonly nextCursor?: string;
|
|
@@ -267,6 +317,22 @@ interface SubmissionPage {
|
|
|
267
317
|
}
|
|
268
318
|
interface PagedSubmissionStorageAdapter extends FormStorageAdapter {
|
|
269
319
|
listSubmissionPage(formId: string, options?: SubmissionPageQueryOptions): Promise<SubmissionPage>;
|
|
320
|
+
listTextAnswerPage?(formId: string, fieldIdOrOptions?: string | TextAnswerPageQueryOptions, options?: TextAnswerPageQueryOptions): Promise<TextAnswerPage>;
|
|
321
|
+
}
|
|
322
|
+
interface TextAnswerItem {
|
|
323
|
+
readonly responseId: string;
|
|
324
|
+
readonly formId: string;
|
|
325
|
+
readonly formVersion: number;
|
|
326
|
+
readonly fieldId: string;
|
|
327
|
+
readonly text: string;
|
|
328
|
+
readonly locale?: string;
|
|
329
|
+
readonly submittedAt: string;
|
|
330
|
+
readonly metadata?: Readonly<Record<string, JsonValue>>;
|
|
331
|
+
}
|
|
332
|
+
interface TextAnswerPage {
|
|
333
|
+
readonly items: readonly TextAnswerItem[];
|
|
334
|
+
readonly nextCursor?: string;
|
|
335
|
+
readonly hasMore: boolean;
|
|
270
336
|
}
|
|
271
337
|
interface VersionTransitionPlan {
|
|
272
338
|
readonly formId: string;
|
|
@@ -276,13 +342,32 @@ interface VersionTransitionPlan {
|
|
|
276
342
|
readonly draftToDeleteVersion?: number;
|
|
277
343
|
readonly publishedRecordToSave?: FormVersionRecord;
|
|
278
344
|
readonly archivedRecordsToSave?: readonly FormVersionRecord[];
|
|
345
|
+
readonly events: readonly VersionTransitionEvent[];
|
|
346
|
+
/** The complete next state value used by persistent adapters. */
|
|
347
|
+
readonly nextVersion?: number;
|
|
279
348
|
readonly timestamp: string;
|
|
280
349
|
}
|
|
350
|
+
type StorageCommitError = {
|
|
351
|
+
readonly type: "revision_conflict";
|
|
352
|
+
readonly expectedRevision: number;
|
|
353
|
+
readonly actualRevision?: number;
|
|
354
|
+
} | {
|
|
355
|
+
readonly type: "draft_already_exists";
|
|
356
|
+
readonly currentDraftVersion: number;
|
|
357
|
+
} | {
|
|
358
|
+
readonly type: "invalid_transition";
|
|
359
|
+
readonly message: string;
|
|
360
|
+
} | {
|
|
361
|
+
readonly type: "storage_error";
|
|
362
|
+
readonly cause: unknown;
|
|
363
|
+
};
|
|
281
364
|
interface VersionedFormStorageAdapter extends FormStorageAdapter {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
365
|
+
getVersionState(formId: string): Promise<FormVersionState | null>;
|
|
366
|
+
getVersionRecord(formId: string, version: number): Promise<FormVersionRecord | null>;
|
|
367
|
+
listVersionRecords(formId: string): Promise<readonly FormVersionRecord[]>;
|
|
368
|
+
commitVersionTransition(plan: VersionTransitionPlan): Promise<Result<{
|
|
369
|
+
readonly success: true;
|
|
370
|
+
}, StorageCommitError>>;
|
|
286
371
|
}
|
|
287
372
|
interface BaseQuestionAggregate {
|
|
288
373
|
readonly fieldId: string;
|
|
@@ -442,9 +527,17 @@ interface SubmissionCursorValue {
|
|
|
442
527
|
readonly submittedAt: string;
|
|
443
528
|
readonly responseId: string;
|
|
444
529
|
}
|
|
530
|
+
interface TextAnswerCursorValue {
|
|
531
|
+
readonly responseId: string;
|
|
532
|
+
readonly fieldId: string;
|
|
533
|
+
}
|
|
445
534
|
declare function encodeSubmissionCursor(value: SubmissionCursorValue): string;
|
|
446
535
|
declare function decodeSubmissionCursor(cursor: string): SubmissionCursorValue;
|
|
536
|
+
declare function encodeTextAnswerCursor(value: TextAnswerCursorValue): string;
|
|
537
|
+
declare function decodeTextAnswerCursor(cursor: string): TextAnswerCursorValue;
|
|
447
538
|
declare function normalizeSubmissionPageSize(pageSize: number | undefined, fallback?: number): number;
|
|
539
|
+
declare function jsonValuesEqual(left: JsonValue | undefined, right: JsonValue | undefined): boolean;
|
|
540
|
+
declare function matchesSubmissionFilter(submission: FormSubmission, filter: SubmissionFilter): boolean;
|
|
448
541
|
declare function matchesSubmissionPageFilters(submission: FormSubmission, options: Pick<SubmissionPageQueryOptions, "filter" | "metadataFilters">): boolean;
|
|
449
542
|
|
|
450
543
|
interface CollectedLocales {
|
|
@@ -520,4 +613,4 @@ declare function calculatePageVisibility(schema: FormSchema, currentAnswers: Rea
|
|
|
520
613
|
declare function calculateFieldVisibility(schema: FormSchema, currentAnswers: Readonly<Record<string, unknown>>): Readonly<Record<string, boolean>>;
|
|
521
614
|
declare function selectVisibleAnswers(schema: FormSchema, currentAnswers: FormValues): FormValues;
|
|
522
615
|
|
|
523
|
-
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, createPublishTransitionPlan, 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 };
|
|
616
|
+
export { type AccumulatorReport, type AccumulatorResponse, type AccumulatorSkipReason, type AnswerValidationResult, type AsyncTranslationAdapter, type BaseField, type CheckboxField, type CheckboxQuestionAggregate, type ChoiceDistributionEntry, type ChoiceOption, type ChoiceQuestionAggregate, type CloneVersionOptions, type CollectedLocales, type ConditionOperator, type ConditionValue, type CreateSubmissionOptions, type CrossTabulationResult, type CsvColumnContext, type CsvColumnDef, type CsvExportOptions, type DeleteDraftOptions, type DisplayCondition, type ExtensibleNode, type FieldOption, type FieldType, type FormAnalytics, type FormEvent, type FormEventType, type FormField, type FormPage, type FormPolicy, type FormResponse, type FormSchema, type FormStorageAdapter, type FormSubmission, type FormValue, type FormValues, type FormVersionRecord, type FormVersionState, type FormVersionStatus, type JsonValue, type LocalizedText, type MultiSelectField, type NodeWritableStream, type NumberField, type NumberQuestionAggregate, type NumericSummary, type OptionAggregate, type PagedSubmissionStorageAdapter, type PopulateTranslationOptions, type PublishDraftOptions, type PublishDraftResult, type Question, type QuestionAggregate, type QuestionType, type RatingField, type ResponseAccumulator, type ResponseAccumulatorOptions, type Result, type SchemaIssue, type SchemaStructureIssue, type SchemaStructureIssueType, type SchemaTranslations, type SchemaValidationResult, type SelectField, type StorageAdapter, type StorageCommitError, type StreamCsvOptions, type SubmissionCursorValue, type SubmissionFilter, type SubmissionPage, type SubmissionPageQueryOptions, type SubmissionQueryOptions, type TextAnswerCursorValue, type TextAnswerItem, type TextAnswerPage, type TextAnswerPageQueryOptions, type TextField, type TextQuestionAggregate, type TranslationAdapter, type TranslationReport, type TranslationSlot, type ValidateFormSchemaOptions, type ValidationCode, type ValidationError, type ValidationIssue, type VersionTransitionError, type VersionTransitionEvent, type VersionTransitionPlan, type VersionedFormStorageAdapter, type WebhookConfig, type WebhookDispatchResult, aggregateResponses, assertValidFormSchema, assertVersionMutable, calculateChoiceDistribution, calculateCrossTabulation, calculateFieldVisibility, calculateNumericSummary, calculatePageVisibility, cloneVersionToDraft, collectSchemaLocales, createCloneTransitionPlan, createDeleteDraftTransitionPlan, createPublishTransitionPlan, createResponseAccumulator, createSubmission, decodeSubmissionCursor, decodeTextAnswerCursor, deleteDraft, dispatchWebhook, encodeSubmissionCursor, encodeTextAnswerCursor, escapeCsvCell, exportResponsesToCsv, exportResponsesToCsvStream, isDisplayConditionSatisfied, isQuestionVisible, jsonValuesEqual, matchesSubmissionFilter, matchesSubmissionPageFilters, normalizeSubmissionPageSize, pipeResponsesToCsvStream, populateSchemaTranslations, publishDraft, resolveFormTranslation, resolveLocalizedSchema, sanitizeSchema, selectVisibleAnswers, transformFieldType, validateAnswers, validateFormSchema, validatePageAnswers, validateSchemaStructure };
|
package/dist/index.js
CHANGED
|
@@ -1504,6 +1504,24 @@ function decodeSubmissionCursor(cursor) {
|
|
|
1504
1504
|
throw new TypeError("cursor must be a valid form-engine cursor.", { cause });
|
|
1505
1505
|
}
|
|
1506
1506
|
}
|
|
1507
|
+
function encodeTextAnswerCursor(value) {
|
|
1508
|
+
if (value.responseId.length === 0 || value.fieldId.length === 0) {
|
|
1509
|
+
throw new TypeError("Text answer cursor values must not be empty.");
|
|
1510
|
+
}
|
|
1511
|
+
return encodeBase64(new TextEncoder().encode(JSON.stringify(value)));
|
|
1512
|
+
}
|
|
1513
|
+
function decodeTextAnswerCursor(cursor) {
|
|
1514
|
+
try {
|
|
1515
|
+
const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(decodeBase64(cursor)));
|
|
1516
|
+
if (typeof parsed !== "object" || parsed === null || !("responseId" in parsed) || typeof parsed.responseId !== "string" || parsed.responseId.length === 0 || !("fieldId" in parsed) || typeof parsed.fieldId !== "string" || parsed.fieldId.length === 0) {
|
|
1517
|
+
throw new TypeError("text answer cursor payload is invalid.");
|
|
1518
|
+
}
|
|
1519
|
+
return { responseId: parsed.responseId, fieldId: parsed.fieldId };
|
|
1520
|
+
} catch (cause) {
|
|
1521
|
+
if (cause instanceof TypeError && cause.message === "text answer cursor payload is invalid.") throw cause;
|
|
1522
|
+
throw new TypeError("cursor must be a valid text answer cursor.", { cause });
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1507
1525
|
function normalizeSubmissionPageSize(pageSize, fallback = 100) {
|
|
1508
1526
|
const value = pageSize ?? fallback;
|
|
1509
1527
|
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("pageSize must be a positive safe integer.");
|
|
@@ -1528,8 +1546,39 @@ function jsonValuesEqual(left, right) {
|
|
|
1528
1546
|
const rightKeys = Object.keys(right);
|
|
1529
1547
|
return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.hasOwn(right, key) && jsonValuesEqual(left[key], right[key]));
|
|
1530
1548
|
}
|
|
1549
|
+
function readSubmissionPath(submission, path) {
|
|
1550
|
+
if (path.length === 0 || path.split(".").some((part) => part.length === 0 || part === "__proto__" || part === "constructor")) {
|
|
1551
|
+
return void 0;
|
|
1552
|
+
}
|
|
1553
|
+
let current = submission;
|
|
1554
|
+
for (const part of path.split(".")) {
|
|
1555
|
+
if (typeof current !== "object" || current === null || !Object.hasOwn(current, part)) return void 0;
|
|
1556
|
+
current = current[part];
|
|
1557
|
+
}
|
|
1558
|
+
return current;
|
|
1559
|
+
}
|
|
1560
|
+
function compareRangeValue(value, boundary, direction) {
|
|
1561
|
+
if (typeof value === "number" && typeof boundary === "number") {
|
|
1562
|
+
return direction === "from" ? value >= boundary : value <= boundary;
|
|
1563
|
+
}
|
|
1564
|
+
if (typeof value === "string" && typeof boundary === "string") {
|
|
1565
|
+
return direction === "from" ? value >= boundary : value <= boundary;
|
|
1566
|
+
}
|
|
1567
|
+
return false;
|
|
1568
|
+
}
|
|
1569
|
+
function matchesSubmissionFilter(submission, filter) {
|
|
1570
|
+
if (filter.op === "and") return filter.filters.every((item) => matchesSubmissionFilter(submission, item));
|
|
1571
|
+
if (filter.op === "or") return filter.filters.some((item) => matchesSubmissionFilter(submission, item));
|
|
1572
|
+
const value = readSubmissionPath(submission, filter.path);
|
|
1573
|
+
if (filter.op === "eq") return jsonValuesEqual(value, filter.value);
|
|
1574
|
+
if (filter.op === "in") return filter.values.some((candidate) => jsonValuesEqual(value, candidate));
|
|
1575
|
+
if (filter.op === "exists") return filter.value ? value !== void 0 : value === void 0;
|
|
1576
|
+
return (filter.from === void 0 || compareRangeValue(value, filter.from, "from")) && (filter.to === void 0 || compareRangeValue(value, filter.to, "to"));
|
|
1577
|
+
}
|
|
1531
1578
|
function matchesSubmissionPageFilters(submission, options) {
|
|
1532
|
-
if (options.filter !== void 0 && !options.filter(submission))
|
|
1579
|
+
if (options.filter !== void 0 && !(typeof options.filter === "function" ? options.filter(submission) : matchesSubmissionFilter(submission, options.filter))) {
|
|
1580
|
+
return false;
|
|
1581
|
+
}
|
|
1533
1582
|
if (options.metadataFilters === void 0) return true;
|
|
1534
1583
|
return Object.entries(options.metadataFilters).every(
|
|
1535
1584
|
([key, value]) => jsonValuesEqual(submission.metadata?.[key], value)
|
|
@@ -1988,6 +2037,21 @@ function validateState(state) {
|
|
|
1988
2037
|
throw new TypeError("revision must be a non-negative safe integer.");
|
|
1989
2038
|
}
|
|
1990
2039
|
}
|
|
2040
|
+
function requireTimestamp(value, name) {
|
|
2041
|
+
const timestamp = value ?? "1970-01-01T00:00:00.000Z";
|
|
2042
|
+
if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError(`${name} must be a valid date string.`);
|
|
2043
|
+
return timestamp;
|
|
2044
|
+
}
|
|
2045
|
+
function transitionEvent(type, state, nextState, affectedVersions, occurredAt) {
|
|
2046
|
+
return {
|
|
2047
|
+
type,
|
|
2048
|
+
formId: state.formId,
|
|
2049
|
+
fromRevision: state.revision,
|
|
2050
|
+
toRevision: nextState.revision,
|
|
2051
|
+
affectedVersions,
|
|
2052
|
+
occurredAt
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
1991
2055
|
function cloneVersionToDraft(state, sourceSchema, options = {}) {
|
|
1992
2056
|
validateState(state);
|
|
1993
2057
|
if (sourceSchema.id !== state.formId) throw new TypeError("sourceSchema.id must match state.formId.");
|
|
@@ -2031,16 +2095,61 @@ function cloneVersionToDraft(state, sourceSchema, options = {}) {
|
|
|
2031
2095
|
}
|
|
2032
2096
|
};
|
|
2033
2097
|
}
|
|
2098
|
+
function createCloneTransitionPlan(state, sourceRecord, options = {}) {
|
|
2099
|
+
if (sourceRecord.formId !== state.formId || sourceRecord.schema.id !== sourceRecord.formId || sourceRecord.schema.version !== sourceRecord.version || sourceRecord.status !== "published" && !options.allowedSourceVersions?.includes(sourceRecord.version)) {
|
|
2100
|
+
return {
|
|
2101
|
+
success: false,
|
|
2102
|
+
error: {
|
|
2103
|
+
type: "invalid_source_version",
|
|
2104
|
+
requestedVersion: sourceRecord.version,
|
|
2105
|
+
...state.publishedVersion === void 0 ? {} : { publishedVersion: state.publishedVersion }
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
const result = cloneVersionToDraft(state, sourceRecord.schema, options);
|
|
2110
|
+
if (!result.success) return result;
|
|
2111
|
+
const timestamp = requireTimestamp(options.clonedAt, "clonedAt");
|
|
2112
|
+
const draftRecord = {
|
|
2113
|
+
formId: state.formId,
|
|
2114
|
+
version: result.value.draftSchema.version,
|
|
2115
|
+
status: "draft",
|
|
2116
|
+
schema: result.value.draftSchema,
|
|
2117
|
+
revision: 1,
|
|
2118
|
+
createdFromVersion: sourceRecord.version,
|
|
2119
|
+
createdAt: timestamp,
|
|
2120
|
+
...options.metadata === void 0 ? {} : { metadata: options.metadata }
|
|
2121
|
+
};
|
|
2122
|
+
return {
|
|
2123
|
+
success: true,
|
|
2124
|
+
value: {
|
|
2125
|
+
nextState: result.value.nextState,
|
|
2126
|
+
plan: {
|
|
2127
|
+
formId: state.formId,
|
|
2128
|
+
expectedRevision: options.expectedRevision ?? state.revision,
|
|
2129
|
+
nextRevision: result.value.nextState.revision,
|
|
2130
|
+
draftToCreate: draftRecord,
|
|
2131
|
+
events: [transitionEvent("draft.created", state, result.value.nextState, [draftRecord.version], timestamp)],
|
|
2132
|
+
nextVersion: result.value.nextState.nextVersion,
|
|
2133
|
+
timestamp
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2034
2138
|
function validatePublishedRecord(state, record) {
|
|
2035
|
-
if (
|
|
2036
|
-
|
|
2139
|
+
if (state.publishedVersion !== void 0 && record?.version !== state.publishedVersion) {
|
|
2140
|
+
return {
|
|
2141
|
+
success: false,
|
|
2142
|
+
error: { type: "missing_published_record", expectedVersion: state.publishedVersion }
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
if (record === void 0) return void 0;
|
|
2146
|
+
if (record.formId !== state.formId || record.status !== "published" || record.schema.id !== record.formId || record.schema.version !== record.version) {
|
|
2037
2147
|
throw new TypeError("currentPublishedRecord must match the state's published version.");
|
|
2038
2148
|
}
|
|
2149
|
+
return void 0;
|
|
2039
2150
|
}
|
|
2040
2151
|
function transitionTimestamp(options) {
|
|
2041
|
-
|
|
2042
|
-
if (!Number.isFinite(Date.parse(timestamp))) throw new TypeError("publishedAt must be a valid date string.");
|
|
2043
|
-
return timestamp;
|
|
2152
|
+
return requireTimestamp(options.publishedAt ?? options.timestamp, "publishedAt");
|
|
2044
2153
|
}
|
|
2045
2154
|
async function publishDraft(state, draftSchema, options = {}) {
|
|
2046
2155
|
validateState(state);
|
|
@@ -2049,7 +2158,8 @@ async function publishDraft(state, draftSchema, options = {}) {
|
|
|
2049
2158
|
if (state.draftVersion === void 0 || draftSchema.id !== state.formId || draftSchema.version !== state.draftVersion) {
|
|
2050
2159
|
return { success: false, error: { type: "draft_not_found" } };
|
|
2051
2160
|
}
|
|
2052
|
-
validatePublishedRecord(state, options.currentPublishedRecord);
|
|
2161
|
+
const publishedRecordError = validatePublishedRecord(state, options.currentPublishedRecord);
|
|
2162
|
+
if (publishedRecordError !== void 0) return publishedRecordError;
|
|
2053
2163
|
const validation = await options.validate?.(draftSchema);
|
|
2054
2164
|
if (validation === false || Array.isArray(validation) && validation.length > 0) {
|
|
2055
2165
|
return {
|
|
@@ -2123,6 +2233,19 @@ async function createPublishTransitionPlan(state, draftRecord, options = {}) {
|
|
|
2123
2233
|
draftToDeleteVersion: draftRecord.version,
|
|
2124
2234
|
publishedRecordToSave,
|
|
2125
2235
|
archivedRecordsToSave: result.value.archivedRecords,
|
|
2236
|
+
events: [
|
|
2237
|
+
...result.value.archivedRecords.length === 0 ? [] : [
|
|
2238
|
+
transitionEvent(
|
|
2239
|
+
"version.archived",
|
|
2240
|
+
state,
|
|
2241
|
+
result.value.nextState,
|
|
2242
|
+
result.value.archivedRecords.map((record) => record.version),
|
|
2243
|
+
timestamp
|
|
2244
|
+
)
|
|
2245
|
+
],
|
|
2246
|
+
transitionEvent("version.published", state, result.value.nextState, [draftRecord.version], timestamp)
|
|
2247
|
+
],
|
|
2248
|
+
nextVersion: result.value.nextState.nextVersion,
|
|
2126
2249
|
timestamp
|
|
2127
2250
|
}
|
|
2128
2251
|
}
|
|
@@ -2139,6 +2262,29 @@ function deleteDraft(state, options = {}) {
|
|
|
2139
2262
|
value: { nextState: { ...stateWithoutDraft, revision: state.revision + 1 } }
|
|
2140
2263
|
};
|
|
2141
2264
|
}
|
|
2265
|
+
function createDeleteDraftTransitionPlan(state, draftRecord, options = {}) {
|
|
2266
|
+
if (draftRecord.formId !== state.formId || draftRecord.version !== state.draftVersion || draftRecord.status !== "draft" || draftRecord.schema.id !== state.formId || draftRecord.schema.version !== draftRecord.version) {
|
|
2267
|
+
return { success: false, error: { type: "draft_not_found" } };
|
|
2268
|
+
}
|
|
2269
|
+
const result = deleteDraft(state, options);
|
|
2270
|
+
if (!result.success) return result;
|
|
2271
|
+
const timestamp = requireTimestamp(options.deletedAt, "deletedAt");
|
|
2272
|
+
return {
|
|
2273
|
+
success: true,
|
|
2274
|
+
value: {
|
|
2275
|
+
nextState: result.value.nextState,
|
|
2276
|
+
plan: {
|
|
2277
|
+
formId: state.formId,
|
|
2278
|
+
expectedRevision: options.expectedRevision ?? state.revision,
|
|
2279
|
+
nextRevision: result.value.nextState.revision,
|
|
2280
|
+
draftToDeleteVersion: draftRecord.version,
|
|
2281
|
+
events: [transitionEvent("draft.deleted", state, result.value.nextState, [draftRecord.version], timestamp)],
|
|
2282
|
+
nextVersion: result.value.nextState.nextVersion,
|
|
2283
|
+
timestamp
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2142
2288
|
function assertVersionMutable(status) {
|
|
2143
2289
|
if (status !== "draft") {
|
|
2144
2290
|
const error = { type: "version_immutable", status };
|
|
@@ -2156,18 +2302,24 @@ export {
|
|
|
2156
2302
|
calculatePageVisibility,
|
|
2157
2303
|
cloneVersionToDraft,
|
|
2158
2304
|
collectSchemaLocales,
|
|
2305
|
+
createCloneTransitionPlan,
|
|
2306
|
+
createDeleteDraftTransitionPlan,
|
|
2159
2307
|
createPublishTransitionPlan,
|
|
2160
2308
|
createResponseAccumulator,
|
|
2161
2309
|
createSubmission,
|
|
2162
2310
|
decodeSubmissionCursor,
|
|
2311
|
+
decodeTextAnswerCursor,
|
|
2163
2312
|
deleteDraft,
|
|
2164
2313
|
dispatchWebhook,
|
|
2165
2314
|
encodeSubmissionCursor,
|
|
2315
|
+
encodeTextAnswerCursor,
|
|
2166
2316
|
escapeCsvCell,
|
|
2167
2317
|
exportResponsesToCsv,
|
|
2168
2318
|
exportResponsesToCsvStream,
|
|
2169
2319
|
isDisplayConditionSatisfied,
|
|
2170
2320
|
isQuestionVisible,
|
|
2321
|
+
jsonValuesEqual,
|
|
2322
|
+
matchesSubmissionFilter,
|
|
2171
2323
|
matchesSubmissionPageFilters,
|
|
2172
2324
|
normalizeSubmissionPageSize,
|
|
2173
2325
|
pipeResponsesToCsvStream,
|