@kequtech/tenon-orm 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/agent-guide.md +7 -5
- package/docs/human-guide.md +23 -23
- package/docs/reference.md +17 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ try {
|
|
|
29
29
|
const email = 'reader@example.test';
|
|
30
30
|
const user = await db.query(
|
|
31
31
|
sql<{ id: number; email: string }>`
|
|
32
|
-
SELECT id, email FROM public.
|
|
32
|
+
SELECT id, email FROM public.users WHERE email = ${email}
|
|
33
33
|
`,
|
|
34
34
|
{ returns: 'oneOrNone' },
|
|
35
35
|
);
|
package/docs/agent-guide.md
CHANGED
|
@@ -9,6 +9,8 @@ This guide contains the setup and contracts needed for routine implementation; t
|
|
|
9
9
|
|
|
10
10
|
1. Inspect the application's connection setup, migrations, generated types, query modules, and check
|
|
11
11
|
commands. Reuse them. Do not replace a working migration runner just to use Tenon queries.
|
|
12
|
+
For new schemas, prefer plural `snake_case` table names and `snake_case` column names; preserve
|
|
13
|
+
existing schemas' names.
|
|
12
14
|
2. Write straightforward SQL with explicit selected columns. Use separate complete queries when
|
|
13
15
|
variants return different shapes. Introduce fragments only for actual reuse or variable
|
|
14
16
|
composition.
|
|
@@ -47,12 +49,12 @@ or configure `search_path`; CLI `--namespace` does not change runtime query name
|
|
|
47
49
|
|
|
48
50
|
```ts
|
|
49
51
|
import { sql, type QueryClient } from '@kequtech/tenon-orm';
|
|
50
|
-
import type {
|
|
52
|
+
import type { Rows } from './generated-schema.js';
|
|
51
53
|
|
|
52
|
-
type Summary = Pick<
|
|
54
|
+
type Summary = Pick<Rows['users'], 'id' | 'email'>;
|
|
53
55
|
|
|
54
56
|
function userById(client: QueryClient, { id }: { id: number }) {
|
|
55
|
-
return client.query(sql<Summary>`SELECT id, email FROM public.
|
|
57
|
+
return client.query(sql<Summary>`SELECT id, email FROM public.users WHERE id = ${id}`, {
|
|
56
58
|
returns: 'oneOrNone',
|
|
57
59
|
});
|
|
58
60
|
}
|
|
@@ -98,9 +100,9 @@ For example, share a search condition between a page and count query:
|
|
|
98
100
|
|
|
99
101
|
```ts
|
|
100
102
|
const search = sql.fragment`active = ${true} AND email ILIKE ${pattern}`;
|
|
101
|
-
const page = sql<Summary>`SELECT id, email FROM public.
|
|
103
|
+
const page = sql<Summary>`SELECT id, email FROM public.users
|
|
102
104
|
WHERE ${search} ORDER BY id LIMIT ${limit}`;
|
|
103
|
-
const count = sql<{ count: string }>`SELECT count(*) AS count FROM public.
|
|
105
|
+
const count = sql<{ count: string }>`SELECT count(*) AS count FROM public.users
|
|
104
106
|
WHERE ${search}`;
|
|
105
107
|
```
|
|
106
108
|
|
package/docs/human-guide.md
CHANGED
|
@@ -36,8 +36,9 @@ application then closes that pool.
|
|
|
36
36
|
|
|
37
37
|
## Create a table
|
|
38
38
|
|
|
39
|
-
If you already have a schema and migration runner, you can keep them. For
|
|
40
|
-
|
|
39
|
+
If you already have a schema and migration runner, you can keep them. For new schemas, prefer plural
|
|
40
|
+
`snake_case` table names and `snake_case` column names. Tenon preserves your schema's exact names.
|
|
41
|
+
Create a migration:
|
|
41
42
|
|
|
42
43
|
```sh
|
|
43
44
|
npx tenon new create_users
|
|
@@ -47,7 +48,7 @@ Fill the generated files:
|
|
|
47
48
|
|
|
48
49
|
```sql
|
|
49
50
|
-- migrations/0001_create_users.up.sql
|
|
50
|
-
CREATE TABLE public.
|
|
51
|
+
CREATE TABLE public.users (
|
|
51
52
|
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
52
53
|
email text NOT NULL UNIQUE,
|
|
53
54
|
active boolean NOT NULL DEFAULT true
|
|
@@ -56,7 +57,7 @@ CREATE TABLE public."User" (
|
|
|
56
57
|
|
|
57
58
|
```sql
|
|
58
59
|
-- migrations/0001_create_users.down.sql
|
|
59
|
-
DROP TABLE public.
|
|
60
|
+
DROP TABLE public.users;
|
|
60
61
|
```
|
|
61
62
|
|
|
62
63
|
For development, add a script to `package.json` that applies migrations and updates generated types:
|
|
@@ -87,10 +88,10 @@ import { sql } from '@kequtech/tenon-orm';
|
|
|
87
88
|
import { db } from './db.js';
|
|
88
89
|
|
|
89
90
|
const email = 'reader@example.test';
|
|
90
|
-
await db.query(sql`INSERT INTO public.
|
|
91
|
+
await db.query(sql`INSERT INTO public.users (email) VALUES (${email})`);
|
|
91
92
|
const user = await db.query(
|
|
92
93
|
sql<{ id: number; email: string }>`
|
|
93
|
-
SELECT id, email FROM public.
|
|
94
|
+
SELECT id, email FROM public.users WHERE email = ${email}
|
|
94
95
|
`,
|
|
95
96
|
{ returns: 'oneOrNone' },
|
|
96
97
|
);
|
|
@@ -107,8 +108,8 @@ These options do not change your SQL.
|
|
|
107
108
|
Generated table types can help describe projections without repeating every field:
|
|
108
109
|
|
|
109
110
|
```ts
|
|
110
|
-
import type {
|
|
111
|
-
type UserSummary = Pick<
|
|
111
|
+
import type { Rows } from './generated-schema.js';
|
|
112
|
+
type UserSummary = Pick<Rows['users'], 'id' | 'email'>;
|
|
112
113
|
```
|
|
113
114
|
|
|
114
115
|
You can then write `sql<UserSummary>`. A table type describes the full table; a join or aggregate
|
|
@@ -123,15 +124,14 @@ supplied values easy to read:
|
|
|
123
124
|
|
|
124
125
|
```ts
|
|
125
126
|
import { sql, type QueryClient } from '@kequtech/tenon-orm';
|
|
126
|
-
import type {
|
|
127
|
+
import type { Rows } from './generated-schema.js';
|
|
127
128
|
|
|
128
|
-
type UserSummary = Pick<
|
|
129
|
+
type UserSummary = Pick<Rows['users'], 'id' | 'email'>;
|
|
129
130
|
|
|
130
131
|
export function userByEmail(client: QueryClient, { email }: { email: string }) {
|
|
131
|
-
return client.query(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
);
|
|
132
|
+
return client.query(sql<UserSummary>`SELECT id, email FROM public.users WHERE email = ${email}`, {
|
|
133
|
+
returns: 'oneOrNone',
|
|
134
|
+
});
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
const user = await userByEmail(db, { email: 'reader@example.test' });
|
|
@@ -147,7 +147,7 @@ await db.transaction(async (tx) => {
|
|
|
147
147
|
const user = await userByEmail(tx, { email: 'reader@example.test' });
|
|
148
148
|
if (!user) throw new Error('User not found');
|
|
149
149
|
|
|
150
|
-
await tx.query(sql`UPDATE public.
|
|
150
|
+
await tx.query(sql`UPDATE public.users SET active = false WHERE id = ${user.id}`, {
|
|
151
151
|
affectedRows: { exact: 1 },
|
|
152
152
|
});
|
|
153
153
|
});
|
|
@@ -172,11 +172,11 @@ aligned, with `DEFAULT` for missing cells.
|
|
|
172
172
|
```ts
|
|
173
173
|
import type { Rows } from './generated-schema.js';
|
|
174
174
|
|
|
175
|
-
const input: Rows.Insert['
|
|
176
|
-
await db.query(sql`INSERT INTO public.
|
|
175
|
+
const input: Rows.Insert['users'] = { email: 'another@example.test' };
|
|
176
|
+
await db.query(sql`INSERT INTO public.users ${sql.insertValues(input, ['email'])}`);
|
|
177
177
|
|
|
178
|
-
const patch: Pick<Rows.Update['
|
|
179
|
-
await db.query(sql`UPDATE public.
|
|
178
|
+
const patch: Pick<Rows.Update['users'], 'active'> = { active: false };
|
|
179
|
+
await db.query(sql`UPDATE public.users SET ${sql.assignments(patch, ['active'])}
|
|
180
180
|
WHERE email = ${input.email}`);
|
|
181
181
|
```
|
|
182
182
|
|
|
@@ -227,7 +227,7 @@ For fixed conditions, write them directly:
|
|
|
227
227
|
|
|
228
228
|
```ts
|
|
229
229
|
const users = await db.query(
|
|
230
|
-
sql<UserSummary>`SELECT id, email FROM public.
|
|
230
|
+
sql<UserSummary>`SELECT id, email FROM public.users WHERE active = ${true}`,
|
|
231
231
|
{ returns: 'many' },
|
|
232
232
|
);
|
|
233
233
|
```
|
|
@@ -238,9 +238,9 @@ its count:
|
|
|
238
238
|
|
|
239
239
|
```ts
|
|
240
240
|
const search = sql.fragment`active = ${true} AND email ILIKE ${pattern}`;
|
|
241
|
-
const page = sql<UserSummary>`SELECT id, email FROM public.
|
|
241
|
+
const page = sql<UserSummary>`SELECT id, email FROM public.users
|
|
242
242
|
WHERE ${search} ORDER BY id LIMIT ${limit}`;
|
|
243
|
-
const count = sql<{ count: string }>`SELECT count(*) AS count FROM public.
|
|
243
|
+
const count = sql<{ count: string }>`SELECT count(*) AS count FROM public.users
|
|
244
244
|
WHERE ${search}`;
|
|
245
245
|
```
|
|
246
246
|
|
|
@@ -258,7 +258,7 @@ for (const term of terms) {
|
|
|
258
258
|
filters.push(sql.fragment`email ILIKE ${'%' + term + '%'}`);
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
const query = sql<UserSummary>`SELECT id, email FROM public.
|
|
261
|
+
const query = sql<UserSummary>`SELECT id, email FROM public.users
|
|
262
262
|
WHERE ${sql.join(filters, 'AND')} ORDER BY id`;
|
|
263
263
|
```
|
|
264
264
|
|
package/docs/reference.md
CHANGED
|
@@ -78,7 +78,7 @@ types from SQL context.
|
|
|
78
78
|
Use an array parameter for a set of values:
|
|
79
79
|
|
|
80
80
|
```ts
|
|
81
|
-
const query = sql<{ id: number }>`SELECT id FROM public.
|
|
81
|
+
const query = sql<{ id: number }>`SELECT id FROM public.users
|
|
82
82
|
WHERE id = ANY(${ids}::integer[])`;
|
|
83
83
|
```
|
|
84
84
|
|
|
@@ -114,7 +114,7 @@ const conditions = [sql.fragment`active = ${true}`];
|
|
|
114
114
|
if (email !== undefined) conditions.push(sql.fragment`email = ${email}`);
|
|
115
115
|
if (afterId !== undefined) conditions.push(sql.fragment`id > ${afterId}`);
|
|
116
116
|
|
|
117
|
-
const query = sql<{ id: number }>`SELECT id FROM public.
|
|
117
|
+
const query = sql<{ id: number }>`SELECT id FROM public.users
|
|
118
118
|
WHERE ${sql.join(conditions, 'AND')} ORDER BY id`;
|
|
119
119
|
```
|
|
120
120
|
|
|
@@ -205,7 +205,7 @@ See the [mutation cookbook](mutations.md) for common patterns and
|
|
|
205
205
|
```ts
|
|
206
206
|
await db.transaction(async (tx) => {
|
|
207
207
|
const id = 42;
|
|
208
|
-
await tx.query(sql`UPDATE public.
|
|
208
|
+
await tx.query(sql`UPDATE public.users SET active = false WHERE id = ${id}`, {
|
|
209
209
|
affectedRows: { exact: 1 },
|
|
210
210
|
});
|
|
211
211
|
// Use tx for every query that belongs to this transaction.
|
|
@@ -260,9 +260,18 @@ current database. It never rewrites the file. Pending migration files
|
|
|
260
260
|
are a separate concern. The representation includes table columns, constraints, indexes, and
|
|
261
261
|
referenced domains, not every PostgreSQL object.
|
|
262
262
|
|
|
263
|
-
Generation reads a consistent snapshot of ordinary tables and partitioned roots. `Rows['
|
|
264
|
-
describes one returned row
|
|
265
|
-
|
|
263
|
+
Generation reads a consistent snapshot of ordinary tables and partitioned roots. `Rows['users']`
|
|
264
|
+
describes one returned row. Application type names can use PascalCase:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
import type { Rows } from './generated-schema.js';
|
|
268
|
+
|
|
269
|
+
type User = Rows['users'];
|
|
270
|
+
type UserSummary = Pick<User, 'id' | 'email'>;
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Use `sql<User>` for a full row or `sql<UserSummary>` for that projection. Generated table keys and
|
|
274
|
+
named aliases preserve exact SQL names; the generated `users` alias also means `Rows['users']`.
|
|
266
275
|
`type AuditRow = Rows['audit-log']` works for names that cannot be exported as identifiers.
|
|
267
276
|
Views, foreign tables, and attached partitions use query-specific types. A missing namespace
|
|
268
277
|
fails; an existing empty schema is valid.
|
|
@@ -417,7 +426,7 @@ It covers a separate SQL lint command, fix previews, and application refinements
|
|
|
417
426
|
Write a query without a result annotation:
|
|
418
427
|
|
|
419
428
|
```ts
|
|
420
|
-
const query = sql`SELECT id, email FROM public.
|
|
429
|
+
const query = sql`SELECT id, email FROM public.users WHERE id = ${id}`;
|
|
421
430
|
```
|
|
422
431
|
|
|
423
432
|
Run `npm run lint:fix`. SafeQL can write `sql<{ id: number; email: string }>` using the connected
|
|
@@ -540,7 +549,7 @@ safeql.configs.connections({
|
|
|
540
549
|
plugins: [safeqlPlugin],
|
|
541
550
|
targets: [{ tag: 'sql' }],
|
|
542
551
|
overrides: {
|
|
543
|
-
columns: { '
|
|
552
|
+
columns: { 'users.tags': 'PgArray<string>', 'users.amounts': 'PgArray<number>' },
|
|
544
553
|
},
|
|
545
554
|
});
|
|
546
555
|
```
|