@monlite/core 0.3.0 → 0.6.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
@@ -16,6 +16,39 @@ interface SystemFields {
16
16
  }
17
17
  /** A stored document: the user's shape plus monlite's system fields. */
18
18
  type WithId<T> = T & SystemFields;
19
+ /** SQLite column affinity for a structured-collection field. */
20
+ type ColumnType = "TEXT" | "INTEGER" | "REAL" | "BLOB" | "JSON";
21
+ /** Rich column definition for a structured collection. */
22
+ interface ColumnDef {
23
+ type: ColumnType;
24
+ /** Create a secondary index on this column. */
25
+ index?: boolean;
26
+ unique?: boolean;
27
+ notNull?: boolean;
28
+ /** Default value (string/number literal, or null). */
29
+ default?: string | number | null;
30
+ /** Foreign-key target, e.g. `"users(_id)"` or `"users"`. */
31
+ references?: string;
32
+ }
33
+ /** Map of field name to column type (or full definition). */
34
+ type CollectionSchema = Record<string, ColumnType | ColumnDef>;
35
+ interface CollectionOptions {
36
+ /**
37
+ * Declare native SQL columns ("structured" mode). Listed fields become real
38
+ * typed columns — fast, indexable, joinable — and any other fields overflow
39
+ * into a JSON column. Omit for schema-free document mode. The CRUD/query API
40
+ * is identical either way.
41
+ */
42
+ schema?: CollectionSchema;
43
+ }
44
+ type CollectionMode = "document" | "structured";
45
+ /** A column as reported by {@link Monlite.$schema}. */
46
+ interface ColumnInfo {
47
+ name: string;
48
+ type: string;
49
+ notNull: boolean;
50
+ primaryKey: boolean;
51
+ }
19
52
  /** Per-field operators, Prisma-style (no `$` prefix). */
20
53
  interface FieldFilter<V = any> {
21
54
  equals?: V | null;
@@ -34,6 +67,11 @@ interface FieldFilter<V = any> {
34
67
  has?: any;
35
68
  /** Field presence. `true` requires the field to exist, `false` requires absence. */
36
69
  exists?: boolean;
70
+ /**
71
+ * Case sensitivity for `contains`/`startsWith`/`endsWith`. Default is
72
+ * case-sensitive; `"insensitive"` matches case-insensitively (ASCII).
73
+ */
74
+ mode?: "default" | "insensitive";
37
75
  }
38
76
  /** A value used directly as a filter is shorthand for `{ equals: value }`. */
39
77
  type FilterInput<V> = V | FieldFilter<V>;
@@ -168,6 +206,17 @@ interface MonliteOptions {
168
206
  * when installed, otherwise the built-in `node:sqlite` (Node >= 22.5).
169
207
  */
170
208
  driver?: DriverName;
209
+ /**
210
+ * Enable sync metadata (change feed, tombstones, version tracking) so the
211
+ * database can replicate via `@monlite/sync`. Off by default — adds zero
212
+ * overhead when disabled.
213
+ */
214
+ sync?: boolean;
215
+ /**
216
+ * Stable node identity used for last-write-wins tie-breaking. Auto-generated
217
+ * and persisted in the database on first sync-enabled open if omitted.
218
+ */
219
+ nodeId?: string;
171
220
  /** Auto-create indexes on frequently-queried JSON paths. Default `true`. */
172
221
  autoIndex?: boolean;
173
222
  /** Number of times a path must be queried before an index is created. Default `10`. */
@@ -176,35 +225,65 @@ interface MonliteOptions {
176
225
  readonly?: boolean;
177
226
  /** Use SQLite WAL journal mode for better concurrency. Default `true`. */
178
227
  wal?: boolean;
228
+ /** Milliseconds to wait on a locked database before erroring. Default `5000`. */
229
+ busyTimeout?: number;
179
230
  /** Verbose logger for executed SQL (debugging). */
180
231
  verbose?: (sql: string) => void;
181
232
  }
182
233
 
183
234
  /**
184
- * A document collection. Backed by a single SQLite table whose rows store the
185
- * document as JSON in a `data` column. Created lazily on first write/read.
235
+ * A collection. In **document** mode (default) every document is stored as JSON
236
+ * in a `data` column — schema-free. In **structured** mode (when a `schema` is
237
+ * given) the listed fields become real, typed SQL columns (fast, indexable,
238
+ * joinable) while any other fields overflow into a JSON `data` column. The CRUD
239
+ * and query API is identical in both modes.
186
240
  */
187
241
  declare class Collection<T = Doc> {
188
242
  private readonly mon;
189
243
  readonly name: string;
244
+ readonly mode: CollectionMode;
190
245
  private initialized;
246
+ private readonly columnDefs;
247
+ private readonly columnOrder;
248
+ /** Declared native columns (empty in document mode). */
249
+ private readonly columns;
250
+ private readonly jsonColumns;
251
+ private insertSqlCache?;
191
252
  private readonly trackPath;
192
- constructor(mon: Monlite, name: string);
253
+ constructor(mon: Monlite, name: string, options?: CollectionOptions);
193
254
  private get db();
255
+ /** Run a DB operation, normalizing driver errors into typed MonliteErrors. */
256
+ private guard;
257
+ /** Native column names declared for this collection (structured mode). */
258
+ get columnNames(): string[];
194
259
  private ensureTable;
195
260
  private rowToDoc;
196
- private prepareInsert;
261
+ private encodeColumn;
262
+ private insertColumns;
263
+ private insertSql;
264
+ /** Split an input document into a row aligned with `insertColumns()`. */
265
+ private buildInsert;
266
+ /** Build the `SET` clause + values to persist an updated document. */
267
+ private buildUpdateSet;
268
+ /** Sync store, but only for document collections (structured sync is future work). */
269
+ private get recorder();
197
270
  create(args: CreateArgs<T>): Promise<WithId<T>>;
198
271
  createMany(args: CreateManyArgs<T>): Promise<{
199
272
  count: number;
200
273
  }>;
201
274
  findMany(args?: FindManyArgs<T>): Promise<WithId<T>[]>;
202
275
  findFirst(args?: FindFirstArgs<T>): Promise<WithId<T> | null>;
276
+ /** Alias of {@link findFirst} for Prisma familiarity. */
277
+ findUnique(args?: FindFirstArgs<T>): Promise<WithId<T> | null>;
278
+ /** Like {@link findFirst} but throws if no document matches. */
279
+ findFirstOrThrow(args?: FindFirstArgs<T>): Promise<WithId<T>>;
280
+ /** True if at least one document matches. */
281
+ exists(where?: WhereInput<T>): Promise<boolean>;
203
282
  findById(id: string): Promise<WithId<T> | null>;
204
283
  count(args?: CountArgs<T>): Promise<number>;
205
284
  /**
206
- * Return the distinct values of a field across the collection. Array fields
207
- * are unwound (each element counts as a value), matching MongoDB's `distinct`.
285
+ * Return the distinct values of a field. Array fields stored in JSON are
286
+ * unwound (each element counts as a value), matching MongoDB's `distinct`.
208
287
  */
209
288
  distinct(field: string, where?: WhereInput<T>): Promise<any[]>;
210
289
  private runUpdate;
@@ -265,6 +344,127 @@ declare class AutoIndexer {
265
344
  reset(collection?: string): void;
266
345
  }
267
346
 
347
+ /**
348
+ * Versions are LWW (last-write-wins) tokens of the form
349
+ * `<zero-padded-ms>:<nodeId>` so that plain string comparison yields the
350
+ * correct ordering: newer wall-clock time wins, ties broken by node id.
351
+ *
352
+ * (The design reserves room to swap this for a hybrid logical clock later;
353
+ * the on-disk column is a plain string, so the format can evolve.)
354
+ */
355
+ type Version = string;
356
+ /**
357
+ * Build a version token. The optional `seq` is a per-node monotonic counter
358
+ * that makes versions unique even within the same millisecond — important so
359
+ * that cursor-based pulls (`> version`) never skip a same-timestamp change.
360
+ */
361
+ declare function makeVersion(ts: number, nodeId: string, seq?: number): Version;
362
+ declare function compareVersions(a: Version, b: Version): number;
363
+ declare function versionTs(v: Version): number;
364
+
365
+ type SyncOp = "upsert" | "delete";
366
+ /** A locally-originated change ready to be pushed to a remote. */
367
+ interface LocalChange {
368
+ seq: number;
369
+ collection: string;
370
+ _id: string;
371
+ op: SyncOp;
372
+ version: Version;
373
+ ts: number;
374
+ /** Full document (with system fields) for `upsert`; absent for `delete`. */
375
+ doc?: Record<string, any>;
376
+ }
377
+ /** A change received from a remote, to be applied locally. */
378
+ interface RemoteChange {
379
+ collection: string;
380
+ _id: string;
381
+ op: SyncOp;
382
+ version: Version;
383
+ doc?: Record<string, any>;
384
+ }
385
+ type ConflictResolver = (ctx: {
386
+ collection: string;
387
+ _id: string;
388
+ local: {
389
+ version: Version;
390
+ };
391
+ remote: {
392
+ version: Version;
393
+ doc?: Record<string, any>;
394
+ };
395
+ }) => "local" | "remote";
396
+ interface ApplyResult {
397
+ applied: boolean;
398
+ conflict: boolean;
399
+ winner: "local" | "remote" | "none";
400
+ }
401
+ interface SyncStateRow {
402
+ remote: string;
403
+ cursor: string | null;
404
+ lastPullAt: number | null;
405
+ lastPushSeq: number | null;
406
+ lastPushAt: number | null;
407
+ }
408
+ interface ConflictRow {
409
+ collection: string;
410
+ _id: string;
411
+ localVersion: Version;
412
+ remoteVersion: Version;
413
+ winner: "local" | "remote";
414
+ ts: number;
415
+ }
416
+ /**
417
+ * Low-level sync primitives stored alongside the data in the same `.db` file:
418
+ * an append-only change feed, tombstones, per-remote cursors and a conflict
419
+ * log. Created only when a database is opened with `{ sync: true }`. The
420
+ * `@monlite/sync` engine drives this; apps rarely touch it directly.
421
+ */
422
+ declare class SyncStore {
423
+ private readonly db;
424
+ readonly nodeId: string;
425
+ private versionSeq;
426
+ constructor(db: Driver, nodeId?: string);
427
+ private init;
428
+ private resolveNodeId;
429
+ /** True if this database tracks sync metadata (always, once constructed). */
430
+ get enabled(): boolean;
431
+ /** Append a locally-originated change to the feed. Call inside a write txn. */
432
+ recordLocal(collection: string, id: string, op: SyncOp, ts: number): Version;
433
+ /** Current (latest) version of a document, or null if never recorded. */
434
+ currentVersion(collection: string, id: string): Version | null;
435
+ /** Latest unpushed local change per document (the push queue). */
436
+ pending(collections?: string[], limit?: number): LocalChange[];
437
+ /** Mark the given changes (and any earlier local rows per doc) as pushed. */
438
+ markPushed(changes: LocalChange[]): void;
439
+ /**
440
+ * Apply a remote change, resolving conflicts against the local version.
441
+ * Remote-applied changes are recorded with `source='remote'` so they are
442
+ * never pushed back (echo prevention).
443
+ */
444
+ applyRemote(change: RemoteChange, resolver?: ConflictResolver): ApplyResult;
445
+ private applyData;
446
+ /**
447
+ * Latest change per document with `seq` greater than the given watermark,
448
+ * as RemoteChanges (used when this database acts as a sync *source*, e.g. the
449
+ * monlite-as-remote adapter). Returns the new watermark to resume from.
450
+ */
451
+ changesSince(seq: number, collections?: string[], limit?: number): {
452
+ changes: RemoteChange[];
453
+ maxSeq: number;
454
+ };
455
+ /**
456
+ * Enqueue existing documents (created before sync was enabled, or never
457
+ * recorded) as local upserts so they can be pushed. Idempotent.
458
+ */
459
+ seed(collections: string[]): number;
460
+ getState(remote: string): SyncStateRow;
461
+ setState(remote: string, patch: Partial<Omit<SyncStateRow, "remote">>): void;
462
+ private recordConflict;
463
+ conflicts(): ConflictRow[];
464
+ private readDoc;
465
+ private ensureCollTable;
466
+ }
467
+
268
468
  /**
269
469
  * A monlite database — a thin document layer over a single SQLite file.
270
470
  * Create one with {@link createDb}.
@@ -274,15 +474,25 @@ declare class Monlite {
274
474
  readonly driver: Driver;
275
475
  /** @internal */
276
476
  readonly autoIndexer: AutoIndexer;
477
+ /** @internal Sync metadata store; present only when `{ sync: true }`. */
478
+ readonly $sync?: SyncStore;
277
479
  private readonly collections;
278
480
  private closed;
279
481
  constructor(filename: string, options?: MonliteOptions);
482
+ /** Stable node id for LWW tie-breaking (only when sync is enabled). */
483
+ get nodeId(): string | undefined;
280
484
  /** The underlying native database handle (escape hatch). */
281
485
  get sqlite(): any;
282
486
  /** Name of the active backend: `"better-sqlite3"` or `"node:sqlite"`. */
283
487
  get driverName(): string;
284
- /** Get (or lazily create) a typed collection handle. */
285
- collection<T = Doc>(name: string): Collection<T>;
488
+ /**
489
+ * Get (or lazily create) a typed collection handle. Pass `{ schema }` to make
490
+ * it a structured collection backed by native SQL columns; omit for the
491
+ * default schema-free document mode. Options apply only on first access.
492
+ */
493
+ collection<T = Doc>(name: string, options?: CollectionOptions): Collection<T>;
494
+ /** Inspect a collection's physical columns (PRAGMA table_info). */
495
+ $schema(name: string): Promise<ColumnInfo[]>;
286
496
  /** Tagged-template SQL query returning rows. Values are safely parameterized. */
287
497
  $queryRaw<R = any>(strings: TemplateStringsArray, ...values: any[]): Promise<R[]>;
288
498
  /** Like {@link $queryRaw} but takes a raw SQL string and positional params. */
@@ -314,15 +524,54 @@ declare function createDb(filename: string, options?: MonliteOptions): Monlite;
314
524
 
315
525
  /** Base error for all monlite-originated failures. */
316
526
  declare class MonliteError extends Error {
317
- constructor(message: string);
527
+ constructor(message: string, options?: {
528
+ cause?: unknown;
529
+ });
318
530
  }
319
531
  /** Thrown when a query/update payload is malformed. */
320
532
  declare class MonliteQueryError extends MonliteError {
321
- constructor(message: string);
533
+ constructor(message: string, options?: {
534
+ cause?: unknown;
535
+ });
536
+ }
537
+ /** A database constraint was violated (base class for the specific kinds). */
538
+ declare class MonliteConstraintError extends MonliteError {
539
+ readonly collection?: string;
540
+ constructor(message: string, options?: {
541
+ cause?: unknown;
542
+ collection?: string;
543
+ });
322
544
  }
545
+ /** A UNIQUE (or primary-key) constraint was violated. */
546
+ declare class MonliteUniqueConstraintError extends MonliteConstraintError {
547
+ constructor(message: string, options?: {
548
+ cause?: unknown;
549
+ collection?: string;
550
+ });
551
+ }
552
+ /** A NOT NULL constraint was violated. */
553
+ declare class MonliteNotNullError extends MonliteConstraintError {
554
+ constructor(message: string, options?: {
555
+ cause?: unknown;
556
+ collection?: string;
557
+ });
558
+ }
559
+ /** A FOREIGN KEY constraint was violated. */
560
+ declare class MonliteForeignKeyError extends MonliteConstraintError {
561
+ constructor(message: string, options?: {
562
+ cause?: unknown;
563
+ collection?: string;
564
+ });
565
+ }
566
+ /**
567
+ * Normalize a raw driver error (better-sqlite3 `SqliteError` or node:sqlite
568
+ * error) into a typed {@link MonliteError}. The two backends differ in error
569
+ * shape, so we sniff both the `code` and the message text.
570
+ */
571
+ declare function normalizeDriverError(err: unknown, collection?: string): MonliteError;
323
572
 
324
573
  declare function objectId(): string;
325
574
  /** True when a value looks like a monlite/ObjectId id (24 hex chars). */
326
575
  declare function isObjectId(value: unknown): value is string;
327
576
 
328
- export { type AggregateArgs, type AggregateResult, Collection, type CountArgs, type CreateArgs, type CreateManyArgs, Monlite as Db, type DeleteArgs, type Doc, type Driver, type DriverName, type FieldFilter, type FieldSelection, type FilterInput, type FindFirstArgs, type FindManyArgs, type GroupByArgs, type GroupByResult, type HavingComparison, type HavingInput, Monlite, MonliteError, type MonliteOptions, MonliteQueryError, type OrderBy, type PreparedStatement, type Select, type SortOrder, type SystemFields, type UpdateArgs, type UpdateData, type UpdateOperators, type UpsertArgs, type WhereInput as WhereClause, type WhereInput, type WithId, createDb, isObjectId, objectId };
577
+ 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 FieldFilter, type FieldSelection, type FilterInput, type FindFirstArgs, type FindManyArgs, type GroupByArgs, type GroupByResult, type HavingComparison, type HavingInput, type LocalChange, Monlite, MonliteConstraintError, MonliteError, MonliteForeignKeyError, MonliteNotNullError, type MonliteOptions, MonliteQueryError, MonliteUniqueConstraintError, type OrderBy, 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 WhereInput as WhereClause, type WhereInput, type WithId, compareVersions, createDb, isObjectId, makeVersion, normalizeDriverError, objectId, versionTs };