Skip to content

Commit 18e894f

Browse files
PAMulliganclaude
andauthored
fix(soft-delete): make softDelete() callable with a typed db client (#104)
softDelete()'s `db` parameter was typed `PgDatabase<PgQueryResultHKT>`, which pins the schema generics to their `Record<string, never>` defaults. Because that schema generic is invariant, passing a real, schema-typed Drizzle client (e.g. `PostgresJsDatabase<typeof schema>`) fails to compile with TS2379 — so the documented `await softDelete(db, table, id)` pattern could not actually be used in a generated project. Widen the signature to infer the query-result and schema generics, which accepts any concrete Drizzle client while staying fully backward compatible (broadening a constraint never breaks existing callers). Add templates/snippets/node/src/routes/users.ts: a typechecked Hono route that exercises softDelete(), softDeleteFilter(), and includeDeletedQuerySchema against the real db client, so the typecheck suite now covers an actual soft-delete invocation and this regression cannot return unnoticed. Refs #53 (soft-delete feature landed in #103). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e797033 commit 18e894f

2 files changed

Lines changed: 75 additions & 3 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Hono } from 'hono';
2+
import { eq } from 'drizzle-orm';
3+
import { db } from '../db/client';
4+
import { users } from '../db/schema';
5+
import {
6+
includeDeletedQuerySchema,
7+
softDelete,
8+
softDeleteFilter,
9+
} from '../db/soft-delete';
10+
11+
/**
12+
* Example resource routes demonstrating the soft-delete pattern from
13+
* `../db/soft-delete`. Wire them into the app where a database is available:
14+
*
15+
* import { usersRoutes } from './routes/users';
16+
* app.route('/users', usersRoutes);
17+
*
18+
* Soft-delete rules of thumb:
19+
* - Reads filter out deleted rows with `softDeleteFilter` (or `notDeleted`).
20+
* - `?include_deleted=true` lets admin callers see soft-deleted rows; the
21+
* `includeDeletedQuerySchema` parses that flag into a boolean.
22+
* - DELETE stamps `deleted_at` via `softDelete` instead of removing the row.
23+
*/
24+
export const usersRoutes = new Hono()
25+
// GET /users — list active users. Admins pass ?include_deleted=true for all.
26+
.get('/', async (c) => {
27+
const { include_deleted } = includeDeletedQuerySchema.parse(c.req.query());
28+
const rows = await db
29+
.select()
30+
.from(users)
31+
.where(softDeleteFilter(users, include_deleted));
32+
return c.json({ users: rows, total: rows.length });
33+
})
34+
35+
// GET /users/:id — fetch one user. Soft-deleted rows return 404 unless
36+
// ?include_deleted=true is supplied.
37+
.get('/:id', async (c) => {
38+
const id = c.req.param('id');
39+
const { include_deleted } = includeDeletedQuerySchema.parse(c.req.query());
40+
const rows = await db
41+
.select()
42+
.from(users)
43+
.where(softDeleteFilter(users, include_deleted, eq(users.id, id)));
44+
const user = rows[0];
45+
if (!user) {
46+
return c.json({ error: 'User not found' }, 404);
47+
}
48+
return c.json(user);
49+
})
50+
51+
// DELETE /users/:id — soft delete: stamps deleted_at instead of removing.
52+
// `softDelete` returns the number of rows transitioned to deleted (0 or 1),
53+
// so a missing or already-deleted row resolves to a 404.
54+
.delete('/:id', async (c) => {
55+
const id = c.req.param('id');
56+
const affected = await softDelete(db, users, id);
57+
if (affected === 0) {
58+
return c.json({ error: 'User not found' }, 404);
59+
}
60+
return c.json({ message: 'User deleted' });
61+
});

templates/snippets/shared/src/db/soft-delete.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { and, eq, isNull, type SQL } from 'drizzle-orm';
1+
import {
2+
and,
3+
eq,
4+
isNull,
5+
type SQL,
6+
type TablesRelationalConfig,
7+
} from 'drizzle-orm';
28
import type {
39
PgColumn,
410
PgDatabase,
@@ -92,8 +98,13 @@ export function softDeleteFilter(
9298
* const affected = await softDelete(db, users, id);
9399
* if (affected === 0) throw new NotFoundError('User', id);
94100
*/
95-
export async function softDelete<TTable extends SoftDeleteTable>(
96-
db: PgDatabase<PgQueryResultHKT>,
101+
export async function softDelete<
102+
TTable extends SoftDeleteTable,
103+
TQueryResult extends PgQueryResultHKT,
104+
TFullSchema extends Record<string, unknown>,
105+
TSchema extends TablesRelationalConfig,
106+
>(
107+
db: PgDatabase<TQueryResult, TFullSchema, TSchema>,
97108
table: TTable,
98109
id: string | number,
99110
): Promise<number> {

0 commit comments

Comments
 (0)