@atscript/mongo 0.1.26 → 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
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Annotations Reference — @atscript/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
|
+
```
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Collections & CRUD — @atscript/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/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
|
+
- `@db.index.plain` → standard indexes
|
|
113
|
+
- `@db.index.unique` → unique indexes
|
|
114
|
+
- `@db.index.fulltext` → text indexes (weight 1)
|
|
115
|
+
- `@db.mongo.index.text` → text indexes (custom weight)
|
|
116
|
+
- `@db.mongo.search.*` → Atlas Search indexes
|
|
117
|
+
|
|
118
|
+
### Querying
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// Find documents
|
|
122
|
+
const cursor = users.collection.find({ isActive: true })
|
|
123
|
+
|
|
124
|
+
// Use the raw MongoDB collection for queries
|
|
125
|
+
const doc = await users.collection.findOne({ _id: users.prepareId(id) })
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `prepareId(id)`
|
|
129
|
+
|
|
130
|
+
Converts a string ID to `ObjectId` if the collection uses `mongo.objectId` type for `_id`.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
const objectId = users.prepareId('507f1f77bcf86cd799439011')
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Best Practices
|
|
137
|
+
|
|
138
|
+
- Use `getValidator()` for context-specific validation before custom operations
|
|
139
|
+
- Call `syncIndexes()` on application startup to ensure indexes match definitions
|
|
140
|
+
- Use `prepareId()` when working with raw MongoDB queries to handle ObjectId conversion
|
|
141
|
+
- The `flatMap` property is lazily built — first access triggers computation
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Core Setup — @atscript/mongo
|
|
2
|
+
|
|
3
|
+
> Plugin installation, configuration, and architecture overview.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @atscript/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/mongo'
|
|
21
|
+
|
|
22
|
+
export default defineConfig({
|
|
23
|
+
rootDir: 'src',
|
|
24
|
+
plugins: [ts(), MongoPlugin()],
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The plugin registers:
|
|
29
|
+
- **Primitives**: `mongo.objectId` (24-char hex string), `mongo.vector` (number array)
|
|
30
|
+
- **Annotations**: All `@db.mongo.*` annotations (collection, indexes, search, patch, array)
|
|
31
|
+
|
|
32
|
+
## Architecture
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
@atscript/mongo
|
|
36
|
+
├── plugin/
|
|
37
|
+
│ ├── index.ts — MongoPlugin factory
|
|
38
|
+
│ ├── annotations.ts — All db.mongo.* annotation specs
|
|
39
|
+
│ └── primitives.ts — mongo.objectId, mongo.vector
|
|
40
|
+
└── lib/
|
|
41
|
+
├── as-mongo.ts — AsMongo: MongoDB client wrapper
|
|
42
|
+
├── as-collection.ts — AsCollection: collection abstraction (validation, indexes, CRUD)
|
|
43
|
+
├── collection-patcher.ts — Converts patch payloads to MongoDB aggregation pipelines
|
|
44
|
+
└── validate-plugins.ts — Validator plugins for ObjectId and unique arrays
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Primitives
|
|
48
|
+
|
|
49
|
+
### `mongo.objectId`
|
|
50
|
+
|
|
51
|
+
A string type constrained to `/^[a-fA-F0-9]{24}$/`. Used for MongoDB `_id` fields.
|
|
52
|
+
|
|
53
|
+
```atscript
|
|
54
|
+
export interface User {
|
|
55
|
+
_id: mongo.objectId
|
|
56
|
+
name: string
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `mongo.vector`
|
|
61
|
+
|
|
62
|
+
An alias for `number[]`. Used for vector search fields.
|
|
63
|
+
|
|
64
|
+
```atscript
|
|
65
|
+
export interface Document {
|
|
66
|
+
embedding: mongo.vector
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Regenerating atscript.d.ts
|
|
71
|
+
|
|
72
|
+
After annotation changes, regenerate the type declarations:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
cd packages/mongo && node ../typescript/dist/cli.cjs -f dts
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Best Practices
|
|
79
|
+
|
|
80
|
+
- Always use `@db.table` to name your collections — it's required by `AsCollection`
|
|
81
|
+
- `@db.mongo.collection` is optional — it only auto-injects `_id: mongo.objectId` if missing
|
|
82
|
+
- Use `mongo.objectId` type for `_id` fields when you want ObjectId-based IDs
|
|
83
|
+
- Use `string` type for `_id` when you want string-based IDs
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Patch Strategies — @atscript/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
|
+
- `$reduce` + `$filter` + `$concatArrays` for keyed upsert/remove
|
|
200
|
+
- `$map` + `$cond` + `$mergeObjects` for keyed update with merge
|
|
201
|
+
- `$setUnion` for unique/non-keyed insert
|
|
202
|
+
- `$setDifference` for non-keyed remove
|
|
203
|
+
- `$concatArrays` for plain append
|
|
204
|
+
|
|
205
|
+
All operations are performed atomically in a single `updateOne()` call using an aggregation pipeline.
|