@moltendb-web/query 0.1.0
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/README.md +209 -0
- package/dist/esm/index.js +443 -0
- package/dist/index.d.ts +360 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +443 -0
- package/dist/index.js +454 -0
- package/dist/index.js.map +1 -0
- package/package.json +29 -0
- package/src/index.ts +583 -0
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# @moltendb-web/query
|
|
2
|
+
|
|
3
|
+
Type-safe, chainable query builder for [MoltenDB](https://github.com/maximilian27/MoltenDB).
|
|
4
|
+
|
|
5
|
+
Works in vanilla JavaScript and TypeScript. Compiles as an npm module (CJS + ESM + `.d.ts`).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @moltendb-web/query
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { MoltenDBClient, WorkerTransport } from '@moltendb-web/query';
|
|
21
|
+
|
|
22
|
+
// Connect to a MoltenDB WASM Web Worker
|
|
23
|
+
const worker = new Worker('./moltendb-worker.js', { type: 'module' });
|
|
24
|
+
const db = new MoltenDBClient(new WorkerTransport(worker));
|
|
25
|
+
|
|
26
|
+
// GET — query with WHERE, field projection, sort and pagination
|
|
27
|
+
const results = await db.collection('laptops')
|
|
28
|
+
.get()
|
|
29
|
+
.where({ brand: 'Apple' })
|
|
30
|
+
.fields(['brand', 'model', 'price'])
|
|
31
|
+
.sort([{ field: 'price', order: 'asc' }])
|
|
32
|
+
.count(5)
|
|
33
|
+
.exec();
|
|
34
|
+
|
|
35
|
+
// SET — insert / upsert
|
|
36
|
+
await db.collection('laptops')
|
|
37
|
+
.set({
|
|
38
|
+
lp1: { brand: 'Lenovo', model: 'ThinkPad X1', price: 1499, in_stock: true },
|
|
39
|
+
lp2: { brand: 'Apple', model: 'MacBook Pro', price: 3499, in_stock: true },
|
|
40
|
+
})
|
|
41
|
+
.exec();
|
|
42
|
+
|
|
43
|
+
// UPDATE — partial patch (only listed fields are changed)
|
|
44
|
+
await db.collection('laptops')
|
|
45
|
+
.update({ lp4: { price: 1749, in_stock: true } })
|
|
46
|
+
.exec();
|
|
47
|
+
|
|
48
|
+
// DELETE — single key
|
|
49
|
+
await db.collection('laptops').delete().keys('lp6').exec();
|
|
50
|
+
|
|
51
|
+
// DELETE — batch
|
|
52
|
+
await db.collection('laptops').delete().keys(['lp4', 'lp5']).exec();
|
|
53
|
+
|
|
54
|
+
// DELETE — drop entire collection
|
|
55
|
+
await db.collection('laptops').delete().drop().exec();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Operations & allowed fields
|
|
61
|
+
|
|
62
|
+
Each operation only exposes the methods that are valid for it — invalid
|
|
63
|
+
combinations are caught at compile time by TypeScript.
|
|
64
|
+
|
|
65
|
+
### `get()` — read / query
|
|
66
|
+
|
|
67
|
+
| Method | Description |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `.keys(key \| key[])` | Fetch one or more documents by key |
|
|
70
|
+
| `.where(clause)` | Filter with operators: `$eq $ne $gt $gte $lt $lte $in $nin $contains` |
|
|
71
|
+
| `.fields(string[])` | Return only these fields (dot-notation supported) |
|
|
72
|
+
| `.excludedFields(string[])` | Return everything except these fields |
|
|
73
|
+
| `.joins(JoinSpec[])` | Embed related documents from other collections |
|
|
74
|
+
| `.sort(SortSpec[])` | Sort results (multi-field, asc/desc) |
|
|
75
|
+
| `.count(n)` | Limit results to N documents |
|
|
76
|
+
| `.offset(n)` | Skip first N results (pagination) |
|
|
77
|
+
| `.build()` | Return the raw JSON payload without sending |
|
|
78
|
+
| `.exec()` | Send the query and return the result |
|
|
79
|
+
|
|
80
|
+
### `set(data)` — insert / upsert
|
|
81
|
+
|
|
82
|
+
| Method | Description |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `.extends(map)` | Embed snapshots from other collections at insert time |
|
|
85
|
+
| `.build()` | Return the raw JSON payload without sending |
|
|
86
|
+
| `.exec()` | Send and return `{ count, status }` |
|
|
87
|
+
|
|
88
|
+
`data` can be a `{ key: document }` map or a `Document[]` array (UUIDv7 keys are auto-assigned for arrays).
|
|
89
|
+
|
|
90
|
+
### `update(data)` — partial patch
|
|
91
|
+
|
|
92
|
+
| Method | Description |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `.build()` | Return the raw JSON payload without sending |
|
|
95
|
+
| `.exec()` | Send and return `{ count, status }` |
|
|
96
|
+
|
|
97
|
+
Only the fields present in each patch object are updated — all other fields are left unchanged.
|
|
98
|
+
|
|
99
|
+
### `delete()` — delete documents or drop collection
|
|
100
|
+
|
|
101
|
+
| Method | Description |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `.keys(key \| key[])` | Delete one or more documents by key |
|
|
104
|
+
| `.drop()` | Drop the entire collection |
|
|
105
|
+
| `.build()` | Return the raw JSON payload without sending |
|
|
106
|
+
| `.exec()` | Send and return `{ count, status }` |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## WHERE operators
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
// Exact equality (implicit)
|
|
114
|
+
.where({ brand: 'Apple' })
|
|
115
|
+
|
|
116
|
+
// Comparison
|
|
117
|
+
.where({ price: { $gt: 1000, $lt: 3000 } })
|
|
118
|
+
.where({ 'specs.cpu.cores': { $gte: 12 } })
|
|
119
|
+
|
|
120
|
+
// Not equal
|
|
121
|
+
.where({ 'specs.cpu.brand': { $ne: 'Intel' } })
|
|
122
|
+
|
|
123
|
+
// In / not-in list
|
|
124
|
+
.where({ brand: { $in: ['Apple', 'Dell'] } })
|
|
125
|
+
.where({ brand: { $nin: ['Framework'] } })
|
|
126
|
+
|
|
127
|
+
// Contains (string substring or array element)
|
|
128
|
+
.where({ model: { $contains: 'Pro' } })
|
|
129
|
+
.where({ tags: { $contains: 'gaming' } })
|
|
130
|
+
|
|
131
|
+
// Multiple conditions (implicit AND)
|
|
132
|
+
.where({ in_stock: true, 'specs.cpu.brand': 'Intel' })
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Joins
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
const results = await db.collection('laptops')
|
|
141
|
+
.get()
|
|
142
|
+
.fields(['brand', 'model', 'price'])
|
|
143
|
+
.joins([
|
|
144
|
+
{ alias: 'ram', from: 'memory', on: 'memory_id', fields: ['capacity_gb', 'type'] },
|
|
145
|
+
{ alias: 'screen', from: 'display', on: 'display_id', fields: ['refresh_hz', 'panel'] },
|
|
146
|
+
])
|
|
147
|
+
.exec();
|
|
148
|
+
// Each result: { brand, model, price, ram: { capacity_gb, type }, screen: { refresh_hz, panel } }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Extends (snapshot embedding at insert time)
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
await db.collection('laptops')
|
|
157
|
+
.set({
|
|
158
|
+
lp7: {
|
|
159
|
+
brand: 'MSI', model: 'Titan GT77', price: 3299,
|
|
160
|
+
specs: { cpu: { brand: 'Intel', cores: 16, ghz: 5.0 } },
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
.extends({ ram: 'memory.mem4', screen: 'display.dsp3' })
|
|
164
|
+
.exec();
|
|
165
|
+
// lp7 is stored with the full mem4 and dsp3 documents embedded inline.
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Custom transport
|
|
171
|
+
|
|
172
|
+
Implement `MoltenTransport` to connect to any backend:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { MoltenTransport, MoltenDBClient, Document, JsonValue } from '@moltendb-web/query';
|
|
176
|
+
|
|
177
|
+
class FetchTransport implements MoltenTransport {
|
|
178
|
+
constructor(private baseUrl: string, private token: string) {}
|
|
179
|
+
|
|
180
|
+
async send(action: 'get' | 'set' | 'update' | 'delete', payload: Document): Promise<JsonValue> {
|
|
181
|
+
const res = await fetch(`${this.baseUrl}/${action}`, {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
headers: {
|
|
184
|
+
'Content-Type': 'application/json',
|
|
185
|
+
'Authorization': `Bearer ${this.token}`,
|
|
186
|
+
},
|
|
187
|
+
body: JSON.stringify(payload),
|
|
188
|
+
});
|
|
189
|
+
return res.json();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const db = new MoltenDBClient(new FetchTransport('https://localhost:1538', myToken));
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Build
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
npm run build # emit dist/index.js (CJS), dist/index.esm.js (ESM), dist/index.d.ts
|
|
202
|
+
npm run typecheck # type-check without emitting
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT OR Apache-2.0
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
// ─── MoltenDB Query Builder ───────────────────────────────────────────────────
|
|
2
|
+
// Chainable, type-safe query builder for MoltenDB.
|
|
3
|
+
//
|
|
4
|
+
// Each operation has its own builder class that only exposes the methods
|
|
5
|
+
// that are valid for that operation — matching the server's allowed-property
|
|
6
|
+
// lists exactly:
|
|
7
|
+
//
|
|
8
|
+
// GET_ALLOWED: collection, keys, where, fields, excludedFields,
|
|
9
|
+
// joins, sort, count, offset
|
|
10
|
+
// SET_ALLOWED: collection, data, extends
|
|
11
|
+
// UPDATE_ALLOWED: collection, data
|
|
12
|
+
// DELETE_ALLOWED: collection, keys, drop
|
|
13
|
+
//
|
|
14
|
+
// Usage (vanilla JS or TypeScript):
|
|
15
|
+
//
|
|
16
|
+
// const db = new MoltenDBClient(worker);
|
|
17
|
+
//
|
|
18
|
+
// // GET — chainable query
|
|
19
|
+
// const results = await db.collection('laptops')
|
|
20
|
+
// .get()
|
|
21
|
+
// .where({ brand: 'Apple' })
|
|
22
|
+
// .fields(['brand', 'model', 'price'])
|
|
23
|
+
// .sort([{ field: 'price', order: 'asc' }])
|
|
24
|
+
// .count(10)
|
|
25
|
+
// .exec();
|
|
26
|
+
//
|
|
27
|
+
// // SET — insert/upsert
|
|
28
|
+
// await db.collection('laptops')
|
|
29
|
+
// .set({ lp1: { brand: 'Lenovo', price: 1499 } })
|
|
30
|
+
// .exec();
|
|
31
|
+
//
|
|
32
|
+
// // UPDATE — partial patch
|
|
33
|
+
// await db.collection('laptops')
|
|
34
|
+
// .update({ lp4: { price: 1749, in_stock: true } })
|
|
35
|
+
// .exec();
|
|
36
|
+
//
|
|
37
|
+
// // DELETE — single key, batch, or drop
|
|
38
|
+
// await db.collection('laptops').delete().keys('lp6').exec();
|
|
39
|
+
// await db.collection('laptops').delete().keys(['lp4', 'lp5']).exec();
|
|
40
|
+
// await db.collection('laptops').delete().drop().exec();
|
|
41
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
// ─── WorkerTransport ──────────────────────────────────────────────────────────
|
|
43
|
+
/**
|
|
44
|
+
* Default transport that communicates with a MoltenDB Web Worker.
|
|
45
|
+
*
|
|
46
|
+
* The worker must follow the moltendb-worker.js message protocol:
|
|
47
|
+
* postMessage({ id, action, ...payload })
|
|
48
|
+
* onmessage → { id, result } | { id, error }
|
|
49
|
+
*/
|
|
50
|
+
export class WorkerTransport {
|
|
51
|
+
constructor(worker, startId = 0) {
|
|
52
|
+
this.pending = new Map();
|
|
53
|
+
this.messageId = startId;
|
|
54
|
+
this.worker = worker;
|
|
55
|
+
this.worker.addEventListener('message', (event) => {
|
|
56
|
+
const { id, result, error } = event.data;
|
|
57
|
+
const p = this.pending.get(id);
|
|
58
|
+
if (!p)
|
|
59
|
+
return;
|
|
60
|
+
this.pending.delete(id);
|
|
61
|
+
if (error)
|
|
62
|
+
p.reject(new Error(error));
|
|
63
|
+
else
|
|
64
|
+
p.resolve(result ?? null);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
send(action, payload) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const id = this.messageId++;
|
|
70
|
+
this.pending.set(id, { resolve, reject });
|
|
71
|
+
this.worker.postMessage({ id, action, ...payload });
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// ─── GetQuery ─────────────────────────────────────────────────────────────────
|
|
76
|
+
/**
|
|
77
|
+
* Builder for GET (read/query) operations.
|
|
78
|
+
*
|
|
79
|
+
* Allowed fields: collection, keys, where, fields, excludedFields,
|
|
80
|
+
* joins, sort, count, offset
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* const rows = await db.collection('laptops')
|
|
84
|
+
* .get()
|
|
85
|
+
* .where({ brand: 'Apple' })
|
|
86
|
+
* .fields(['brand', 'model', 'price'])
|
|
87
|
+
* .sort([{ field: 'price', order: 'asc' }])
|
|
88
|
+
* .count(5)
|
|
89
|
+
* .exec();
|
|
90
|
+
*/
|
|
91
|
+
export class GetQuery {
|
|
92
|
+
/** @internal */
|
|
93
|
+
constructor(transport, collection) {
|
|
94
|
+
this.transport = transport;
|
|
95
|
+
this.payload = { collection };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Fetch a single document by key, or a batch of documents by key array.
|
|
99
|
+
*
|
|
100
|
+
* @param keys - A single key string or an array of key strings.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* // Single key
|
|
104
|
+
* .keys('lp2')
|
|
105
|
+
* // Batch
|
|
106
|
+
* .keys(['lp1', 'lp3', 'lp5'])
|
|
107
|
+
*/
|
|
108
|
+
keys(keys) {
|
|
109
|
+
this.payload['keys'] = keys;
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Filter documents using a WHERE clause.
|
|
114
|
+
* Multiple conditions are combined with implicit AND.
|
|
115
|
+
*
|
|
116
|
+
* Supported operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $contains
|
|
117
|
+
* Dot-notation is supported for nested fields.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* .where({ brand: 'Apple' })
|
|
121
|
+
* .where({ price: { $gt: 1000, $lt: 3000 } })
|
|
122
|
+
* .where({ 'specs.cpu.brand': 'Intel', in_stock: true })
|
|
123
|
+
*/
|
|
124
|
+
where(clause) {
|
|
125
|
+
this.payload['where'] = clause;
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Project the response to only the specified fields (dot-notation supported).
|
|
130
|
+
* Cannot be combined with {@link excludedFields}.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* .fields(['brand', 'model', 'specs.cpu.ghz'])
|
|
134
|
+
*/
|
|
135
|
+
fields(fields) {
|
|
136
|
+
this.payload['fields'] = fields;
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Return all fields except the specified ones.
|
|
141
|
+
* Cannot be combined with {@link fields}.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* .excludedFields(['price', 'memory_id', 'display_id'])
|
|
145
|
+
*/
|
|
146
|
+
excludedFields(fields) {
|
|
147
|
+
this.payload['excludedFields'] = fields;
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Join related documents from other collections.
|
|
152
|
+
* Each join embeds the related document under the given alias.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* .joins([
|
|
156
|
+
* { alias: 'ram', from: 'memory', on: 'memory_id', fields: ['capacity_gb', 'type'] },
|
|
157
|
+
* { alias: 'screen', from: 'display', on: 'display_id', fields: ['refresh_hz', 'panel'] },
|
|
158
|
+
* ])
|
|
159
|
+
*/
|
|
160
|
+
joins(specs) {
|
|
161
|
+
this.payload['joins'] = specs.map(({ alias, from, on, fields }) => ({
|
|
162
|
+
[alias]: fields ? { from, on, fields } : { from, on },
|
|
163
|
+
}));
|
|
164
|
+
return this;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Sort the results.
|
|
168
|
+
* Multiple specs are applied in priority order (first = primary sort key).
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* .sort([{ field: 'price', order: 'asc' }])
|
|
172
|
+
* .sort([{ field: 'brand', order: 'asc' }, { field: 'price', order: 'desc' }])
|
|
173
|
+
*/
|
|
174
|
+
sort(specs) {
|
|
175
|
+
this.payload['sort'] = specs;
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Limit the number of results returned (applied after filtering and sorting).
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* .count(10)
|
|
183
|
+
*/
|
|
184
|
+
count(n) {
|
|
185
|
+
this.payload['count'] = n;
|
|
186
|
+
return this;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Skip the first N results (for pagination, applied after sorting).
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* .offset(20).count(10) // page 3 of 10
|
|
193
|
+
*/
|
|
194
|
+
offset(n) {
|
|
195
|
+
this.payload['offset'] = n;
|
|
196
|
+
return this;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Build and return the raw JSON payload without sending it.
|
|
200
|
+
* Useful for debugging or passing to a custom transport.
|
|
201
|
+
*/
|
|
202
|
+
build() {
|
|
203
|
+
return { ...this.payload };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Execute the query and return the result.
|
|
207
|
+
* Returns a single document for single-key lookups, or an array for all others.
|
|
208
|
+
*/
|
|
209
|
+
exec() {
|
|
210
|
+
return this.transport.send('get', this.payload);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ─── SetQuery ─────────────────────────────────────────────────────────────────
|
|
214
|
+
/**
|
|
215
|
+
* Builder for SET (insert / upsert) operations.
|
|
216
|
+
*
|
|
217
|
+
* Allowed fields: collection, data, extends
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* await db.collection('laptops')
|
|
221
|
+
* .set({
|
|
222
|
+
* lp1: { brand: 'Lenovo', model: 'ThinkPad X1', price: 1499 },
|
|
223
|
+
* lp2: { brand: 'Apple', model: 'MacBook Pro', price: 3499 },
|
|
224
|
+
* })
|
|
225
|
+
* .exec();
|
|
226
|
+
*/
|
|
227
|
+
export class SetQuery {
|
|
228
|
+
/** @internal */
|
|
229
|
+
constructor(transport, collection, data) {
|
|
230
|
+
this.transport = transport;
|
|
231
|
+
this.payload = { collection, data: data };
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Embed data from other collections into each document at insert time.
|
|
235
|
+
* The referenced document is fetched once and stored as a snapshot.
|
|
236
|
+
*
|
|
237
|
+
* Format: `{ alias: "collection.key" }`
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* .extends({ ram: 'memory.mem4', screen: 'display.dsp3' })
|
|
241
|
+
*/
|
|
242
|
+
extends(map) {
|
|
243
|
+
// The extends map is applied to every document in data.
|
|
244
|
+
// We store it at the top level; the server resolves it per-document.
|
|
245
|
+
const data = this.payload['data'];
|
|
246
|
+
if (Array.isArray(data)) {
|
|
247
|
+
// Array format — inject extends into each item
|
|
248
|
+
this.payload['data'] = data.map((doc) => ({
|
|
249
|
+
...doc,
|
|
250
|
+
extends: map,
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
else if (data && typeof data === 'object') {
|
|
254
|
+
// Object format — inject extends into each document value
|
|
255
|
+
const updated = {};
|
|
256
|
+
for (const [key, doc] of Object.entries(data)) {
|
|
257
|
+
updated[key] = { ...doc, extends: map };
|
|
258
|
+
}
|
|
259
|
+
this.payload['data'] = updated;
|
|
260
|
+
}
|
|
261
|
+
return this;
|
|
262
|
+
}
|
|
263
|
+
/** Build and return the raw JSON payload without sending it. */
|
|
264
|
+
build() {
|
|
265
|
+
return { ...this.payload };
|
|
266
|
+
}
|
|
267
|
+
/** Execute the insert/upsert and return `{ count, status }`. */
|
|
268
|
+
exec() {
|
|
269
|
+
return this.transport.send('set', this.payload);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// ─── UpdateQuery ──────────────────────────────────────────────────────────────
|
|
273
|
+
/**
|
|
274
|
+
* Builder for UPDATE (partial patch / merge) operations.
|
|
275
|
+
*
|
|
276
|
+
* Allowed fields: collection, data
|
|
277
|
+
*
|
|
278
|
+
* Only the fields present in each patch object are updated —
|
|
279
|
+
* all other fields on the existing document are left unchanged.
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* await db.collection('laptops')
|
|
283
|
+
* .update({ lp4: { price: 1749, in_stock: true } })
|
|
284
|
+
* .exec();
|
|
285
|
+
*/
|
|
286
|
+
export class UpdateQuery {
|
|
287
|
+
/** @internal */
|
|
288
|
+
constructor(transport, collection, data) {
|
|
289
|
+
this.transport = transport;
|
|
290
|
+
this.payload = { collection, data: data };
|
|
291
|
+
}
|
|
292
|
+
/** Build and return the raw JSON payload without sending it. */
|
|
293
|
+
build() {
|
|
294
|
+
return { ...this.payload };
|
|
295
|
+
}
|
|
296
|
+
/** Execute the patch and return `{ count, status }`. */
|
|
297
|
+
exec() {
|
|
298
|
+
return this.transport.send('update', this.payload);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// ─── DeleteQuery ──────────────────────────────────────────────────────────────
|
|
302
|
+
/**
|
|
303
|
+
* Builder for DELETE operations.
|
|
304
|
+
*
|
|
305
|
+
* Allowed fields: collection, keys, drop
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* // Delete a single document
|
|
309
|
+
* await db.collection('laptops').delete().keys('lp6').exec();
|
|
310
|
+
*
|
|
311
|
+
* // Delete multiple documents
|
|
312
|
+
* await db.collection('laptops').delete().keys(['lp4', 'lp5']).exec();
|
|
313
|
+
*
|
|
314
|
+
* // Drop the entire collection
|
|
315
|
+
* await db.collection('laptops').delete().drop().exec();
|
|
316
|
+
*/
|
|
317
|
+
export class DeleteQuery {
|
|
318
|
+
/** @internal */
|
|
319
|
+
constructor(transport, collection) {
|
|
320
|
+
this.transport = transport;
|
|
321
|
+
this.payload = { collection };
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Delete a single document by key, or multiple documents by key array.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* .keys('lp6')
|
|
328
|
+
* .keys(['lp4', 'lp5'])
|
|
329
|
+
*/
|
|
330
|
+
keys(keys) {
|
|
331
|
+
this.payload['keys'] = keys;
|
|
332
|
+
return this;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Drop the entire collection (deletes all documents).
|
|
336
|
+
* Cannot be combined with {@link keys}.
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* .drop()
|
|
340
|
+
*/
|
|
341
|
+
drop() {
|
|
342
|
+
this.payload['drop'] = true;
|
|
343
|
+
return this;
|
|
344
|
+
}
|
|
345
|
+
/** Build and return the raw JSON payload without sending it. */
|
|
346
|
+
build() {
|
|
347
|
+
return { ...this.payload };
|
|
348
|
+
}
|
|
349
|
+
/** Execute the delete and return `{ count, status }`. */
|
|
350
|
+
exec() {
|
|
351
|
+
return this.transport.send('delete', this.payload);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// ─── CollectionHandle ─────────────────────────────────────────────────────────
|
|
355
|
+
/**
|
|
356
|
+
* A handle to a specific collection.
|
|
357
|
+
* Returned by `MoltenDBClient.collection(name)`.
|
|
358
|
+
* Use it to start any of the four operation builders.
|
|
359
|
+
*/
|
|
360
|
+
export class CollectionHandle {
|
|
361
|
+
/** @internal */
|
|
362
|
+
constructor(transport, collectionName) {
|
|
363
|
+
this.transport = transport;
|
|
364
|
+
this.collectionName = collectionName;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Start a GET (read/query) builder for this collection.
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* db.collection('laptops').get().where({ brand: 'Apple' }).exec()
|
|
371
|
+
*/
|
|
372
|
+
get() {
|
|
373
|
+
return new GetQuery(this.transport, this.collectionName);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Start a SET (insert/upsert) builder for this collection.
|
|
377
|
+
*
|
|
378
|
+
* @param data - A map of `{ key: document }` pairs, or an array of documents
|
|
379
|
+
* (keys are auto-generated as UUIDv7 when using array format).
|
|
380
|
+
*
|
|
381
|
+
* @example
|
|
382
|
+
* db.collection('laptops').set({ lp1: { brand: 'Lenovo', price: 1499 } }).exec()
|
|
383
|
+
*/
|
|
384
|
+
set(data) {
|
|
385
|
+
return new SetQuery(this.transport, this.collectionName, data);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Start an UPDATE (partial patch) builder for this collection.
|
|
389
|
+
*
|
|
390
|
+
* @param data - A map of `{ key: patch }` pairs. Only the provided fields are updated.
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* db.collection('laptops').update({ lp4: { price: 1749 } }).exec()
|
|
394
|
+
*/
|
|
395
|
+
update(data) {
|
|
396
|
+
return new UpdateQuery(this.transport, this.collectionName, data);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Start a DELETE builder for this collection.
|
|
400
|
+
* Chain `.keys(...)` or `.drop()` to specify what to delete.
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* db.collection('laptops').delete().keys('lp6').exec()
|
|
404
|
+
*/
|
|
405
|
+
delete() {
|
|
406
|
+
return new DeleteQuery(this.transport, this.collectionName);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// ─── MoltenDBClient ───────────────────────────────────────────────────────────
|
|
410
|
+
/**
|
|
411
|
+
* The main entry point for the MoltenDB query builder.
|
|
412
|
+
*
|
|
413
|
+
* Accepts any {@link MoltenTransport} implementation — use {@link WorkerTransport}
|
|
414
|
+
* to connect to a MoltenDB WASM Web Worker, or provide your own transport
|
|
415
|
+
* for HTTP, WebSocket, or testing.
|
|
416
|
+
*
|
|
417
|
+
* @example
|
|
418
|
+
* // Browser + WASM Web Worker
|
|
419
|
+
* const worker = new Worker('./moltendb-worker.js', { type: 'module' });
|
|
420
|
+
* const transport = new WorkerTransport(worker);
|
|
421
|
+
* const db = new MoltenDBClient(transport);
|
|
422
|
+
*
|
|
423
|
+
* const results = await db.collection('laptops')
|
|
424
|
+
* .get()
|
|
425
|
+
* .where({ in_stock: true })
|
|
426
|
+
* .sort([{ field: 'price', order: 'asc' }])
|
|
427
|
+
* .count(5)
|
|
428
|
+
* .exec();
|
|
429
|
+
*/
|
|
430
|
+
export class MoltenDBClient {
|
|
431
|
+
constructor(transport) {
|
|
432
|
+
this.transport = transport;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Select a collection to operate on.
|
|
436
|
+
* Returns a {@link CollectionHandle} from which you can start any query builder.
|
|
437
|
+
*
|
|
438
|
+
* @param name - The collection name (e.g. `'laptops'`).
|
|
439
|
+
*/
|
|
440
|
+
collection(name) {
|
|
441
|
+
return new CollectionHandle(this.transport, name);
|
|
442
|
+
}
|
|
443
|
+
}
|