@monlite/core 1.2.0 → 1.4.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/dist/index.d.cts CHANGED
@@ -76,6 +76,8 @@ declare class Collection<T = Doc> {
76
76
  /** @internal Synchronous core of exists (used by reactivity). */
77
77
  existsCore(where: WhereInput<T> | undefined): boolean;
78
78
  findMany(args?: FindManyArgs<T>): Promise<WithId<T>[]>;
79
+ /** Resolve one `$lookup` spec against already-fetched rows (2 queries, no N+1). */
80
+ private applyLookup;
79
81
  findFirst(args?: FindFirstArgs<T>): Promise<WithId<T> | null>;
80
82
  /** Alias of {@link findFirst} for Prisma familiarity. */
81
83
  findUnique(args?: FindFirstArgs<T>): Promise<WithId<T> | null>;
@@ -140,6 +142,48 @@ interface MonlitePlugin {
140
142
  collectionMethods?: Record<string, (collection: Collection<any>, ...args: any[]) => any>;
141
143
  }
142
144
 
145
+ /**
146
+ * The minimal SQLite driver surface monlite needs. Implemented by both the
147
+ * better-sqlite3 and the built-in node:sqlite backends so the rest of the
148
+ * codebase is engine-agnostic.
149
+ */
150
+ interface RunResult {
151
+ changes: number;
152
+ lastInsertRowid: number | bigint;
153
+ }
154
+ interface PreparedStatement {
155
+ run(...params: any[]): RunResult;
156
+ get(...params: any[]): any;
157
+ all(...params: any[]): any[];
158
+ }
159
+ interface Driver {
160
+ /** Backend identifier, e.g. "better-sqlite3" or "node:sqlite". */
161
+ readonly name: string;
162
+ exec(sql: string): void;
163
+ prepare(sql: string): PreparedStatement;
164
+ /** Run `fn` inside a transaction; rolls back and rethrows if it throws. */
165
+ transaction<T>(fn: () => T): T;
166
+ close(): void;
167
+ /** Rotate the encryption key (encrypted backends only). */
168
+ rekey?(key: string, cipher?: string): void;
169
+ /** The underlying native handle (better-sqlite3 Database / node:sqlite DatabaseSync). */
170
+ readonly raw: any;
171
+ }
172
+ interface DriverOpenOptions {
173
+ readonly?: boolean;
174
+ wal?: boolean;
175
+ /** Milliseconds to wait on a locked database before erroring. Default 5000. */
176
+ busyTimeout?: number;
177
+ /** Allow loading SQLite extensions (needed by `@monlite/vector`). Default false. */
178
+ allowExtensions?: boolean;
179
+ /** Encrypt the database at rest (better-sqlite3-multiple-ciphers only). */
180
+ encryption?: {
181
+ key: string;
182
+ cipher?: string;
183
+ };
184
+ verbose?: (sql: string) => void;
185
+ }
186
+
143
187
  /**
144
188
  * Public type surface for monlite.
145
189
  *
@@ -299,18 +343,38 @@ type Select<T = Doc> = {
299
343
  } & {
300
344
  [path: string]: boolean;
301
345
  };
346
+ /**
347
+ * A `$lookup`-style left join: for each result, fetch matching documents from
348
+ * another collection and attach them. With `unwind`, emit one row per match
349
+ * (`$unwind`); `unwind: "preserve"` also keeps rows that have no match.
350
+ */
351
+ interface LookupSpec {
352
+ /** The collection to join. */
353
+ from: string;
354
+ /** Field on this collection to match on. */
355
+ localField: string;
356
+ /** Field on the `from` collection to match against. */
357
+ foreignField: string;
358
+ /** Output field that receives the matched document(s). */
359
+ as: string;
360
+ /** Flatten the `as` array to a single object (one output row per match). */
361
+ unwind?: boolean | "preserve";
362
+ }
302
363
  interface FindManyArgs<T = Doc> {
303
364
  where?: WhereInput<T>;
304
365
  orderBy?: OrderBy<T>;
305
366
  select?: Select<T>;
306
367
  skip?: number;
307
368
  take?: number;
369
+ /** Join related documents from other collections ($lookup / $unwind). */
370
+ lookup?: LookupSpec | LookupSpec[];
308
371
  }
309
372
  interface FindFirstArgs<T = Doc> {
310
373
  where?: WhereInput<T>;
311
374
  orderBy?: OrderBy<T>;
312
375
  select?: Select<T>;
313
376
  skip?: number;
377
+ lookup?: LookupSpec | LookupSpec[];
314
378
  }
315
379
  interface CreateArgs<T = Doc> {
316
380
  data: Partial<T> & Record<string, any>;
@@ -396,8 +460,10 @@ interface MonliteOptions {
396
460
  /**
397
461
  * Which SQLite backend to use. `"auto"` (default) prefers `better-sqlite3`
398
462
  * when installed, otherwise the built-in `node:sqlite` (Node >= 22.5).
463
+ * You can also pass a custom {@link Driver} instance (e.g. `@monlite/wasm`
464
+ * for the browser).
399
465
  */
400
- driver?: DriverName;
466
+ driver?: DriverName | Driver;
401
467
  /**
402
468
  * Encrypt the database at rest. Requires the `better-sqlite3-multiple-ciphers`
403
469
  * package (a drop-in for `better-sqlite3`); not supported on `node:sqlite`.
@@ -433,34 +499,6 @@ interface MonliteOptions {
433
499
  verbose?: (sql: string) => void;
434
500
  }
435
501
 
436
- /**
437
- * The minimal SQLite driver surface monlite needs. Implemented by both the
438
- * better-sqlite3 and the built-in node:sqlite backends so the rest of the
439
- * codebase is engine-agnostic.
440
- */
441
- interface RunResult {
442
- changes: number;
443
- lastInsertRowid: number | bigint;
444
- }
445
- interface PreparedStatement {
446
- run(...params: any[]): RunResult;
447
- get(...params: any[]): any;
448
- all(...params: any[]): any[];
449
- }
450
- interface Driver {
451
- /** Backend identifier, e.g. "better-sqlite3" or "node:sqlite". */
452
- readonly name: string;
453
- exec(sql: string): void;
454
- prepare(sql: string): PreparedStatement;
455
- /** Run `fn` inside a transaction; rolls back and rethrows if it throws. */
456
- transaction<T>(fn: () => T): T;
457
- close(): void;
458
- /** Rotate the encryption key (encrypted backends only). */
459
- rekey?(key: string, cipher?: string): void;
460
- /** The underlying native handle (better-sqlite3 Database / node:sqlite DatabaseSync). */
461
- readonly raw: any;
462
- }
463
-
464
502
  /**
465
503
  * Tracks which JSON paths are queried per collection and silently creates a
466
504
  * SQLite expression index once a path crosses the configured threshold.
@@ -772,4 +810,4 @@ declare function objectId(): string;
772
810
  /** True when a value looks like a monlite/ObjectId id (24 hex chars). */
773
811
  declare function isObjectId(value: unknown): value is string;
774
812
 
775
- export { type AggregateArgs, type AggregateResult, type ApplyResult, Collection, type CollectionMode, type CollectionOptions, type CollectionSchema, type ColumnDef, type ColumnInfo, type ColumnType, type ConflictResolver, type ConflictRow, type CountArgs, type CreateArgs, type CreateManyArgs, Monlite as Db, type DeleteArgs, type Doc, type Driver, type DriverName, type EncryptionOptions, type ExplainResult, type FieldFilter, type FieldSelection, type FilterInput, type FindFirstArgs, type FindManyArgs, type GroupByArgs, type GroupByResult, type HavingComparison, type HavingInput, type LiveEvent, type LocalChange, type MigrateOptions, Monlite, MonliteConstraintError, MonliteEncryptionError, MonliteError, MonliteForeignKeyError, MonliteNotNullError, type MonliteOptions, type MonlitePlugin, MonliteQueryError, MonliteUniqueConstraintError, type OrderBy, type PluginChange, type PreparedStatement, type RemoteChange, type Select, type SortOrder, type SyncOp, type SyncStateRow, SyncStore, type SystemFields, type UpdateArgs, type UpdateData, type UpdateOperators, type UpsertArgs, type Version, type WatchHandle, type WhereInput as WhereClause, type WhereInput, type WithId, compareVersions, createDb, isObjectId, makeVersion, normalizeDriverError, objectId, versionTs };
813
+ export { type AggregateArgs, type AggregateResult, type ApplyResult, Collection, type CollectionMode, type CollectionOptions, type CollectionSchema, type ColumnDef, type ColumnInfo, type ColumnType, type ConflictResolver, type ConflictRow, type CountArgs, type CreateArgs, type CreateManyArgs, Monlite as Db, type DeleteArgs, type Doc, type Driver, type DriverName, type DriverOpenOptions, type EncryptionOptions, type ExplainResult, type FieldFilter, type FieldSelection, type FilterInput, type FindFirstArgs, type FindManyArgs, type GroupByArgs, type GroupByResult, type HavingComparison, type HavingInput, type LiveEvent, type LocalChange, type LookupSpec, type MigrateOptions, Monlite, MonliteConstraintError, MonliteEncryptionError, MonliteError, MonliteForeignKeyError, MonliteNotNullError, type MonliteOptions, type MonlitePlugin, MonliteQueryError, MonliteUniqueConstraintError, type OrderBy, type PluginChange, type PreparedStatement, type RemoteChange, type RunResult, type Select, type SortOrder, type SyncOp, type SyncStateRow, SyncStore, type SystemFields, type UpdateArgs, type UpdateData, type UpdateOperators, type UpsertArgs, type Version, type WatchHandle, type WhereInput as WhereClause, type WhereInput, type WithId, compareVersions, createDb, isObjectId, makeVersion, normalizeDriverError, objectId, versionTs };
package/dist/index.d.ts CHANGED
@@ -76,6 +76,8 @@ declare class Collection<T = Doc> {
76
76
  /** @internal Synchronous core of exists (used by reactivity). */
77
77
  existsCore(where: WhereInput<T> | undefined): boolean;
78
78
  findMany(args?: FindManyArgs<T>): Promise<WithId<T>[]>;
79
+ /** Resolve one `$lookup` spec against already-fetched rows (2 queries, no N+1). */
80
+ private applyLookup;
79
81
  findFirst(args?: FindFirstArgs<T>): Promise<WithId<T> | null>;
80
82
  /** Alias of {@link findFirst} for Prisma familiarity. */
81
83
  findUnique(args?: FindFirstArgs<T>): Promise<WithId<T> | null>;
@@ -140,6 +142,48 @@ interface MonlitePlugin {
140
142
  collectionMethods?: Record<string, (collection: Collection<any>, ...args: any[]) => any>;
141
143
  }
142
144
 
145
+ /**
146
+ * The minimal SQLite driver surface monlite needs. Implemented by both the
147
+ * better-sqlite3 and the built-in node:sqlite backends so the rest of the
148
+ * codebase is engine-agnostic.
149
+ */
150
+ interface RunResult {
151
+ changes: number;
152
+ lastInsertRowid: number | bigint;
153
+ }
154
+ interface PreparedStatement {
155
+ run(...params: any[]): RunResult;
156
+ get(...params: any[]): any;
157
+ all(...params: any[]): any[];
158
+ }
159
+ interface Driver {
160
+ /** Backend identifier, e.g. "better-sqlite3" or "node:sqlite". */
161
+ readonly name: string;
162
+ exec(sql: string): void;
163
+ prepare(sql: string): PreparedStatement;
164
+ /** Run `fn` inside a transaction; rolls back and rethrows if it throws. */
165
+ transaction<T>(fn: () => T): T;
166
+ close(): void;
167
+ /** Rotate the encryption key (encrypted backends only). */
168
+ rekey?(key: string, cipher?: string): void;
169
+ /** The underlying native handle (better-sqlite3 Database / node:sqlite DatabaseSync). */
170
+ readonly raw: any;
171
+ }
172
+ interface DriverOpenOptions {
173
+ readonly?: boolean;
174
+ wal?: boolean;
175
+ /** Milliseconds to wait on a locked database before erroring. Default 5000. */
176
+ busyTimeout?: number;
177
+ /** Allow loading SQLite extensions (needed by `@monlite/vector`). Default false. */
178
+ allowExtensions?: boolean;
179
+ /** Encrypt the database at rest (better-sqlite3-multiple-ciphers only). */
180
+ encryption?: {
181
+ key: string;
182
+ cipher?: string;
183
+ };
184
+ verbose?: (sql: string) => void;
185
+ }
186
+
143
187
  /**
144
188
  * Public type surface for monlite.
145
189
  *
@@ -299,18 +343,38 @@ type Select<T = Doc> = {
299
343
  } & {
300
344
  [path: string]: boolean;
301
345
  };
346
+ /**
347
+ * A `$lookup`-style left join: for each result, fetch matching documents from
348
+ * another collection and attach them. With `unwind`, emit one row per match
349
+ * (`$unwind`); `unwind: "preserve"` also keeps rows that have no match.
350
+ */
351
+ interface LookupSpec {
352
+ /** The collection to join. */
353
+ from: string;
354
+ /** Field on this collection to match on. */
355
+ localField: string;
356
+ /** Field on the `from` collection to match against. */
357
+ foreignField: string;
358
+ /** Output field that receives the matched document(s). */
359
+ as: string;
360
+ /** Flatten the `as` array to a single object (one output row per match). */
361
+ unwind?: boolean | "preserve";
362
+ }
302
363
  interface FindManyArgs<T = Doc> {
303
364
  where?: WhereInput<T>;
304
365
  orderBy?: OrderBy<T>;
305
366
  select?: Select<T>;
306
367
  skip?: number;
307
368
  take?: number;
369
+ /** Join related documents from other collections ($lookup / $unwind). */
370
+ lookup?: LookupSpec | LookupSpec[];
308
371
  }
309
372
  interface FindFirstArgs<T = Doc> {
310
373
  where?: WhereInput<T>;
311
374
  orderBy?: OrderBy<T>;
312
375
  select?: Select<T>;
313
376
  skip?: number;
377
+ lookup?: LookupSpec | LookupSpec[];
314
378
  }
315
379
  interface CreateArgs<T = Doc> {
316
380
  data: Partial<T> & Record<string, any>;
@@ -396,8 +460,10 @@ interface MonliteOptions {
396
460
  /**
397
461
  * Which SQLite backend to use. `"auto"` (default) prefers `better-sqlite3`
398
462
  * when installed, otherwise the built-in `node:sqlite` (Node >= 22.5).
463
+ * You can also pass a custom {@link Driver} instance (e.g. `@monlite/wasm`
464
+ * for the browser).
399
465
  */
400
- driver?: DriverName;
466
+ driver?: DriverName | Driver;
401
467
  /**
402
468
  * Encrypt the database at rest. Requires the `better-sqlite3-multiple-ciphers`
403
469
  * package (a drop-in for `better-sqlite3`); not supported on `node:sqlite`.
@@ -433,34 +499,6 @@ interface MonliteOptions {
433
499
  verbose?: (sql: string) => void;
434
500
  }
435
501
 
436
- /**
437
- * The minimal SQLite driver surface monlite needs. Implemented by both the
438
- * better-sqlite3 and the built-in node:sqlite backends so the rest of the
439
- * codebase is engine-agnostic.
440
- */
441
- interface RunResult {
442
- changes: number;
443
- lastInsertRowid: number | bigint;
444
- }
445
- interface PreparedStatement {
446
- run(...params: any[]): RunResult;
447
- get(...params: any[]): any;
448
- all(...params: any[]): any[];
449
- }
450
- interface Driver {
451
- /** Backend identifier, e.g. "better-sqlite3" or "node:sqlite". */
452
- readonly name: string;
453
- exec(sql: string): void;
454
- prepare(sql: string): PreparedStatement;
455
- /** Run `fn` inside a transaction; rolls back and rethrows if it throws. */
456
- transaction<T>(fn: () => T): T;
457
- close(): void;
458
- /** Rotate the encryption key (encrypted backends only). */
459
- rekey?(key: string, cipher?: string): void;
460
- /** The underlying native handle (better-sqlite3 Database / node:sqlite DatabaseSync). */
461
- readonly raw: any;
462
- }
463
-
464
502
  /**
465
503
  * Tracks which JSON paths are queried per collection and silently creates a
466
504
  * SQLite expression index once a path crosses the configured threshold.
@@ -772,4 +810,4 @@ declare function objectId(): string;
772
810
  /** True when a value looks like a monlite/ObjectId id (24 hex chars). */
773
811
  declare function isObjectId(value: unknown): value is string;
774
812
 
775
- export { type AggregateArgs, type AggregateResult, type ApplyResult, Collection, type CollectionMode, type CollectionOptions, type CollectionSchema, type ColumnDef, type ColumnInfo, type ColumnType, type ConflictResolver, type ConflictRow, type CountArgs, type CreateArgs, type CreateManyArgs, Monlite as Db, type DeleteArgs, type Doc, type Driver, type DriverName, type EncryptionOptions, type ExplainResult, type FieldFilter, type FieldSelection, type FilterInput, type FindFirstArgs, type FindManyArgs, type GroupByArgs, type GroupByResult, type HavingComparison, type HavingInput, type LiveEvent, type LocalChange, type MigrateOptions, Monlite, MonliteConstraintError, MonliteEncryptionError, MonliteError, MonliteForeignKeyError, MonliteNotNullError, type MonliteOptions, type MonlitePlugin, MonliteQueryError, MonliteUniqueConstraintError, type OrderBy, type PluginChange, type PreparedStatement, type RemoteChange, type Select, type SortOrder, type SyncOp, type SyncStateRow, SyncStore, type SystemFields, type UpdateArgs, type UpdateData, type UpdateOperators, type UpsertArgs, type Version, type WatchHandle, type WhereInput as WhereClause, type WhereInput, type WithId, compareVersions, createDb, isObjectId, makeVersion, normalizeDriverError, objectId, versionTs };
813
+ export { type AggregateArgs, type AggregateResult, type ApplyResult, Collection, type CollectionMode, type CollectionOptions, type CollectionSchema, type ColumnDef, type ColumnInfo, type ColumnType, type ConflictResolver, type ConflictRow, type CountArgs, type CreateArgs, type CreateManyArgs, Monlite as Db, type DeleteArgs, type Doc, type Driver, type DriverName, type DriverOpenOptions, type EncryptionOptions, type ExplainResult, type FieldFilter, type FieldSelection, type FilterInput, type FindFirstArgs, type FindManyArgs, type GroupByArgs, type GroupByResult, type HavingComparison, type HavingInput, type LiveEvent, type LocalChange, type LookupSpec, type MigrateOptions, Monlite, MonliteConstraintError, MonliteEncryptionError, MonliteError, MonliteForeignKeyError, MonliteNotNullError, type MonliteOptions, type MonlitePlugin, MonliteQueryError, MonliteUniqueConstraintError, type OrderBy, type PluginChange, type PreparedStatement, type RemoteChange, type RunResult, type Select, type SortOrder, type SyncOp, type SyncStateRow, SyncStore, type SystemFields, type UpdateArgs, type UpdateData, type UpdateOperators, type UpsertArgs, type Version, type WatchHandle, type WhereInput as WhereClause, type WhereInput, type WithId, compareVersions, createDb, isObjectId, makeVersion, normalizeDriverError, objectId, versionTs };
package/dist/index.js CHANGED
@@ -1150,7 +1150,56 @@ var Collection = class {
1150
1150
  return this.db.prepare(`SELECT 1 FROM "${this.name}" WHERE ${clause} LIMIT 1`).get(...params) != null;
1151
1151
  }
1152
1152
  async findMany(args = {}) {
1153
- return this.findManyCore(args);
1153
+ if (!args.lookup) return this.findManyCore(args);
1154
+ const specs = Array.isArray(args.lookup) ? args.lookup : [args.lookup];
1155
+ let rows = this.findManyCore({
1156
+ ...args,
1157
+ select: void 0,
1158
+ lookup: void 0
1159
+ });
1160
+ for (const spec of specs) rows = await this.applyLookup(rows, spec);
1161
+ if (args.select) {
1162
+ rows = rows.map((r) => {
1163
+ const projected = project(r, args.select);
1164
+ for (const spec of specs) projected[spec.as] = r[spec.as];
1165
+ return projected;
1166
+ });
1167
+ }
1168
+ return rows;
1169
+ }
1170
+ /** Resolve one `$lookup` spec against already-fetched rows (2 queries, no N+1). */
1171
+ async applyLookup(rows, spec) {
1172
+ const localValues = [
1173
+ ...new Set(
1174
+ rows.map((r) => r[spec.localField]).filter((v) => v !== void 0 && v !== null)
1175
+ )
1176
+ ];
1177
+ const foreign = localValues.length ? await this.mon.collection(spec.from).findMany({
1178
+ where: { [spec.foreignField]: { in: localValues } }
1179
+ }) : [];
1180
+ const byKey = /* @__PURE__ */ new Map();
1181
+ for (const f of foreign) {
1182
+ const key = f[spec.foreignField];
1183
+ const list = byKey.get(key);
1184
+ if (list) list.push(f);
1185
+ else byKey.set(key, [f]);
1186
+ }
1187
+ if (spec.unwind) {
1188
+ const out = [];
1189
+ for (const r of rows) {
1190
+ const matches = byKey.get(r[spec.localField]) ?? [];
1191
+ if (matches.length === 0) {
1192
+ if (spec.unwind === "preserve") out.push({ ...r, [spec.as]: null });
1193
+ } else {
1194
+ for (const m of matches) out.push({ ...r, [spec.as]: m });
1195
+ }
1196
+ }
1197
+ return out;
1198
+ }
1199
+ return rows.map((r) => ({
1200
+ ...r,
1201
+ [spec.as]: byKey.get(r[spec.localField]) ?? []
1202
+ }));
1154
1203
  }
1155
1204
  async findFirst(args = {}) {
1156
1205
  const rows = await this.findMany({ ...args, take: 1 });
@@ -1612,7 +1661,11 @@ function loadNodeSqlite() {
1612
1661
  return null;
1613
1662
  }
1614
1663
  }
1664
+ function isDriverInstance(d) {
1665
+ return typeof d === "object" && d !== null && typeof d.prepare === "function" && typeof d.exec === "function";
1666
+ }
1615
1667
  function createDriver(filename, options = {}) {
1668
+ if (isDriverInstance(options.driver)) return options.driver;
1616
1669
  const choice = options.driver ?? "auto";
1617
1670
  if (options.encryption) {
1618
1671
  if (choice === "node:sqlite") {