@atscript/mongo 0.0.16 → 0.0.17

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 CHANGED
@@ -250,7 +250,35 @@ const annotations = { mongo: {
250
250
  description: "The **name of the vector search index** this field should be used as a filter for."
251
251
  }]
252
252
  })
253
- }
253
+ },
254
+ patch: { strategy: new __atscript_core.AnnotationSpec({
255
+ 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",
256
+ nodeType: ["prop"],
257
+ multiple: false,
258
+ argument: {
259
+ name: "strategy",
260
+ type: "string",
261
+ description: "The **patch strategy** for this field: `\"replace\"` (default) or `\"merge\"`.",
262
+ values: ["replace", "merge"]
263
+ },
264
+ validate(token, args, doc) {
265
+ const field = token.parentNode;
266
+ const errors = [];
267
+ const definition = field.getDefinition();
268
+ if (!definition) return errors;
269
+ let wrongType = false;
270
+ if ((0, __atscript_core.isRef)(definition)) {
271
+ const def = doc.unwindType(definition.id, definition.chain)?.def;
272
+ if (!(0, __atscript_core.isStructure)(def) && !(0, __atscript_core.isInterface)(def) && !(0, __atscript_core.isArray)(def)) wrongType = true;
273
+ } else if (!(0, __atscript_core.isStructure)(definition) && !(0, __atscript_core.isInterface)(definition) && !(0, __atscript_core.isArray)(definition)) wrongType = true;
274
+ if (wrongType) errors.push({
275
+ message: `[mongo] type of object or array expected when using @mongo.patch.strategy`,
276
+ severity: 1,
277
+ range: token.range
278
+ });
279
+ return errors;
280
+ }
281
+ }) }
254
282
  } };
255
283
 
256
284
  //#endregion
@@ -289,9 +317,14 @@ function _define_property$1(obj, key, value) {
289
317
  else obj[key] = value;
290
318
  return obj;
291
319
  }
292
- const INDEX_PREFIX = "anscript__";
320
+ const INDEX_PREFIX = "atscript__";
293
321
  const DEFAULT_INDEX_NAME = "DEFAULT";
294
- function indexKey(type, name) {
322
+ /**
323
+ * Generates a key for mongo index
324
+ * @param type index type
325
+ * @param name index name
326
+ * @returns index key
327
+ */ function indexKey(type, name) {
295
328
  const cleanName = name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 127 - INDEX_PREFIX.length - type.length - 2);
296
329
  return `${INDEX_PREFIX}${type}__${cleanName}`;
297
330
  }
@@ -303,6 +336,57 @@ var AsCollection = class {
303
336
  const exists = await this.exists();
304
337
  if (!exists) await this.asMongo.db.createCollection(this.name, { comment: "Created by Atscript Mongo Collection" });
305
338
  }
339
+ /**
340
+ * Returns the a type definition of the "_id" prop.
341
+ */ get idType() {
342
+ const idProp = this.type.type.props.get("_id");
343
+ const idTags = idProp?.type.tags;
344
+ if (idTags?.has("objectId") && idTags?.has("mongo")) return "objectId";
345
+ if (idProp?.type.kind === "") return idProp.type.designType;
346
+ return "objectId";
347
+ }
348
+ /**
349
+ * Transforms an "_id" value to the expected type (`ObjectId`, `number`, or `string`).
350
+ * Assumes input has already been validated.
351
+ *
352
+ * @param {string | number | ObjectId} id - The validated ID.
353
+ * @returns {string | number | ObjectId} - The transformed ID.
354
+ * @throws {Error} If the `_id` type is unknown.
355
+ */ prepareId(id) {
356
+ switch (this.idType) {
357
+ case "objectId": return id instanceof mongodb.ObjectId ? id : new mongodb.ObjectId(id);
358
+ case "number": return Number(id);
359
+ case "string": return String(id);
360
+ default: throw new Error("Unknown \"_id\" type");
361
+ }
362
+ }
363
+ /**
364
+ * Retrieves a validator for a given purpose. If the validator is not already cached,
365
+ * it creates and stores a new one based on the purpose.
366
+ *
367
+ * @param {TValidatorPurpose} purpose - The validation purpose (`input`, `update`, `patch`).
368
+ * @returns {Validator} The corresponding validator instance.
369
+ * @throws {Error} If an unknown purpose is provided.
370
+ */ getValidator(purpose) {
371
+ if (!this.validators.has(purpose)) switch (purpose) {
372
+ case "insert": {
373
+ this.validators.set(purpose, this.type.validator(this.idType === "objectId" ? { skipList: new Set(["_id"]) } : {}));
374
+ break;
375
+ }
376
+ case "update": {
377
+ this.validators.set(purpose, this.type.validator());
378
+ break;
379
+ }
380
+ case "patch": {
381
+ this.validators.set(purpose, this.type.validator({ partial: (def, path) => {
382
+ return path === "" || def.metadata.get("mongo.patch.strategy") === "merge";
383
+ } }));
384
+ break;
385
+ }
386
+ default: throw new Error(`Unknown validator purpose: ${purpose}`);
387
+ }
388
+ return this.validators.get(purpose);
389
+ }
306
390
  get type() {
307
391
  return this._type;
308
392
  }
@@ -350,11 +434,13 @@ else {
350
434
  _flattenType(type, prefix) {
351
435
  switch (type.type.kind) {
352
436
  case "object":
437
+ this._flatMap?.set(prefix || "", type);
353
438
  const items = Array.from(type.type.props.entries());
354
439
  for (const [key, value] of items) this._flattenType(value, prefix ? `${prefix}.${key}` : key);
355
440
  break;
356
441
  case "array":
357
- this._flattenType(type.type.of, prefix);
442
+ this._flatMap?.set(prefix || "", type);
443
+ if (type.type.of.type.kind) this._flattenType(type.type.of, prefix);
358
444
  break;
359
445
  case "intersection":
360
446
  case "tuple":
@@ -502,18 +588,53 @@ else toUpdate.add(remote.name);
502
588
  default:
503
589
  }
504
590
  }
591
+ prepareInsert(payload) {
592
+ const v = this.getValidator("insert");
593
+ if (v.validate(payload)) {
594
+ const data = { ...payload };
595
+ if (data._id) data._id = this.prepareId(data._id);
596
+ else if (this.idType !== "objectId") throw new Error("Missing \"_id\" field");
597
+ return data;
598
+ }
599
+ throw new Error("Invalid payload");
600
+ }
601
+ prepareUpdate(payload) {
602
+ const v = this.getValidator("insert");
603
+ if (v.validate(payload)) {
604
+ const data = { ...payload };
605
+ data._id = this.prepareId(data._id);
606
+ return data;
607
+ }
608
+ throw new Error("Invalid payload");
609
+ }
610
+ preparePatch(payload) {
611
+ const v = this.getValidator("patch");
612
+ if (v.validate(payload)) return { $set: this._flattenPayload(payload) };
613
+ throw new Error("Invalid payload");
614
+ }
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
+ }
505
624
  constructor(asMongo, _type, logger = NoopLogger) {
506
625
  _define_property$1(this, "asMongo", void 0);
507
626
  _define_property$1(this, "_type", void 0);
508
627
  _define_property$1(this, "logger", void 0);
509
628
  _define_property$1(this, "name", void 0);
510
629
  _define_property$1(this, "collection", void 0);
630
+ _define_property$1(this, "validators", void 0);
511
631
  _define_property$1(this, "_indexes", void 0);
512
632
  _define_property$1(this, "_vectorFilters", void 0);
513
633
  _define_property$1(this, "_flatMap", void 0);
514
634
  this.asMongo = asMongo;
515
635
  this._type = _type;
516
636
  this.logger = logger;
637
+ this.validators = new Map();
517
638
  this._indexes = new Map();
518
639
  this._vectorFilters = new Map();
519
640
  if (!(0, __atscript_typescript.isAnnotatedType)(_type)) throw new Error("Atscript Annotated Type expected");
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { TAtscriptPlugin } from '@atscript/core';
2
2
  import { TAtscriptAnnotatedTypeConstructor, TAtscriptAnnotatedType, TAtscriptTypeObject, TMetadataMap } from '@atscript/typescript';
3
3
  import * as mongodb from 'mongodb';
4
- import { MongoClient, Collection } from 'mongodb';
4
+ import { MongoClient, Collection, ObjectId, WithId, UpdateFilter, MatchKeysAndValues } from 'mongodb';
5
5
 
6
6
  declare const MongoPlugin: () => TAtscriptPlugin;
7
7
 
@@ -34,30 +34,39 @@ 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
+ get idType(): 'string' | 'number' | 'objectId';
52
+ prepareId<D = string | number | ObjectId>(id: string | number | ObjectId): D;
53
+ getValidator(purpose: TValidatorPurpose): any;
46
54
  get type(): TAtscriptAnnotatedType<TAtscriptTypeObject>;
47
- protected _indexes: Map<string, TIndex>;
48
- protected _vectorFilters: Map<string, string>;
49
55
  get indexes(): Map<string, TIndex>;
50
56
  protected _addIndexField(type: TPlainIndex['type'], name: string, field: string, weight?: number): void;
51
57
  protected _setSearchIndex(type: TSearchIndex['type'], name: string | undefined, definition: TMongoSearchIndexDefinition): void;
52
58
  protected _addFieldToSearchIndex(type: TSearchIndex['type'], _name: string | undefined, fieldName: string, analyzer?: string): void;
53
- protected _flatMap?: Map<string, TAtscriptAnnotatedType>;
54
59
  protected _flattenType(type: TAtscriptAnnotatedType, prefix?: string): void;
55
60
  protected _prepareIndexesForCollection(): void;
56
61
  protected _finalizeIndexesForCollection(): void;
57
62
  protected _prepareIndexesForField(fieldName: string, metadata: TMetadataMap<AtscriptMetadata>): void;
58
63
  protected _flatten(): void;
59
- get flatMap(): Map<string, TAtscriptAnnotatedType> | undefined;
64
+ get flatMap(): Map<string, TAtscriptAnnotatedType>;
60
65
  syncIndexes(): Promise<void>;
66
+ prepareInsert(payload: any): InstanceType<T>;
67
+ prepareUpdate(payload: any): WithId<InstanceType<T>>;
68
+ preparePatch(payload: any): UpdateFilter<InstanceType<T>>;
69
+ protected _flattenPayload(payload: T, prefix?: string, obj?: MatchKeysAndValues<InstanceType<T>>): MatchKeysAndValues<InstanceType<T>>;
61
70
  }
62
71
  type TVectorSimilarity = 'cosine' | 'euclidean' | 'dotProduct';
63
72
  type TMongoSearchIndexDefinition = {
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { AnnotationSpec, isInterface, isPrimitive, isRef, isStructure } from "@atscript/core";
1
+ import { AnnotationSpec, isArray, isInterface, isPrimitive, isRef, isStructure } from "@atscript/core";
2
2
  import { isAnnotatedType } from "@atscript/typescript";
3
- import { MongoClient } from "mongodb";
3
+ import { MongoClient, ObjectId } from "mongodb";
4
4
 
5
5
  //#region packages/mongo/src/plugin/primitives.ts
6
6
  const primitives = { mongo: { extensions: {
@@ -226,7 +226,35 @@ const annotations = { mongo: {
226
226
  description: "The **name of the vector search index** this field should be used as a filter for."
227
227
  }]
228
228
  })
229
- }
229
+ },
230
+ patch: { strategy: new AnnotationSpec({
231
+ 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",
232
+ nodeType: ["prop"],
233
+ multiple: false,
234
+ argument: {
235
+ name: "strategy",
236
+ type: "string",
237
+ description: "The **patch strategy** for this field: `\"replace\"` (default) or `\"merge\"`.",
238
+ values: ["replace", "merge"]
239
+ },
240
+ validate(token, args, doc) {
241
+ const field = token.parentNode;
242
+ const errors = [];
243
+ const definition = field.getDefinition();
244
+ if (!definition) return errors;
245
+ let wrongType = false;
246
+ if (isRef(definition)) {
247
+ const def = doc.unwindType(definition.id, definition.chain)?.def;
248
+ if (!isStructure(def) && !isInterface(def) && !isArray(def)) wrongType = true;
249
+ } else if (!isStructure(definition) && !isInterface(definition) && !isArray(definition)) wrongType = true;
250
+ if (wrongType) errors.push({
251
+ message: `[mongo] type of object or array expected when using @mongo.patch.strategy`,
252
+ severity: 1,
253
+ range: token.range
254
+ });
255
+ return errors;
256
+ }
257
+ }) }
230
258
  } };
231
259
 
232
260
  //#endregion
@@ -265,9 +293,14 @@ function _define_property$1(obj, key, value) {
265
293
  else obj[key] = value;
266
294
  return obj;
267
295
  }
268
- const INDEX_PREFIX = "anscript__";
296
+ const INDEX_PREFIX = "atscript__";
269
297
  const DEFAULT_INDEX_NAME = "DEFAULT";
270
- function indexKey(type, name) {
298
+ /**
299
+ * Generates a key for mongo index
300
+ * @param type index type
301
+ * @param name index name
302
+ * @returns index key
303
+ */ function indexKey(type, name) {
271
304
  const cleanName = name.replace(/[^a-z0-9_.-]/gi, "_").replace(/_+/g, "_").slice(0, 127 - INDEX_PREFIX.length - type.length - 2);
272
305
  return `${INDEX_PREFIX}${type}__${cleanName}`;
273
306
  }
@@ -279,6 +312,57 @@ var AsCollection = class {
279
312
  const exists = await this.exists();
280
313
  if (!exists) await this.asMongo.db.createCollection(this.name, { comment: "Created by Atscript Mongo Collection" });
281
314
  }
315
+ /**
316
+ * Returns the a type definition of the "_id" prop.
317
+ */ get idType() {
318
+ const idProp = this.type.type.props.get("_id");
319
+ const idTags = idProp?.type.tags;
320
+ if (idTags?.has("objectId") && idTags?.has("mongo")) return "objectId";
321
+ if (idProp?.type.kind === "") return idProp.type.designType;
322
+ return "objectId";
323
+ }
324
+ /**
325
+ * Transforms an "_id" value to the expected type (`ObjectId`, `number`, or `string`).
326
+ * Assumes input has already been validated.
327
+ *
328
+ * @param {string | number | ObjectId} id - The validated ID.
329
+ * @returns {string | number | ObjectId} - The transformed ID.
330
+ * @throws {Error} If the `_id` type is unknown.
331
+ */ prepareId(id) {
332
+ switch (this.idType) {
333
+ case "objectId": return id instanceof ObjectId ? id : new ObjectId(id);
334
+ case "number": return Number(id);
335
+ case "string": return String(id);
336
+ default: throw new Error("Unknown \"_id\" type");
337
+ }
338
+ }
339
+ /**
340
+ * Retrieves a validator for a given purpose. If the validator is not already cached,
341
+ * it creates and stores a new one based on the purpose.
342
+ *
343
+ * @param {TValidatorPurpose} purpose - The validation purpose (`input`, `update`, `patch`).
344
+ * @returns {Validator} The corresponding validator instance.
345
+ * @throws {Error} If an unknown purpose is provided.
346
+ */ getValidator(purpose) {
347
+ if (!this.validators.has(purpose)) switch (purpose) {
348
+ case "insert": {
349
+ this.validators.set(purpose, this.type.validator(this.idType === "objectId" ? { skipList: new Set(["_id"]) } : {}));
350
+ break;
351
+ }
352
+ case "update": {
353
+ this.validators.set(purpose, this.type.validator());
354
+ break;
355
+ }
356
+ case "patch": {
357
+ this.validators.set(purpose, this.type.validator({ partial: (def, path) => {
358
+ return path === "" || def.metadata.get("mongo.patch.strategy") === "merge";
359
+ } }));
360
+ break;
361
+ }
362
+ default: throw new Error(`Unknown validator purpose: ${purpose}`);
363
+ }
364
+ return this.validators.get(purpose);
365
+ }
282
366
  get type() {
283
367
  return this._type;
284
368
  }
@@ -326,11 +410,13 @@ else {
326
410
  _flattenType(type, prefix) {
327
411
  switch (type.type.kind) {
328
412
  case "object":
413
+ this._flatMap?.set(prefix || "", type);
329
414
  const items = Array.from(type.type.props.entries());
330
415
  for (const [key, value] of items) this._flattenType(value, prefix ? `${prefix}.${key}` : key);
331
416
  break;
332
417
  case "array":
333
- this._flattenType(type.type.of, prefix);
418
+ this._flatMap?.set(prefix || "", type);
419
+ if (type.type.of.type.kind) this._flattenType(type.type.of, prefix);
334
420
  break;
335
421
  case "intersection":
336
422
  case "tuple":
@@ -478,18 +564,53 @@ else toUpdate.add(remote.name);
478
564
  default:
479
565
  }
480
566
  }
567
+ prepareInsert(payload) {
568
+ const v = this.getValidator("insert");
569
+ if (v.validate(payload)) {
570
+ const data = { ...payload };
571
+ if (data._id) data._id = this.prepareId(data._id);
572
+ else if (this.idType !== "objectId") throw new Error("Missing \"_id\" field");
573
+ return data;
574
+ }
575
+ throw new Error("Invalid payload");
576
+ }
577
+ prepareUpdate(payload) {
578
+ const v = this.getValidator("insert");
579
+ if (v.validate(payload)) {
580
+ const data = { ...payload };
581
+ data._id = this.prepareId(data._id);
582
+ return data;
583
+ }
584
+ throw new Error("Invalid payload");
585
+ }
586
+ preparePatch(payload) {
587
+ const v = this.getValidator("patch");
588
+ if (v.validate(payload)) return { $set: this._flattenPayload(payload) };
589
+ throw new Error("Invalid payload");
590
+ }
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
+ }
481
600
  constructor(asMongo, _type, logger = NoopLogger) {
482
601
  _define_property$1(this, "asMongo", void 0);
483
602
  _define_property$1(this, "_type", void 0);
484
603
  _define_property$1(this, "logger", void 0);
485
604
  _define_property$1(this, "name", void 0);
486
605
  _define_property$1(this, "collection", void 0);
606
+ _define_property$1(this, "validators", void 0);
487
607
  _define_property$1(this, "_indexes", void 0);
488
608
  _define_property$1(this, "_vectorFilters", void 0);
489
609
  _define_property$1(this, "_flatMap", void 0);
490
610
  this.asMongo = asMongo;
491
611
  this._type = _type;
492
612
  this.logger = logger;
613
+ this.validators = new Map();
493
614
  this._indexes = new Map();
494
615
  this._vectorFilters = new Map();
495
616
  if (!isAnnotatedType(_type)) throw new Error("Atscript Annotated Type expected");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/mongo",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "Mongodb plugin for atscript.",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -35,8 +35,8 @@
35
35
  "license": "ISC",
36
36
  "dependencies": {
37
37
  "mongodb": "^6.13.0",
38
- "@atscript/core": "^0.0.16",
39
- "@atscript/typescript": "^0.0.16"
38
+ "@atscript/core": "^0.0.17",
39
+ "@atscript/typescript": "^0.0.17"
40
40
  },
41
41
  "devDependencies": {
42
42
  "vitest": "^3.0.0"