@form-engine-ts/storage-mongodb 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 +8 -4
- package/dist/index.cjs +218 -18
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +218 -17
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -29,9 +29,13 @@ The caller owns the MongoDB connection lifecycle. Collection names can be custom
|
|
|
29
29
|
|
|
30
30
|
`listSubmissionPage(formId, { version, pageSize, cursor, since, until, locale })` performs bounded reads using the
|
|
31
31
|
compound `submittedAt`/response-ID cursor. Run `createIndexes()` after upgrading to create the matching compound index.
|
|
32
|
-
|
|
32
|
+
Metadata filters and the generic submission-filter AST are translated to MongoDB query operators before page sizing.
|
|
33
|
+
Legacy predicate filters remain supported and are applied client-side. `listTextAnswerPage` returns stable cursor pages
|
|
34
|
+
of individual text/textarea answers without loading every answer body at once.
|
|
33
35
|
|
|
34
|
-
Version records are stored in `form_versions`, with unique `(formId, version)`
|
|
35
|
-
|
|
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
|
|
36
39
|
`revision_conflict` to losing concurrent publishers; on a real MongoDB client, the state and record changes are committed
|
|
37
|
-
in one transaction.
|
|
40
|
+
in one transaction. Clone, publish, and draft-delete plans persist complete version state, affected records, and audit
|
|
41
|
+
events. State/record/list reads and typed commit errors are exposed through the full `VersionedFormStorageAdapter` API.
|
package/dist/index.cjs
CHANGED
|
@@ -20,7 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
createMongoDbStorage: () => createMongoDbStorage
|
|
23
|
+
createMongoDbStorage: () => createMongoDbStorage,
|
|
24
|
+
submissionFilterToMongo: () => submissionFilterToMongo
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(index_exports);
|
|
26
27
|
var import_core = require("@form-engine-ts/core");
|
|
@@ -45,6 +46,35 @@ function schemaDocumentId(formId, formVersion) {
|
|
|
45
46
|
function versionDocumentId(formId, formVersion) {
|
|
46
47
|
return `version:${encodeURIComponent(formId)}:${formVersion}`;
|
|
47
48
|
}
|
|
49
|
+
function versionEventDocumentId(formId, fromRevision, eventIndex) {
|
|
50
|
+
return `event:${encodeURIComponent(formId)}:${fromRevision}:${eventIndex}`;
|
|
51
|
+
}
|
|
52
|
+
function mongoSubmissionPath(path) {
|
|
53
|
+
if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(path) || path.split(".").some((part) => part.startsWith("$"))) {
|
|
54
|
+
throw new TypeError(`Invalid submission filter path: ${path}`);
|
|
55
|
+
}
|
|
56
|
+
const [root, ...rest] = path.split(".");
|
|
57
|
+
const suffix = rest.length === 0 ? "" : `.${rest.join(".")}`;
|
|
58
|
+
if (root === "id" || root === "responseId") return `_id${suffix}`;
|
|
59
|
+
if (root === "formId" || root === "formVersion" || root === "submittedAt") return `${root}${suffix}`;
|
|
60
|
+
if (root === "locale") return `submission.locale${suffix}`;
|
|
61
|
+
return `submission.${path}`;
|
|
62
|
+
}
|
|
63
|
+
function submissionFilterToMongo(filter) {
|
|
64
|
+
if (filter.op === "and" || filter.op === "or") {
|
|
65
|
+
return { [filter.op === "and" ? "$and" : "$or"]: filter.filters.map(submissionFilterToMongo) };
|
|
66
|
+
}
|
|
67
|
+
const path = mongoSubmissionPath(filter.path);
|
|
68
|
+
if (filter.op === "eq") return { [path]: filter.value };
|
|
69
|
+
if (filter.op === "in") return { [path]: { $in: [...filter.values] } };
|
|
70
|
+
if (filter.op === "exists") return { [path]: { $exists: filter.value } };
|
|
71
|
+
return {
|
|
72
|
+
[path]: {
|
|
73
|
+
...filter.from === void 0 ? {} : { $gte: filter.from },
|
|
74
|
+
...filter.to === void 0 ? {} : { $lte: filter.to }
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
48
78
|
function assertVersionRecord(record, formId, expectedStatus) {
|
|
49
79
|
if (record.formId !== formId || record.schema.id !== formId || record.schema.version !== record.version || !Number.isSafeInteger(record.version) || record.version < 1 || !Number.isSafeInteger(record.revision) || record.revision < 0 || expectedStatus !== void 0 && record.status !== expectedStatus) {
|
|
50
80
|
throw new TypeError("Version transition record is inconsistent.");
|
|
@@ -55,6 +85,7 @@ function assertTransitionPlan(plan) {
|
|
|
55
85
|
if (plan.formId.trim().length === 0 || !Number.isSafeInteger(plan.expectedRevision) || plan.expectedRevision < 0 || plan.nextRevision !== plan.expectedRevision + 1 || !Number.isFinite(Date.parse(plan.timestamp))) {
|
|
56
86
|
throw new TypeError("Version transition plan is invalid.");
|
|
57
87
|
}
|
|
88
|
+
if (!Array.isArray(plan.events)) throw new TypeError("Version transition plan events are required.");
|
|
58
89
|
if (plan.draftToCreate !== void 0) assertVersionRecord(plan.draftToCreate, plan.formId, "draft");
|
|
59
90
|
if (plan.publishedRecordToSave !== void 0) {
|
|
60
91
|
assertVersionRecord(plan.publishedRecordToSave, plan.formId, "published");
|
|
@@ -64,9 +95,20 @@ function assertTransitionPlan(plan) {
|
|
|
64
95
|
throw new TypeError("draftToDeleteVersion must be a positive safe integer.");
|
|
65
96
|
}
|
|
66
97
|
}
|
|
98
|
+
function parseVersionRecordDocument(document) {
|
|
99
|
+
assertVersionRecord(document.record, document.formId);
|
|
100
|
+
if (document._id !== versionDocumentId(document.formId, document.version) || document.record.version !== document.version || document.record.status !== document.status) {
|
|
101
|
+
throw new Error(`MongoDB version document "${String(document._id)}" has inconsistent metadata.`);
|
|
102
|
+
}
|
|
103
|
+
return cloneJson(document.record);
|
|
104
|
+
}
|
|
67
105
|
function isCasConflict(error) {
|
|
68
106
|
if (!isRecord(error)) return false;
|
|
69
|
-
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;
|
|
70
112
|
return typeof error.hasErrorLabel === "function" && error.hasErrorLabel("TransientTransactionError") === true;
|
|
71
113
|
}
|
|
72
114
|
function parseSchemaDocument(document) {
|
|
@@ -110,10 +152,23 @@ function createMongoDbStorage(options) {
|
|
|
110
152
|
"form_version_states",
|
|
111
153
|
"versionStatesCollectionName"
|
|
112
154
|
);
|
|
155
|
+
const versionEventsCollectionName = "form_version_events";
|
|
113
156
|
const schemas = options.db.collection(schemasCollectionName);
|
|
114
157
|
const submissions = options.db.collection(responsesCollectionName);
|
|
115
158
|
const versions = options.db.collection(versionsCollectionName);
|
|
116
159
|
const versionStates = options.db.collection(versionStatesCollectionName);
|
|
160
|
+
const versionEvents = options.db.collection(versionEventsCollectionName);
|
|
161
|
+
const storageRevisionConflict = async (plan) => {
|
|
162
|
+
const actual = await versionStates.findOne({ _id: plan.formId });
|
|
163
|
+
return {
|
|
164
|
+
success: false,
|
|
165
|
+
error: {
|
|
166
|
+
type: "revision_conflict",
|
|
167
|
+
expectedRevision: plan.expectedRevision,
|
|
168
|
+
...actual === null ? {} : { actualRevision: actual.revision }
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
};
|
|
117
172
|
const upsertVersionRecord = async (record, session) => {
|
|
118
173
|
const stored = cloneJson(record);
|
|
119
174
|
await versions.updateOne(
|
|
@@ -123,7 +178,17 @@ function createMongoDbStorage(options) {
|
|
|
123
178
|
);
|
|
124
179
|
};
|
|
125
180
|
const applyVersionTransition = async (plan, session) => {
|
|
126
|
-
const
|
|
181
|
+
const inferredNextVersion = Math.max(
|
|
182
|
+
1,
|
|
183
|
+
(plan.draftToCreate?.version ?? 0) + 1,
|
|
184
|
+
(plan.draftToDeleteVersion ?? 0) + 1,
|
|
185
|
+
(plan.publishedRecordToSave?.version ?? 0) + 1
|
|
186
|
+
);
|
|
187
|
+
const stateSet = {
|
|
188
|
+
revision: plan.nextRevision,
|
|
189
|
+
nextVersion: plan.nextVersion ?? inferredNextVersion,
|
|
190
|
+
updatedAt: plan.timestamp
|
|
191
|
+
};
|
|
127
192
|
if (plan.draftToCreate !== void 0) stateSet.draftVersion = plan.draftToCreate.version;
|
|
128
193
|
if (plan.publishedRecordToSave !== void 0) stateSet.publishedVersion = plan.publishedRecordToSave.version;
|
|
129
194
|
const stateUpdate = {
|
|
@@ -139,7 +204,15 @@ function createMongoDbStorage(options) {
|
|
|
139
204
|
}
|
|
140
205
|
);
|
|
141
206
|
if (stateResult.matchedCount === 0 && stateResult.upsertedCount === 0) {
|
|
142
|
-
|
|
207
|
+
const actual = await versionStates.findOne({ _id: plan.formId }, session === void 0 ? {} : { session });
|
|
208
|
+
return {
|
|
209
|
+
success: false,
|
|
210
|
+
error: {
|
|
211
|
+
type: "revision_conflict",
|
|
212
|
+
expectedRevision: plan.expectedRevision,
|
|
213
|
+
...actual === null ? {} : { actualRevision: actual.revision }
|
|
214
|
+
}
|
|
215
|
+
};
|
|
143
216
|
}
|
|
144
217
|
if (plan.draftToDeleteVersion !== void 0) {
|
|
145
218
|
await versions.deleteOne(
|
|
@@ -148,9 +221,16 @@ function createMongoDbStorage(options) {
|
|
|
148
221
|
);
|
|
149
222
|
}
|
|
150
223
|
if (plan.draftToCreate !== void 0) await upsertVersionRecord(plan.draftToCreate, session);
|
|
151
|
-
if (plan.publishedRecordToSave !== void 0) await upsertVersionRecord(plan.publishedRecordToSave, session);
|
|
152
224
|
for (const record of plan.archivedRecordsToSave ?? []) await upsertVersionRecord(record, session);
|
|
153
|
-
|
|
225
|
+
if (plan.publishedRecordToSave !== void 0) await upsertVersionRecord(plan.publishedRecordToSave, session);
|
|
226
|
+
for (const [eventIndex, event] of plan.events.entries()) {
|
|
227
|
+
await versionEvents.updateOne(
|
|
228
|
+
{ _id: versionEventDocumentId(plan.formId, plan.expectedRevision, eventIndex) },
|
|
229
|
+
{ $set: { formId: plan.formId, fromRevision: plan.expectedRevision, eventIndex, event: cloneJson(event) } },
|
|
230
|
+
{ upsert: true, ...session === void 0 ? {} : { session } }
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return { success: true, value: { success: true } };
|
|
154
234
|
};
|
|
155
235
|
return {
|
|
156
236
|
async saveSchema(schema) {
|
|
@@ -202,7 +282,7 @@ function createMongoDbStorage(options) {
|
|
|
202
282
|
...options2.since === void 0 ? {} : { $gte: options2.since },
|
|
203
283
|
...options2.until === void 0 ? {} : { $lte: options2.until }
|
|
204
284
|
};
|
|
205
|
-
const
|
|
285
|
+
const baseFilter = {
|
|
206
286
|
formId,
|
|
207
287
|
...options2.version === void 0 ? {} : { formVersion: options2.version },
|
|
208
288
|
...options2.locale === void 0 ? {} : { "submission.locale": options2.locale },
|
|
@@ -214,8 +294,16 @@ function createMongoDbStorage(options) {
|
|
|
214
294
|
]
|
|
215
295
|
}
|
|
216
296
|
};
|
|
297
|
+
const serverFilters = [baseFilter];
|
|
298
|
+
if (options2.filter !== void 0 && typeof options2.filter !== "function") {
|
|
299
|
+
serverFilters.push(submissionFilterToMongo(options2.filter));
|
|
300
|
+
}
|
|
301
|
+
for (const [key, value] of Object.entries(options2.metadataFilters ?? {})) {
|
|
302
|
+
serverFilters.push({ [`submission.metadata.${key}`]: value });
|
|
303
|
+
}
|
|
304
|
+
const filter = serverFilters.length === 1 ? baseFilter : { $and: serverFilters };
|
|
217
305
|
const sorted = submissions.find(filter).sort({ submittedAt: 1, _id: 1 });
|
|
218
|
-
const requiresClientFiltering = options2.filter
|
|
306
|
+
const requiresClientFiltering = typeof options2.filter === "function";
|
|
219
307
|
const documents = requiresClientFiltering ? await sorted.toArray() : await sorted.limit(pageSize + 1).toArray();
|
|
220
308
|
const candidates = documents.map(parseSubmissionDocument).filter((item) => (0, import_core.matchesSubmissionPageFilters)(item, options2));
|
|
221
309
|
const hasMore = candidates.length > pageSize;
|
|
@@ -227,6 +315,69 @@ function createMongoDbStorage(options) {
|
|
|
227
315
|
...hasMore && last !== void 0 ? { nextCursor: (0, import_core.encodeSubmissionCursor)({ submittedAt: last.submittedAt, responseId: last.id }) } : {}
|
|
228
316
|
};
|
|
229
317
|
},
|
|
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
|
+
}
|
|
324
|
+
const pageSize = (0, import_core.normalizeSubmissionPageSize)(options2.pageSize);
|
|
325
|
+
const cursor = options2.cursor === void 0 ? void 0 : (0, import_core.decodeTextAnswerCursor)(options2.cursor);
|
|
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.");
|
|
328
|
+
}
|
|
329
|
+
const submittedAt = options2.since === void 0 && options2.until === void 0 ? void 0 : {
|
|
330
|
+
...options2.since === void 0 ? {} : { $gte: options2.since },
|
|
331
|
+
...options2.until === void 0 ? {} : { $lte: options2.until }
|
|
332
|
+
};
|
|
333
|
+
const baseFilter = {
|
|
334
|
+
formId,
|
|
335
|
+
...options2.version === void 0 ? {} : { formVersion: options2.version },
|
|
336
|
+
...options2.locale === void 0 ? {} : { "submission.locale": options2.locale },
|
|
337
|
+
...submittedAt === void 0 ? {} : { submittedAt },
|
|
338
|
+
...cursor === void 0 ? {} : { _id: { $gte: cursor.responseId } },
|
|
339
|
+
...requestedFieldIds?.length === 1 ? { [`submission.values.${requestedFieldIds[0]}`]: { $type: "string" } } : {}
|
|
340
|
+
};
|
|
341
|
+
const serverFilters = [baseFilter];
|
|
342
|
+
if (options2.filter !== void 0 && typeof options2.filter !== "function") {
|
|
343
|
+
serverFilters.push(submissionFilterToMongo(options2.filter));
|
|
344
|
+
}
|
|
345
|
+
for (const [key, value] of Object.entries(options2.metadataFilters ?? {})) {
|
|
346
|
+
serverFilters.push({ [`submission.metadata.${key}`]: value });
|
|
347
|
+
}
|
|
348
|
+
const filter = serverFilters.length === 1 ? baseFilter : { $and: serverFilters };
|
|
349
|
+
const sorted = submissions.find(filter).sort({ _id: 1 });
|
|
350
|
+
const documents = await sorted.toArray();
|
|
351
|
+
const candidates = documents.map(parseSubmissionDocument).filter((submission) => (0, import_core.matchesSubmissionPageFilters)(submission, options2)).flatMap((submission) => {
|
|
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 [];
|
|
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
|
+
});
|
|
371
|
+
});
|
|
372
|
+
const hasMore = candidates.length > pageSize;
|
|
373
|
+
const items = candidates.slice(0, pageSize);
|
|
374
|
+
const last = items.at(-1);
|
|
375
|
+
return {
|
|
376
|
+
items,
|
|
377
|
+
hasMore,
|
|
378
|
+
...hasMore && last !== void 0 ? { nextCursor: (0, import_core.encodeTextAnswerCursor)({ responseId: last.responseId, fieldId: last.fieldId }) } : {}
|
|
379
|
+
};
|
|
380
|
+
},
|
|
230
381
|
async deleteSubmission(submissionId) {
|
|
231
382
|
await submissions.deleteOne({ _id: submissionId });
|
|
232
383
|
},
|
|
@@ -238,33 +389,67 @@ function createMongoDbStorage(options) {
|
|
|
238
389
|
schemas.deleteMany({}),
|
|
239
390
|
submissions.deleteMany({}),
|
|
240
391
|
versions.deleteMany({}),
|
|
241
|
-
versionStates.deleteMany({})
|
|
392
|
+
versionStates.deleteMany({}),
|
|
393
|
+
versionEvents.deleteMany({})
|
|
242
394
|
]);
|
|
243
395
|
},
|
|
396
|
+
async getVersionState(formId) {
|
|
397
|
+
const document = await versionStates.findOne({ _id: formId });
|
|
398
|
+
if (document === null) return null;
|
|
399
|
+
if (!Number.isSafeInteger(document.revision) || document.revision < 0) {
|
|
400
|
+
throw new Error(`MongoDB version state "${formId}" is invalid.`);
|
|
401
|
+
}
|
|
402
|
+
const nextVersion = document.nextVersion ?? Math.max(1, (document.draftVersion ?? 0) + 1, (document.publishedVersion ?? 0) + 1);
|
|
403
|
+
return {
|
|
404
|
+
formId,
|
|
405
|
+
revision: document.revision,
|
|
406
|
+
nextVersion,
|
|
407
|
+
...document.draftVersion === void 0 ? {} : { draftVersion: document.draftVersion },
|
|
408
|
+
...document.publishedVersion === void 0 ? {} : { publishedVersion: document.publishedVersion }
|
|
409
|
+
};
|
|
410
|
+
},
|
|
411
|
+
async getVersionRecord(formId, version) {
|
|
412
|
+
const document = await versions.findOne({ _id: versionDocumentId(formId, version) });
|
|
413
|
+
return document === null ? null : parseVersionRecordDocument(document);
|
|
414
|
+
},
|
|
415
|
+
async listVersionRecords(formId) {
|
|
416
|
+
return (await versions.find({ formId }).sort({ version: 1 }).toArray()).map(parseVersionRecordDocument);
|
|
417
|
+
},
|
|
244
418
|
async commitVersionTransition(plan) {
|
|
245
|
-
|
|
419
|
+
try {
|
|
420
|
+
assertTransitionPlan(plan);
|
|
421
|
+
} catch (cause) {
|
|
422
|
+
return {
|
|
423
|
+
success: false,
|
|
424
|
+
error: { type: "invalid_transition", message: cause instanceof Error ? cause.message : String(cause) }
|
|
425
|
+
};
|
|
426
|
+
}
|
|
246
427
|
const client = options.db.client;
|
|
247
428
|
if (client === void 0 || typeof client.startSession !== "function") {
|
|
248
429
|
try {
|
|
249
430
|
return await applyVersionTransition(plan);
|
|
250
431
|
} catch (error) {
|
|
251
|
-
if (isCasConflict(error))
|
|
252
|
-
|
|
432
|
+
if (isCasConflict(error)) {
|
|
433
|
+
return storageRevisionConflict(plan);
|
|
434
|
+
}
|
|
435
|
+
return { success: false, error: { type: "storage_error", cause: error } };
|
|
253
436
|
}
|
|
254
437
|
}
|
|
255
438
|
const session = client.startSession();
|
|
256
439
|
try {
|
|
257
440
|
let result = {
|
|
258
441
|
success: false,
|
|
259
|
-
error: "revision_conflict"
|
|
442
|
+
error: { type: "revision_conflict", expectedRevision: plan.expectedRevision }
|
|
260
443
|
};
|
|
261
444
|
await session.withTransaction(async () => {
|
|
262
445
|
result = await applyVersionTransition(plan, session);
|
|
263
446
|
});
|
|
264
447
|
return result;
|
|
265
448
|
} catch (error) {
|
|
266
|
-
if (isCasConflict(error))
|
|
267
|
-
|
|
449
|
+
if (isCasConflict(error)) {
|
|
450
|
+
return storageRevisionConflict(plan);
|
|
451
|
+
}
|
|
452
|
+
return { success: false, error: { type: "storage_error", cause: error } };
|
|
268
453
|
} finally {
|
|
269
454
|
await session.endSession();
|
|
270
455
|
}
|
|
@@ -276,8 +461,22 @@ function createMongoDbStorage(options) {
|
|
|
276
461
|
{ key: { "submission.locale": 1 }, name: "form_responses_locale" }
|
|
277
462
|
]),
|
|
278
463
|
versions.createIndexes([
|
|
279
|
-
{ key: { formId: 1, version: 1 }, name: "
|
|
280
|
-
{
|
|
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
|
+
}
|
|
477
|
+
]),
|
|
478
|
+
versionEvents.createIndexes([
|
|
479
|
+
{ key: { formId: 1, fromRevision: 1, eventIndex: 1 }, name: "form_version_events_revision" }
|
|
281
480
|
])
|
|
282
481
|
]);
|
|
283
482
|
}
|
|
@@ -285,5 +484,6 @@ function createMongoDbStorage(options) {
|
|
|
285
484
|
}
|
|
286
485
|
// Annotate the CommonJS export names for ESM import in node:
|
|
287
486
|
0 && (module.exports = {
|
|
288
|
-
createMongoDbStorage
|
|
487
|
+
createMongoDbStorage,
|
|
488
|
+
submissionFilterToMongo
|
|
289
489
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { PagedSubmissionStorageAdapter, VersionedFormStorageAdapter } from '@form-engine-ts/core';
|
|
2
|
-
import { Db } from 'mongodb';
|
|
1
|
+
import { PagedSubmissionStorageAdapter, VersionedFormStorageAdapter, SubmissionFilter } from '@form-engine-ts/core';
|
|
2
|
+
import { Db, Document } from 'mongodb';
|
|
3
3
|
|
|
4
4
|
interface MongoDbStorageOptions {
|
|
5
5
|
readonly db: Db;
|
|
@@ -11,6 +11,7 @@ interface MongoDbStorageOptions {
|
|
|
11
11
|
interface MongoDbStorageAdapter extends PagedSubmissionStorageAdapter, VersionedFormStorageAdapter {
|
|
12
12
|
createIndexes(): Promise<void>;
|
|
13
13
|
}
|
|
14
|
+
declare function submissionFilterToMongo(filter: SubmissionFilter): Document;
|
|
14
15
|
declare function createMongoDbStorage(options: MongoDbStorageOptions): MongoDbStorageAdapter;
|
|
15
16
|
|
|
16
|
-
export { type MongoDbStorageAdapter, type MongoDbStorageOptions, createMongoDbStorage };
|
|
17
|
+
export { type MongoDbStorageAdapter, type MongoDbStorageOptions, createMongoDbStorage, submissionFilterToMongo };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { PagedSubmissionStorageAdapter, VersionedFormStorageAdapter } from '@form-engine-ts/core';
|
|
2
|
-
import { Db } from 'mongodb';
|
|
1
|
+
import { PagedSubmissionStorageAdapter, VersionedFormStorageAdapter, SubmissionFilter } from '@form-engine-ts/core';
|
|
2
|
+
import { Db, Document } from 'mongodb';
|
|
3
3
|
|
|
4
4
|
interface MongoDbStorageOptions {
|
|
5
5
|
readonly db: Db;
|
|
@@ -11,6 +11,7 @@ interface MongoDbStorageOptions {
|
|
|
11
11
|
interface MongoDbStorageAdapter extends PagedSubmissionStorageAdapter, VersionedFormStorageAdapter {
|
|
12
12
|
createIndexes(): Promise<void>;
|
|
13
13
|
}
|
|
14
|
+
declare function submissionFilterToMongo(filter: SubmissionFilter): Document;
|
|
14
15
|
declare function createMongoDbStorage(options: MongoDbStorageOptions): MongoDbStorageAdapter;
|
|
15
16
|
|
|
16
|
-
export { type MongoDbStorageAdapter, type MongoDbStorageOptions, createMongoDbStorage };
|
|
17
|
+
export { type MongoDbStorageAdapter, type MongoDbStorageOptions, createMongoDbStorage, submissionFilterToMongo };
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import {
|
|
3
3
|
assertValidFormSchema,
|
|
4
4
|
decodeSubmissionCursor,
|
|
5
|
+
decodeTextAnswerCursor,
|
|
5
6
|
encodeSubmissionCursor,
|
|
7
|
+
encodeTextAnswerCursor,
|
|
6
8
|
matchesSubmissionPageFilters,
|
|
7
9
|
normalizeSubmissionPageSize
|
|
8
10
|
} from "@form-engine-ts/core";
|
|
@@ -27,6 +29,35 @@ function schemaDocumentId(formId, formVersion) {
|
|
|
27
29
|
function versionDocumentId(formId, formVersion) {
|
|
28
30
|
return `version:${encodeURIComponent(formId)}:${formVersion}`;
|
|
29
31
|
}
|
|
32
|
+
function versionEventDocumentId(formId, fromRevision, eventIndex) {
|
|
33
|
+
return `event:${encodeURIComponent(formId)}:${fromRevision}:${eventIndex}`;
|
|
34
|
+
}
|
|
35
|
+
function mongoSubmissionPath(path) {
|
|
36
|
+
if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(path) || path.split(".").some((part) => part.startsWith("$"))) {
|
|
37
|
+
throw new TypeError(`Invalid submission filter path: ${path}`);
|
|
38
|
+
}
|
|
39
|
+
const [root, ...rest] = path.split(".");
|
|
40
|
+
const suffix = rest.length === 0 ? "" : `.${rest.join(".")}`;
|
|
41
|
+
if (root === "id" || root === "responseId") return `_id${suffix}`;
|
|
42
|
+
if (root === "formId" || root === "formVersion" || root === "submittedAt") return `${root}${suffix}`;
|
|
43
|
+
if (root === "locale") return `submission.locale${suffix}`;
|
|
44
|
+
return `submission.${path}`;
|
|
45
|
+
}
|
|
46
|
+
function submissionFilterToMongo(filter) {
|
|
47
|
+
if (filter.op === "and" || filter.op === "or") {
|
|
48
|
+
return { [filter.op === "and" ? "$and" : "$or"]: filter.filters.map(submissionFilterToMongo) };
|
|
49
|
+
}
|
|
50
|
+
const path = mongoSubmissionPath(filter.path);
|
|
51
|
+
if (filter.op === "eq") return { [path]: filter.value };
|
|
52
|
+
if (filter.op === "in") return { [path]: { $in: [...filter.values] } };
|
|
53
|
+
if (filter.op === "exists") return { [path]: { $exists: filter.value } };
|
|
54
|
+
return {
|
|
55
|
+
[path]: {
|
|
56
|
+
...filter.from === void 0 ? {} : { $gte: filter.from },
|
|
57
|
+
...filter.to === void 0 ? {} : { $lte: filter.to }
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
30
61
|
function assertVersionRecord(record, formId, expectedStatus) {
|
|
31
62
|
if (record.formId !== formId || record.schema.id !== formId || record.schema.version !== record.version || !Number.isSafeInteger(record.version) || record.version < 1 || !Number.isSafeInteger(record.revision) || record.revision < 0 || expectedStatus !== void 0 && record.status !== expectedStatus) {
|
|
32
63
|
throw new TypeError("Version transition record is inconsistent.");
|
|
@@ -37,6 +68,7 @@ function assertTransitionPlan(plan) {
|
|
|
37
68
|
if (plan.formId.trim().length === 0 || !Number.isSafeInteger(plan.expectedRevision) || plan.expectedRevision < 0 || plan.nextRevision !== plan.expectedRevision + 1 || !Number.isFinite(Date.parse(plan.timestamp))) {
|
|
38
69
|
throw new TypeError("Version transition plan is invalid.");
|
|
39
70
|
}
|
|
71
|
+
if (!Array.isArray(plan.events)) throw new TypeError("Version transition plan events are required.");
|
|
40
72
|
if (plan.draftToCreate !== void 0) assertVersionRecord(plan.draftToCreate, plan.formId, "draft");
|
|
41
73
|
if (plan.publishedRecordToSave !== void 0) {
|
|
42
74
|
assertVersionRecord(plan.publishedRecordToSave, plan.formId, "published");
|
|
@@ -46,9 +78,20 @@ function assertTransitionPlan(plan) {
|
|
|
46
78
|
throw new TypeError("draftToDeleteVersion must be a positive safe integer.");
|
|
47
79
|
}
|
|
48
80
|
}
|
|
81
|
+
function parseVersionRecordDocument(document) {
|
|
82
|
+
assertVersionRecord(document.record, document.formId);
|
|
83
|
+
if (document._id !== versionDocumentId(document.formId, document.version) || document.record.version !== document.version || document.record.status !== document.status) {
|
|
84
|
+
throw new Error(`MongoDB version document "${String(document._id)}" has inconsistent metadata.`);
|
|
85
|
+
}
|
|
86
|
+
return cloneJson(document.record);
|
|
87
|
+
}
|
|
49
88
|
function isCasConflict(error) {
|
|
50
89
|
if (!isRecord(error)) return false;
|
|
51
|
-
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;
|
|
52
95
|
return typeof error.hasErrorLabel === "function" && error.hasErrorLabel("TransientTransactionError") === true;
|
|
53
96
|
}
|
|
54
97
|
function parseSchemaDocument(document) {
|
|
@@ -92,10 +135,23 @@ function createMongoDbStorage(options) {
|
|
|
92
135
|
"form_version_states",
|
|
93
136
|
"versionStatesCollectionName"
|
|
94
137
|
);
|
|
138
|
+
const versionEventsCollectionName = "form_version_events";
|
|
95
139
|
const schemas = options.db.collection(schemasCollectionName);
|
|
96
140
|
const submissions = options.db.collection(responsesCollectionName);
|
|
97
141
|
const versions = options.db.collection(versionsCollectionName);
|
|
98
142
|
const versionStates = options.db.collection(versionStatesCollectionName);
|
|
143
|
+
const versionEvents = options.db.collection(versionEventsCollectionName);
|
|
144
|
+
const storageRevisionConflict = async (plan) => {
|
|
145
|
+
const actual = await versionStates.findOne({ _id: plan.formId });
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
error: {
|
|
149
|
+
type: "revision_conflict",
|
|
150
|
+
expectedRevision: plan.expectedRevision,
|
|
151
|
+
...actual === null ? {} : { actualRevision: actual.revision }
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
};
|
|
99
155
|
const upsertVersionRecord = async (record, session) => {
|
|
100
156
|
const stored = cloneJson(record);
|
|
101
157
|
await versions.updateOne(
|
|
@@ -105,7 +161,17 @@ function createMongoDbStorage(options) {
|
|
|
105
161
|
);
|
|
106
162
|
};
|
|
107
163
|
const applyVersionTransition = async (plan, session) => {
|
|
108
|
-
const
|
|
164
|
+
const inferredNextVersion = Math.max(
|
|
165
|
+
1,
|
|
166
|
+
(plan.draftToCreate?.version ?? 0) + 1,
|
|
167
|
+
(plan.draftToDeleteVersion ?? 0) + 1,
|
|
168
|
+
(plan.publishedRecordToSave?.version ?? 0) + 1
|
|
169
|
+
);
|
|
170
|
+
const stateSet = {
|
|
171
|
+
revision: plan.nextRevision,
|
|
172
|
+
nextVersion: plan.nextVersion ?? inferredNextVersion,
|
|
173
|
+
updatedAt: plan.timestamp
|
|
174
|
+
};
|
|
109
175
|
if (plan.draftToCreate !== void 0) stateSet.draftVersion = plan.draftToCreate.version;
|
|
110
176
|
if (plan.publishedRecordToSave !== void 0) stateSet.publishedVersion = plan.publishedRecordToSave.version;
|
|
111
177
|
const stateUpdate = {
|
|
@@ -121,7 +187,15 @@ function createMongoDbStorage(options) {
|
|
|
121
187
|
}
|
|
122
188
|
);
|
|
123
189
|
if (stateResult.matchedCount === 0 && stateResult.upsertedCount === 0) {
|
|
124
|
-
|
|
190
|
+
const actual = await versionStates.findOne({ _id: plan.formId }, session === void 0 ? {} : { session });
|
|
191
|
+
return {
|
|
192
|
+
success: false,
|
|
193
|
+
error: {
|
|
194
|
+
type: "revision_conflict",
|
|
195
|
+
expectedRevision: plan.expectedRevision,
|
|
196
|
+
...actual === null ? {} : { actualRevision: actual.revision }
|
|
197
|
+
}
|
|
198
|
+
};
|
|
125
199
|
}
|
|
126
200
|
if (plan.draftToDeleteVersion !== void 0) {
|
|
127
201
|
await versions.deleteOne(
|
|
@@ -130,9 +204,16 @@ function createMongoDbStorage(options) {
|
|
|
130
204
|
);
|
|
131
205
|
}
|
|
132
206
|
if (plan.draftToCreate !== void 0) await upsertVersionRecord(plan.draftToCreate, session);
|
|
133
|
-
if (plan.publishedRecordToSave !== void 0) await upsertVersionRecord(plan.publishedRecordToSave, session);
|
|
134
207
|
for (const record of plan.archivedRecordsToSave ?? []) await upsertVersionRecord(record, session);
|
|
135
|
-
|
|
208
|
+
if (plan.publishedRecordToSave !== void 0) await upsertVersionRecord(plan.publishedRecordToSave, session);
|
|
209
|
+
for (const [eventIndex, event] of plan.events.entries()) {
|
|
210
|
+
await versionEvents.updateOne(
|
|
211
|
+
{ _id: versionEventDocumentId(plan.formId, plan.expectedRevision, eventIndex) },
|
|
212
|
+
{ $set: { formId: plan.formId, fromRevision: plan.expectedRevision, eventIndex, event: cloneJson(event) } },
|
|
213
|
+
{ upsert: true, ...session === void 0 ? {} : { session } }
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return { success: true, value: { success: true } };
|
|
136
217
|
};
|
|
137
218
|
return {
|
|
138
219
|
async saveSchema(schema) {
|
|
@@ -184,7 +265,7 @@ function createMongoDbStorage(options) {
|
|
|
184
265
|
...options2.since === void 0 ? {} : { $gte: options2.since },
|
|
185
266
|
...options2.until === void 0 ? {} : { $lte: options2.until }
|
|
186
267
|
};
|
|
187
|
-
const
|
|
268
|
+
const baseFilter = {
|
|
188
269
|
formId,
|
|
189
270
|
...options2.version === void 0 ? {} : { formVersion: options2.version },
|
|
190
271
|
...options2.locale === void 0 ? {} : { "submission.locale": options2.locale },
|
|
@@ -196,8 +277,16 @@ function createMongoDbStorage(options) {
|
|
|
196
277
|
]
|
|
197
278
|
}
|
|
198
279
|
};
|
|
280
|
+
const serverFilters = [baseFilter];
|
|
281
|
+
if (options2.filter !== void 0 && typeof options2.filter !== "function") {
|
|
282
|
+
serverFilters.push(submissionFilterToMongo(options2.filter));
|
|
283
|
+
}
|
|
284
|
+
for (const [key, value] of Object.entries(options2.metadataFilters ?? {})) {
|
|
285
|
+
serverFilters.push({ [`submission.metadata.${key}`]: value });
|
|
286
|
+
}
|
|
287
|
+
const filter = serverFilters.length === 1 ? baseFilter : { $and: serverFilters };
|
|
199
288
|
const sorted = submissions.find(filter).sort({ submittedAt: 1, _id: 1 });
|
|
200
|
-
const requiresClientFiltering = options2.filter
|
|
289
|
+
const requiresClientFiltering = typeof options2.filter === "function";
|
|
201
290
|
const documents = requiresClientFiltering ? await sorted.toArray() : await sorted.limit(pageSize + 1).toArray();
|
|
202
291
|
const candidates = documents.map(parseSubmissionDocument).filter((item) => matchesSubmissionPageFilters(item, options2));
|
|
203
292
|
const hasMore = candidates.length > pageSize;
|
|
@@ -209,6 +298,69 @@ function createMongoDbStorage(options) {
|
|
|
209
298
|
...hasMore && last !== void 0 ? { nextCursor: encodeSubmissionCursor({ submittedAt: last.submittedAt, responseId: last.id }) } : {}
|
|
210
299
|
};
|
|
211
300
|
},
|
|
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
|
+
}
|
|
307
|
+
const pageSize = normalizeSubmissionPageSize(options2.pageSize);
|
|
308
|
+
const cursor = options2.cursor === void 0 ? void 0 : decodeTextAnswerCursor(options2.cursor);
|
|
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.");
|
|
311
|
+
}
|
|
312
|
+
const submittedAt = options2.since === void 0 && options2.until === void 0 ? void 0 : {
|
|
313
|
+
...options2.since === void 0 ? {} : { $gte: options2.since },
|
|
314
|
+
...options2.until === void 0 ? {} : { $lte: options2.until }
|
|
315
|
+
};
|
|
316
|
+
const baseFilter = {
|
|
317
|
+
formId,
|
|
318
|
+
...options2.version === void 0 ? {} : { formVersion: options2.version },
|
|
319
|
+
...options2.locale === void 0 ? {} : { "submission.locale": options2.locale },
|
|
320
|
+
...submittedAt === void 0 ? {} : { submittedAt },
|
|
321
|
+
...cursor === void 0 ? {} : { _id: { $gte: cursor.responseId } },
|
|
322
|
+
...requestedFieldIds?.length === 1 ? { [`submission.values.${requestedFieldIds[0]}`]: { $type: "string" } } : {}
|
|
323
|
+
};
|
|
324
|
+
const serverFilters = [baseFilter];
|
|
325
|
+
if (options2.filter !== void 0 && typeof options2.filter !== "function") {
|
|
326
|
+
serverFilters.push(submissionFilterToMongo(options2.filter));
|
|
327
|
+
}
|
|
328
|
+
for (const [key, value] of Object.entries(options2.metadataFilters ?? {})) {
|
|
329
|
+
serverFilters.push({ [`submission.metadata.${key}`]: value });
|
|
330
|
+
}
|
|
331
|
+
const filter = serverFilters.length === 1 ? baseFilter : { $and: serverFilters };
|
|
332
|
+
const sorted = submissions.find(filter).sort({ _id: 1 });
|
|
333
|
+
const documents = await sorted.toArray();
|
|
334
|
+
const candidates = documents.map(parseSubmissionDocument).filter((submission) => matchesSubmissionPageFilters(submission, options2)).flatMap((submission) => {
|
|
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 [];
|
|
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
|
+
});
|
|
354
|
+
});
|
|
355
|
+
const hasMore = candidates.length > pageSize;
|
|
356
|
+
const items = candidates.slice(0, pageSize);
|
|
357
|
+
const last = items.at(-1);
|
|
358
|
+
return {
|
|
359
|
+
items,
|
|
360
|
+
hasMore,
|
|
361
|
+
...hasMore && last !== void 0 ? { nextCursor: encodeTextAnswerCursor({ responseId: last.responseId, fieldId: last.fieldId }) } : {}
|
|
362
|
+
};
|
|
363
|
+
},
|
|
212
364
|
async deleteSubmission(submissionId) {
|
|
213
365
|
await submissions.deleteOne({ _id: submissionId });
|
|
214
366
|
},
|
|
@@ -220,33 +372,67 @@ function createMongoDbStorage(options) {
|
|
|
220
372
|
schemas.deleteMany({}),
|
|
221
373
|
submissions.deleteMany({}),
|
|
222
374
|
versions.deleteMany({}),
|
|
223
|
-
versionStates.deleteMany({})
|
|
375
|
+
versionStates.deleteMany({}),
|
|
376
|
+
versionEvents.deleteMany({})
|
|
224
377
|
]);
|
|
225
378
|
},
|
|
379
|
+
async getVersionState(formId) {
|
|
380
|
+
const document = await versionStates.findOne({ _id: formId });
|
|
381
|
+
if (document === null) return null;
|
|
382
|
+
if (!Number.isSafeInteger(document.revision) || document.revision < 0) {
|
|
383
|
+
throw new Error(`MongoDB version state "${formId}" is invalid.`);
|
|
384
|
+
}
|
|
385
|
+
const nextVersion = document.nextVersion ?? Math.max(1, (document.draftVersion ?? 0) + 1, (document.publishedVersion ?? 0) + 1);
|
|
386
|
+
return {
|
|
387
|
+
formId,
|
|
388
|
+
revision: document.revision,
|
|
389
|
+
nextVersion,
|
|
390
|
+
...document.draftVersion === void 0 ? {} : { draftVersion: document.draftVersion },
|
|
391
|
+
...document.publishedVersion === void 0 ? {} : { publishedVersion: document.publishedVersion }
|
|
392
|
+
};
|
|
393
|
+
},
|
|
394
|
+
async getVersionRecord(formId, version) {
|
|
395
|
+
const document = await versions.findOne({ _id: versionDocumentId(formId, version) });
|
|
396
|
+
return document === null ? null : parseVersionRecordDocument(document);
|
|
397
|
+
},
|
|
398
|
+
async listVersionRecords(formId) {
|
|
399
|
+
return (await versions.find({ formId }).sort({ version: 1 }).toArray()).map(parseVersionRecordDocument);
|
|
400
|
+
},
|
|
226
401
|
async commitVersionTransition(plan) {
|
|
227
|
-
|
|
402
|
+
try {
|
|
403
|
+
assertTransitionPlan(plan);
|
|
404
|
+
} catch (cause) {
|
|
405
|
+
return {
|
|
406
|
+
success: false,
|
|
407
|
+
error: { type: "invalid_transition", message: cause instanceof Error ? cause.message : String(cause) }
|
|
408
|
+
};
|
|
409
|
+
}
|
|
228
410
|
const client = options.db.client;
|
|
229
411
|
if (client === void 0 || typeof client.startSession !== "function") {
|
|
230
412
|
try {
|
|
231
413
|
return await applyVersionTransition(plan);
|
|
232
414
|
} catch (error) {
|
|
233
|
-
if (isCasConflict(error))
|
|
234
|
-
|
|
415
|
+
if (isCasConflict(error)) {
|
|
416
|
+
return storageRevisionConflict(plan);
|
|
417
|
+
}
|
|
418
|
+
return { success: false, error: { type: "storage_error", cause: error } };
|
|
235
419
|
}
|
|
236
420
|
}
|
|
237
421
|
const session = client.startSession();
|
|
238
422
|
try {
|
|
239
423
|
let result = {
|
|
240
424
|
success: false,
|
|
241
|
-
error: "revision_conflict"
|
|
425
|
+
error: { type: "revision_conflict", expectedRevision: plan.expectedRevision }
|
|
242
426
|
};
|
|
243
427
|
await session.withTransaction(async () => {
|
|
244
428
|
result = await applyVersionTransition(plan, session);
|
|
245
429
|
});
|
|
246
430
|
return result;
|
|
247
431
|
} catch (error) {
|
|
248
|
-
if (isCasConflict(error))
|
|
249
|
-
|
|
432
|
+
if (isCasConflict(error)) {
|
|
433
|
+
return storageRevisionConflict(plan);
|
|
434
|
+
}
|
|
435
|
+
return { success: false, error: { type: "storage_error", cause: error } };
|
|
250
436
|
} finally {
|
|
251
437
|
await session.endSession();
|
|
252
438
|
}
|
|
@@ -258,13 +444,28 @@ function createMongoDbStorage(options) {
|
|
|
258
444
|
{ key: { "submission.locale": 1 }, name: "form_responses_locale" }
|
|
259
445
|
]),
|
|
260
446
|
versions.createIndexes([
|
|
261
|
-
{ key: { formId: 1, version: 1 }, name: "
|
|
262
|
-
{
|
|
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
|
+
}
|
|
460
|
+
]),
|
|
461
|
+
versionEvents.createIndexes([
|
|
462
|
+
{ key: { formId: 1, fromRevision: 1, eventIndex: 1 }, name: "form_version_events_revision" }
|
|
263
463
|
])
|
|
264
464
|
]);
|
|
265
465
|
}
|
|
266
466
|
};
|
|
267
467
|
}
|
|
268
468
|
export {
|
|
269
|
-
createMongoDbStorage
|
|
469
|
+
createMongoDbStorage,
|
|
470
|
+
submissionFilterToMongo
|
|
270
471
|
};
|
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"
|