@kequtech/tenon-orm 0.0.0-stage → 0.1.1
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/LICENSE +15 -0
- package/README.md +105 -2
- package/dist/affected-rows.d.ts +11 -0
- package/dist/affected-rows.js +30 -0
- package/dist/baseline.d.ts +19 -0
- package/dist/baseline.js +100 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +158 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +54 -0
- package/dist/connection.d.ts +15 -0
- package/dist/connection.js +51 -0
- package/dist/database.d.ts +7 -0
- package/dist/database.js +44 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +18 -0
- package/dist/generate.d.ts +9 -0
- package/dist/generate.js +34 -0
- package/dist/generation/catalog.d.ts +19 -0
- package/dist/generation/catalog.js +35 -0
- package/dist/generation/row-types.d.ts +2 -0
- package/dist/generation/row-types.js +115 -0
- package/dist/generation/schema-summary.d.ts +6 -0
- package/dist/generation/schema-summary.js +114 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +7 -0
- package/dist/migrations/baseline-sql.d.ts +3 -0
- package/dist/migrations/baseline-sql.js +38 -0
- package/dist/migrations/connection.d.ts +9 -0
- package/dist/migrations/connection.js +65 -0
- package/dist/migrations/files.d.ts +21 -0
- package/dist/migrations/files.js +131 -0
- package/dist/migrations/history.d.ts +12 -0
- package/dist/migrations/history.js +30 -0
- package/dist/migrations/sql.d.ts +2 -0
- package/dist/migrations/sql.js +94 -0
- package/dist/migrations.d.ts +10 -0
- package/dist/migrations.js +68 -0
- package/dist/mutations.d.ts +11 -0
- package/dist/mutations.js +87 -0
- package/dist/query-options.d.ts +15 -0
- package/dist/query-options.js +35 -0
- package/dist/query.d.ts +38 -0
- package/dist/query.js +129 -0
- package/dist/safeql/composition.d.ts +9 -0
- package/dist/safeql/composition.js +159 -0
- package/dist/safeql/eslint.d.ts +2 -0
- package/dist/safeql/eslint.js +45 -0
- package/dist/safeql/mutations.d.ts +3 -0
- package/dist/safeql/mutations.js +74 -0
- package/dist/safeql/plugin.d.ts +3 -0
- package/dist/safeql/plugin.js +43 -0
- package/dist/safeql/static.d.ts +10 -0
- package/dist/safeql/static.js +59 -0
- package/dist/safeql.d.ts +6 -0
- package/dist/safeql.js +7 -0
- package/dist/sql.d.ts +5 -0
- package/dist/sql.js +28 -0
- package/dist/transaction-sql.d.ts +2 -0
- package/dist/transaction-sql.js +67 -0
- package/dist/transaction.d.ts +4 -0
- package/dist/transaction.js +75 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.js +1 -0
- package/dist/validation.d.ts +1 -0
- package/dist/validation.js +10 -0
- package/docs/adoption-guide.md +91 -0
- package/docs/agent-guide.md +310 -0
- package/docs/human-guide.md +271 -0
- package/docs/mutations.md +115 -0
- package/docs/reference.md +568 -0
- package/package.json +92 -4
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Mutation cookbook
|
|
2
|
+
|
|
3
|
+
Keep SQL structure visible and let the official helpers handle object fields. See the
|
|
4
|
+
[reference](reference.md#object-based-mutation-fragments) for the complete contract. Examples use
|
|
5
|
+
application-owned inputs and clients; add the authorization predicates required by your product.
|
|
6
|
+
|
|
7
|
+
## Defaults, undefined, and null in batches
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
await db.query(
|
|
11
|
+
sql`INSERT INTO profiles ${sql.insertValues([
|
|
12
|
+
{ name: 'Ada', bio: null },
|
|
13
|
+
{ name: 'Lin', score: 4, bio: undefined },
|
|
14
|
+
])} RETURNING id`,
|
|
15
|
+
);
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The first row uses the score default. The second uses the bio default. Explicit null in the first
|
|
19
|
+
row stays null. Columns are aligned across rows. Omit generated columns and let PostgreSQL enforce
|
|
20
|
+
required fields and constraints.
|
|
21
|
+
|
|
22
|
+
For runtime-built rows, supply an inline column list for SafeQL:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const rows: { name: string; bio?: string | null; score?: number }[] = buildRows();
|
|
26
|
+
await db.query(sql`INSERT INTO profiles ${sql.insertValues(rows, ['name', 'bio', 'score'])}`);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This list selects and orders fields; unlisted fields are ignored. It does not assert that the
|
|
30
|
+
entire input object contains only allowed keys. Keep narrower application input validation.
|
|
31
|
+
|
|
32
|
+
## Patches mixed with SQL expressions
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
await db.query(sql`UPDATE profiles
|
|
36
|
+
SET ${sql.assignments({ name, bio })}, revision = revision + 1, score = DEFAULT
|
|
37
|
+
WHERE id = ${id}`);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Undefined fields are omitted; null clears a field. Expressions, `DEFAULT`, and references to
|
|
41
|
+
`excluded.column` belong in SQL, not in mutation objects.
|
|
42
|
+
|
|
43
|
+
An empty patch throws even if the surrounding statement has other assignments. Choose an
|
|
44
|
+
expression-only query explicitly when that is the intended behavior:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const patch: { name?: string; bio?: string | null } = readPatch();
|
|
48
|
+
if (patch.name !== undefined || patch.bio !== undefined) {
|
|
49
|
+
await db.query(sql`UPDATE profiles
|
|
50
|
+
SET ${sql.assignments(patch, ['name', 'bio'])}, revision = revision + 1
|
|
51
|
+
WHERE id = ${id}`);
|
|
52
|
+
} else {
|
|
53
|
+
await db.query(sql`UPDATE profiles SET revision = revision + 1 WHERE id = ${id}`);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Preserve an omitted field during an upsert
|
|
58
|
+
|
|
59
|
+
Use complete query variants when an option changes conflict behavior. Both templates can be
|
|
60
|
+
checked independently; the checker need not choose a JavaScript branch:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
if (email !== undefined) {
|
|
64
|
+
await db.query(sql`INSERT INTO preferences ${sql.insertValues({ profile_id, in_app, email })}
|
|
65
|
+
ON CONFLICT (profile_id) DO UPDATE
|
|
66
|
+
SET in_app = excluded.in_app, email = excluded.email`);
|
|
67
|
+
} else {
|
|
68
|
+
await db.query(sql`INSERT INTO preferences ${sql.insertValues({ profile_id, in_app })}
|
|
69
|
+
ON CONFLICT (profile_id) DO UPDATE SET in_app = excluded.in_app`);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The second query leaves an existing email value unchanged. Missing an insert field uses its
|
|
74
|
+
database default; that alone does not decide what an upsert should overwrite.
|
|
75
|
+
|
|
76
|
+
## JSONB arrays and PostgreSQL arrays
|
|
77
|
+
|
|
78
|
+
Tenon preserves pg's value conversion. A JavaScript array binds as a PostgreSQL array. To write an
|
|
79
|
+
array into a JSON/JSONB column, serialize it explicitly:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const handles = ['ada', 'lin'];
|
|
83
|
+
await db.query(
|
|
84
|
+
sql`INSERT INTO deliveries ${sql.insertValues({
|
|
85
|
+
recipients_json: JSON.stringify(handles), // jsonb column
|
|
86
|
+
recipient_handles: handles, // text[] column
|
|
87
|
+
})}`,
|
|
88
|
+
);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Objects used as JSON values retain pg's ordinary JSON conversion. The helper does not infer JSON
|
|
92
|
+
versus array encoding from the table schema. SafeQL's projection checks do not validate stored JSON
|
|
93
|
+
contents or runtime input values.
|
|
94
|
+
|
|
95
|
+
## Batches in one transaction
|
|
96
|
+
|
|
97
|
+
Choose the batch size in the caller and keep all writes on the transaction client:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
await db.transaction(async (tx) => {
|
|
101
|
+
for (let offset = 0; offset < rows.length; offset += 300) {
|
|
102
|
+
const batch = rows.slice(offset, offset + 300);
|
|
103
|
+
await tx.query(sql`INSERT INTO profiles ${sql.insertValues(batch, ['name', 'bio', 'score'])}`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
A failure rolls back all batches. The helper rejects an empty batch; this loop makes no insert
|
|
109
|
+
calls for empty input. The query-wide limit is 65,535 bound parameters, including parameters outside
|
|
110
|
+
the mutation fragment. Defaults consume no parameters. Account for your selected column count and
|
|
111
|
+
surrounding query when choosing a batch size; Tenon never splits queries automatically.
|
|
112
|
+
|
|
113
|
+
Reusable authorization fragments remain valid at runtime. Keep shared policy in application
|
|
114
|
+
functions and test it rather than duplicating it for static coverage. Function-generated SQL,
|
|
115
|
+
conditional fragments, and dynamic filter lists remain outside static checking.
|
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
# Tenon reference
|
|
2
|
+
|
|
3
|
+
Use this document to look up exact API behavior, checker setup, and limitations. The
|
|
4
|
+
[human guide](human-guide.md) teaches the workflow; the [agent guide](agent-guide.md) is a
|
|
5
|
+
self-contained implementation guide. [Adoption](adoption-guide.md) covers replacing an existing ORM
|
|
6
|
+
and transferring migration history.
|
|
7
|
+
|
|
8
|
+
## Installation and connection
|
|
9
|
+
|
|
10
|
+
Runtime requirements: Node.js 24+, PostgreSQL 16+, ESM, and `pg`. TypeScript consumers need
|
|
11
|
+
TypeScript 6+ and development typings for Node and `pg`. Keep your application's strict TypeScript
|
|
12
|
+
setup; Tenon needs no special compiler configuration. `"type": "module"` and TypeScript's
|
|
13
|
+
`"module": "NodeNext"` are a usual Node setup. Checker compatibility is listed separately below.
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install @kequtech/tenon-orm pg
|
|
17
|
+
npm install --save-dev @types/pg
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`createTenon({ connection, onQuery? })` returns a `Database` with `query`, `transaction`, and
|
|
21
|
+
`close`. The required `connection` accepts a URL, `pg.PoolConfig`, or existing `pg.Pool`. An
|
|
22
|
+
explicit `{}` selects pg's environment defaults. Tenon does not load environment files
|
|
23
|
+
automatically.
|
|
24
|
+
|
|
25
|
+
Reuse an instance per database connection. `close()` closes pools created by Tenon; supplied pools
|
|
26
|
+
remain application-owned. Finish outstanding work before closing. Install your own
|
|
27
|
+
`pool.on('error', handler)` on supplied pools; Tenon reports idle errors on owned pools through
|
|
28
|
+
`process.emitWarning`.
|
|
29
|
+
|
|
30
|
+
Qualify SQL names or configure the driver's `search_path`. The CLI's `--namespace` selects the
|
|
31
|
+
schema used by tooling, not a runtime query namespace.
|
|
32
|
+
|
|
33
|
+
## Queries and result modes
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const result = await db.query(sql<Row>`SELECT ...`, {
|
|
37
|
+
returns: 'many',
|
|
38
|
+
label: 'list-users',
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`sql<Row>` produces a `SqlQuery<Row>` with SQL `text` and ordered `values`. It does not execute.
|
|
43
|
+
`Row` describes one selected row; fields are `unknown` when no annotation is supplied. Type
|
|
44
|
+
annotations do not verify SQL or validate JSON. Full table types from generation do not
|
|
45
|
+
automatically describe joins or aggregates.
|
|
46
|
+
|
|
47
|
+
`query` accepts `{ returns?, affectedRows?, label? }`. Unknown options are rejected. `label` must be
|
|
48
|
+
a nonempty string when supplied and is used for diagnostics.
|
|
49
|
+
|
|
50
|
+
| `returns` | Result | Returned rows allowed |
|
|
51
|
+
| ------------------ | --------------------- | --------------------- |
|
|
52
|
+
| `native` (default) | pg `QueryResult<Row>` | Any |
|
|
53
|
+
| `many` | `Row[]` | Any, including zero |
|
|
54
|
+
| `oneOrNone` | `Row \| undefined` | Zero or one |
|
|
55
|
+
| `one` | `Row` | Exactly one |
|
|
56
|
+
|
|
57
|
+
Native results retain `command`, `rowCount`, `fields`, and `rows`; a null row count means none was
|
|
58
|
+
reported. Result modes never add `LIMIT` or `RETURNING`. A count mismatch throws; returned-row
|
|
59
|
+
checks alone do not undo an autocommitted write. Use `affectedRows` for write limits that roll back
|
|
60
|
+
on failure.
|
|
61
|
+
|
|
62
|
+
Reusable queries are ordinary functions. Accept a `QueryClient` and named object inputs when useful;
|
|
63
|
+
both `Database` and `Transaction` implement `QueryClient`. Keep result annotations on the SQL and
|
|
64
|
+
execution options at the query call. Public types include `QueryOptions`, `QueryReturnMode`, and
|
|
65
|
+
`QueryOutput<Row, Mode>`.
|
|
66
|
+
|
|
67
|
+
## SQL parameters and identifiers
|
|
68
|
+
|
|
69
|
+
The `sql` tag binds ordinary interpolations as `$1`, `$2`, etc. Never quote interpolations or author
|
|
70
|
+
manual parameter placeholders inside templates. Every public query executes one statement, including
|
|
71
|
+
parameterless queries.
|
|
72
|
+
|
|
73
|
+
Values use pg's conversions, including arrays, JSON, dates, and `undefined` becoming SQL null. Cast
|
|
74
|
+
standalone parameter projections to the intended SQL type: `SELECT ${1.5} AS value` returns text,
|
|
75
|
+
while `SELECT ${1.5}::float8 AS value` returns a JavaScript number. PostgreSQL infers parameter
|
|
76
|
+
types from SQL context.
|
|
77
|
+
|
|
78
|
+
Use an array parameter for a set of values:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const query = sql<{ id: number }>`SELECT id FROM public."User"
|
|
82
|
+
WHERE id = ANY(${ids}::integer[])`;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`identifier(schema, table)` quotes each component separately. Names must be nonempty, contain no
|
|
86
|
+
NUL, and fit PostgreSQL's 63-byte identifier limit. Choose names from application-approved
|
|
87
|
+
alternatives; quoting does not authorize access. Ordinary strings such as `'DESC'` are bound values,
|
|
88
|
+
not SQL syntax. For ordering, use complete query alternatives or application-selected fragments
|
|
89
|
+
containing `ASC` or `DESC`; runtime-selected fragments need checker exclusions and tests.
|
|
90
|
+
|
|
91
|
+
`unsafeSql<Row>(text, values)` accepts externally authored, trusted SQL and an explicit values
|
|
92
|
+
array, including `[]` when empty. It bypasses static SQL checking. Never concatenate request input
|
|
93
|
+
into its text. Full `sql` or `unsafeSql` queries cannot be inserted into other queries; use
|
|
94
|
+
fragments for partial SQL.
|
|
95
|
+
|
|
96
|
+
## SQL fragments and joins
|
|
97
|
+
|
|
98
|
+
`sql.fragment` creates an immutable piece of SQL with bound values. It has no row type and cannot
|
|
99
|
+
execute alone. Insert it into a complete `sql<Row>` query. Constant fragments can share a predicate
|
|
100
|
+
between, for example, a list query and a count query. Ordinary fixed conditions are usually clearer
|
|
101
|
+
directly in the query.
|
|
102
|
+
|
|
103
|
+
`sql.join(fragments, separator)` combines a nonempty array of fragments into a new fragment.
|
|
104
|
+
Separators are exactly:
|
|
105
|
+
|
|
106
|
+
| Argument | Inserted SQL | Typical use |
|
|
107
|
+
| -------- | ------------ | -------------------------- |
|
|
108
|
+
| `'AND'` | `AND` | All conditions must hold |
|
|
109
|
+
| `'OR'` | `OR` | Any condition may hold |
|
|
110
|
+
| `','` | `, ` | Projections or assignments |
|
|
111
|
+
|
|
112
|
+
No default, arbitrary string, or fragment separator is accepted. Runtime validation also protects
|
|
113
|
+
JavaScript callers. A single item is returned as a new fragment without extra separators. Empty
|
|
114
|
+
lists and blank fragments throw; choose empty-list behavior explicitly in application code.
|
|
115
|
+
|
|
116
|
+
Joins do not add parentheses or alter SQL precedence. Group alternatives before combining them with
|
|
117
|
+
other conditions:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const alternatives = sql.join(
|
|
121
|
+
[sql.fragment`email = ${primary}`, sql.fragment`email = ${secondary}`],
|
|
122
|
+
'OR',
|
|
123
|
+
);
|
|
124
|
+
const conditions = sql.join(
|
|
125
|
+
[sql.fragment`(${alternatives})`, sql.fragment`active = ${true}`],
|
|
126
|
+
'AND',
|
|
127
|
+
);
|
|
128
|
+
const query = sql<{ id: number }>`SELECT id FROM public."User" WHERE ${conditions}`;
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Each fragment use receives correctly ordered parameters, including repeated and nested uses. SQL
|
|
132
|
+
strings and comments containing literal `$1` text are left intact.
|
|
133
|
+
|
|
134
|
+
Static checking supports constant fragments, literal join arrays, literal or const-string
|
|
135
|
+
separators, and [official mutation helpers](#object-based-mutation-fragments). Runtime-built lists
|
|
136
|
+
and selected fragments execute safely with respect to parameter binding, but the checker cannot
|
|
137
|
+
verify their SQL or result types. Prefer fixed SQL when it fits.
|
|
138
|
+
Otherwise keep an explicit result annotation, use a narrow explained lint suppression, and test
|
|
139
|
+
every variant.
|
|
140
|
+
|
|
141
|
+
## Object-based mutation fragments
|
|
142
|
+
|
|
143
|
+
`sql.insertValues(input, columns?)` and `sql.assignments(input, columns?)` return fragments. Keep the
|
|
144
|
+
operation, table, authorization predicate, conflict policy, expressions, and `RETURNING` in SQL:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const created = await db.query(
|
|
148
|
+
sql<{ id: number }>`
|
|
149
|
+
INSERT INTO profiles ${sql.insertValues([
|
|
150
|
+
{ name: 'Ada', bio: null },
|
|
151
|
+
{ name: 'Lin', score: 4 },
|
|
152
|
+
])}
|
|
153
|
+
ON CONFLICT (name) DO NOTHING
|
|
154
|
+
RETURNING id
|
|
155
|
+
`,
|
|
156
|
+
{ returns: 'many' },
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
await db.query(sql`
|
|
160
|
+
UPDATE profiles SET ${sql.assignments({ name: input.name, bio: input.bio })},
|
|
161
|
+
revision = revision + 1
|
|
162
|
+
WHERE id = ${id} AND owner_id = ${viewer}
|
|
163
|
+
`);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Both helpers accept plain objects (including null-prototype objects), quote each column as one
|
|
167
|
+
identifier, and bind cell values as data. Inputs are read when the fragment is created; replacing a
|
|
168
|
+
field afterward does not change its captured value. Nested values retain the driver's usual
|
|
169
|
+
serialization behavior. Only own data properties are read; accessors are rejected.
|
|
170
|
+
|
|
171
|
+
| Input | Insert behavior | Assignment behavior |
|
|
172
|
+
| -------------------------------------------- | ---------------------------------- | --------------------------------------- |
|
|
173
|
+
| Missing or `undefined` field | PostgreSQL `DEFAULT` for that cell | Omitted; the stored column is unchanged |
|
|
174
|
+
| Explicit `null` | Bound SQL null | Bound SQL null |
|
|
175
|
+
| Other value | Bound parameter | Bound parameter |
|
|
176
|
+
| SQL fragment, identifier, or query as a cell | Rejected | Rejected |
|
|
177
|
+
|
|
178
|
+
Without `columns`, inserts infer the union of enumerable keys with defined values across all rows,
|
|
179
|
+
in first-seen order. Heterogeneous rows stay aligned; missing cells use `DEFAULT`. One object or a
|
|
180
|
+
one-row batch with no defined fields emits `DEFAULT VALUES`. An empty batch throws. A multi-row
|
|
181
|
+
batch with no inferred columns throws; supply explicit columns to emit multiple default rows.
|
|
182
|
+
|
|
183
|
+
Assignments infer enumerable keys and omit undefined values. An empty object or a selection with
|
|
184
|
+
no defined values throws, so the helper never emits an empty `SET` clause. This also applies when
|
|
185
|
+
the surrounding statement contains arithmetic or other assignments: branch explicitly when the
|
|
186
|
+
intended update consists only of those expressions.
|
|
187
|
+
|
|
188
|
+
An optional nonempty column list selects and orders fields. It does not validate that the input
|
|
189
|
+
object has no additional keys. Unlisted fields are ignored; missing insert cells use `DEFAULT`,
|
|
190
|
+
while missing update fields are omitted. Duplicate or invalid column
|
|
191
|
+
names throw. TypeScript checks selected names against the input's known keys. The list is also the
|
|
192
|
+
way to make runtime objects and batches available to static SQL checking:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
const rows: { name: string; bio?: string | null }[] = buildRows();
|
|
196
|
+
const insert = sql`INSERT INTO profiles ${sql.insertValues(rows, ['name', 'bio'])} RETURNING id`;
|
|
197
|
+
|
|
198
|
+
const patch: { name?: string; bio?: string | null } = readPatch();
|
|
199
|
+
const update = sql`UPDATE profiles SET ${sql.assignments(patch, ['name', 'bio'])} WHERE id = ${id}`;
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The helpers have no schema metadata. Omit generated columns; PostgreSQL enforces identity,
|
|
203
|
+
generation, required-column, and constraint rules. They do not generate insert/update input types
|
|
204
|
+
or prove that the JavaScript input values match database column types. Retain application input
|
|
205
|
+
contracts and validation. To reset an update field to its default, write `column = DEFAULT` in the
|
|
206
|
+
surrounding SQL. Put arithmetic and `excluded.column` expressions there too; these helpers accept
|
|
207
|
+
values only, with no expression/default sentinel.
|
|
208
|
+
|
|
209
|
+
A compiled query, including surrounding interpolations, is limited to 65,535 bound parameters.
|
|
210
|
+
Tenon rejects larger queries and does not split them automatically. Choose batch sizes and
|
|
211
|
+
transaction boundaries in the caller. Helper validation errors identify invalid rows/columns
|
|
212
|
+
without printing cell values.
|
|
213
|
+
|
|
214
|
+
### Checking mutation fragments
|
|
215
|
+
|
|
216
|
+
SafeQL recognizes these official helpers directly through their Tenon function declarations,
|
|
217
|
+
including aliases. It never analyzes their implementation or executes application data builders.
|
|
218
|
+
Without an explicit column list, checking accepts inline object literals and inline insert arrays
|
|
219
|
+
of object literals. Keys must be literal names; object spreads, computed keys, methods, accessors,
|
|
220
|
+
and objects stored in variables require an explicit inline column list. Existing constant string
|
|
221
|
+
resolution works for entries of that list; column arrays stored in variables are unsupported.
|
|
222
|
+
|
|
223
|
+
For runtime inputs with explicit columns, the checker analyzes a representative nonempty insert
|
|
224
|
+
row or assignment list containing **all selected columns**. This checks column references, SQL
|
|
225
|
+
composition, conflict clauses, and `RETURNING` annotations. Runtime omission/default decisions and
|
|
226
|
+
batch length do not require interpreting JavaScript. The check is conservative: an optional
|
|
227
|
+
selected column is checked even when a particular runtime call omits it. It does not establish
|
|
228
|
+
nonempty input, valid runtime values, successful constraints, or every default's behavior; runtime
|
|
229
|
+
validation and database tests cover those contracts. Unknown inputs without explicit columns
|
|
230
|
+
produce `tenon/check-composition` diagnostics. See the [mutation cookbook](mutations.md) for
|
|
231
|
+
empty patches, conditional upserts, arrays, and batching.
|
|
232
|
+
|
|
233
|
+
## Transactions, guarded writes, and errors
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
await db.transaction(async (tx) => {
|
|
237
|
+
const id = 42;
|
|
238
|
+
await tx.query(sql`UPDATE public."User" SET active = false WHERE id = ${id}`, {
|
|
239
|
+
affectedRows: { exact: 1 },
|
|
240
|
+
});
|
|
241
|
+
// Use tx for every query that belongs to this transaction.
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Transactions belong to one database instance; they do not coordinate writes across databases. The
|
|
246
|
+
callback commits on success and rolls back when an error escapes. Await all work before returning;
|
|
247
|
+
the callback client expires afterward. A helper accepting `QueryClient` works with either `db` or
|
|
248
|
+
`tx`. Nested callback transactions and automatic retries are not provided.
|
|
249
|
+
|
|
250
|
+
`affectedRows: { exact: n }` requires n affected rows; `{ max: n }` permits up to n. Both require
|
|
251
|
+
nonnegative safe integers. Checks apply to INSERT, UPDATE, DELETE, and MERGE, independent of
|
|
252
|
+
`RETURNING`. A root call uses a transaction; a call on `tx` uses a savepoint. A mismatch rolls back
|
|
253
|
+
before throwing. You may catch a guarded failure and continue the outer transaction. Unguarded
|
|
254
|
+
PostgreSQL errors normally abort that transaction, even when caught.
|
|
255
|
+
|
|
256
|
+
The count covers the final command, excluding additional trigger, cascade, or CTE effects. It cannot
|
|
257
|
+
identify the wrong single row. Returned-row checks alone do not undo autocommitted writes. Rollback
|
|
258
|
+
cannot undo sequence advances or external side effects of database functions.
|
|
259
|
+
|
|
260
|
+
Set isolation and timeouts with SQL, for example `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`
|
|
261
|
+
before data queries and `SET LOCAL statement_timeout = '5s'`. Manual savepoints are available
|
|
262
|
+
through SQL; reserve `tenon_affected_rows_` names for Tenon. Direct BEGIN/COMMIT/ROLLBACK boundaries
|
|
263
|
+
are owned by `transaction`.
|
|
264
|
+
|
|
265
|
+
SQL errors retain pg's codes and details. Invalid inputs and returned-row mismatches throw ordinary
|
|
266
|
+
`Error` objects. `AffectedRowsError` carries `expected`, `actual`, `command`, and code
|
|
267
|
+
`TENON_AFFECTED_ROWS_MISMATCH` or `TENON_AFFECTED_ROWS_UNSUPPORTED`. When both work and cleanup
|
|
268
|
+
fail, `AggregateError` preserves both failures. A lost commit acknowledgment can leave the outcome
|
|
269
|
+
unknown; retry only when the application's operation makes that safe.
|
|
270
|
+
|
|
271
|
+
Optional `createTenon({ connection, onQuery })` observes completed calls with `label`, `durationMs`,
|
|
272
|
+
`outcome`, and available `command`, `rowCount`, `errorCode`. It receives no SQL or values. Observers
|
|
273
|
+
are not awaited; failures emit warnings. A successful callback query does not mean its outer
|
|
274
|
+
transaction has committed.
|
|
275
|
+
|
|
276
|
+
## Generated types and checking
|
|
277
|
+
|
|
278
|
+
`generateDefinitions({ connection, namespace?, onWarning? })` returns TypeScript source for the
|
|
279
|
+
connected schema. It does not need migration files or history. Table type generation is optional.
|
|
280
|
+
SafeQL reads the database directly and does not require these generated files; they are useful when
|
|
281
|
+
reusing table row shapes. The CLI writes that source atomically. This command checks the existing
|
|
282
|
+
file without rewriting it:
|
|
283
|
+
|
|
284
|
+
```sh
|
|
285
|
+
npx tenon generate --out src/generated-schema.ts --check
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`--check` compares the generated representation with the current database. Pending migration files
|
|
289
|
+
are a separate concern. The representation includes table columns, constraints, indexes, and
|
|
290
|
+
referenced domains, not every PostgreSQL object.
|
|
291
|
+
|
|
292
|
+
Generation reads a consistent snapshot of ordinary tables and partitioned roots. Use the generated
|
|
293
|
+
named type in queries, for example `sql<User>`. For other table names, define an alias such as
|
|
294
|
+
`type AuditRow = Rows['audit-log']` and use `sql<AuditRow>`; SafeQL resolves named aliases but not
|
|
295
|
+
indexed access directly in `sql<Rows['audit-log']>`. Views, foreign tables, and attached partitions
|
|
296
|
+
use query-specific types. A missing namespace fails; an existing empty schema is valid.
|
|
297
|
+
|
|
298
|
+
| PostgreSQL type | Default pg value |
|
|
299
|
+
| ---------------------------------------------- | ----------------------------------------------- |
|
|
300
|
+
| smallint, integer, oid, real, double precision | `number` |
|
|
301
|
+
| bigint, numeric | `string` |
|
|
302
|
+
| boolean | `boolean` |
|
|
303
|
+
| text, varchar, char, uuid, time, timetz | `string` |
|
|
304
|
+
| date, timestamp, timestamptz | `Date \| number` (numbers represent infinities) |
|
|
305
|
+
| enum | String literal union; `never` for an empty enum |
|
|
306
|
+
| json, jsonb | `unknown` |
|
|
307
|
+
| bytea | `Buffer` |
|
|
308
|
+
|
|
309
|
+
Nullable columns add null. Domains use verified underlying mappings and the column's own
|
|
310
|
+
nullability. Unverified types warn and become `unknown`. Verified arrays use
|
|
311
|
+
`PgArray<T> = (T | null | PgArray<T>)[]` for null elements and multiple dimensions. Numeric arrays
|
|
312
|
+
contain numbers and can lose precision; cast to text[] for exact values. Enum arrays and arrays of
|
|
313
|
+
domains are unverified. Finite dates and timestamps without time zone use Node's local timezone.
|
|
314
|
+
Custom pg parsers need matching application types.
|
|
315
|
+
|
|
316
|
+
## SQL checking with SafeQL
|
|
317
|
+
|
|
318
|
+
SafeQL checks SQL against a real PostgreSQL schema and infers result types. ESLint runs that check
|
|
319
|
+
on your source files. Tenon runs the queries in the application; it does not import or run either
|
|
320
|
+
tool during query execution.
|
|
321
|
+
|
|
322
|
+
Configure [SafeQL normally](https://safeql.dev/api/). Tenon's optional `safeql` entry point exports
|
|
323
|
+
two objects:
|
|
324
|
+
|
|
325
|
+
- `safeqlPlugin` is a SafeQL plugin descriptor. Add it to each Tenon connection's `plugins` list. It
|
|
326
|
+
translates constant fragments, official mutation helpers, and identifiers, preserves parameter
|
|
327
|
+
behavior, and supplies pg result mappings. SafeQL owns database analysis and annotation fixes.
|
|
328
|
+
- `eslintPlugin` supplies `tenon/check-composition`. Register it in ESLint and enable the rule to
|
|
329
|
+
report queries that the adapter cannot resolve, including runtime-built fragments and escaped
|
|
330
|
+
outer templates.
|
|
331
|
+
|
|
332
|
+
Use both. The adapter skips unsupported queries; the companion rule makes those exclusions visible
|
|
333
|
+
as ordinary, individually suppressible lint errors. Neither replaces or wraps SafeQL's rule.
|
|
334
|
+
|
|
335
|
+
### Install and configure
|
|
336
|
+
|
|
337
|
+
Install the checking tools as application development dependencies, reusing the application's
|
|
338
|
+
existing TypeScript installation:
|
|
339
|
+
|
|
340
|
+
```sh
|
|
341
|
+
npm install --save-dev eslint typescript-eslint @ts-safeql/eslint-plugin
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
SafeQL and ESLint are the integration's hosts, and the adapter imports TypeScript to inspect query
|
|
345
|
+
expressions. They are optional peers because Tenon's database runtime does not need them.
|
|
346
|
+
`typescript-eslint` is an application-owned parser dependency, not a Tenon peer. Your ESLint
|
|
347
|
+
configuration imports SafeQL and the parser explicitly. Use a TypeScript version supported by that
|
|
348
|
+
lint toolchain. The `pg` driver remains Tenon's required runtime peer.
|
|
349
|
+
|
|
350
|
+
For a project with `src/**/*.ts` and a `tsconfig.json`:
|
|
351
|
+
|
|
352
|
+
```js
|
|
353
|
+
// eslint.config.mjs
|
|
354
|
+
import { defineConfig } from 'eslint/config';
|
|
355
|
+
import safeql from '@ts-safeql/eslint-plugin/config';
|
|
356
|
+
import tseslint from 'typescript-eslint';
|
|
357
|
+
import { safeqlPlugin, eslintPlugin } from '@kequtech/tenon-orm/safeql';
|
|
358
|
+
|
|
359
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
360
|
+
if (!databaseUrl) throw new Error('DATABASE_URL is required for SQL checking');
|
|
361
|
+
|
|
362
|
+
export default defineConfig({
|
|
363
|
+
files: ['src/**/*.ts'],
|
|
364
|
+
languageOptions: {
|
|
365
|
+
parser: tseslint.parser,
|
|
366
|
+
parserOptions: { projectService: true },
|
|
367
|
+
},
|
|
368
|
+
plugins: { tenon: eslintPlugin },
|
|
369
|
+
rules: { 'tenon/check-composition': 'error' },
|
|
370
|
+
extends: [
|
|
371
|
+
safeql.configs.connections({
|
|
372
|
+
databaseUrl,
|
|
373
|
+
plugins: [safeqlPlugin],
|
|
374
|
+
targets: [{ tag: 'sql' }],
|
|
375
|
+
}),
|
|
376
|
+
],
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Add these settings to your existing ESLint setup if you have one; reuse its type-aware parser
|
|
381
|
+
configuration. `projectService` uses the nearest TypeScript project for each file. Supply
|
|
382
|
+
`DATABASE_URL` through your application's environment setup and point it at a development/test
|
|
383
|
+
database with migrations applied. This example does not load `.env` files.
|
|
384
|
+
|
|
385
|
+
The `.mjs` example avoids a current declaration mismatch between SafeQL's configuration helper and
|
|
386
|
+
ESLint's `defineConfig`. This affects configuration typechecking, not query result types.
|
|
387
|
+
|
|
388
|
+
`targets` selects the SQL tag spelling. If you alias `sql`, add the alias to the targets. For a
|
|
389
|
+
single database, `targets: [{ tag: { regex: '.*' } }]` selects all tags; Tenon's adapter filters
|
|
390
|
+
that down to actual Tenon queries, including aliases and object properties. Neither the adapter nor
|
|
391
|
+
the rule executes application code.
|
|
392
|
+
|
|
393
|
+
Reuse the application's lint scripts. If none exist, add these, adjusting file selection as needed.
|
|
394
|
+
They run all enabled ESLint rules, and `lint:fix` can change non-SQL code too:
|
|
395
|
+
|
|
396
|
+
```json
|
|
397
|
+
{
|
|
398
|
+
"scripts": {
|
|
399
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
400
|
+
"lint:fix": "eslint \"src/**/*.ts\" --fix"
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### Generate and check annotations
|
|
406
|
+
|
|
407
|
+
Write a query without a result annotation:
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
const query = sql`SELECT id, email FROM public."User" WHERE id = ${id}`;
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Run `npm run lint:fix`. SafeQL can write `sql<{ id: number; email: string }>` using the connected
|
|
414
|
+
schema. It can also repair an incorrect annotation. Review the changes, run TypeScript and database
|
|
415
|
+
tests, and commit the result. Without an annotation, Tenon's result fields stay `unknown`.
|
|
416
|
+
|
|
417
|
+
Run `npm run lint` in CI without `--fix`. Missing/incorrect annotations, invalid SQL, and
|
|
418
|
+
unsupported composition fail the check without rewriting files. After schema changes, run the
|
|
419
|
+
development [`db:migrate` script](human-guide.md#create-a-table) before checking queries. Inspect
|
|
420
|
+
SQL mistakes before accepting types generated from an unintended query. ESLint's fix command may
|
|
421
|
+
also apply other enabled rules.
|
|
422
|
+
|
|
423
|
+
### Configuration choices and multiple databases
|
|
424
|
+
|
|
425
|
+
All connection, target, and override options belong to SafeQL. There is no Tenon-specific
|
|
426
|
+
configuration wrapper or separate database setting. Keep `safeqlPlugin` on each Tenon connection:
|
|
427
|
+
without it, SafeQL does not resolve Tenon fragments and can infer different parameter types from
|
|
428
|
+
those returned at runtime.
|
|
429
|
+
|
|
430
|
+
For multiple databases, pass a connection array to `safeql.configs.connections`. With
|
|
431
|
+
`usersDatabaseUrl` and `billingDatabaseUrl` set to your development/test URLs, replace the single
|
|
432
|
+
connection in the setup above with:
|
|
433
|
+
|
|
434
|
+
```js
|
|
435
|
+
safeql.configs.connections([
|
|
436
|
+
{
|
|
437
|
+
databaseUrl: usersDatabaseUrl,
|
|
438
|
+
plugins: [safeqlPlugin],
|
|
439
|
+
targets: [{ tag: 'usersSql' }],
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
databaseUrl: billingDatabaseUrl,
|
|
443
|
+
plugins: [safeqlPlugin],
|
|
444
|
+
targets: [{ tag: 'billingSql' }],
|
|
445
|
+
},
|
|
446
|
+
]);
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Queries can then share a file:
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
import { sql as usersSql, sql as billingSql } from '@kequtech/tenon-orm';
|
|
453
|
+
|
|
454
|
+
const users = usersSql`SELECT id, email FROM public.users`;
|
|
455
|
+
const invoices = billingSql`SELECT id, total FROM public.invoices`;
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
The aliases only select a checker connection; they are the same runtime `sql` function. Execute each
|
|
459
|
+
query with the correct `createTenon` instance. Neither SafeQL nor Tenon's composition rule follows
|
|
460
|
+
`db.query` calls to infer which database will execute a query. Keep connection targets distinct so a
|
|
461
|
+
query is checked against its intended database.
|
|
462
|
+
|
|
463
|
+
If queries are already separated by file, keep the `sql` tag and use an ESLint config object per
|
|
464
|
+
file group, each extending its own `safeql.configs.connections({ ... })`. Use non-overlapping
|
|
465
|
+
`files` patterns when database settings differ. Normal SafeQL configurations for other libraries can
|
|
466
|
+
coexist; add Tenon's adapter only to connections checking Tenon queries.
|
|
467
|
+
|
|
468
|
+
If a database has same-named tables in different schemas, qualify them in SQL, for example
|
|
469
|
+
`SELECT id FROM billing.accounts`. SafeQL can infer the wrong type for an unqualified table even
|
|
470
|
+
when the connection's `search_path` selects the correct table at execution. Explicit schema names
|
|
471
|
+
avoid that ambiguity.
|
|
472
|
+
|
|
473
|
+
### Supported SQL and limitations
|
|
474
|
+
|
|
475
|
+
Within the configured targets, the adapter recognizes Tenon's `sql` export through imports, local
|
|
476
|
+
aliases, namespace imports, and object properties that retain its TypeScript type. It checks
|
|
477
|
+
constant fragments and `sql.join` with nonempty literal arrays, including nested fragments.
|
|
478
|
+
Separators and `identifier()` names must resolve to literal strings or const string initializers.
|
|
479
|
+
Values use the same parameter placeholders in static checking and execution, inside or outside
|
|
480
|
+
fragments. PostgreSQL infers their types from SQL context; Tenon does not add casts based on
|
|
481
|
+
JavaScript types.
|
|
482
|
+
|
|
483
|
+
The checker follows const fragment initializers, including imported constants with inspectable
|
|
484
|
+
source. It does not inspect function bodies, substitute helper arguments/defaults, or choose
|
|
485
|
+
JavaScript conditional branches. Function-generated fragments and conditional SQL structure are
|
|
486
|
+
unsupported even when a particular helper appears simple or a condition is a literal. Mutable
|
|
487
|
+
references, runtime filter lists, and dynamic identifiers are also unsupported. Ordinary value
|
|
488
|
+
expressions remain bound parameters; calls that produce data for an official mutation helper can
|
|
489
|
+
use its explicit column-list contract.
|
|
490
|
+
|
|
491
|
+
Reusable authorization and other application policy can still live in fragment-returning functions
|
|
492
|
+
at runtime. Keep that policy centralized and cover its behavior with database tests. Suppress
|
|
493
|
+
`tenon/check-composition` only on the particular query, with a reason, when its variants are tested.
|
|
494
|
+
Do not duplicate policy merely to obtain static coverage. SQL and annotation errors in checkable
|
|
495
|
+
queries continue to come from `@ts-safeql/check-sql`.
|
|
496
|
+
|
|
497
|
+
Ordinary functions containing complete `sql` templates remain checkable. Each template is analyzed
|
|
498
|
+
where it is written, including constant fragments defined in that function:
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
function findUsers(email: string) {
|
|
502
|
+
const predicate = sql.fragment`email = ${email}`;
|
|
503
|
+
return sql`SELECT id FROM users WHERE ${predicate}`;
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Use literal line breaks in `sql` templates and bind string values with `${value}`. The composition
|
|
508
|
+
rule rejects JavaScript escapes such as `\n` or `\'` in the outer template: SafeQL reads raw
|
|
509
|
+
template text, while Tenon executes decoded JavaScript text. This diagnostic prevents checking
|
|
510
|
+
different SQL from what runs. Escapes inside bound values are fine; constant fragments are decoded
|
|
511
|
+
by Tenon's adapter before checking.
|
|
512
|
+
|
|
513
|
+
This checks SQL structure and returned-row annotations, not JavaScript input compatibility or
|
|
514
|
+
runtime values. For example, a boolean passed to an integer comparison is rejected by PostgreSQL at
|
|
515
|
+
execution, not by this lint rule. Keep typed function parameters, validate external inputs, and test
|
|
516
|
+
database behavior. Explicit SQL casts are checked as part of the SQL.
|
|
517
|
+
|
|
518
|
+
The adapter supplies scalar mappings for bigint/numeric strings, unknown JSON, and `Date | number`
|
|
519
|
+
temporal values (including infinities). For recursive arrays, use column overrides matching
|
|
520
|
+
generated types, for example:
|
|
521
|
+
|
|
522
|
+
```js
|
|
523
|
+
safeql.configs.connections({
|
|
524
|
+
databaseUrl,
|
|
525
|
+
plugins: [safeqlPlugin],
|
|
526
|
+
targets: [{ tag: 'sql' }],
|
|
527
|
+
overrides: {
|
|
528
|
+
columns: { 'User.tags': 'PgArray<string>', 'User.amounts': 'PgArray<number>' },
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
Import `PgArray` from generated definitions in files using that annotation. Numeric arrays use
|
|
534
|
+
numbers with pg's default parser, even though numeric scalars use strings. Array dimensions/null
|
|
535
|
+
elements, custom parsers, and unverified PostgreSQL types need appropriate overrides and database
|
|
536
|
+
tests; a checker cannot infer your parser policy. Column keys are `Table.column`, so duplicate table
|
|
537
|
+
names across schemas need care. Type overrides merge with the adapter's defaults. `unsafeSql` is
|
|
538
|
+
outside static checking, and arbitrary JSON still needs application validation.
|
|
539
|
+
|
|
540
|
+
## Command reference
|
|
541
|
+
|
|
542
|
+
All database commands use `DATABASE_URL`; environment files are not loaded automatically. For
|
|
543
|
+
multiple databases, run commands with the intended database's URL and migration directory, and use
|
|
544
|
+
distinct generated output files if generating table types.
|
|
545
|
+
|
|
546
|
+
```text
|
|
547
|
+
tenon new <name> [--directory migrations]
|
|
548
|
+
tenon migrate [--directory migrations] [--namespace public]
|
|
549
|
+
tenon status [--directory migrations] [--namespace public]
|
|
550
|
+
tenon rollback [--directory migrations] [--namespace public] [--steps 1]
|
|
551
|
+
tenon generate [--namespace public] [--out src/generated-schema.ts] [--check]
|
|
552
|
+
tenon baseline capture|adopt [--directory migrations] [--namespace public]
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
Migration filenames are `<numeric-id>_<name>.up.sql` and optional matching down files. IDs sort
|
|
556
|
+
numerically. Each migration and its history entry commit together; earlier files remain applied if a
|
|
557
|
+
later file fails. Status reports applied/pending IDs. Rollback checks that every selected file has
|
|
558
|
+
down SQL before executing any. Use dollar-quoted function bodies and explicit `E'...'` for backslash
|
|
559
|
+
escapes. Nontransactional DDL such as `CREATE INDEX CONCURRENTLY` needs a separate procedure.
|
|
560
|
+
|
|
561
|
+
Tools coordinate through session advisory locks per database/namespace. Use direct connections or
|
|
562
|
+
session pooling, with PostgreSQL timeouts to bound waiting.
|
|
563
|
+
|
|
564
|
+
Programmatic `migrate`, `migrationStatus`, and `rollback` accept
|
|
565
|
+
`{ connection, directory?, namespace? }`; rollback also accepts `steps`.
|
|
566
|
+
`createMigration({ name, directory? })` scaffolds files; `readMigrations(directory?)` reads
|
|
567
|
+
validated SQL and checksums. Baseline capture/adoption are covered in the
|
|
568
|
+
[adoption guide](adoption-guide.md).
|