@monlite/core 1.3.0 → 2.0.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
@@ -75,12 +75,22 @@ declare class Collection<T = Doc> {
75
75
  findManyCore(args?: FindManyArgs<T>): WithId<T>[];
76
76
  /** @internal Synchronous core of exists (used by reactivity). */
77
77
  existsCore(where: WhereInput<T> | undefined): boolean;
78
- findMany(args?: FindManyArgs<T>): Promise<WithId<T>[]>;
79
- findFirst(args?: FindFirstArgs<T>): Promise<WithId<T> | null>;
78
+ findMany<S extends Select<T> | undefined = undefined>(args?: Omit<FindManyArgs<T>, "select"> & {
79
+ select?: S;
80
+ }): Promise<Projected<T, S>[]>;
81
+ /** Resolve one `$lookup` spec against already-fetched rows (2 queries, no N+1). */
82
+ private applyLookup;
83
+ findFirst<S extends Select<T> | undefined = undefined>(args?: Omit<FindFirstArgs<T>, "select"> & {
84
+ select?: S;
85
+ }): Promise<Projected<T, S> | null>;
80
86
  /** Alias of {@link findFirst} for Prisma familiarity. */
81
- findUnique(args?: FindFirstArgs<T>): Promise<WithId<T> | null>;
87
+ findUnique<S extends Select<T> | undefined = undefined>(args?: Omit<FindFirstArgs<T>, "select"> & {
88
+ select?: S;
89
+ }): Promise<Projected<T, S> | null>;
82
90
  /** Like {@link findFirst} but throws if no document matches. */
83
- findFirstOrThrow(args?: FindFirstArgs<T>): Promise<WithId<T>>;
91
+ findFirstOrThrow<S extends Select<T> | undefined = undefined>(args?: Omit<FindFirstArgs<T>, "select"> & {
92
+ select?: S;
93
+ }): Promise<Projected<T, S>>;
84
94
  /** True if at least one document matches. */
85
95
  exists(where?: WhereInput<T>): Promise<boolean>;
86
96
  /**
@@ -299,9 +309,12 @@ interface FieldFilter<V = any> {
299
309
  }
300
310
  /** A value used directly as a filter is shorthand for `{ equals: value }`. */
301
311
  type FilterInput<V> = V | FieldFilter<V>;
312
+ /** A dot-notation nested path, e.g. `"address.city"`. */
313
+ type DotPath = `${string}.${string}`;
302
314
  /**
303
- * Where input. Known fields are typed from `T`; nested paths can also be
304
- * addressed with dot notation (e.g. `"address.city"`).
315
+ * Where input. For a **typed** collection, keys are checked against `T` (an
316
+ * unknown field is a type error); dot-notation nested paths are still allowed.
317
+ * For an **untyped** collection (`Doc`), any field is accepted (schema-free).
305
318
  */
306
319
  type WhereInput<T = Doc> = {
307
320
  [K in keyof T]?: FilterInput<T[K]>;
@@ -313,7 +326,7 @@ type WhereInput<T = Doc> = {
313
326
  OR?: WhereInput<T> | WhereInput<T>[];
314
327
  NOT?: WhereInput<T> | WhereInput<T>[];
315
328
  } & {
316
- [path: string]: any;
329
+ [path: DotPath]: FilterInput<any>;
317
330
  };
318
331
  /** Mongo-inspired update operators. */
319
332
  interface UpdateOperators {
@@ -330,29 +343,63 @@ interface UpdateOperators {
330
343
  type UpdateData<T = Doc> = (Partial<T> & Record<string, any>) | UpdateOperators;
331
344
  type SortOrder = "asc" | "desc";
332
345
  type OrderBy<T = Doc> = ({
333
- [K in keyof T]?: SortOrder;
346
+ [K in keyof WithId<T>]?: SortOrder;
334
347
  } & {
335
- [path: string]: SortOrder;
348
+ [path: DotPath]: SortOrder;
336
349
  }) | Array<{
337
- [path: string]: SortOrder;
350
+ [K in keyof WithId<T>]?: SortOrder;
351
+ } & {
352
+ [path: DotPath]: SortOrder;
338
353
  }>;
339
354
  type Select<T = Doc> = {
340
- [K in keyof T]?: boolean;
355
+ [K in keyof WithId<T>]?: boolean;
341
356
  } & {
342
- [path: string]: boolean;
357
+ [path: DotPath]: boolean;
358
+ };
359
+ /** True for the schema-free `Doc` (and `any`), so typed collections opt in only. */
360
+ type IsLoose<T> = string extends keyof T ? true : false;
361
+ /** Keys of a select set to a truthy value (a widened `boolean` counts as truthy). */
362
+ type SelectedKeys<S> = keyof {
363
+ [K in keyof S as S[K] extends false ? never : K]: K;
343
364
  };
365
+ /**
366
+ * The shape `findMany`/`findFirst` return for a given `select`. Untyped (`Doc`)
367
+ * collections and absent/empty selects return the full document; a typed select
368
+ * narrows to exactly the chosen fields.
369
+ */
370
+ type Projected<T, S> = IsLoose<T> extends true ? WithId<T> : [S] extends [undefined] ? WithId<T> : keyof S extends never ? WithId<T> : Pick<WithId<T>, Extract<SelectedKeys<S>, keyof WithId<T>>>;
371
+ /**
372
+ * A `$lookup`-style left join: for each result, fetch matching documents from
373
+ * another collection and attach them. With `unwind`, emit one row per match
374
+ * (`$unwind`); `unwind: "preserve"` also keeps rows that have no match.
375
+ */
376
+ interface LookupSpec {
377
+ /** The collection to join. */
378
+ from: string;
379
+ /** Field on this collection to match on. */
380
+ localField: string;
381
+ /** Field on the `from` collection to match against. */
382
+ foreignField: string;
383
+ /** Output field that receives the matched document(s). */
384
+ as: string;
385
+ /** Flatten the `as` array to a single object (one output row per match). */
386
+ unwind?: boolean | "preserve";
387
+ }
344
388
  interface FindManyArgs<T = Doc> {
345
389
  where?: WhereInput<T>;
346
390
  orderBy?: OrderBy<T>;
347
391
  select?: Select<T>;
348
392
  skip?: number;
349
393
  take?: number;
394
+ /** Join related documents from other collections ($lookup / $unwind). */
395
+ lookup?: LookupSpec | LookupSpec[];
350
396
  }
351
397
  interface FindFirstArgs<T = Doc> {
352
398
  where?: WhereInput<T>;
353
399
  orderBy?: OrderBy<T>;
354
400
  select?: Select<T>;
355
401
  skip?: number;
402
+ lookup?: LookupSpec | LookupSpec[];
356
403
  }
357
404
  interface CreateArgs<T = Doc> {
358
405
  data: Partial<T> & Record<string, any>;
@@ -788,4 +835,4 @@ declare function objectId(): string;
788
835
  /** True when a value looks like a monlite/ObjectId id (24 hex chars). */
789
836
  declare function isObjectId(value: unknown): value is string;
790
837
 
791
- 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 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 };
838
+ 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 DotPath, 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 Projected, 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
@@ -75,12 +75,22 @@ declare class Collection<T = Doc> {
75
75
  findManyCore(args?: FindManyArgs<T>): WithId<T>[];
76
76
  /** @internal Synchronous core of exists (used by reactivity). */
77
77
  existsCore(where: WhereInput<T> | undefined): boolean;
78
- findMany(args?: FindManyArgs<T>): Promise<WithId<T>[]>;
79
- findFirst(args?: FindFirstArgs<T>): Promise<WithId<T> | null>;
78
+ findMany<S extends Select<T> | undefined = undefined>(args?: Omit<FindManyArgs<T>, "select"> & {
79
+ select?: S;
80
+ }): Promise<Projected<T, S>[]>;
81
+ /** Resolve one `$lookup` spec against already-fetched rows (2 queries, no N+1). */
82
+ private applyLookup;
83
+ findFirst<S extends Select<T> | undefined = undefined>(args?: Omit<FindFirstArgs<T>, "select"> & {
84
+ select?: S;
85
+ }): Promise<Projected<T, S> | null>;
80
86
  /** Alias of {@link findFirst} for Prisma familiarity. */
81
- findUnique(args?: FindFirstArgs<T>): Promise<WithId<T> | null>;
87
+ findUnique<S extends Select<T> | undefined = undefined>(args?: Omit<FindFirstArgs<T>, "select"> & {
88
+ select?: S;
89
+ }): Promise<Projected<T, S> | null>;
82
90
  /** Like {@link findFirst} but throws if no document matches. */
83
- findFirstOrThrow(args?: FindFirstArgs<T>): Promise<WithId<T>>;
91
+ findFirstOrThrow<S extends Select<T> | undefined = undefined>(args?: Omit<FindFirstArgs<T>, "select"> & {
92
+ select?: S;
93
+ }): Promise<Projected<T, S>>;
84
94
  /** True if at least one document matches. */
85
95
  exists(where?: WhereInput<T>): Promise<boolean>;
86
96
  /**
@@ -299,9 +309,12 @@ interface FieldFilter<V = any> {
299
309
  }
300
310
  /** A value used directly as a filter is shorthand for `{ equals: value }`. */
301
311
  type FilterInput<V> = V | FieldFilter<V>;
312
+ /** A dot-notation nested path, e.g. `"address.city"`. */
313
+ type DotPath = `${string}.${string}`;
302
314
  /**
303
- * Where input. Known fields are typed from `T`; nested paths can also be
304
- * addressed with dot notation (e.g. `"address.city"`).
315
+ * Where input. For a **typed** collection, keys are checked against `T` (an
316
+ * unknown field is a type error); dot-notation nested paths are still allowed.
317
+ * For an **untyped** collection (`Doc`), any field is accepted (schema-free).
305
318
  */
306
319
  type WhereInput<T = Doc> = {
307
320
  [K in keyof T]?: FilterInput<T[K]>;
@@ -313,7 +326,7 @@ type WhereInput<T = Doc> = {
313
326
  OR?: WhereInput<T> | WhereInput<T>[];
314
327
  NOT?: WhereInput<T> | WhereInput<T>[];
315
328
  } & {
316
- [path: string]: any;
329
+ [path: DotPath]: FilterInput<any>;
317
330
  };
318
331
  /** Mongo-inspired update operators. */
319
332
  interface UpdateOperators {
@@ -330,29 +343,63 @@ interface UpdateOperators {
330
343
  type UpdateData<T = Doc> = (Partial<T> & Record<string, any>) | UpdateOperators;
331
344
  type SortOrder = "asc" | "desc";
332
345
  type OrderBy<T = Doc> = ({
333
- [K in keyof T]?: SortOrder;
346
+ [K in keyof WithId<T>]?: SortOrder;
334
347
  } & {
335
- [path: string]: SortOrder;
348
+ [path: DotPath]: SortOrder;
336
349
  }) | Array<{
337
- [path: string]: SortOrder;
350
+ [K in keyof WithId<T>]?: SortOrder;
351
+ } & {
352
+ [path: DotPath]: SortOrder;
338
353
  }>;
339
354
  type Select<T = Doc> = {
340
- [K in keyof T]?: boolean;
355
+ [K in keyof WithId<T>]?: boolean;
341
356
  } & {
342
- [path: string]: boolean;
357
+ [path: DotPath]: boolean;
358
+ };
359
+ /** True for the schema-free `Doc` (and `any`), so typed collections opt in only. */
360
+ type IsLoose<T> = string extends keyof T ? true : false;
361
+ /** Keys of a select set to a truthy value (a widened `boolean` counts as truthy). */
362
+ type SelectedKeys<S> = keyof {
363
+ [K in keyof S as S[K] extends false ? never : K]: K;
343
364
  };
365
+ /**
366
+ * The shape `findMany`/`findFirst` return for a given `select`. Untyped (`Doc`)
367
+ * collections and absent/empty selects return the full document; a typed select
368
+ * narrows to exactly the chosen fields.
369
+ */
370
+ type Projected<T, S> = IsLoose<T> extends true ? WithId<T> : [S] extends [undefined] ? WithId<T> : keyof S extends never ? WithId<T> : Pick<WithId<T>, Extract<SelectedKeys<S>, keyof WithId<T>>>;
371
+ /**
372
+ * A `$lookup`-style left join: for each result, fetch matching documents from
373
+ * another collection and attach them. With `unwind`, emit one row per match
374
+ * (`$unwind`); `unwind: "preserve"` also keeps rows that have no match.
375
+ */
376
+ interface LookupSpec {
377
+ /** The collection to join. */
378
+ from: string;
379
+ /** Field on this collection to match on. */
380
+ localField: string;
381
+ /** Field on the `from` collection to match against. */
382
+ foreignField: string;
383
+ /** Output field that receives the matched document(s). */
384
+ as: string;
385
+ /** Flatten the `as` array to a single object (one output row per match). */
386
+ unwind?: boolean | "preserve";
387
+ }
344
388
  interface FindManyArgs<T = Doc> {
345
389
  where?: WhereInput<T>;
346
390
  orderBy?: OrderBy<T>;
347
391
  select?: Select<T>;
348
392
  skip?: number;
349
393
  take?: number;
394
+ /** Join related documents from other collections ($lookup / $unwind). */
395
+ lookup?: LookupSpec | LookupSpec[];
350
396
  }
351
397
  interface FindFirstArgs<T = Doc> {
352
398
  where?: WhereInput<T>;
353
399
  orderBy?: OrderBy<T>;
354
400
  select?: Select<T>;
355
401
  skip?: number;
402
+ lookup?: LookupSpec | LookupSpec[];
356
403
  }
357
404
  interface CreateArgs<T = Doc> {
358
405
  data: Partial<T> & Record<string, any>;
@@ -788,4 +835,4 @@ declare function objectId(): string;
788
835
  /** True when a value looks like a monlite/ObjectId id (24 hex chars). */
789
836
  declare function isObjectId(value: unknown): value is string;
790
837
 
791
- 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 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 };
838
+ 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 DotPath, 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 Projected, 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,59 @@ 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
+ const a = args;
1154
+ if (!a.lookup) {
1155
+ return this.findManyCore(a);
1156
+ }
1157
+ const specs = Array.isArray(a.lookup) ? a.lookup : [a.lookup];
1158
+ let rows = this.findManyCore({
1159
+ ...a,
1160
+ select: void 0,
1161
+ lookup: void 0
1162
+ });
1163
+ for (const spec of specs) rows = await this.applyLookup(rows, spec);
1164
+ if (a.select) {
1165
+ rows = rows.map((r) => {
1166
+ const projected = project(r, a.select);
1167
+ for (const spec of specs) projected[spec.as] = r[spec.as];
1168
+ return projected;
1169
+ });
1170
+ }
1171
+ return rows;
1172
+ }
1173
+ /** Resolve one `$lookup` spec against already-fetched rows (2 queries, no N+1). */
1174
+ async applyLookup(rows, spec) {
1175
+ const localValues = [
1176
+ ...new Set(
1177
+ rows.map((r) => r[spec.localField]).filter((v) => v !== void 0 && v !== null)
1178
+ )
1179
+ ];
1180
+ const foreign = localValues.length ? await this.mon.collection(spec.from).findMany({
1181
+ where: { [spec.foreignField]: { in: localValues } }
1182
+ }) : [];
1183
+ const byKey = /* @__PURE__ */ new Map();
1184
+ for (const f of foreign) {
1185
+ const key = f[spec.foreignField];
1186
+ const list = byKey.get(key);
1187
+ if (list) list.push(f);
1188
+ else byKey.set(key, [f]);
1189
+ }
1190
+ if (spec.unwind) {
1191
+ const out = [];
1192
+ for (const r of rows) {
1193
+ const matches = byKey.get(r[spec.localField]) ?? [];
1194
+ if (matches.length === 0) {
1195
+ if (spec.unwind === "preserve") out.push({ ...r, [spec.as]: null });
1196
+ } else {
1197
+ for (const m of matches) out.push({ ...r, [spec.as]: m });
1198
+ }
1199
+ }
1200
+ return out;
1201
+ }
1202
+ return rows.map((r) => ({
1203
+ ...r,
1204
+ [spec.as]: byKey.get(r[spec.localField]) ?? []
1205
+ }));
1154
1206
  }
1155
1207
  async findFirst(args = {}) {
1156
1208
  const rows = await this.findMany({ ...args, take: 1 });