@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 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."User" WHERE email = ${email}
32
+ SELECT id, email FROM public.users WHERE email = ${email}
33
33
  `,
34
34
  { returns: 'oneOrNone' },
35
35
  );
@@ -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 { User } from './generated-schema.js';
52
+ import type { Rows } from './generated-schema.js';
51
53
 
52
- type Summary = Pick<User, 'id' | 'email'>;
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."User" WHERE id = ${id}`, {
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."User"
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."User"
105
+ const count = sql<{ count: string }>`SELECT count(*) AS count FROM public.users
104
106
  WHERE ${search}`;
105
107
  ```
106
108
 
@@ -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 a new schema, create a
40
- migration:
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."User" (
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."User";
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."User" (email) VALUES (${email})`);
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."User" WHERE email = ${email}
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 { User } from './generated-schema.js';
111
- type UserSummary = Pick<User, 'id' | 'email'>;
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 { User } from './generated-schema.js';
127
+ import type { Rows } from './generated-schema.js';
127
128
 
128
- type UserSummary = Pick<User, 'id' | 'email'>;
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
- sql<UserSummary>`SELECT id, email FROM public."User" WHERE email = ${email}`,
133
- { returns: 'oneOrNone' },
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."User" SET active = false WHERE id = ${user.id}`, {
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['User'] = { email: 'another@example.test' };
176
- await db.query(sql`INSERT INTO public."User" ${sql.insertValues(input, ['email'])}`);
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['User'], 'active'> = { active: false };
179
- await db.query(sql`UPDATE public."User" SET ${sql.assignments(patch, ['active'])}
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."User" WHERE active = ${true}`,
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."User"
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."User"
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."User"
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."User"
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."User"
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."User" SET active = false WHERE id = ${id}`, {
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['User']`
264
- describes one returned row; the generated `User` alias means the same thing. Use `sql<User>` for a
265
- full row or `Pick<User, 'id' | 'email'>` for a projection. Table keys preserve exact SQL names;
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."User" WHERE id = ${id}`;
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: { 'User.tags': 'PgArray<string>', 'User.amounts': 'PgArray<number>' },
552
+ columns: { 'users.tags': 'PgArray<string>', 'users.amounts': 'PgArray<number>' },
544
553
  },
545
554
  });
546
555
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kequtech/tenon-orm",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "A PostgreSQL ORM designed for coding agents, with explicit SQL, typed queries, and migrations",