@atscript/mongo 0.0.16 → 0.0.18
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/dist/index.cjs +500 -7
- package/dist/index.d.ts +86 -7
- package/dist/index.mjs +503 -10
- package/package.json +7 -8
package/dist/index.cjs
CHANGED
|
@@ -106,6 +106,15 @@ const annotations = { mongo: {
|
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
108
|
}),
|
|
109
|
+
autoIndexes: new __atscript_core.AnnotationSpec({
|
|
110
|
+
description: "Switch on/off the automatic index creation. Works with as-mongo moost controller.\n\nDefault: true",
|
|
111
|
+
nodeType: ["interface"],
|
|
112
|
+
argument: {
|
|
113
|
+
name: "type",
|
|
114
|
+
type: "boolean",
|
|
115
|
+
description: "On/Off the automatic index creation"
|
|
116
|
+
}
|
|
117
|
+
}),
|
|
109
118
|
index: {
|
|
110
119
|
plain: new __atscript_core.AnnotationSpec({
|
|
111
120
|
description: "Defines a **standard MongoDB index** on a field.\n\n- Improves query performance on indexed fields.\n- Can be used for **single-field** or **compound** indexes.\n\n**Example:**\n```atscript\n@mongo.index.plain \"departmentIndex\"\ndepartment: string\n```\n",
|
|
@@ -250,7 +259,57 @@ const annotations = { mongo: {
|
|
|
250
259
|
description: "The **name of the vector search index** this field should be used as a filter for."
|
|
251
260
|
}]
|
|
252
261
|
})
|
|
253
|
-
}
|
|
262
|
+
},
|
|
263
|
+
patch: { strategy: new __atscript_core.AnnotationSpec({
|
|
264
|
+
description: "Defines the **patching strategy** for updating MongoDB documents.\n\n- **\"replace\"** → The field or object will be **fully replaced**.\n- **\"merge\"** → The field or object will be **merged recursively** (applies only to objects, not arrays).\n\n**Example:**\n```atscript\n@mongo.patch.strategy \"merge\"\nsettings: {\n notifications: boolean\n preferences: {\n theme: string\n }\n}\n```\n",
|
|
265
|
+
nodeType: ["prop"],
|
|
266
|
+
multiple: false,
|
|
267
|
+
argument: {
|
|
268
|
+
name: "strategy",
|
|
269
|
+
type: "string",
|
|
270
|
+
description: "The **patch strategy** for this field: `\"replace\"` (default) or `\"merge\"`.",
|
|
271
|
+
values: ["replace", "merge"]
|
|
272
|
+
},
|
|
273
|
+
validate(token, args, doc) {
|
|
274
|
+
const field = token.parentNode;
|
|
275
|
+
const errors = [];
|
|
276
|
+
const definition = field.getDefinition();
|
|
277
|
+
if (!definition) return errors;
|
|
278
|
+
let wrongType = false;
|
|
279
|
+
if ((0, __atscript_core.isRef)(definition)) {
|
|
280
|
+
const def = doc.unwindType(definition.id, definition.chain)?.def;
|
|
281
|
+
if (!(0, __atscript_core.isStructure)(def) && !(0, __atscript_core.isInterface)(def) && !(0, __atscript_core.isArray)(def)) wrongType = true;
|
|
282
|
+
} else if (!(0, __atscript_core.isStructure)(definition) && !(0, __atscript_core.isInterface)(definition) && !(0, __atscript_core.isArray)(definition)) wrongType = true;
|
|
283
|
+
if (wrongType) errors.push({
|
|
284
|
+
message: `[mongo] type of object or array expected when using @mongo.patch.strategy`,
|
|
285
|
+
severity: 1,
|
|
286
|
+
range: token.range
|
|
287
|
+
});
|
|
288
|
+
return errors;
|
|
289
|
+
}
|
|
290
|
+
}) },
|
|
291
|
+
array: { uniqueItems: new __atscript_core.AnnotationSpec({
|
|
292
|
+
description: "Marks an **array field** as containing *globally unique items* when handling **patch `$insert` operations**.\n\n- Forces the patcher to use **set-semantics** (`$setUnion`) instead of a plain append, so duplicates are silently skipped.\n- Has **no effect** on `$replace`, `$update`, or `$remove`.\n- If the array’s element type already defines one or more `@meta.isKey` properties, *uniqueness is implied* and this annotation is unnecessary (but harmless).\n\n**Example:**\n```atscript\n@mongo.array.uniqueItems\ntags: string[]\n\n// Later in a patch payload …\n{\n $insert: {\n tags: [\"mongo\", \"mongo\"] // second \"mongo\" is ignored\n }\n}\n```\n",
|
|
293
|
+
nodeType: ["prop"],
|
|
294
|
+
multiple: false,
|
|
295
|
+
validate(token, args, doc) {
|
|
296
|
+
const field = token.parentNode;
|
|
297
|
+
const errors = [];
|
|
298
|
+
const definition = field.getDefinition();
|
|
299
|
+
if (!definition) return errors;
|
|
300
|
+
let wrongType = false;
|
|
301
|
+
if ((0, __atscript_core.isRef)(definition)) {
|
|
302
|
+
const def = doc.unwindType(definition.id, definition.chain)?.def;
|
|
303
|
+
if (!(0, __atscript_core.isArray)(def)) wrongType = true;
|
|
304
|
+
} else if (!(0, __atscript_core.isArray)(definition)) wrongType = true;
|
|
305
|
+
if (wrongType) errors.push({
|
|
306
|
+
message: `[mongo] type of array expected when using @mongo.array.uniqueItems`,
|
|
307
|
+
severity: 1,
|
|
308
|
+
range: token.range
|
|
309
|
+
});
|
|
310
|
+
return errors;
|
|
311
|
+
}
|
|
312
|
+
}) }
|
|
254
313
|
} };
|
|
255
314
|
|
|
256
315
|
//#endregion
|
|
@@ -277,6 +336,304 @@ const NoopLogger = {
|
|
|
277
336
|
debug: () => {}
|
|
278
337
|
};
|
|
279
338
|
|
|
339
|
+
//#endregion
|
|
340
|
+
//#region packages/mongo/src/lib/validate-plugins.ts
|
|
341
|
+
const validateMongoIdPlugin = (ctx, def, value) => {
|
|
342
|
+
if (ctx.path === "_id" && def.type.tags.has("objectId")) return ctx.validateAnnotatedType(def, value instanceof mongodb.ObjectId ? value.toString() : value);
|
|
343
|
+
};
|
|
344
|
+
const validateMongoUniqueArrayItemsPlugin = (ctx, def, value) => {
|
|
345
|
+
if (def.metadata.has("mongo.array.uniqueItems") && def.type.kind === "array") {
|
|
346
|
+
if (Array.isArray(value)) {
|
|
347
|
+
const separator = "▼↩";
|
|
348
|
+
const seen = new Set();
|
|
349
|
+
const keyProps = CollectionPatcher.getKeyProps(def);
|
|
350
|
+
for (const item of value) {
|
|
351
|
+
let key = "";
|
|
352
|
+
if (keyProps.size) for (const prop of keyProps) key += JSON.stringify(item[prop]) + separator;
|
|
353
|
+
else key = JSON.stringify(item);
|
|
354
|
+
if (seen.has(key)) {
|
|
355
|
+
ctx.error(`Duplicate items are not allowed`);
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
seen.add(key);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return undefined;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region packages/mongo/src/lib/collection-patcher.ts
|
|
367
|
+
function _define_property$2(obj, key, value) {
|
|
368
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
369
|
+
value,
|
|
370
|
+
enumerable: true,
|
|
371
|
+
configurable: true,
|
|
372
|
+
writable: true
|
|
373
|
+
});
|
|
374
|
+
else obj[key] = value;
|
|
375
|
+
return obj;
|
|
376
|
+
}
|
|
377
|
+
var CollectionPatcher = class CollectionPatcher {
|
|
378
|
+
/**
|
|
379
|
+
* Extract a set of *key properties* (annotated with `@meta.isKey`) from an
|
|
380
|
+
* array‐of‐objects type definition. These keys uniquely identify an element
|
|
381
|
+
* inside the array and are later used for `$update`, `$remove` and `$upsert`.
|
|
382
|
+
*
|
|
383
|
+
* @param def Atscript array type
|
|
384
|
+
* @returns Set of property names marked as keys; empty set if none
|
|
385
|
+
*/ static getKeyProps(def) {
|
|
386
|
+
if (def.type.of.type.kind === "object") {
|
|
387
|
+
const objType = def.type.of.type;
|
|
388
|
+
const keyProps = new Set();
|
|
389
|
+
for (const [key, val] of objType.props.entries()) if (val.metadata.get("meta.isKey")) keyProps.add(key);
|
|
390
|
+
return keyProps;
|
|
391
|
+
}
|
|
392
|
+
return new Set();
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Build a runtime *Validator* that understands the extended patch payload.
|
|
396
|
+
*
|
|
397
|
+
* * Adds per‑array *patch* wrappers (the `$replace`, `$insert`, … fields).
|
|
398
|
+
* * Honors `mongo.patch.strategy === "merge"` metadata.
|
|
399
|
+
*
|
|
400
|
+
* @param collection Target collection wrapper
|
|
401
|
+
* @returns Atscript Validator
|
|
402
|
+
*/ static prepareValidator(collection) {
|
|
403
|
+
return collection.type.validator({
|
|
404
|
+
plugins: [validateMongoIdPlugin, validateMongoUniqueArrayItemsPlugin],
|
|
405
|
+
replace: (def, path) => {
|
|
406
|
+
if (path === "" && def.type.kind === "object") {
|
|
407
|
+
const obj = (0, __atscript_typescript.defineAnnotatedType)("object").copyMetadata(def.metadata);
|
|
408
|
+
for (const [prop, type] of def.type.props.entries()) obj.prop(prop, (0, __atscript_typescript.defineAnnotatedType)().refTo(type).copyMetadata(type.metadata).optional(prop !== "_id").$type);
|
|
409
|
+
return obj.$type;
|
|
410
|
+
}
|
|
411
|
+
if (def.type.kind === "array" && collection.flatMap.get(path)?.metadata.get("mongo.__topLevelArray") && !def.metadata.has("mongo.__patchArrayValue")) {
|
|
412
|
+
const defArray = def;
|
|
413
|
+
const mergeStrategy = defArray.metadata.get("mongo.patch.strategy") === "merge";
|
|
414
|
+
function getPatchType() {
|
|
415
|
+
const isPrimitive$1 = (0, __atscript_typescript.isAnnotatedTypeOfPrimitive)(defArray.type.of);
|
|
416
|
+
if (isPrimitive$1) return (0, __atscript_typescript.defineAnnotatedType)().refTo(def).copyMetadata(def.metadata).annotate("mongo.__patchArrayValue").optional().$type;
|
|
417
|
+
if (defArray.type.of.type.kind === "object") {
|
|
418
|
+
const objType = defArray.type.of.type;
|
|
419
|
+
const t = (0, __atscript_typescript.defineAnnotatedType)("object").copyMetadata(defArray.type.of.metadata);
|
|
420
|
+
const keyProps = CollectionPatcher.getKeyProps(defArray);
|
|
421
|
+
for (const [key, val] of objType.props.entries()) if (keyProps.size) if (keyProps.has(key)) t.prop(key, (0, __atscript_typescript.defineAnnotatedType)().refTo(val).copyMetadata(def.metadata).$type);
|
|
422
|
+
else t.prop(key, (0, __atscript_typescript.defineAnnotatedType)().refTo(val).copyMetadata(def.metadata).optional().$type);
|
|
423
|
+
else t.prop(key, (0, __atscript_typescript.defineAnnotatedType)().refTo(val).copyMetadata(def.metadata).optional(!!val.optional).$type);
|
|
424
|
+
return (0, __atscript_typescript.defineAnnotatedType)("array").of(t.$type).copyMetadata(def.metadata).annotate("mongo.__patchArrayValue").optional().$type;
|
|
425
|
+
}
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
const fullType = (0, __atscript_typescript.defineAnnotatedType)().refTo(def).copyMetadata(def.metadata).annotate("mongo.__patchArrayValue").optional().$type;
|
|
429
|
+
const patchType = getPatchType();
|
|
430
|
+
return patchType ? (0, __atscript_typescript.defineAnnotatedType)("object").prop("$replace", fullType).prop("$insert", fullType).prop("$upsert", fullType).prop("$update", mergeStrategy ? patchType : fullType).prop("$remove", patchType).optional().$type : (0, __atscript_typescript.defineAnnotatedType)("object").prop("$replace", fullType).prop("$insert", fullType).optional().$type;
|
|
431
|
+
}
|
|
432
|
+
return def;
|
|
433
|
+
},
|
|
434
|
+
partial: (def, path) => {
|
|
435
|
+
return path !== "" && def.metadata.get("mongo.patch.strategy") === "merge";
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Entry point – walk the payload, build `filter`, `update` and `options`.
|
|
441
|
+
*
|
|
442
|
+
* @returns Helper object exposing both individual parts and
|
|
443
|
+
* a `.toArgs()` convenience callback.
|
|
444
|
+
*/ preparePatch() {
|
|
445
|
+
this.filterObj = { _id: this.collection.prepareId(this.payload._id) };
|
|
446
|
+
this.flattenPayload(this.payload);
|
|
447
|
+
let updateFilter = this.updatePipeline;
|
|
448
|
+
return {
|
|
449
|
+
toArgs: () => [
|
|
450
|
+
this.filterObj,
|
|
451
|
+
updateFilter,
|
|
452
|
+
this.optionsObj
|
|
453
|
+
],
|
|
454
|
+
filter: this.filterObj,
|
|
455
|
+
updateFilter,
|
|
456
|
+
updateOptions: this.optionsObj
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Helper – lazily create `$set` section and assign *key* → *value*.
|
|
461
|
+
*
|
|
462
|
+
* @param key Fully‑qualified dotted path
|
|
463
|
+
* @param val Value to be written
|
|
464
|
+
* @private
|
|
465
|
+
*/ _set(key, val) {
|
|
466
|
+
for (const pipe of this.updatePipeline) {
|
|
467
|
+
if (!pipe.$set) pipe.$set = {};
|
|
468
|
+
if (!pipe.$set[key]) {
|
|
469
|
+
pipe.$set[key] = val;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
this.updatePipeline.push({ $set: { [key]: val } });
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Recursively walk through the patch *payload* and convert it into `$set`/…
|
|
477
|
+
* statements. Top‑level arrays are delegated to {@link parseArrayPatch}.
|
|
478
|
+
*
|
|
479
|
+
* @param payload Current payload chunk
|
|
480
|
+
* @param prefix Dotted path accumulated so far
|
|
481
|
+
* @private
|
|
482
|
+
*/ flattenPayload(payload, prefix = "") {
|
|
483
|
+
const evalKey = (k) => prefix ? `${prefix}.${k}` : k;
|
|
484
|
+
for (const [_key, value] of Object.entries(payload)) {
|
|
485
|
+
const key = evalKey(_key);
|
|
486
|
+
const flatType = this.collection.flatMap.get(key);
|
|
487
|
+
const topLevelArray = flatType?.metadata?.get("mongo.__topLevelArray");
|
|
488
|
+
if (typeof value === "object" && topLevelArray) this.parseArrayPatch(key, value);
|
|
489
|
+
else if (typeof value === "object" && this.collection.flatMap.get(key)?.metadata?.get("mongo.patch.strategy") === "merge") this.flattenPayload(value, key);
|
|
490
|
+
else if (key !== "_id") this._set(key, value);
|
|
491
|
+
}
|
|
492
|
+
return this.updatePipeline;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Dispatch a *single* array patch. Exactly one of `$replace`, `$insert`,
|
|
496
|
+
* `$upsert`, `$update`, `$remove` must be present – otherwise we throw.
|
|
497
|
+
*
|
|
498
|
+
* @param key Dotted path to the array field
|
|
499
|
+
* @param value Payload slice for that field
|
|
500
|
+
* @private
|
|
501
|
+
*/ parseArrayPatch(key, value) {
|
|
502
|
+
const flatType = this.collection.flatMap.get(key);
|
|
503
|
+
const toRemove = value.$remove;
|
|
504
|
+
const toReplace = value.$replace;
|
|
505
|
+
const toInsert = value.$insert;
|
|
506
|
+
const toUpsert = value.$upsert;
|
|
507
|
+
const toUpdate = value.$update;
|
|
508
|
+
const keyProps = flatType?.type.kind === "array" ? CollectionPatcher.getKeyProps(flatType) : new Set();
|
|
509
|
+
this._remove(key, toRemove, keyProps);
|
|
510
|
+
this._replace(key, toReplace);
|
|
511
|
+
this._insert(key, toInsert, keyProps);
|
|
512
|
+
this._upsert(key, toUpsert, keyProps);
|
|
513
|
+
this._update(key, toUpdate, keyProps);
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Build an *aggregation‐expression* that checks equality by **all** keys in
|
|
517
|
+
* `keys`. Example output for keys `["id", "lang"]` and bases `a`, `b`:
|
|
518
|
+
* ```json
|
|
519
|
+
* { "$and": [ { "$eq": ["$$a.id", "$$b.id"] }, { "$eq": ["$$a.lang", "$$b.lang"] } ] }
|
|
520
|
+
* ```
|
|
521
|
+
*
|
|
522
|
+
* @param keys Ordered list of key property names
|
|
523
|
+
* @param left Base token for *left* expression (e.g. `"$$el"`)
|
|
524
|
+
* @param right Base token for *right* expression (e.g. `"$$this"`)
|
|
525
|
+
*/ _keysEqual(keys, left, right) {
|
|
526
|
+
return keys.map((k) => ({ $eq: [`${left}.${k}`, `${right}.${k}`] })).reduce((acc, cur) => acc ? { $and: [acc, cur] } : cur);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* `$replace` – overwrite the entire array with `input`.
|
|
530
|
+
*
|
|
531
|
+
* @param key Dotted path to the array
|
|
532
|
+
* @param input New array value (may be `undefined`)
|
|
533
|
+
* @private
|
|
534
|
+
*/ _replace(key, input) {
|
|
535
|
+
if (input) this._set(key, input);
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* `$insert`
|
|
539
|
+
* - plain append → $concatArrays
|
|
540
|
+
* - unique / keyed → delegate to _upsert (insert-or-update)
|
|
541
|
+
*/ _insert(key, input, keyProps) {
|
|
542
|
+
if (!input?.length) return;
|
|
543
|
+
const uniqueItems = this.collection.flatMap.get(key)?.metadata?.has("mongo.array.uniqueItems");
|
|
544
|
+
if (uniqueItems || keyProps.size > 0) this._upsert(key, input, keyProps);
|
|
545
|
+
else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* `$upsert`
|
|
549
|
+
* - keyed → remove existing matching by key(s) then append candidate
|
|
550
|
+
* - unique → $setUnion (deep equality)
|
|
551
|
+
*/ _upsert(key, input, keyProps) {
|
|
552
|
+
if (!input?.length) return;
|
|
553
|
+
if (keyProps.size) {
|
|
554
|
+
const keys = [...keyProps];
|
|
555
|
+
this._set(key, { $reduce: {
|
|
556
|
+
input,
|
|
557
|
+
initialValue: { $ifNull: [`$${key}`, []] },
|
|
558
|
+
in: { $let: {
|
|
559
|
+
vars: {
|
|
560
|
+
acc: "$$value",
|
|
561
|
+
cand: "$$this"
|
|
562
|
+
},
|
|
563
|
+
in: { $concatArrays: [{ $filter: {
|
|
564
|
+
input: "$$acc",
|
|
565
|
+
as: "el",
|
|
566
|
+
cond: { $not: this._keysEqual(keys, "$$el", "$$cand") }
|
|
567
|
+
} }, ["$$cand"]] }
|
|
568
|
+
} }
|
|
569
|
+
} });
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
this._set(key, { $setUnion: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* `$update`
|
|
576
|
+
* - keyed → map array and merge / replace matching element(s)
|
|
577
|
+
* - non-keyed → behave like `$addToSet` (insert only when not present)
|
|
578
|
+
*/ _update(key, input, keyProps) {
|
|
579
|
+
if (!input?.length) return;
|
|
580
|
+
if (keyProps.size) {
|
|
581
|
+
const mergeStrategy = this.collection.flatMap.get(key)?.metadata?.get("mongo.patch.strategy") === "merge";
|
|
582
|
+
const keys = [...keyProps];
|
|
583
|
+
this._set(key, { $reduce: {
|
|
584
|
+
input,
|
|
585
|
+
initialValue: { $ifNull: [`$${key}`, []] },
|
|
586
|
+
in: { $map: {
|
|
587
|
+
input: "$$value",
|
|
588
|
+
as: "el",
|
|
589
|
+
in: { $cond: [
|
|
590
|
+
this._keysEqual(keys, "$$el", "$$this"),
|
|
591
|
+
mergeStrategy ? { $mergeObjects: ["$$el", "$$this"] } : "$$this",
|
|
592
|
+
"$$el"
|
|
593
|
+
] }
|
|
594
|
+
} }
|
|
595
|
+
} });
|
|
596
|
+
} else this._set(key, { $setUnion: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* `$remove`
|
|
600
|
+
* - keyed → filter out any element whose key set matches a payload item
|
|
601
|
+
* - non-keyed → deep equality remove (`$setDifference`)
|
|
602
|
+
*/ _remove(key, input, keyProps) {
|
|
603
|
+
if (!input?.length) return;
|
|
604
|
+
if (keyProps.size) {
|
|
605
|
+
const keys = [...keyProps];
|
|
606
|
+
this._set(key, { $let: {
|
|
607
|
+
vars: { rem: input },
|
|
608
|
+
in: { $filter: {
|
|
609
|
+
input: { $ifNull: [`$${key}`, []] },
|
|
610
|
+
as: "el",
|
|
611
|
+
cond: { $not: { $anyElementTrue: { $map: {
|
|
612
|
+
input: "$$rem",
|
|
613
|
+
as: "r",
|
|
614
|
+
in: this._keysEqual(keys, "$$el", "$$r")
|
|
615
|
+
} } } }
|
|
616
|
+
} }
|
|
617
|
+
} });
|
|
618
|
+
} else this._set(key, { $setDifference: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
619
|
+
}
|
|
620
|
+
constructor(collection, payload) {
|
|
621
|
+
_define_property$2(this, "collection", void 0);
|
|
622
|
+
_define_property$2(this, "payload", void 0);
|
|
623
|
+
/**
|
|
624
|
+
* Internal accumulator: filter passed to `updateOne()`.
|
|
625
|
+
* Filled only with the `_id` field right now.
|
|
626
|
+
*/ _define_property$2(this, "filterObj", void 0);
|
|
627
|
+
/** MongoDB *update* document being built. */ _define_property$2(this, "updatePipeline", void 0);
|
|
628
|
+
/** Additional *options* (mainly `arrayFilters`). */ _define_property$2(this, "optionsObj", void 0);
|
|
629
|
+
this.collection = collection;
|
|
630
|
+
this.payload = payload;
|
|
631
|
+
this.filterObj = {};
|
|
632
|
+
this.updatePipeline = [];
|
|
633
|
+
this.optionsObj = {};
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
280
637
|
//#endregion
|
|
281
638
|
//#region packages/mongo/src/lib/as-collection.ts
|
|
282
639
|
function _define_property$1(obj, key, value) {
|
|
@@ -289,9 +646,14 @@ function _define_property$1(obj, key, value) {
|
|
|
289
646
|
else obj[key] = value;
|
|
290
647
|
return obj;
|
|
291
648
|
}
|
|
292
|
-
const INDEX_PREFIX = "
|
|
649
|
+
const INDEX_PREFIX = "atscript__";
|
|
293
650
|
const DEFAULT_INDEX_NAME = "DEFAULT";
|
|
294
|
-
|
|
651
|
+
/**
|
|
652
|
+
* Generates a key for mongo index
|
|
653
|
+
* @param type index type
|
|
654
|
+
* @param name index name
|
|
655
|
+
* @returns index key
|
|
656
|
+
*/ function indexKey(type, name) {
|
|
295
657
|
const cleanName = name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 127 - INDEX_PREFIX.length - type.length - 2);
|
|
296
658
|
return `${INDEX_PREFIX}${type}__${cleanName}`;
|
|
297
659
|
}
|
|
@@ -303,6 +665,64 @@ var AsCollection = class {
|
|
|
303
665
|
const exists = await this.exists();
|
|
304
666
|
if (!exists) await this.asMongo.db.createCollection(this.name, { comment: "Created by Atscript Mongo Collection" });
|
|
305
667
|
}
|
|
668
|
+
/**
|
|
669
|
+
* Returns the a type definition of the "_id" prop.
|
|
670
|
+
*/ get idType() {
|
|
671
|
+
const idProp = this.type.type.props.get("_id");
|
|
672
|
+
const idTags = idProp?.type.tags;
|
|
673
|
+
if (idTags?.has("objectId") && idTags?.has("mongo")) return "objectId";
|
|
674
|
+
if (idProp?.type.kind === "") return idProp.type.designType;
|
|
675
|
+
return "objectId";
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Transforms an "_id" value to the expected type (`ObjectId`, `number`, or `string`).
|
|
679
|
+
* Assumes input has already been validated.
|
|
680
|
+
*
|
|
681
|
+
* @param {string | number | ObjectId} id - The validated ID.
|
|
682
|
+
* @returns {string | number | ObjectId} - The transformed ID.
|
|
683
|
+
* @throws {Error} If the `_id` type is unknown.
|
|
684
|
+
*/ prepareId(id) {
|
|
685
|
+
switch (this.idType) {
|
|
686
|
+
case "objectId": return id instanceof mongodb.ObjectId ? id : new mongodb.ObjectId(id);
|
|
687
|
+
case "number": return Number(id);
|
|
688
|
+
case "string": return String(id);
|
|
689
|
+
default: throw new Error("Unknown \"_id\" type");
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Retrieves a validator for a given purpose. If the validator is not already cached,
|
|
694
|
+
* it creates and stores a new one based on the purpose.
|
|
695
|
+
*
|
|
696
|
+
* @param {TValidatorPurpose} purpose - The validation purpose (`input`, `update`, `patch`).
|
|
697
|
+
* @returns {Validator} The corresponding validator instance.
|
|
698
|
+
* @throws {Error} If an unknown purpose is provided.
|
|
699
|
+
*/ getValidator(purpose) {
|
|
700
|
+
if (!this.validators.has(purpose)) switch (purpose) {
|
|
701
|
+
case "insert": {
|
|
702
|
+
this.validators.set(purpose, this.type.validator({
|
|
703
|
+
plugins: [validateMongoIdPlugin, validateMongoUniqueArrayItemsPlugin],
|
|
704
|
+
replace(type, path) {
|
|
705
|
+
if (path === "_id" && type.type.tags.has("objectId")) return {
|
|
706
|
+
...type,
|
|
707
|
+
optional: true
|
|
708
|
+
};
|
|
709
|
+
return type;
|
|
710
|
+
}
|
|
711
|
+
}));
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
case "update": {
|
|
715
|
+
this.validators.set(purpose, this.type.validator({ plugins: [validateMongoIdPlugin] }));
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
case "patch": {
|
|
719
|
+
this.validators.set(purpose, CollectionPatcher.prepareValidator(this));
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
default: throw new Error(`Unknown validator purpose: ${purpose}`);
|
|
723
|
+
}
|
|
724
|
+
return this.validators.get(purpose);
|
|
725
|
+
}
|
|
306
726
|
get type() {
|
|
307
727
|
return this._type;
|
|
308
728
|
}
|
|
@@ -347,18 +767,24 @@ else {
|
|
|
347
767
|
if (analyzer) index.definition.mappings.fields[fieldName].analyzer = analyzer;
|
|
348
768
|
}
|
|
349
769
|
}
|
|
350
|
-
_flattenType(type, prefix) {
|
|
770
|
+
_flattenType(type, prefix = "", inComplexTypeOrArray = false) {
|
|
351
771
|
switch (type.type.kind) {
|
|
352
772
|
case "object":
|
|
773
|
+
this._flatMap?.set(prefix || "", type);
|
|
353
774
|
const items = Array.from(type.type.props.entries());
|
|
354
|
-
for (const [key, value] of items) this._flattenType(value, prefix ? `${prefix}.${key}` : key);
|
|
775
|
+
for (const [key, value] of items) this._flattenType(value, prefix ? `${prefix}.${key}` : key, inComplexTypeOrArray);
|
|
355
776
|
break;
|
|
356
777
|
case "array":
|
|
357
|
-
|
|
778
|
+
let typeArray = type;
|
|
779
|
+
if (!inComplexTypeOrArray) {
|
|
780
|
+
typeArray = (0, __atscript_typescript.defineAnnotatedType)().refTo(type).copyMetadata(type.metadata).$type;
|
|
781
|
+
typeArray.metadata.set("mongo.__topLevelArray", true);
|
|
782
|
+
}
|
|
783
|
+
this._flatMap?.set(prefix || "", typeArray);
|
|
358
784
|
break;
|
|
359
785
|
case "intersection":
|
|
360
786
|
case "tuple":
|
|
361
|
-
case "union": for (const item of type.type.items) this._flattenType(item, prefix);
|
|
787
|
+
case "union": for (const item of type.type.items) this._flattenType(item, prefix, true);
|
|
362
788
|
default:
|
|
363
789
|
this._flatMap?.set(prefix || "", type);
|
|
364
790
|
break;
|
|
@@ -379,6 +805,9 @@ else {
|
|
|
379
805
|
text: { fuzzy: { maxEdits: textSearch.fuzzy || 0 } }
|
|
380
806
|
});
|
|
381
807
|
}
|
|
808
|
+
get uniqueProps() {
|
|
809
|
+
return this._uniqueProps;
|
|
810
|
+
}
|
|
382
811
|
_finalizeIndexesForCollection() {
|
|
383
812
|
for (const [key, value] of Array.from(this._vectorFilters.entries())) {
|
|
384
813
|
const index = this._indexes.get(key);
|
|
@@ -387,6 +816,10 @@ else {
|
|
|
387
816
|
path: value
|
|
388
817
|
});
|
|
389
818
|
}
|
|
819
|
+
for (const [, value] of Array.from(this._indexes.entries())) if (value.type === "unique") {
|
|
820
|
+
const keys = Object.keys(value.fields);
|
|
821
|
+
if (keys.length === 1) this._uniqueProps.add(keys[0]);
|
|
822
|
+
}
|
|
390
823
|
}
|
|
391
824
|
_prepareIndexesForField(fieldName, metadata) {
|
|
392
825
|
for (const index of metadata.get("mongo.index.plain") || []) this._addIndexField("plain", index === true ? fieldName : index, fieldName);
|
|
@@ -502,20 +935,80 @@ else toUpdate.add(remote.name);
|
|
|
502
935
|
default:
|
|
503
936
|
}
|
|
504
937
|
}
|
|
938
|
+
insert(payload, options) {
|
|
939
|
+
const toInsert = this.prepareInsert(payload);
|
|
940
|
+
return Array.isArray(toInsert) ? this.collection.insertMany(toInsert, options) : this.collection.insertOne(toInsert, options);
|
|
941
|
+
}
|
|
942
|
+
replace(payload, options) {
|
|
943
|
+
const [filter, replace, opts] = this.prepareReplace(payload).toArgs();
|
|
944
|
+
return this.collection.replaceOne(filter, replace, {
|
|
945
|
+
...opts,
|
|
946
|
+
...options
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
update(payload, options) {
|
|
950
|
+
const [filter, update, opts] = this.prepareUpdate(payload).toArgs();
|
|
951
|
+
return this.collection.updateOne(filter, update, {
|
|
952
|
+
...opts,
|
|
953
|
+
...options
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
prepareInsert(payload) {
|
|
957
|
+
const v = this.getValidator("insert");
|
|
958
|
+
const arr = Array.isArray(payload) ? payload : [payload];
|
|
959
|
+
const prepared = [];
|
|
960
|
+
for (const item of arr) if (v.validate(item)) {
|
|
961
|
+
const data = { ...item };
|
|
962
|
+
if (data._id) data._id = this.prepareId(data._id);
|
|
963
|
+
else if (this.idType !== "objectId") throw new Error("Missing \"_id\" field");
|
|
964
|
+
prepared.push(data);
|
|
965
|
+
} else throw new Error("Invalid payload");
|
|
966
|
+
return prepared.length === 1 ? prepared[0] : prepared;
|
|
967
|
+
}
|
|
968
|
+
prepareReplace(payload) {
|
|
969
|
+
const v = this.getValidator("update");
|
|
970
|
+
if (v.validate(payload)) {
|
|
971
|
+
const _id = this.prepareId(payload._id);
|
|
972
|
+
const data = {
|
|
973
|
+
...payload,
|
|
974
|
+
_id
|
|
975
|
+
};
|
|
976
|
+
return {
|
|
977
|
+
toArgs: () => [
|
|
978
|
+
{ _id },
|
|
979
|
+
data,
|
|
980
|
+
{}
|
|
981
|
+
],
|
|
982
|
+
filter: { _id },
|
|
983
|
+
updateFilter: data,
|
|
984
|
+
updateOptions: {}
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
throw new Error("Invalid payload");
|
|
988
|
+
}
|
|
989
|
+
prepareUpdate(payload) {
|
|
990
|
+
const v = this.getValidator("patch");
|
|
991
|
+
if (v.validate(payload)) return new CollectionPatcher(this, payload).preparePatch();
|
|
992
|
+
throw new Error("Invalid payload");
|
|
993
|
+
}
|
|
505
994
|
constructor(asMongo, _type, logger = NoopLogger) {
|
|
506
995
|
_define_property$1(this, "asMongo", void 0);
|
|
507
996
|
_define_property$1(this, "_type", void 0);
|
|
508
997
|
_define_property$1(this, "logger", void 0);
|
|
509
998
|
_define_property$1(this, "name", void 0);
|
|
510
999
|
_define_property$1(this, "collection", void 0);
|
|
1000
|
+
_define_property$1(this, "validators", void 0);
|
|
511
1001
|
_define_property$1(this, "_indexes", void 0);
|
|
512
1002
|
_define_property$1(this, "_vectorFilters", void 0);
|
|
513
1003
|
_define_property$1(this, "_flatMap", void 0);
|
|
1004
|
+
_define_property$1(this, "_uniqueProps", void 0);
|
|
514
1005
|
this.asMongo = asMongo;
|
|
515
1006
|
this._type = _type;
|
|
516
1007
|
this.logger = logger;
|
|
1008
|
+
this.validators = new Map();
|
|
517
1009
|
this._indexes = new Map();
|
|
518
1010
|
this._vectorFilters = new Map();
|
|
1011
|
+
this._uniqueProps = new Set();
|
|
519
1012
|
if (!(0, __atscript_typescript.isAnnotatedType)(_type)) throw new Error("Atscript Annotated Type expected");
|
|
520
1013
|
const name = _type.metadata.get("mongo.collection");
|
|
521
1014
|
if (!name) throw new Error("@mongo.collection annotation expected with collection name");
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TAtscriptPlugin } from '@atscript/core';
|
|
2
|
-
import { TAtscriptAnnotatedTypeConstructor, TAtscriptAnnotatedType, TAtscriptTypeObject, TMetadataMap } from '@atscript/typescript';
|
|
3
2
|
import * as mongodb from 'mongodb';
|
|
4
|
-
import { MongoClient, Collection } from 'mongodb';
|
|
3
|
+
import { MongoClient, Collection, ObjectId, InsertOneOptions, ReplaceOptions, UpdateOptions, OptionalUnlessRequiredId, Filter, WithoutId } from 'mongodb';
|
|
4
|
+
import { TAtscriptAnnotatedTypeConstructor, TAtscriptAnnotatedType, TAtscriptTypeObject, TMetadataMap } from '@atscript/typescript';
|
|
5
5
|
|
|
6
6
|
declare const MongoPlugin: () => TAtscriptPlugin;
|
|
7
7
|
|
|
@@ -34,30 +34,88 @@ type TSearchIndex = {
|
|
|
34
34
|
definition: TMongoSearchIndexDefinition;
|
|
35
35
|
};
|
|
36
36
|
type TIndex = TPlainIndex | TSearchIndex;
|
|
37
|
+
type TValidatorPurpose = 'insert' | 'update' | 'patch';
|
|
37
38
|
declare class AsCollection<T extends TAtscriptAnnotatedTypeConstructor> {
|
|
38
39
|
protected readonly asMongo: AsMongo;
|
|
39
40
|
protected readonly _type: T;
|
|
40
41
|
protected readonly logger: TGenericLogger;
|
|
41
42
|
readonly name: string;
|
|
42
43
|
readonly collection: Collection<InstanceType<T>>;
|
|
44
|
+
protected readonly validators: Map<TValidatorPurpose, Validator<T>>;
|
|
45
|
+
protected _indexes: Map<string, TIndex>;
|
|
46
|
+
protected _vectorFilters: Map<string, string>;
|
|
47
|
+
protected _flatMap?: Map<string, TAtscriptAnnotatedType>;
|
|
43
48
|
constructor(asMongo: AsMongo, _type: T, logger?: TGenericLogger);
|
|
44
49
|
exists(): Promise<boolean>;
|
|
45
50
|
ensureExists(): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Returns the a type definition of the "_id" prop.
|
|
53
|
+
*/
|
|
54
|
+
get idType(): 'string' | 'number' | 'objectId';
|
|
55
|
+
/**
|
|
56
|
+
* Transforms an "_id" value to the expected type (`ObjectId`, `number`, or `string`).
|
|
57
|
+
* Assumes input has already been validated.
|
|
58
|
+
*
|
|
59
|
+
* @param {string | number | ObjectId} id - The validated ID.
|
|
60
|
+
* @returns {string | number | ObjectId} - The transformed ID.
|
|
61
|
+
* @throws {Error} If the `_id` type is unknown.
|
|
62
|
+
*/
|
|
63
|
+
prepareId<D = string | number | ObjectId>(id: string | number | ObjectId): D;
|
|
64
|
+
/**
|
|
65
|
+
* Retrieves a validator for a given purpose. If the validator is not already cached,
|
|
66
|
+
* it creates and stores a new one based on the purpose.
|
|
67
|
+
*
|
|
68
|
+
* @param {TValidatorPurpose} purpose - The validation purpose (`input`, `update`, `patch`).
|
|
69
|
+
* @returns {Validator} The corresponding validator instance.
|
|
70
|
+
* @throws {Error} If an unknown purpose is provided.
|
|
71
|
+
*/
|
|
72
|
+
getValidator(purpose: TValidatorPurpose): any;
|
|
46
73
|
get type(): TAtscriptAnnotatedType<TAtscriptTypeObject>;
|
|
47
|
-
protected _indexes: Map<string, TIndex>;
|
|
48
|
-
protected _vectorFilters: Map<string, string>;
|
|
49
74
|
get indexes(): Map<string, TIndex>;
|
|
50
75
|
protected _addIndexField(type: TPlainIndex['type'], name: string, field: string, weight?: number): void;
|
|
51
76
|
protected _setSearchIndex(type: TSearchIndex['type'], name: string | undefined, definition: TMongoSearchIndexDefinition): void;
|
|
52
77
|
protected _addFieldToSearchIndex(type: TSearchIndex['type'], _name: string | undefined, fieldName: string, analyzer?: string): void;
|
|
53
|
-
protected
|
|
54
|
-
protected _flattenType(type: TAtscriptAnnotatedType, prefix?: string): void;
|
|
78
|
+
protected _flattenType(type: TAtscriptAnnotatedType, prefix?: string, inComplexTypeOrArray?: boolean): void;
|
|
55
79
|
protected _prepareIndexesForCollection(): void;
|
|
80
|
+
protected _uniqueProps: Set<string>;
|
|
81
|
+
get uniqueProps(): Set<string>;
|
|
56
82
|
protected _finalizeIndexesForCollection(): void;
|
|
57
83
|
protected _prepareIndexesForField(fieldName: string, metadata: TMetadataMap<AtscriptMetadata>): void;
|
|
58
84
|
protected _flatten(): void;
|
|
59
|
-
get flatMap(): Map<string, TAtscriptAnnotatedType
|
|
85
|
+
get flatMap(): Map<string, TAtscriptAnnotatedType>;
|
|
60
86
|
syncIndexes(): Promise<void>;
|
|
87
|
+
insert(payload: (Omit<InstanceType<T>, '_id'> & {
|
|
88
|
+
_id?: InstanceType<T>['_id'] | ObjectId;
|
|
89
|
+
}) | (Omit<InstanceType<T>, '_id'> & {
|
|
90
|
+
_id?: InstanceType<T>['_id'] | ObjectId;
|
|
91
|
+
})[], options?: InsertOneOptions): Promise<mongodb.InsertManyResult<InstanceType<T>>> | Promise<mongodb.InsertOneResult<InstanceType<T>>>;
|
|
92
|
+
replace(payload: Omit<InstanceType<T>, '_id'> & {
|
|
93
|
+
_id: InstanceType<T>['_id'] | ObjectId;
|
|
94
|
+
}, options?: ReplaceOptions): Promise<mongodb.UpdateResult<InstanceType<T>>>;
|
|
95
|
+
update(payload: AsMongoPatch<Omit<InstanceType<T>, '_id'>> & {
|
|
96
|
+
_id: InstanceType<T>['_id'] | ObjectId;
|
|
97
|
+
}, options?: UpdateOptions): Promise<mongodb.UpdateResult<InstanceType<T>>>;
|
|
98
|
+
prepareInsert(payload: (Omit<InstanceType<T>, '_id'> & {
|
|
99
|
+
_id?: InstanceType<T>['_id'] | ObjectId;
|
|
100
|
+
}) | (Omit<InstanceType<T>, '_id'> & {
|
|
101
|
+
_id?: InstanceType<T>['_id'] | ObjectId;
|
|
102
|
+
})[]): OptionalUnlessRequiredId<InstanceType<T>> | OptionalUnlessRequiredId<InstanceType<T>>[];
|
|
103
|
+
prepareReplace(payload: Omit<InstanceType<T>, '_id'> & {
|
|
104
|
+
_id: InstanceType<T>['_id'] | ObjectId;
|
|
105
|
+
}): {
|
|
106
|
+
toArgs: () => [Filter<InstanceType<T>>, WithoutId<InstanceType<T>>, ReplaceOptions];
|
|
107
|
+
filter: Filter<InstanceType<T>>;
|
|
108
|
+
updateFilter: WithoutId<InstanceType<T>>;
|
|
109
|
+
updateOptions: ReplaceOptions;
|
|
110
|
+
};
|
|
111
|
+
prepareUpdate(payload: AsMongoPatch<Omit<InstanceType<T>, '_id'>> & {
|
|
112
|
+
_id: InstanceType<T>['_id'] | ObjectId;
|
|
113
|
+
}): {
|
|
114
|
+
toArgs: () => [Filter<InstanceType<T>>, mongodb.Document[] | mongodb.UpdateFilter<InstanceType<T>>, UpdateOptions];
|
|
115
|
+
filter: Filter<InstanceType<T>>;
|
|
116
|
+
updateFilter: mongodb.Document[];
|
|
117
|
+
updateOptions: UpdateOptions;
|
|
118
|
+
};
|
|
61
119
|
}
|
|
62
120
|
type TVectorSimilarity = 'cosine' | 'euclidean' | 'dotProduct';
|
|
63
121
|
type TMongoSearchIndexDefinition = {
|
|
@@ -81,5 +139,26 @@ type TMongoSearchIndexDefinition = {
|
|
|
81
139
|
};
|
|
82
140
|
};
|
|
83
141
|
};
|
|
142
|
+
type TArrayPatch<A extends readonly unknown[]> = {
|
|
143
|
+
$replace?: A;
|
|
144
|
+
$insert?: A;
|
|
145
|
+
$upsert?: A;
|
|
146
|
+
$update?: Partial<TArrayElement<A>>[];
|
|
147
|
+
$remove?: Partial<TArrayElement<A>>[];
|
|
148
|
+
};
|
|
149
|
+
type TArrayElement<ArrayType extends readonly unknown[]> = ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
|
|
150
|
+
/**
|
|
151
|
+
* AsMongoPatch<T>
|
|
152
|
+
* ─────────────────
|
|
153
|
+
* - For every key K in T:
|
|
154
|
+
* • if T[K] is `X[]`, rewrite it to `TArrayPatch<X[]>`
|
|
155
|
+
* • otherwise omit the key (feel free to keep it if you want)
|
|
156
|
+
*
|
|
157
|
+
* The result is an *optional* property bag that matches a patch payload
|
|
158
|
+
* for array fields only.
|
|
159
|
+
*/
|
|
160
|
+
type AsMongoPatch<T> = {
|
|
161
|
+
[K in keyof T]?: T[K] extends Array<infer _> ? TArrayPatch<T[K]> : Partial<T[K]>;
|
|
162
|
+
};
|
|
84
163
|
|
|
85
164
|
export { AsCollection, AsMongo, MongoPlugin };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AnnotationSpec, isInterface, isPrimitive, isRef, isStructure } from "@atscript/core";
|
|
2
|
-
import { isAnnotatedType } from "@atscript/typescript";
|
|
3
|
-
import { MongoClient } from "mongodb";
|
|
1
|
+
import { AnnotationSpec, isArray, isInterface, isPrimitive, isRef, isStructure } from "@atscript/core";
|
|
2
|
+
import { defineAnnotatedType, isAnnotatedType, isAnnotatedTypeOfPrimitive } from "@atscript/typescript";
|
|
3
|
+
import { MongoClient, ObjectId } from "mongodb";
|
|
4
4
|
|
|
5
5
|
//#region packages/mongo/src/plugin/primitives.ts
|
|
6
6
|
const primitives = { mongo: { extensions: {
|
|
@@ -82,6 +82,15 @@ const annotations = { mongo: {
|
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
84
|
}),
|
|
85
|
+
autoIndexes: new AnnotationSpec({
|
|
86
|
+
description: "Switch on/off the automatic index creation. Works with as-mongo moost controller.\n\nDefault: true",
|
|
87
|
+
nodeType: ["interface"],
|
|
88
|
+
argument: {
|
|
89
|
+
name: "type",
|
|
90
|
+
type: "boolean",
|
|
91
|
+
description: "On/Off the automatic index creation"
|
|
92
|
+
}
|
|
93
|
+
}),
|
|
85
94
|
index: {
|
|
86
95
|
plain: new AnnotationSpec({
|
|
87
96
|
description: "Defines a **standard MongoDB index** on a field.\n\n- Improves query performance on indexed fields.\n- Can be used for **single-field** or **compound** indexes.\n\n**Example:**\n```atscript\n@mongo.index.plain \"departmentIndex\"\ndepartment: string\n```\n",
|
|
@@ -226,7 +235,57 @@ const annotations = { mongo: {
|
|
|
226
235
|
description: "The **name of the vector search index** this field should be used as a filter for."
|
|
227
236
|
}]
|
|
228
237
|
})
|
|
229
|
-
}
|
|
238
|
+
},
|
|
239
|
+
patch: { strategy: new AnnotationSpec({
|
|
240
|
+
description: "Defines the **patching strategy** for updating MongoDB documents.\n\n- **\"replace\"** → The field or object will be **fully replaced**.\n- **\"merge\"** → The field or object will be **merged recursively** (applies only to objects, not arrays).\n\n**Example:**\n```atscript\n@mongo.patch.strategy \"merge\"\nsettings: {\n notifications: boolean\n preferences: {\n theme: string\n }\n}\n```\n",
|
|
241
|
+
nodeType: ["prop"],
|
|
242
|
+
multiple: false,
|
|
243
|
+
argument: {
|
|
244
|
+
name: "strategy",
|
|
245
|
+
type: "string",
|
|
246
|
+
description: "The **patch strategy** for this field: `\"replace\"` (default) or `\"merge\"`.",
|
|
247
|
+
values: ["replace", "merge"]
|
|
248
|
+
},
|
|
249
|
+
validate(token, args, doc) {
|
|
250
|
+
const field = token.parentNode;
|
|
251
|
+
const errors = [];
|
|
252
|
+
const definition = field.getDefinition();
|
|
253
|
+
if (!definition) return errors;
|
|
254
|
+
let wrongType = false;
|
|
255
|
+
if (isRef(definition)) {
|
|
256
|
+
const def = doc.unwindType(definition.id, definition.chain)?.def;
|
|
257
|
+
if (!isStructure(def) && !isInterface(def) && !isArray(def)) wrongType = true;
|
|
258
|
+
} else if (!isStructure(definition) && !isInterface(definition) && !isArray(definition)) wrongType = true;
|
|
259
|
+
if (wrongType) errors.push({
|
|
260
|
+
message: `[mongo] type of object or array expected when using @mongo.patch.strategy`,
|
|
261
|
+
severity: 1,
|
|
262
|
+
range: token.range
|
|
263
|
+
});
|
|
264
|
+
return errors;
|
|
265
|
+
}
|
|
266
|
+
}) },
|
|
267
|
+
array: { uniqueItems: new AnnotationSpec({
|
|
268
|
+
description: "Marks an **array field** as containing *globally unique items* when handling **patch `$insert` operations**.\n\n- Forces the patcher to use **set-semantics** (`$setUnion`) instead of a plain append, so duplicates are silently skipped.\n- Has **no effect** on `$replace`, `$update`, or `$remove`.\n- If the array’s element type already defines one or more `@meta.isKey` properties, *uniqueness is implied* and this annotation is unnecessary (but harmless).\n\n**Example:**\n```atscript\n@mongo.array.uniqueItems\ntags: string[]\n\n// Later in a patch payload …\n{\n $insert: {\n tags: [\"mongo\", \"mongo\"] // second \"mongo\" is ignored\n }\n}\n```\n",
|
|
269
|
+
nodeType: ["prop"],
|
|
270
|
+
multiple: false,
|
|
271
|
+
validate(token, args, doc) {
|
|
272
|
+
const field = token.parentNode;
|
|
273
|
+
const errors = [];
|
|
274
|
+
const definition = field.getDefinition();
|
|
275
|
+
if (!definition) return errors;
|
|
276
|
+
let wrongType = false;
|
|
277
|
+
if (isRef(definition)) {
|
|
278
|
+
const def = doc.unwindType(definition.id, definition.chain)?.def;
|
|
279
|
+
if (!isArray(def)) wrongType = true;
|
|
280
|
+
} else if (!isArray(definition)) wrongType = true;
|
|
281
|
+
if (wrongType) errors.push({
|
|
282
|
+
message: `[mongo] type of array expected when using @mongo.array.uniqueItems`,
|
|
283
|
+
severity: 1,
|
|
284
|
+
range: token.range
|
|
285
|
+
});
|
|
286
|
+
return errors;
|
|
287
|
+
}
|
|
288
|
+
}) }
|
|
230
289
|
} };
|
|
231
290
|
|
|
232
291
|
//#endregion
|
|
@@ -253,6 +312,304 @@ const NoopLogger = {
|
|
|
253
312
|
debug: () => {}
|
|
254
313
|
};
|
|
255
314
|
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region packages/mongo/src/lib/validate-plugins.ts
|
|
317
|
+
const validateMongoIdPlugin = (ctx, def, value) => {
|
|
318
|
+
if (ctx.path === "_id" && def.type.tags.has("objectId")) return ctx.validateAnnotatedType(def, value instanceof ObjectId ? value.toString() : value);
|
|
319
|
+
};
|
|
320
|
+
const validateMongoUniqueArrayItemsPlugin = (ctx, def, value) => {
|
|
321
|
+
if (def.metadata.has("mongo.array.uniqueItems") && def.type.kind === "array") {
|
|
322
|
+
if (Array.isArray(value)) {
|
|
323
|
+
const separator = "▼↩";
|
|
324
|
+
const seen = new Set();
|
|
325
|
+
const keyProps = CollectionPatcher.getKeyProps(def);
|
|
326
|
+
for (const item of value) {
|
|
327
|
+
let key = "";
|
|
328
|
+
if (keyProps.size) for (const prop of keyProps) key += JSON.stringify(item[prop]) + separator;
|
|
329
|
+
else key = JSON.stringify(item);
|
|
330
|
+
if (seen.has(key)) {
|
|
331
|
+
ctx.error(`Duplicate items are not allowed`);
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
seen.add(key);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return undefined;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region packages/mongo/src/lib/collection-patcher.ts
|
|
343
|
+
function _define_property$2(obj, key, value) {
|
|
344
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
345
|
+
value,
|
|
346
|
+
enumerable: true,
|
|
347
|
+
configurable: true,
|
|
348
|
+
writable: true
|
|
349
|
+
});
|
|
350
|
+
else obj[key] = value;
|
|
351
|
+
return obj;
|
|
352
|
+
}
|
|
353
|
+
var CollectionPatcher = class CollectionPatcher {
|
|
354
|
+
/**
|
|
355
|
+
* Extract a set of *key properties* (annotated with `@meta.isKey`) from an
|
|
356
|
+
* array‐of‐objects type definition. These keys uniquely identify an element
|
|
357
|
+
* inside the array and are later used for `$update`, `$remove` and `$upsert`.
|
|
358
|
+
*
|
|
359
|
+
* @param def Atscript array type
|
|
360
|
+
* @returns Set of property names marked as keys; empty set if none
|
|
361
|
+
*/ static getKeyProps(def) {
|
|
362
|
+
if (def.type.of.type.kind === "object") {
|
|
363
|
+
const objType = def.type.of.type;
|
|
364
|
+
const keyProps = new Set();
|
|
365
|
+
for (const [key, val] of objType.props.entries()) if (val.metadata.get("meta.isKey")) keyProps.add(key);
|
|
366
|
+
return keyProps;
|
|
367
|
+
}
|
|
368
|
+
return new Set();
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Build a runtime *Validator* that understands the extended patch payload.
|
|
372
|
+
*
|
|
373
|
+
* * Adds per‑array *patch* wrappers (the `$replace`, `$insert`, … fields).
|
|
374
|
+
* * Honors `mongo.patch.strategy === "merge"` metadata.
|
|
375
|
+
*
|
|
376
|
+
* @param collection Target collection wrapper
|
|
377
|
+
* @returns Atscript Validator
|
|
378
|
+
*/ static prepareValidator(collection) {
|
|
379
|
+
return collection.type.validator({
|
|
380
|
+
plugins: [validateMongoIdPlugin, validateMongoUniqueArrayItemsPlugin],
|
|
381
|
+
replace: (def, path) => {
|
|
382
|
+
if (path === "" && def.type.kind === "object") {
|
|
383
|
+
const obj = defineAnnotatedType("object").copyMetadata(def.metadata);
|
|
384
|
+
for (const [prop, type] of def.type.props.entries()) obj.prop(prop, defineAnnotatedType().refTo(type).copyMetadata(type.metadata).optional(prop !== "_id").$type);
|
|
385
|
+
return obj.$type;
|
|
386
|
+
}
|
|
387
|
+
if (def.type.kind === "array" && collection.flatMap.get(path)?.metadata.get("mongo.__topLevelArray") && !def.metadata.has("mongo.__patchArrayValue")) {
|
|
388
|
+
const defArray = def;
|
|
389
|
+
const mergeStrategy = defArray.metadata.get("mongo.patch.strategy") === "merge";
|
|
390
|
+
function getPatchType() {
|
|
391
|
+
const isPrimitive$1 = isAnnotatedTypeOfPrimitive(defArray.type.of);
|
|
392
|
+
if (isPrimitive$1) return defineAnnotatedType().refTo(def).copyMetadata(def.metadata).annotate("mongo.__patchArrayValue").optional().$type;
|
|
393
|
+
if (defArray.type.of.type.kind === "object") {
|
|
394
|
+
const objType = defArray.type.of.type;
|
|
395
|
+
const t = defineAnnotatedType("object").copyMetadata(defArray.type.of.metadata);
|
|
396
|
+
const keyProps = CollectionPatcher.getKeyProps(defArray);
|
|
397
|
+
for (const [key, val] of objType.props.entries()) if (keyProps.size) if (keyProps.has(key)) t.prop(key, defineAnnotatedType().refTo(val).copyMetadata(def.metadata).$type);
|
|
398
|
+
else t.prop(key, defineAnnotatedType().refTo(val).copyMetadata(def.metadata).optional().$type);
|
|
399
|
+
else t.prop(key, defineAnnotatedType().refTo(val).copyMetadata(def.metadata).optional(!!val.optional).$type);
|
|
400
|
+
return defineAnnotatedType("array").of(t.$type).copyMetadata(def.metadata).annotate("mongo.__patchArrayValue").optional().$type;
|
|
401
|
+
}
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
const fullType = defineAnnotatedType().refTo(def).copyMetadata(def.metadata).annotate("mongo.__patchArrayValue").optional().$type;
|
|
405
|
+
const patchType = getPatchType();
|
|
406
|
+
return patchType ? defineAnnotatedType("object").prop("$replace", fullType).prop("$insert", fullType).prop("$upsert", fullType).prop("$update", mergeStrategy ? patchType : fullType).prop("$remove", patchType).optional().$type : defineAnnotatedType("object").prop("$replace", fullType).prop("$insert", fullType).optional().$type;
|
|
407
|
+
}
|
|
408
|
+
return def;
|
|
409
|
+
},
|
|
410
|
+
partial: (def, path) => {
|
|
411
|
+
return path !== "" && def.metadata.get("mongo.patch.strategy") === "merge";
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Entry point – walk the payload, build `filter`, `update` and `options`.
|
|
417
|
+
*
|
|
418
|
+
* @returns Helper object exposing both individual parts and
|
|
419
|
+
* a `.toArgs()` convenience callback.
|
|
420
|
+
*/ preparePatch() {
|
|
421
|
+
this.filterObj = { _id: this.collection.prepareId(this.payload._id) };
|
|
422
|
+
this.flattenPayload(this.payload);
|
|
423
|
+
let updateFilter = this.updatePipeline;
|
|
424
|
+
return {
|
|
425
|
+
toArgs: () => [
|
|
426
|
+
this.filterObj,
|
|
427
|
+
updateFilter,
|
|
428
|
+
this.optionsObj
|
|
429
|
+
],
|
|
430
|
+
filter: this.filterObj,
|
|
431
|
+
updateFilter,
|
|
432
|
+
updateOptions: this.optionsObj
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Helper – lazily create `$set` section and assign *key* → *value*.
|
|
437
|
+
*
|
|
438
|
+
* @param key Fully‑qualified dotted path
|
|
439
|
+
* @param val Value to be written
|
|
440
|
+
* @private
|
|
441
|
+
*/ _set(key, val) {
|
|
442
|
+
for (const pipe of this.updatePipeline) {
|
|
443
|
+
if (!pipe.$set) pipe.$set = {};
|
|
444
|
+
if (!pipe.$set[key]) {
|
|
445
|
+
pipe.$set[key] = val;
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
this.updatePipeline.push({ $set: { [key]: val } });
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Recursively walk through the patch *payload* and convert it into `$set`/…
|
|
453
|
+
* statements. Top‑level arrays are delegated to {@link parseArrayPatch}.
|
|
454
|
+
*
|
|
455
|
+
* @param payload Current payload chunk
|
|
456
|
+
* @param prefix Dotted path accumulated so far
|
|
457
|
+
* @private
|
|
458
|
+
*/ flattenPayload(payload, prefix = "") {
|
|
459
|
+
const evalKey = (k) => prefix ? `${prefix}.${k}` : k;
|
|
460
|
+
for (const [_key, value] of Object.entries(payload)) {
|
|
461
|
+
const key = evalKey(_key);
|
|
462
|
+
const flatType = this.collection.flatMap.get(key);
|
|
463
|
+
const topLevelArray = flatType?.metadata?.get("mongo.__topLevelArray");
|
|
464
|
+
if (typeof value === "object" && topLevelArray) this.parseArrayPatch(key, value);
|
|
465
|
+
else if (typeof value === "object" && this.collection.flatMap.get(key)?.metadata?.get("mongo.patch.strategy") === "merge") this.flattenPayload(value, key);
|
|
466
|
+
else if (key !== "_id") this._set(key, value);
|
|
467
|
+
}
|
|
468
|
+
return this.updatePipeline;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Dispatch a *single* array patch. Exactly one of `$replace`, `$insert`,
|
|
472
|
+
* `$upsert`, `$update`, `$remove` must be present – otherwise we throw.
|
|
473
|
+
*
|
|
474
|
+
* @param key Dotted path to the array field
|
|
475
|
+
* @param value Payload slice for that field
|
|
476
|
+
* @private
|
|
477
|
+
*/ parseArrayPatch(key, value) {
|
|
478
|
+
const flatType = this.collection.flatMap.get(key);
|
|
479
|
+
const toRemove = value.$remove;
|
|
480
|
+
const toReplace = value.$replace;
|
|
481
|
+
const toInsert = value.$insert;
|
|
482
|
+
const toUpsert = value.$upsert;
|
|
483
|
+
const toUpdate = value.$update;
|
|
484
|
+
const keyProps = flatType?.type.kind === "array" ? CollectionPatcher.getKeyProps(flatType) : new Set();
|
|
485
|
+
this._remove(key, toRemove, keyProps);
|
|
486
|
+
this._replace(key, toReplace);
|
|
487
|
+
this._insert(key, toInsert, keyProps);
|
|
488
|
+
this._upsert(key, toUpsert, keyProps);
|
|
489
|
+
this._update(key, toUpdate, keyProps);
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Build an *aggregation‐expression* that checks equality by **all** keys in
|
|
493
|
+
* `keys`. Example output for keys `["id", "lang"]` and bases `a`, `b`:
|
|
494
|
+
* ```json
|
|
495
|
+
* { "$and": [ { "$eq": ["$$a.id", "$$b.id"] }, { "$eq": ["$$a.lang", "$$b.lang"] } ] }
|
|
496
|
+
* ```
|
|
497
|
+
*
|
|
498
|
+
* @param keys Ordered list of key property names
|
|
499
|
+
* @param left Base token for *left* expression (e.g. `"$$el"`)
|
|
500
|
+
* @param right Base token for *right* expression (e.g. `"$$this"`)
|
|
501
|
+
*/ _keysEqual(keys, left, right) {
|
|
502
|
+
return keys.map((k) => ({ $eq: [`${left}.${k}`, `${right}.${k}`] })).reduce((acc, cur) => acc ? { $and: [acc, cur] } : cur);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* `$replace` – overwrite the entire array with `input`.
|
|
506
|
+
*
|
|
507
|
+
* @param key Dotted path to the array
|
|
508
|
+
* @param input New array value (may be `undefined`)
|
|
509
|
+
* @private
|
|
510
|
+
*/ _replace(key, input) {
|
|
511
|
+
if (input) this._set(key, input);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* `$insert`
|
|
515
|
+
* - plain append → $concatArrays
|
|
516
|
+
* - unique / keyed → delegate to _upsert (insert-or-update)
|
|
517
|
+
*/ _insert(key, input, keyProps) {
|
|
518
|
+
if (!input?.length) return;
|
|
519
|
+
const uniqueItems = this.collection.flatMap.get(key)?.metadata?.has("mongo.array.uniqueItems");
|
|
520
|
+
if (uniqueItems || keyProps.size > 0) this._upsert(key, input, keyProps);
|
|
521
|
+
else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* `$upsert`
|
|
525
|
+
* - keyed → remove existing matching by key(s) then append candidate
|
|
526
|
+
* - unique → $setUnion (deep equality)
|
|
527
|
+
*/ _upsert(key, input, keyProps) {
|
|
528
|
+
if (!input?.length) return;
|
|
529
|
+
if (keyProps.size) {
|
|
530
|
+
const keys = [...keyProps];
|
|
531
|
+
this._set(key, { $reduce: {
|
|
532
|
+
input,
|
|
533
|
+
initialValue: { $ifNull: [`$${key}`, []] },
|
|
534
|
+
in: { $let: {
|
|
535
|
+
vars: {
|
|
536
|
+
acc: "$$value",
|
|
537
|
+
cand: "$$this"
|
|
538
|
+
},
|
|
539
|
+
in: { $concatArrays: [{ $filter: {
|
|
540
|
+
input: "$$acc",
|
|
541
|
+
as: "el",
|
|
542
|
+
cond: { $not: this._keysEqual(keys, "$$el", "$$cand") }
|
|
543
|
+
} }, ["$$cand"]] }
|
|
544
|
+
} }
|
|
545
|
+
} });
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
this._set(key, { $setUnion: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* `$update`
|
|
552
|
+
* - keyed → map array and merge / replace matching element(s)
|
|
553
|
+
* - non-keyed → behave like `$addToSet` (insert only when not present)
|
|
554
|
+
*/ _update(key, input, keyProps) {
|
|
555
|
+
if (!input?.length) return;
|
|
556
|
+
if (keyProps.size) {
|
|
557
|
+
const mergeStrategy = this.collection.flatMap.get(key)?.metadata?.get("mongo.patch.strategy") === "merge";
|
|
558
|
+
const keys = [...keyProps];
|
|
559
|
+
this._set(key, { $reduce: {
|
|
560
|
+
input,
|
|
561
|
+
initialValue: { $ifNull: [`$${key}`, []] },
|
|
562
|
+
in: { $map: {
|
|
563
|
+
input: "$$value",
|
|
564
|
+
as: "el",
|
|
565
|
+
in: { $cond: [
|
|
566
|
+
this._keysEqual(keys, "$$el", "$$this"),
|
|
567
|
+
mergeStrategy ? { $mergeObjects: ["$$el", "$$this"] } : "$$this",
|
|
568
|
+
"$$el"
|
|
569
|
+
] }
|
|
570
|
+
} }
|
|
571
|
+
} });
|
|
572
|
+
} else this._set(key, { $setUnion: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* `$remove`
|
|
576
|
+
* - keyed → filter out any element whose key set matches a payload item
|
|
577
|
+
* - non-keyed → deep equality remove (`$setDifference`)
|
|
578
|
+
*/ _remove(key, input, keyProps) {
|
|
579
|
+
if (!input?.length) return;
|
|
580
|
+
if (keyProps.size) {
|
|
581
|
+
const keys = [...keyProps];
|
|
582
|
+
this._set(key, { $let: {
|
|
583
|
+
vars: { rem: input },
|
|
584
|
+
in: { $filter: {
|
|
585
|
+
input: { $ifNull: [`$${key}`, []] },
|
|
586
|
+
as: "el",
|
|
587
|
+
cond: { $not: { $anyElementTrue: { $map: {
|
|
588
|
+
input: "$$rem",
|
|
589
|
+
as: "r",
|
|
590
|
+
in: this._keysEqual(keys, "$$el", "$$r")
|
|
591
|
+
} } } }
|
|
592
|
+
} }
|
|
593
|
+
} });
|
|
594
|
+
} else this._set(key, { $setDifference: [{ $ifNull: [`$${key}`, []] }, input] });
|
|
595
|
+
}
|
|
596
|
+
constructor(collection, payload) {
|
|
597
|
+
_define_property$2(this, "collection", void 0);
|
|
598
|
+
_define_property$2(this, "payload", void 0);
|
|
599
|
+
/**
|
|
600
|
+
* Internal accumulator: filter passed to `updateOne()`.
|
|
601
|
+
* Filled only with the `_id` field right now.
|
|
602
|
+
*/ _define_property$2(this, "filterObj", void 0);
|
|
603
|
+
/** MongoDB *update* document being built. */ _define_property$2(this, "updatePipeline", void 0);
|
|
604
|
+
/** Additional *options* (mainly `arrayFilters`). */ _define_property$2(this, "optionsObj", void 0);
|
|
605
|
+
this.collection = collection;
|
|
606
|
+
this.payload = payload;
|
|
607
|
+
this.filterObj = {};
|
|
608
|
+
this.updatePipeline = [];
|
|
609
|
+
this.optionsObj = {};
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
256
613
|
//#endregion
|
|
257
614
|
//#region packages/mongo/src/lib/as-collection.ts
|
|
258
615
|
function _define_property$1(obj, key, value) {
|
|
@@ -265,9 +622,14 @@ function _define_property$1(obj, key, value) {
|
|
|
265
622
|
else obj[key] = value;
|
|
266
623
|
return obj;
|
|
267
624
|
}
|
|
268
|
-
const INDEX_PREFIX = "
|
|
625
|
+
const INDEX_PREFIX = "atscript__";
|
|
269
626
|
const DEFAULT_INDEX_NAME = "DEFAULT";
|
|
270
|
-
|
|
627
|
+
/**
|
|
628
|
+
* Generates a key for mongo index
|
|
629
|
+
* @param type index type
|
|
630
|
+
* @param name index name
|
|
631
|
+
* @returns index key
|
|
632
|
+
*/ function indexKey(type, name) {
|
|
271
633
|
const cleanName = name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 127 - INDEX_PREFIX.length - type.length - 2);
|
|
272
634
|
return `${INDEX_PREFIX}${type}__${cleanName}`;
|
|
273
635
|
}
|
|
@@ -279,6 +641,64 @@ var AsCollection = class {
|
|
|
279
641
|
const exists = await this.exists();
|
|
280
642
|
if (!exists) await this.asMongo.db.createCollection(this.name, { comment: "Created by Atscript Mongo Collection" });
|
|
281
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* Returns the a type definition of the "_id" prop.
|
|
646
|
+
*/ get idType() {
|
|
647
|
+
const idProp = this.type.type.props.get("_id");
|
|
648
|
+
const idTags = idProp?.type.tags;
|
|
649
|
+
if (idTags?.has("objectId") && idTags?.has("mongo")) return "objectId";
|
|
650
|
+
if (idProp?.type.kind === "") return idProp.type.designType;
|
|
651
|
+
return "objectId";
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Transforms an "_id" value to the expected type (`ObjectId`, `number`, or `string`).
|
|
655
|
+
* Assumes input has already been validated.
|
|
656
|
+
*
|
|
657
|
+
* @param {string | number | ObjectId} id - The validated ID.
|
|
658
|
+
* @returns {string | number | ObjectId} - The transformed ID.
|
|
659
|
+
* @throws {Error} If the `_id` type is unknown.
|
|
660
|
+
*/ prepareId(id) {
|
|
661
|
+
switch (this.idType) {
|
|
662
|
+
case "objectId": return id instanceof ObjectId ? id : new ObjectId(id);
|
|
663
|
+
case "number": return Number(id);
|
|
664
|
+
case "string": return String(id);
|
|
665
|
+
default: throw new Error("Unknown \"_id\" type");
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Retrieves a validator for a given purpose. If the validator is not already cached,
|
|
670
|
+
* it creates and stores a new one based on the purpose.
|
|
671
|
+
*
|
|
672
|
+
* @param {TValidatorPurpose} purpose - The validation purpose (`input`, `update`, `patch`).
|
|
673
|
+
* @returns {Validator} The corresponding validator instance.
|
|
674
|
+
* @throws {Error} If an unknown purpose is provided.
|
|
675
|
+
*/ getValidator(purpose) {
|
|
676
|
+
if (!this.validators.has(purpose)) switch (purpose) {
|
|
677
|
+
case "insert": {
|
|
678
|
+
this.validators.set(purpose, this.type.validator({
|
|
679
|
+
plugins: [validateMongoIdPlugin, validateMongoUniqueArrayItemsPlugin],
|
|
680
|
+
replace(type, path) {
|
|
681
|
+
if (path === "_id" && type.type.tags.has("objectId")) return {
|
|
682
|
+
...type,
|
|
683
|
+
optional: true
|
|
684
|
+
};
|
|
685
|
+
return type;
|
|
686
|
+
}
|
|
687
|
+
}));
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
case "update": {
|
|
691
|
+
this.validators.set(purpose, this.type.validator({ plugins: [validateMongoIdPlugin] }));
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
case "patch": {
|
|
695
|
+
this.validators.set(purpose, CollectionPatcher.prepareValidator(this));
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
default: throw new Error(`Unknown validator purpose: ${purpose}`);
|
|
699
|
+
}
|
|
700
|
+
return this.validators.get(purpose);
|
|
701
|
+
}
|
|
282
702
|
get type() {
|
|
283
703
|
return this._type;
|
|
284
704
|
}
|
|
@@ -323,18 +743,24 @@ else {
|
|
|
323
743
|
if (analyzer) index.definition.mappings.fields[fieldName].analyzer = analyzer;
|
|
324
744
|
}
|
|
325
745
|
}
|
|
326
|
-
_flattenType(type, prefix) {
|
|
746
|
+
_flattenType(type, prefix = "", inComplexTypeOrArray = false) {
|
|
327
747
|
switch (type.type.kind) {
|
|
328
748
|
case "object":
|
|
749
|
+
this._flatMap?.set(prefix || "", type);
|
|
329
750
|
const items = Array.from(type.type.props.entries());
|
|
330
|
-
for (const [key, value] of items) this._flattenType(value, prefix ? `${prefix}.${key}` : key);
|
|
751
|
+
for (const [key, value] of items) this._flattenType(value, prefix ? `${prefix}.${key}` : key, inComplexTypeOrArray);
|
|
331
752
|
break;
|
|
332
753
|
case "array":
|
|
333
|
-
|
|
754
|
+
let typeArray = type;
|
|
755
|
+
if (!inComplexTypeOrArray) {
|
|
756
|
+
typeArray = defineAnnotatedType().refTo(type).copyMetadata(type.metadata).$type;
|
|
757
|
+
typeArray.metadata.set("mongo.__topLevelArray", true);
|
|
758
|
+
}
|
|
759
|
+
this._flatMap?.set(prefix || "", typeArray);
|
|
334
760
|
break;
|
|
335
761
|
case "intersection":
|
|
336
762
|
case "tuple":
|
|
337
|
-
case "union": for (const item of type.type.items) this._flattenType(item, prefix);
|
|
763
|
+
case "union": for (const item of type.type.items) this._flattenType(item, prefix, true);
|
|
338
764
|
default:
|
|
339
765
|
this._flatMap?.set(prefix || "", type);
|
|
340
766
|
break;
|
|
@@ -355,6 +781,9 @@ else {
|
|
|
355
781
|
text: { fuzzy: { maxEdits: textSearch.fuzzy || 0 } }
|
|
356
782
|
});
|
|
357
783
|
}
|
|
784
|
+
get uniqueProps() {
|
|
785
|
+
return this._uniqueProps;
|
|
786
|
+
}
|
|
358
787
|
_finalizeIndexesForCollection() {
|
|
359
788
|
for (const [key, value] of Array.from(this._vectorFilters.entries())) {
|
|
360
789
|
const index = this._indexes.get(key);
|
|
@@ -363,6 +792,10 @@ else {
|
|
|
363
792
|
path: value
|
|
364
793
|
});
|
|
365
794
|
}
|
|
795
|
+
for (const [, value] of Array.from(this._indexes.entries())) if (value.type === "unique") {
|
|
796
|
+
const keys = Object.keys(value.fields);
|
|
797
|
+
if (keys.length === 1) this._uniqueProps.add(keys[0]);
|
|
798
|
+
}
|
|
366
799
|
}
|
|
367
800
|
_prepareIndexesForField(fieldName, metadata) {
|
|
368
801
|
for (const index of metadata.get("mongo.index.plain") || []) this._addIndexField("plain", index === true ? fieldName : index, fieldName);
|
|
@@ -478,20 +911,80 @@ else toUpdate.add(remote.name);
|
|
|
478
911
|
default:
|
|
479
912
|
}
|
|
480
913
|
}
|
|
914
|
+
insert(payload, options) {
|
|
915
|
+
const toInsert = this.prepareInsert(payload);
|
|
916
|
+
return Array.isArray(toInsert) ? this.collection.insertMany(toInsert, options) : this.collection.insertOne(toInsert, options);
|
|
917
|
+
}
|
|
918
|
+
replace(payload, options) {
|
|
919
|
+
const [filter, replace, opts] = this.prepareReplace(payload).toArgs();
|
|
920
|
+
return this.collection.replaceOne(filter, replace, {
|
|
921
|
+
...opts,
|
|
922
|
+
...options
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
update(payload, options) {
|
|
926
|
+
const [filter, update, opts] = this.prepareUpdate(payload).toArgs();
|
|
927
|
+
return this.collection.updateOne(filter, update, {
|
|
928
|
+
...opts,
|
|
929
|
+
...options
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
prepareInsert(payload) {
|
|
933
|
+
const v = this.getValidator("insert");
|
|
934
|
+
const arr = Array.isArray(payload) ? payload : [payload];
|
|
935
|
+
const prepared = [];
|
|
936
|
+
for (const item of arr) if (v.validate(item)) {
|
|
937
|
+
const data = { ...item };
|
|
938
|
+
if (data._id) data._id = this.prepareId(data._id);
|
|
939
|
+
else if (this.idType !== "objectId") throw new Error("Missing \"_id\" field");
|
|
940
|
+
prepared.push(data);
|
|
941
|
+
} else throw new Error("Invalid payload");
|
|
942
|
+
return prepared.length === 1 ? prepared[0] : prepared;
|
|
943
|
+
}
|
|
944
|
+
prepareReplace(payload) {
|
|
945
|
+
const v = this.getValidator("update");
|
|
946
|
+
if (v.validate(payload)) {
|
|
947
|
+
const _id = this.prepareId(payload._id);
|
|
948
|
+
const data = {
|
|
949
|
+
...payload,
|
|
950
|
+
_id
|
|
951
|
+
};
|
|
952
|
+
return {
|
|
953
|
+
toArgs: () => [
|
|
954
|
+
{ _id },
|
|
955
|
+
data,
|
|
956
|
+
{}
|
|
957
|
+
],
|
|
958
|
+
filter: { _id },
|
|
959
|
+
updateFilter: data,
|
|
960
|
+
updateOptions: {}
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
throw new Error("Invalid payload");
|
|
964
|
+
}
|
|
965
|
+
prepareUpdate(payload) {
|
|
966
|
+
const v = this.getValidator("patch");
|
|
967
|
+
if (v.validate(payload)) return new CollectionPatcher(this, payload).preparePatch();
|
|
968
|
+
throw new Error("Invalid payload");
|
|
969
|
+
}
|
|
481
970
|
constructor(asMongo, _type, logger = NoopLogger) {
|
|
482
971
|
_define_property$1(this, "asMongo", void 0);
|
|
483
972
|
_define_property$1(this, "_type", void 0);
|
|
484
973
|
_define_property$1(this, "logger", void 0);
|
|
485
974
|
_define_property$1(this, "name", void 0);
|
|
486
975
|
_define_property$1(this, "collection", void 0);
|
|
976
|
+
_define_property$1(this, "validators", void 0);
|
|
487
977
|
_define_property$1(this, "_indexes", void 0);
|
|
488
978
|
_define_property$1(this, "_vectorFilters", void 0);
|
|
489
979
|
_define_property$1(this, "_flatMap", void 0);
|
|
980
|
+
_define_property$1(this, "_uniqueProps", void 0);
|
|
490
981
|
this.asMongo = asMongo;
|
|
491
982
|
this._type = _type;
|
|
492
983
|
this.logger = logger;
|
|
984
|
+
this.validators = new Map();
|
|
493
985
|
this._indexes = new Map();
|
|
494
986
|
this._vectorFilters = new Map();
|
|
987
|
+
this._uniqueProps = new Set();
|
|
495
988
|
if (!isAnnotatedType(_type)) throw new Error("Atscript Annotated Type expected");
|
|
496
989
|
const name = _type.metadata.get("mongo.collection");
|
|
497
990
|
if (!name) throw new Error("@mongo.collection annotation expected with collection name");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/mongo",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"description": "Mongodb plugin for atscript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
"./package.json": "./package.json"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
-
"dist"
|
|
18
|
-
"cli.cjs"
|
|
17
|
+
"dist"
|
|
19
18
|
],
|
|
20
19
|
"keywords": [
|
|
21
20
|
"atscript",
|
|
@@ -33,13 +32,13 @@
|
|
|
33
32
|
},
|
|
34
33
|
"homepage": "https://github.com/moostjs/atscript/tree/main/packages/mongo#readme",
|
|
35
34
|
"license": "ISC",
|
|
36
|
-
"
|
|
37
|
-
"mongodb": "^6.
|
|
38
|
-
"@atscript/core": "^0.0.
|
|
39
|
-
"@atscript/typescript": "^0.0.
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"mongodb": "^6.17.0",
|
|
37
|
+
"@atscript/core": "^0.0.18",
|
|
38
|
+
"@atscript/typescript": "^0.0.18"
|
|
40
39
|
},
|
|
41
40
|
"devDependencies": {
|
|
42
|
-
"vitest": "
|
|
41
|
+
"vitest": "3.2.4"
|
|
43
42
|
},
|
|
44
43
|
"scripts": {
|
|
45
44
|
"pub": "pnpm publish --access public",
|