@atscript/mongo 0.0.17 → 0.0.19
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 +411 -32
- package/dist/index.d.ts +78 -7
- package/dist/index.mjs +412 -33
- 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",
|
|
@@ -278,6 +287,28 @@ const annotations = { mongo: {
|
|
|
278
287
|
});
|
|
279
288
|
return errors;
|
|
280
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
|
+
}
|
|
281
312
|
}) }
|
|
282
313
|
} };
|
|
283
314
|
|
|
@@ -305,6 +336,304 @@ const NoopLogger = {
|
|
|
305
336
|
debug: () => {}
|
|
306
337
|
};
|
|
307
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
|
+
|
|
308
637
|
//#endregion
|
|
309
638
|
//#region packages/mongo/src/lib/as-collection.ts
|
|
310
639
|
function _define_property$1(obj, key, value) {
|
|
@@ -370,17 +699,24 @@ var AsCollection = class {
|
|
|
370
699
|
*/ getValidator(purpose) {
|
|
371
700
|
if (!this.validators.has(purpose)) switch (purpose) {
|
|
372
701
|
case "insert": {
|
|
373
|
-
this.validators.set(purpose, this.type.validator(
|
|
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
|
+
}));
|
|
374
712
|
break;
|
|
375
713
|
}
|
|
376
714
|
case "update": {
|
|
377
|
-
this.validators.set(purpose, this.type.validator());
|
|
715
|
+
this.validators.set(purpose, this.type.validator({ plugins: [validateMongoIdPlugin] }));
|
|
378
716
|
break;
|
|
379
717
|
}
|
|
380
718
|
case "patch": {
|
|
381
|
-
this.validators.set(purpose,
|
|
382
|
-
return path === "" || def.metadata.get("mongo.patch.strategy") === "merge";
|
|
383
|
-
} }));
|
|
719
|
+
this.validators.set(purpose, CollectionPatcher.prepareValidator(this));
|
|
384
720
|
break;
|
|
385
721
|
}
|
|
386
722
|
default: throw new Error(`Unknown validator purpose: ${purpose}`);
|
|
@@ -431,20 +767,24 @@ else {
|
|
|
431
767
|
if (analyzer) index.definition.mappings.fields[fieldName].analyzer = analyzer;
|
|
432
768
|
}
|
|
433
769
|
}
|
|
434
|
-
_flattenType(type, prefix) {
|
|
770
|
+
_flattenType(type, prefix = "", inComplexTypeOrArray = false) {
|
|
435
771
|
switch (type.type.kind) {
|
|
436
772
|
case "object":
|
|
437
773
|
this._flatMap?.set(prefix || "", type);
|
|
438
774
|
const items = Array.from(type.type.props.entries());
|
|
439
|
-
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);
|
|
440
776
|
break;
|
|
441
777
|
case "array":
|
|
442
|
-
|
|
443
|
-
if (
|
|
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);
|
|
444
784
|
break;
|
|
445
785
|
case "intersection":
|
|
446
786
|
case "tuple":
|
|
447
|
-
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);
|
|
448
788
|
default:
|
|
449
789
|
this._flatMap?.set(prefix || "", type);
|
|
450
790
|
break;
|
|
@@ -465,6 +805,9 @@ else {
|
|
|
465
805
|
text: { fuzzy: { maxEdits: textSearch.fuzzy || 0 } }
|
|
466
806
|
});
|
|
467
807
|
}
|
|
808
|
+
get uniqueProps() {
|
|
809
|
+
return this._uniqueProps;
|
|
810
|
+
}
|
|
468
811
|
_finalizeIndexesForCollection() {
|
|
469
812
|
for (const [key, value] of Array.from(this._vectorFilters.entries())) {
|
|
470
813
|
const index = this._indexes.get(key);
|
|
@@ -473,6 +816,10 @@ else {
|
|
|
473
816
|
path: value
|
|
474
817
|
});
|
|
475
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
|
+
}
|
|
476
823
|
}
|
|
477
824
|
_prepareIndexesForField(fieldName, metadata) {
|
|
478
825
|
for (const index of metadata.get("mongo.index.plain") || []) this._addIndexField("plain", index === true ? fieldName : index, fieldName);
|
|
@@ -588,39 +935,62 @@ else toUpdate.add(remote.name);
|
|
|
588
935
|
default:
|
|
589
936
|
}
|
|
590
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
|
+
}
|
|
591
956
|
prepareInsert(payload) {
|
|
592
957
|
const v = this.getValidator("insert");
|
|
593
|
-
|
|
594
|
-
|
|
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 };
|
|
595
962
|
if (data._id) data._id = this.prepareId(data._id);
|
|
596
963
|
else if (this.idType !== "objectId") throw new Error("Missing \"_id\" field");
|
|
597
|
-
|
|
598
|
-
}
|
|
599
|
-
|
|
964
|
+
prepared.push(data);
|
|
965
|
+
} else throw new Error("Invalid payload");
|
|
966
|
+
return prepared.length === 1 ? prepared[0] : prepared;
|
|
600
967
|
}
|
|
601
|
-
|
|
602
|
-
const v = this.getValidator("
|
|
968
|
+
prepareReplace(payload) {
|
|
969
|
+
const v = this.getValidator("update");
|
|
603
970
|
if (v.validate(payload)) {
|
|
604
|
-
const
|
|
605
|
-
data
|
|
606
|
-
|
|
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
|
+
};
|
|
607
986
|
}
|
|
608
987
|
throw new Error("Invalid payload");
|
|
609
988
|
}
|
|
610
|
-
|
|
989
|
+
prepareUpdate(payload) {
|
|
611
990
|
const v = this.getValidator("patch");
|
|
612
|
-
if (v.validate(payload)) return
|
|
991
|
+
if (v.validate(payload)) return new CollectionPatcher(this, payload).preparePatch();
|
|
613
992
|
throw new Error("Invalid payload");
|
|
614
993
|
}
|
|
615
|
-
_flattenPayload(payload, prefix = "", obj = {}) {
|
|
616
|
-
const evalKey = (k) => prefix ? `${prefix}.${k}` : k;
|
|
617
|
-
for (const [_key, value] of Object.entries(payload)) {
|
|
618
|
-
const key = evalKey(_key);
|
|
619
|
-
if (typeof value === "object" && this.flatMap.get(key)?.metadata?.get("mongo.patch.strategy") === "merge") this._flattenPayload(value, key, obj);
|
|
620
|
-
else obj[key] = value;
|
|
621
|
-
}
|
|
622
|
-
return obj;
|
|
623
|
-
}
|
|
624
994
|
constructor(asMongo, _type, logger = NoopLogger) {
|
|
625
995
|
_define_property$1(this, "asMongo", void 0);
|
|
626
996
|
_define_property$1(this, "_type", void 0);
|
|
@@ -631,12 +1001,14 @@ else obj[key] = value;
|
|
|
631
1001
|
_define_property$1(this, "_indexes", void 0);
|
|
632
1002
|
_define_property$1(this, "_vectorFilters", void 0);
|
|
633
1003
|
_define_property$1(this, "_flatMap", void 0);
|
|
1004
|
+
_define_property$1(this, "_uniqueProps", void 0);
|
|
634
1005
|
this.asMongo = asMongo;
|
|
635
1006
|
this._type = _type;
|
|
636
1007
|
this.logger = logger;
|
|
637
1008
|
this.validators = new Map();
|
|
638
1009
|
this._indexes = new Map();
|
|
639
1010
|
this._vectorFilters = new Map();
|
|
1011
|
+
this._uniqueProps = new Set();
|
|
640
1012
|
if (!(0, __atscript_typescript.isAnnotatedType)(_type)) throw new Error("Atscript Annotated Type expected");
|
|
641
1013
|
const name = _type.metadata.get("mongo.collection");
|
|
642
1014
|
if (!name) throw new Error("@mongo.collection annotation expected with collection name");
|
|
@@ -715,13 +1087,20 @@ var AsMongo = class {
|
|
|
715
1087
|
return list.has(name);
|
|
716
1088
|
}
|
|
717
1089
|
getCollection(type, logger) {
|
|
718
|
-
|
|
1090
|
+
let collection = this._collections.get(type);
|
|
1091
|
+
if (!collection) {
|
|
1092
|
+
collection = new AsCollection(this, type, logger || this.logger);
|
|
1093
|
+
this._collections.set(type, collection);
|
|
1094
|
+
}
|
|
1095
|
+
return collection;
|
|
719
1096
|
}
|
|
720
1097
|
constructor(client, logger = NoopLogger) {
|
|
721
1098
|
_define_property(this, "logger", void 0);
|
|
722
1099
|
_define_property(this, "client", void 0);
|
|
723
1100
|
_define_property(this, "collectionsList", void 0);
|
|
1101
|
+
_define_property(this, "_collections", void 0);
|
|
724
1102
|
this.logger = logger;
|
|
1103
|
+
this._collections = new WeakMap();
|
|
725
1104
|
if (typeof client === "string") this.client = new mongodb.MongoClient(client);
|
|
726
1105
|
else this.client = client;
|
|
727
1106
|
}
|
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, ObjectId,
|
|
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
|
|
|
@@ -22,6 +22,7 @@ declare class AsMongo {
|
|
|
22
22
|
protected getCollectionsList(): Promise<Set<string>>;
|
|
23
23
|
collectionExists(name: string): Promise<boolean>;
|
|
24
24
|
getCollection<T extends TAtscriptAnnotatedTypeConstructor>(type: T, logger?: TGenericLogger): AsCollection<T>;
|
|
25
|
+
private _collections;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
type TPlainIndex = {
|
|
@@ -48,25 +49,74 @@ declare class AsCollection<T extends TAtscriptAnnotatedTypeConstructor> {
|
|
|
48
49
|
constructor(asMongo: AsMongo, _type: T, logger?: TGenericLogger);
|
|
49
50
|
exists(): Promise<boolean>;
|
|
50
51
|
ensureExists(): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Returns the a type definition of the "_id" prop.
|
|
54
|
+
*/
|
|
51
55
|
get idType(): 'string' | 'number' | 'objectId';
|
|
56
|
+
/**
|
|
57
|
+
* Transforms an "_id" value to the expected type (`ObjectId`, `number`, or `string`).
|
|
58
|
+
* Assumes input has already been validated.
|
|
59
|
+
*
|
|
60
|
+
* @param {string | number | ObjectId} id - The validated ID.
|
|
61
|
+
* @returns {string | number | ObjectId} - The transformed ID.
|
|
62
|
+
* @throws {Error} If the `_id` type is unknown.
|
|
63
|
+
*/
|
|
52
64
|
prepareId<D = string | number | ObjectId>(id: string | number | ObjectId): D;
|
|
65
|
+
/**
|
|
66
|
+
* Retrieves a validator for a given purpose. If the validator is not already cached,
|
|
67
|
+
* it creates and stores a new one based on the purpose.
|
|
68
|
+
*
|
|
69
|
+
* @param {TValidatorPurpose} purpose - The validation purpose (`input`, `update`, `patch`).
|
|
70
|
+
* @returns {Validator} The corresponding validator instance.
|
|
71
|
+
* @throws {Error} If an unknown purpose is provided.
|
|
72
|
+
*/
|
|
53
73
|
getValidator(purpose: TValidatorPurpose): any;
|
|
54
74
|
get type(): TAtscriptAnnotatedType<TAtscriptTypeObject>;
|
|
55
75
|
get indexes(): Map<string, TIndex>;
|
|
56
76
|
protected _addIndexField(type: TPlainIndex['type'], name: string, field: string, weight?: number): void;
|
|
57
77
|
protected _setSearchIndex(type: TSearchIndex['type'], name: string | undefined, definition: TMongoSearchIndexDefinition): void;
|
|
58
78
|
protected _addFieldToSearchIndex(type: TSearchIndex['type'], _name: string | undefined, fieldName: string, analyzer?: string): void;
|
|
59
|
-
protected _flattenType(type: TAtscriptAnnotatedType, prefix?: string): void;
|
|
79
|
+
protected _flattenType(type: TAtscriptAnnotatedType, prefix?: string, inComplexTypeOrArray?: boolean): void;
|
|
60
80
|
protected _prepareIndexesForCollection(): void;
|
|
81
|
+
protected _uniqueProps: Set<string>;
|
|
82
|
+
get uniqueProps(): Set<string>;
|
|
61
83
|
protected _finalizeIndexesForCollection(): void;
|
|
62
84
|
protected _prepareIndexesForField(fieldName: string, metadata: TMetadataMap<AtscriptMetadata>): void;
|
|
63
85
|
protected _flatten(): void;
|
|
64
86
|
get flatMap(): Map<string, TAtscriptAnnotatedType>;
|
|
65
87
|
syncIndexes(): Promise<void>;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
88
|
+
insert(payload: (Omit<InstanceType<T>, '_id'> & {
|
|
89
|
+
_id?: InstanceType<T>['_id'] | ObjectId;
|
|
90
|
+
}) | (Omit<InstanceType<T>, '_id'> & {
|
|
91
|
+
_id?: InstanceType<T>['_id'] | ObjectId;
|
|
92
|
+
})[], options?: InsertOneOptions): Promise<mongodb.InsertManyResult<InstanceType<T>>> | Promise<mongodb.InsertOneResult<InstanceType<T>>>;
|
|
93
|
+
replace(payload: Omit<InstanceType<T>, '_id'> & {
|
|
94
|
+
_id: InstanceType<T>['_id'] | ObjectId;
|
|
95
|
+
}, options?: ReplaceOptions): Promise<mongodb.UpdateResult<InstanceType<T>>>;
|
|
96
|
+
update(payload: AsMongoPatch<Omit<InstanceType<T>, '_id'>> & {
|
|
97
|
+
_id: InstanceType<T>['_id'] | ObjectId;
|
|
98
|
+
}, options?: UpdateOptions): Promise<mongodb.UpdateResult<InstanceType<T>>>;
|
|
99
|
+
prepareInsert(payload: (Omit<InstanceType<T>, '_id'> & {
|
|
100
|
+
_id?: InstanceType<T>['_id'] | ObjectId;
|
|
101
|
+
}) | (Omit<InstanceType<T>, '_id'> & {
|
|
102
|
+
_id?: InstanceType<T>['_id'] | ObjectId;
|
|
103
|
+
})[]): OptionalUnlessRequiredId<InstanceType<T>> | OptionalUnlessRequiredId<InstanceType<T>>[];
|
|
104
|
+
prepareReplace(payload: Omit<InstanceType<T>, '_id'> & {
|
|
105
|
+
_id: InstanceType<T>['_id'] | ObjectId;
|
|
106
|
+
}): {
|
|
107
|
+
toArgs: () => [Filter<InstanceType<T>>, WithoutId<InstanceType<T>>, ReplaceOptions];
|
|
108
|
+
filter: Filter<InstanceType<T>>;
|
|
109
|
+
updateFilter: WithoutId<InstanceType<T>>;
|
|
110
|
+
updateOptions: ReplaceOptions;
|
|
111
|
+
};
|
|
112
|
+
prepareUpdate(payload: AsMongoPatch<Omit<InstanceType<T>, '_id'>> & {
|
|
113
|
+
_id: InstanceType<T>['_id'] | ObjectId;
|
|
114
|
+
}): {
|
|
115
|
+
toArgs: () => [Filter<InstanceType<T>>, mongodb.Document[] | mongodb.UpdateFilter<InstanceType<T>>, UpdateOptions];
|
|
116
|
+
filter: Filter<InstanceType<T>>;
|
|
117
|
+
updateFilter: mongodb.Document[];
|
|
118
|
+
updateOptions: UpdateOptions;
|
|
119
|
+
};
|
|
70
120
|
}
|
|
71
121
|
type TVectorSimilarity = 'cosine' | 'euclidean' | 'dotProduct';
|
|
72
122
|
type TMongoSearchIndexDefinition = {
|
|
@@ -90,5 +140,26 @@ type TMongoSearchIndexDefinition = {
|
|
|
90
140
|
};
|
|
91
141
|
};
|
|
92
142
|
};
|
|
143
|
+
type TArrayPatch<A extends readonly unknown[]> = {
|
|
144
|
+
$replace?: A;
|
|
145
|
+
$insert?: A;
|
|
146
|
+
$upsert?: A;
|
|
147
|
+
$update?: Partial<TArrayElement<A>>[];
|
|
148
|
+
$remove?: Partial<TArrayElement<A>>[];
|
|
149
|
+
};
|
|
150
|
+
type TArrayElement<ArrayType extends readonly unknown[]> = ArrayType extends readonly (infer ElementType)[] ? ElementType : never;
|
|
151
|
+
/**
|
|
152
|
+
* AsMongoPatch<T>
|
|
153
|
+
* ─────────────────
|
|
154
|
+
* - For every key K in T:
|
|
155
|
+
* • if T[K] is `X[]`, rewrite it to `TArrayPatch<X[]>`
|
|
156
|
+
* • otherwise omit the key (feel free to keep it if you want)
|
|
157
|
+
*
|
|
158
|
+
* The result is an *optional* property bag that matches a patch payload
|
|
159
|
+
* for array fields only.
|
|
160
|
+
*/
|
|
161
|
+
type AsMongoPatch<T> = {
|
|
162
|
+
[K in keyof T]?: T[K] extends Array<infer _> ? TArrayPatch<T[K]> : Partial<T[K]>;
|
|
163
|
+
};
|
|
93
164
|
|
|
94
165
|
export { AsCollection, AsMongo, MongoPlugin };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AnnotationSpec, isArray, isInterface, isPrimitive, isRef, isStructure } from "@atscript/core";
|
|
2
|
-
import { isAnnotatedType } from "@atscript/typescript";
|
|
2
|
+
import { defineAnnotatedType, isAnnotatedType, isAnnotatedTypeOfPrimitive } from "@atscript/typescript";
|
|
3
3
|
import { MongoClient, ObjectId } from "mongodb";
|
|
4
4
|
|
|
5
5
|
//#region packages/mongo/src/plugin/primitives.ts
|
|
@@ -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",
|
|
@@ -254,6 +263,28 @@ const annotations = { mongo: {
|
|
|
254
263
|
});
|
|
255
264
|
return errors;
|
|
256
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
|
+
}
|
|
257
288
|
}) }
|
|
258
289
|
} };
|
|
259
290
|
|
|
@@ -281,6 +312,304 @@ const NoopLogger = {
|
|
|
281
312
|
debug: () => {}
|
|
282
313
|
};
|
|
283
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
|
+
|
|
284
613
|
//#endregion
|
|
285
614
|
//#region packages/mongo/src/lib/as-collection.ts
|
|
286
615
|
function _define_property$1(obj, key, value) {
|
|
@@ -346,17 +675,24 @@ var AsCollection = class {
|
|
|
346
675
|
*/ getValidator(purpose) {
|
|
347
676
|
if (!this.validators.has(purpose)) switch (purpose) {
|
|
348
677
|
case "insert": {
|
|
349
|
-
this.validators.set(purpose, this.type.validator(
|
|
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
|
+
}));
|
|
350
688
|
break;
|
|
351
689
|
}
|
|
352
690
|
case "update": {
|
|
353
|
-
this.validators.set(purpose, this.type.validator());
|
|
691
|
+
this.validators.set(purpose, this.type.validator({ plugins: [validateMongoIdPlugin] }));
|
|
354
692
|
break;
|
|
355
693
|
}
|
|
356
694
|
case "patch": {
|
|
357
|
-
this.validators.set(purpose,
|
|
358
|
-
return path === "" || def.metadata.get("mongo.patch.strategy") === "merge";
|
|
359
|
-
} }));
|
|
695
|
+
this.validators.set(purpose, CollectionPatcher.prepareValidator(this));
|
|
360
696
|
break;
|
|
361
697
|
}
|
|
362
698
|
default: throw new Error(`Unknown validator purpose: ${purpose}`);
|
|
@@ -407,20 +743,24 @@ else {
|
|
|
407
743
|
if (analyzer) index.definition.mappings.fields[fieldName].analyzer = analyzer;
|
|
408
744
|
}
|
|
409
745
|
}
|
|
410
|
-
_flattenType(type, prefix) {
|
|
746
|
+
_flattenType(type, prefix = "", inComplexTypeOrArray = false) {
|
|
411
747
|
switch (type.type.kind) {
|
|
412
748
|
case "object":
|
|
413
749
|
this._flatMap?.set(prefix || "", type);
|
|
414
750
|
const items = Array.from(type.type.props.entries());
|
|
415
|
-
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);
|
|
416
752
|
break;
|
|
417
753
|
case "array":
|
|
418
|
-
|
|
419
|
-
if (
|
|
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);
|
|
420
760
|
break;
|
|
421
761
|
case "intersection":
|
|
422
762
|
case "tuple":
|
|
423
|
-
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);
|
|
424
764
|
default:
|
|
425
765
|
this._flatMap?.set(prefix || "", type);
|
|
426
766
|
break;
|
|
@@ -441,6 +781,9 @@ else {
|
|
|
441
781
|
text: { fuzzy: { maxEdits: textSearch.fuzzy || 0 } }
|
|
442
782
|
});
|
|
443
783
|
}
|
|
784
|
+
get uniqueProps() {
|
|
785
|
+
return this._uniqueProps;
|
|
786
|
+
}
|
|
444
787
|
_finalizeIndexesForCollection() {
|
|
445
788
|
for (const [key, value] of Array.from(this._vectorFilters.entries())) {
|
|
446
789
|
const index = this._indexes.get(key);
|
|
@@ -449,6 +792,10 @@ else {
|
|
|
449
792
|
path: value
|
|
450
793
|
});
|
|
451
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
|
+
}
|
|
452
799
|
}
|
|
453
800
|
_prepareIndexesForField(fieldName, metadata) {
|
|
454
801
|
for (const index of metadata.get("mongo.index.plain") || []) this._addIndexField("plain", index === true ? fieldName : index, fieldName);
|
|
@@ -564,39 +911,62 @@ else toUpdate.add(remote.name);
|
|
|
564
911
|
default:
|
|
565
912
|
}
|
|
566
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
|
+
}
|
|
567
932
|
prepareInsert(payload) {
|
|
568
933
|
const v = this.getValidator("insert");
|
|
569
|
-
|
|
570
|
-
|
|
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 };
|
|
571
938
|
if (data._id) data._id = this.prepareId(data._id);
|
|
572
939
|
else if (this.idType !== "objectId") throw new Error("Missing \"_id\" field");
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
|
|
940
|
+
prepared.push(data);
|
|
941
|
+
} else throw new Error("Invalid payload");
|
|
942
|
+
return prepared.length === 1 ? prepared[0] : prepared;
|
|
576
943
|
}
|
|
577
|
-
|
|
578
|
-
const v = this.getValidator("
|
|
944
|
+
prepareReplace(payload) {
|
|
945
|
+
const v = this.getValidator("update");
|
|
579
946
|
if (v.validate(payload)) {
|
|
580
|
-
const
|
|
581
|
-
data
|
|
582
|
-
|
|
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
|
+
};
|
|
583
962
|
}
|
|
584
963
|
throw new Error("Invalid payload");
|
|
585
964
|
}
|
|
586
|
-
|
|
965
|
+
prepareUpdate(payload) {
|
|
587
966
|
const v = this.getValidator("patch");
|
|
588
|
-
if (v.validate(payload)) return
|
|
967
|
+
if (v.validate(payload)) return new CollectionPatcher(this, payload).preparePatch();
|
|
589
968
|
throw new Error("Invalid payload");
|
|
590
969
|
}
|
|
591
|
-
_flattenPayload(payload, prefix = "", obj = {}) {
|
|
592
|
-
const evalKey = (k) => prefix ? `${prefix}.${k}` : k;
|
|
593
|
-
for (const [_key, value] of Object.entries(payload)) {
|
|
594
|
-
const key = evalKey(_key);
|
|
595
|
-
if (typeof value === "object" && this.flatMap.get(key)?.metadata?.get("mongo.patch.strategy") === "merge") this._flattenPayload(value, key, obj);
|
|
596
|
-
else obj[key] = value;
|
|
597
|
-
}
|
|
598
|
-
return obj;
|
|
599
|
-
}
|
|
600
970
|
constructor(asMongo, _type, logger = NoopLogger) {
|
|
601
971
|
_define_property$1(this, "asMongo", void 0);
|
|
602
972
|
_define_property$1(this, "_type", void 0);
|
|
@@ -607,12 +977,14 @@ else obj[key] = value;
|
|
|
607
977
|
_define_property$1(this, "_indexes", void 0);
|
|
608
978
|
_define_property$1(this, "_vectorFilters", void 0);
|
|
609
979
|
_define_property$1(this, "_flatMap", void 0);
|
|
980
|
+
_define_property$1(this, "_uniqueProps", void 0);
|
|
610
981
|
this.asMongo = asMongo;
|
|
611
982
|
this._type = _type;
|
|
612
983
|
this.logger = logger;
|
|
613
984
|
this.validators = new Map();
|
|
614
985
|
this._indexes = new Map();
|
|
615
986
|
this._vectorFilters = new Map();
|
|
987
|
+
this._uniqueProps = new Set();
|
|
616
988
|
if (!isAnnotatedType(_type)) throw new Error("Atscript Annotated Type expected");
|
|
617
989
|
const name = _type.metadata.get("mongo.collection");
|
|
618
990
|
if (!name) throw new Error("@mongo.collection annotation expected with collection name");
|
|
@@ -691,13 +1063,20 @@ var AsMongo = class {
|
|
|
691
1063
|
return list.has(name);
|
|
692
1064
|
}
|
|
693
1065
|
getCollection(type, logger) {
|
|
694
|
-
|
|
1066
|
+
let collection = this._collections.get(type);
|
|
1067
|
+
if (!collection) {
|
|
1068
|
+
collection = new AsCollection(this, type, logger || this.logger);
|
|
1069
|
+
this._collections.set(type, collection);
|
|
1070
|
+
}
|
|
1071
|
+
return collection;
|
|
695
1072
|
}
|
|
696
1073
|
constructor(client, logger = NoopLogger) {
|
|
697
1074
|
_define_property(this, "logger", void 0);
|
|
698
1075
|
_define_property(this, "client", void 0);
|
|
699
1076
|
_define_property(this, "collectionsList", void 0);
|
|
1077
|
+
_define_property(this, "_collections", void 0);
|
|
700
1078
|
this.logger = logger;
|
|
1079
|
+
this._collections = new WeakMap();
|
|
701
1080
|
if (typeof client === "string") this.client = new MongoClient(client);
|
|
702
1081
|
else this.client = client;
|
|
703
1082
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/mongo",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
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.19",
|
|
38
|
+
"@atscript/typescript": "^0.0.19"
|
|
40
39
|
},
|
|
41
40
|
"devDependencies": {
|
|
42
|
-
"vitest": "
|
|
41
|
+
"vitest": "3.2.4"
|
|
43
42
|
},
|
|
44
43
|
"scripts": {
|
|
45
44
|
"pub": "pnpm publish --access public",
|