@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.cjs +63 -82
- package/dist/index.mjs +63 -82
- package/package.json +11 -5
- package/scripts/setup-skills.js +78 -0
- package/skills/atscript-mongo/.gitkeep +0 -0
- package/skills/atscript-mongo/SKILL.md +45 -0
- package/skills/atscript-mongo/annotations.md +168 -0
- package/skills/atscript-mongo/collections.md +141 -0
- package/skills/atscript-mongo/core.md +83 -0
- package/skills/atscript-mongo/patches.md +205 -0
package/dist/index.mjs
CHANGED
|
@@ -19,15 +19,10 @@ const analyzers = [
|
|
|
19
19
|
"lucene.russian",
|
|
20
20
|
"lucene.arabic"
|
|
21
21
|
];
|
|
22
|
-
const annotations = {
|
|
22
|
+
const annotations = {
|
|
23
23
|
collection: new AnnotationSpec({
|
|
24
|
-
description: "
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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.
|
|
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.
|
|
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 array
|
|
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:
|
|
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 `@
|
|
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("
|
|
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("
|
|
782
|
-
|
|
783
|
-
|
|
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("
|
|
1012
|
-
if (!name) throw new Error("@
|
|
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.
|
|
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.
|
|
41
|
-
"@atscript/typescript": "^0.1.
|
|
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
|
+
```
|