@atscript/db-mongo 0.1.54 → 0.1.55

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-mongo",
3
- "version": "0.1.54",
3
+ "version": "0.1.55",
4
4
  "description": "Mongodb plugin for atscript.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -18,13 +18,8 @@
18
18
  "url": "git+https://github.com/moostjs/atscript-db.git",
19
19
  "directory": "packages/db-mongo"
20
20
  },
21
- "bin": {
22
- "atscript-db-mongo-skill": "./scripts/setup-skills.js"
23
- },
24
21
  "files": [
25
- "dist",
26
- "skills",
27
- "scripts/setup-skills.js"
22
+ "dist"
28
23
  ],
29
24
  "type": "module",
30
25
  "main": "dist/index.mjs",
@@ -51,23 +46,22 @@
51
46
  "access": "public"
52
47
  },
53
48
  "devDependencies": {
54
- "@atscript/core": "^0.1.48",
55
- "@atscript/typescript": "^0.1.48",
49
+ "@atscript/core": "^0.1.50",
50
+ "@atscript/typescript": "^0.1.50",
56
51
  "mongodb": "^6.17.0",
57
- "unplugin-atscript": "^0.1.48"
52
+ "unplugin-atscript": "^0.1.50"
58
53
  },
59
54
  "peerDependencies": {
60
- "@atscript/core": "^0.1.48",
61
- "@atscript/typescript": "^0.1.48",
55
+ "@atscript/core": "^0.1.50",
56
+ "@atscript/typescript": "^0.1.50",
62
57
  "mongodb": "^6.17.0",
63
- "@atscript/db": "^0.1.54"
58
+ "@atscript/db": "^0.1.55"
64
59
  },
65
60
  "scripts": {
66
61
  "postinstall": "asc -f dts",
67
62
  "build": "vp pack",
68
63
  "dev": "vp pack --watch",
69
64
  "test": "vp test",
70
- "check": "vp check",
71
- "setup-skills": "node ./scripts/setup-skills.js"
65
+ "check": "vp check"
72
66
  }
73
67
  }
@@ -1,88 +0,0 @@
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-db-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 = new Set(process.argv.slice(2));
28
- const isGlobal = args.has("--global") || args.has("-g");
29
- const isPostinstall = args.has("--postinstall");
30
- let installed = 0,
31
- skipped = 0;
32
- const installedDirs = [];
33
-
34
- for (const [agentName, cfg] of Object.entries(AGENTS)) {
35
- const targetBase = isGlobal ? cfg.global : path.join(process.cwd(), cfg.dir);
36
- const agentRootDir = path.dirname(cfg.global); // Check if the agent has ever been installed globally
37
-
38
- // In postinstall mode: silently skip agents that aren't set up globally
39
- if (isPostinstall || isGlobal) {
40
- if (!fs.existsSync(agentRootDir)) {
41
- skipped++;
42
- continue;
43
- }
44
- }
45
-
46
- const dest = path.join(targetBase, SKILL_NAME);
47
- try {
48
- fs.mkdirSync(dest, { recursive: true });
49
- fs.cpSync(SKILL_SRC, dest, { recursive: true });
50
- console.log(`✅ ${agentName}: installed to ${dest}`);
51
- installed++;
52
- if (!isGlobal) installedDirs.push(cfg.dir + "/" + SKILL_NAME);
53
- } catch (err) {
54
- console.warn(`⚠️ ${agentName}: failed — ${err.message}`);
55
- }
56
- }
57
-
58
- // Add locally-installed skill dirs to .gitignore
59
- if (!isGlobal && installedDirs.length > 0) {
60
- const gitignorePath = path.join(process.cwd(), ".gitignore");
61
- let gitignoreContent = "";
62
- try {
63
- gitignoreContent = fs.readFileSync(gitignorePath, "utf8");
64
- } catch {}
65
- const linesToAdd = installedDirs.filter((d) => !gitignoreContent.includes(d));
66
- if (linesToAdd.length > 0) {
67
- const hasHeader = gitignoreContent.includes("# AI agent skills");
68
- const block =
69
- (gitignoreContent && !gitignoreContent.endsWith("\n") ? "\n" : "") +
70
- (hasHeader ? "" : "\n# AI agent skills (auto-generated by setup-skills)\n") +
71
- linesToAdd.join("\n") +
72
- "\n";
73
- fs.appendFileSync(gitignorePath, block);
74
- console.log(`📝 Added ${linesToAdd.length} entries to .gitignore`);
75
- }
76
- }
77
-
78
- if (installed === 0 && isPostinstall) {
79
- // Silence is fine — no agents present, nothing to do
80
- } else if (installed === 0 && skipped === Object.keys(AGENTS).length) {
81
- console.log(
82
- "No agent directories detected. Try --global or run without it for project-local install.",
83
- );
84
- } else if (installed === 0) {
85
- console.log("Nothing installed. Run without --global to install project-locally.");
86
- } else {
87
- console.log(`\n✨ Done! Restart your AI agent to pick up the "${SKILL_NAME}" skill.`);
88
- }
File without changes
@@ -1,45 +0,0 @@
1
- ---
2
- name: atscript-db-mongo
3
- description: Use this skill when working with @atscript/db-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/db-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/db-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
- ```
@@ -1,168 +0,0 @@
1
- # Annotations Reference — @atscript/db-mongo
2
-
3
- > All database and MongoDB-specific annotations available when using the mongo plugin.
4
-
5
- ## Annotation Namespaces
6
-
7
- Annotations are split between core `@db.*` (database-generic) and `@db.mongo.*` (MongoDB-specific).
8
-
9
- ## Core `@db.*` Annotations
10
-
11
- These come from `@atscript/core` and are used by the mongo plugin at runtime.
12
-
13
- ### `@db.table "name"` (interface-level)
14
-
15
- Names the collection. **Required** for `AsCollection` to work.
16
-
17
- ```atscript
18
- @db.table 'users'
19
- export interface User {
20
- name: string
21
- }
22
- ```
23
-
24
- ### `@db.index.plain "name?", "sort?"` (field-level, multiple)
25
-
26
- Standard index. Fields sharing the same name form a compound index.
27
-
28
- ```atscript
29
- @db.table 'products'
30
- export interface Product {
31
- @db.index.plain 'cat_status'
32
- category: string
33
-
34
- @db.index.plain 'cat_status'
35
- status: string
36
- }
37
- ```
38
-
39
- ### `@db.index.unique "name?"` (field-level, multiple)
40
-
41
- Unique constraint index.
42
-
43
- ```atscript
44
- @db.table 'users'
45
- export interface User {
46
- @db.index.unique 'email_idx'
47
- email: string.email
48
- }
49
- ```
50
-
51
- ### `@db.index.fulltext "name?"` (field-level, multiple)
52
-
53
- Generic fulltext index (always weight 1 in MongoDB).
54
-
55
- ```atscript
56
- @db.table 'articles'
57
- export interface Article {
58
- @db.index.fulltext
59
- title: string
60
- }
61
- ```
62
-
63
- ## MongoDB-Specific `@db.mongo.*` Annotations
64
-
65
- ### `@db.mongo.collection` (interface-level, no args)
66
-
67
- Optional convenience annotation. When present, auto-injects `_id: mongo.objectId` if the interface doesn't define one. Validates that `_id` (if present) is not optional and is of type string, number, or mongo.objectId.
68
-
69
- ```atscript
70
- @db.table 'users'
71
- @db.mongo.collection
72
- export interface User {
73
- // _id: mongo.objectId — auto-injected
74
- name: string
75
- }
76
- ```
77
-
78
- ### `@db.mongo.autoIndexes true|false` (interface-level)
79
-
80
- Toggle automatic index creation when `syncIndexes()` is called. Default: true.
81
-
82
- ### `@db.mongo.index.text weight?` (field-level)
83
-
84
- MongoDB-specific text index with optional weight (number). Extends `@db.index.fulltext` with weight support.
85
-
86
- ```atscript
87
- @db.table 'articles'
88
- export interface Article {
89
- @db.mongo.index.text 10
90
- title: string
91
-
92
- @db.mongo.index.text 1
93
- body: string
94
- }
95
- ```
96
-
97
- ### `@db.mongo.search.dynamic "analyzer?", fuzzy?` (interface-level)
98
-
99
- Dynamic Atlas Search index.
100
-
101
- ### `@db.mongo.search.static "analyzer?", fuzzy?, "indexName?"` (interface-level, multiple)
102
-
103
- Named static Atlas Search index.
104
-
105
- ### `@db.mongo.search.text "analyzer?", "indexName?"` (field-level, multiple)
106
-
107
- Atlas Search text field mapping.
108
-
109
- ### `@db.mongo.search.vector dimensions, "similarity?", "indexName?"` (field-level)
110
-
111
- Vector search index. Similarity: `"cosine"`, `"euclidean"`, or `"dotProduct"`.
112
-
113
- ```atscript
114
- @db.table 'documents'
115
- export interface Document {
116
- @db.mongo.search.vector 1536, "cosine", "vector_idx"
117
- embedding: mongo.vector
118
- }
119
- ```
120
-
121
- ### `@db.mongo.search.filter "indexName"` (field-level, multiple)
122
-
123
- Pre-filter field for vector search.
124
-
125
- ### `@db.mongo.patch.strategy "replace"|"merge"` (field-level)
126
-
127
- Controls how nested objects and arrays are updated during patch operations. See [patches.md](patches.md) for details.
128
-
129
- ### `@db.mongo.array.uniqueItems` (field-level)
130
-
131
- Enforces set-semantics on array `$insert` operations — duplicates are silently dropped.
132
-
133
- ```atscript
134
- @db.table 'tags'
135
- export interface TaggedItem {
136
- @db.mongo.array.uniqueItems
137
- tags: string[]
138
- }
139
- ```
140
-
141
- ## Common Patterns
142
-
143
- ### Full collection definition
144
-
145
- ```atscript
146
- @db.table 'users'
147
- @db.mongo.collection
148
- export interface User {
149
- @db.index.unique 'email_idx'
150
- email: string.email
151
-
152
- @db.mongo.index.text 5
153
- @expect.minLength 2
154
- name: string
155
-
156
- @db.index.plain 'status_idx'
157
- isActive: boolean
158
-
159
- @db.mongo.patch.strategy 'merge'
160
- profile: {
161
- bio?: string
162
- avatar?: string
163
- }
164
-
165
- @db.mongo.array.uniqueItems
166
- tags?: string[]
167
- }
168
- ```
@@ -1,142 +0,0 @@
1
- # Collections & CRUD — @atscript/db-mongo
2
-
3
- > Using AsMongo and AsCollection for database operations.
4
-
5
- ## AsMongo
6
-
7
- Entry point for MongoDB operations. Wraps a `MongoClient` and provides a collection registry.
8
-
9
- ```typescript
10
- import { AsMongo } from "@atscript/db-mongo";
11
-
12
- // From connection string
13
- const asMongo = new AsMongo("mongodb://localhost:27017/mydb");
14
-
15
- // From existing MongoClient
16
- const asMongo = new AsMongo(existingClient);
17
-
18
- // With logger
19
- const asMongo = new AsMongo(connectionString, myLogger);
20
- ```
21
-
22
- ### `getCollection<T>(type, logger?)`
23
-
24
- Returns an `AsCollection<T>` for the given Atscript annotated type. Collections are cached per type.
25
-
26
- ```typescript
27
- import { User } from "./user.as";
28
-
29
- const users = asMongo.getCollection(User);
30
- ```
31
-
32
- ## AsCollection
33
-
34
- Core collection abstraction providing validation, CRUD operations, and index management.
35
-
36
- ### Properties
37
-
38
- - **`name`** — Collection name (from `@db.table`)
39
- - **`collection`** — Raw MongoDB `Collection` instance
40
- - **`flatMap`** — `Map<string, TAtscriptAnnotatedType>` of all fields in dot-notation
41
-
42
- ### Insert
43
-
44
- ```typescript
45
- // Insert one
46
- const result = await users.insert({
47
- email: "alice@example.com",
48
- name: "Alice",
49
- isActive: true,
50
- });
51
-
52
- // Insert many
53
- const result = await users.insert([
54
- { email: "alice@example.com", name: "Alice", isActive: true },
55
- { email: "bob@example.com", name: "Bob", isActive: false },
56
- ]);
57
- ```
58
-
59
- Validates the payload before inserting. Auto-generates `ObjectId` for `_id` if type is `mongo.objectId`.
60
-
61
- ### Replace
62
-
63
- ```typescript
64
- await users.replace({
65
- _id: "507f1f77bcf86cd799439011",
66
- email: "alice@new.com",
67
- name: "Alice Updated",
68
- isActive: true,
69
- });
70
- ```
71
-
72
- Validates the full document and replaces by `_id`.
73
-
74
- ### Update (Patch)
75
-
76
- ```typescript
77
- await users.update({
78
- _id: "507f1f77bcf86cd799439011",
79
- name: "New Name",
80
- // Only updates specified fields
81
- });
82
- ```
83
-
84
- Uses `CollectionPatcher` internally to build MongoDB aggregation pipelines. See [patches.md](patches.md) for array patch operations.
85
-
86
- ### Validation
87
-
88
- ```typescript
89
- // Get a validator for different contexts
90
- const insertValidator = users.getValidator("insert");
91
- const updateValidator = users.getValidator("update");
92
- const patchValidator = users.getValidator("patch");
93
-
94
- // Create a custom validator
95
- const validator = users.createValidator({
96
- partial: true,
97
- plugins: [myPlugin],
98
- skipList: new Set(["internalField"]),
99
- });
100
- ```
101
-
102
- ### Index Management
103
-
104
- ```typescript
105
- // Sync indexes — creates/drops to match .as definitions
106
- await users.syncIndexes();
107
- ```
108
-
109
- Only manages indexes prefixed with `atscript__`. User-created indexes are not touched.
110
-
111
- Reads index definitions from:
112
-
113
- - `@db.index.plain` → standard indexes
114
- - `@db.index.unique` → unique indexes
115
- - `@db.index.fulltext` → text indexes (weight 1)
116
- - `@db.mongo.index.text` → text indexes (custom weight)
117
- - `@db.mongo.search.*` → Atlas Search indexes
118
-
119
- ### Querying
120
-
121
- ```typescript
122
- // Find documents
123
- const cursor = users.collection.find({ isActive: true });
124
-
125
- // Use the raw MongoDB collection for queries
126
- const doc = await users.collection.findOne({ _id: users.prepareId(id) });
127
- ```
128
-
129
- ### `prepareId(id)`
130
-
131
- Converts a string ID to `ObjectId` if the collection uses `mongo.objectId` type for `_id`.
132
-
133
- ```typescript
134
- const objectId = users.prepareId("507f1f77bcf86cd799439011");
135
- ```
136
-
137
- ## Best Practices
138
-
139
- - Use `getValidator()` for context-specific validation before custom operations
140
- - Call `syncIndexes()` on application startup to ensure indexes match definitions
141
- - Use `prepareId()` when working with raw MongoDB queries to handle ObjectId conversion
142
- - The `flatMap` property is lazily built — first access triggers computation
@@ -1,84 +0,0 @@
1
- # Core Setup — @atscript/db-mongo
2
-
3
- > Plugin installation, configuration, and architecture overview.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- npm install @atscript/db-mongo
9
- # peer dependencies:
10
- npm install @atscript/core @atscript/typescript mongodb
11
- ```
12
-
13
- ## Plugin Configuration
14
-
15
- Add `MongoPlugin()` to your `atscript.config.ts`:
16
-
17
- ```typescript
18
- import { defineConfig } from "@atscript/core";
19
- import { ts } from "@atscript/typescript";
20
- import MongoPlugin from "@atscript/db-mongo/plugin";
21
-
22
- export default defineConfig({
23
- rootDir: "src",
24
- plugins: [ts(), MongoPlugin()],
25
- });
26
- ```
27
-
28
- The plugin registers:
29
-
30
- - **Primitives**: `mongo.objectId` (24-char hex string), `mongo.vector` (number array)
31
- - **Annotations**: All `@db.mongo.*` annotations (collection, indexes, search, patch, array)
32
-
33
- ## Architecture
34
-
35
- ```
36
- @atscript/db-mongo
37
- ├── plugin/
38
- │ ├── index.ts — MongoPlugin factory
39
- │ ├── annotations.ts — All db.mongo.* annotation specs
40
- │ └── primitives.ts — mongo.objectId, mongo.vector
41
- └── lib/
42
- ├── as-mongo.ts — AsMongo: MongoDB client wrapper
43
- ├── as-collection.ts — AsCollection: collection abstraction (validation, indexes, CRUD)
44
- ├── collection-patcher.ts — Converts patch payloads to MongoDB aggregation pipelines
45
- └── validate-plugins.ts — Validator plugins for ObjectId and unique arrays
46
- ```
47
-
48
- ## Primitives
49
-
50
- ### `mongo.objectId`
51
-
52
- A string type constrained to `/^[a-fA-F0-9]{24}$/`. Used for MongoDB `_id` fields.
53
-
54
- ```atscript
55
- export interface User {
56
- _id: mongo.objectId
57
- name: string
58
- }
59
- ```
60
-
61
- ### `mongo.vector`
62
-
63
- An alias for `number[]`. Used for vector search fields.
64
-
65
- ```atscript
66
- export interface Document {
67
- embedding: mongo.vector
68
- }
69
- ```
70
-
71
- ## Regenerating atscript.d.ts
72
-
73
- After annotation changes, regenerate the type declarations:
74
-
75
- ```bash
76
- cd packages/db-mongo && node ../typescript/dist/cli.cjs -f dts
77
- ```
78
-
79
- ## Best Practices
80
-
81
- - Always use `@db.table` to name your collections — it's required by `AsCollection`
82
- - `@db.mongo.collection` is optional — it only auto-injects `_id: mongo.objectId` if missing
83
- - Use `mongo.objectId` type for `_id` fields when you want ObjectId-based IDs
84
- - Use `string` type for `_id` when you want string-based IDs
@@ -1,206 +0,0 @@
1
- # Patch Strategies — @atscript/db-mongo
2
-
3
- > How `@db.mongo.patch.strategy` and array patch operations work.
4
-
5
- ## Overview
6
-
7
- When updating documents via `AsCollection.update()`, the `CollectionPatcher` converts your patch payload into MongoDB aggregation pipeline stages. The behavior depends on two things:
8
-
9
- 1. **`@db.mongo.patch.strategy`** on objects — controls whether nested objects are replaced or merged
10
- 2. **Array key fields** (`@expect.array.key`) and patch operations — controls how array elements are matched and modified
11
-
12
- ## Object Patch Strategies
13
-
14
- ### Default (no annotation) — Replace
15
-
16
- Without `@db.mongo.patch.strategy`, nested objects are fully replaced:
17
-
18
- ```atscript
19
- @db.table 'users'
20
- export interface User {
21
- address: {
22
- line1: string
23
- city: string
24
- zip: string
25
- }
26
- }
27
- ```
28
-
29
- ```typescript
30
- // This replaces the entire address object
31
- await users.update({
32
- _id: id,
33
- address: { line1: "123 Main St", city: "NYC", zip: "10001" },
34
- });
35
- ```
36
-
37
- ### `@db.mongo.patch.strategy 'replace'`
38
-
39
- Explicit replacement — same as default. The entire nested object is overwritten.
40
-
41
- ### `@db.mongo.patch.strategy 'merge'`
42
-
43
- Individual fields within the nested object are updated without affecting unspecified fields:
44
-
45
- ```atscript
46
- @db.table 'users'
47
- export interface User {
48
- @db.mongo.patch.strategy 'merge'
49
- contacts: {
50
- email: string
51
- phone: string
52
- }
53
- }
54
- ```
55
-
56
- ```typescript
57
- // Only updates phone, email is preserved
58
- await users.update({
59
- _id: id,
60
- contacts: { phone: "+1-555-0100" },
61
- });
62
- ```
63
-
64
- ### Nested strategies
65
-
66
- Strategies can be applied at any nesting level:
67
-
68
- ```atscript
69
- @db.table 'config'
70
- export interface Config {
71
- @db.mongo.patch.strategy 'merge'
72
- settings: {
73
- @db.mongo.patch.strategy 'replace'
74
- theme: { primary: string, secondary: string }
75
-
76
- @db.mongo.patch.strategy 'merge'
77
- notifications: { email: boolean, push: boolean }
78
- }
79
- }
80
- ```
81
-
82
- ## Array Patch Operations
83
-
84
- Top-level arrays in a patch payload use a structured format with operation keys.
85
-
86
- ### `$replace`
87
-
88
- Replaces the entire array:
89
-
90
- ```typescript
91
- await collection.update({
92
- _id: id,
93
- tags: { $replace: ["new", "tags", "only"] },
94
- });
95
- ```
96
-
97
- ### `$insert`
98
-
99
- Appends items to the array:
100
-
101
- ```typescript
102
- await collection.update({
103
- _id: id,
104
- tags: { $insert: ["newTag1", "newTag2"] },
105
- });
106
- ```
107
-
108
- If `@db.mongo.array.uniqueItems` is set, duplicates are silently dropped (uses `$setUnion`).
109
-
110
- ### `$upsert`
111
-
112
- Insert-or-update by key. For keyed arrays (`@expect.array.key`), removes existing elements matching the key(s) and appends the new ones:
113
-
114
- ```atscript
115
- @db.table 'products'
116
- export interface Product {
117
- items: {
118
- @expect.array.key
119
- sku: string
120
- quantity: number
121
- price: number
122
- }[]
123
- }
124
- ```
125
-
126
- ```typescript
127
- await products.update({
128
- _id: id,
129
- items: {
130
- $upsert: [
131
- { sku: "ABC", quantity: 10, price: 9.99 }, // replaces existing ABC or inserts
132
- ],
133
- },
134
- });
135
- ```
136
-
137
- For non-keyed arrays, behaves like `$addToSet` (deep equality).
138
-
139
- ### `$update`
140
-
141
- Updates existing array elements matched by key:
142
-
143
- ```typescript
144
- await products.update({
145
- _id: id,
146
- items: {
147
- $update: [
148
- { sku: "ABC", quantity: 20 }, // updates only quantity for sku=ABC
149
- ],
150
- },
151
- });
152
- ```
153
-
154
- With `@db.mongo.patch.strategy 'merge'` on the array field, uses `$mergeObjects` to merge into the matched element. Without it, replaces the matched element entirely.
155
-
156
- ### `$remove`
157
-
158
- Removes array elements:
159
-
160
- ```typescript
161
- // Keyed — removes by key match
162
- await products.update({
163
- _id: id,
164
- items: {
165
- $remove: [{ sku: "ABC" }],
166
- },
167
- });
168
-
169
- // Non-keyed — removes by deep equality
170
- await collection.update({
171
- _id: id,
172
- tags: {
173
- $remove: ["obsoleteTag"],
174
- },
175
- });
176
- ```
177
-
178
- ## Array Keys
179
-
180
- Use `@expect.array.key` to mark fields that uniquely identify array elements:
181
-
182
- ```atscript
183
- export interface Translations {
184
- entries: {
185
- @expect.array.key
186
- lang: string
187
- @expect.array.key
188
- key: string
189
- value: string
190
- }[]
191
- }
192
- ```
193
-
194
- Multiple key fields form a composite key — elements are matched when ALL key fields match.
195
-
196
- ## Implementation Details
197
-
198
- The `CollectionPatcher` converts patch payloads into MongoDB aggregation pipeline stages using:
199
-
200
- - `$reduce` + `$filter` + `$concatArrays` for keyed upsert/remove
201
- - `$map` + `$cond` + `$mergeObjects` for keyed update with merge
202
- - `$setUnion` for unique/non-keyed insert
203
- - `$setDifference` for non-keyed remove
204
- - `$concatArrays` for plain append
205
-
206
- All operations are performed atomically in a single `updateOne()` call using an aggregation pipeline.