@lunora/server 1.0.0-alpha.1 → 1.0.0-alpha.10

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.
Files changed (29) hide show
  1. package/README.md +5 -1
  2. package/__assets__/package-og.svg +1 -1
  3. package/dist/data-model.d.mts +104 -16
  4. package/dist/data-model.d.ts +104 -16
  5. package/dist/index.d.mts +269 -25
  6. package/dist/index.d.ts +269 -25
  7. package/dist/index.mjs +16 -12
  8. package/dist/packem_shared/{LunoraError-DhggBJZF.mjs → LunoraError-DN7Zhhvu.mjs} +4 -1
  9. package/dist/packem_shared/{definePresence-D5LtwGl0.mjs → PRESENCE_DEFAULT_TTL_MS-D8viLY1S.mjs} +4 -4
  10. package/dist/packem_shared/{bindTableFacade-DCuyr46L.mjs → bindOrm-Ce57S3N9.mjs} +58 -1
  11. package/dist/packem_shared/buildRlsReadRegistry-1jexWrb3.mjs +107 -0
  12. package/dist/packem_shared/createSecrets-TsIP9lOa.mjs +55 -0
  13. package/dist/packem_shared/{defineAggregateIndex-DzqxtAyV.mjs → defineAggregateIndex-ZdyU78gh.mjs} +58 -3
  14. package/dist/packem_shared/defineMutator-EIXAWhs9.mjs +11 -0
  15. package/dist/packem_shared/defineShape-CJ27Wx7o.mjs +17 -0
  16. package/dist/packem_shared/functions-Di9FUNkf.mjs +5 -0
  17. package/dist/packem_shared/{httpAction-B7FYUEgr.mjs → httpAction-FLwfsePg.mjs} +1 -1
  18. package/dist/packem_shared/{initLunora-CATvPsVt.mjs → initLunora-lxwHTEV3.mjs} +17 -3
  19. package/dist/packem_shared/{mask-CkZJHHMM.mjs → mask-BV_jNzsN.mjs} +2 -2
  20. package/dist/packem_shared/policy-tag-DvpVH2tv.mjs +13 -0
  21. package/dist/packem_shared/{rls-Zhf5wEeJ.mjs → rls-2Jhd0uev.mjs} +22 -4
  22. package/dist/packem_shared/{storageRules-4a30FSpI.mjs → storageRules-Cje6Woea.mjs} +1 -1
  23. package/dist/rls/testing.mjs +1 -1
  24. package/dist/types.d.mts +130 -2
  25. package/dist/types.d.ts +130 -2
  26. package/package.json +5 -5
  27. /package/dist/packem_shared/{defineEnv-DjFkpkSP.mjs → LunoraEnvError-DjFkpkSP.mjs} +0 -0
  28. /package/dist/packem_shared/{defineSchemaExtension-Ck5_TUO8.mjs → composePluginMiddleware-Ck5_TUO8.mjs} +0 -0
  29. /package/dist/packem_shared/{definePolicy-De67zPDS.mjs → createPolicyDsl-De67zPDS.mjs} +0 -0
@@ -49,6 +49,13 @@ interface QueryArgs<TDocument> {
49
49
  cursor?: null | string;
50
50
  limit?: number;
51
51
  orderBy?: OrderBy<TDocument>[];
52
+ /**
53
+ * Project each returned row down to these columns (plus the system fields
54
+ * `_id`/`_creationTime`, always retained). Trims wire payload for wide rows;
55
+ * relations requested via `with` are still attached. Reactivity is unaffected —
56
+ * the engine still reads the whole row to track dependencies.
57
+ */
58
+ select?: ReadonlyArray<keyof TDocument & string>;
52
59
  where?: Where<TDocument>;
53
60
  }
54
61
  /**
@@ -97,6 +104,12 @@ type WhereOf<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = Rel
97
104
  /** {@link QueryArgs} with the relation-aware {@link WhereOf} `where` typing. */
98
105
  interface QueryArgsOf<DM, REL extends Record<keyof DM, object>, T extends keyof DM> {
99
106
  cursor?: null | string;
107
+ /**
108
+ * Include soft-deleted rows (`.softDelete()` tables only). Default hides them;
109
+ * `true` returns deleted rows alongside live ones. No effect on a table
110
+ * without `.softDelete()`.
111
+ */
112
+ includeDeleted?: boolean;
100
113
  limit?: number;
101
114
  orderBy?: OrderBy<DM[T]>[];
102
115
  where?: WhereOf<DM, REL, T>;
@@ -110,11 +123,15 @@ interface QueryPage<TDocument> {
110
123
  type NestedWithArgument<WK> = WK extends {
111
124
  with: infer NW;
112
125
  } ? NW : {};
126
+ /** The nested `select` tuple inside a relation's with-value, or `undefined` (no projection). */
127
+ type NestedSelectArgument<WK> = WK extends {
128
+ select: infer S;
129
+ } ? S : undefined;
113
130
  /**
114
131
  * The `with` argument for table `T`: each relation can be `true` (load with no
115
- * refinements) or an object. `many` relations accept `where`/`orderBy`/`limit`
116
- * plus a nested `with`; `one` relations accept only a nested `with`. The
117
- * reserved `_count` key requests per-relation aggregate counts.
132
+ * refinements) or an object. `many` relations accept `where`/`orderBy`/`limit`/
133
+ * `select` plus a nested `with`; `one` relations accept `select` + a nested
134
+ * `with`. The reserved `_count` key requests per-relation aggregate counts.
118
135
  */
119
136
  type WithArg<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = { [K in keyof REL[T]]?: REL[T][K] extends {
120
137
  __relationKind: "many";
@@ -125,18 +142,24 @@ type WithArg<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = { [
125
142
  __relationKind: "one";
126
143
  __target: infer Target extends keyof DM;
127
144
  } ? boolean | {
145
+ select?: ReadonlyArray<keyof DM[Target] & string>;
128
146
  with?: WithArg<DM, REL, Target>;
129
147
  } : never } & {
130
148
  _count?: { [K in keyof REL[T]]?: true };
131
149
  };
132
- /** Resolve a single relation descriptor + its with-value to the loaded type. */
150
+ /**
151
+ * Resolve a single relation descriptor + its with-value to the loaded type,
152
+ * threading the nested `select` tuple into the projected child shape (the 5th
153
+ * `LoadWith` arg) so `with: { author: { select: ["name"] } }` narrows the loaded
154
+ * `author` to the selected columns + system fields.
155
+ */
133
156
  type LoadRelation<DM, REL extends Record<keyof DM, object>, R, WK> = R extends {
134
157
  __relationKind: "one";
135
158
  __target: infer Target extends keyof DM;
136
- } ? LoadWith<DM, REL, Target, NestedWithArgument<WK>> | null : R extends {
159
+ } ? LoadWith<DM, REL, Target, NestedWithArgument<WK>, NestedSelectArgument<WK>> | null : R extends {
137
160
  __relationKind: "many";
138
161
  __target: infer Target extends keyof DM;
139
- } ? LoadWith<DM, REL, Target, NestedWithArgument<WK>>[] : never;
162
+ } ? LoadWith<DM, REL, Target, NestedWithArgument<WK>, NestedSelectArgument<WK>>[] : never;
140
163
  /** The relation keys of `W` that were actually requested (not `false`/`undefined`). */
141
164
  type LoadedRelations<DM, REL extends Record<keyof DM, object>, T extends keyof DM, W> = { [K in keyof W as K extends keyof REL[T] ? (W[K] extends false | undefined ? never : K) : never]: K extends keyof REL[T] ? LoadRelation<DM, REL, REL[T][K], W[K]> : never };
142
165
  /** The `_count` projection of `W`, if any. */
@@ -145,8 +168,20 @@ type LoadedCount<W> = W extends {
145
168
  } ? {
146
169
  _count: { [K in keyof C]: number };
147
170
  } : {};
148
- /** `Doc&lt;T>` narrowed to exactly the relations requested in the with-arg `W`. */
149
- type LoadWith<DM, REL extends Record<keyof DM, object>, T extends keyof DM, W> = DM[T] & LoadedCount<W> & LoadedRelations<DM, REL, T, W>;
171
+ /** System columns a `select` projection always retains, so cursors and by-id reuse keep working. */
172
+ type SelectAlwaysKeep<DM, T extends keyof DM> = ("_creationTime" | "_id") & keyof DM[T];
173
+ /**
174
+ * `DM[T]` narrowed to the columns named by a `select` tuple `S` (plus the system
175
+ * fields). `undefined` (the default — no `select`) keeps the full document.
176
+ */
177
+ type ProjectDoc<DM, T extends keyof DM, S> = S extends ReadonlyArray<infer K> ? (K extends keyof DM[T] ? Pick<DM[T], (K & keyof DM[T]) | SelectAlwaysKeep<DM, T>> : DM[T]) : DM[T];
178
+ /**
179
+ * `Doc&lt;T>` narrowed to exactly the relations requested in the with-arg `W` and,
180
+ * when a `select` tuple `S` is supplied, to its projected columns. `S` defaults
181
+ * to `undefined` so the 4-argument form (the codegen-emitted callers) keeps the
182
+ * full document.
183
+ */
184
+ type LoadWith<DM, REL extends Record<keyof DM, object>, T extends keyof DM, W, S = undefined> = LoadedCount<W> & LoadedRelations<DM, REL, T, W> & ProjectDoc<DM, T, S>;
150
185
  /** Reducer applied by an aggregate (`avg`/`count`/`max`/`min`/`sum`). */
151
186
  type AggregateOp = "avg" | "count" | "max" | "min" | "sum";
152
187
  /**
@@ -260,15 +295,20 @@ interface TableReaderFacade<DM, REL extends Record<keyof DM, object>, RANK exten
260
295
  * uses to inject `baseWhere` and `restrictsCounts`.
261
296
  */
262
297
  count: (where?: RestrictableQueryOptionsOf<DM, REL, T> | WhereOf<DM, REL, T>) => Promise<number>;
263
- findFirst: <W extends WithArg<DM, REL, T> = {}>(args?: QueryArgsOf<DM, REL, T> & {
298
+ /** `true` when at least one row matches `where` (any row when omitted). RLS-filtered exactly like `findFirst`. */
299
+ exists: (where?: WhereOf<DM, REL, T>) => Promise<boolean>;
300
+ findFirst: <W extends WithArg<DM, REL, T> = {}, S extends ReadonlyArray<keyof DM[T] & string> | undefined = undefined>(args?: QueryArgsOf<DM, REL, T> & {
301
+ select?: S;
264
302
  with?: W;
265
- }) => Promise<LoadWith<DM, REL, T, W> | null>;
266
- findFirstOrThrow: <W extends WithArg<DM, REL, T> = {}>(args?: QueryArgsOf<DM, REL, T> & {
303
+ }) => Promise<LoadWith<DM, REL, T, W, S> | null>;
304
+ findFirstOrThrow: <W extends WithArg<DM, REL, T> = {}, S extends ReadonlyArray<keyof DM[T] & string> | undefined = undefined>(args?: QueryArgsOf<DM, REL, T> & {
305
+ select?: S;
267
306
  with?: W;
268
- }) => Promise<LoadWith<DM, REL, T, W>>;
269
- findMany: <W extends WithArg<DM, REL, T> = {}>(args?: QueryArgsOf<DM, REL, T> & {
307
+ }) => Promise<LoadWith<DM, REL, T, W, S>>;
308
+ findMany: <W extends WithArg<DM, REL, T> = {}, S extends ReadonlyArray<keyof DM[T] & string> | undefined = undefined>(args?: QueryArgsOf<DM, REL, T> & {
309
+ select?: S;
270
310
  with?: W;
271
- }) => Promise<QueryPage<LoadWith<DM, REL, T, W>>>;
311
+ }) => Promise<QueryPage<LoadWith<DM, REL, T, W, S>>>;
272
312
  get: (id: Id<string & T>) => Promise<DM[T] | null>;
273
313
  /**
274
314
  * Group rows by the named keys and apply `agg` per group (defaults to
@@ -299,6 +339,11 @@ interface TableReaderFacade<DM, REL extends Record<keyof DM, object>, RANK exten
299
339
  }
300
340
  /** Read-write typed table accessor exposed on `MutationCtx.db.&lt;table>` / `ActionCtx.db.&lt;table>`. */
301
341
  interface TableWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends Record<keyof DM, object>, RANK extends Record<keyof DM, string>, SEARCH extends Record<keyof DM, string>, T extends keyof DM> extends TableReaderFacade<DM, REL, RANK, SEARCH, T> {
342
+ /**
343
+ * Delete a row by id. On a `.softDelete()` table this flips the marker column
344
+ * (and cascades as a soft delete) instead of removing the row; use
345
+ * {@link TableWriterFacade.hardDelete} to force physical removal.
346
+ */
302
347
  delete: (id: Id<string & T>) => Promise<void>;
303
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. */
304
349
  deleteMany: (ids: ReadonlyArray<Id<string & T>>, options?: {
@@ -306,7 +351,21 @@ interface TableWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends
306
351
  }) => Promise<{
307
352
  deleted: number;
308
353
  }>;
309
- insert: (values: IM[T]) => Promise<Id<string & T>>;
354
+ /** Physically remove a row (and physically cascade `onDelete`), bypassing `.softDelete()`. Same as `delete()` on a non-soft table. */
355
+ hardDelete: (id: Id<string & T>) => Promise<void>;
356
+ /**
357
+ * Insert a document, returning its minted id. With `{ skipDuplicates: true }`
358
+ * a UNIQUE-constraint breach resolves to `null` (the row already exists)
359
+ * instead of throwing — the return type widens to `Id | null` on that overload.
360
+ */
361
+ insert: {
362
+ (values: IM[T], options: {
363
+ skipDuplicates: true;
364
+ }): Promise<Id<string & T> | null>;
365
+ (values: IM[T], options?: {
366
+ skipDuplicates?: boolean;
367
+ }): Promise<Id<string & T>>;
368
+ };
310
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. */
311
370
  insertMany: (values: ReadonlyArray<IM[T]>, options?: {
312
371
  limit?: number;
@@ -320,9 +379,38 @@ interface TableWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends
320
379
  limit?: number;
321
380
  }) => Promise<void>;
322
381
  replace: (id: Id<string & T>, values: IM[T]) => Promise<void>;
382
+ /** Un-soft-delete a row by id: clears the `.softDelete()` marker so list reads see it again. Throws on a non-soft table. */
383
+ restore: (id: Id<string & T>) => Promise<void>;
384
+ /**
385
+ * Insert when no existing row matches `target`, otherwise patch the match with
386
+ * `update` (defaulting to `create`). `target` names a `.unique()` column (or a
387
+ * tuple) used to look it up. Returns the row id and whether it was `created`.
388
+ * Composes `findFirst` + `insert`/`patch`, so RLS gates each step.
389
+ */
390
+ upsert: (args: {
391
+ create: IM[T];
392
+ target: UpsertTargetOf<DM, T>;
393
+ update?: Partial<IM[T]>;
394
+ }) => Promise<{
395
+ created: boolean;
396
+ id: Id<string & T>;
397
+ }>;
398
+ /** Sequential {@link TableWriterFacade.upsert} over many rows sharing one `target`; one result per input row, in order. */
399
+ upsertMany: (args: {
400
+ rows: ReadonlyArray<{
401
+ create: IM[T];
402
+ update?: Partial<IM[T]>;
403
+ }>;
404
+ target: UpsertTargetOf<DM, T>;
405
+ }) => Promise<{
406
+ created: boolean;
407
+ id: Id<string & T>;
408
+ }[]>;
323
409
  }
410
+ /** Conflict target for `upsert`/`upsertMany`: one column of table `T`, or a tuple of them. */
411
+ type UpsertTargetOf<DM, T extends keyof DM> = ReadonlyArray<keyof DM[T] & string> | (keyof DM[T] & string);
324
412
  /** Per-table read facade — `ctx.db.&lt;table>` on a `QueryCtx`. */
325
413
  type DatabaseReaderFacade<DM, REL extends Record<keyof DM, object>, RANK extends Record<keyof DM, string>, SEARCH extends Record<keyof DM, string>> = { readonly [T in keyof DM]: TableReaderFacade<DM, REL, RANK, SEARCH, T> };
326
414
  /** Per-table read-write facade — `ctx.db.&lt;table>` on a `MutationCtx` / `ActionCtx`. */
327
415
  type DatabaseWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends Record<keyof DM, object>, RANK extends Record<keyof DM, string>, SEARCH extends Record<keyof DM, string>> = { readonly [T in keyof DM]: TableWriterFacade<DM, IM, REL, RANK, SEARCH, T> };
328
- export { AggregateOp, DatabaseReaderFacade, DatabaseWriterFacade, GroupByEntry, Id, LoadWith, ManyRelationWhere, OneRelationWhere, OrderBy, QueryArgs, QueryArgsOf, QueryPage, RankPage, RankResult, RestrictableQueryOptions, RestrictableQueryOptionsOf, SearchFilterBuilder, SearchReader, TableAggregateOptions, TableAggregateOptionsOf, TableGroupByOptions, TableGroupByOptionsOf, TableRankOptions, TableRankPageOptions, TableReaderFacade, TableWriterFacade, Where, WhereOf, WhereOperators, WithArg };
416
+ export { AggregateOp, DatabaseReaderFacade, DatabaseWriterFacade, GroupByEntry, Id, LoadWith, ManyRelationWhere, OneRelationWhere, OrderBy, QueryArgs, QueryArgsOf, QueryPage, RankPage, RankResult, RestrictableQueryOptions, RestrictableQueryOptionsOf, SearchFilterBuilder, SearchReader, TableAggregateOptions, TableAggregateOptionsOf, TableGroupByOptions, TableGroupByOptionsOf, TableRankOptions, TableRankPageOptions, TableReaderFacade, TableWriterFacade, UpsertTargetOf, Where, WhereOf, WhereOperators, WithArg };
@@ -49,6 +49,13 @@ interface QueryArgs<TDocument> {
49
49
  cursor?: null | string;
50
50
  limit?: number;
51
51
  orderBy?: OrderBy<TDocument>[];
52
+ /**
53
+ * Project each returned row down to these columns (plus the system fields
54
+ * `_id`/`_creationTime`, always retained). Trims wire payload for wide rows;
55
+ * relations requested via `with` are still attached. Reactivity is unaffected —
56
+ * the engine still reads the whole row to track dependencies.
57
+ */
58
+ select?: ReadonlyArray<keyof TDocument & string>;
52
59
  where?: Where<TDocument>;
53
60
  }
54
61
  /**
@@ -97,6 +104,12 @@ type WhereOf<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = Rel
97
104
  /** {@link QueryArgs} with the relation-aware {@link WhereOf} `where` typing. */
98
105
  interface QueryArgsOf<DM, REL extends Record<keyof DM, object>, T extends keyof DM> {
99
106
  cursor?: null | string;
107
+ /**
108
+ * Include soft-deleted rows (`.softDelete()` tables only). Default hides them;
109
+ * `true` returns deleted rows alongside live ones. No effect on a table
110
+ * without `.softDelete()`.
111
+ */
112
+ includeDeleted?: boolean;
100
113
  limit?: number;
101
114
  orderBy?: OrderBy<DM[T]>[];
102
115
  where?: WhereOf<DM, REL, T>;
@@ -110,11 +123,15 @@ interface QueryPage<TDocument> {
110
123
  type NestedWithArgument<WK> = WK extends {
111
124
  with: infer NW;
112
125
  } ? NW : {};
126
+ /** The nested `select` tuple inside a relation's with-value, or `undefined` (no projection). */
127
+ type NestedSelectArgument<WK> = WK extends {
128
+ select: infer S;
129
+ } ? S : undefined;
113
130
  /**
114
131
  * The `with` argument for table `T`: each relation can be `true` (load with no
115
- * refinements) or an object. `many` relations accept `where`/`orderBy`/`limit`
116
- * plus a nested `with`; `one` relations accept only a nested `with`. The
117
- * reserved `_count` key requests per-relation aggregate counts.
132
+ * refinements) or an object. `many` relations accept `where`/`orderBy`/`limit`/
133
+ * `select` plus a nested `with`; `one` relations accept `select` + a nested
134
+ * `with`. The reserved `_count` key requests per-relation aggregate counts.
118
135
  */
119
136
  type WithArg<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = { [K in keyof REL[T]]?: REL[T][K] extends {
120
137
  __relationKind: "many";
@@ -125,18 +142,24 @@ type WithArg<DM, REL extends Record<keyof DM, object>, T extends keyof DM> = { [
125
142
  __relationKind: "one";
126
143
  __target: infer Target extends keyof DM;
127
144
  } ? boolean | {
145
+ select?: ReadonlyArray<keyof DM[Target] & string>;
128
146
  with?: WithArg<DM, REL, Target>;
129
147
  } : never } & {
130
148
  _count?: { [K in keyof REL[T]]?: true };
131
149
  };
132
- /** Resolve a single relation descriptor + its with-value to the loaded type. */
150
+ /**
151
+ * Resolve a single relation descriptor + its with-value to the loaded type,
152
+ * threading the nested `select` tuple into the projected child shape (the 5th
153
+ * `LoadWith` arg) so `with: { author: { select: ["name"] } }` narrows the loaded
154
+ * `author` to the selected columns + system fields.
155
+ */
133
156
  type LoadRelation<DM, REL extends Record<keyof DM, object>, R, WK> = R extends {
134
157
  __relationKind: "one";
135
158
  __target: infer Target extends keyof DM;
136
- } ? LoadWith<DM, REL, Target, NestedWithArgument<WK>> | null : R extends {
159
+ } ? LoadWith<DM, REL, Target, NestedWithArgument<WK>, NestedSelectArgument<WK>> | null : R extends {
137
160
  __relationKind: "many";
138
161
  __target: infer Target extends keyof DM;
139
- } ? LoadWith<DM, REL, Target, NestedWithArgument<WK>>[] : never;
162
+ } ? LoadWith<DM, REL, Target, NestedWithArgument<WK>, NestedSelectArgument<WK>>[] : never;
140
163
  /** The relation keys of `W` that were actually requested (not `false`/`undefined`). */
141
164
  type LoadedRelations<DM, REL extends Record<keyof DM, object>, T extends keyof DM, W> = { [K in keyof W as K extends keyof REL[T] ? (W[K] extends false | undefined ? never : K) : never]: K extends keyof REL[T] ? LoadRelation<DM, REL, REL[T][K], W[K]> : never };
142
165
  /** The `_count` projection of `W`, if any. */
@@ -145,8 +168,20 @@ type LoadedCount<W> = W extends {
145
168
  } ? {
146
169
  _count: { [K in keyof C]: number };
147
170
  } : {};
148
- /** `Doc&lt;T>` narrowed to exactly the relations requested in the with-arg `W`. */
149
- type LoadWith<DM, REL extends Record<keyof DM, object>, T extends keyof DM, W> = DM[T] & LoadedCount<W> & LoadedRelations<DM, REL, T, W>;
171
+ /** System columns a `select` projection always retains, so cursors and by-id reuse keep working. */
172
+ type SelectAlwaysKeep<DM, T extends keyof DM> = ("_creationTime" | "_id") & keyof DM[T];
173
+ /**
174
+ * `DM[T]` narrowed to the columns named by a `select` tuple `S` (plus the system
175
+ * fields). `undefined` (the default — no `select`) keeps the full document.
176
+ */
177
+ type ProjectDoc<DM, T extends keyof DM, S> = S extends ReadonlyArray<infer K> ? (K extends keyof DM[T] ? Pick<DM[T], (K & keyof DM[T]) | SelectAlwaysKeep<DM, T>> : DM[T]) : DM[T];
178
+ /**
179
+ * `Doc&lt;T>` narrowed to exactly the relations requested in the with-arg `W` and,
180
+ * when a `select` tuple `S` is supplied, to its projected columns. `S` defaults
181
+ * to `undefined` so the 4-argument form (the codegen-emitted callers) keeps the
182
+ * full document.
183
+ */
184
+ type LoadWith<DM, REL extends Record<keyof DM, object>, T extends keyof DM, W, S = undefined> = LoadedCount<W> & LoadedRelations<DM, REL, T, W> & ProjectDoc<DM, T, S>;
150
185
  /** Reducer applied by an aggregate (`avg`/`count`/`max`/`min`/`sum`). */
151
186
  type AggregateOp = "avg" | "count" | "max" | "min" | "sum";
152
187
  /**
@@ -260,15 +295,20 @@ interface TableReaderFacade<DM, REL extends Record<keyof DM, object>, RANK exten
260
295
  * uses to inject `baseWhere` and `restrictsCounts`.
261
296
  */
262
297
  count: (where?: RestrictableQueryOptionsOf<DM, REL, T> | WhereOf<DM, REL, T>) => Promise<number>;
263
- findFirst: <W extends WithArg<DM, REL, T> = {}>(args?: QueryArgsOf<DM, REL, T> & {
298
+ /** `true` when at least one row matches `where` (any row when omitted). RLS-filtered exactly like `findFirst`. */
299
+ exists: (where?: WhereOf<DM, REL, T>) => Promise<boolean>;
300
+ findFirst: <W extends WithArg<DM, REL, T> = {}, S extends ReadonlyArray<keyof DM[T] & string> | undefined = undefined>(args?: QueryArgsOf<DM, REL, T> & {
301
+ select?: S;
264
302
  with?: W;
265
- }) => Promise<LoadWith<DM, REL, T, W> | null>;
266
- findFirstOrThrow: <W extends WithArg<DM, REL, T> = {}>(args?: QueryArgsOf<DM, REL, T> & {
303
+ }) => Promise<LoadWith<DM, REL, T, W, S> | null>;
304
+ findFirstOrThrow: <W extends WithArg<DM, REL, T> = {}, S extends ReadonlyArray<keyof DM[T] & string> | undefined = undefined>(args?: QueryArgsOf<DM, REL, T> & {
305
+ select?: S;
267
306
  with?: W;
268
- }) => Promise<LoadWith<DM, REL, T, W>>;
269
- findMany: <W extends WithArg<DM, REL, T> = {}>(args?: QueryArgsOf<DM, REL, T> & {
307
+ }) => Promise<LoadWith<DM, REL, T, W, S>>;
308
+ findMany: <W extends WithArg<DM, REL, T> = {}, S extends ReadonlyArray<keyof DM[T] & string> | undefined = undefined>(args?: QueryArgsOf<DM, REL, T> & {
309
+ select?: S;
270
310
  with?: W;
271
- }) => Promise<QueryPage<LoadWith<DM, REL, T, W>>>;
311
+ }) => Promise<QueryPage<LoadWith<DM, REL, T, W, S>>>;
272
312
  get: (id: Id<string & T>) => Promise<DM[T] | null>;
273
313
  /**
274
314
  * Group rows by the named keys and apply `agg` per group (defaults to
@@ -299,6 +339,11 @@ interface TableReaderFacade<DM, REL extends Record<keyof DM, object>, RANK exten
299
339
  }
300
340
  /** Read-write typed table accessor exposed on `MutationCtx.db.&lt;table>` / `ActionCtx.db.&lt;table>`. */
301
341
  interface TableWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends Record<keyof DM, object>, RANK extends Record<keyof DM, string>, SEARCH extends Record<keyof DM, string>, T extends keyof DM> extends TableReaderFacade<DM, REL, RANK, SEARCH, T> {
342
+ /**
343
+ * Delete a row by id. On a `.softDelete()` table this flips the marker column
344
+ * (and cascades as a soft delete) instead of removing the row; use
345
+ * {@link TableWriterFacade.hardDelete} to force physical removal.
346
+ */
302
347
  delete: (id: Id<string & T>) => Promise<void>;
303
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. */
304
349
  deleteMany: (ids: ReadonlyArray<Id<string & T>>, options?: {
@@ -306,7 +351,21 @@ interface TableWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends
306
351
  }) => Promise<{
307
352
  deleted: number;
308
353
  }>;
309
- insert: (values: IM[T]) => Promise<Id<string & T>>;
354
+ /** Physically remove a row (and physically cascade `onDelete`), bypassing `.softDelete()`. Same as `delete()` on a non-soft table. */
355
+ hardDelete: (id: Id<string & T>) => Promise<void>;
356
+ /**
357
+ * Insert a document, returning its minted id. With `{ skipDuplicates: true }`
358
+ * a UNIQUE-constraint breach resolves to `null` (the row already exists)
359
+ * instead of throwing — the return type widens to `Id | null` on that overload.
360
+ */
361
+ insert: {
362
+ (values: IM[T], options: {
363
+ skipDuplicates: true;
364
+ }): Promise<Id<string & T> | null>;
365
+ (values: IM[T], options?: {
366
+ skipDuplicates?: boolean;
367
+ }): Promise<Id<string & T>>;
368
+ };
310
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. */
311
370
  insertMany: (values: ReadonlyArray<IM[T]>, options?: {
312
371
  limit?: number;
@@ -320,9 +379,38 @@ interface TableWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends
320
379
  limit?: number;
321
380
  }) => Promise<void>;
322
381
  replace: (id: Id<string & T>, values: IM[T]) => Promise<void>;
382
+ /** Un-soft-delete a row by id: clears the `.softDelete()` marker so list reads see it again. Throws on a non-soft table. */
383
+ restore: (id: Id<string & T>) => Promise<void>;
384
+ /**
385
+ * Insert when no existing row matches `target`, otherwise patch the match with
386
+ * `update` (defaulting to `create`). `target` names a `.unique()` column (or a
387
+ * tuple) used to look it up. Returns the row id and whether it was `created`.
388
+ * Composes `findFirst` + `insert`/`patch`, so RLS gates each step.
389
+ */
390
+ upsert: (args: {
391
+ create: IM[T];
392
+ target: UpsertTargetOf<DM, T>;
393
+ update?: Partial<IM[T]>;
394
+ }) => Promise<{
395
+ created: boolean;
396
+ id: Id<string & T>;
397
+ }>;
398
+ /** Sequential {@link TableWriterFacade.upsert} over many rows sharing one `target`; one result per input row, in order. */
399
+ upsertMany: (args: {
400
+ rows: ReadonlyArray<{
401
+ create: IM[T];
402
+ update?: Partial<IM[T]>;
403
+ }>;
404
+ target: UpsertTargetOf<DM, T>;
405
+ }) => Promise<{
406
+ created: boolean;
407
+ id: Id<string & T>;
408
+ }[]>;
323
409
  }
410
+ /** Conflict target for `upsert`/`upsertMany`: one column of table `T`, or a tuple of them. */
411
+ type UpsertTargetOf<DM, T extends keyof DM> = ReadonlyArray<keyof DM[T] & string> | (keyof DM[T] & string);
324
412
  /** Per-table read facade — `ctx.db.&lt;table>` on a `QueryCtx`. */
325
413
  type DatabaseReaderFacade<DM, REL extends Record<keyof DM, object>, RANK extends Record<keyof DM, string>, SEARCH extends Record<keyof DM, string>> = { readonly [T in keyof DM]: TableReaderFacade<DM, REL, RANK, SEARCH, T> };
326
414
  /** Per-table read-write facade — `ctx.db.&lt;table>` on a `MutationCtx` / `ActionCtx`. */
327
415
  type DatabaseWriterFacade<DM, IM extends Record<keyof DM, object>, REL extends Record<keyof DM, object>, RANK extends Record<keyof DM, string>, SEARCH extends Record<keyof DM, string>> = { readonly [T in keyof DM]: TableWriterFacade<DM, IM, REL, RANK, SEARCH, T> };
328
- export { AggregateOp, DatabaseReaderFacade, DatabaseWriterFacade, GroupByEntry, Id, LoadWith, ManyRelationWhere, OneRelationWhere, OrderBy, QueryArgs, QueryArgsOf, QueryPage, RankPage, RankResult, RestrictableQueryOptions, RestrictableQueryOptionsOf, SearchFilterBuilder, SearchReader, TableAggregateOptions, TableAggregateOptionsOf, TableGroupByOptions, TableGroupByOptionsOf, TableRankOptions, TableRankPageOptions, TableReaderFacade, TableWriterFacade, Where, WhereOf, WhereOperators, WithArg };
416
+ export { AggregateOp, DatabaseReaderFacade, DatabaseWriterFacade, GroupByEntry, Id, LoadWith, ManyRelationWhere, OneRelationWhere, OrderBy, QueryArgs, QueryArgsOf, QueryPage, RankPage, RankResult, RestrictableQueryOptions, RestrictableQueryOptionsOf, SearchFilterBuilder, SearchReader, TableAggregateOptions, TableAggregateOptionsOf, TableGroupByOptions, TableGroupByOptionsOf, TableRankOptions, TableRankPageOptions, TableReaderFacade, TableWriterFacade, UpsertTargetOf, Where, WhereOf, WhereOperators, WithArg };