@lunora/server 1.0.0-alpha.18 → 1.0.0-alpha.19

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.
@@ -345,12 +345,24 @@ interface TableWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends
345
345
  * {@link TableWriterFacade.hardDelete} to force physical removal.
346
346
  */
347
347
  delete: (id: Id<string & T>) => Promise<void>;
348
- /** Delete many rows in this table by id in one call; returns the *requested* id count (unknown ids are no-ops). Atomic within a mutation (a throw rolls the mutation back); an action has no transaction span. */
349
- deleteMany: (ids: ReadonlyArray<Id<string & T>>, options?: {
350
- limit?: number;
351
- }) => Promise<{
352
- deleted: number;
353
- }>;
348
+ /**
349
+ * Delete many rows in this table. Pass an array of ids (requested count is
350
+ * returned; unknown ids are no-ops) or `{ where }` to delete matching rows
351
+ * (actual removed count is returned). Atomic within a mutation.
352
+ */
353
+ deleteMany: {
354
+ (ids: ReadonlyArray<Id<string & T>>, options?: {
355
+ limit?: number;
356
+ }): Promise<{
357
+ deleted: number;
358
+ }>;
359
+ (args: {
360
+ limit?: number;
361
+ where: Partial<DM[T]>;
362
+ }): Promise<{
363
+ deleted: number;
364
+ }>;
365
+ };
354
366
  /** Physically remove a row (and physically cascade `onDelete`), bypassing `.softDelete()`. Same as `delete()` on a non-soft table. */
355
367
  hardDelete: (id: Id<string & T>) => Promise<void>;
356
368
  /**
@@ -366,18 +378,44 @@ interface TableWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends
366
378
  skipDuplicates?: boolean;
367
379
  }): Promise<Id<string & T>>;
368
380
  };
369
- /** Insert many documents into this table in one call, returning the minted ids in input order. Atomic within a mutation (a throw rolls the mutation back); an action has no transaction span. */
370
- insertMany: (values: ReadonlyArray<IM[T]>, options?: {
371
- limit?: number;
372
- }) => Promise<Id<string & T>[]>;
381
+ /**
382
+ * Insert many documents into this table in one call, returning the minted ids
383
+ * in input order. With `{ skipDuplicates: true }`, UNIQUE breaches resolve to
384
+ * `null` for that row instead of failing the batch. Atomic within a mutation.
385
+ */
386
+ insertMany: {
387
+ (values: ReadonlyArray<IM[T]>, options: {
388
+ limit?: number;
389
+ skipDuplicates: true;
390
+ }): Promise<(Id<string & T> | null)[]>;
391
+ (values: ReadonlyArray<IM[T]>, options?: {
392
+ limit?: number;
393
+ skipDuplicates?: boolean;
394
+ }): Promise<Id<string & T>[]>;
395
+ };
373
396
  patch: (id: Id<string & T>, values: Partial<IM[T]>) => Promise<void>;
374
- /** Patch many rows in this table by id in one call. Atomic within a mutation (a throw rolls the mutation back); an action has no transaction span. */
375
- patchMany: (patches: ReadonlyArray<{
376
- id: Id<string & T>;
377
- values: Partial<IM[T]>;
378
- }>, options?: {
379
- limit?: number;
380
- }) => Promise<void>;
397
+ /**
398
+ * Patch many rows in this table. Pass an array of `{ id, values }` or
399
+ * `{ where, values }` to patch matching rows with the same values. Returns
400
+ * the actual patched count. Atomic within a mutation.
401
+ */
402
+ patchMany: {
403
+ (patches: ReadonlyArray<{
404
+ id: Id<string & T>;
405
+ values: Partial<IM[T]>;
406
+ }>, options?: {
407
+ limit?: number;
408
+ }): Promise<{
409
+ patched: number;
410
+ }>;
411
+ (args: {
412
+ limit?: number;
413
+ values: Partial<IM[T]>;
414
+ where: Partial<DM[T]>;
415
+ }): Promise<{
416
+ patched: number;
417
+ }>;
418
+ };
381
419
  replace: (id: Id<string & T>, values: IM[T]) => Promise<void>;
382
420
  /** Un-soft-delete a row by id: clears the `.softDelete()` marker so list reads see it again. Throws on a non-soft table. */
383
421
  restore: (id: Id<string & T>) => Promise<void>;
@@ -345,12 +345,24 @@ interface TableWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends
345
345
  * {@link TableWriterFacade.hardDelete} to force physical removal.
346
346
  */
347
347
  delete: (id: Id<string & T>) => Promise<void>;
348
- /** Delete many rows in this table by id in one call; returns the *requested* id count (unknown ids are no-ops). Atomic within a mutation (a throw rolls the mutation back); an action has no transaction span. */
349
- deleteMany: (ids: ReadonlyArray<Id<string & T>>, options?: {
350
- limit?: number;
351
- }) => Promise<{
352
- deleted: number;
353
- }>;
348
+ /**
349
+ * Delete many rows in this table. Pass an array of ids (requested count is
350
+ * returned; unknown ids are no-ops) or `{ where }` to delete matching rows
351
+ * (actual removed count is returned). Atomic within a mutation.
352
+ */
353
+ deleteMany: {
354
+ (ids: ReadonlyArray<Id<string & T>>, options?: {
355
+ limit?: number;
356
+ }): Promise<{
357
+ deleted: number;
358
+ }>;
359
+ (args: {
360
+ limit?: number;
361
+ where: Partial<DM[T]>;
362
+ }): Promise<{
363
+ deleted: number;
364
+ }>;
365
+ };
354
366
  /** Physically remove a row (and physically cascade `onDelete`), bypassing `.softDelete()`. Same as `delete()` on a non-soft table. */
355
367
  hardDelete: (id: Id<string & T>) => Promise<void>;
356
368
  /**
@@ -366,18 +378,44 @@ interface TableWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends
366
378
  skipDuplicates?: boolean;
367
379
  }): Promise<Id<string & T>>;
368
380
  };
369
- /** Insert many documents into this table in one call, returning the minted ids in input order. Atomic within a mutation (a throw rolls the mutation back); an action has no transaction span. */
370
- insertMany: (values: ReadonlyArray<IM[T]>, options?: {
371
- limit?: number;
372
- }) => Promise<Id<string & T>[]>;
381
+ /**
382
+ * Insert many documents into this table in one call, returning the minted ids
383
+ * in input order. With `{ skipDuplicates: true }`, UNIQUE breaches resolve to
384
+ * `null` for that row instead of failing the batch. Atomic within a mutation.
385
+ */
386
+ insertMany: {
387
+ (values: ReadonlyArray<IM[T]>, options: {
388
+ limit?: number;
389
+ skipDuplicates: true;
390
+ }): Promise<(Id<string & T> | null)[]>;
391
+ (values: ReadonlyArray<IM[T]>, options?: {
392
+ limit?: number;
393
+ skipDuplicates?: boolean;
394
+ }): Promise<Id<string & T>[]>;
395
+ };
373
396
  patch: (id: Id<string & T>, values: Partial<IM[T]>) => Promise<void>;
374
- /** Patch many rows in this table by id in one call. Atomic within a mutation (a throw rolls the mutation back); an action has no transaction span. */
375
- patchMany: (patches: ReadonlyArray<{
376
- id: Id<string & T>;
377
- values: Partial<IM[T]>;
378
- }>, options?: {
379
- limit?: number;
380
- }) => Promise<void>;
397
+ /**
398
+ * Patch many rows in this table. Pass an array of `{ id, values }` or
399
+ * `{ where, values }` to patch matching rows with the same values. Returns
400
+ * the actual patched count. Atomic within a mutation.
401
+ */
402
+ patchMany: {
403
+ (patches: ReadonlyArray<{
404
+ id: Id<string & T>;
405
+ values: Partial<IM[T]>;
406
+ }>, options?: {
407
+ limit?: number;
408
+ }): Promise<{
409
+ patched: number;
410
+ }>;
411
+ (args: {
412
+ limit?: number;
413
+ values: Partial<IM[T]>;
414
+ where: Partial<DM[T]>;
415
+ }): Promise<{
416
+ patched: number;
417
+ }>;
418
+ };
381
419
  replace: (id: Id<string & T>, values: IM[T]) => Promise<void>;
382
420
  /** Un-soft-delete a row by id: clears the `.softDelete()` marker so list reads see it again. Throws on a non-soft table. */
383
421
  restore: (id: Id<string & T>) => Promise<void>;
package/dist/index.d.mts CHANGED
@@ -299,6 +299,11 @@ interface FacadeWriterLike {
299
299
  }, expectedTable?: string): Promise<{
300
300
  deleted: number;
301
301
  }>;
302
+ deleteWhere?(tableName: string, where: Record<string, unknown>, options?: {
303
+ limit?: number;
304
+ }): Promise<{
305
+ deleted: number;
306
+ }>;
302
307
  findFirst(tableName: string, args?: unknown): Promise<unknown>;
303
308
  findFirstOrThrow(tableName: string, args?: unknown): Promise<unknown>;
304
309
  findMany(tableName: string, args?: unknown): Promise<unknown>;
@@ -307,14 +312,25 @@ interface FacadeWriterLike {
307
312
  insert(tableName: string, document: Record<string, unknown>): Promise<string>;
308
313
  insertMany?(tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
309
314
  limit?: number;
310
- }): Promise<string[]>;
315
+ skipDuplicates?: boolean;
316
+ }): Promise<(string | null)[]>;
311
317
  patch(id: string, patch: Record<string, unknown>, expectedTable?: string): Promise<void>;
312
318
  patchMany?(patches: ReadonlyArray<{
313
319
  id: string;
314
320
  patch: Record<string, unknown>;
315
321
  }>, options?: {
316
322
  limit?: number;
317
- }, expectedTable?: string): Promise<void>;
323
+ }, expectedTable?: string): Promise<{
324
+ patched: number;
325
+ }>;
326
+ patchWhere?(tableName: string, args: {
327
+ patch: Record<string, unknown>;
328
+ where: Record<string, unknown>;
329
+ }, options?: {
330
+ limit?: number;
331
+ }): Promise<{
332
+ patched: number;
333
+ }>;
318
334
  query(tableName: string): {
319
335
  withSearchIndex(indexName: string, search: (q: unknown) => unknown): unknown;
320
336
  };
@@ -328,11 +344,19 @@ interface FacadeEntry {
328
344
  aggregate: (options: unknown) => Promise<unknown>;
329
345
  count: (where?: unknown) => Promise<number>;
330
346
  delete: (id: string) => Promise<void>;
331
- deleteMany: (ids: ReadonlyArray<string>, options?: {
332
- limit?: number;
333
- }) => Promise<{
334
- deleted: number;
335
- }>;
347
+ deleteMany: {
348
+ (ids: ReadonlyArray<string>, options?: {
349
+ limit?: number;
350
+ }): Promise<{
351
+ deleted: number;
352
+ }>;
353
+ (args: {
354
+ limit?: number;
355
+ where: Record<string, unknown>;
356
+ }): Promise<{
357
+ deleted: number;
358
+ }>;
359
+ };
336
360
  /** `true` when at least one row matches `where` (or any row exists when omitted). Honors RLS like `findFirst`. */
337
361
  exists: (where?: unknown) => Promise<boolean>;
338
362
  findFirst: (args?: unknown) => Promise<unknown>;
@@ -343,16 +367,34 @@ interface FacadeEntry {
343
367
  /** Physically remove a row (and physically cascade), bypassing `.softDelete()`. */
344
368
  hardDelete: (id: string) => Promise<void>;
345
369
  insert: (document: Record<string, unknown>, options?: FacadeInsertOptions) => Promise<null | string>;
370
+ /**
371
+ * Insert many documents into this table in one call. With
372
+ * `{ skipDuplicates: true }`, UNIQUE breaches resolve to `null` for that row
373
+ * instead of failing the batch. The typed facade narrows the return to
374
+ * `Id&lt;T>[]` when skipDuplicates is not requested.
375
+ */
346
376
  insertMany: (documents: ReadonlyArray<Record<string, unknown>>, options?: {
347
377
  limit?: number;
348
- }) => Promise<string[]>;
378
+ skipDuplicates?: boolean;
379
+ }) => Promise<(string | null)[]>;
349
380
  patch: (id: string, patch: Record<string, unknown>) => Promise<void>;
350
- patchMany: (patches: ReadonlyArray<{
351
- id: string;
352
- values: Record<string, unknown>;
353
- }>, options?: {
354
- limit?: number;
355
- }) => Promise<void>;
381
+ patchMany: {
382
+ (patches: ReadonlyArray<{
383
+ id: string;
384
+ values: Record<string, unknown>;
385
+ }>, options?: {
386
+ limit?: number;
387
+ }): Promise<{
388
+ patched: number;
389
+ }>;
390
+ (args: {
391
+ limit?: number;
392
+ values: Record<string, unknown>;
393
+ where: Record<string, unknown>;
394
+ }): Promise<{
395
+ patched: number;
396
+ }>;
397
+ };
356
398
  rank: (indexName: string, options: unknown) => Promise<unknown>;
357
399
  rankPage: (indexName: string, options?: unknown) => Promise<unknown>;
358
400
  replace: (id: string, document: Record<string, unknown>) => Promise<void>;
@@ -843,6 +885,11 @@ interface MaskDatabase {
843
885
  }) => Promise<{
844
886
  deleted: number;
845
887
  }>;
888
+ deleteWhere?: (tableName: string, where: Record<string, unknown>, options?: {
889
+ limit?: number;
890
+ }) => Promise<{
891
+ deleted: number;
892
+ }>;
846
893
  findFirst: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown> | null>;
847
894
  findFirstOrThrow: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown>>;
848
895
  findMany: (tableName: string, args?: QueryArgs$1) => Promise<QueryPage$1>;
@@ -854,7 +901,8 @@ interface MaskDatabase {
854
901
  insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
855
902
  insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
856
903
  limit?: number;
857
- }) => Promise<string[]>;
904
+ skipDuplicates?: boolean;
905
+ }) => Promise<(string | null)[]>;
858
906
  lookupById?: (id: string, expectedTable?: string) => Promise<null | {
859
907
  row: Record<string, unknown>;
860
908
  tableName: string;
@@ -865,7 +913,17 @@ interface MaskDatabase {
865
913
  patch: Record<string, unknown>;
866
914
  }>, options?: {
867
915
  limit?: number;
868
- }) => Promise<void>;
916
+ }) => Promise<{
917
+ patched: number;
918
+ }>;
919
+ patchWhere?: (tableName: string, args: {
920
+ patch: Record<string, unknown>;
921
+ where: Record<string, unknown>;
922
+ }, options?: {
923
+ limit?: number;
924
+ }) => Promise<{
925
+ patched: number;
926
+ }>;
869
927
  query: (tableName: string) => TableReaderLike$1;
870
928
  rank: (tableName: string, indexName: string, options: unknown) => Promise<null | {
871
929
  position: number;
@@ -1737,6 +1795,11 @@ interface DatabaseWriterLike {
1737
1795
  }, expectedTable?: string) => Promise<{
1738
1796
  deleted: number;
1739
1797
  }>;
1798
+ deleteWhere?: (tableName: string, where: WhereInput, options?: {
1799
+ limit?: number;
1800
+ }) => Promise<{
1801
+ deleted: number;
1802
+ }>;
1740
1803
  findFirst: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown> | null>;
1741
1804
  findFirstOrThrow: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown>>;
1742
1805
  findMany: (tableName: string, args?: QueryArgs) => Promise<QueryPage>;
@@ -1753,7 +1816,8 @@ interface DatabaseWriterLike {
1753
1816
  insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
1754
1817
  insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
1755
1818
  limit?: number;
1756
- }) => Promise<string[]>;
1819
+ skipDuplicates?: boolean;
1820
+ }) => Promise<(string | null)[]>;
1757
1821
  insertManyUnsafe: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
1758
1822
  allowExplicitId?: boolean;
1759
1823
  limit?: number;
@@ -1776,7 +1840,17 @@ interface DatabaseWriterLike {
1776
1840
  patch: Record<string, unknown>;
1777
1841
  }>, options?: {
1778
1842
  limit?: number;
1779
- }, expectedTable?: string) => Promise<void>;
1843
+ }, expectedTable?: string) => Promise<{
1844
+ patched: number;
1845
+ }>;
1846
+ patchWhere?: (tableName: string, args: {
1847
+ patch: Record<string, unknown>;
1848
+ where: WhereInput;
1849
+ }, options?: {
1850
+ limit?: number;
1851
+ }) => Promise<{
1852
+ patched: number;
1853
+ }>;
1780
1854
  query: (tableName: string) => TableReaderLike;
1781
1855
  /**
1782
1856
  * Rank a row within its partition. A position is a count-of-rows-before, so
package/dist/index.d.ts CHANGED
@@ -299,6 +299,11 @@ interface FacadeWriterLike {
299
299
  }, expectedTable?: string): Promise<{
300
300
  deleted: number;
301
301
  }>;
302
+ deleteWhere?(tableName: string, where: Record<string, unknown>, options?: {
303
+ limit?: number;
304
+ }): Promise<{
305
+ deleted: number;
306
+ }>;
302
307
  findFirst(tableName: string, args?: unknown): Promise<unknown>;
303
308
  findFirstOrThrow(tableName: string, args?: unknown): Promise<unknown>;
304
309
  findMany(tableName: string, args?: unknown): Promise<unknown>;
@@ -307,14 +312,25 @@ interface FacadeWriterLike {
307
312
  insert(tableName: string, document: Record<string, unknown>): Promise<string>;
308
313
  insertMany?(tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
309
314
  limit?: number;
310
- }): Promise<string[]>;
315
+ skipDuplicates?: boolean;
316
+ }): Promise<(string | null)[]>;
311
317
  patch(id: string, patch: Record<string, unknown>, expectedTable?: string): Promise<void>;
312
318
  patchMany?(patches: ReadonlyArray<{
313
319
  id: string;
314
320
  patch: Record<string, unknown>;
315
321
  }>, options?: {
316
322
  limit?: number;
317
- }, expectedTable?: string): Promise<void>;
323
+ }, expectedTable?: string): Promise<{
324
+ patched: number;
325
+ }>;
326
+ patchWhere?(tableName: string, args: {
327
+ patch: Record<string, unknown>;
328
+ where: Record<string, unknown>;
329
+ }, options?: {
330
+ limit?: number;
331
+ }): Promise<{
332
+ patched: number;
333
+ }>;
318
334
  query(tableName: string): {
319
335
  withSearchIndex(indexName: string, search: (q: unknown) => unknown): unknown;
320
336
  };
@@ -328,11 +344,19 @@ interface FacadeEntry {
328
344
  aggregate: (options: unknown) => Promise<unknown>;
329
345
  count: (where?: unknown) => Promise<number>;
330
346
  delete: (id: string) => Promise<void>;
331
- deleteMany: (ids: ReadonlyArray<string>, options?: {
332
- limit?: number;
333
- }) => Promise<{
334
- deleted: number;
335
- }>;
347
+ deleteMany: {
348
+ (ids: ReadonlyArray<string>, options?: {
349
+ limit?: number;
350
+ }): Promise<{
351
+ deleted: number;
352
+ }>;
353
+ (args: {
354
+ limit?: number;
355
+ where: Record<string, unknown>;
356
+ }): Promise<{
357
+ deleted: number;
358
+ }>;
359
+ };
336
360
  /** `true` when at least one row matches `where` (or any row exists when omitted). Honors RLS like `findFirst`. */
337
361
  exists: (where?: unknown) => Promise<boolean>;
338
362
  findFirst: (args?: unknown) => Promise<unknown>;
@@ -343,16 +367,34 @@ interface FacadeEntry {
343
367
  /** Physically remove a row (and physically cascade), bypassing `.softDelete()`. */
344
368
  hardDelete: (id: string) => Promise<void>;
345
369
  insert: (document: Record<string, unknown>, options?: FacadeInsertOptions) => Promise<null | string>;
370
+ /**
371
+ * Insert many documents into this table in one call. With
372
+ * `{ skipDuplicates: true }`, UNIQUE breaches resolve to `null` for that row
373
+ * instead of failing the batch. The typed facade narrows the return to
374
+ * `Id&lt;T>[]` when skipDuplicates is not requested.
375
+ */
346
376
  insertMany: (documents: ReadonlyArray<Record<string, unknown>>, options?: {
347
377
  limit?: number;
348
- }) => Promise<string[]>;
378
+ skipDuplicates?: boolean;
379
+ }) => Promise<(string | null)[]>;
349
380
  patch: (id: string, patch: Record<string, unknown>) => Promise<void>;
350
- patchMany: (patches: ReadonlyArray<{
351
- id: string;
352
- values: Record<string, unknown>;
353
- }>, options?: {
354
- limit?: number;
355
- }) => Promise<void>;
381
+ patchMany: {
382
+ (patches: ReadonlyArray<{
383
+ id: string;
384
+ values: Record<string, unknown>;
385
+ }>, options?: {
386
+ limit?: number;
387
+ }): Promise<{
388
+ patched: number;
389
+ }>;
390
+ (args: {
391
+ limit?: number;
392
+ values: Record<string, unknown>;
393
+ where: Record<string, unknown>;
394
+ }): Promise<{
395
+ patched: number;
396
+ }>;
397
+ };
356
398
  rank: (indexName: string, options: unknown) => Promise<unknown>;
357
399
  rankPage: (indexName: string, options?: unknown) => Promise<unknown>;
358
400
  replace: (id: string, document: Record<string, unknown>) => Promise<void>;
@@ -843,6 +885,11 @@ interface MaskDatabase {
843
885
  }) => Promise<{
844
886
  deleted: number;
845
887
  }>;
888
+ deleteWhere?: (tableName: string, where: Record<string, unknown>, options?: {
889
+ limit?: number;
890
+ }) => Promise<{
891
+ deleted: number;
892
+ }>;
846
893
  findFirst: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown> | null>;
847
894
  findFirstOrThrow: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown>>;
848
895
  findMany: (tableName: string, args?: QueryArgs$1) => Promise<QueryPage$1>;
@@ -854,7 +901,8 @@ interface MaskDatabase {
854
901
  insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
855
902
  insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
856
903
  limit?: number;
857
- }) => Promise<string[]>;
904
+ skipDuplicates?: boolean;
905
+ }) => Promise<(string | null)[]>;
858
906
  lookupById?: (id: string, expectedTable?: string) => Promise<null | {
859
907
  row: Record<string, unknown>;
860
908
  tableName: string;
@@ -865,7 +913,17 @@ interface MaskDatabase {
865
913
  patch: Record<string, unknown>;
866
914
  }>, options?: {
867
915
  limit?: number;
868
- }) => Promise<void>;
916
+ }) => Promise<{
917
+ patched: number;
918
+ }>;
919
+ patchWhere?: (tableName: string, args: {
920
+ patch: Record<string, unknown>;
921
+ where: Record<string, unknown>;
922
+ }, options?: {
923
+ limit?: number;
924
+ }) => Promise<{
925
+ patched: number;
926
+ }>;
869
927
  query: (tableName: string) => TableReaderLike$1;
870
928
  rank: (tableName: string, indexName: string, options: unknown) => Promise<null | {
871
929
  position: number;
@@ -1737,6 +1795,11 @@ interface DatabaseWriterLike {
1737
1795
  }, expectedTable?: string) => Promise<{
1738
1796
  deleted: number;
1739
1797
  }>;
1798
+ deleteWhere?: (tableName: string, where: WhereInput, options?: {
1799
+ limit?: number;
1800
+ }) => Promise<{
1801
+ deleted: number;
1802
+ }>;
1740
1803
  findFirst: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown> | null>;
1741
1804
  findFirstOrThrow: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown>>;
1742
1805
  findMany: (tableName: string, args?: QueryArgs) => Promise<QueryPage>;
@@ -1753,7 +1816,8 @@ interface DatabaseWriterLike {
1753
1816
  insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
1754
1817
  insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
1755
1818
  limit?: number;
1756
- }) => Promise<string[]>;
1819
+ skipDuplicates?: boolean;
1820
+ }) => Promise<(string | null)[]>;
1757
1821
  insertManyUnsafe: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
1758
1822
  allowExplicitId?: boolean;
1759
1823
  limit?: number;
@@ -1776,7 +1840,17 @@ interface DatabaseWriterLike {
1776
1840
  patch: Record<string, unknown>;
1777
1841
  }>, options?: {
1778
1842
  limit?: number;
1779
- }, expectedTable?: string) => Promise<void>;
1843
+ }, expectedTable?: string) => Promise<{
1844
+ patched: number;
1845
+ }>;
1846
+ patchWhere?: (tableName: string, args: {
1847
+ patch: Record<string, unknown>;
1848
+ where: WhereInput;
1849
+ }, options?: {
1850
+ limit?: number;
1851
+ }) => Promise<{
1852
+ patched: number;
1853
+ }>;
1780
1854
  query: (tableName: string) => TableReaderLike;
1781
1855
  /**
1782
1856
  * Rank a row within its partition. A position is a count-of-rows-before, so
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ export { initLunora } from './packem_shared/initLunora-sQUqaejx.mjs';
3
3
  export { createSecrets } from './packem_shared/createSecrets-DwaR2rNG.mjs';
4
4
  export { LunoraEnvError, defineEnv, redactSecrets } from './packem_shared/LunoraEnvError-BGmd1Qs0.mjs';
5
5
  export { LunoraError } from './packem_shared/LunoraError-WbxmrpxR.mjs';
6
- export { bindOrm, bindTableFacade } from './packem_shared/bindOrm-CNXKgfUa.mjs';
6
+ export { bindOrm, bindTableFacade } from './packem_shared/bindOrm-Bfgl7f-Q.mjs';
7
7
  export { httpAction, httpRoute, httpRouter, isSafeHeaderValue, serveStorageObject } from './packem_shared/httpAction-DCXoYPIk.mjs';
8
8
  export { defineIdentity } from './packem_shared/defineIdentity-DiX4zM9x.mjs';
9
9
  export { onConnect, onDisconnect } from './packem_shared/onConnect-CIPXKPyw.mjs';
@@ -17,11 +17,11 @@ export { defineShape } from './packem_shared/defineShape-C5scNOrf.mjs';
17
17
  export { anyApi } from './types.mjs';
18
18
  export { cronJobs } from '@lunora/scheduler';
19
19
  export { ValidationError, v } from '@lunora/values';
20
- export { buildRlsReadRegistry, composeShapeReadWhere } from './packem_shared/buildRlsReadRegistry-DFBFLydB.mjs';
20
+ export { buildRlsReadRegistry, composeShapeReadWhere } from './packem_shared/buildRlsReadRegistry-D54vUQe4.mjs';
21
21
  export { createPolicyDsl, definePermission, definePolicies, definePolicy, defineRole } from './packem_shared/createPolicyDsl-By3QB4he.mjs';
22
22
  export { defineStorageRule, defineStorageRules } from './packem_shared/defineStorageRule-B5nL4Z1P.mjs';
23
- export { mask } from './packem_shared/mask-Dc8G5Gl7.mjs';
24
- export { rls } from './packem_shared/rls-3sCJIH6F.mjs';
23
+ export { mask } from './packem_shared/mask-BT1YgRbF.mjs';
24
+ export { rls } from './packem_shared/rls-B_ZWgslr.mjs';
25
25
  export { storageRules } from './packem_shared/storageRules-6QxzDOcx.mjs';
26
26
 
27
27
  const VERSION = "0.0.0";
@@ -44,15 +44,24 @@ const bindTableFacade = (writer, tableName) => {
44
44
  count: (where) => writer.count(tableName, where),
45
45
  delete: (id) => writer.delete(id, tableName),
46
46
  // `deleteMany`/`patchMany` forward the bound `tableName` as `expectedTable`
47
- // (threaded through the writer + the RLS middleware's per-id gate) so every
48
- // batched id is scoped to this table — the same IDOR guard the single-row
49
- // `delete`/`patch` apply. `patchMany` maps the facade's `values` payload to
50
- // the writer's `{ id, patch }` shape.
51
- deleteMany: (ids, options) => {
52
- if (writer.deleteMany === void 0) {
53
- throw new LunoraError$1("INTERNAL", `ctx.db.${tableName}.deleteMany is unavailable: this writer has no batch delete`);
47
+ // for id-based calls (threaded through the writer + the RLS middleware's
48
+ // per-id gate) so every batched id is scoped to this table — the same IDOR
49
+ // guard the single-row `delete`/`patch` apply. The where-based form routes
50
+ // through the structural writer's `deleteMany(tableName, { where })`.
51
+ // `patchMany` maps the facade's `values` payload to the writer's
52
+ // `{ id, patch }` shape.
53
+ deleteMany: (first, options) => {
54
+ if (Array.isArray(first)) {
55
+ if (writer.deleteMany === void 0) {
56
+ throw new LunoraError$1("INTERNAL", `ctx.db.${tableName}.deleteMany is unavailable: this writer has no batch delete`);
57
+ }
58
+ return writer.deleteMany(first, options, tableName);
54
59
  }
55
- return writer.deleteMany(ids, options, tableName);
60
+ if (writer.deleteWhere === void 0) {
61
+ throw new LunoraError$1("INTERNAL", `ctx.db.${tableName}.deleteMany({ where }) is unavailable: this writer has no where-based delete`);
62
+ }
63
+ const whereArgs = first;
64
+ return writer.deleteWhere(tableName, whereArgs.where, { limit: whereArgs.limit });
56
65
  },
57
66
  // `exists` reuses `findFirst` (RLS-filtered, indexed when a `.withIndex`-able
58
67
  // `where` is supplied) and only asks whether a row came back — no count scan.
@@ -72,17 +81,24 @@ const bindTableFacade = (writer, tableName) => {
72
81
  return writer.insertMany(tableName, documents, options);
73
82
  },
74
83
  patch: (id, patch) => writer.patch(id, patch, tableName),
75
- patchMany: (patches, options) => {
76
- if (writer.patchMany === void 0) {
77
- throw new LunoraError$1("INTERNAL", `ctx.db.${tableName}.patchMany is unavailable: this writer has no batch patch`);
84
+ patchMany: (first, options) => {
85
+ if (Array.isArray(first)) {
86
+ if (writer.patchMany === void 0) {
87
+ throw new LunoraError$1("INTERNAL", `ctx.db.${tableName}.patchMany is unavailable: this writer has no batch patch`);
88
+ }
89
+ return writer.patchMany(
90
+ first.map((entry) => {
91
+ return { id: entry.id, patch: entry.values };
92
+ }),
93
+ options,
94
+ tableName
95
+ );
96
+ }
97
+ if (writer.patchWhere === void 0) {
98
+ throw new LunoraError$1("INTERNAL", `ctx.db.${tableName}.patchMany({ where, values }) is unavailable: this writer has no where-based patch`);
78
99
  }
79
- return writer.patchMany(
80
- patches.map((entry) => {
81
- return { id: entry.id, patch: entry.values };
82
- }),
83
- options,
84
- tableName
85
- );
100
+ const whereArgs = first;
101
+ return writer.patchWhere(tableName, { patch: whereArgs.values, where: whereArgs.where }, { limit: whereArgs.limit });
86
102
  },
87
103
  rank: (indexName, options) => writer.rank(tableName, indexName, options),
88
104
  rankPage: (indexName, options) => writer.rankPage(tableName, indexName, options),
@@ -1,4 +1,4 @@
1
- import { indexRolePermissions, computeReadBaseWhere, permissionName } from './rls-3sCJIH6F.mjs';
1
+ import { indexRolePermissions, computeReadBaseWhere, permissionName } from './rls-B_ZWgslr.mjs';
2
2
  import { r as readRlsTag } from './policy-tag-DvpVH2tv.mjs';
3
3
 
4
4
  const FALSE_PREDICATE = { OR: [] };
@@ -1,5 +1,5 @@
1
1
  import { LunoraError } from './LunoraError-WbxmrpxR.mjs';
2
- import { bindTableFacade, bindOrm } from './bindOrm-CNXKgfUa.mjs';
2
+ import { bindTableFacade, bindOrm } from './bindOrm-Bfgl7f-Q.mjs';
3
3
 
4
4
  const permissionName = (permission) => typeof permission === "string" ? permission : permission.name;
5
5
  const indexRolePermissions = (roles) => {
@@ -1,5 +1,5 @@
1
1
  import { LunoraError } from './LunoraError-WbxmrpxR.mjs';
2
- import { bindTableFacade, bindOrm } from './bindOrm-CNXKgfUa.mjs';
2
+ import { bindTableFacade, bindOrm } from './bindOrm-Bfgl7f-Q.mjs';
3
3
  import { t as tagRlsMiddleware } from './policy-tag-DvpVH2tv.mjs';
4
4
 
5
5
  const DEFAULT_BATCH_LIMIT = 500;
@@ -333,6 +333,16 @@ const wrapDatabase = (base, raw, perTable, context) => {
333
333
  }
334
334
  return { deleted: ids.length };
335
335
  },
336
+ async deleteWhere(tableName, where, options) {
337
+ const { baseWhere } = readBase(tableName);
338
+ const resolved = await route(tableName).findMany(tableName, {
339
+ baseWhere: mergeBaseWhere(where, baseWhere),
340
+ relationBaseWhere: relationReadFilter
341
+ });
342
+ const ids = resolved.page.map((row) => String(row["_id"]));
343
+ assertBatchLimit(ids.length, options?.limit, "deleteWhere");
344
+ return wrapped.deleteMany(ids, options);
345
+ },
336
346
  async findFirst(tableName, args) {
337
347
  const { baseWhere } = readBase(tableName);
338
348
  return route(tableName).findFirst(tableName, {
@@ -436,6 +446,19 @@ const wrapDatabase = (base, raw, perTable, context) => {
436
446
  expectedTable
437
447
  );
438
448
  }
449
+ return { patched: patches.length };
450
+ },
451
+ async patchWhere(tableName, args, options) {
452
+ const { baseWhere } = readBase(tableName);
453
+ const resolved = await route(tableName).findMany(tableName, {
454
+ baseWhere: mergeBaseWhere(args.where, baseWhere),
455
+ relationBaseWhere: relationReadFilter
456
+ });
457
+ const patches = resolved.page.map((row) => {
458
+ return { id: String(row["_id"]), patch: args.patch };
459
+ });
460
+ assertBatchLimit(patches.length, options?.limit, "patchWhere");
461
+ return wrapped.patchMany(patches, options);
439
462
  },
440
463
  query(tableName) {
441
464
  const { baseWhere } = readBase(tableName);
@@ -1,4 +1,4 @@
1
- import { indexRolePermissions, computeReadBaseWhere, matchesWhere, evaluateWrite, permissionName } from '../packem_shared/rls-3sCJIH6F.mjs';
1
+ import { indexRolePermissions, computeReadBaseWhere, matchesWhere, evaluateWrite, permissionName } from '../packem_shared/rls-B_ZWgslr.mjs';
2
2
 
3
3
  const expectPolicy = (policies, options = {}) => {
4
4
  const rolePermissions = indexRolePermissions(options.roles);
package/dist/types.d.mts CHANGED
@@ -518,6 +518,16 @@ interface BatchWriteOptions {
518
518
  /** Reject the call when the batch size exceeds this value (default 500). */
519
519
  limit?: number;
520
520
  }
521
+ /** Options accepted by {@link DatabaseWriter.insertMany} and the per-table facade. */
522
+ interface InsertManyOptions extends BatchWriteOptions {
523
+ /**
524
+ * When `true`, a UNIQUE-constraint breach for a row resolves to `null`
525
+ * instead of throwing — the rest of the batch is still inserted. Skipped rows
526
+ * keep their input-order slot with `null` in the returned array. Mirrors
527
+ * better-drizzle's `createMany({ skipDuplicates: true })`.
528
+ */
529
+ skipDuplicates?: boolean;
530
+ }
521
531
  interface DatabaseWriter extends DatabaseReader {
522
532
  delete: <T extends string>(id: Id<T>) => Promise<void>;
523
533
  /**
@@ -535,6 +545,18 @@ interface DatabaseWriter extends DatabaseReader {
535
545
  deleted: number;
536
546
  }>;
537
547
  /**
548
+ * Delete every row matching `where` in one call. Matching rows are resolved
549
+ * first, then each row is deleted through the single-row delete pipeline
550
+ * (triggers, companion sync, CDC, broadcast) so reactive subscriptions and
551
+ * search/aggregate companions stay correct.
552
+ *
553
+ * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
554
+ * BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
555
+ */
556
+ deleteWhere: (tableName: string, where: Record<string, unknown>, options?: BatchWriteOptions) => Promise<{
557
+ deleted: number;
558
+ }>;
559
+ /**
538
560
  * Insert a document, returning its server id.
539
561
  *
540
562
  * Pass `options.clientId` (a UUID) to key the row yourself — for an
@@ -551,12 +573,21 @@ interface DatabaseWriter extends DatabaseReader {
551
573
  * row gets defaults, validators, triggers, and a per-row RLS check — but the
552
574
  * caller pays one round-trip instead of N.
553
575
  *
576
+ * Pass `{ skipDuplicates: true }` to turn UNIQUE-constraint breaches into
577
+ * `null` results for that row instead of failing the whole batch; the rest of
578
+ * the batch is still inserted and order is preserved.
579
+ *
554
580
  * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
555
581
  * BEGIN/COMMIT span, so a mid-batch failure (an invalid or RLS-denied row)
556
582
  * rolls back the whole mutation. (In an action there is no transaction span,
557
583
  * so the prior inserts persist; the in-memory test harness mirrors the span.)
558
584
  */
559
- insertMany: <T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options?: BatchWriteOptions) => Promise<Id<T>[]>;
585
+ insertMany: {
586
+ <T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options: BatchWriteOptions & {
587
+ skipDuplicates: true;
588
+ }): Promise<(Id<T> | null)[]>;
589
+ <T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options?: InsertManyOptions): Promise<Id<T>[]>;
590
+ };
560
591
  /**
561
592
  * **Trusted** bulk insert: one multi-row `INSERT` that **skips per-row
562
593
  * `.check()` validators and before/after triggers** for throughput on data you
@@ -576,7 +607,8 @@ interface DatabaseWriter extends DatabaseReader {
576
607
  patch: <T extends string>(id: Id<T>, patch: Record<string, unknown>) => Promise<void>;
577
608
  /**
578
609
  * Patch many rows by id in one call. Each `{ id, patch }` is applied like a
579
- * single `patch()` (per-row triggers + RLS).
610
+ * single `patch()` (per-row triggers + RLS). Returns the number of rows
611
+ * actually patched.
580
612
  *
581
613
  * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
582
614
  * BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
@@ -586,7 +618,24 @@ interface DatabaseWriter extends DatabaseReader {
586
618
  patchMany: <T extends string>(patches: ReadonlyArray<{
587
619
  id: Id<T>;
588
620
  patch: Record<string, unknown>;
589
- }>, options?: BatchWriteOptions) => Promise<void>;
621
+ }>, options?: BatchWriteOptions) => Promise<{
622
+ patched: number;
623
+ }>;
624
+ /**
625
+ * Patch every row matching `where` with the same `patch` in one call. The
626
+ * matching rows are resolved first, then each row is updated through the
627
+ * single-row patch pipeline (OCC, triggers, companion sync, CDC, broadcast)
628
+ * so reactive subscriptions and search/aggregate companions stay correct.
629
+ *
630
+ * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
631
+ * BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
632
+ */
633
+ patchWhere: (tableName: string, args: {
634
+ patch: Record<string, unknown>;
635
+ where: Record<string, unknown>;
636
+ }, options?: BatchWriteOptions) => Promise<{
637
+ patched: number;
638
+ }>;
590
639
  replace: <T extends string>(id: Id<T>, document: Record<string, unknown>) => Promise<void>;
591
640
  }
592
641
  /** Authenticated identity surfaced into every context. */
package/dist/types.d.ts CHANGED
@@ -518,6 +518,16 @@ interface BatchWriteOptions {
518
518
  /** Reject the call when the batch size exceeds this value (default 500). */
519
519
  limit?: number;
520
520
  }
521
+ /** Options accepted by {@link DatabaseWriter.insertMany} and the per-table facade. */
522
+ interface InsertManyOptions extends BatchWriteOptions {
523
+ /**
524
+ * When `true`, a UNIQUE-constraint breach for a row resolves to `null`
525
+ * instead of throwing — the rest of the batch is still inserted. Skipped rows
526
+ * keep their input-order slot with `null` in the returned array. Mirrors
527
+ * better-drizzle's `createMany({ skipDuplicates: true })`.
528
+ */
529
+ skipDuplicates?: boolean;
530
+ }
521
531
  interface DatabaseWriter extends DatabaseReader {
522
532
  delete: <T extends string>(id: Id<T>) => Promise<void>;
523
533
  /**
@@ -535,6 +545,18 @@ interface DatabaseWriter extends DatabaseReader {
535
545
  deleted: number;
536
546
  }>;
537
547
  /**
548
+ * Delete every row matching `where` in one call. Matching rows are resolved
549
+ * first, then each row is deleted through the single-row delete pipeline
550
+ * (triggers, companion sync, CDC, broadcast) so reactive subscriptions and
551
+ * search/aggregate companions stay correct.
552
+ *
553
+ * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
554
+ * BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
555
+ */
556
+ deleteWhere: (tableName: string, where: Record<string, unknown>, options?: BatchWriteOptions) => Promise<{
557
+ deleted: number;
558
+ }>;
559
+ /**
538
560
  * Insert a document, returning its server id.
539
561
  *
540
562
  * Pass `options.clientId` (a UUID) to key the row yourself — for an
@@ -551,12 +573,21 @@ interface DatabaseWriter extends DatabaseReader {
551
573
  * row gets defaults, validators, triggers, and a per-row RLS check — but the
552
574
  * caller pays one round-trip instead of N.
553
575
  *
576
+ * Pass `{ skipDuplicates: true }` to turn UNIQUE-constraint breaches into
577
+ * `null` results for that row instead of failing the whole batch; the rest of
578
+ * the batch is still inserted and order is preserved.
579
+ *
554
580
  * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
555
581
  * BEGIN/COMMIT span, so a mid-batch failure (an invalid or RLS-denied row)
556
582
  * rolls back the whole mutation. (In an action there is no transaction span,
557
583
  * so the prior inserts persist; the in-memory test harness mirrors the span.)
558
584
  */
559
- insertMany: <T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options?: BatchWriteOptions) => Promise<Id<T>[]>;
585
+ insertMany: {
586
+ <T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options: BatchWriteOptions & {
587
+ skipDuplicates: true;
588
+ }): Promise<(Id<T> | null)[]>;
589
+ <T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options?: InsertManyOptions): Promise<Id<T>[]>;
590
+ };
560
591
  /**
561
592
  * **Trusted** bulk insert: one multi-row `INSERT` that **skips per-row
562
593
  * `.check()` validators and before/after triggers** for throughput on data you
@@ -576,7 +607,8 @@ interface DatabaseWriter extends DatabaseReader {
576
607
  patch: <T extends string>(id: Id<T>, patch: Record<string, unknown>) => Promise<void>;
577
608
  /**
578
609
  * Patch many rows by id in one call. Each `{ id, patch }` is applied like a
579
- * single `patch()` (per-row triggers + RLS).
610
+ * single `patch()` (per-row triggers + RLS). Returns the number of rows
611
+ * actually patched.
580
612
  *
581
613
  * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
582
614
  * BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
@@ -586,7 +618,24 @@ interface DatabaseWriter extends DatabaseReader {
586
618
  patchMany: <T extends string>(patches: ReadonlyArray<{
587
619
  id: Id<T>;
588
620
  patch: Record<string, unknown>;
589
- }>, options?: BatchWriteOptions) => Promise<void>;
621
+ }>, options?: BatchWriteOptions) => Promise<{
622
+ patched: number;
623
+ }>;
624
+ /**
625
+ * Patch every row matching `where` with the same `patch` in one call. The
626
+ * matching rows are resolved first, then each row is updated through the
627
+ * single-row patch pipeline (OCC, triggers, companion sync, CDC, broadcast)
628
+ * so reactive subscriptions and search/aggregate companions stay correct.
629
+ *
630
+ * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
631
+ * BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
632
+ */
633
+ patchWhere: (tableName: string, args: {
634
+ patch: Record<string, unknown>;
635
+ where: Record<string, unknown>;
636
+ }, options?: BatchWriteOptions) => Promise<{
637
+ patched: number;
638
+ }>;
590
639
  replace: <T extends string>(id: Id<T>, document: Record<string, unknown>) => Promise<void>;
591
640
  }
592
641
  /** Authenticated identity surfaced into every context. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/server",
3
- "version": "1.0.0-alpha.18",
3
+ "version": "1.0.0-alpha.19",
4
4
  "description": "Server primitives for Lunora: defineSchema, defineTable, query, mutation, and action",
5
5
  "keywords": [
6
6
  "backend",