@form-engine-ts/storage-mongodb 2.7.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 +3 -2
- package/dist/index.cjs +49 -25
- package/dist/index.js +49 -25
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -33,8 +33,9 @@ Metadata filters and the generic submission-filter AST are translated to MongoDB
|
|
|
33
33
|
Legacy predicate filters remain supported and are applied client-side. `listTextAnswerPage` returns stable cursor pages
|
|
34
34
|
of individual text/textarea answers without loading every answer body at once.
|
|
35
35
|
|
|
36
|
-
Version records are stored in `form_versions`, with unique `(formId, version)`
|
|
37
|
-
|
|
36
|
+
Version records are stored in `form_versions`, with a unique `(formId, version)` index. Partial unique indexes allow at
|
|
37
|
+
most one Draft and one Published record per form, while any number of Archived records remain available. Transition state
|
|
38
|
+
lives in `form_version_states`. `commitVersionTransition(plan)` compares `expectedRevision` atomically and returns
|
|
38
39
|
`revision_conflict` to losing concurrent publishers; on a real MongoDB client, the state and record changes are committed
|
|
39
40
|
in one transaction. Clone, publish, and draft-delete plans persist complete version state, affected records, and audit
|
|
40
41
|
events. State/record/list reads and typed commit errors are exposed through the full `VersionedFormStorageAdapter` API.
|
package/dist/index.cjs
CHANGED
|
@@ -104,7 +104,11 @@ function parseVersionRecordDocument(document) {
|
|
|
104
104
|
}
|
|
105
105
|
function isCasConflict(error) {
|
|
106
106
|
if (!isRecord(error)) return false;
|
|
107
|
-
if (error.code === 11e3
|
|
107
|
+
if (error.code === 11e3) {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error.message ?? "");
|
|
109
|
+
return !/unique_(?:draft|published|version)_per_form/u.test(message);
|
|
110
|
+
}
|
|
111
|
+
if (error.code === 112 || error.code === 251) return true;
|
|
108
112
|
return typeof error.hasErrorLabel === "function" && error.hasErrorLabel("TransientTransactionError") === true;
|
|
109
113
|
}
|
|
110
114
|
function parseSchemaDocument(document) {
|
|
@@ -217,8 +221,8 @@ function createMongoDbStorage(options) {
|
|
|
217
221
|
);
|
|
218
222
|
}
|
|
219
223
|
if (plan.draftToCreate !== void 0) await upsertVersionRecord(plan.draftToCreate, session);
|
|
220
|
-
if (plan.publishedRecordToSave !== void 0) await upsertVersionRecord(plan.publishedRecordToSave, session);
|
|
221
224
|
for (const record of plan.archivedRecordsToSave ?? []) await upsertVersionRecord(record, session);
|
|
225
|
+
if (plan.publishedRecordToSave !== void 0) await upsertVersionRecord(plan.publishedRecordToSave, session);
|
|
222
226
|
for (const [eventIndex, event] of plan.events.entries()) {
|
|
223
227
|
await versionEvents.updateOne(
|
|
224
228
|
{ _id: versionEventDocumentId(plan.formId, plan.expectedRevision, eventIndex) },
|
|
@@ -311,12 +315,16 @@ function createMongoDbStorage(options) {
|
|
|
311
315
|
...hasMore && last !== void 0 ? { nextCursor: (0, import_core.encodeSubmissionCursor)({ submittedAt: last.submittedAt, responseId: last.id }) } : {}
|
|
312
316
|
};
|
|
313
317
|
},
|
|
314
|
-
async listTextAnswerPage(formId,
|
|
315
|
-
|
|
318
|
+
async listTextAnswerPage(formId, fieldIdOrOptions, providedOptions) {
|
|
319
|
+
const options2 = typeof fieldIdOrOptions === "string" ? providedOptions ?? {} : fieldIdOrOptions ?? {};
|
|
320
|
+
const requestedFieldIds = typeof fieldIdOrOptions === "string" ? [fieldIdOrOptions] : options2.fieldIds === void 0 ? void 0 : [...new Set(options2.fieldIds)];
|
|
321
|
+
if (requestedFieldIds?.some((fieldId) => fieldId.trim().length === 0)) {
|
|
322
|
+
throw new TypeError("fieldIds must not contain empty values.");
|
|
323
|
+
}
|
|
316
324
|
const pageSize = (0, import_core.normalizeSubmissionPageSize)(options2.pageSize);
|
|
317
325
|
const cursor = options2.cursor === void 0 ? void 0 : (0, import_core.decodeTextAnswerCursor)(options2.cursor);
|
|
318
|
-
if (cursor !== void 0 &&
|
|
319
|
-
throw new TypeError("Text answer cursor fieldId does not match the query.");
|
|
326
|
+
if (cursor !== void 0 && requestedFieldIds !== void 0 && !requestedFieldIds.includes(cursor.fieldId)) {
|
|
327
|
+
throw new TypeError("Text answer cursor fieldId does not match the query fields.");
|
|
320
328
|
}
|
|
321
329
|
const submittedAt = options2.since === void 0 && options2.until === void 0 ? void 0 : {
|
|
322
330
|
...options2.since === void 0 ? {} : { $gte: options2.since },
|
|
@@ -327,8 +335,8 @@ function createMongoDbStorage(options) {
|
|
|
327
335
|
...options2.version === void 0 ? {} : { formVersion: options2.version },
|
|
328
336
|
...options2.locale === void 0 ? {} : { "submission.locale": options2.locale },
|
|
329
337
|
...submittedAt === void 0 ? {} : { submittedAt },
|
|
330
|
-
...cursor === void 0 ? {} : { _id: { $
|
|
331
|
-
[`submission.values.${
|
|
338
|
+
...cursor === void 0 ? {} : { _id: { $gte: cursor.responseId } },
|
|
339
|
+
...requestedFieldIds?.length === 1 ? { [`submission.values.${requestedFieldIds[0]}`]: { $type: "string" } } : {}
|
|
332
340
|
};
|
|
333
341
|
const serverFilters = [baseFilter];
|
|
334
342
|
if (options2.filter !== void 0 && typeof options2.filter !== "function") {
|
|
@@ -339,22 +347,27 @@ function createMongoDbStorage(options) {
|
|
|
339
347
|
}
|
|
340
348
|
const filter = serverFilters.length === 1 ? baseFilter : { $and: serverFilters };
|
|
341
349
|
const sorted = submissions.find(filter).sort({ _id: 1 });
|
|
342
|
-
const documents =
|
|
350
|
+
const documents = await sorted.toArray();
|
|
343
351
|
const candidates = documents.map(parseSubmissionDocument).filter((submission) => (0, import_core.matchesSubmissionPageFilters)(submission, options2)).flatMap((submission) => {
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
{
|
|
348
|
-
|
|
349
|
-
formId: submission.formId,
|
|
350
|
-
formVersion: submission.formVersion,
|
|
351
|
-
fieldId,
|
|
352
|
-
text,
|
|
353
|
-
locale: submission.locale,
|
|
354
|
-
submittedAt: submission.submittedAt,
|
|
355
|
-
...submission.metadata === void 0 ? {} : { metadata: submission.metadata }
|
|
352
|
+
const entries = requestedFieldIds === void 0 ? Object.entries(submission.values) : requestedFieldIds.map((fieldId) => [fieldId, submission.values[fieldId]]);
|
|
353
|
+
return entries.flatMap(([fieldId, text]) => {
|
|
354
|
+
if (typeof text !== "string" || text.length === 0) return [];
|
|
355
|
+
if (cursor !== void 0 && (submission.id < cursor.responseId || submission.id === cursor.responseId && fieldId <= cursor.fieldId)) {
|
|
356
|
+
return [];
|
|
356
357
|
}
|
|
357
|
-
|
|
358
|
+
return [
|
|
359
|
+
{
|
|
360
|
+
responseId: submission.id,
|
|
361
|
+
formId: submission.formId,
|
|
362
|
+
formVersion: submission.formVersion,
|
|
363
|
+
fieldId,
|
|
364
|
+
text,
|
|
365
|
+
locale: submission.locale,
|
|
366
|
+
submittedAt: submission.submittedAt,
|
|
367
|
+
...submission.metadata === void 0 ? {} : { metadata: submission.metadata }
|
|
368
|
+
}
|
|
369
|
+
];
|
|
370
|
+
});
|
|
358
371
|
});
|
|
359
372
|
const hasMore = candidates.length > pageSize;
|
|
360
373
|
const items = candidates.slice(0, pageSize);
|
|
@@ -362,7 +375,7 @@ function createMongoDbStorage(options) {
|
|
|
362
375
|
return {
|
|
363
376
|
items,
|
|
364
377
|
hasMore,
|
|
365
|
-
...hasMore && last !== void 0 ? { nextCursor: (0, import_core.encodeTextAnswerCursor)({ responseId: last.responseId, fieldId }) } : {}
|
|
378
|
+
...hasMore && last !== void 0 ? { nextCursor: (0, import_core.encodeTextAnswerCursor)({ responseId: last.responseId, fieldId: last.fieldId }) } : {}
|
|
366
379
|
};
|
|
367
380
|
},
|
|
368
381
|
async deleteSubmission(submissionId) {
|
|
@@ -448,8 +461,19 @@ function createMongoDbStorage(options) {
|
|
|
448
461
|
{ key: { "submission.locale": 1 }, name: "form_responses_locale" }
|
|
449
462
|
]),
|
|
450
463
|
versions.createIndexes([
|
|
451
|
-
{ key: { formId: 1, version: 1 }, name: "
|
|
452
|
-
{
|
|
464
|
+
{ key: { formId: 1, version: 1 }, name: "unique_version_per_form", unique: true },
|
|
465
|
+
{
|
|
466
|
+
key: { formId: 1 },
|
|
467
|
+
name: "unique_draft_per_form",
|
|
468
|
+
unique: true,
|
|
469
|
+
partialFilterExpression: { status: "draft" }
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
key: { formId: 1 },
|
|
473
|
+
name: "unique_published_per_form",
|
|
474
|
+
unique: true,
|
|
475
|
+
partialFilterExpression: { status: "published" }
|
|
476
|
+
}
|
|
453
477
|
]),
|
|
454
478
|
versionEvents.createIndexes([
|
|
455
479
|
{ key: { formId: 1, fromRevision: 1, eventIndex: 1 }, name: "form_version_events_revision" }
|
package/dist/index.js
CHANGED
|
@@ -87,7 +87,11 @@ function parseVersionRecordDocument(document) {
|
|
|
87
87
|
}
|
|
88
88
|
function isCasConflict(error) {
|
|
89
89
|
if (!isRecord(error)) return false;
|
|
90
|
-
if (error.code === 11e3
|
|
90
|
+
if (error.code === 11e3) {
|
|
91
|
+
const message = error instanceof Error ? error.message : String(error.message ?? "");
|
|
92
|
+
return !/unique_(?:draft|published|version)_per_form/u.test(message);
|
|
93
|
+
}
|
|
94
|
+
if (error.code === 112 || error.code === 251) return true;
|
|
91
95
|
return typeof error.hasErrorLabel === "function" && error.hasErrorLabel("TransientTransactionError") === true;
|
|
92
96
|
}
|
|
93
97
|
function parseSchemaDocument(document) {
|
|
@@ -200,8 +204,8 @@ function createMongoDbStorage(options) {
|
|
|
200
204
|
);
|
|
201
205
|
}
|
|
202
206
|
if (plan.draftToCreate !== void 0) await upsertVersionRecord(plan.draftToCreate, session);
|
|
203
|
-
if (plan.publishedRecordToSave !== void 0) await upsertVersionRecord(plan.publishedRecordToSave, session);
|
|
204
207
|
for (const record of plan.archivedRecordsToSave ?? []) await upsertVersionRecord(record, session);
|
|
208
|
+
if (plan.publishedRecordToSave !== void 0) await upsertVersionRecord(plan.publishedRecordToSave, session);
|
|
205
209
|
for (const [eventIndex, event] of plan.events.entries()) {
|
|
206
210
|
await versionEvents.updateOne(
|
|
207
211
|
{ _id: versionEventDocumentId(plan.formId, plan.expectedRevision, eventIndex) },
|
|
@@ -294,12 +298,16 @@ function createMongoDbStorage(options) {
|
|
|
294
298
|
...hasMore && last !== void 0 ? { nextCursor: encodeSubmissionCursor({ submittedAt: last.submittedAt, responseId: last.id }) } : {}
|
|
295
299
|
};
|
|
296
300
|
},
|
|
297
|
-
async listTextAnswerPage(formId,
|
|
298
|
-
|
|
301
|
+
async listTextAnswerPage(formId, fieldIdOrOptions, providedOptions) {
|
|
302
|
+
const options2 = typeof fieldIdOrOptions === "string" ? providedOptions ?? {} : fieldIdOrOptions ?? {};
|
|
303
|
+
const requestedFieldIds = typeof fieldIdOrOptions === "string" ? [fieldIdOrOptions] : options2.fieldIds === void 0 ? void 0 : [...new Set(options2.fieldIds)];
|
|
304
|
+
if (requestedFieldIds?.some((fieldId) => fieldId.trim().length === 0)) {
|
|
305
|
+
throw new TypeError("fieldIds must not contain empty values.");
|
|
306
|
+
}
|
|
299
307
|
const pageSize = normalizeSubmissionPageSize(options2.pageSize);
|
|
300
308
|
const cursor = options2.cursor === void 0 ? void 0 : decodeTextAnswerCursor(options2.cursor);
|
|
301
|
-
if (cursor !== void 0 &&
|
|
302
|
-
throw new TypeError("Text answer cursor fieldId does not match the query.");
|
|
309
|
+
if (cursor !== void 0 && requestedFieldIds !== void 0 && !requestedFieldIds.includes(cursor.fieldId)) {
|
|
310
|
+
throw new TypeError("Text answer cursor fieldId does not match the query fields.");
|
|
303
311
|
}
|
|
304
312
|
const submittedAt = options2.since === void 0 && options2.until === void 0 ? void 0 : {
|
|
305
313
|
...options2.since === void 0 ? {} : { $gte: options2.since },
|
|
@@ -310,8 +318,8 @@ function createMongoDbStorage(options) {
|
|
|
310
318
|
...options2.version === void 0 ? {} : { formVersion: options2.version },
|
|
311
319
|
...options2.locale === void 0 ? {} : { "submission.locale": options2.locale },
|
|
312
320
|
...submittedAt === void 0 ? {} : { submittedAt },
|
|
313
|
-
...cursor === void 0 ? {} : { _id: { $
|
|
314
|
-
[`submission.values.${
|
|
321
|
+
...cursor === void 0 ? {} : { _id: { $gte: cursor.responseId } },
|
|
322
|
+
...requestedFieldIds?.length === 1 ? { [`submission.values.${requestedFieldIds[0]}`]: { $type: "string" } } : {}
|
|
315
323
|
};
|
|
316
324
|
const serverFilters = [baseFilter];
|
|
317
325
|
if (options2.filter !== void 0 && typeof options2.filter !== "function") {
|
|
@@ -322,22 +330,27 @@ function createMongoDbStorage(options) {
|
|
|
322
330
|
}
|
|
323
331
|
const filter = serverFilters.length === 1 ? baseFilter : { $and: serverFilters };
|
|
324
332
|
const sorted = submissions.find(filter).sort({ _id: 1 });
|
|
325
|
-
const documents =
|
|
333
|
+
const documents = await sorted.toArray();
|
|
326
334
|
const candidates = documents.map(parseSubmissionDocument).filter((submission) => matchesSubmissionPageFilters(submission, options2)).flatMap((submission) => {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
{
|
|
331
|
-
|
|
332
|
-
formId: submission.formId,
|
|
333
|
-
formVersion: submission.formVersion,
|
|
334
|
-
fieldId,
|
|
335
|
-
text,
|
|
336
|
-
locale: submission.locale,
|
|
337
|
-
submittedAt: submission.submittedAt,
|
|
338
|
-
...submission.metadata === void 0 ? {} : { metadata: submission.metadata }
|
|
335
|
+
const entries = requestedFieldIds === void 0 ? Object.entries(submission.values) : requestedFieldIds.map((fieldId) => [fieldId, submission.values[fieldId]]);
|
|
336
|
+
return entries.flatMap(([fieldId, text]) => {
|
|
337
|
+
if (typeof text !== "string" || text.length === 0) return [];
|
|
338
|
+
if (cursor !== void 0 && (submission.id < cursor.responseId || submission.id === cursor.responseId && fieldId <= cursor.fieldId)) {
|
|
339
|
+
return [];
|
|
339
340
|
}
|
|
340
|
-
|
|
341
|
+
return [
|
|
342
|
+
{
|
|
343
|
+
responseId: submission.id,
|
|
344
|
+
formId: submission.formId,
|
|
345
|
+
formVersion: submission.formVersion,
|
|
346
|
+
fieldId,
|
|
347
|
+
text,
|
|
348
|
+
locale: submission.locale,
|
|
349
|
+
submittedAt: submission.submittedAt,
|
|
350
|
+
...submission.metadata === void 0 ? {} : { metadata: submission.metadata }
|
|
351
|
+
}
|
|
352
|
+
];
|
|
353
|
+
});
|
|
341
354
|
});
|
|
342
355
|
const hasMore = candidates.length > pageSize;
|
|
343
356
|
const items = candidates.slice(0, pageSize);
|
|
@@ -345,7 +358,7 @@ function createMongoDbStorage(options) {
|
|
|
345
358
|
return {
|
|
346
359
|
items,
|
|
347
360
|
hasMore,
|
|
348
|
-
...hasMore && last !== void 0 ? { nextCursor: encodeTextAnswerCursor({ responseId: last.responseId, fieldId }) } : {}
|
|
361
|
+
...hasMore && last !== void 0 ? { nextCursor: encodeTextAnswerCursor({ responseId: last.responseId, fieldId: last.fieldId }) } : {}
|
|
349
362
|
};
|
|
350
363
|
},
|
|
351
364
|
async deleteSubmission(submissionId) {
|
|
@@ -431,8 +444,19 @@ function createMongoDbStorage(options) {
|
|
|
431
444
|
{ key: { "submission.locale": 1 }, name: "form_responses_locale" }
|
|
432
445
|
]),
|
|
433
446
|
versions.createIndexes([
|
|
434
|
-
{ key: { formId: 1, version: 1 }, name: "
|
|
435
|
-
{
|
|
447
|
+
{ key: { formId: 1, version: 1 }, name: "unique_version_per_form", unique: true },
|
|
448
|
+
{
|
|
449
|
+
key: { formId: 1 },
|
|
450
|
+
name: "unique_draft_per_form",
|
|
451
|
+
unique: true,
|
|
452
|
+
partialFilterExpression: { status: "draft" }
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
key: { formId: 1 },
|
|
456
|
+
name: "unique_published_per_form",
|
|
457
|
+
unique: true,
|
|
458
|
+
partialFilterExpression: { status: "published" }
|
|
459
|
+
}
|
|
436
460
|
]),
|
|
437
461
|
versionEvents.createIndexes([
|
|
438
462
|
{ key: { formId: 1, fromRevision: 1, eventIndex: 1 }, name: "form_version_events_revision" }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@form-engine-ts/storage-mongodb",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"typescript"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@form-engine-ts/core": "2.
|
|
42
|
+
"@form-engine-ts/core": "2.8.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"mongodb": "^6.0.0"
|