@atscript/mongo 0.1.27 → 0.1.28

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.mjs CHANGED
@@ -19,15 +19,10 @@ const analyzers = [
19
19
  "lucene.russian",
20
20
  "lucene.arabic"
21
21
  ];
22
- const annotations = { mongo: {
22
+ const annotations = {
23
23
  collection: new AnnotationSpec({
24
- description: "Defines a **MongoDB collection**. This annotation is required to mark an interface as a collection.\n\n- Automatically enforces a **non-optional** `_id` field.\n- `_id` must be of type **`string`**, **`number`**, or **`mongo.objectId`**.\n- Ensures that `_id` is included if not explicitly defined.\n\n**Example:**\n```atscript\n@mongo.collection \"users\"\nexport interface User {\n _id: mongo.objectId\n email: string.email\n}\n```\n",
24
+ description: "Marks an interface as a **MongoDB collection**.\n\n- Use together with `@db.table \"name\"` which provides the collection name.\n- Automatically injects a **non-optional** `_id` field if not explicitly defined.\n- `_id` must be of type **`string`**, **`number`**, or **`mongo.objectId`**.\n\n**Example:**\n```atscript\n@db.table \"users\"\n@db.mongo.collection\nexport interface User {\n _id: mongo.objectId\n email: string.email\n}\n```\n",
25
25
  nodeType: ["interface"],
26
- argument: {
27
- name: "name",
28
- type: "string",
29
- description: "The **name of the MongoDB collection**."
30
- },
31
26
  validate(token, args, doc) {
32
27
  const parent = token.parentNode;
33
28
  const struc = parent?.getDefinition();
@@ -36,7 +31,7 @@ const annotations = { mongo: {
36
31
  const _id = parent.props.get("_id");
37
32
  const isOptional = !!_id.token("optional");
38
33
  if (isOptional) errors.push({
39
- message: `[mongo] _id can't be optional in Mongo Collection`,
34
+ message: `[db.mongo] _id can't be optional in Mongo Collection`,
40
35
  severity: 1,
41
36
  range: _id.token("identifier").range
42
37
  });
@@ -48,7 +43,7 @@ const annotations = { mongo: {
48
43
  if (isPrimitive(def) && !["string", "number"].includes(def.config.type)) wrongType = true;
49
44
  } else wrongType = true;
50
45
  if (wrongType) errors.push({
51
- message: `[mongo] _id must be of type string, number or mongo.objectId`,
46
+ message: `[db.mongo] _id must be of type string, number or mongo.objectId`,
52
47
  severity: 1,
53
48
  range: _id.token("identifier").range
54
49
  });
@@ -74,43 +69,19 @@ const annotations = { mongo: {
74
69
  description: "On/Off the automatic index creation"
75
70
  }
76
71
  }),
77
- index: {
78
- plain: new AnnotationSpec({
79
- 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",
80
- multiple: true,
81
- nodeType: ["prop"],
82
- argument: {
83
- optional: true,
84
- name: "indexName",
85
- type: "string",
86
- description: "The **name of the index** (optional). If omitted, property name is used."
87
- }
88
- }),
89
- unique: new AnnotationSpec({
90
- description: "Creates a **unique index** on a field to ensure no duplicate values exist.\n\n- Enforces uniqueness at the database level.\n- Automatically prevents duplicate entries.\n- Typically used for **emails, usernames, and IDs**.\n\n**Example:**\n```atscript\n@mongo.index.unique \"uniqueEmailIndex\"\nemail: string.email\n```\n",
91
- multiple: true,
92
- nodeType: ["prop"],
93
- argument: {
94
- optional: true,
95
- name: "indexName",
96
- type: "string",
97
- description: "The **name of the unique index** (optional). If omitted, property name is used."
98
- }
99
- }),
100
- text: new AnnotationSpec({
101
- description: "Creates a **legacy MongoDB text index** for full-text search.\n\n**⚠ WARNING:** *Text indexes slow down database operations. Use `@mongo.defineTextSearch` instead for better performance.*\n\n- Allows **basic full-text search** on a field.\n- Does **not support fuzzy matching or ranking**.\n- **Replaced by MongoDB Atlas Search Indexes (`@mongo.searchIndex.text`).**\n\n**Example:**\n```atscript\n@mongo.index.text 5\nbio: string\n```\n",
102
- nodeType: ["prop"],
103
- argument: {
104
- optional: true,
105
- name: "weight",
106
- type: "number",
107
- description: "Field importance in search results (higher = more relevant). Defaults to `1`."
108
- }
109
- })
110
- },
72
+ index: { text: new AnnotationSpec({
73
+ description: "Creates a **legacy MongoDB text index** for full-text search with optional **weight** specification.\n\nUse this when you need per-field weight control. For simple full-text indexing without weights, use the generic `@db.index.fulltext` instead.\n\n**Example:**\n```atscript\n@db.mongo.index.text 5\nbio: string\n```\n",
74
+ nodeType: ["prop"],
75
+ argument: {
76
+ optional: true,
77
+ name: "weight",
78
+ type: "number",
79
+ description: "Field importance in search results (higher = more relevant). Defaults to `1`."
80
+ }
81
+ }) },
111
82
  search: {
112
83
  dynamic: new AnnotationSpec({
113
- description: "Creates a **dynamic MongoDB Search Index** that applies to the entire collection.\n\n- **Indexes all text fields automatically** (no need to specify fields).\n- Supports **language analyzers** for text tokenization.\n- Enables **fuzzy search** (typo tolerance) if needed.\n\n**Example:**\n```atscript\n@mongo.search.dynamic \"lucene.english\", 1\nexport interface MongoCollection {}\n```\n",
84
+ description: "Creates a **dynamic MongoDB Search Index** that applies to the entire collection.\n\n- **Indexes all text fields automatically** (no need to specify fields).\n- Supports **language analyzers** for text tokenization.\n- Enables **fuzzy search** (typo tolerance) if needed.\n\n**Example:**\n```atscript\n@db.mongo.search.dynamic \"lucene.english\", 1\nexport interface MongoCollection {}\n```\n",
114
85
  nodeType: ["interface"],
115
86
  multiple: false,
116
87
  argument: [{
@@ -127,7 +98,7 @@ const annotations = { mongo: {
127
98
  }]
128
99
  }),
129
100
  static: new AnnotationSpec({
130
- description: "Defines a **MongoDB Atlas Search Index** for the collection. The props can refer to this index using `@mongo.search.text` annotation.\n\n- **Creates a named search index** for full-text search.\n- **Specify analyzers and fuzzy search** behavior at the index level.\n- **Fields must explicitly use `@mongo.useTextSearch`** to be included in this search index.\n\n**Example:**\n```atscript\n@mongo.search.static \"lucene.english\", 1, \"mySearchIndex\"\nexport interface MongoCollection {}\n```\n",
101
+ description: "Defines a **MongoDB Atlas Search Index** for the collection. The props can refer to this index using `@db.mongo.search.text` annotation.\n\n- **Creates a named search index** for full-text search.\n- **Specify analyzers and fuzzy search** behavior at the index level.\n- **Fields must explicitly use `@db.mongo.search.text`** to be included in this search index.\n\n**Example:**\n```atscript\n@db.mongo.search.static \"lucene.english\", 1, \"mySearchIndex\"\nexport interface MongoCollection {}\n```\n",
131
102
  nodeType: ["interface"],
132
103
  multiple: true,
133
104
  argument: [
@@ -148,12 +119,12 @@ const annotations = { mongo: {
148
119
  optional: true,
149
120
  name: "indexName",
150
121
  type: "string",
151
- description: "The name of the search index. Fields must reference this name using `@mongo.search.text`. If not set, defaults to `\"DEFAULT\"`."
122
+ description: "The name of the search index. Fields must reference this name using `@db.mongo.search.text`. If not set, defaults to `\"DEFAULT\"`."
152
123
  }
153
124
  ]
154
125
  }),
155
126
  text: new AnnotationSpec({
156
- description: "Marks a field to be **included in a MongoDB Atlas Search Index** defined by `@mongo.search.static`.\n\n- **The field has to reference an existing search index name**.\n- If index name is not defined, a new search index with default attributes will be created.\n\n**Example:**\n```atscript\n@mongo.search.text \"lucene.english\", \"mySearchIndex\"\nfirstName: string\n```\n",
127
+ description: "Marks a field to be **included in a MongoDB Atlas Search Index** defined by `@db.mongo.search.static`.\n\n- **The field has to reference an existing search index name**.\n- If index name is not defined, a new search index with default attributes will be created.\n\n**Example:**\n```atscript\n@db.mongo.search.text \"lucene.english\", \"mySearchIndex\"\nfirstName: string\n```\n",
157
128
  nodeType: ["prop"],
158
129
  multiple: true,
159
130
  argument: [{
@@ -166,11 +137,11 @@ const annotations = { mongo: {
166
137
  optional: true,
167
138
  name: "indexName",
168
139
  type: "string",
169
- description: "The **name of the search index** defined in `@mongo.defineTextSearch`. This links the field to the correct index. If not set, defaults to `\"DEFAULT\"`."
140
+ description: "The **name of the search index** defined in `@db.mongo.search.static`. This links the field to the correct index. If not set, defaults to `\"DEFAULT\"`."
170
141
  }]
171
142
  }),
172
143
  vector: new AnnotationSpec({
173
- description: "Creates a **MongoDB Vector Search Index** for **semantic search, embeddings, and AI-powered search**.\n\n- Each field that stores vector embeddings **must define its own vector index**.\n- Supports **cosine similarity, Euclidean distance, and dot product similarity**.\n- Vector fields must be an **array of numbers**.\n\n**Example:**\n```atscript\n@mongo.search.vector 512, \"cosine\"\nembedding: mongo.vector\n```\n",
144
+ description: "Creates a **MongoDB Vector Search Index** for **semantic search, embeddings, and AI-powered search**.\n\n- Each field that stores vector embeddings **must define its own vector index**.\n- Supports **cosine similarity, Euclidean distance, and dot product similarity**.\n- Vector fields must be an **array of numbers**.\n\n**Example:**\n```atscript\n@db.mongo.search.vector 512, \"cosine\"\nembedding: mongo.vector\n```\n",
174
145
  nodeType: ["prop"],
175
146
  multiple: false,
176
147
  argument: [
@@ -208,7 +179,7 @@ const annotations = { mongo: {
208
179
  ]
209
180
  }),
210
181
  filter: new AnnotationSpec({
211
- description: "Assigns a field as a **filter field** for a **MongoDB Vector Search Index**.\n\n- The assigned field **must be indexed** for efficient filtering.\n- Filters allow vector search queries to return results **only within a specific category, user group, or tag**.\n- The vector index must be defined using `@mongo.search.vector`.\n\n**Example:**\n```atscript\n@mongo.search.vector 512, \"cosine\"\nembedding: number[]\n\n@mongo.search.filter \"embedding\"\ncategory: string\n```\n",
182
+ description: "Assigns a field as a **filter field** for a **MongoDB Vector Search Index**.\n\n- The assigned field **must be indexed** for efficient filtering.\n- Filters allow vector search queries to return results **only within a specific category, user group, or tag**.\n- The vector index must be defined using `@db.mongo.search.vector`.\n\n**Example:**\n```atscript\n@db.mongo.search.vector 512, \"cosine\"\nembedding: number[]\n\n@db.mongo.search.filter \"embedding\"\ncategory: string\n```\n",
212
183
  nodeType: ["prop"],
213
184
  multiple: true,
214
185
  argument: [{
@@ -220,7 +191,7 @@ const annotations = { mongo: {
220
191
  })
221
192
  },
222
193
  patch: { strategy: new AnnotationSpec({
223
- 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",
194
+ 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@db.mongo.patch.strategy \"merge\"\nsettings: {\n notifications: boolean\n preferences: {\n theme: string\n }\n}\n```\n",
224
195
  nodeType: ["prop"],
225
196
  multiple: false,
226
197
  argument: {
@@ -240,7 +211,7 @@ const annotations = { mongo: {
240
211
  if (!isStructure(def) && !isInterface(def) && !isArray(def)) wrongType = true;
241
212
  } else if (!isStructure(definition) && !isInterface(definition) && !isArray(definition)) wrongType = true;
242
213
  if (wrongType) errors.push({
243
- message: `[mongo] type of object or array expected when using @mongo.patch.strategy`,
214
+ message: `[db.mongo] type of object or array expected when using @db.mongo.patch.strategy`,
244
215
  severity: 1,
245
216
  range: token.range
246
217
  });
@@ -248,7 +219,7 @@ const annotations = { mongo: {
248
219
  }
249
220
  }) },
250
221
  array: { uniqueItems: new AnnotationSpec({
251
- 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 arrays 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",
222
+ 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 `@expect.array.key` properties, *uniqueness is implied* and this annotation is unnecessary (but harmless).\n\n**Example:**\n```atscript\n@db.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",
252
223
  nodeType: ["prop"],
253
224
  multiple: false,
254
225
  validate(token, args, doc) {
@@ -262,14 +233,14 @@ const annotations = { mongo: {
262
233
  if (!isArray(def)) wrongType = true;
263
234
  } else if (!isArray(definition)) wrongType = true;
264
235
  if (wrongType) errors.push({
265
- message: `[mongo] type of array expected when using @mongo.array.uniqueItems`,
236
+ message: `[db.mongo] type of array expected when using @db.mongo.array.uniqueItems`,
266
237
  severity: 1,
267
238
  range: token.range
268
239
  });
269
240
  return errors;
270
241
  }
271
242
  }) }
272
- } };
243
+ };
273
244
 
274
245
  //#endregion
275
246
  //#region packages/mongo/src/plugin/primitives.ts
@@ -277,7 +248,7 @@ const primitives = { mongo: { extensions: {
277
248
  objectId: {
278
249
  type: "string",
279
250
  documentation: "Represents a **MongoDB ObjectId**.\n\n- Stored as a **string** but can be converted to an ObjectId at runtime.\n- Useful for handling `_id` fields and queries that require ObjectId conversion.\n- Automatically converts string `_id` values into **MongoDB ObjectId** when needed.\n\n**Example:**\n```atscript\nuserId: mongo.objectId\n```\n",
280
- expect: { pattern: /^[a-fA-F0-9]{24}$/ }
251
+ annotations: { "expect.pattern": { pattern: "^[a-fA-F0-9]{24}$" } }
281
252
  },
282
253
  vector: {
283
254
  type: {
@@ -295,7 +266,7 @@ const MongoPlugin = () => ({
295
266
  config() {
296
267
  return {
297
268
  primitives,
298
- annotations
269
+ annotations: { db: { mongo: annotations } }
299
270
  };
300
271
  }
301
272
  });
@@ -306,7 +277,7 @@ const validateMongoIdPlugin = (ctx, def, value) => {
306
277
  if (ctx.path === "_id" && def.type.tags.has("objectId")) return ctx.validateAnnotatedType(def, value instanceof ObjectId ? value.toString() : value);
307
278
  };
308
279
  const validateMongoUniqueArrayItemsPlugin = (ctx, def, value) => {
309
- if (def.metadata.has("mongo.array.uniqueItems") && def.type.kind === "array") {
280
+ if (def.metadata.has("db.mongo.array.uniqueItems") && def.type.kind === "array") {
310
281
  if (Array.isArray(value)) {
311
282
  const separator = "▼↩";
312
283
  const seen = new Set();
@@ -340,7 +311,7 @@ else obj[key] = value;
340
311
  }
341
312
  var CollectionPatcher = class CollectionPatcher {
342
313
  /**
343
- * Extract a set of *key properties* (annotated with `@meta.isKey`) from an
314
+ * Extract a set of *key properties* (annotated with `@expect.array.key`) from an
344
315
  * array‐of‐objects type definition. These keys uniquely identify an element
345
316
  * inside the array and are later used for `$update`, `$remove` and `$upsert`.
346
317
  *
@@ -350,7 +321,7 @@ var CollectionPatcher = class CollectionPatcher {
350
321
  if (def.type.of.type.kind === "object") {
351
322
  const objType = def.type.of.type;
352
323
  const keyProps = new Set();
353
- for (const [key, val] of objType.props.entries()) if (val.metadata.get("meta.isKey")) keyProps.add(key);
324
+ for (const [key, val] of objType.props.entries()) if (val.metadata.get("expect.array.key")) keyProps.add(key);
354
325
  return keyProps;
355
326
  }
356
327
  return new Set();
@@ -359,7 +330,7 @@ var CollectionPatcher = class CollectionPatcher {
359
330
  * Build a runtime *Validator* that understands the extended patch payload.
360
331
  *
361
332
  * * Adds per‑array *patch* wrappers (the `$replace`, `$insert`, … fields).
362
- * * Honors `mongo.patch.strategy === "merge"` metadata.
333
+ * * Honors `db.mongo.patch.strategy === "merge"` metadata.
363
334
  *
364
335
  * @param collection Target collection wrapper
365
336
  * @returns Atscript Validator
@@ -372,12 +343,12 @@ var CollectionPatcher = class CollectionPatcher {
372
343
  for (const [prop, type] of def.type.props.entries()) obj.prop(prop, defineAnnotatedType().refTo(type).copyMetadata(type.metadata).optional(prop !== "_id").$type);
373
344
  return obj.$type;
374
345
  }
375
- if (def.type.kind === "array" && collection.flatMap.get(path)?.metadata.get("mongo.__topLevelArray") && !def.metadata.has("mongo.__patchArrayValue")) {
346
+ if (def.type.kind === "array" && collection.flatMap.get(path)?.metadata.get("db.mongo.__topLevelArray") && !def.metadata.has("db.mongo.__patchArrayValue")) {
376
347
  const defArray = def;
377
- const mergeStrategy = defArray.metadata.get("mongo.patch.strategy") === "merge";
348
+ const mergeStrategy = defArray.metadata.get("db.mongo.patch.strategy") === "merge";
378
349
  function getPatchType() {
379
350
  const isPrimitive$1 = isAnnotatedTypeOfPrimitive(defArray.type.of);
380
- if (isPrimitive$1) return defineAnnotatedType().refTo(def).copyMetadata(def.metadata).annotate("mongo.__patchArrayValue").optional().$type;
351
+ if (isPrimitive$1) return defineAnnotatedType().refTo(def).copyMetadata(def.metadata).annotate("db.mongo.__patchArrayValue").optional().$type;
381
352
  if (defArray.type.of.type.kind === "object") {
382
353
  const objType = defArray.type.of.type;
383
354
  const t = defineAnnotatedType("object").copyMetadata(defArray.type.of.metadata);
@@ -385,17 +356,17 @@ var CollectionPatcher = class CollectionPatcher {
385
356
  for (const [key, val] of objType.props.entries()) if (keyProps.size > 0) if (keyProps.has(key)) t.prop(key, defineAnnotatedType().refTo(val).copyMetadata(def.metadata).$type);
386
357
  else t.prop(key, defineAnnotatedType().refTo(val).copyMetadata(def.metadata).optional().$type);
387
358
  else t.prop(key, defineAnnotatedType().refTo(val).copyMetadata(def.metadata).optional(!!val.optional).$type);
388
- return defineAnnotatedType("array").of(t.$type).copyMetadata(def.metadata).annotate("mongo.__patchArrayValue").optional().$type;
359
+ return defineAnnotatedType("array").of(t.$type).copyMetadata(def.metadata).annotate("db.mongo.__patchArrayValue").optional().$type;
389
360
  }
390
361
  return undefined;
391
362
  }
392
- const fullType = defineAnnotatedType().refTo(def).copyMetadata(def.metadata).annotate("mongo.__patchArrayValue").optional().$type;
363
+ const fullType = defineAnnotatedType().refTo(def).copyMetadata(def.metadata).annotate("db.mongo.__patchArrayValue").optional().$type;
393
364
  const patchType = getPatchType();
394
365
  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;
395
366
  }
396
367
  return def;
397
368
  },
398
- partial: (def, path) => path !== "" && def.metadata.get("mongo.patch.strategy") === "merge"
369
+ partial: (def, path) => path !== "" && def.metadata.get("db.mongo.patch.strategy") === "merge"
399
370
  });
400
371
  }
401
372
  /**
@@ -446,9 +417,9 @@ else t.prop(key, defineAnnotatedType().refTo(val).copyMetadata(def.metadata).opt
446
417
  for (const [_key, value] of Object.entries(payload)) {
447
418
  const key = evalKey(_key);
448
419
  const flatType = this.collection.flatMap.get(key);
449
- const topLevelArray = flatType?.metadata?.get("mongo.__topLevelArray");
420
+ const topLevelArray = flatType?.metadata?.get("db.mongo.__topLevelArray");
450
421
  if (typeof value === "object" && topLevelArray) this.parseArrayPatch(key, value);
451
- else if (typeof value === "object" && this.collection.flatMap.get(key)?.metadata?.get("mongo.patch.strategy") === "merge") this.flattenPayload(value, key);
422
+ else if (typeof value === "object" && this.collection.flatMap.get(key)?.metadata?.get("db.mongo.patch.strategy") === "merge") this.flattenPayload(value, key);
452
423
  else if (key !== "_id") this._set(key, value);
453
424
  }
454
425
  return this.updatePipeline;
@@ -502,7 +473,7 @@ else if (key !== "_id") this._set(key, value);
502
473
  * - unique / keyed → delegate to _upsert (insert-or-update)
503
474
  */ _insert(key, input, keyProps) {
504
475
  if (!input?.length) return;
505
- const uniqueItems = this.collection.flatMap.get(key)?.metadata?.has("mongo.array.uniqueItems");
476
+ const uniqueItems = this.collection.flatMap.get(key)?.metadata?.has("db.mongo.array.uniqueItems");
506
477
  if (uniqueItems || keyProps.size > 0) this._upsert(key, input, keyProps);
507
478
  else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
508
479
  }
@@ -540,7 +511,7 @@ else this._set(key, { $concatArrays: [{ $ifNull: [`$${key}`, []] }, input] });
540
511
  */ _update(key, input, keyProps) {
541
512
  if (!input?.length) return;
542
513
  if (keyProps.size > 0) {
543
- const mergeStrategy = this.collection.flatMap.get(key)?.metadata?.get("mongo.patch.strategy") === "merge";
514
+ const mergeStrategy = this.collection.flatMap.get(key)?.metadata?.get("db.mongo.patch.strategy") === "merge";
544
515
  const keys = [...keyProps];
545
516
  this._set(key, { $reduce: {
546
517
  input,
@@ -749,13 +720,13 @@ else {
749
720
  }
750
721
  _prepareIndexesForCollection() {
751
722
  const typeMeta = this.type.metadata;
752
- const dynamicText = typeMeta.get("mongo.search.dynamic");
723
+ const dynamicText = typeMeta.get("db.mongo.search.dynamic");
753
724
  if (dynamicText) this._setSearchIndex("dynamic_text", "_", {
754
725
  mappings: { dynamic: true },
755
726
  analyzer: dynamicText.analyzer,
756
727
  text: { fuzzy: { maxEdits: dynamicText.fuzzy || 0 } }
757
728
  });
758
- for (const textSearch of typeMeta.get("mongo.search.static") || []) this._setSearchIndex("search_text", textSearch.indexName, {
729
+ for (const textSearch of typeMeta.get("db.mongo.search.static") || []) this._setSearchIndex("search_text", textSearch.indexName, {
759
730
  mappings: { fields: {} },
760
731
  analyzer: textSearch.analyzer,
761
732
  text: { fuzzy: { maxEdits: textSearch.fuzzy || 0 } }
@@ -778,25 +749,35 @@ else {
778
749
  }
779
750
  }
780
751
  _prepareIndexesForField(fieldName, metadata) {
781
- for (const index of metadata.get("mongo.index.plain") || []) this._addIndexField("plain", index === true ? fieldName : index, fieldName);
782
- for (const index of metadata.get("mongo.index.unique") || []) this._addIndexField("unique", index === true ? fieldName : index, fieldName);
783
- const textWeight = metadata.get("mongo.index.text");
752
+ for (const index of metadata.get("db.index.plain") || []) {
753
+ const name = index === true ? fieldName : index.name || fieldName;
754
+ this._addIndexField("plain", name, fieldName);
755
+ }
756
+ for (const index of metadata.get("db.index.unique") || []) {
757
+ const name = index === true ? fieldName : index.name || fieldName;
758
+ this._addIndexField("unique", name, fieldName);
759
+ }
760
+ for (const index of metadata.get("db.index.fulltext") || []) {
761
+ const name = index === true ? "" : index.name || "";
762
+ this._addIndexField("text", name, fieldName, 1);
763
+ }
764
+ const textWeight = metadata.get("db.mongo.index.text");
784
765
  if (textWeight) this._addIndexField("text", "", fieldName, textWeight === true ? 1 : textWeight);
785
- for (const index of metadata.get("mongo.search.text") || []) this._addFieldToSearchIndex("search_text", index.indexName, fieldName, index.analyzer);
786
- const vectorIndex = metadata.get("mongo.search.vector");
766
+ for (const index of metadata.get("db.mongo.search.text") || []) this._addFieldToSearchIndex("search_text", index.indexName, fieldName, index.analyzer);
767
+ const vectorIndex = metadata.get("db.mongo.search.vector");
787
768
  if (vectorIndex) this._setSearchIndex("vector", vectorIndex.indexName || fieldName, { fields: [{
788
769
  type: "vector",
789
770
  path: fieldName,
790
771
  similarity: vectorIndex.similarity || "dotProduct",
791
772
  numDimensions: vectorIndex.dimensions
792
773
  }] });
793
- for (const index of metadata.get("mongo.search.filter") || []) this._vectorFilters.set(indexKey("vector", index.indexName), fieldName);
774
+ for (const index of metadata.get("db.mongo.search.filter") || []) this._vectorFilters.set(indexKey("vector", index.indexName), fieldName);
794
775
  }
795
776
  _flatten() {
796
777
  if (!this._flatMap) {
797
778
  this._prepareIndexesForCollection();
798
779
  this._flatMap = flattenAnnotatedType(this.type, {
799
- topLevelArrayTag: "mongo.__topLevelArray",
780
+ topLevelArrayTag: "db.mongo.__topLevelArray",
800
781
  excludePhantomTypes: true,
801
782
  onField: (path, _type, metadata) => this._prepareIndexesForField(path, metadata)
802
783
  });
@@ -1008,8 +989,8 @@ else if (this.idType !== "objectId") throw new Error("Missing \"_id\" field");
1008
989
  this._vectorFilters = new Map();
1009
990
  this._uniqueProps = new Set();
1010
991
  if (!isAnnotatedType(_type)) throw new Error("Atscript Annotated Type expected");
1011
- const name = _type.metadata.get("mongo.collection");
1012
- if (!name) throw new Error("@mongo.collection annotation expected with collection name");
992
+ const name = _type.metadata.get("db.table");
993
+ if (!name) throw new Error("@db.table annotation expected with collection name");
1013
994
  if (_type.type.kind !== "object") throw new Error("Mongo collection must be an object type");
1014
995
  this.name = name;
1015
996
  this.collection = asMongo.db.collection(name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/mongo",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "description": "Mongodb plugin for atscript.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -19,9 +19,14 @@
19
19
  "directory": "packages/mongo"
20
20
  },
21
21
  "files": [
22
- "dist"
22
+ "dist",
23
+ "skills",
24
+ "scripts/setup-skills.js"
23
25
  ],
24
26
  "type": "module",
27
+ "bin": {
28
+ "atscript-mongo-skill": "./scripts/setup-skills.js"
29
+ },
25
30
  "main": "dist/index.mjs",
26
31
  "types": "dist/index.d.ts",
27
32
  "exports": {
@@ -37,11 +42,12 @@
37
42
  },
38
43
  "peerDependencies": {
39
44
  "mongodb": "^6.17.0",
40
- "@atscript/core": "^0.1.27",
41
- "@atscript/typescript": "^0.1.27"
45
+ "@atscript/core": "^0.1.28",
46
+ "@atscript/typescript": "^0.1.28"
42
47
  },
43
48
  "scripts": {
44
49
  "pub": "pnpm publish --access public",
45
- "test": "vitest"
50
+ "test": "vitest",
51
+ "setup-skills": "node ./scripts/setup-skills.js"
46
52
  }
47
53
  }
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ /* prettier-ignore */
3
+ import fs from 'fs'
4
+ import path from 'path'
5
+ import os from 'os'
6
+ import { fileURLToPath } from 'url'
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+
10
+ const SKILL_NAME = 'atscript-mongo'
11
+ const SKILL_SRC = path.join(__dirname, '..', 'skills', SKILL_NAME)
12
+
13
+ if (!fs.existsSync(SKILL_SRC)) {
14
+ console.error(`No skills found at ${SKILL_SRC}`)
15
+ console.error('Add your SKILL.md files to the skills/' + SKILL_NAME + '/ directory first.')
16
+ process.exit(1)
17
+ }
18
+
19
+ const AGENTS = {
20
+ 'Claude Code': { dir: '.claude/skills', global: path.join(os.homedir(), '.claude', 'skills') },
21
+ 'Cursor': { dir: '.cursor/skills', global: path.join(os.homedir(), '.cursor', 'skills') },
22
+ 'Windsurf': { dir: '.windsurf/skills', global: path.join(os.homedir(), '.windsurf', 'skills') },
23
+ 'Codex': { dir: '.codex/skills', global: path.join(os.homedir(), '.codex', 'skills') },
24
+ 'OpenCode': { dir: '.opencode/skills', global: path.join(os.homedir(), '.opencode', 'skills') },
25
+ }
26
+
27
+ const args = process.argv.slice(2)
28
+ const isGlobal = args.includes('--global') || args.includes('-g')
29
+ const isPostinstall = args.includes('--postinstall')
30
+ let installed = 0, skipped = 0
31
+ const installedDirs = []
32
+
33
+ for (const [agentName, cfg] of Object.entries(AGENTS)) {
34
+ const targetBase = isGlobal ? cfg.global : path.join(process.cwd(), cfg.dir)
35
+ const agentRootDir = path.dirname(cfg.global) // Check if the agent has ever been installed globally
36
+
37
+ // In postinstall mode: silently skip agents that aren't set up globally
38
+ if (isPostinstall || isGlobal) {
39
+ if (!fs.existsSync(agentRootDir)) { skipped++; continue }
40
+ }
41
+
42
+ const dest = path.join(targetBase, SKILL_NAME)
43
+ try {
44
+ fs.mkdirSync(dest, { recursive: true })
45
+ fs.cpSync(SKILL_SRC, dest, { recursive: true })
46
+ console.log(`✅ ${agentName}: installed to ${dest}`)
47
+ installed++
48
+ if (!isGlobal) installedDirs.push(cfg.dir + '/' + SKILL_NAME)
49
+ } catch (err) {
50
+ console.warn(`⚠️ ${agentName}: failed — ${err.message}`)
51
+ }
52
+ }
53
+
54
+ // Add locally-installed skill dirs to .gitignore
55
+ if (!isGlobal && installedDirs.length > 0) {
56
+ const gitignorePath = path.join(process.cwd(), '.gitignore')
57
+ let gitignoreContent = ''
58
+ try { gitignoreContent = fs.readFileSync(gitignorePath, 'utf8') } catch {}
59
+ const linesToAdd = installedDirs.filter(d => !gitignoreContent.includes(d))
60
+ if (linesToAdd.length > 0) {
61
+ const hasHeader = gitignoreContent.includes('# AI agent skills')
62
+ const block = (gitignoreContent && !gitignoreContent.endsWith('\n') ? '\n' : '')
63
+ + (hasHeader ? '' : '\n# AI agent skills (auto-generated by setup-skills)\n')
64
+ + linesToAdd.join('\n') + '\n'
65
+ fs.appendFileSync(gitignorePath, block)
66
+ console.log(`📝 Added ${linesToAdd.length} entries to .gitignore`)
67
+ }
68
+ }
69
+
70
+ if (installed === 0 && isPostinstall) {
71
+ // Silence is fine — no agents present, nothing to do
72
+ } else if (installed === 0 && skipped === Object.keys(AGENTS).length) {
73
+ console.log('No agent directories detected. Try --global or run without it for project-local install.')
74
+ } else if (installed === 0) {
75
+ console.log('Nothing installed. Run without --global to install project-locally.')
76
+ } else {
77
+ console.log(`\n✨ Done! Restart your AI agent to pick up the "${SKILL_NAME}" skill.`)
78
+ }
File without changes
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: atscript-mongo
3
+ description: Use this skill when working with @atscript/mongo — to define MongoDB collections with @db.table and @db.mongo.collection, create indexes with @db.index.plain/@db.index.unique/@db.mongo.index.text, configure Atlas Search with @db.mongo.search.*, control patch strategies with @db.mongo.patch.strategy, use AsCollection for CRUD operations (insert/replace/update/syncIndexes), validate data with createValidator, or configure MongoPlugin in atscript.config.
4
+ ---
5
+
6
+ # @atscript/mongo
7
+
8
+ MongoDB metadata extension for Atscript. Defines annotations for collections, indexes, search, and patch strategies, plus runtime classes (`AsCollection`, `AsMongo`) with built-in validation, filtering, querying, and writing.
9
+
10
+ ## How to use this skill
11
+
12
+ Read the domain file that matches the task. Do not load all files — only what you need.
13
+
14
+ | Domain | File | Load when... |
15
+ |--------|------|-------------|
16
+ | Core setup & plugin config | [core.md](core.md) | Installing the plugin, configuring atscript.config, understanding the plugin architecture |
17
+ | Annotations reference | [annotations.md](annotations.md) | Writing .as files with database and MongoDB annotations, understanding annotation arguments |
18
+ | Collections & CRUD | [collections.md](collections.md) | Using AsCollection/AsMongo for insert, replace, update, query, or sync indexes |
19
+ | Patch strategies | [patches.md](patches.md) | Working with @db.mongo.patch.strategy, array patch operations ($insert/$upsert/$update/$remove/$replace) |
20
+
21
+ ## Quick reference
22
+
23
+ ```atscript
24
+ @db.table 'users'
25
+ @db.mongo.collection
26
+ export interface User {
27
+ @db.index.unique 'email_idx'
28
+ email: string.email
29
+
30
+ @db.mongo.index.text 5
31
+ name: string
32
+
33
+ @db.index.plain 'status_idx'
34
+ isActive: boolean
35
+ }
36
+ ```
37
+
38
+ ```typescript
39
+ import { AsMongo } from '@atscript/mongo'
40
+ import { User } from './user.as'
41
+
42
+ const asMongo = new AsMongo('mongodb://localhost:27017/mydb')
43
+ const users = asMongo.getCollection(User)
44
+ await users.insert({ email: 'a@b.com', name: 'Alice', isActive: true })
45
+ ```