@lunora/server 1.0.0-alpha.17 → 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.
package/README.md CHANGED
@@ -99,6 +99,52 @@ The builder chain is `<builder>.input(validators).<kind>(handler)`, plus `.use(m
99
99
 
100
100
  > This README covers the basics. For the full API, options, and guides, see the **[documentation](https://lunora.sh/docs/packages/server)**.
101
101
 
102
+ ### Caching with Workers Cache
103
+
104
+ Lunora supports Cloudflare Workers Cache for HTTP actions (`httpRoute`). RPC queries and mutations are `POST /_lunora/rpc` and are not cacheable at the edge by design.
105
+
106
+ **Enable Workers Cache** in `wrangler.jsonc`:
107
+
108
+ ```jsonc
109
+ {
110
+ "cache": { "enabled": true },
111
+ }
112
+ ```
113
+
114
+ The dev server and CLI automatically bump `compatibility_date` to the minimum required when cache is enabled — you do not need to set it manually.
115
+
116
+ **Set cache headers declaratively** on an `httpRoute`:
117
+
118
+ ```ts
119
+ import { httpRoute } from "./_generated/server";
120
+
121
+ export const getProduct = httpRoute
122
+ .get("/api/products/:id")
123
+ .params({ id: v.string() })
124
+ .cacheControl("public, max-age=300, stale-while-revalidate=3600")
125
+ .cacheTag("products")
126
+ .handler(async ({ ctx, params }) => {
127
+ return { id: params.id, name: "Widget" };
128
+ });
129
+ ```
130
+
131
+ **Purge cache by tag** from an action handler:
132
+
133
+ ```ts
134
+ import { action } from "./_generated/server";
135
+
136
+ export const refreshProducts = action.action(async ({ ctx }) => {
137
+ if (!ctx.cache) {
138
+ throw new Error("Workers Cache is not enabled in wrangler.jsonc");
139
+ }
140
+
141
+ await ctx.cache.purge({ tags: ["products"] });
142
+ return { ok: true };
143
+ });
144
+ ```
145
+
146
+ The `ctx.cache.purge` API accepts `{ tags?: string[]; purgeEverything?: boolean }`. Only action handlers expose `ctx.cache`; queries and mutations run inside the Durable Object and do not have access to the Worker-level cache binding.
147
+
102
148
  ## Related
103
149
 
104
150
  - [`@lunora/values`](https://www.npmjs.com/package/@lunora/values) — the `v.*` validators re-exported here.
@@ -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
@@ -1,7 +1,7 @@
1
1
  import { Validator, Infer, ValidatorMap, InferValidatorMap, v } from '@lunora/values';
2
2
  export { type ColumnValidator, type Id, type Infer, ValidationError, type Validator, type ValidatorKind, v } from '@lunora/values';
3
3
  import { ArgsValidator, InferArgs, RegisteredAction, ActionCtx, MutationCtx, RegisteredMutation, QueryCtx, RegisteredQuery, RegisteredStream, FunctionKind, Secrets, LifecycleEvent, RegisteredLifecycleHook, TableDefinition, RegisteredFunction, VectorIndexDefinition, Schema, AggregateOp, DurableObjectJurisdiction, RelationDefinition, GlobalBackend, OnDeleteAction, ExternalSourceDefinition, TriggerBuilder, TriggerDefinition, VectorEmbedder, VectorMetric, AggregateIndexDefinition, RankIndexDefinition } from "./types.mjs";
4
- export { type AnyApi, type AuthState, type DatabaseReader, type DatabaseWriter, type FunctionVisibility, type IndexDefinition, type IndexRangeBuilder, type LifecycleEventKind, type LunoraLogger, type PaginationOptions, type PaginationResult, type RankSortKey, type ReadOnlyStorage, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type SearchFilterBuilder, type SearchIndexDefinition, type ShardMode, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerCtx, type TriggerDatabase, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type VectorMatch, type VectorMatches, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, anyApi } from "./types.mjs";
4
+ export { type AnyApi, type AuthState, type CachePurge, type DatabaseReader, type DatabaseWriter, type FunctionVisibility, type IndexDefinition, type IndexRangeBuilder, type LifecycleEventKind, type LunoraLogger, type PaginationOptions, type PaginationResult, type RankSortKey, type ReadOnlyStorage, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type SearchFilterBuilder, type SearchIndexDefinition, type ShardMode, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerCtx, type TriggerDatabase, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type VectorMatch, type VectorMatches, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, anyApi } from "./types.mjs";
5
5
  import { LunoraError as LunoraError$1, LunoraErrorCode } from '@lunora/errors';
6
6
  export type { LunoraErrorCode } from '@lunora/errors';
7
7
  import { Context, Hono } from 'hono';
@@ -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>;
@@ -435,7 +477,7 @@ type HttpMethod = "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PU
435
477
  * `storage` surface — reach the data layer through `runQuery` / `runMutation` /
436
478
  * `runAction`, which forward to the owning shard.
437
479
  */
438
- type HttpActionCtx = Pick<ActionCtx, "auth" | "fetch" | "runAction" | "runMutation" | "runQuery">;
480
+ type HttpActionCtx = Pick<ActionCtx, "auth" | "cache" | "fetch" | "runAction" | "runMutation" | "runQuery">;
439
481
  /** A raw handler wrapped by {@link httpAction}. Receives the raw request, returns the raw response. */
440
482
  type HttpActionHandler = (context: HttpActionCtx, request: Request) => Promise<Response> | Response;
441
483
  /**
@@ -519,6 +561,16 @@ interface HttpStreamHandlerOptions<SearchParams extends ArgsValidator, Params ex
519
561
  */
520
562
  interface HttpRouteBuilder<SearchParams extends ArgsValidator, Body extends ArgsValidator, Params extends ArgsValidator, Output = undefined> {
521
563
  body: <B extends ArgsValidator>(validators: B) => HttpRouteBuilder<SearchParams, B & Body, Params, Output>;
564
+ /**
565
+ * Attach a `Cache-Control` header to the response. Only meaningful when
566
+ * Workers Cache is enabled in `wrangler.jsonc` (`"cache": { "enabled": true }`).
567
+ */
568
+ cacheControl: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
569
+ /**
570
+ * Attach a `Cache-Tag` header to the response for tag-based purging via
571
+ * `ctx.cache.purge({ tags: [...] })`.
572
+ */
573
+ cacheTag: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
522
574
  handler: [Output] extends [undefined] ? <R>(handler: (options: HttpRouteHandlerOptions<SearchParams, Body, Params>) => Promise<R> | R) => LunoraRouteHandler : (handler: (options: HttpRouteHandlerOptions<SearchParams, Body, Params>) => Output | Promise<Output>) => LunoraRouteHandler;
523
575
  output: <V extends Validator>(validator: V) => HttpRouteBuilder<SearchParams, Body, Params, Infer<V>>;
524
576
  params: <P extends ArgsValidator>(validators: P) => HttpRouteBuilder<SearchParams, Body, P & Params, Output>;
@@ -533,6 +585,11 @@ interface HttpRouteBuilder<SearchParams extends ArgsValidator, Body extends Args
533
585
  * handler's yielded type.
534
586
  */
535
587
  stream: <R>(handler: (options: HttpStreamHandlerOptions<SearchParams, Params>) => AsyncGenerator<R, void, void> | AsyncIterable<R>) => LunoraRouteHandler;
588
+ /**
589
+ * Attach a `Vary` header to the response so Cloudflare stores separate
590
+ * cached variants per distinct value of the listed request headers.
591
+ */
592
+ vary: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
536
593
  }
537
594
  /** Opens a fresh {@link HttpRouteBuilder}. The `path` documents intent; hono owns the actual routing at mount. */
538
595
  type HttpRouteFactory = (path: string) => HttpRouteBuilder<EmptyArgs, EmptyArgs, EmptyArgs>;
@@ -828,6 +885,11 @@ interface MaskDatabase {
828
885
  }) => Promise<{
829
886
  deleted: number;
830
887
  }>;
888
+ deleteWhere?: (tableName: string, where: Record<string, unknown>, options?: {
889
+ limit?: number;
890
+ }) => Promise<{
891
+ deleted: number;
892
+ }>;
831
893
  findFirst: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown> | null>;
832
894
  findFirstOrThrow: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown>>;
833
895
  findMany: (tableName: string, args?: QueryArgs$1) => Promise<QueryPage$1>;
@@ -839,7 +901,8 @@ interface MaskDatabase {
839
901
  insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
840
902
  insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
841
903
  limit?: number;
842
- }) => Promise<string[]>;
904
+ skipDuplicates?: boolean;
905
+ }) => Promise<(string | null)[]>;
843
906
  lookupById?: (id: string, expectedTable?: string) => Promise<null | {
844
907
  row: Record<string, unknown>;
845
908
  tableName: string;
@@ -850,7 +913,17 @@ interface MaskDatabase {
850
913
  patch: Record<string, unknown>;
851
914
  }>, options?: {
852
915
  limit?: number;
853
- }) => 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
+ }>;
854
927
  query: (tableName: string) => TableReaderLike$1;
855
928
  rank: (tableName: string, indexName: string, options: unknown) => Promise<null | {
856
929
  position: number;
@@ -1722,6 +1795,11 @@ interface DatabaseWriterLike {
1722
1795
  }, expectedTable?: string) => Promise<{
1723
1796
  deleted: number;
1724
1797
  }>;
1798
+ deleteWhere?: (tableName: string, where: WhereInput, options?: {
1799
+ limit?: number;
1800
+ }) => Promise<{
1801
+ deleted: number;
1802
+ }>;
1725
1803
  findFirst: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown> | null>;
1726
1804
  findFirstOrThrow: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown>>;
1727
1805
  findMany: (tableName: string, args?: QueryArgs) => Promise<QueryPage>;
@@ -1738,7 +1816,8 @@ interface DatabaseWriterLike {
1738
1816
  insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
1739
1817
  insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
1740
1818
  limit?: number;
1741
- }) => Promise<string[]>;
1819
+ skipDuplicates?: boolean;
1820
+ }) => Promise<(string | null)[]>;
1742
1821
  insertManyUnsafe: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
1743
1822
  allowExplicitId?: boolean;
1744
1823
  limit?: number;
@@ -1761,7 +1840,17 @@ interface DatabaseWriterLike {
1761
1840
  patch: Record<string, unknown>;
1762
1841
  }>, options?: {
1763
1842
  limit?: number;
1764
- }, 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
+ }>;
1765
1854
  query: (tableName: string) => TableReaderLike;
1766
1855
  /**
1767
1856
  * Rank a row within its partition. A position is a count-of-rows-before, so
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Validator, Infer, ValidatorMap, InferValidatorMap, v } from '@lunora/values';
2
2
  export { type ColumnValidator, type Id, type Infer, ValidationError, type Validator, type ValidatorKind, v } from '@lunora/values';
3
3
  import { ArgsValidator, InferArgs, RegisteredAction, ActionCtx, MutationCtx, RegisteredMutation, QueryCtx, RegisteredQuery, RegisteredStream, FunctionKind, Secrets, LifecycleEvent, RegisteredLifecycleHook, TableDefinition, RegisteredFunction, VectorIndexDefinition, Schema, AggregateOp, DurableObjectJurisdiction, RelationDefinition, GlobalBackend, OnDeleteAction, ExternalSourceDefinition, TriggerBuilder, TriggerDefinition, VectorEmbedder, VectorMetric, AggregateIndexDefinition, RankIndexDefinition } from "./types.js";
4
- export { type AnyApi, type AuthState, type DatabaseReader, type DatabaseWriter, type FunctionVisibility, type IndexDefinition, type IndexRangeBuilder, type LifecycleEventKind, type LunoraLogger, type PaginationOptions, type PaginationResult, type RankSortKey, type ReadOnlyStorage, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type SearchFilterBuilder, type SearchIndexDefinition, type ShardMode, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerCtx, type TriggerDatabase, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type VectorMatch, type VectorMatches, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, anyApi } from "./types.js";
4
+ export { type AnyApi, type AuthState, type CachePurge, type DatabaseReader, type DatabaseWriter, type FunctionVisibility, type IndexDefinition, type IndexRangeBuilder, type LifecycleEventKind, type LunoraLogger, type PaginationOptions, type PaginationResult, type RankSortKey, type ReadOnlyStorage, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type SearchFilterBuilder, type SearchIndexDefinition, type ShardMode, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerCtx, type TriggerDatabase, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type VectorMatch, type VectorMatches, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, anyApi } from "./types.js";
5
5
  import { LunoraError as LunoraError$1, LunoraErrorCode } from '@lunora/errors';
6
6
  export type { LunoraErrorCode } from '@lunora/errors';
7
7
  import { Context, Hono } from 'hono';
@@ -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>;
@@ -435,7 +477,7 @@ type HttpMethod = "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PU
435
477
  * `storage` surface — reach the data layer through `runQuery` / `runMutation` /
436
478
  * `runAction`, which forward to the owning shard.
437
479
  */
438
- type HttpActionCtx = Pick<ActionCtx, "auth" | "fetch" | "runAction" | "runMutation" | "runQuery">;
480
+ type HttpActionCtx = Pick<ActionCtx, "auth" | "cache" | "fetch" | "runAction" | "runMutation" | "runQuery">;
439
481
  /** A raw handler wrapped by {@link httpAction}. Receives the raw request, returns the raw response. */
440
482
  type HttpActionHandler = (context: HttpActionCtx, request: Request) => Promise<Response> | Response;
441
483
  /**
@@ -519,6 +561,16 @@ interface HttpStreamHandlerOptions<SearchParams extends ArgsValidator, Params ex
519
561
  */
520
562
  interface HttpRouteBuilder<SearchParams extends ArgsValidator, Body extends ArgsValidator, Params extends ArgsValidator, Output = undefined> {
521
563
  body: <B extends ArgsValidator>(validators: B) => HttpRouteBuilder<SearchParams, B & Body, Params, Output>;
564
+ /**
565
+ * Attach a `Cache-Control` header to the response. Only meaningful when
566
+ * Workers Cache is enabled in `wrangler.jsonc` (`"cache": { "enabled": true }`).
567
+ */
568
+ cacheControl: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
569
+ /**
570
+ * Attach a `Cache-Tag` header to the response for tag-based purging via
571
+ * `ctx.cache.purge({ tags: [...] })`.
572
+ */
573
+ cacheTag: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
522
574
  handler: [Output] extends [undefined] ? <R>(handler: (options: HttpRouteHandlerOptions<SearchParams, Body, Params>) => Promise<R> | R) => LunoraRouteHandler : (handler: (options: HttpRouteHandlerOptions<SearchParams, Body, Params>) => Output | Promise<Output>) => LunoraRouteHandler;
523
575
  output: <V extends Validator>(validator: V) => HttpRouteBuilder<SearchParams, Body, Params, Infer<V>>;
524
576
  params: <P extends ArgsValidator>(validators: P) => HttpRouteBuilder<SearchParams, Body, P & Params, Output>;
@@ -533,6 +585,11 @@ interface HttpRouteBuilder<SearchParams extends ArgsValidator, Body extends Args
533
585
  * handler's yielded type.
534
586
  */
535
587
  stream: <R>(handler: (options: HttpStreamHandlerOptions<SearchParams, Params>) => AsyncGenerator<R, void, void> | AsyncIterable<R>) => LunoraRouteHandler;
588
+ /**
589
+ * Attach a `Vary` header to the response so Cloudflare stores separate
590
+ * cached variants per distinct value of the listed request headers.
591
+ */
592
+ vary: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
536
593
  }
537
594
  /** Opens a fresh {@link HttpRouteBuilder}. The `path` documents intent; hono owns the actual routing at mount. */
538
595
  type HttpRouteFactory = (path: string) => HttpRouteBuilder<EmptyArgs, EmptyArgs, EmptyArgs>;
@@ -828,6 +885,11 @@ interface MaskDatabase {
828
885
  }) => Promise<{
829
886
  deleted: number;
830
887
  }>;
888
+ deleteWhere?: (tableName: string, where: Record<string, unknown>, options?: {
889
+ limit?: number;
890
+ }) => Promise<{
891
+ deleted: number;
892
+ }>;
831
893
  findFirst: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown> | null>;
832
894
  findFirstOrThrow: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown>>;
833
895
  findMany: (tableName: string, args?: QueryArgs$1) => Promise<QueryPage$1>;
@@ -839,7 +901,8 @@ interface MaskDatabase {
839
901
  insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
840
902
  insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
841
903
  limit?: number;
842
- }) => Promise<string[]>;
904
+ skipDuplicates?: boolean;
905
+ }) => Promise<(string | null)[]>;
843
906
  lookupById?: (id: string, expectedTable?: string) => Promise<null | {
844
907
  row: Record<string, unknown>;
845
908
  tableName: string;
@@ -850,7 +913,17 @@ interface MaskDatabase {
850
913
  patch: Record<string, unknown>;
851
914
  }>, options?: {
852
915
  limit?: number;
853
- }) => 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
+ }>;
854
927
  query: (tableName: string) => TableReaderLike$1;
855
928
  rank: (tableName: string, indexName: string, options: unknown) => Promise<null | {
856
929
  position: number;
@@ -1722,6 +1795,11 @@ interface DatabaseWriterLike {
1722
1795
  }, expectedTable?: string) => Promise<{
1723
1796
  deleted: number;
1724
1797
  }>;
1798
+ deleteWhere?: (tableName: string, where: WhereInput, options?: {
1799
+ limit?: number;
1800
+ }) => Promise<{
1801
+ deleted: number;
1802
+ }>;
1725
1803
  findFirst: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown> | null>;
1726
1804
  findFirstOrThrow: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown>>;
1727
1805
  findMany: (tableName: string, args?: QueryArgs) => Promise<QueryPage>;
@@ -1738,7 +1816,8 @@ interface DatabaseWriterLike {
1738
1816
  insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
1739
1817
  insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
1740
1818
  limit?: number;
1741
- }) => Promise<string[]>;
1819
+ skipDuplicates?: boolean;
1820
+ }) => Promise<(string | null)[]>;
1742
1821
  insertManyUnsafe: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
1743
1822
  allowExplicitId?: boolean;
1744
1823
  limit?: number;
@@ -1761,7 +1840,17 @@ interface DatabaseWriterLike {
1761
1840
  patch: Record<string, unknown>;
1762
1841
  }>, options?: {
1763
1842
  limit?: number;
1764
- }, 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
+ }>;
1765
1854
  query: (tableName: string) => TableReaderLike;
1766
1855
  /**
1767
1856
  * Rank a row within its partition. A position is a count-of-rows-before, so
package/dist/index.mjs CHANGED
@@ -3,8 +3,8 @@ 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';
7
- export { httpAction, httpRoute, httpRouter, isSafeHeaderValue, serveStorageObject } from './packem_shared/httpAction-C_L82dxR.mjs';
6
+ export { bindOrm, bindTableFacade } from './packem_shared/bindOrm-Bfgl7f-Q.mjs';
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';
10
10
  export { defineMigration } from './packem_shared/defineMigration-Hx01yIht.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: [] };
@@ -126,7 +126,21 @@ const buildRouteHandler = (state, userHandler) => async (c) => {
126
126
  const body = Object.keys(state.body).length > 0 ? await parseBody(state.body, c) : {};
127
127
  const result = await userHandler({ body, ctx: context, params, searchParams });
128
128
  const payload = state.output ? applyOutput(state.output, result) : result;
129
- return payload === void 0 ? new Response(null, { status: 204 }) : Response.json(payload);
129
+ const headers = {};
130
+ if (state.cacheControl) {
131
+ headers["cache-control"] = state.cacheControl;
132
+ }
133
+ if (state.cacheTag) {
134
+ headers["cache-tag"] = state.cacheTag;
135
+ }
136
+ if (state.vary) {
137
+ headers.vary = state.vary;
138
+ }
139
+ const hasCacheHeaders = Object.keys(headers).length > 0;
140
+ if (payload === void 0) {
141
+ return new Response(null, { headers: hasCacheHeaders ? headers : void 0, status: 204 });
142
+ }
143
+ return Response.json(payload, { headers: hasCacheHeaders ? headers : void 0 });
130
144
  } catch (error) {
131
145
  return errorResponse(error);
132
146
  }
@@ -202,25 +216,31 @@ const buildStreamHandler = (state, userHandler) => (
202
216
  }
203
217
  }
204
218
  });
205
- return new Response(stream, {
206
- headers: {
207
- "cache-control": "no-cache, no-transform",
208
- "content-type": "text/event-stream; charset=utf-8",
209
- // Hint to proxies (including Cloudflare's own buffering layer)
210
- // that this response must not be coalesced.
211
- "x-accel-buffering": "no"
212
- }
213
- });
219
+ const headers = {
220
+ // SSE responses must stay uncacheable so proxies don't buffer or
221
+ // coalesce live frames. `cacheControl()` is intentionally ignored
222
+ // for stream() routes; `cacheTag`/`vary` are also omitted because
223
+ // they only make sense alongside a cacheable response.
224
+ "cache-control": "no-cache, no-transform",
225
+ "content-type": "text/event-stream; charset=utf-8",
226
+ // Hint to proxies (including Cloudflare's own buffering layer)
227
+ // that this response must not be coalesced.
228
+ "x-accel-buffering": "no"
229
+ };
230
+ return new Response(stream, { headers });
214
231
  }
215
232
  );
216
233
  const makeRouteBuilder = (state) => {
217
234
  return {
218
235
  body: (validators) => makeRouteBuilder({ ...state, body: { ...state.body, ...validators } }),
236
+ cacheControl: (value) => makeRouteBuilder({ ...state, cacheControl: value }),
237
+ cacheTag: (value) => makeRouteBuilder({ ...state, cacheTag: value }),
219
238
  handler: (userHandler) => buildRouteHandler(state, userHandler),
220
239
  output: (validator) => makeRouteBuilder({ ...state, output: validator }),
221
240
  params: (validators) => makeRouteBuilder({ ...state, params: { ...state.params, ...validators } }),
222
241
  searchParams: (validators) => makeRouteBuilder({ ...state, searchParams: { ...state.searchParams, ...validators } }),
223
- stream: (userHandler) => buildStreamHandler(state, userHandler)
242
+ stream: (userHandler) => buildStreamHandler(state, userHandler),
243
+ vary: (value) => makeRouteBuilder({ ...state, vary: value })
224
244
  };
225
245
  };
226
246
  const makeRouteFactory = (method) => (path) => makeRouteBuilder({ body: {}, method, params: {}, path, searchParams: {} });
@@ -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. */
@@ -690,6 +739,21 @@ interface Workflows {
690
739
  get: <Params = Record<string, unknown>>(name: string) => WorkflowHandle<Params>;
691
740
  }
692
741
  /**
742
+ * Programmatic cache purge surface exposed on {@link ActionCtx}. Actions run
743
+ * in the Worker (not the DO), so they can reach the Worker's `ctx.cache.purge`.
744
+ * Queries and mutations do not expose this — they run inside the Durable Object.
745
+ */
746
+ interface CachePurge {
747
+ /**
748
+ * Purge cached responses matching the given tags, or everything when
749
+ * `purgeEverything` is true. Only available in action handlers.
750
+ */
751
+ purge: (options: {
752
+ purgeEverything?: boolean;
753
+ tags?: string[];
754
+ }) => Promise<unknown>;
755
+ }
756
+ /**
693
757
  * Structural projection of workers-types' `SecretsStoreSecret` binding — the
694
758
  * per-secret `secrets_store_secrets[]` binding whose `.get()` resolves the
695
759
  * secret value (or throws if it does not exist). Mirrored structurally so the
@@ -1136,6 +1200,13 @@ interface MutationCtx {
1136
1200
  }
1137
1201
  interface ActionCtx {
1138
1202
  readonly auth: AuthState;
1203
+ /**
1204
+ * Programmatic Workers Cache purge; see {@link CachePurge}.
1205
+ * **Action-only** — actions run in the Worker, which has a `cache` binding.
1206
+ * Queries and mutations run inside the Durable Object and do not expose this.
1207
+ * Optional at runtime because Workers Cache is only present when enabled.
1208
+ */
1209
+ readonly cache?: CachePurge;
1139
1210
  readonly db: DatabaseWriter;
1140
1211
  /**
1141
1212
  * The validated, typed environment. Populated only when the project declares
@@ -1178,4 +1249,4 @@ interface ActionCtx {
1178
1249
  */
1179
1250
  type AnyApi = Record<string, Record<string, RegisteredFunction<ArgsValidator, unknown, FunctionKind>>>;
1180
1251
  declare const anyApi: AnyApi;
1181
- export { type ActionCtx, type AggregateIndexDefinition, type AggregateOp, type AnyApi, type ArgsValidator, type AuthState, type DatabaseReader, type DatabaseWriter, type DurableObjectJurisdiction, type ExternalSourceDefinition, type ExternalSourceMode, type ExternalSourceRefresh, type FunctionKind, type FunctionVisibility, type GlobalBackend, type IndexDefinition, type IndexRangeBuilder, type InferArgs, type LifecycleEvent, type LifecycleEventKind, type LunoraLogger, type MutationCtx, type OnDeleteAction, type PaginationOptions, type PaginationResult, type QueryCtx, type RankIndexDefinition, type RankSortKey, type ReadOnlyStorage, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMutation, type RegisteredQuery, type RegisteredStream, type RelationDefinition, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type Schema, type SearchFilterBuilder, type SearchIndexDefinition, type Secrets, type SecretsStoreSecretLike, type ShardMode, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableDefinition, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerBuilder, type TriggerCtx, type TriggerDatabase, type TriggerDefinition, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type VectorEmbedder, type VectorIndexDefinition, type VectorMatch, type VectorMatches, type VectorMetric, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, anyApi };
1252
+ export { type ActionCtx, type AggregateIndexDefinition, type AggregateOp, type AnyApi, type ArgsValidator, type AuthState, type CachePurge, type DatabaseReader, type DatabaseWriter, type DurableObjectJurisdiction, type ExternalSourceDefinition, type ExternalSourceMode, type ExternalSourceRefresh, type FunctionKind, type FunctionVisibility, type GlobalBackend, type IndexDefinition, type IndexRangeBuilder, type InferArgs, type LifecycleEvent, type LifecycleEventKind, type LunoraLogger, type MutationCtx, type OnDeleteAction, type PaginationOptions, type PaginationResult, type QueryCtx, type RankIndexDefinition, type RankSortKey, type ReadOnlyStorage, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMutation, type RegisteredQuery, type RegisteredStream, type RelationDefinition, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type Schema, type SearchFilterBuilder, type SearchIndexDefinition, type Secrets, type SecretsStoreSecretLike, type ShardMode, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableDefinition, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerBuilder, type TriggerCtx, type TriggerDatabase, type TriggerDefinition, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type VectorEmbedder, type VectorIndexDefinition, type VectorMatch, type VectorMatches, type VectorMetric, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, anyApi };
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. */
@@ -690,6 +739,21 @@ interface Workflows {
690
739
  get: <Params = Record<string, unknown>>(name: string) => WorkflowHandle<Params>;
691
740
  }
692
741
  /**
742
+ * Programmatic cache purge surface exposed on {@link ActionCtx}. Actions run
743
+ * in the Worker (not the DO), so they can reach the Worker's `ctx.cache.purge`.
744
+ * Queries and mutations do not expose this — they run inside the Durable Object.
745
+ */
746
+ interface CachePurge {
747
+ /**
748
+ * Purge cached responses matching the given tags, or everything when
749
+ * `purgeEverything` is true. Only available in action handlers.
750
+ */
751
+ purge: (options: {
752
+ purgeEverything?: boolean;
753
+ tags?: string[];
754
+ }) => Promise<unknown>;
755
+ }
756
+ /**
693
757
  * Structural projection of workers-types' `SecretsStoreSecret` binding — the
694
758
  * per-secret `secrets_store_secrets[]` binding whose `.get()` resolves the
695
759
  * secret value (or throws if it does not exist). Mirrored structurally so the
@@ -1136,6 +1200,13 @@ interface MutationCtx {
1136
1200
  }
1137
1201
  interface ActionCtx {
1138
1202
  readonly auth: AuthState;
1203
+ /**
1204
+ * Programmatic Workers Cache purge; see {@link CachePurge}.
1205
+ * **Action-only** — actions run in the Worker, which has a `cache` binding.
1206
+ * Queries and mutations run inside the Durable Object and do not expose this.
1207
+ * Optional at runtime because Workers Cache is only present when enabled.
1208
+ */
1209
+ readonly cache?: CachePurge;
1139
1210
  readonly db: DatabaseWriter;
1140
1211
  /**
1141
1212
  * The validated, typed environment. Populated only when the project declares
@@ -1178,4 +1249,4 @@ interface ActionCtx {
1178
1249
  */
1179
1250
  type AnyApi = Record<string, Record<string, RegisteredFunction<ArgsValidator, unknown, FunctionKind>>>;
1180
1251
  declare const anyApi: AnyApi;
1181
- export { type ActionCtx, type AggregateIndexDefinition, type AggregateOp, type AnyApi, type ArgsValidator, type AuthState, type DatabaseReader, type DatabaseWriter, type DurableObjectJurisdiction, type ExternalSourceDefinition, type ExternalSourceMode, type ExternalSourceRefresh, type FunctionKind, type FunctionVisibility, type GlobalBackend, type IndexDefinition, type IndexRangeBuilder, type InferArgs, type LifecycleEvent, type LifecycleEventKind, type LunoraLogger, type MutationCtx, type OnDeleteAction, type PaginationOptions, type PaginationResult, type QueryCtx, type RankIndexDefinition, type RankSortKey, type ReadOnlyStorage, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMutation, type RegisteredQuery, type RegisteredStream, type RelationDefinition, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type Schema, type SearchFilterBuilder, type SearchIndexDefinition, type Secrets, type SecretsStoreSecretLike, type ShardMode, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableDefinition, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerBuilder, type TriggerCtx, type TriggerDatabase, type TriggerDefinition, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type VectorEmbedder, type VectorIndexDefinition, type VectorMatch, type VectorMatches, type VectorMetric, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, anyApi };
1252
+ export { type ActionCtx, type AggregateIndexDefinition, type AggregateOp, type AnyApi, type ArgsValidator, type AuthState, type CachePurge, type DatabaseReader, type DatabaseWriter, type DurableObjectJurisdiction, type ExternalSourceDefinition, type ExternalSourceMode, type ExternalSourceRefresh, type FunctionKind, type FunctionVisibility, type GlobalBackend, type IndexDefinition, type IndexRangeBuilder, type InferArgs, type LifecycleEvent, type LifecycleEventKind, type LunoraLogger, type MutationCtx, type OnDeleteAction, type PaginationOptions, type PaginationResult, type QueryCtx, type RankIndexDefinition, type RankSortKey, type ReadOnlyStorage, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMutation, type RegisteredQuery, type RegisteredStream, type RelationDefinition, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type Schema, type SearchFilterBuilder, type SearchIndexDefinition, type Secrets, type SecretsStoreSecretLike, type ShardMode, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableDefinition, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerBuilder, type TriggerCtx, type TriggerDatabase, type TriggerDefinition, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type VectorEmbedder, type VectorIndexDefinition, type VectorMatch, type VectorMatches, type VectorMetric, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, anyApi };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/server",
3
- "version": "1.0.0-alpha.17",
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",