@yolo-labs/yolo-cli 0.23.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.
@@ -0,0 +1,1087 @@
1
+ // ../flexdb/src/error.ts
2
+ var FlexDBError = class _FlexDBError extends Error {
3
+ /** Stable machine-readable code, e.g. `INVALID_COLLECTION_NAME`. */
4
+ code;
5
+ constructor(message, code = "FLEXDB_ERROR") {
6
+ super(message);
7
+ this.name = "FlexDBError";
8
+ this.code = code;
9
+ Object.setPrototypeOf(this, _FlexDBError.prototype);
10
+ }
11
+ };
12
+
13
+ // ../flexdb/src/sql.ts
14
+ var COLLECTION_RE = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
15
+ var SEGMENT_RE = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
16
+ var DANGEROUS_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
17
+ function assertCollectionName(name) {
18
+ if (typeof name !== "string" || !COLLECTION_RE.test(name)) {
19
+ throw new FlexDBError(
20
+ `Invalid collection name: ${JSON.stringify(name)}. Must match ${COLLECTION_RE.source}.`,
21
+ "INVALID_COLLECTION_NAME"
22
+ );
23
+ }
24
+ }
25
+ function assertFieldPath(path) {
26
+ if (typeof path !== "string" || path.length === 0) {
27
+ throw new FlexDBError(
28
+ `Invalid field path: ${JSON.stringify(path)}. Must be a non-empty string.`,
29
+ "INVALID_FIELD_PATH"
30
+ );
31
+ }
32
+ const segments = path.split(".");
33
+ for (const seg of segments) {
34
+ if (!SEGMENT_RE.test(seg)) {
35
+ throw new FlexDBError(
36
+ `Invalid field path segment: ${JSON.stringify(seg)} in ${JSON.stringify(path)}. Each segment must match ${SEGMENT_RE.source}.`,
37
+ "INVALID_FIELD_PATH"
38
+ );
39
+ }
40
+ if (DANGEROUS_SEGMENTS.has(seg)) {
41
+ throw new FlexDBError(
42
+ `Reserved field path segment: ${JSON.stringify(seg)} in ${JSON.stringify(path)}.`,
43
+ "INVALID_FIELD_PATH"
44
+ );
45
+ }
46
+ }
47
+ }
48
+ function jsonPath(path) {
49
+ assertFieldPath(path);
50
+ return `$.${path}`;
51
+ }
52
+ function jsonExtract(path) {
53
+ return `json_extract(document, '${jsonPath(path)}')`;
54
+ }
55
+ function indexName(collection, fields) {
56
+ assertCollectionName(collection);
57
+ const parts = fields.map((f) => {
58
+ assertFieldPath(f);
59
+ return f.replace(/\./g, "_");
60
+ });
61
+ const hash = hashFields(fields);
62
+ return `idx_${collection}__${parts.join("__")}__${hash}`;
63
+ }
64
+ function hashFields(fields) {
65
+ const input = fields.join("\0");
66
+ let h = 2166136261;
67
+ for (let i = 0; i < input.length; i++) {
68
+ h ^= input.charCodeAt(i);
69
+ h = Math.imul(h, 16777619);
70
+ }
71
+ return (h >>> 0).toString(16).padStart(8, "0");
72
+ }
73
+ function assertNonNegativeInt(value, label) {
74
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || !Number.isSafeInteger(value)) {
75
+ throw new FlexDBError(
76
+ `Invalid ${label}: ${JSON.stringify(value)}. Must be a non-negative integer.`,
77
+ "INVALID_OPTION"
78
+ );
79
+ }
80
+ return value;
81
+ }
82
+
83
+ // ../flexdb/src/matcher.ts
84
+ var KNOWN_FIELD_OPERATORS = /* @__PURE__ */ new Set([
85
+ "$eq",
86
+ "$ne",
87
+ "$gt",
88
+ "$gte",
89
+ "$lt",
90
+ "$lte",
91
+ "$in",
92
+ "$nin",
93
+ "$exists",
94
+ "$regex",
95
+ "$options",
96
+ "$not"
97
+ // field-level $not negates an inner operator expression
98
+ ]);
99
+ var LOGICAL_OPERATORS = /* @__PURE__ */ new Set(["$and", "$or", "$nor", "$not"]);
100
+ var MISSING = Symbol("missing");
101
+ function resolvePath(doc, path) {
102
+ const segments = path.split(".");
103
+ let current = doc;
104
+ for (const seg of segments) {
105
+ if (current === null || current === void 0 || typeof current !== "object") {
106
+ return MISSING;
107
+ }
108
+ if (!Object.prototype.hasOwnProperty.call(current, seg)) {
109
+ return MISSING;
110
+ }
111
+ current = current[seg];
112
+ }
113
+ return current;
114
+ }
115
+ function typeRank(v) {
116
+ if (v === null || v === void 0) return 0;
117
+ if (typeof v === "number") return 1;
118
+ if (typeof v === "string") return 2;
119
+ if (typeof v === "boolean") return 3;
120
+ return 4;
121
+ }
122
+ function compareValues(a, b) {
123
+ const ra = typeRank(a);
124
+ const rb = typeRank(b);
125
+ if (ra !== rb) return ra - rb;
126
+ switch (ra) {
127
+ case 0:
128
+ return 0;
129
+ case 1: {
130
+ const na = a;
131
+ const nb = b;
132
+ if (na < nb) return -1;
133
+ if (na > nb) return 1;
134
+ return 0;
135
+ }
136
+ case 2: {
137
+ const sa = a;
138
+ const sb = b;
139
+ if (sa < sb) return -1;
140
+ if (sa > sb) return 1;
141
+ return 0;
142
+ }
143
+ case 3:
144
+ return a === b ? 0 : a ? 1 : -1;
145
+ default: {
146
+ const sa = JSON.stringify(a);
147
+ const sb = JSON.stringify(b);
148
+ if (sa < sb) return -1;
149
+ if (sa > sb) return 1;
150
+ return 0;
151
+ }
152
+ }
153
+ }
154
+ function deepEqual(a, b) {
155
+ if (a === b) return true;
156
+ if (a === null || b === null) return a === b;
157
+ if (typeof a !== typeof b) return false;
158
+ if (typeof a !== "object") return false;
159
+ if (Array.isArray(a) || Array.isArray(b)) {
160
+ if (!Array.isArray(a) || !Array.isArray(b)) return false;
161
+ if (a.length !== b.length) return false;
162
+ for (let i = 0; i < a.length; i++) {
163
+ if (!deepEqual(a[i], b[i])) return false;
164
+ }
165
+ return true;
166
+ }
167
+ const ao = a;
168
+ const bo = b;
169
+ const ak = Object.keys(ao);
170
+ const bk = Object.keys(bo);
171
+ if (ak.length !== bk.length) return false;
172
+ for (const k of ak) {
173
+ if (!Object.prototype.hasOwnProperty.call(bo, k)) return false;
174
+ if (!deepEqual(ao[k], bo[k])) return false;
175
+ }
176
+ return true;
177
+ }
178
+ function equalsField(fieldValue, target) {
179
+ if (deepEqual(fieldValue, target)) return true;
180
+ if (Array.isArray(fieldValue)) {
181
+ return fieldValue.some((el) => deepEqual(el, target));
182
+ }
183
+ return false;
184
+ }
185
+ function rangeMatch(fieldValue, operand, pass) {
186
+ const test = (v) => typeRank(v) === typeRank(operand) && pass(compareValues(v, operand));
187
+ if (Array.isArray(fieldValue)) {
188
+ return fieldValue.some(test);
189
+ }
190
+ return test(fieldValue);
191
+ }
192
+ function buildRegex(spec) {
193
+ let source;
194
+ let flags = "";
195
+ if (typeof spec === "string") {
196
+ source = spec;
197
+ } else if (spec && typeof spec === "object" && "$regex" in spec) {
198
+ const obj = spec;
199
+ if (typeof obj.$regex !== "string") {
200
+ throw new FlexDBError("$regex must be a string", "INVALID_QUERY");
201
+ }
202
+ source = obj.$regex;
203
+ if (obj.$options !== void 0) {
204
+ if (typeof obj.$options !== "string") {
205
+ throw new FlexDBError("$options must be a string", "INVALID_QUERY");
206
+ }
207
+ flags = obj.$options;
208
+ }
209
+ } else {
210
+ throw new FlexDBError("$regex must be a string or { $regex, $options }", "INVALID_QUERY");
211
+ }
212
+ try {
213
+ return new RegExp(source, flags);
214
+ } catch (e) {
215
+ throw new FlexDBError(
216
+ `Invalid $regex: ${e instanceof Error ? e.message : String(e)}`,
217
+ "INVALID_QUERY"
218
+ );
219
+ }
220
+ }
221
+ function matchField(fieldValue, condition) {
222
+ const present = fieldValue !== MISSING;
223
+ const value = present ? fieldValue : void 0;
224
+ if (!isOperatorDoc(condition)) {
225
+ if (!present) return condition === null;
226
+ return equalsField(value, condition);
227
+ }
228
+ const ops = condition;
229
+ if ("$options" in ops && !("$regex" in ops)) {
230
+ throw new FlexDBError("$options is only valid alongside $regex", "INVALID_QUERY");
231
+ }
232
+ for (const [op, operand] of Object.entries(ops)) {
233
+ if (op === "$options") continue;
234
+ if (!KNOWN_FIELD_OPERATORS.has(op)) {
235
+ throw new FlexDBError(`Unknown query operator: ${op}`, "INVALID_QUERY");
236
+ }
237
+ switch (op) {
238
+ case "$eq":
239
+ if (!present) {
240
+ if (operand !== null) return false;
241
+ } else if (!equalsField(value, operand)) {
242
+ return false;
243
+ }
244
+ break;
245
+ case "$ne":
246
+ if (!present) {
247
+ if (operand === null) return false;
248
+ } else if (equalsField(value, operand)) {
249
+ return false;
250
+ }
251
+ break;
252
+ case "$gt":
253
+ if (!present || !rangeMatch(value, operand, (c) => c > 0)) return false;
254
+ break;
255
+ case "$gte":
256
+ if (!present || !rangeMatch(value, operand, (c) => c >= 0)) return false;
257
+ break;
258
+ case "$lt":
259
+ if (!present || !rangeMatch(value, operand, (c) => c < 0)) return false;
260
+ break;
261
+ case "$lte":
262
+ if (!present || !rangeMatch(value, operand, (c) => c <= 0)) return false;
263
+ break;
264
+ case "$in": {
265
+ if (!Array.isArray(operand)) {
266
+ throw new FlexDBError("$in requires an array", "INVALID_QUERY");
267
+ }
268
+ if (!present) {
269
+ if (!operand.some((t) => t === null)) return false;
270
+ } else if (!operand.some((t) => equalsField(value, t))) {
271
+ return false;
272
+ }
273
+ break;
274
+ }
275
+ case "$nin": {
276
+ if (!Array.isArray(operand)) {
277
+ throw new FlexDBError("$nin requires an array", "INVALID_QUERY");
278
+ }
279
+ if (!present) {
280
+ if (operand.some((t) => t === null)) return false;
281
+ } else if (operand.some((t) => equalsField(value, t))) {
282
+ return false;
283
+ }
284
+ break;
285
+ }
286
+ case "$exists": {
287
+ if (typeof operand !== "boolean") {
288
+ throw new FlexDBError("$exists requires a boolean", "INVALID_QUERY");
289
+ }
290
+ if (present !== operand) return false;
291
+ break;
292
+ }
293
+ case "$regex": {
294
+ const re = buildRegex(
295
+ "$options" in ops ? { $regex: operand, $options: ops.$options } : operand
296
+ );
297
+ if (!present) return false;
298
+ if (Array.isArray(value)) {
299
+ if (!value.some((el) => typeof el === "string" && re.test(el))) return false;
300
+ } else {
301
+ if (typeof value !== "string" || !re.test(value)) return false;
302
+ }
303
+ break;
304
+ }
305
+ case "$not": {
306
+ if (!isOperatorDoc(operand)) {
307
+ throw new FlexDBError(
308
+ "$not requires an operator expression (e.g. { $gte: 1 } or { $regex })",
309
+ "INVALID_QUERY"
310
+ );
311
+ }
312
+ if (matchField(fieldValue, operand)) return false;
313
+ break;
314
+ }
315
+ default:
316
+ throw new FlexDBError(`Unhandled query operator: ${op}`, "INVALID_QUERY");
317
+ }
318
+ }
319
+ return true;
320
+ }
321
+ function isOperatorDoc(v) {
322
+ if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
323
+ const keys = Object.keys(v);
324
+ if (keys.length === 0) return false;
325
+ return keys.every((k) => k.startsWith("$"));
326
+ }
327
+ function assertNonEmptyLogicalArray(op, condition) {
328
+ if (!Array.isArray(condition)) {
329
+ throw new FlexDBError(`${op} requires an array`, "INVALID_QUERY");
330
+ }
331
+ if (condition.length === 0) {
332
+ throw new FlexDBError(`${op} requires a non-empty array`, "INVALID_QUERY");
333
+ }
334
+ }
335
+ function assertSubQueryObject(op, value) {
336
+ if (!isPlainObject(value)) {
337
+ throw new FlexDBError(`${op} entries must be query objects`, "INVALID_QUERY");
338
+ }
339
+ }
340
+ function isPlainObject(v) {
341
+ if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
342
+ const proto = Object.getPrototypeOf(v);
343
+ return proto === Object.prototype || proto === null;
344
+ }
345
+ function matchesQuery(doc, query) {
346
+ if (!query || typeof query !== "object") return true;
347
+ for (const [key, condition] of Object.entries(query)) {
348
+ if (key === "$options") {
349
+ throw new FlexDBError("$options is only valid alongside a field $regex", "INVALID_QUERY");
350
+ }
351
+ if (LOGICAL_OPERATORS.has(key)) {
352
+ switch (key) {
353
+ case "$and": {
354
+ assertNonEmptyLogicalArray("$and", condition);
355
+ if (!condition.every((sub) => matchesQuery(doc, sub))) return false;
356
+ break;
357
+ }
358
+ case "$or": {
359
+ assertNonEmptyLogicalArray("$or", condition);
360
+ if (!condition.some((sub) => matchesQuery(doc, sub))) return false;
361
+ break;
362
+ }
363
+ case "$nor": {
364
+ assertNonEmptyLogicalArray("$nor", condition);
365
+ if (condition.some((sub) => matchesQuery(doc, sub))) return false;
366
+ break;
367
+ }
368
+ case "$not": {
369
+ if (matchesQuery(doc, condition)) return false;
370
+ break;
371
+ }
372
+ default:
373
+ break;
374
+ }
375
+ continue;
376
+ }
377
+ if (key.startsWith("$")) {
378
+ throw new FlexDBError(`Unknown top-level operator: ${key}`, "INVALID_QUERY");
379
+ }
380
+ assertFieldPath(key);
381
+ const fieldValue = resolvePath(doc, key);
382
+ if (!matchField(fieldValue, condition)) {
383
+ return false;
384
+ }
385
+ }
386
+ return true;
387
+ }
388
+ function assertValidQuery(query) {
389
+ if (query === void 0 || query === null) return;
390
+ if (!isPlainObject(query)) {
391
+ throw new FlexDBError("Query must be a plain object", "INVALID_QUERY");
392
+ }
393
+ for (const [key, condition] of Object.entries(query)) {
394
+ if (key === "$options") {
395
+ throw new FlexDBError("$options is only valid alongside a field $regex", "INVALID_QUERY");
396
+ }
397
+ if (LOGICAL_OPERATORS.has(key)) {
398
+ if (key === "$not") {
399
+ assertSubQueryObject(key, condition);
400
+ assertValidQuery(condition);
401
+ continue;
402
+ }
403
+ assertNonEmptyLogicalArray(key, condition);
404
+ for (const sub of condition) {
405
+ assertSubQueryObject(key, sub);
406
+ assertValidQuery(sub);
407
+ }
408
+ continue;
409
+ }
410
+ if (key.startsWith("$")) {
411
+ throw new FlexDBError(`Unknown top-level operator: ${key}`, "INVALID_QUERY");
412
+ }
413
+ assertFieldPath(key);
414
+ assertValidFieldCondition(condition);
415
+ }
416
+ }
417
+ function assertValidFieldCondition(condition) {
418
+ if (!isOperatorDoc(condition)) return;
419
+ const ops = condition;
420
+ if ("$options" in ops && !("$regex" in ops)) {
421
+ throw new FlexDBError("$options is only valid alongside $regex", "INVALID_QUERY");
422
+ }
423
+ for (const [op, operand] of Object.entries(ops)) {
424
+ if (op === "$options") continue;
425
+ if (!KNOWN_FIELD_OPERATORS.has(op)) {
426
+ throw new FlexDBError(`Unknown query operator: ${op}`, "INVALID_QUERY");
427
+ }
428
+ if ((op === "$in" || op === "$nin") && !Array.isArray(operand)) {
429
+ throw new FlexDBError(`${op} requires an array`, "INVALID_QUERY");
430
+ }
431
+ if (op === "$exists" && typeof operand !== "boolean") {
432
+ throw new FlexDBError("$exists requires a boolean", "INVALID_QUERY");
433
+ }
434
+ if (op === "$not") {
435
+ if (!isOperatorDoc(operand)) {
436
+ throw new FlexDBError(
437
+ "$not requires an operator expression (e.g. { $gte: 1 } or { $regex })",
438
+ "INVALID_QUERY"
439
+ );
440
+ }
441
+ assertValidFieldCondition(operand);
442
+ }
443
+ if (op === "$regex") {
444
+ buildRegex("$options" in ops ? { $regex: operand, $options: ops.$options } : operand);
445
+ }
446
+ }
447
+ }
448
+ function sortDocuments(docs, sort) {
449
+ const entries = Object.entries(sort);
450
+ for (const [field, dir] of entries) {
451
+ assertFieldPath(field);
452
+ if (dir !== 1 && dir !== -1) {
453
+ throw new FlexDBError(
454
+ `Invalid sort direction for "${field}": ${JSON.stringify(dir)}. Use 1 or -1.`,
455
+ "INVALID_SORT"
456
+ );
457
+ }
458
+ }
459
+ return docs.sort((a, b) => {
460
+ for (const [field, dir] of entries) {
461
+ const av = resolvePath(a, field);
462
+ const bv = resolvePath(b, field);
463
+ const an = av === MISSING ? null : av;
464
+ const bn = bv === MISSING ? null : bv;
465
+ const cmp = compareValues(an, bn);
466
+ if (cmp !== 0) return dir === 1 ? cmp : -cmp;
467
+ }
468
+ return 0;
469
+ });
470
+ }
471
+
472
+ // ../flexdb/src/update.ts
473
+ var UPDATE_OPERATORS = /* @__PURE__ */ new Set(["$set", "$unset", "$inc", "$push", "$pull", "$addToSet"]);
474
+ function cloneValue(value) {
475
+ if (value === null || typeof value !== "object") return value;
476
+ return JSON.parse(JSON.stringify(value));
477
+ }
478
+ function isOwnTraversable(cur, seg) {
479
+ if (!Object.prototype.hasOwnProperty.call(cur, seg)) return false;
480
+ const next = cur[seg];
481
+ return next !== null && typeof next === "object" && !Array.isArray(next);
482
+ }
483
+ function defineOwn(obj, key, value) {
484
+ Object.defineProperty(obj, key, { value, writable: true, enumerable: true, configurable: true });
485
+ }
486
+ function setPath(doc, path, value) {
487
+ const segs = path.split(".");
488
+ let cur = doc;
489
+ for (let i = 0; i < segs.length - 1; i++) {
490
+ const seg = segs[i];
491
+ if (!isOwnTraversable(cur, seg)) {
492
+ defineOwn(cur, seg, {});
493
+ }
494
+ cur = cur[seg];
495
+ }
496
+ defineOwn(cur, segs[segs.length - 1], cloneValue(value));
497
+ }
498
+ function unsetPath(doc, path) {
499
+ const segs = path.split(".");
500
+ let cur = doc;
501
+ for (let i = 0; i < segs.length - 1; i++) {
502
+ if (!isOwnTraversable(cur, segs[i])) return;
503
+ cur = cur[segs[i]];
504
+ }
505
+ delete cur[segs[segs.length - 1]];
506
+ }
507
+ function getPath(doc, path) {
508
+ const segs = path.split(".");
509
+ let cur = doc;
510
+ for (const seg of segs) {
511
+ if (cur === null || typeof cur !== "object" || Array.isArray(cur)) return void 0;
512
+ if (!Object.prototype.hasOwnProperty.call(cur, seg)) return void 0;
513
+ cur = cur[seg];
514
+ }
515
+ return cur;
516
+ }
517
+ function validateUpdate(update) {
518
+ if (!update || typeof update !== "object" || Array.isArray(update)) {
519
+ throw new FlexDBError("Update must be an object of update operators", "INVALID_UPDATE");
520
+ }
521
+ const keys = Object.keys(update);
522
+ if (keys.length === 0) {
523
+ throw new FlexDBError("Update must contain at least one operator", "INVALID_UPDATE");
524
+ }
525
+ for (const k of keys) {
526
+ if (!UPDATE_OPERATORS.has(k)) {
527
+ throw new FlexDBError(
528
+ k.startsWith("$") ? `Unsupported update operator: ${k}` : `Update keys must be operators (got bare field "${k}"); use $set`,
529
+ "INVALID_UPDATE"
530
+ );
531
+ }
532
+ const v = update[k];
533
+ if (!v || typeof v !== "object" || Array.isArray(v)) {
534
+ throw new FlexDBError(`${k} must be an object`, "INVALID_UPDATE");
535
+ }
536
+ for (const path of Object.keys(v)) {
537
+ assertFieldPath(path);
538
+ }
539
+ }
540
+ }
541
+ function assertValidUpdate(update) {
542
+ applyUpdate({}, update);
543
+ }
544
+ function applyUpdate(doc, update) {
545
+ validateUpdate(update);
546
+ const result = JSON.parse(JSON.stringify(doc));
547
+ const originalId = result._id;
548
+ if (update.$set) {
549
+ for (const [path, value] of Object.entries(update.$set)) {
550
+ if (path === "_id") continue;
551
+ setPath(result, path, value);
552
+ }
553
+ }
554
+ if (update.$unset) {
555
+ for (const path of Object.keys(update.$unset)) {
556
+ if (path === "_id") continue;
557
+ unsetPath(result, path);
558
+ }
559
+ }
560
+ if (update.$inc) {
561
+ for (const [path, delta] of Object.entries(update.$inc)) {
562
+ if (typeof delta !== "number") {
563
+ throw new FlexDBError(`$inc on "${path}" requires a number`, "INVALID_UPDATE");
564
+ }
565
+ const current = getPath(result, path);
566
+ if (current === void 0 || current === null) {
567
+ setPath(result, path, delta);
568
+ } else if (typeof current === "number") {
569
+ setPath(result, path, current + delta);
570
+ } else {
571
+ throw new FlexDBError(`Cannot $inc non-numeric field "${path}"`, "INVALID_UPDATE");
572
+ }
573
+ }
574
+ }
575
+ if (update.$push) {
576
+ for (const [path, value] of Object.entries(update.$push)) {
577
+ const current = getPath(result, path);
578
+ if (current === void 0) {
579
+ setPath(result, path, [value]);
580
+ } else if (Array.isArray(current)) {
581
+ current.push(cloneValue(value));
582
+ } else {
583
+ throw new FlexDBError(`Cannot $push to non-array field "${path}"`, "INVALID_UPDATE");
584
+ }
585
+ }
586
+ }
587
+ if (update.$pull) {
588
+ for (const [path, value] of Object.entries(update.$pull)) {
589
+ const current = getPath(result, path);
590
+ if (current === void 0) continue;
591
+ if (!Array.isArray(current)) {
592
+ throw new FlexDBError(`Cannot $pull from non-array field "${path}"`, "INVALID_UPDATE");
593
+ }
594
+ const filtered = current.filter((el) => !deepEqual(el, value));
595
+ setPath(result, path, filtered);
596
+ }
597
+ }
598
+ if (update.$addToSet) {
599
+ for (const [path, value] of Object.entries(update.$addToSet)) {
600
+ const current = getPath(result, path);
601
+ if (current === void 0) {
602
+ setPath(result, path, [value]);
603
+ } else if (Array.isArray(current)) {
604
+ if (!current.some((el) => deepEqual(el, value))) {
605
+ current.push(cloneValue(value));
606
+ }
607
+ } else {
608
+ throw new FlexDBError(`Cannot $addToSet on non-array field "${path}"`, "INVALID_UPDATE");
609
+ }
610
+ }
611
+ }
612
+ defineOwn(result, "_id", originalId);
613
+ return result;
614
+ }
615
+ function applyUpsertFromUpdate(query, update, id) {
616
+ const seed = {};
617
+ const ctx = { id };
618
+ seedFromQuery(query, seed, ctx);
619
+ const applied = applyUpdate(seed, update);
620
+ defineOwn(applied, "_id", ctx.id);
621
+ return applied;
622
+ }
623
+ function seedFromQuery(query, seed, ctx) {
624
+ for (const [key, value] of Object.entries(query)) {
625
+ if (key === "$and") {
626
+ if (Array.isArray(value)) {
627
+ for (const sub of value) {
628
+ if (sub && typeof sub === "object" && !Array.isArray(sub)) {
629
+ seedFromQuery(sub, seed, ctx);
630
+ }
631
+ }
632
+ }
633
+ continue;
634
+ }
635
+ if (key.startsWith("$")) continue;
636
+ const eq = equalityValue(value);
637
+ if (eq === NO_EQUALITY) continue;
638
+ if (key === "_id") {
639
+ if (typeof eq !== "string") {
640
+ throw new FlexDBError(
641
+ `Cannot upsert with a non-string _id filter: ${JSON.stringify(eq)}. _id must be a string.`,
642
+ "INVALID_QUERY"
643
+ );
644
+ }
645
+ ctx.id = eq;
646
+ continue;
647
+ }
648
+ assertFieldPath(key);
649
+ setPath(seed, key, eq);
650
+ }
651
+ }
652
+ var NO_EQUALITY = Symbol("no-equality");
653
+ function equalityValue(value) {
654
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
655
+ return value;
656
+ }
657
+ const keys = Object.keys(value);
658
+ if (keys.length === 0) return value;
659
+ if (!keys.every((k) => k.startsWith("$"))) return value;
660
+ if ("$eq" in value) return value.$eq;
661
+ return NO_EQUALITY;
662
+ }
663
+
664
+ // ../flexdb/src/pushdown.ts
665
+ var MAX_IN_PUSHDOWN = 90;
666
+ function isPushableId(v) {
667
+ return typeof v === "string";
668
+ }
669
+ function analyzeQuery(query, _indexedPaths) {
670
+ const sqlClauses = [];
671
+ let fullyPushed = true;
672
+ const keys = Object.keys(query);
673
+ if (keys.length === 0) {
674
+ return { sqlClauses, fullyPushed: true };
675
+ }
676
+ for (const key of keys) {
677
+ if (key === "_id") {
678
+ const pushed = pushIdCondition(query[key], sqlClauses);
679
+ if (!pushed) fullyPushed = false;
680
+ continue;
681
+ }
682
+ fullyPushed = false;
683
+ }
684
+ return { sqlClauses, fullyPushed };
685
+ }
686
+ function pushIdCondition(condition, out) {
687
+ if (isPushableId(condition)) {
688
+ out.push({ sql: "id = ?", params: [condition] });
689
+ return true;
690
+ }
691
+ if (condition && typeof condition === "object" && !Array.isArray(condition)) {
692
+ const ops = condition;
693
+ const opKeys = Object.keys(ops);
694
+ if (opKeys.length === 1 && opKeys[0] === "$eq" && isPushableId(ops.$eq)) {
695
+ out.push({ sql: "id = ?", params: [ops.$eq] });
696
+ return true;
697
+ }
698
+ if (opKeys.length === 1 && opKeys[0] === "$in" && Array.isArray(ops.$in) && ops.$in.every(isPushableId)) {
699
+ const arr = ops.$in;
700
+ if (arr.length === 0) {
701
+ out.push({ sql: "0 = 1", params: [] });
702
+ return true;
703
+ }
704
+ if (arr.length > MAX_IN_PUSHDOWN) {
705
+ return false;
706
+ }
707
+ out.push({
708
+ sql: `id IN (${arr.map(() => "?").join(", ")})`,
709
+ params: arr
710
+ });
711
+ return true;
712
+ }
713
+ }
714
+ return false;
715
+ }
716
+
717
+ // ../flexdb/src/flexdb.ts
718
+ var MAX_ROWS_PER_INSERT = 30;
719
+ var MAX_IDS_PER_IN = 90;
720
+ var MAX_STATEMENTS_PER_BATCH = 40;
721
+ function assertWriteFilter(query) {
722
+ if (query === null || query === void 0) {
723
+ throw new FlexDBError("A filter is required for update/delete operations", "INVALID_QUERY");
724
+ }
725
+ }
726
+ function chunk(arr, size) {
727
+ const out = [];
728
+ for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
729
+ return out;
730
+ }
731
+ var FlexDB = class {
732
+ #d1;
733
+ #initialized = false;
734
+ #initPromise = null;
735
+ constructor(d1) {
736
+ if (!d1 || typeof d1.prepare !== "function") {
737
+ throw new FlexDBError("FlexDB requires a D1Database binding", "INVALID_BINDING");
738
+ }
739
+ this.#d1 = d1;
740
+ }
741
+ /**
742
+ * Lazily create the table + base index. Idempotent and single-flight PER
743
+ * instance, so a long-lived (module-cached) handle runs the DDL once and then
744
+ * skips it on every subsequent request — see the README's handle-caching
745
+ * quickstart, which is how callers avoid per-request DDL (codex P2 r19).
746
+ */
747
+ async ensureInitialized() {
748
+ if (this.#initialized) return;
749
+ if (this.#initPromise) return this.#initPromise;
750
+ this.#initPromise = (async () => {
751
+ try {
752
+ await this.#d1.batch([
753
+ this.#d1.prepare(
754
+ `CREATE TABLE IF NOT EXISTS _flexdb_documents (
755
+ collection TEXT NOT NULL,
756
+ id TEXT NOT NULL,
757
+ document TEXT NOT NULL,
758
+ created_at INTEGER DEFAULT (strftime('%s','now')),
759
+ updated_at INTEGER DEFAULT (strftime('%s','now')),
760
+ PRIMARY KEY (collection, id)
761
+ )`
762
+ ),
763
+ this.#d1.prepare(
764
+ "CREATE INDEX IF NOT EXISTS idx_flexdb_documents_collection ON _flexdb_documents(collection)"
765
+ )
766
+ ]);
767
+ this.#initialized = true;
768
+ } catch (error) {
769
+ this.#initPromise = null;
770
+ const msg = error instanceof Error ? error.message : String(error);
771
+ throw new FlexDBError(`FlexDB initialization failed: ${msg}`, "INIT_FAILED");
772
+ }
773
+ })();
774
+ return this.#initPromise;
775
+ }
776
+ /** Get a typed collection handle. Validates the name eagerly. */
777
+ collection(name, options) {
778
+ assertCollectionName(name);
779
+ if (options?.indexes) {
780
+ for (const f of options.indexes) assertFieldPath(f);
781
+ }
782
+ return new Collection(this.#d1, name, options, () => this.ensureInitialized());
783
+ }
784
+ /** The underlying D1 binding (escape hatch for power users). */
785
+ get d1() {
786
+ return this.#d1;
787
+ }
788
+ };
789
+ var Collection = class {
790
+ #db;
791
+ #name;
792
+ #indexes;
793
+ #ensureInit;
794
+ #indexesEnsured = null;
795
+ constructor(db, name, options, ensureInit) {
796
+ this.#db = db;
797
+ this.#name = name;
798
+ this.#indexes = new Set(options?.indexes ?? []);
799
+ this.#ensureInit = ensureInit;
800
+ }
801
+ async #ready() {
802
+ await this.#ensureInit();
803
+ if (this.#indexes.size > 0) {
804
+ if (!this.#indexesEnsured) {
805
+ this.#indexesEnsured = this.#createDeclaredIndexes();
806
+ }
807
+ await this.#indexesEnsured;
808
+ }
809
+ }
810
+ async #createDeclaredIndexes() {
811
+ for (const field of this.#indexes) {
812
+ await this.#createSingleFieldIndex(field);
813
+ }
814
+ }
815
+ async #createSingleFieldIndex(field) {
816
+ const name = indexName(this.#name, [field]);
817
+ const expr = jsonExtract(field);
818
+ await this.#db.prepare(
819
+ `CREATE INDEX IF NOT EXISTS ${name}
820
+ ON _flexdb_documents(collection, ${expr})`
821
+ ).run();
822
+ }
823
+ /** Declared-index set used for pushdown decisions. */
824
+ #indexedPaths() {
825
+ return this.#indexes;
826
+ }
827
+ // --- Inserts ---
828
+ async insertOne(doc) {
829
+ await this.#ready();
830
+ const id = crypto.randomUUID();
831
+ const stored = { ...doc, _id: id };
832
+ await this.#db.prepare("INSERT INTO _flexdb_documents (collection, id, document) VALUES (?, ?, ?)").bind(this.#name, id, JSON.stringify(stored)).run();
833
+ return { insertedId: id };
834
+ }
835
+ async insertMany(docs) {
836
+ await this.#ready();
837
+ if (!Array.isArray(docs)) {
838
+ throw new FlexDBError("insertMany requires an array of documents", "INVALID_ARGUMENT");
839
+ }
840
+ if (docs.length === 0) return { insertedIds: [] };
841
+ const insertedIds = [];
842
+ const rows = docs.map((doc) => {
843
+ const id = crypto.randomUUID();
844
+ insertedIds.push(id);
845
+ return { id, json: JSON.stringify({ ...doc, _id: id }) };
846
+ });
847
+ const statements = chunk(rows, MAX_ROWS_PER_INSERT).map((group) => {
848
+ const placeholders = group.map(() => "(?, ?, ?)").join(", ");
849
+ const params = [];
850
+ for (const r of group) params.push(this.#name, r.id, r.json);
851
+ return this.#db.prepare(`INSERT INTO _flexdb_documents (collection, id, document) VALUES ${placeholders}`).bind(...params);
852
+ });
853
+ await this.#runBatched(statements);
854
+ return { insertedIds };
855
+ }
856
+ // --- Reads ---
857
+ async findOne(query) {
858
+ const results = await this.#execute(query ?? {}, { limit: 1 });
859
+ return results.length > 0 ? results[0] : null;
860
+ }
861
+ find(query, options) {
862
+ return new FlexFindCursor(
863
+ query ?? {},
864
+ options ?? {},
865
+ (q, o) => this.#execute(q, o)
866
+ );
867
+ }
868
+ async countDocuments(query) {
869
+ await this.#ready();
870
+ const q = query ?? {};
871
+ assertValidQuery(q);
872
+ const plan = analyzeQuery(q, this.#indexedPaths());
873
+ if (plan.fullyPushed) {
874
+ const { sql, params } = this.#buildSelect(plan, { count: true });
875
+ const row = await this.#db.prepare(sql).bind(...params).first();
876
+ return row?.n ?? 0;
877
+ }
878
+ const docs = await this.#execute(q, {});
879
+ return docs.length;
880
+ }
881
+ // --- Updates ---
882
+ async updateOne(query, update) {
883
+ await this.#ready();
884
+ assertWriteFilter(query);
885
+ assertValidUpdate(update);
886
+ const doc = await this.findOne(query);
887
+ if (!doc) return { matchedCount: 0, modifiedCount: 0 };
888
+ const updated = applyUpdate(doc, update);
889
+ if (deepEqual(doc, updated)) return { matchedCount: 1, modifiedCount: 0 };
890
+ await this.#writeDocument(doc._id, updated);
891
+ return { matchedCount: 1, modifiedCount: 1 };
892
+ }
893
+ async updateMany(query, update) {
894
+ await this.#ready();
895
+ assertWriteFilter(query);
896
+ assertValidUpdate(update);
897
+ const docs = await this.#execute(query, {});
898
+ if (docs.length === 0) return { matchedCount: 0, modifiedCount: 0 };
899
+ const statements = [];
900
+ for (const doc of docs) {
901
+ const updated = applyUpdate(doc, update);
902
+ if (deepEqual(doc, updated)) continue;
903
+ statements.push(
904
+ this.#db.prepare(
905
+ `UPDATE _flexdb_documents SET document = ?, updated_at = strftime('%s','now')
906
+ WHERE collection = ? AND id = ?`
907
+ ).bind(JSON.stringify(updated), this.#name, doc._id)
908
+ );
909
+ }
910
+ await this.#runBatched(statements);
911
+ return { matchedCount: docs.length, modifiedCount: statements.length };
912
+ }
913
+ async findOneAndUpdate(query, update, options) {
914
+ await this.#ready();
915
+ assertWriteFilter(query);
916
+ assertValidUpdate(update);
917
+ const doc = await this.findOne(query);
918
+ if (!doc) {
919
+ if (options?.upsert) {
920
+ const created = applyUpsertFromUpdate(query, update, crypto.randomUUID());
921
+ await this.#db.prepare("INSERT INTO _flexdb_documents (collection, id, document) VALUES (?, ?, ?)").bind(this.#name, created._id, JSON.stringify(created)).run();
922
+ return created;
923
+ }
924
+ return null;
925
+ }
926
+ const updated = applyUpdate(doc, update);
927
+ if (!deepEqual(doc, updated)) {
928
+ await this.#writeDocument(doc._id, updated);
929
+ }
930
+ return updated;
931
+ }
932
+ // --- Deletes ---
933
+ async deleteOne(query) {
934
+ await this.#ready();
935
+ assertWriteFilter(query);
936
+ const doc = await this.findOne(query);
937
+ if (!doc) return { deletedCount: 0 };
938
+ await this.#db.prepare("DELETE FROM _flexdb_documents WHERE collection = ? AND id = ?").bind(this.#name, doc._id).run();
939
+ return { deletedCount: 1 };
940
+ }
941
+ async deleteMany(query) {
942
+ await this.#ready();
943
+ assertWriteFilter(query);
944
+ const docs = await this.#execute(query, {});
945
+ if (docs.length === 0) return { deletedCount: 0 };
946
+ const ids = docs.map((doc) => doc._id);
947
+ const statements = chunk(ids, MAX_IDS_PER_IN).map(
948
+ (group) => this.#db.prepare(
949
+ `DELETE FROM _flexdb_documents WHERE collection = ? AND id IN (${group.map(() => "?").join(", ")})`
950
+ ).bind(this.#name, ...group)
951
+ );
952
+ await this.#runBatched(statements);
953
+ return { deletedCount: ids.length };
954
+ }
955
+ // --- Indexing ---
956
+ async createIndex(fields) {
957
+ await this.#ensureInit();
958
+ if (!Array.isArray(fields) || fields.length === 0) {
959
+ throw new FlexDBError("createIndex requires a non-empty array of field paths", "INVALID_ARGUMENT");
960
+ }
961
+ for (const f of fields) assertFieldPath(f);
962
+ if (fields.length === 1) {
963
+ this.#indexes.add(fields[0]);
964
+ await this.#createSingleFieldIndex(fields[0]);
965
+ return;
966
+ }
967
+ const name = indexName(this.#name, fields);
968
+ const exprs = fields.map((f) => jsonExtract(f)).join(", ");
969
+ await this.#db.prepare(`CREATE INDEX IF NOT EXISTS ${name} ON _flexdb_documents(collection, ${exprs})`).run();
970
+ for (const f of fields) this.#indexes.add(f);
971
+ }
972
+ // --- Internals ---
973
+ async #writeDocument(id, doc) {
974
+ await this.#db.prepare(
975
+ `UPDATE _flexdb_documents SET document = ?, updated_at = strftime('%s','now')
976
+ WHERE collection = ? AND id = ?`
977
+ ).bind(JSON.stringify(doc), this.#name, id).run();
978
+ }
979
+ /** Run statements in batches under D1's per-invocation statement cap. */
980
+ async #runBatched(statements) {
981
+ for (const group of chunk(statements, MAX_STATEMENTS_PER_BATCH)) {
982
+ if (group.length > 0) await this.#db.batch(group);
983
+ }
984
+ }
985
+ /**
986
+ * Build the SELECT (or COUNT) statement for a pushdown plan. Only validated
987
+ * identifiers are interpolated; every value is parameter-bound.
988
+ */
989
+ #buildSelect(plan, opts = {}) {
990
+ const projection = opts.count ? "COUNT(*) AS n" : "document";
991
+ let sql = `SELECT ${projection} FROM _flexdb_documents WHERE collection = ?`;
992
+ const params = [this.#name];
993
+ for (const clause of plan.sqlClauses) {
994
+ sql += ` AND ${clause.sql}`;
995
+ params.push(...clause.params);
996
+ }
997
+ return { sql, params };
998
+ }
999
+ /**
1000
+ * The query executor behind findOne/find/count helpers.
1001
+ *
1002
+ * Pushdown + the LIMIT fix:
1003
+ * - Field conditions on INDEXED paths with SQL-mappable operators are pushed
1004
+ * into the SELECT (`AND json_extract(...) <op> ?`), plus the `_id` PK
1005
+ * fast-path. The `_id` and indexed-eq pushes let SQLite use indexes.
1006
+ * - SQL LIMIT/OFFSET are appended ONLY when the WHOLE query was pushed down
1007
+ * (`plan.fullyPushed`) AND there is no JS sort. Otherwise we fetch all
1008
+ * rows the SQL prefilter matched, run the full matcher in JS, sort, and
1009
+ * THEN apply skip/limit in JS — fixing the upstream bug where LIMIT ran
1010
+ * before the JS filter and returned too few documents.
1011
+ */
1012
+ async #execute(query, options) {
1013
+ await this.#ready();
1014
+ assertValidQuery(query);
1015
+ const plan = analyzeQuery(query, this.#indexedPaths());
1016
+ const skip = options.skip !== void 0 ? assertNonNegativeInt(options.skip, "skip") : 0;
1017
+ const limit = options.limit !== void 0 ? assertNonNegativeInt(options.limit, "limit") : void 0;
1018
+ const hasSort = !!options.sort && Object.keys(options.sort).length > 0;
1019
+ const canPushPagination = plan.fullyPushed && !hasSort;
1020
+ const { sql: baseSql, params } = this.#buildSelect(plan);
1021
+ let sql = baseSql;
1022
+ if (canPushPagination) {
1023
+ if (limit !== void 0) {
1024
+ sql += ` LIMIT ${limit}`;
1025
+ if (skip > 0) sql += ` OFFSET ${skip}`;
1026
+ } else if (skip > 0) {
1027
+ sql += ` LIMIT -1 OFFSET ${skip}`;
1028
+ }
1029
+ }
1030
+ const result = await this.#db.prepare(sql).bind(...params).all();
1031
+ const rows = result.results ?? [];
1032
+ let docs = [];
1033
+ for (const row of rows) {
1034
+ let parsed;
1035
+ try {
1036
+ parsed = JSON.parse(row.document);
1037
+ } catch {
1038
+ continue;
1039
+ }
1040
+ docs.push(parsed);
1041
+ }
1042
+ if (!plan.fullyPushed) {
1043
+ docs = docs.filter((doc) => matchesQuery(doc, query));
1044
+ }
1045
+ if (hasSort) {
1046
+ docs = sortDocuments(docs, options.sort);
1047
+ }
1048
+ if (!canPushPagination) {
1049
+ if (skip > 0) docs = docs.slice(skip);
1050
+ if (limit !== void 0) docs = docs.slice(0, limit);
1051
+ }
1052
+ return docs;
1053
+ }
1054
+ };
1055
+ var FlexFindCursor = class {
1056
+ #query;
1057
+ #options;
1058
+ #executor;
1059
+ constructor(query, options, executor) {
1060
+ this.#query = query;
1061
+ this.#options = { ...options };
1062
+ this.#executor = executor;
1063
+ }
1064
+ sort(sort) {
1065
+ this.#options.sort = sort;
1066
+ return this;
1067
+ }
1068
+ limit(limit) {
1069
+ this.#options.limit = limit;
1070
+ return this;
1071
+ }
1072
+ skip(skip) {
1073
+ this.#options.skip = skip;
1074
+ return this;
1075
+ }
1076
+ toArray() {
1077
+ return this.#executor(this.#query, this.#options);
1078
+ }
1079
+ then(onfulfilled, onrejected) {
1080
+ return this.toArray().then(onfulfilled, onrejected);
1081
+ }
1082
+ };
1083
+ export {
1084
+ Collection,
1085
+ FlexDB,
1086
+ FlexDBError
1087
+ };