@vantreeseba/drizzle-graphql 7.6.0 → 7.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -237,6 +237,43 @@ type RelationResolverFactory = (params: {
237
237
  * both end up loaded in one process.
238
238
  */
239
239
  declare const drizzleExecutorKey: unique symbol;
240
+ /** The write a hook is attached to. `restore` only exists for a soft-deleting table. */
241
+ type WriteOperation = 'insert' | 'update' | 'updateMany' | 'upsert' | 'delete' | 'restore';
242
+ /** What a write hook is handed. See {@link BuildSchemaConfig.onWrite}. */
243
+ type WriteHookPayload = {
244
+ /** Drizzle schema key of the table being written. */
245
+ table: string;
246
+ operation: WriteOperation;
247
+ /** `'before'` runs ahead of the statement, `'after'` once it has returned. */
248
+ position: 'before' | 'after';
249
+ /** Whether the field writes a single row (`createUsersSingle`) or a list. */
250
+ single: boolean;
251
+ /** The field's GraphQL arguments, exactly as the resolver received them. */
252
+ args: any;
253
+ /**
254
+ * The rows the write produced, as the database returned them — before the output mapper,
255
+ * so values are database-shaped. Empty at the `before` position, and on MySQL, whose
256
+ * mutations return no rows.
257
+ */
258
+ rows: any[];
259
+ context: any;
260
+ info: any;
261
+ /** The executor the mutation itself ran on. Throwing from the hook rolls it back. */
262
+ tx: any;
263
+ };
264
+ type WriteHook = (payload: WriteHookPayload) => void | Promise<void>;
265
+ type WriteHookPositions = {
266
+ before?: WriteHook;
267
+ after?: WriteHook;
268
+ };
269
+ type WriteHooks = WriteHook | WriteHookPositions;
270
+ /** Resolved, per-field hooks: `undefined` when the table registered none. */
271
+ type ResolvedWriteHooks = {
272
+ before?: WriteHook;
273
+ after?: WriteHook;
274
+ };
275
+ /** Build-time lookup of a table's hooks, installed on {@link TablePolicies}. */
276
+ type WriteHookFor = (tableName: string, operation: WriteOperation) => ResolvedWriteHooks | undefined;
240
277
  /**
241
278
  * Creates a RelationResolverFactory that generates field-level resolvers for each relation.
242
279
  * Each resolver:
@@ -251,7 +288,7 @@ declare const drizzleExecutorKey: unique symbol;
251
288
  * each dialect builder states it: `'nulls-largest'` for PostgreSQL, `'nulls-smallest'` for
252
289
  * MySQL and SQLite.
253
290
  */
254
- declare const createRelationResolverFactory: (db: any, tables: Record<string, Table>, nullOrdering: NullOrdering, filterCtx?: RelationFilterBase, limits?: LimitPolicyFor, scopes?: ScopeFor) => RelationResolverFactory;
291
+ declare const createRelationResolverFactory: (db: any, tables: Record<string, Table>, nullOrdering: NullOrdering, filterCtx?: RelationFilterBase, limits?: LimitPolicyFor, policies?: TablePolicies) => RelationResolverFactory;
255
292
  /** {@link BuildSchemaConfig.limits} resolved for one table. `undefined` means no policy. */
256
293
  type ResolvedLimitPolicy = {
257
294
  defaultLimit: number | undefined;
@@ -321,6 +358,48 @@ declare const extractFilters: <TTable extends Table>(table: TTable, tableName: s
321
358
  type ScopeHook<TContext = any> = (context: TContext, table: any) => SQL | Record<string, any> | undefined | null;
322
359
  /** Build-time lookup: the scope configured for a table, if any. */
323
360
  type ScopeFor = (tableName: string) => ScopeHook | undefined;
361
+ /**
362
+ * One table's soft-delete convention, resolved against the real column at build time.
363
+ *
364
+ * `nullable` picks the shape of the predicate. A nullable column marks a row deleted by
365
+ * holding a value at all (`deletedAt IS NOT NULL`), which is the common timestamp form; a
366
+ * non-nullable one marks it by holding `marker` (`isDeleted = true`), so the column has to
367
+ * have a constant that means "deleted" and another that means "not deleted".
368
+ */
369
+ type SoftDeleteInfo = {
370
+ /** Property name of the column on the drizzle table — also the key on an aliased proxy. */
371
+ columnName: string;
372
+ column: Column;
373
+ nullable: boolean;
374
+ /** Evaluated per delete, so a timestamp form stamps the moment of the delete. */
375
+ writeDeleted: () => any;
376
+ /** Written by the restore mutation. */
377
+ writeRestored: any;
378
+ /** Non-nullable form only: the constant that means "this row is deleted". */
379
+ marker?: any;
380
+ };
381
+ /** Build-time lookup: the soft-delete convention of a table, if it declares one. */
382
+ type SoftDeleteFor = (tableName: string) => SoftDeleteInfo | undefined;
383
+ /** Produces one column's value from the GraphQL context. See `BuildSchemaConfig.contextValues`. */
384
+ type ContextValueHook<TContext = any> = (context: TContext) => any;
385
+ /** Build-time lookup: the context-derived columns of a table, keyed by column property name. */
386
+ type ContextValuesFor = (tableName: string) => Record<string, ContextValueHook> | undefined;
387
+ /**
388
+ * The two request-time policies a generated resolver applies, passed to the dialect
389
+ * generators as one value so a table's read scope and its server-owned columns travel
390
+ * together. Both stay `undefined` when nothing is configured, and every call site checks
391
+ * before doing any work — an unconfigured build generates exactly the SQL it did before.
392
+ */
393
+ type TablePolicies = {
394
+ /** See `BuildSchemaConfig.scope`. */
395
+ scope?: ScopeFor;
396
+ /** See `BuildSchemaConfig.contextValues`. */
397
+ contextValues?: ContextValuesFor;
398
+ /** See `BuildSchemaConfig.softDelete`. */
399
+ softDelete?: SoftDeleteFor;
400
+ /** See `BuildSchemaConfig.onWrite`. */
401
+ onWrite?: WriteHookFor;
402
+ };
324
403
  /**
325
404
  * Where a dialect's default sort places NULLs relative to non-NULL values:
326
405
  * - `nulls-largest` — PostgreSQL: NULLs sort as the largest values (last in ASC, first in DESC).
@@ -719,8 +798,14 @@ type SchemaFeatures = {
719
798
  * Each relation field offers `create` and `connect`; the update input adds `disconnect`
720
799
  * and, for a to-many relation, `set` (replace the whole set). `disconnect` and `set` are
721
800
  * generated only when the foreign key is nullable — there is no way to detach a row whose
722
- * key is NOT NULL — and many-to-many (`.through()`) relations are not written through at
723
- * all. Nesting is one level deep: the row a nested `create` inserts takes columns only.
801
+ * key is NOT NULL. Nesting is one level deep: the row a nested `create` inserts takes
802
+ * columns only.
803
+ *
804
+ * A many-to-many (`.through()`) relation is written as junction rows: `connect` on the
805
+ * create input, and `connect` / `disconnect` / `set` on the update input, all
806
+ * unconditional, since unlinking deletes a junction row rather than nulling a column.
807
+ * It offers no `create` — inserting a row and linking it are two writes to two tables —
808
+ * and `set: []` clears the relation.
724
809
  *
725
810
  * The whole tree runs in one transaction (a savepoint when the request already carries
726
811
  * one), and a column a relation fills in (`authorId`, when `author: { create: … }` is
@@ -863,6 +948,38 @@ type ScopeConfig<TContext = any> = Record<string, RowScope<TContext>>;
863
948
  * request context, keyed by Drizzle schema key and then by column property name.
864
949
  */
865
950
  type ContextValuesConfig<TContext = any> = Record<string, Record<string, (context: TContext) => any>>;
951
+ /**
952
+ * How one table marks a row deleted: the column's property name, or that name plus the values
953
+ * written on delete and on restore. See {@link BuildSchemaConfig.softDelete}.
954
+ */
955
+ type SoftDeleteColumn = string | {
956
+ /** Property name of the column that marks a row deleted. */
957
+ column: string;
958
+ /**
959
+ * Written when a row is deleted. A function is evaluated per delete. Defaults by column
960
+ * type: `new Date()` for a timestamp, an ISO string for text, `Date.now()` for a number,
961
+ * `true` for a boolean. Required, as a constant, when the column is NOT NULL and not a
962
+ * boolean — the predicate that hides marked rows has to compare against it.
963
+ */
964
+ deletedValue?: any;
965
+ /**
966
+ * Written when a row is restored. Defaults to `null` for a nullable column and `false`
967
+ * for a NOT NULL boolean; required for any other NOT NULL column.
968
+ */
969
+ restoredValue?: any;
970
+ };
971
+
972
+ /**
973
+ * Write hooks, either applied to every table (a bare function, or an object naming the
974
+ * positions) or registered per table by Drizzle schema key. See
975
+ * {@link BuildSchemaConfig.onWrite}.
976
+ */
977
+ type OnWriteConfig = WriteHooks | Record<string, WriteHooks>;
978
+ /**
979
+ * Per-table soft-delete declarations, either keyed by Drizzle schema key or as a rule applied
980
+ * to every table. See {@link BuildSchemaConfig.softDelete}.
981
+ */
982
+ type SoftDeleteConfig = Record<string, SoftDeleteColumn> | ((table: any, tableName: string) => SoftDeleteColumn | undefined | null);
866
983
  /**
867
984
  * {@link BuildSchemaConfig.exclude} — tables and columns to keep out of the generated schema.
868
985
  *
@@ -921,6 +1038,11 @@ type BuildSchemaConfig = {
921
1038
  update?: string;
922
1039
  /** Prefix for upsert mutations (e.g., 'users' -> 'upsertUsers') */
923
1040
  upsert?: string;
1041
+ /**
1042
+ * Prefix for the restore mutations of a soft-deleting table (e.g., 'users' ->
1043
+ * 'restoreUsers'). Only used when {@link BuildSchemaConfig.softDelete} names the table.
1044
+ */
1045
+ restore?: string;
924
1046
  };
925
1047
  /**
926
1048
  * Customizes query name suffixes for generated GraphQL operations.
@@ -1151,6 +1273,83 @@ type BuildSchemaConfig = {
1151
1273
  * name), the same keying {@link BuildSchemaConfig.exclude} uses.
1152
1274
  */
1153
1275
  contextValues?: ContextValuesConfig;
1276
+ /**
1277
+ * Tables that mark rows deleted instead of removing them.
1278
+ *
1279
+ * ```ts
1280
+ * buildSchema(db, {
1281
+ * softDelete: {
1282
+ * users: 'deletedAt', // nullable timestamp: stamped on delete
1283
+ * posts: { column: 'isDeleted', deletedValue: true, restoredValue: false },
1284
+ * },
1285
+ * });
1286
+ *
1287
+ * // or as a convention across the whole schema:
1288
+ * buildSchema(db, { softDelete: (table) => ('deletedAt' in table ? 'deletedAt' : undefined) });
1289
+ * ```
1290
+ *
1291
+ * For a table that declares one:
1292
+ *
1293
+ * - `delete<Table>` and `delete<Table>Single` issue an `UPDATE` of the marker column
1294
+ * instead of a `DELETE`, and return the rows as they now stand.
1295
+ * - `restore<Table>` and `restore<Table>Single` are generated alongside them, matching only
1296
+ * marked rows and writing the restored value. The prefix is configurable through
1297
+ * {@link BuildSchemaConfig.prefixes}.
1298
+ * - Every generated read hides marked rows: list and single queries, aggregates, `groupBy`,
1299
+ * relation fields on both the eager and the batched path, and relation aggregates. Each
1300
+ * of those fields takes a `deleted: EXCLUDE | INCLUDE | ONLY` argument (default
1301
+ * `EXCLUDE`); `ONLY` is a trash view.
1302
+ * - Every generated write skips them too: an `update`, a `delete`, or a nested `connect`
1303
+ * cannot reach a row that is already marked.
1304
+ * - The marker column is removed from the create and update inputs, so the only ways to set
1305
+ * or clear it are the delete and restore mutations.
1306
+ * - The predicate is ANDed on before {@link BuildSchemaConfig.scope}, and both are ANDed
1307
+ * after the client's `where` — a request can only ever narrow them.
1308
+ *
1309
+ * Two consequences of the pattern itself, which this does not paper over:
1310
+ *
1311
+ * - A marked row still occupies its unique keys, so inserting a row that reuses the natural
1312
+ * key of a deleted one still fails on the constraint.
1313
+ * - An upsert whose conflict target hits a marked row updates nothing on PostgreSQL and
1314
+ * SQLite (the conflict predicate excludes it) but revives it on MySQL, whose
1315
+ * `ON DUPLICATE KEY UPDATE` takes no predicate.
1316
+ *
1317
+ * Marking a parent deleted does not mark its children — a cascade is a schema decision.
1318
+ */
1319
+ softDelete?: SoftDeleteConfig;
1320
+ /**
1321
+ * A hook that runs **inside the mutation's transaction**, so its writes commit or roll back
1322
+ * with the mutation itself — the position an audit row, an outbox row, or a denormalized
1323
+ * counter needs, and the one a `graphql-middleware` wrapper cannot occupy.
1324
+ *
1325
+ * ```ts
1326
+ * buildSchema(db, {
1327
+ * onWrite: {
1328
+ * posts: async ({ table, operation, rows, tx }) => {
1329
+ * await tx.insert(auditLog).values(rows.map((row) => ({ table, operation, rowId: row.id })));
1330
+ * },
1331
+ * },
1332
+ * });
1333
+ * ```
1334
+ *
1335
+ * A bare function is the **`after`** hook — the position that has rows. `{ before, after }`
1336
+ * names them explicitly; `before` receives the field's args ahead of the statement, for a
1337
+ * check that has to read inside the same snapshot. Registering a bare function (or a
1338
+ * positions object) at the top level applies it to every table; anything else is read as a
1339
+ * table map keyed by Drizzle schema key, and an unknown name fails the build.
1340
+ *
1341
+ * - **`tx` is the executor the mutation ran on** — a caller-supplied executor under
1342
+ * `drizzleExecutorKey`, the request's auto-transaction, or, for a field that would
1343
+ * otherwise have run unwrapped, a transaction opened for it because the hook is there.
1344
+ * - **Throwing rolls the mutation back.** That is the point of the position.
1345
+ * - **`rows` are the post-write rows as the database returned them**, before the output
1346
+ * mapper. Empty at the `before` position, and on MySQL, whose mutations return no rows.
1347
+ * - The hook fires for the **mutation field's own table**. Rows a nested write creates in
1348
+ * another table do not fire that table's hook.
1349
+ *
1350
+ * Off by default, and a build without it opens exactly the transactions it did before.
1351
+ */
1352
+ onWrite?: OnWriteConfig;
1154
1353
  /**
1155
1354
  * Tables and columns to leave out of the generated schema entirely.
1156
1355
  *
@@ -1439,7 +1638,7 @@ type DrizzleFieldKind = 'query' | 'mutation' | 'relation' | 'aggregate' | 'type'
1439
1638
  * What the field does. Arity is carried separately by `single`, so `select` covers both
1440
1639
  * the list query and its `…Single` sibling rather than being spelled twice.
1441
1640
  */
1442
- type DrizzleFieldOperation = 'select' | 'insert' | 'update' | 'updateMany' | 'upsert' | 'delete' | 'aggregate' | 'groupBy' | 'relation' | 'relationAggregate';
1641
+ type DrizzleFieldOperation = 'select' | 'insert' | 'update' | 'updateMany' | 'upsert' | 'delete' | 'restore' | 'aggregate' | 'groupBy' | 'relation' | 'relationAggregate';
1443
1642
  /** The argument holding the rows a field is about, when it has one. */
1444
1643
  type DrizzleTargetArg = 'where' | 'values' | 'updates';
1445
1644
  /** `extensions.drizzle` on a generated field. */
@@ -1613,4 +1812,4 @@ declare const resolveSelection: (info: GraphQLResolveInfo | ResolveTree, options
1613
1812
 
1614
1813
  declare const buildSchema: <TDbClient extends AnyDrizzleDB<any>>(db: TDbClient, config?: BuildSchemaConfig) => GeneratedData<TDbClient>;
1615
1814
 
1616
- export { type AggregateResolver, type AnyDrizzleDB, type BuildSchemaConfig, type ColumnDeprecator, type ColumnDescriber, type ColumnDocInfo, type ColumnTypeMapper, type ColumnTypeMapperInfo, type ComplexityConfig, type ContextValuesConfig, type DeleteResolver, type DeleteSingleResolver, type DrizzleExtension, type DrizzleFieldExtension, type DrizzleFieldKind, type DrizzleFieldOperation, type DrizzleTargetArg, type DrizzleTypeExtension, type EnumNameInfo, type EnumNameMapper, type ExtractRelations, type ExtractTableByName, type ExtractTableRelations, type ExtractTables, type FeatureSwitch, type GeneratedData, type GeneratedEntities, type GeneratedInputs, type GeneratedOutputs, GraphQLBigIntString, GraphQLDecimalString, type IdentifiedRows, type InsertArrResolver, type InsertResolver, type LimitsConfig, type MutationReturnlessResult, type MutationsCore, type QueriesCore, type RelationDescriber, type RelationResolverFactory, type ResolveSelectionOptions, type RowScope, type RowScopeFilter, type ScalarOverride, type ScalarOverridesConfig, type SchemaDocs, type SchemaExclusions, type SchemaFeatures, type ScopeConfig, type SelectResolver, type SelectSingleResolver, type SelectionOptions, type TableDescriber, type TableLimitPolicy, type TableNamedRelations, type UpdateManyArgs, type UpdateManyEntry, type UpdateManyResolver, type UpdateResolver, type UpdateSingleResolver, type UpsertArgs, type UpsertArrResolver, type UpsertConflictArgs, type UpsertResolver, buildSchema, createRelationResolverFactory, defaultErrorMapper, drizzleExecutorKey, drizzleExtension, extractFilters, extractOrderBy, extractRelationJoinColumns, identifyRows, isDrizzleFieldExtension, resolveSelection, selectionToWith, singularizeMapper };
1815
+ export { type AggregateResolver, type AnyDrizzleDB, type BuildSchemaConfig, type ColumnDeprecator, type ColumnDescriber, type ColumnDocInfo, type ColumnTypeMapper, type ColumnTypeMapperInfo, type ComplexityConfig, type ContextValuesConfig, type DeleteResolver, type DeleteSingleResolver, type DrizzleExtension, type DrizzleFieldExtension, type DrizzleFieldKind, type DrizzleFieldOperation, type DrizzleTargetArg, type DrizzleTypeExtension, type EnumNameInfo, type EnumNameMapper, type ExtractRelations, type ExtractTableByName, type ExtractTableRelations, type ExtractTables, type FeatureSwitch, type GeneratedData, type GeneratedEntities, type GeneratedInputs, type GeneratedOutputs, GraphQLBigIntString, GraphQLDecimalString, type IdentifiedRows, type InsertArrResolver, type InsertResolver, type LimitsConfig, type MutationReturnlessResult, type MutationsCore, type OnWriteConfig, type QueriesCore, type RelationDescriber, type RelationResolverFactory, type ResolveSelectionOptions, type RowScope, type RowScopeFilter, type ScalarOverride, type ScalarOverridesConfig, type SchemaDocs, type SchemaExclusions, type SchemaFeatures, type ScopeConfig, type SelectResolver, type SelectSingleResolver, type SelectionOptions, type SoftDeleteColumn, type SoftDeleteConfig, type TableDescriber, type TableLimitPolicy, type TableNamedRelations, type UpdateManyArgs, type UpdateManyEntry, type UpdateManyResolver, type UpdateResolver, type UpdateSingleResolver, type UpsertArgs, type UpsertArrResolver, type UpsertConflictArgs, type UpsertResolver, type WriteHook, type WriteHookPayload, type WriteHookPositions, type WriteHooks, type WriteOperation, buildSchema, createRelationResolverFactory, defaultErrorMapper, drizzleExecutorKey, drizzleExtension, extractFilters, extractOrderBy, extractRelationJoinColumns, identifyRows, isDrizzleFieldExtension, resolveSelection, selectionToWith, singularizeMapper };
package/dist/index.d.ts CHANGED
@@ -237,6 +237,43 @@ type RelationResolverFactory = (params: {
237
237
  * both end up loaded in one process.
238
238
  */
239
239
  declare const drizzleExecutorKey: unique symbol;
240
+ /** The write a hook is attached to. `restore` only exists for a soft-deleting table. */
241
+ type WriteOperation = 'insert' | 'update' | 'updateMany' | 'upsert' | 'delete' | 'restore';
242
+ /** What a write hook is handed. See {@link BuildSchemaConfig.onWrite}. */
243
+ type WriteHookPayload = {
244
+ /** Drizzle schema key of the table being written. */
245
+ table: string;
246
+ operation: WriteOperation;
247
+ /** `'before'` runs ahead of the statement, `'after'` once it has returned. */
248
+ position: 'before' | 'after';
249
+ /** Whether the field writes a single row (`createUsersSingle`) or a list. */
250
+ single: boolean;
251
+ /** The field's GraphQL arguments, exactly as the resolver received them. */
252
+ args: any;
253
+ /**
254
+ * The rows the write produced, as the database returned them — before the output mapper,
255
+ * so values are database-shaped. Empty at the `before` position, and on MySQL, whose
256
+ * mutations return no rows.
257
+ */
258
+ rows: any[];
259
+ context: any;
260
+ info: any;
261
+ /** The executor the mutation itself ran on. Throwing from the hook rolls it back. */
262
+ tx: any;
263
+ };
264
+ type WriteHook = (payload: WriteHookPayload) => void | Promise<void>;
265
+ type WriteHookPositions = {
266
+ before?: WriteHook;
267
+ after?: WriteHook;
268
+ };
269
+ type WriteHooks = WriteHook | WriteHookPositions;
270
+ /** Resolved, per-field hooks: `undefined` when the table registered none. */
271
+ type ResolvedWriteHooks = {
272
+ before?: WriteHook;
273
+ after?: WriteHook;
274
+ };
275
+ /** Build-time lookup of a table's hooks, installed on {@link TablePolicies}. */
276
+ type WriteHookFor = (tableName: string, operation: WriteOperation) => ResolvedWriteHooks | undefined;
240
277
  /**
241
278
  * Creates a RelationResolverFactory that generates field-level resolvers for each relation.
242
279
  * Each resolver:
@@ -251,7 +288,7 @@ declare const drizzleExecutorKey: unique symbol;
251
288
  * each dialect builder states it: `'nulls-largest'` for PostgreSQL, `'nulls-smallest'` for
252
289
  * MySQL and SQLite.
253
290
  */
254
- declare const createRelationResolverFactory: (db: any, tables: Record<string, Table>, nullOrdering: NullOrdering, filterCtx?: RelationFilterBase, limits?: LimitPolicyFor, scopes?: ScopeFor) => RelationResolverFactory;
291
+ declare const createRelationResolverFactory: (db: any, tables: Record<string, Table>, nullOrdering: NullOrdering, filterCtx?: RelationFilterBase, limits?: LimitPolicyFor, policies?: TablePolicies) => RelationResolverFactory;
255
292
  /** {@link BuildSchemaConfig.limits} resolved for one table. `undefined` means no policy. */
256
293
  type ResolvedLimitPolicy = {
257
294
  defaultLimit: number | undefined;
@@ -321,6 +358,48 @@ declare const extractFilters: <TTable extends Table>(table: TTable, tableName: s
321
358
  type ScopeHook<TContext = any> = (context: TContext, table: any) => SQL | Record<string, any> | undefined | null;
322
359
  /** Build-time lookup: the scope configured for a table, if any. */
323
360
  type ScopeFor = (tableName: string) => ScopeHook | undefined;
361
+ /**
362
+ * One table's soft-delete convention, resolved against the real column at build time.
363
+ *
364
+ * `nullable` picks the shape of the predicate. A nullable column marks a row deleted by
365
+ * holding a value at all (`deletedAt IS NOT NULL`), which is the common timestamp form; a
366
+ * non-nullable one marks it by holding `marker` (`isDeleted = true`), so the column has to
367
+ * have a constant that means "deleted" and another that means "not deleted".
368
+ */
369
+ type SoftDeleteInfo = {
370
+ /** Property name of the column on the drizzle table — also the key on an aliased proxy. */
371
+ columnName: string;
372
+ column: Column;
373
+ nullable: boolean;
374
+ /** Evaluated per delete, so a timestamp form stamps the moment of the delete. */
375
+ writeDeleted: () => any;
376
+ /** Written by the restore mutation. */
377
+ writeRestored: any;
378
+ /** Non-nullable form only: the constant that means "this row is deleted". */
379
+ marker?: any;
380
+ };
381
+ /** Build-time lookup: the soft-delete convention of a table, if it declares one. */
382
+ type SoftDeleteFor = (tableName: string) => SoftDeleteInfo | undefined;
383
+ /** Produces one column's value from the GraphQL context. See `BuildSchemaConfig.contextValues`. */
384
+ type ContextValueHook<TContext = any> = (context: TContext) => any;
385
+ /** Build-time lookup: the context-derived columns of a table, keyed by column property name. */
386
+ type ContextValuesFor = (tableName: string) => Record<string, ContextValueHook> | undefined;
387
+ /**
388
+ * The two request-time policies a generated resolver applies, passed to the dialect
389
+ * generators as one value so a table's read scope and its server-owned columns travel
390
+ * together. Both stay `undefined` when nothing is configured, and every call site checks
391
+ * before doing any work — an unconfigured build generates exactly the SQL it did before.
392
+ */
393
+ type TablePolicies = {
394
+ /** See `BuildSchemaConfig.scope`. */
395
+ scope?: ScopeFor;
396
+ /** See `BuildSchemaConfig.contextValues`. */
397
+ contextValues?: ContextValuesFor;
398
+ /** See `BuildSchemaConfig.softDelete`. */
399
+ softDelete?: SoftDeleteFor;
400
+ /** See `BuildSchemaConfig.onWrite`. */
401
+ onWrite?: WriteHookFor;
402
+ };
324
403
  /**
325
404
  * Where a dialect's default sort places NULLs relative to non-NULL values:
326
405
  * - `nulls-largest` — PostgreSQL: NULLs sort as the largest values (last in ASC, first in DESC).
@@ -719,8 +798,14 @@ type SchemaFeatures = {
719
798
  * Each relation field offers `create` and `connect`; the update input adds `disconnect`
720
799
  * and, for a to-many relation, `set` (replace the whole set). `disconnect` and `set` are
721
800
  * generated only when the foreign key is nullable — there is no way to detach a row whose
722
- * key is NOT NULL — and many-to-many (`.through()`) relations are not written through at
723
- * all. Nesting is one level deep: the row a nested `create` inserts takes columns only.
801
+ * key is NOT NULL. Nesting is one level deep: the row a nested `create` inserts takes
802
+ * columns only.
803
+ *
804
+ * A many-to-many (`.through()`) relation is written as junction rows: `connect` on the
805
+ * create input, and `connect` / `disconnect` / `set` on the update input, all
806
+ * unconditional, since unlinking deletes a junction row rather than nulling a column.
807
+ * It offers no `create` — inserting a row and linking it are two writes to two tables —
808
+ * and `set: []` clears the relation.
724
809
  *
725
810
  * The whole tree runs in one transaction (a savepoint when the request already carries
726
811
  * one), and a column a relation fills in (`authorId`, when `author: { create: … }` is
@@ -863,6 +948,38 @@ type ScopeConfig<TContext = any> = Record<string, RowScope<TContext>>;
863
948
  * request context, keyed by Drizzle schema key and then by column property name.
864
949
  */
865
950
  type ContextValuesConfig<TContext = any> = Record<string, Record<string, (context: TContext) => any>>;
951
+ /**
952
+ * How one table marks a row deleted: the column's property name, or that name plus the values
953
+ * written on delete and on restore. See {@link BuildSchemaConfig.softDelete}.
954
+ */
955
+ type SoftDeleteColumn = string | {
956
+ /** Property name of the column that marks a row deleted. */
957
+ column: string;
958
+ /**
959
+ * Written when a row is deleted. A function is evaluated per delete. Defaults by column
960
+ * type: `new Date()` for a timestamp, an ISO string for text, `Date.now()` for a number,
961
+ * `true` for a boolean. Required, as a constant, when the column is NOT NULL and not a
962
+ * boolean — the predicate that hides marked rows has to compare against it.
963
+ */
964
+ deletedValue?: any;
965
+ /**
966
+ * Written when a row is restored. Defaults to `null` for a nullable column and `false`
967
+ * for a NOT NULL boolean; required for any other NOT NULL column.
968
+ */
969
+ restoredValue?: any;
970
+ };
971
+
972
+ /**
973
+ * Write hooks, either applied to every table (a bare function, or an object naming the
974
+ * positions) or registered per table by Drizzle schema key. See
975
+ * {@link BuildSchemaConfig.onWrite}.
976
+ */
977
+ type OnWriteConfig = WriteHooks | Record<string, WriteHooks>;
978
+ /**
979
+ * Per-table soft-delete declarations, either keyed by Drizzle schema key or as a rule applied
980
+ * to every table. See {@link BuildSchemaConfig.softDelete}.
981
+ */
982
+ type SoftDeleteConfig = Record<string, SoftDeleteColumn> | ((table: any, tableName: string) => SoftDeleteColumn | undefined | null);
866
983
  /**
867
984
  * {@link BuildSchemaConfig.exclude} — tables and columns to keep out of the generated schema.
868
985
  *
@@ -921,6 +1038,11 @@ type BuildSchemaConfig = {
921
1038
  update?: string;
922
1039
  /** Prefix for upsert mutations (e.g., 'users' -> 'upsertUsers') */
923
1040
  upsert?: string;
1041
+ /**
1042
+ * Prefix for the restore mutations of a soft-deleting table (e.g., 'users' ->
1043
+ * 'restoreUsers'). Only used when {@link BuildSchemaConfig.softDelete} names the table.
1044
+ */
1045
+ restore?: string;
924
1046
  };
925
1047
  /**
926
1048
  * Customizes query name suffixes for generated GraphQL operations.
@@ -1151,6 +1273,83 @@ type BuildSchemaConfig = {
1151
1273
  * name), the same keying {@link BuildSchemaConfig.exclude} uses.
1152
1274
  */
1153
1275
  contextValues?: ContextValuesConfig;
1276
+ /**
1277
+ * Tables that mark rows deleted instead of removing them.
1278
+ *
1279
+ * ```ts
1280
+ * buildSchema(db, {
1281
+ * softDelete: {
1282
+ * users: 'deletedAt', // nullable timestamp: stamped on delete
1283
+ * posts: { column: 'isDeleted', deletedValue: true, restoredValue: false },
1284
+ * },
1285
+ * });
1286
+ *
1287
+ * // or as a convention across the whole schema:
1288
+ * buildSchema(db, { softDelete: (table) => ('deletedAt' in table ? 'deletedAt' : undefined) });
1289
+ * ```
1290
+ *
1291
+ * For a table that declares one:
1292
+ *
1293
+ * - `delete<Table>` and `delete<Table>Single` issue an `UPDATE` of the marker column
1294
+ * instead of a `DELETE`, and return the rows as they now stand.
1295
+ * - `restore<Table>` and `restore<Table>Single` are generated alongside them, matching only
1296
+ * marked rows and writing the restored value. The prefix is configurable through
1297
+ * {@link BuildSchemaConfig.prefixes}.
1298
+ * - Every generated read hides marked rows: list and single queries, aggregates, `groupBy`,
1299
+ * relation fields on both the eager and the batched path, and relation aggregates. Each
1300
+ * of those fields takes a `deleted: EXCLUDE | INCLUDE | ONLY` argument (default
1301
+ * `EXCLUDE`); `ONLY` is a trash view.
1302
+ * - Every generated write skips them too: an `update`, a `delete`, or a nested `connect`
1303
+ * cannot reach a row that is already marked.
1304
+ * - The marker column is removed from the create and update inputs, so the only ways to set
1305
+ * or clear it are the delete and restore mutations.
1306
+ * - The predicate is ANDed on before {@link BuildSchemaConfig.scope}, and both are ANDed
1307
+ * after the client's `where` — a request can only ever narrow them.
1308
+ *
1309
+ * Two consequences of the pattern itself, which this does not paper over:
1310
+ *
1311
+ * - A marked row still occupies its unique keys, so inserting a row that reuses the natural
1312
+ * key of a deleted one still fails on the constraint.
1313
+ * - An upsert whose conflict target hits a marked row updates nothing on PostgreSQL and
1314
+ * SQLite (the conflict predicate excludes it) but revives it on MySQL, whose
1315
+ * `ON DUPLICATE KEY UPDATE` takes no predicate.
1316
+ *
1317
+ * Marking a parent deleted does not mark its children — a cascade is a schema decision.
1318
+ */
1319
+ softDelete?: SoftDeleteConfig;
1320
+ /**
1321
+ * A hook that runs **inside the mutation's transaction**, so its writes commit or roll back
1322
+ * with the mutation itself — the position an audit row, an outbox row, or a denormalized
1323
+ * counter needs, and the one a `graphql-middleware` wrapper cannot occupy.
1324
+ *
1325
+ * ```ts
1326
+ * buildSchema(db, {
1327
+ * onWrite: {
1328
+ * posts: async ({ table, operation, rows, tx }) => {
1329
+ * await tx.insert(auditLog).values(rows.map((row) => ({ table, operation, rowId: row.id })));
1330
+ * },
1331
+ * },
1332
+ * });
1333
+ * ```
1334
+ *
1335
+ * A bare function is the **`after`** hook — the position that has rows. `{ before, after }`
1336
+ * names them explicitly; `before` receives the field's args ahead of the statement, for a
1337
+ * check that has to read inside the same snapshot. Registering a bare function (or a
1338
+ * positions object) at the top level applies it to every table; anything else is read as a
1339
+ * table map keyed by Drizzle schema key, and an unknown name fails the build.
1340
+ *
1341
+ * - **`tx` is the executor the mutation ran on** — a caller-supplied executor under
1342
+ * `drizzleExecutorKey`, the request's auto-transaction, or, for a field that would
1343
+ * otherwise have run unwrapped, a transaction opened for it because the hook is there.
1344
+ * - **Throwing rolls the mutation back.** That is the point of the position.
1345
+ * - **`rows` are the post-write rows as the database returned them**, before the output
1346
+ * mapper. Empty at the `before` position, and on MySQL, whose mutations return no rows.
1347
+ * - The hook fires for the **mutation field's own table**. Rows a nested write creates in
1348
+ * another table do not fire that table's hook.
1349
+ *
1350
+ * Off by default, and a build without it opens exactly the transactions it did before.
1351
+ */
1352
+ onWrite?: OnWriteConfig;
1154
1353
  /**
1155
1354
  * Tables and columns to leave out of the generated schema entirely.
1156
1355
  *
@@ -1439,7 +1638,7 @@ type DrizzleFieldKind = 'query' | 'mutation' | 'relation' | 'aggregate' | 'type'
1439
1638
  * What the field does. Arity is carried separately by `single`, so `select` covers both
1440
1639
  * the list query and its `…Single` sibling rather than being spelled twice.
1441
1640
  */
1442
- type DrizzleFieldOperation = 'select' | 'insert' | 'update' | 'updateMany' | 'upsert' | 'delete' | 'aggregate' | 'groupBy' | 'relation' | 'relationAggregate';
1641
+ type DrizzleFieldOperation = 'select' | 'insert' | 'update' | 'updateMany' | 'upsert' | 'delete' | 'restore' | 'aggregate' | 'groupBy' | 'relation' | 'relationAggregate';
1443
1642
  /** The argument holding the rows a field is about, when it has one. */
1444
1643
  type DrizzleTargetArg = 'where' | 'values' | 'updates';
1445
1644
  /** `extensions.drizzle` on a generated field. */
@@ -1613,4 +1812,4 @@ declare const resolveSelection: (info: GraphQLResolveInfo | ResolveTree, options
1613
1812
 
1614
1813
  declare const buildSchema: <TDbClient extends AnyDrizzleDB<any>>(db: TDbClient, config?: BuildSchemaConfig) => GeneratedData<TDbClient>;
1615
1814
 
1616
- export { type AggregateResolver, type AnyDrizzleDB, type BuildSchemaConfig, type ColumnDeprecator, type ColumnDescriber, type ColumnDocInfo, type ColumnTypeMapper, type ColumnTypeMapperInfo, type ComplexityConfig, type ContextValuesConfig, type DeleteResolver, type DeleteSingleResolver, type DrizzleExtension, type DrizzleFieldExtension, type DrizzleFieldKind, type DrizzleFieldOperation, type DrizzleTargetArg, type DrizzleTypeExtension, type EnumNameInfo, type EnumNameMapper, type ExtractRelations, type ExtractTableByName, type ExtractTableRelations, type ExtractTables, type FeatureSwitch, type GeneratedData, type GeneratedEntities, type GeneratedInputs, type GeneratedOutputs, GraphQLBigIntString, GraphQLDecimalString, type IdentifiedRows, type InsertArrResolver, type InsertResolver, type LimitsConfig, type MutationReturnlessResult, type MutationsCore, type QueriesCore, type RelationDescriber, type RelationResolverFactory, type ResolveSelectionOptions, type RowScope, type RowScopeFilter, type ScalarOverride, type ScalarOverridesConfig, type SchemaDocs, type SchemaExclusions, type SchemaFeatures, type ScopeConfig, type SelectResolver, type SelectSingleResolver, type SelectionOptions, type TableDescriber, type TableLimitPolicy, type TableNamedRelations, type UpdateManyArgs, type UpdateManyEntry, type UpdateManyResolver, type UpdateResolver, type UpdateSingleResolver, type UpsertArgs, type UpsertArrResolver, type UpsertConflictArgs, type UpsertResolver, buildSchema, createRelationResolverFactory, defaultErrorMapper, drizzleExecutorKey, drizzleExtension, extractFilters, extractOrderBy, extractRelationJoinColumns, identifyRows, isDrizzleFieldExtension, resolveSelection, selectionToWith, singularizeMapper };
1815
+ export { type AggregateResolver, type AnyDrizzleDB, type BuildSchemaConfig, type ColumnDeprecator, type ColumnDescriber, type ColumnDocInfo, type ColumnTypeMapper, type ColumnTypeMapperInfo, type ComplexityConfig, type ContextValuesConfig, type DeleteResolver, type DeleteSingleResolver, type DrizzleExtension, type DrizzleFieldExtension, type DrizzleFieldKind, type DrizzleFieldOperation, type DrizzleTargetArg, type DrizzleTypeExtension, type EnumNameInfo, type EnumNameMapper, type ExtractRelations, type ExtractTableByName, type ExtractTableRelations, type ExtractTables, type FeatureSwitch, type GeneratedData, type GeneratedEntities, type GeneratedInputs, type GeneratedOutputs, GraphQLBigIntString, GraphQLDecimalString, type IdentifiedRows, type InsertArrResolver, type InsertResolver, type LimitsConfig, type MutationReturnlessResult, type MutationsCore, type OnWriteConfig, type QueriesCore, type RelationDescriber, type RelationResolverFactory, type ResolveSelectionOptions, type RowScope, type RowScopeFilter, type ScalarOverride, type ScalarOverridesConfig, type SchemaDocs, type SchemaExclusions, type SchemaFeatures, type ScopeConfig, type SelectResolver, type SelectSingleResolver, type SelectionOptions, type SoftDeleteColumn, type SoftDeleteConfig, type TableDescriber, type TableLimitPolicy, type TableNamedRelations, type UpdateManyArgs, type UpdateManyEntry, type UpdateManyResolver, type UpdateResolver, type UpdateSingleResolver, type UpsertArgs, type UpsertArrResolver, type UpsertConflictArgs, type UpsertResolver, type WriteHook, type WriteHookPayload, type WriteHookPositions, type WriteHooks, type WriteOperation, buildSchema, createRelationResolverFactory, defaultErrorMapper, drizzleExecutorKey, drizzleExtension, extractFilters, extractOrderBy, extractRelationJoinColumns, identifyRows, isDrizzleFieldExtension, resolveSelection, selectionToWith, singularizeMapper };