@lunora/server 1.0.0-alpha.2 → 1.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -325,21 +325,6 @@ declare class LunoraError extends Error {
325
325
  constructor(code: LunoraErrorCode, message?: string);
326
326
  }
327
327
  /**
328
- * The per-table `ctx.db` accessor (the `ctx.db.messages.findMany(...)` form) and
329
- * the kitcn-style `ctx.orm` namespace, as plain runtime helpers. This is the ONE
330
- * source of truth for the facade shape, shared by two callers so they can never
331
- * drift (a drift here is security-relevant — a facade accessor the RLS
332
- * middleware forgot to re-bind would read around policy). `@lunora/codegen`
333
- * emits `ctx.db`/`ctx.orm` by calling these over the raw shard writer (and the
334
- * D1 `globalDb` writer for `.global()` tables); the RLS middleware re-binds the
335
- * policy tables by calling them over the policy-enforcing wrapped writer.
336
- *
337
- * `bindTableFacade(writer, table)` pins `tableName` on the structural writer so
338
- * callers address rows by id (`get`/`delete`/`patch`/`replace`) or by the bound
339
- * table (everything else). The binding is identical regardless of which writer
340
- * is passed — that's the whole point.
341
- */
342
- /**
343
328
  * Minimal structural writer the facade binds over. Declared with **method**
344
329
  * syntax (not arrow properties) so a more-specifically-typed writer — both
345
330
  * `@lunora/do`'s `DatabaseWriterLike` and the RLS middleware's wrapped writer —
@@ -349,7 +334,9 @@ declare class LunoraError extends Error {
349
334
  interface FacadeWriterLike {
350
335
  aggregate(tableName: string, options: unknown): Promise<unknown>;
351
336
  count(tableName: string, where?: unknown): Promise<number>;
352
- delete(id: string, expectedTable?: string): Promise<void>;
337
+ delete(id: string, expectedTable?: string, options?: {
338
+ hard?: boolean;
339
+ }): Promise<void>;
353
340
  deleteMany?(ids: ReadonlyArray<string>, options?: {
354
341
  limit?: number;
355
342
  }, expectedTable?: string): Promise<{
@@ -377,6 +364,7 @@ interface FacadeWriterLike {
377
364
  rank(tableName: string, indexName: string, options: unknown): Promise<unknown>;
378
365
  rankPage(tableName: string, indexName: string, options?: unknown): Promise<unknown>;
379
366
  replace(id: string, document: Record<string, unknown>, expectedTable?: string): Promise<void>;
367
+ restore?(id: string, expectedTable?: string): Promise<void>;
380
368
  }
381
369
  /** The per-table accessor object returned for the `ctx.db` table form. */
382
370
  interface FacadeEntry {
@@ -388,12 +376,16 @@ interface FacadeEntry {
388
376
  }) => Promise<{
389
377
  deleted: number;
390
378
  }>;
379
+ /** `true` when at least one row matches `where` (or any row exists when omitted). Honors RLS like `findFirst`. */
380
+ exists: (where?: unknown) => Promise<boolean>;
391
381
  findFirst: (args?: unknown) => Promise<unknown>;
392
382
  findFirstOrThrow: (args?: unknown) => Promise<unknown>;
393
383
  findMany: (args?: unknown) => Promise<unknown>;
394
384
  get: (id: string) => Promise<unknown>;
395
385
  groupBy: (options: unknown) => Promise<unknown>;
396
- insert: (document: Record<string, unknown>) => Promise<string>;
386
+ /** Physically remove a row (and physically cascade), bypassing `.softDelete()`. */
387
+ hardDelete: (id: string) => Promise<void>;
388
+ insert: (document: Record<string, unknown>, options?: FacadeInsertOptions) => Promise<null | string>;
397
389
  insertMany: (documents: ReadonlyArray<Record<string, unknown>>, options?: {
398
390
  limit?: number;
399
391
  }) => Promise<string[]>;
@@ -407,8 +399,47 @@ interface FacadeEntry {
407
399
  rank: (indexName: string, options: unknown) => Promise<unknown>;
408
400
  rankPage: (indexName: string, options?: unknown) => Promise<unknown>;
409
401
  replace: (id: string, document: Record<string, unknown>) => Promise<void>;
402
+ /** Un-soft-delete a row: clears the `.softDelete()` marker (by-id, so it reaches a row list reads hide). */
403
+ restore: (id: string) => Promise<void>;
404
+ /** Insert when no row matches `target`, else patch the match. Composes `findFirst` + `insert`/`patch`, so RLS applies to each step. */
405
+ upsert: (args: UpsertArgs) => Promise<UpsertResult>;
406
+ /** Sequential `upsert` over many rows sharing one `target`; returns one result per input row in order. */
407
+ upsertMany: (args: UpsertManyArgs) => Promise<UpsertResult[]>;
410
408
  withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => unknown;
411
409
  }
410
+ /** Options accepted by the per-table `insert` accessor. */
411
+ interface FacadeInsertOptions {
412
+ /**
413
+ * When `true`, a UNIQUE-constraint breach is swallowed: the insert becomes a
414
+ * silent no-op and resolves to `null` instead of throwing a `CONFLICT`. Any
415
+ * other error still propagates. Mirrors better-drizzle's `create({ skipDuplicates })`.
416
+ */
417
+ skipDuplicates?: boolean;
418
+ }
419
+ /** The conflict target for `upsert`/`upsertMany`: one field name or a tuple of them. */
420
+ type UpsertTarget = ReadonlyArray<string> | string;
421
+ /** Argument to the per-table `upsert` accessor. */
422
+ interface UpsertArgs {
423
+ /** Document inserted when no existing row matches the `target`. */
424
+ create: Record<string, unknown>;
425
+ /** Field(s) — typically a `.unique()` column or unique index — used to look up an existing row. */
426
+ target: UpsertTarget;
427
+ /** Patch applied when an existing row matches the `target`. Defaults to `create`. */
428
+ update?: Record<string, unknown>;
429
+ }
430
+ /** Result of an `upsert`: the row's id and whether it was freshly inserted (`true`) or updated (`false`). */
431
+ interface UpsertResult {
432
+ created: boolean;
433
+ id: string;
434
+ }
435
+ /** Argument to the per-table `upsertMany` accessor — a shared `target` plus per-row create/update payloads. */
436
+ interface UpsertManyArgs {
437
+ rows: ReadonlyArray<{
438
+ create: Record<string, unknown>;
439
+ update?: Record<string, unknown>;
440
+ }>;
441
+ target: UpsertTarget;
442
+ }
412
443
  /**
413
444
  * Bind a structural writer to one table, producing its `ctx.db` table accessor.
414
445
  *
@@ -426,7 +457,7 @@ declare const bindTableFacade: (writer: FacadeWriterLike, tableName: string) =>
426
457
  interface OrmLike {
427
458
  delete: (table: string, id: string) => Promise<void>;
428
459
  insert: (table: string) => {
429
- values: (document: Record<string, unknown>) => Promise<string>;
460
+ values: (document: Record<string, unknown>) => Promise<null | string>;
430
461
  };
431
462
  query: Record<string, FacadeEntry>;
432
463
  replace: (table: string, id: string) => {
@@ -1168,6 +1199,22 @@ interface TableBuilder<Shape extends Record<string, Validator> = Record<string,
1168
1199
  }) => TableBuilder<Shape>;
1169
1200
  /** Route storage by the named field — one DO per distinct value. */
1170
1201
  shardBy: (field: keyof Shape & string) => TableBuilder<Shape>;
1202
+ /**
1203
+ * Turn on soft delete. Adds a nullable timestamp column (`options.field`,
1204
+ * default `deletedAt`) and changes `ctx.db.&lt;table>.delete()` to **set** it
1205
+ * instead of removing the row; `onDelete: "cascade"` children are recursively
1206
+ * soft-deleted too. **List reads** (`findMany`/`findFirst`/`query()`/`count`/
1207
+ * `aggregate`/relation loads) then hide soft-deleted rows unless they pass
1208
+ * `includeDeleted: true`; by-id `get`/`patch`/`replace` and the new
1209
+ * `restore()` still address the row directly. `hardDelete()` physically
1210
+ * removes it (cascading as a real delete). Note: `includeDeleted` is a read
1211
+ * scope, not access control — anyone who can run the read can set it; a unique
1212
+ * index still rejects a new row that collides with a soft-deleted one (the row
1213
+ * physically persists).
1214
+ */
1215
+ softDelete: (options?: {
1216
+ field?: string;
1217
+ }) => TableBuilder<Shape>;
1171
1218
  /** Declare named lifecycle triggers fired inline within the write path. */
1172
1219
  triggers: (build: (t: TriggerBuilder<Shape>) => Record<string, TriggerDefinition>) => TableBuilder<Shape>;
1173
1220
  /** Declare a vector index over a single text field on this table. */
@@ -1190,7 +1237,7 @@ interface VectorIndexOptions {
1190
1237
  * Build a table definition. Returned object is both the table definition (for
1191
1238
  * `defineSchema`) and a fluent builder for indexes + sharding metadata.
1192
1239
  */
1193
- declare const defineTable: <Shape extends Record<string, Validator>>(shape: Shape) => TableBuilder<Shape>;
1240
+ declare const defineTable: <Shape extends Record<string, Validator>>(inputShape: Shape) => TableBuilder<Shape>;
1194
1241
  /**
1195
1242
  * Declare a standalone vector index (DSL Shape B). Pass the returned value in
1196
1243
  * the `vectorIndexes` map of {@link defineSchema} when the source is derived
@@ -1562,7 +1609,9 @@ interface DatabaseWriterLike {
1562
1609
  */
1563
1610
  aggregate: (tableName: string, options: AggregateArgs) => Promise<null | number>;
1564
1611
  count: (tableName: string, whereOrArgs?: CountArgs | WhereInput) => Promise<number>;
1565
- delete: (id: string, expectedTable?: string) => Promise<void>;
1612
+ delete: (id: string, expectedTable?: string, options?: {
1613
+ hard?: boolean;
1614
+ }) => Promise<void>;
1566
1615
  deleteMany: (ids: ReadonlyArray<string>, options?: {
1567
1616
  limit?: number;
1568
1617
  }, expectedTable?: string) => Promise<{
@@ -1633,6 +1682,7 @@ interface DatabaseWriterLike {
1633
1682
  */
1634
1683
  rankPage: (tableName: string, indexName: string, options?: RankPageArgs) => Promise<QueryPage>;
1635
1684
  replace: (id: string, document: Record<string, unknown>, expectedTable?: string) => Promise<void>;
1685
+ restore?: (id: string, expectedTable?: string) => Promise<void>;
1636
1686
  }
1637
1687
  /**
1638
1688
  * What a procedure's `ctx.db` must structurally satisfy for the middleware
package/dist/index.mjs CHANGED
@@ -2,21 +2,21 @@ export { default as asBucketStorage } from './packem_shared/asBucketStorage-Cnxd
2
2
  export { initLunora } from './packem_shared/initLunora-CATvPsVt.mjs';
3
3
  export { LunoraEnvError, defineEnv, redactSecrets } from './packem_shared/LunoraEnvError-DjFkpkSP.mjs';
4
4
  export { LunoraError } from './packem_shared/LunoraError-DhggBJZF.mjs';
5
- export { bindOrm, bindTableFacade } from './packem_shared/bindOrm-DCuyr46L.mjs';
5
+ export { bindOrm, bindTableFacade } from './packem_shared/bindOrm-Ce57S3N9.mjs';
6
6
  export { httpAction, httpRoute, httpRouter, serveStorageObject } from './packem_shared/httpAction-B7FYUEgr.mjs';
7
7
  export { onConnect, onDisconnect } from './packem_shared/onConnect-CIPXKPyw.mjs';
8
8
  export { defineMigration } from './packem_shared/defineMigration-CAJLr6fx.mjs';
9
9
  export { composePluginMiddleware, defineComponent, definePlugin, defineSchemaExtension, installPlugins, mergeSchemaExtension } from './packem_shared/composePluginMiddleware-Ck5_TUO8.mjs';
10
- export { PRESENCE_DEFAULT_TTL_MS, PRESENCE_TABLE, definePresence, presenceExtension } from './packem_shared/PRESENCE_DEFAULT_TTL_MS-BZYd5-uo.mjs';
10
+ export { PRESENCE_DEFAULT_TTL_MS, PRESENCE_TABLE, definePresence, presenceExtension } from './packem_shared/PRESENCE_DEFAULT_TTL_MS-UQuUI5sV.mjs';
11
11
  export { protectPublic } from './packem_shared/protectPublic-BjFkQ_Or.mjs';
12
- export { defineAggregateIndex, defineRankIndex, defineSchema, defineTable, defineVectorIndex } from './packem_shared/defineAggregateIndex-DxSso0rH.mjs';
12
+ export { defineAggregateIndex, defineRankIndex, defineSchema, defineTable, defineVectorIndex } from './packem_shared/defineAggregateIndex-B20MIOmj.mjs';
13
13
  export { anyApi } from './types.mjs';
14
14
  export { cronJobs } from '@lunora/scheduler';
15
15
  export { ValidationError, v } from '@lunora/values';
16
16
  export { createPolicyDsl, definePermission, definePolicies, definePolicy, defineRole } from './packem_shared/createPolicyDsl-De67zPDS.mjs';
17
17
  export { defineStorageRule, defineStorageRules } from './packem_shared/defineStorageRule-qu0mpilX.mjs';
18
- export { mask } from './packem_shared/mask-Jc84C_hK.mjs';
19
- export { rls } from './packem_shared/rls-BDKRbMCA.mjs';
18
+ export { mask } from './packem_shared/mask-eCUYOwhd.mjs';
19
+ export { rls } from './packem_shared/rls-Bi9HiyDC.mjs';
20
20
  export { storageRules } from './packem_shared/storageRules-4a30FSpI.mjs';
21
21
 
22
22
  const VERSION = "0.0.0";
@@ -3,7 +3,7 @@ import { initLunora } from './initLunora-CATvPsVt.mjs';
3
3
  import { LunoraError } from './LunoraError-DhggBJZF.mjs';
4
4
  import { onDisconnect } from './onConnect-CIPXKPyw.mjs';
5
5
  import { defineSchemaExtension, defineComponent } from './composePluginMiddleware-Ck5_TUO8.mjs';
6
- import { defineTable } from './defineAggregateIndex-DxSso0rH.mjs';
6
+ import { defineTable } from './defineAggregateIndex-B20MIOmj.mjs';
7
7
 
8
8
  const DEFAULT_TTL_MS = 3e4;
9
9
  const MAX_DATA_BYTES = 4096;
@@ -1,4 +1,42 @@
1
+ const isUniqueConflict = (error) => typeof error === "object" && error !== null && error.code === "CONFLICT" && error.kind === "unique";
2
+ const buildUpsertWhere = (tableName, target, create) => {
3
+ const fields = typeof target === "string" ? [target] : target;
4
+ if (fields.length === 0) {
5
+ throw new Error(`ctx.db.${tableName}.upsert: "target" must name at least one field`);
6
+ }
7
+ const where = {};
8
+ for (const field of fields) {
9
+ if (!(field in create)) {
10
+ throw new Error(`ctx.db.${tableName}.upsert: target field "${field}" is missing from the create document`);
11
+ }
12
+ where[field] = create[field];
13
+ }
14
+ return where;
15
+ };
1
16
  const bindTableFacade = (writer, tableName) => {
17
+ const insert = async (document, options) => {
18
+ if (options?.skipDuplicates !== true) {
19
+ return writer.insert(tableName, document);
20
+ }
21
+ try {
22
+ return await writer.insert(tableName, document);
23
+ } catch (error) {
24
+ if (isUniqueConflict(error)) {
25
+ return null;
26
+ }
27
+ throw error;
28
+ }
29
+ };
30
+ const upsert = async ({ create, target, update }) => {
31
+ const where = buildUpsertWhere(tableName, target, create);
32
+ const existing = await writer.findFirst(tableName, { where });
33
+ if (existing && typeof existing["_id"] === "string") {
34
+ await writer.patch(existing["_id"], update ?? create, tableName);
35
+ return { created: false, id: existing["_id"] };
36
+ }
37
+ const id = await writer.insert(tableName, create);
38
+ return { created: true, id };
39
+ };
2
40
  return {
3
41
  aggregate: (options) => writer.aggregate(tableName, options),
4
42
  count: (where) => writer.count(tableName, where),
@@ -14,12 +52,17 @@ const bindTableFacade = (writer, tableName) => {
14
52
  }
15
53
  return writer.deleteMany(ids, options, tableName);
16
54
  },
55
+ // `exists` reuses `findFirst` (RLS-filtered, indexed when a `.withIndex`-able
56
+ // `where` is supplied) and only asks whether a row came back — no count scan.
57
+ exists: async (where) => await writer.findFirst(tableName, where === void 0 ? void 0 : { where }) !== null,
17
58
  findFirst: (args) => writer.findFirst(tableName, args),
18
59
  findFirstOrThrow: (args) => writer.findFirstOrThrow(tableName, args),
19
60
  findMany: (args) => writer.findMany(tableName, args),
20
61
  get: (id) => writer.get(id, tableName),
21
62
  groupBy: (options) => writer.groupBy(tableName, options),
22
- insert: (document) => writer.insert(tableName, document),
63
+ // Physical removal — bypasses `.softDelete()`. RLS gates it as a delete.
64
+ hardDelete: (id) => writer.delete(id, tableName, { hard: true }),
65
+ insert,
23
66
  insertMany: (documents, options) => {
24
67
  if (writer.insertMany === void 0) {
25
68
  throw new Error(`ctx.db.${tableName}.insertMany is unavailable: this writer has no batch insert`);
@@ -42,6 +85,20 @@ const bindTableFacade = (writer, tableName) => {
42
85
  rank: (indexName, options) => writer.rank(tableName, indexName, options),
43
86
  rankPage: (indexName, options) => writer.rankPage(tableName, indexName, options),
44
87
  replace: (id, document) => writer.replace(id, document, tableName),
88
+ restore: async (id) => {
89
+ if (!writer.restore) {
90
+ throw new Error(`ctx.db.${tableName}.restore is unavailable: this writer has no restore (is the table .softDelete()?)`);
91
+ }
92
+ await writer.restore(id, tableName);
93
+ },
94
+ upsert,
95
+ upsertMany: async ({ rows, target }) => {
96
+ const results = [];
97
+ for (const row of rows) {
98
+ results.push(await upsert({ create: row.create, target, update: row.update }));
99
+ }
100
+ return results;
101
+ },
45
102
  withSearchIndex: (indexName, search) => writer.query(tableName).withSearchIndex(indexName, search)
46
103
  };
47
104
  };
@@ -1,4 +1,4 @@
1
- import { isOrWrapsFromValidator } from '@lunora/values';
1
+ import { isOrWrapsFromValidator, v } from '@lunora/values';
2
2
  import { mergeSchemaExtension } from './composePluginMiddleware-Ck5_TUO8.mjs';
3
3
 
4
4
  const relationBuilder = {
@@ -22,7 +22,8 @@ const createTriggerBuilder = () => {
22
22
  beforeUpdate: (handler) => makeTrigger("before", "update", handler)
23
23
  };
24
24
  };
25
- const defineTable = (shape) => {
25
+ const defineTable = (inputShape) => {
26
+ const shape = { ...inputShape };
26
27
  for (const [columnName, validator] of Object.entries(shape)) {
27
28
  if (isOrWrapsFromValidator(validator)) {
28
29
  throw new Error(`defineTable: column "${columnName}" uses v.from() which is args-only — table columns need a concrete v.* type`);
@@ -39,6 +40,7 @@ const defineTable = (shape) => {
39
40
  let shardMode = { kind: "root" };
40
41
  let isExternallyManaged = false;
41
42
  let isPublic = false;
43
+ let softDelete;
42
44
  const builder = {
43
45
  aggregateIndex(name, options) {
44
46
  const op = options?.op ?? "count";
@@ -132,6 +134,17 @@ const defineTable = (shape) => {
132
134
  get shardMode() {
133
135
  return shardMode;
134
136
  },
137
+ get softDeleteMode() {
138
+ return softDelete;
139
+ },
140
+ softDelete(options) {
141
+ const field = options?.field ?? "deletedAt";
142
+ softDelete = { field };
143
+ if (!(field in shape)) {
144
+ shape[field] = v.optional(v.number().nullable());
145
+ }
146
+ return builder;
147
+ },
135
148
  get triggerMap() {
136
149
  return triggers;
137
150
  },
@@ -1,5 +1,5 @@
1
1
  import { LunoraError } from './LunoraError-DhggBJZF.mjs';
2
- import { bindTableFacade, bindOrm } from './bindOrm-DCuyr46L.mjs';
2
+ import { bindTableFacade, bindOrm } from './bindOrm-Ce57S3N9.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-DhggBJZF.mjs';
2
- import { bindTableFacade, bindOrm } from './bindOrm-DCuyr46L.mjs';
2
+ import { bindTableFacade, bindOrm } from './bindOrm-Ce57S3N9.mjs';
3
3
 
4
4
  const DEFAULT_BATCH_LIMIT = 500;
5
5
  const assertBatchLimit = (count, limit, op) => {
@@ -324,7 +324,7 @@ const wrapDatabase = (base, raw, perTable, context) => {
324
324
  restrictsCounts: (args.restrictsCounts ?? false) || restricts
325
325
  });
326
326
  },
327
- delete: (id, expectedTable) => gateById(id, "delete", (writer) => writer.delete(id, expectedTable), void 0, expectedTable),
327
+ delete: (id, expectedTable, options) => gateById(id, "delete", (writer) => writer.delete(id, expectedTable, options), void 0, expectedTable),
328
328
  async deleteMany(ids, options, expectedTable) {
329
329
  assertBatchLimit(ids.length, options?.limit, "deleteMany");
330
330
  for (const id of ids) {
@@ -441,6 +441,22 @@ const wrapDatabase = (base, raw, perTable, context) => {
441
441
  }
442
442
  return reader.filter((document) => matchesWhere(document, baseWhere));
443
443
  },
444
+ // Restore clears the soft-delete marker — a by-id un-delete, gated as an
445
+ // "update" (the policy that governs patch). No post-image check: the
446
+ // writer owns the marker field, so we only enforce the pre-image USING.
447
+ restore: (id, expectedTable) => gateById(
448
+ id,
449
+ "update",
450
+ async (writer) => {
451
+ const perform = writer.restore ?? raw.restore;
452
+ if (!perform) {
453
+ throw new LunoraError("BAD_REQUEST", "restore is not supported by this writer");
454
+ }
455
+ await perform(id, expectedTable);
456
+ },
457
+ void 0,
458
+ expectedTable
459
+ ),
444
460
  replace: (id, document, expectedTable) => gateById(
445
461
  id,
446
462
  "update",
@@ -1,4 +1,4 @@
1
- import { indexRolePermissions, computeReadBaseWhere, matchesWhere, evaluateWrite, permissionName } from '../packem_shared/rls-BDKRbMCA.mjs';
1
+ import { indexRolePermissions, computeReadBaseWhere, matchesWhere, evaluateWrite, permissionName } from '../packem_shared/rls-Bi9HiyDC.mjs';
2
2
 
3
3
  const expectPolicy = (policies, options = {}) => {
4
4
  const rolePermissions = indexRolePermissions(options.roles);
package/dist/types.d.mts CHANGED
@@ -165,6 +165,20 @@ interface TableDefinition<Shape extends Record<string, Validator> = Record<strin
165
165
  shape: Shape;
166
166
  shardMode: ShardMode;
167
167
  /**
168
+ * Set by `.softDelete()` (named `softDeleteMode`, not `softDelete`, so the
169
+ * data field doesn't collide with the fluent `.softDelete()` builder method —
170
+ * same convention as `shardBy()`/`shardMode`). When present, the table carries
171
+ * a nullable timestamp column (`field`, default `deletedAt`):
172
+ * `ctx.db.&lt;table>.delete()` flips it instead of physically removing the row,
173
+ * and **list reads** (`findMany`/`findFirst`/`query()`/`count`/`aggregate`/
174
+ * relation loads) hide rows whose `field` is set unless
175
+ * `includeDeleted: true` is passed. By-id `get`/`patch`/`replace` and
176
+ * `restore` are unaffected. Absent ⇒ deletes are physical, as before.
177
+ */
178
+ softDeleteMode?: {
179
+ field: string;
180
+ };
181
+ /**
168
182
  * Declared lifecycle triggers keyed by accessor name; empty unless
169
183
  * `.triggers()` was called. Named `triggerMap` (not `triggers`) so the
170
184
  * fluent `.triggers((t) => …)` builder method doesn't collide with this
package/dist/types.d.ts CHANGED
@@ -165,6 +165,20 @@ interface TableDefinition<Shape extends Record<string, Validator> = Record<strin
165
165
  shape: Shape;
166
166
  shardMode: ShardMode;
167
167
  /**
168
+ * Set by `.softDelete()` (named `softDeleteMode`, not `softDelete`, so the
169
+ * data field doesn't collide with the fluent `.softDelete()` builder method —
170
+ * same convention as `shardBy()`/`shardMode`). When present, the table carries
171
+ * a nullable timestamp column (`field`, default `deletedAt`):
172
+ * `ctx.db.&lt;table>.delete()` flips it instead of physically removing the row,
173
+ * and **list reads** (`findMany`/`findFirst`/`query()`/`count`/`aggregate`/
174
+ * relation loads) hide rows whose `field` is set unless
175
+ * `includeDeleted: true` is passed. By-id `get`/`patch`/`replace` and
176
+ * `restore` are unaffected. Absent ⇒ deletes are physical, as before.
177
+ */
178
+ softDeleteMode?: {
179
+ field: string;
180
+ };
181
+ /**
168
182
  * Declared lifecycle triggers keyed by accessor name; empty unless
169
183
  * `.triggers()` was called. Named `triggerMap` (not `triggers`) so the
170
184
  * fluent `.triggers((t) => …)` builder method doesn't collide with this
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/server",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.3",
4
4
  "description": "Server primitives for Lunora: defineSchema, defineTable, query, mutation, and action",
5
5
  "keywords": [
6
6
  "backend",
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@lunora/scheduler": "1.0.0-alpha.1",
66
- "@lunora/values": "1.0.0-alpha.1",
66
+ "@lunora/values": "1.0.0-alpha.2",
67
67
  "drizzle-orm": "^0.45.2",
68
68
  "hono": "^4.12.26"
69
69
  },