okmodel 0.1.0 → 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/README.md +219 -17
- package/dist/runtime/pg/pglite.js +1 -1
- package/dist/runtime/pg/postgresjs.js +2 -2
- package/dist/runtime/types.d.ts +18 -2
- package/dist/shared/{kynx7vbm.js → 05w44d8g.js} +37 -3
- package/dist/shared/{ax16zpvm.js → 523ft3e8.js} +3 -3
- package/dist/shared/{7ck0dtry.js → dkn3y60a.js} +146 -31
- package/dist/shared/{px90pf9p.js → hadrfb9h.js} +5 -3
- package/dist/shared/{4j72449d.js → jjb7yht9.js} +1 -1
- package/dist/shared/{vkm3q4x1.js → kprcwaha.js} +1 -1
- package/dist/shared/{g2v6gnc8.js → m3ypagh5.js} +3 -3
- package/dist/shared/{f5thc1s0.js → rgs0st28.js} +21 -5
- package/dist/shared/{ckb1p0a4.js → x5x59srr.js} +2 -2
- package/dist/shared/{tb4ze1tr.js → xqbhg6cn.js} +1 -1
- package/dist/shared/{79ggp945.js → yd1kzwn2.js} +1 -1
- package/dist/tooling/cli.js +5 -5
- package/package.json +18 -3
package/README.md
CHANGED
|
@@ -1,44 +1,246 @@
|
|
|
1
1
|
# OKModel
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
okmodel is a catalog-first TypeScript ORM, PostgreSQL first. Other SQL databases are the direction. Version 0.1 supports PostgreSQL only. The schema is the single source. Migrations and queries come from it.
|
|
4
|
+
|
|
5
|
+
Version 0.1.1. Apache-2.0.
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- [Install](#install)
|
|
10
|
+
- [Quickstart](#quickstart)
|
|
11
|
+
- [Configure](#configure)
|
|
12
|
+
- [Schema](#schema)
|
|
13
|
+
- [Push](#push)
|
|
14
|
+
- [Reviewed migrations](#reviewed-migrations)
|
|
15
|
+
- [One client](#one-client)
|
|
16
|
+
- [Insert](#insert)
|
|
17
|
+
- [Find](#find)
|
|
18
|
+
- [Errors](#errors)
|
|
19
|
+
- [Close](#close)
|
|
20
|
+
- [Commands](#commands)
|
|
21
|
+
- [Roadmap](#roadmap)
|
|
22
|
+
- [Size](#size)
|
|
23
|
+
- [Docs](#docs)
|
|
4
24
|
|
|
5
25
|
## Install
|
|
6
26
|
|
|
27
|
+
Install okmodel and the postgres.js driver.
|
|
28
|
+
|
|
7
29
|
```sh
|
|
8
|
-
bun add okmodel
|
|
30
|
+
bun add okmodel postgres
|
|
9
31
|
```
|
|
10
32
|
|
|
11
|
-
|
|
33
|
+
PGlite is the in-process driver.
|
|
12
34
|
|
|
13
35
|
```sh
|
|
14
|
-
bun add
|
|
15
|
-
# or
|
|
16
|
-
bun add @electric-sql/pglite
|
|
36
|
+
bun add okmodel @electric-sql/pglite
|
|
17
37
|
```
|
|
18
38
|
|
|
19
|
-
|
|
39
|
+
okmodel has no runtime dependencies. The driver is a peer.
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
`DATABASE_URL` is a direct Postgres URL, not a pooler. The client, insert, find, and error blocks are one script, `run.ts`. `script.ts` is the `await using` form. The last block calls `db.close()`.
|
|
44
|
+
|
|
45
|
+
### Configure
|
|
46
|
+
|
|
47
|
+
Point the CLI at the schema and at that URL.
|
|
48
|
+
|
|
49
|
+
`okmodel.config.ts`:
|
|
20
50
|
|
|
21
51
|
```ts
|
|
22
|
-
import { schema, table, t } from "okmodel/pg";
|
|
23
52
|
import { defineConfig } from "okmodel/migrate";
|
|
24
53
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
54
|
+
const url = process.env.DATABASE_URL;
|
|
55
|
+
if (url === undefined || url.length === 0) {
|
|
56
|
+
throw new Error("DATABASE_URL is not set");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default defineConfig({
|
|
60
|
+
schema: "./schema.ts",
|
|
61
|
+
database: url,
|
|
28
62
|
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Schema
|
|
66
|
+
|
|
67
|
+
`t.identity()` is the primary key. An insert omits the id, and the row that comes back carries it. Identity ids come back as strings by default. `t.identity({ as: "number" })` returns numbers. Push and reviewed migrations are two ways to do the same job: pick one per database.
|
|
68
|
+
|
|
69
|
+
Tested on PostgreSQL 15 to 18.
|
|
70
|
+
|
|
71
|
+
`schema.ts`:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { one, schema, table, t } from "okmodel/pg";
|
|
75
|
+
|
|
76
|
+
const authors = table("authors", {
|
|
77
|
+
id: t.identity(),
|
|
78
|
+
name: t.text().unique(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const notes = table(
|
|
82
|
+
"notes",
|
|
83
|
+
{
|
|
84
|
+
id: t.identity(),
|
|
85
|
+
title: t.text(),
|
|
86
|
+
authorId: t.bigint().references("authors", { columns: ["id"] }),
|
|
87
|
+
},
|
|
88
|
+
{ relations: { author: one("authors") } },
|
|
89
|
+
);
|
|
29
90
|
|
|
30
|
-
|
|
91
|
+
const tables = [authors, notes];
|
|
31
92
|
|
|
32
|
-
export default
|
|
93
|
+
export default schema({ tables });
|
|
33
94
|
```
|
|
34
95
|
|
|
96
|
+
### Push
|
|
97
|
+
|
|
98
|
+
Create the tables with push, which is allowed because this target is not protected.
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
bunx okm push
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
A protected target refuses push. Production settings are in the [production checklist](https://github.com/omqkhafi/okmodel/blob/main/docs/production.md).
|
|
105
|
+
|
|
106
|
+
### Reviewed migrations
|
|
107
|
+
|
|
108
|
+
Generate writes a SQL file and `.okm`, and apply runs that file later so you can read the migration first.
|
|
109
|
+
|
|
35
110
|
```sh
|
|
36
|
-
bunx okm build
|
|
37
111
|
bunx okm generate init
|
|
112
|
+
bunx okm migrate apply
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### One client
|
|
116
|
+
|
|
117
|
+
This module is cached, so the process has one client.
|
|
118
|
+
|
|
119
|
+
`db.ts`:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { connect } from "okmodel/pg/postgresjs";
|
|
123
|
+
|
|
124
|
+
import schema from "./schema.ts";
|
|
125
|
+
|
|
126
|
+
const url = process.env.DATABASE_URL;
|
|
127
|
+
if (url === undefined || url.length === 0) throw new Error("DATABASE_URL is not set");
|
|
128
|
+
|
|
129
|
+
export const db = connect(url, { schema });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Insert
|
|
133
|
+
|
|
134
|
+
Insert an author, then a note that points at the author's id.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { db } from "./db.ts";
|
|
138
|
+
|
|
139
|
+
const author = await db.authors.insert({ name: "Ada" });
|
|
140
|
+
const note = await db.notes.insert({
|
|
141
|
+
title: "hello",
|
|
142
|
+
authorId: author.id,
|
|
143
|
+
});
|
|
144
|
+
if (author.name !== "Ada" || note.title !== "hello" || author.id.length === 0) {
|
|
145
|
+
throw new Error("insert did not return the row");
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Find
|
|
150
|
+
|
|
151
|
+
Load that note and its author by the id the insert returned.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const found = await db.notes.find({
|
|
155
|
+
where: { id: note.id },
|
|
156
|
+
limit: 5,
|
|
157
|
+
include: { author: true },
|
|
158
|
+
});
|
|
159
|
+
const row = found[0];
|
|
160
|
+
if (row?.title !== "hello" || row.author?.name !== "Ada") {
|
|
161
|
+
throw new Error("find did not return the note and its author");
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Errors
|
|
166
|
+
|
|
167
|
+
`safe` returns the `OkmError` instead of throwing when the author name is already used.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { OkmError, safe } from "okmodel";
|
|
171
|
+
|
|
172
|
+
const duplicate = await safe(db.authors.insert({ name: "Ada" }));
|
|
173
|
+
if (duplicate.ok || !(duplicate.error instanceof OkmError) || duplicate.error.kind !== "unique") {
|
|
174
|
+
throw new Error("expected an OkmError of kind unique");
|
|
175
|
+
}
|
|
38
176
|
```
|
|
39
177
|
|
|
40
|
-
|
|
178
|
+
### Close
|
|
179
|
+
|
|
180
|
+
A script exits when its queries finish. It does not need `close()` for that. `await using` closes the pool at the end of the block. `db.close()` is that call on a line you choose. A server keeps the client and its connections. A pool opened with `ssl` still waits out the driver's 30 second idle timer.
|
|
181
|
+
|
|
182
|
+
`script.ts`:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { connect } from "okmodel/pg/postgresjs";
|
|
186
|
+
|
|
187
|
+
import schema from "./schema.ts";
|
|
188
|
+
|
|
189
|
+
const url = process.env.DATABASE_URL;
|
|
190
|
+
if (url === undefined || url.length === 0) throw new Error("DATABASE_URL is not set");
|
|
191
|
+
|
|
192
|
+
await using db = connect(url, { schema });
|
|
193
|
+
const author = await db.authors.insert({ name: "Lin" });
|
|
194
|
+
if (author.name !== "Lin" || author.id.length === 0) {
|
|
195
|
+
throw new Error("insert did not return the row");
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
await db.close();
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Commands
|
|
204
|
+
|
|
205
|
+
| Command | What it does |
|
|
206
|
+
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
207
|
+
| `okm build` | Validates the schema and writes `.okm/` (catalog, hash, emitted row types). `okm generate` writes those files too. |
|
|
208
|
+
| `okm check` | Reports a stale `renamedFrom` and a table file the schema does not import. |
|
|
209
|
+
| `okm generate [name]` | Writes a SQL migration and `.okm/`. The name defaults to `migration`. Prints `no changes` when the schema matches. |
|
|
210
|
+
| `okm dev` | Uses a target named `dev`, or creates a PGlite database in `.okm/dev-db`. It does not apply migrations or write the catalog. |
|
|
211
|
+
| `okm push` | Applies the schema directly. Refused when the target is protected. |
|
|
212
|
+
| `okm migrate plan <name>` | Prints the plan and its class. The name is required. |
|
|
213
|
+
| `okm migrate apply` | Replays migration files on the database. |
|
|
214
|
+
| `okm migrate status` | Prints version, catalog hash, and state for each target. |
|
|
215
|
+
| `okm --version` | Prints the package version. |
|
|
216
|
+
|
|
217
|
+
`okmodel` and `okm` are the same command.
|
|
218
|
+
|
|
219
|
+
## Roadmap
|
|
220
|
+
|
|
221
|
+
- [x] 0.1 — Schema, queries, and migrations on PostgreSQL, with postgres.js and PGlite.
|
|
222
|
+
- [ ] 0.2 — Hidden and sensitive fields, validation, traits, tenancy, archive and restore, richer relations, presets, transactions, and operators for JSON, arrays, ranges, and search.
|
|
223
|
+
- [ ] 0.3 — Extensions, domains, functions, triggers, views, roles, and grants.
|
|
224
|
+
- [ ] 0.4 — Safer migration plans, backfill, drift checks, provisioning, reference data, and a testing package.
|
|
225
|
+
- [ ] 0.5 — A primary with replicas, read routing, and a reference app.
|
|
226
|
+
|
|
227
|
+
`okmodel/internal` has no stability promise. Names on that subpath can change or disappear in any release.
|
|
228
|
+
|
|
229
|
+
Each limit, with the version that lifts it, is in [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
|
|
230
|
+
|
|
231
|
+
## Size
|
|
232
|
+
|
|
233
|
+
Measured on this release.
|
|
234
|
+
|
|
235
|
+
| | Minified | Gzip | Cold import |
|
|
236
|
+
| --------------------------------- | -------: | -----: | ----------: |
|
|
237
|
+
| Runtime entry | 5,525 | 2,042 | 1.758 ms |
|
|
238
|
+
| App startup (10 tables, one find) | 77,537 | 25,625 | 9.922 ms |
|
|
239
|
+
|
|
240
|
+
The rest of the measurements are in [size](https://github.com/omqkhafi/okmodel/blob/main/docs/size.md).
|
|
41
241
|
|
|
42
|
-
|
|
242
|
+
## Docs
|
|
43
243
|
|
|
44
|
-
|
|
244
|
+
- [Quickstart](https://github.com/omqkhafi/okmodel/blob/main/docs/quickstart.md)
|
|
245
|
+
- [Production checklist](https://github.com/omqkhafi/okmodel/blob/main/docs/production.md)
|
|
246
|
+
- [Known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md)
|
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
} from "../../shared/hd293j9g.js";
|
|
4
4
|
import {
|
|
5
5
|
open2
|
|
6
|
-
} from "../../shared/
|
|
6
|
+
} from "../../shared/dkn3y60a.js";
|
|
7
7
|
import {
|
|
8
8
|
createClient
|
|
9
|
-
} from "../../shared/
|
|
9
|
+
} from "../../shared/rgs0st28.js";
|
|
10
10
|
import {
|
|
11
11
|
POSTGRESJS_CAPABILITIES,
|
|
12
12
|
readCapability2,
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -305,17 +305,33 @@ export type TableApi<S extends QuerySchema, K extends keyof S["~byName"] & strin
|
|
|
305
305
|
readonly where?: WhereOf<S, K>;
|
|
306
306
|
}, options?: WriteOptions<S, K>): Write<WriteCount>;
|
|
307
307
|
};
|
|
308
|
+
/**
|
|
309
|
+
* `[Symbol.asyncDispose]` when `Symbol` defines it.
|
|
310
|
+
*
|
|
311
|
+
* The check stays in the built declaration. A consumer `lib` without
|
|
312
|
+
* `asyncDispose` does not see the method, including with `skipLibCheck` off.
|
|
313
|
+
*/
|
|
314
|
+
type IfAsyncDisposable<S> = S extends {
|
|
315
|
+
readonly asyncDispose: infer D;
|
|
316
|
+
} ? D extends symbol ? {
|
|
317
|
+
[K in D]: () => Promise<void>;
|
|
318
|
+
} : object : object;
|
|
308
319
|
/** A client typed by its own schema. */
|
|
309
320
|
export type Connected<S extends QuerySchema> = {
|
|
310
321
|
readonly [K in keyof S["~byName"] & string]: TableApi<S, K>;
|
|
311
322
|
} & {
|
|
312
323
|
/** Looks up a table by name. An unknown name is OKM1120. */
|
|
313
324
|
table<K extends keyof S["~byName"] & string>(name: K): TableApi<S, K>;
|
|
314
|
-
/**
|
|
325
|
+
/**
|
|
326
|
+
* Closes the pool when this client opened it.
|
|
327
|
+
*
|
|
328
|
+
* A second call waits on the same close. A client that adopted a pool
|
|
329
|
+
* resolves without closing it.
|
|
330
|
+
*/
|
|
315
331
|
close(): Promise<void>;
|
|
316
332
|
/** Resolves when the dialect and `requires` checks have finished. */
|
|
317
333
|
readonly connected: Promise<void>;
|
|
318
|
-
}
|
|
334
|
+
} & IfAsyncDisposable<typeof Symbol>;
|
|
319
335
|
/**
|
|
320
336
|
* A catalog `okm build` wrote.
|
|
321
337
|
*
|
|
@@ -253,6 +253,8 @@ function planMigration(request) {
|
|
|
253
253
|
const enums = enumEdits(beforeBy, afterBy, schema);
|
|
254
254
|
requireReplacements(picklists, enums, replacements);
|
|
255
255
|
recreateDependents(beforeList, afterList, alters, dropKeys, createKeys, picklists);
|
|
256
|
+
omitOwnedSequences(createKeys, afterBy, request.after);
|
|
257
|
+
omitOwnedSequences(dropKeys, beforeBy, renamed);
|
|
256
258
|
const steps = [];
|
|
257
259
|
steps.push(...renameSteps(request.before, renames, schema));
|
|
258
260
|
steps.push(...keptNames.map((item) => renameShapeStep(item, schema)));
|
|
@@ -284,6 +286,9 @@ function planMigration(request) {
|
|
|
284
286
|
for (const sql of alterColumnSql(change.before, change.after, schema)) {
|
|
285
287
|
steps.push(step(sql, alterClass(sql), "ddl", ACCESS));
|
|
286
288
|
}
|
|
289
|
+
const identitySql = identityChangeSql(change.before, change.after, schema);
|
|
290
|
+
if (identitySql !== undefined)
|
|
291
|
+
steps.push(step(identitySql, "expand", "ddl", ACCESS));
|
|
287
292
|
}
|
|
288
293
|
const createdTables = new Set([...createKeys].map((key) => afterBy.get(key)).filter((object) => object?.kind === "table").map((object) => object.identity.name));
|
|
289
294
|
const emitted = new Set;
|
|
@@ -875,7 +880,29 @@ function columnNullable(source, table, column) {
|
|
|
875
880
|
return false;
|
|
876
881
|
}
|
|
877
882
|
function columnAlterable(before, after) {
|
|
878
|
-
return stable(before.definition.
|
|
883
|
+
return stable(before.definition.generated) === stable(after.definition.generated);
|
|
884
|
+
}
|
|
885
|
+
function identityChangeSql(before, after, schema) {
|
|
886
|
+
if (stable(before.definition.identity) === stable(after.definition.identity))
|
|
887
|
+
return;
|
|
888
|
+
const table = qualify2(schema, after.identity.parent.name);
|
|
889
|
+
const name = quoteIdent(after.identity.name);
|
|
890
|
+
const next = after.definition.identity;
|
|
891
|
+
const previous = before.definition.identity;
|
|
892
|
+
if (next === undefined)
|
|
893
|
+
return `alter table ${table} alter column ${name} drop identity`;
|
|
894
|
+
if (previous === undefined) {
|
|
895
|
+
const generated = next.always ? "always" : "by default";
|
|
896
|
+
return `alter table ${table} alter column ${name} add generated ${generated} as identity`;
|
|
897
|
+
}
|
|
898
|
+
return `alter table ${table} alter column ${name} set generated ${next.always ? "always" : "by default"}`;
|
|
899
|
+
}
|
|
900
|
+
function omitOwnedSequences(keys, by, source) {
|
|
901
|
+
for (const key of keys) {
|
|
902
|
+
const object = by.get(key);
|
|
903
|
+
if (object?.kind === "sequence" && folded(object, source))
|
|
904
|
+
keys.delete(key);
|
|
905
|
+
}
|
|
879
906
|
}
|
|
880
907
|
function sameDefinition(left, right) {
|
|
881
908
|
return stable(left.definition) === stable(right.definition);
|
|
@@ -894,9 +921,16 @@ function folded(object, source) {
|
|
|
894
921
|
return true;
|
|
895
922
|
if (object.kind === "constraint" && object.definition.constraintKind === "primaryKey")
|
|
896
923
|
return true;
|
|
897
|
-
if (object.kind
|
|
924
|
+
if (object.kind !== "sequence")
|
|
925
|
+
return false;
|
|
926
|
+
if (identitySequence(object, source))
|
|
898
927
|
return true;
|
|
899
|
-
|
|
928
|
+
const name = object.identity.name;
|
|
929
|
+
return source.objects.some((column) => {
|
|
930
|
+
if (column.kind !== "column" || column.definition.identity === undefined)
|
|
931
|
+
return false;
|
|
932
|
+
return name === `${column.identity.parent.name}_${column.identity.name}_seq`;
|
|
933
|
+
});
|
|
900
934
|
}
|
|
901
935
|
function anchoredParent(object) {
|
|
902
936
|
if (object.kind === "column" || object.kind === "index" || object.kind === "constraint") {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
open2
|
|
3
|
-
} from "./
|
|
3
|
+
} from "./dkn3y60a.js";
|
|
4
4
|
import"./y9m15dfv.js";
|
|
5
5
|
import {
|
|
6
6
|
loadMigrations
|
|
7
|
-
} from "./
|
|
7
|
+
} from "./jjb7yht9.js";
|
|
8
8
|
import {
|
|
9
9
|
assertTargetPolicy,
|
|
10
10
|
listTargets,
|
|
11
11
|
loadConfig
|
|
12
|
-
} from "./
|
|
12
|
+
} from "./05w44d8g.js";
|
|
13
13
|
import {
|
|
14
14
|
OkmError2
|
|
15
15
|
} from "./ycwz42az.js";
|
|
@@ -10,7 +10,119 @@ import {
|
|
|
10
10
|
} from "./xy79j5hq.js";
|
|
11
11
|
|
|
12
12
|
// src/adapters/pg/postgresjs.ts
|
|
13
|
+
import net from "node:net";
|
|
13
14
|
import postgres from "postgres";
|
|
15
|
+
class PoolSockets {
|
|
16
|
+
held = [];
|
|
17
|
+
busy = 0;
|
|
18
|
+
listeners = 0;
|
|
19
|
+
hostCursor = 0;
|
|
20
|
+
connect(target) {
|
|
21
|
+
const path = typeof target.path === "string" ? target.path : undefined;
|
|
22
|
+
const hosts = target.host;
|
|
23
|
+
const ports = target.port;
|
|
24
|
+
const index = hosts.length === 0 ? 0 : this.hostCursor % hosts.length;
|
|
25
|
+
this.hostCursor += 1;
|
|
26
|
+
const host = hosts[index] ?? "localhost";
|
|
27
|
+
const port = ports[index] ?? ports[0] ?? 5432;
|
|
28
|
+
const socket = path !== undefined ? net.connect(path) : net.connect(port, host);
|
|
29
|
+
this.held.push(socket);
|
|
30
|
+
return socket;
|
|
31
|
+
}
|
|
32
|
+
occupy(run) {
|
|
33
|
+
this.busy += 1;
|
|
34
|
+
this.wake();
|
|
35
|
+
return run().finally(() => {
|
|
36
|
+
this.busy -= 1;
|
|
37
|
+
this.park();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async hold(checkout) {
|
|
41
|
+
this.busy += 1;
|
|
42
|
+
this.wake();
|
|
43
|
+
try {
|
|
44
|
+
const connection = await checkout();
|
|
45
|
+
let released = false;
|
|
46
|
+
const release = connection.release.bind(connection);
|
|
47
|
+
connection.release = () => {
|
|
48
|
+
if (released)
|
|
49
|
+
return Promise.resolve();
|
|
50
|
+
released = true;
|
|
51
|
+
return release().finally(() => {
|
|
52
|
+
this.busy -= 1;
|
|
53
|
+
this.park();
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
return connection;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
this.busy -= 1;
|
|
59
|
+
this.park();
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async* stream(open) {
|
|
64
|
+
this.busy += 1;
|
|
65
|
+
this.wake();
|
|
66
|
+
try {
|
|
67
|
+
yield* open();
|
|
68
|
+
} finally {
|
|
69
|
+
this.busy -= 1;
|
|
70
|
+
this.park();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async listen(start) {
|
|
74
|
+
this.listeners += 1;
|
|
75
|
+
this.wake();
|
|
76
|
+
try {
|
|
77
|
+
const stop = await start();
|
|
78
|
+
let stopped = false;
|
|
79
|
+
return async () => {
|
|
80
|
+
if (stopped)
|
|
81
|
+
return;
|
|
82
|
+
stopped = true;
|
|
83
|
+
try {
|
|
84
|
+
await stop();
|
|
85
|
+
} finally {
|
|
86
|
+
this.listeners -= 1;
|
|
87
|
+
this.park();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
} catch (error) {
|
|
91
|
+
this.listeners -= 1;
|
|
92
|
+
this.park();
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
wake() {
|
|
97
|
+
for (const socket of this.held)
|
|
98
|
+
socket.ref();
|
|
99
|
+
}
|
|
100
|
+
park() {
|
|
101
|
+
if (this.busy > 0 || this.listeners > 0)
|
|
102
|
+
return;
|
|
103
|
+
for (const socket of this.held)
|
|
104
|
+
socket.unref();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function driverOptions(config, state) {
|
|
108
|
+
return {
|
|
109
|
+
max: state.max,
|
|
110
|
+
prepare: state.named,
|
|
111
|
+
idle_timeout: state.plain ? 0 : 30,
|
|
112
|
+
...state.plain ? { max_lifetime: null } : {},
|
|
113
|
+
connect_timeout: 5,
|
|
114
|
+
types: WIRE_TYPES,
|
|
115
|
+
...state.plain ? { socket: (options) => state.sockets.connect(options) } : {},
|
|
116
|
+
...config.ssl !== undefined ? { ssl: config.ssl } : {},
|
|
117
|
+
connection: {
|
|
118
|
+
application_name: "okmodel",
|
|
119
|
+
...config.searchPath !== undefined ? { search_path: config.searchPath } : {}
|
|
120
|
+
},
|
|
121
|
+
onnotice(notice) {
|
|
122
|
+
state.notices.push(notice);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
14
126
|
function open2(config) {
|
|
15
127
|
const max = config.max ?? 10;
|
|
16
128
|
const acquireMs = config.timeouts?.acquire;
|
|
@@ -20,47 +132,50 @@ function open2(config) {
|
|
|
20
132
|
let closed = false;
|
|
21
133
|
const isClosed = () => closed;
|
|
22
134
|
const named = config.prepared === "named";
|
|
23
|
-
const
|
|
135
|
+
const plain = config.ssl === undefined;
|
|
136
|
+
const sockets = new PoolSockets;
|
|
137
|
+
const sql = postgres(config.url, driverOptions(config, {
|
|
24
138
|
max,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
connection: {
|
|
31
|
-
application_name: "okmodel",
|
|
32
|
-
...config.searchPath !== undefined ? { search_path: config.searchPath } : {}
|
|
33
|
-
},
|
|
34
|
-
onnotice(notice) {
|
|
35
|
-
notices.push(notice);
|
|
36
|
-
}
|
|
37
|
-
});
|
|
139
|
+
named,
|
|
140
|
+
plain,
|
|
141
|
+
notices,
|
|
142
|
+
sockets
|
|
143
|
+
}));
|
|
38
144
|
const root = new PgSession(sql, counters, "pool", poolInflight, isClosed, undefined, notices, named);
|
|
39
145
|
const held = { sql, acquireMs, counters, isClosed, notices, named, poolInflight };
|
|
40
146
|
return {
|
|
41
147
|
capabilities: POSTGRESJS_CAPABILITIES,
|
|
42
148
|
execute(text, params, options) {
|
|
43
|
-
return runCall(closed, options, (watch) => root.query(text, params, watch));
|
|
149
|
+
return sockets.occupy(() => runCall(closed, options, (watch) => root.query(text, params, watch)));
|
|
44
150
|
},
|
|
45
151
|
async batch(statements, options) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
152
|
+
return sockets.occupy(async () => {
|
|
153
|
+
const { checkout } = await import("./xqbhg6cn.js");
|
|
154
|
+
const connection = await checkout(held);
|
|
155
|
+
try {
|
|
156
|
+
return await connection.batch(statements, options);
|
|
157
|
+
} finally {
|
|
158
|
+
await connection.release();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
53
161
|
},
|
|
54
|
-
reserve
|
|
55
|
-
|
|
162
|
+
reserve() {
|
|
163
|
+
return sockets.hold(async () => {
|
|
164
|
+
const mod = await import("./xqbhg6cn.js");
|
|
165
|
+
return mod.checkout(held);
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
describe: (text, params) => sockets.occupy(() => import("./xqbhg6cn.js").then((mod) => mod.describe(sql, text, params))),
|
|
56
169
|
stream(text, params) {
|
|
57
|
-
return loadStream(sql, text, params);
|
|
170
|
+
return sockets.stream(() => loadStream(sql, text, params));
|
|
58
171
|
},
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
172
|
+
listen(channel, onNotify) {
|
|
173
|
+
return sockets.listen(async () => {
|
|
174
|
+
const listening = await sql.listen(channel, onNotify);
|
|
175
|
+
return async () => {
|
|
176
|
+
await listening.unlisten();
|
|
177
|
+
};
|
|
178
|
+
});
|
|
64
179
|
},
|
|
65
180
|
cancel() {
|
|
66
181
|
for (const pending of poolInflight)
|
|
@@ -179,7 +294,7 @@ class PgSession {
|
|
|
179
294
|
}
|
|
180
295
|
}
|
|
181
296
|
async function* loadStream(sql, text, params) {
|
|
182
|
-
const { stream } = await import("./
|
|
297
|
+
const { stream } = await import("./xqbhg6cn.js");
|
|
183
298
|
yield* stream(sql, text, params);
|
|
184
299
|
}
|
|
185
300
|
function send(sql, text, params, named) {
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
list,
|
|
16
16
|
isRecord,
|
|
17
17
|
fail
|
|
18
|
-
} from "./
|
|
18
|
+
} from "./rgs0st28.js";
|
|
19
19
|
import {
|
|
20
20
|
throwNamed2
|
|
21
21
|
} from "./ycwz42az.js";
|
|
@@ -304,10 +304,12 @@ function parseInclude(schema, name, relation, value, all) {
|
|
|
304
304
|
}
|
|
305
305
|
function anchored(table, columns) {
|
|
306
306
|
const pk = table.model.primary[0];
|
|
307
|
-
|
|
307
|
+
if (pk === undefined)
|
|
308
|
+
return { presence: undefined, anchor: 0 };
|
|
309
|
+
const selected = columns.findIndex((column) => column.field === pk);
|
|
308
310
|
if (selected >= 0)
|
|
309
311
|
return { presence: undefined, anchor: selected };
|
|
310
|
-
return { presence:
|
|
312
|
+
return { presence: table.columns.get(pk), anchor: columns.length };
|
|
311
313
|
}
|
|
312
314
|
function childIndex(schema, relation) {
|
|
313
315
|
const child = indexes(schema).get(relation.table);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
open2
|
|
3
|
-
} from "./
|
|
3
|
+
} from "./dkn3y60a.js";
|
|
4
4
|
import {
|
|
5
5
|
catalogHash2
|
|
6
6
|
} from "./y9m15dfv.js";
|
|
7
7
|
import {
|
|
8
8
|
loadMigrations
|
|
9
|
-
} from "./
|
|
9
|
+
} from "./jjb7yht9.js";
|
|
10
10
|
import {
|
|
11
11
|
quoteIdent,
|
|
12
12
|
planMigration,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
selectTarget,
|
|
16
16
|
projectHead,
|
|
17
17
|
loadConfig
|
|
18
|
-
} from "./
|
|
18
|
+
} from "./05w44d8g.js";
|
|
19
19
|
import {
|
|
20
20
|
OkmError2
|
|
21
21
|
} from "./ycwz42az.js";
|
|
@@ -711,6 +711,7 @@ function createClient(schema, pool, options) {
|
|
|
711
711
|
for (const name of Object.keys(schema.model)) {
|
|
712
712
|
tables[name] = tableApi(session, name);
|
|
713
713
|
}
|
|
714
|
+
let closing;
|
|
714
715
|
const client = {
|
|
715
716
|
...tables,
|
|
716
717
|
table(name) {
|
|
@@ -726,12 +727,27 @@ function createClient(schema, pool, options) {
|
|
|
726
727
|
}
|
|
727
728
|
},
|
|
728
729
|
close() {
|
|
729
|
-
|
|
730
|
+
if (!options.ownsPool)
|
|
731
|
+
return Promise.resolve();
|
|
732
|
+
closing ??= pool.close();
|
|
733
|
+
return closing;
|
|
730
734
|
},
|
|
731
735
|
connected
|
|
732
736
|
};
|
|
737
|
+
attachAsyncDispose(client, () => client.close());
|
|
733
738
|
return client;
|
|
734
739
|
}
|
|
740
|
+
function attachAsyncDispose(client, close) {
|
|
741
|
+
const symbol = Symbol.asyncDispose;
|
|
742
|
+
if (typeof symbol !== "symbol")
|
|
743
|
+
return;
|
|
744
|
+
Object.defineProperty(client, symbol, {
|
|
745
|
+
configurable: true,
|
|
746
|
+
enumerable: false,
|
|
747
|
+
writable: true,
|
|
748
|
+
value: () => close()
|
|
749
|
+
});
|
|
750
|
+
}
|
|
735
751
|
function tableApi(session, table) {
|
|
736
752
|
return {
|
|
737
753
|
find(options = {}) {
|
|
@@ -759,7 +775,7 @@ function tableApi(session, table) {
|
|
|
759
775
|
}
|
|
760
776
|
var writers;
|
|
761
777
|
function loadWrite() {
|
|
762
|
-
writers ??= import("./
|
|
778
|
+
writers ??= import("./x5x59srr.js");
|
|
763
779
|
return writers;
|
|
764
780
|
}
|
|
765
781
|
function writeHandle(session, op, table, input, options, mods) {
|
|
@@ -834,7 +850,7 @@ function start(session, op, table, options, mods) {
|
|
|
834
850
|
const { plan, params } = await prepare();
|
|
835
851
|
try {
|
|
836
852
|
const result = await session.pool.execute(plan.text, params, callOptions(mods));
|
|
837
|
-
const value = plan.outputs.includes.length === 0 ? decodeResult(plan, result.rows, table) : (await import("./
|
|
853
|
+
const value = plan.outputs.includes.length === 0 ? decodeResult(plan, result.rows, table) : (await import("./hadrfb9h.js")).decodeIncluded(plan, result.rows, table);
|
|
838
854
|
if (mods.required === true && value === null) {
|
|
839
855
|
throw new OkmError2("not_found", `one() on ${table} matched no row.`, withHttp(session.http, { kind: "not_found", table }));
|
|
840
856
|
}
|
|
@@ -909,7 +925,7 @@ async function* streamRows(session, call, prepare) {
|
|
|
909
925
|
})));
|
|
910
926
|
}
|
|
911
927
|
const { plan, params } = await prepare();
|
|
912
|
-
const decode = plan.outputs.includes.length === 0 ? decodeResult : (await import("./
|
|
928
|
+
const decode = plan.outputs.includes.length === 0 ? decodeResult : (await import("./hadrfb9h.js")).decodeIncluded;
|
|
913
929
|
for await (const chunk of session.pool.stream(plan.text, params)) {
|
|
914
930
|
const decoded = decode(plan, chunk, call.table);
|
|
915
931
|
if (!Array.isArray(decoded))
|
|
@@ -1002,7 +1018,7 @@ var includeHooks;
|
|
|
1002
1018
|
function hooksFor(call) {
|
|
1003
1019
|
if (call.include === undefined)
|
|
1004
1020
|
return Promise.resolve(undefined);
|
|
1005
|
-
includeHooks ??= import("./
|
|
1021
|
+
includeHooks ??= import("./hadrfb9h.js").then((mod) => mod.includeHooks);
|
|
1006
1022
|
return includeHooks;
|
|
1007
1023
|
}
|
|
1008
1024
|
function bindNow(schema, call) {
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
quote,
|
|
9
9
|
isRecord,
|
|
10
10
|
fail
|
|
11
|
-
} from "./
|
|
11
|
+
} from "./rgs0st28.js";
|
|
12
12
|
import {
|
|
13
13
|
isOperator,
|
|
14
14
|
operatorName,
|
|
@@ -78,7 +78,7 @@ async function planInsert(table, input, options, mods) {
|
|
|
78
78
|
keyFields: undefined
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
|
-
const conflictMod = options.onConflict === undefined || options.onConflict === "error" ? undefined : await import("./
|
|
81
|
+
const conflictMod = options.onConflict === undefined || options.onConflict === "error" ? undefined : await import("./yd1kzwn2.js");
|
|
82
82
|
const conflict = conflictMod?.readConflict(table, options.onConflict);
|
|
83
83
|
const chunks = chunkRows(rows.map((row) => cellsFor(table, columns, row)));
|
|
84
84
|
const statements = [];
|
package/dist/tooling/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
generateProject,
|
|
6
6
|
planProject,
|
|
7
7
|
checkProject
|
|
8
|
-
} from "../shared/
|
|
8
|
+
} from "../shared/05w44d8g.js";
|
|
9
9
|
import {
|
|
10
10
|
OkmError2
|
|
11
11
|
} from "../shared/ycwz42az.js";
|
|
@@ -41,12 +41,12 @@ async function run(argv, io) {
|
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
if (command === "dev") {
|
|
44
|
-
const { devProject } = await import("../shared/
|
|
44
|
+
const { devProject } = await import("../shared/kprcwaha.js");
|
|
45
45
|
stdout(await devProject(cwd));
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
if (command === "push") {
|
|
49
|
-
const { pushProject } = await import("../shared/
|
|
49
|
+
const { pushProject } = await import("../shared/m3ypagh5.js");
|
|
50
50
|
stdout(await pushProject(cwd, invoke(splitFlags(rest))));
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
@@ -61,12 +61,12 @@ async function run(argv, io) {
|
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
63
|
if (sub === "apply") {
|
|
64
|
-
const { applyProject } = await import("../shared/
|
|
64
|
+
const { applyProject } = await import("../shared/m3ypagh5.js");
|
|
65
65
|
stdout(await applyProject(cwd, invoke(parsed)));
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
if (sub === "status") {
|
|
69
|
-
const { statusProject } = await import("../shared/
|
|
69
|
+
const { statusProject } = await import("../shared/523ft3e8.js");
|
|
70
70
|
stdout(await statusProject(cwd, invoke(parsed)));
|
|
71
71
|
return;
|
|
72
72
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "okmodel",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "okmodel is a catalog-first TypeScript ORM, PostgreSQL first. Other SQL databases are the direction. Version 0.1 supports PostgreSQL only. The schema is the single source. Migrations and queries come from it.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"migrations",
|
|
7
|
+
"orm",
|
|
8
|
+
"postgres",
|
|
9
|
+
"postgresql",
|
|
10
|
+
"typescript"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/omqkhafi/okmodel",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/omqkhafi/okmodel/issues"
|
|
15
|
+
},
|
|
5
16
|
"license": "Apache-2.0",
|
|
6
17
|
"author": "Omq Khafi",
|
|
7
18
|
"repository": {
|
|
@@ -60,13 +71,17 @@
|
|
|
60
71
|
"layers-check": "bun ./scripts/layers-check.ts",
|
|
61
72
|
"core-purity": "bun ./scripts/core-purity.ts",
|
|
62
73
|
"docs:check": "bun ./scripts/docs-check.ts",
|
|
74
|
+
"readme:check": "bun ./scripts/readme-check.ts",
|
|
63
75
|
"compiler-api": "bun ./scripts/compiler-api.ts",
|
|
64
76
|
"publint": "publint run . --strict --pack bun",
|
|
65
77
|
"attw": "bun ./scripts/attw.ts",
|
|
66
78
|
"size": "bun ./scripts/size.ts",
|
|
67
79
|
"bundle-purity": "bun ./scripts/bundle-purity.ts",
|
|
68
80
|
"test": "bun test",
|
|
69
|
-
"
|
|
81
|
+
"test:postgres": "bun ./scripts/postgres-suite.ts",
|
|
82
|
+
"test:tarball": "bun ./scripts/tarball-smoke.ts",
|
|
83
|
+
"verify": "bun ./scripts/verify.ts",
|
|
84
|
+
"check": "bun run format:check && bun run lint && bun run typecheck && bun run editor-check && bun run type-cost && bun run layers-check && bun run core-purity && bun run docs:check && bun run readme:check && bun run compiler-api && bun run bundle-purity && bun run build && bun test && bun run publint && bun run attw && bun run size",
|
|
70
85
|
"bump": "bun ./scripts/bump-version.ts",
|
|
71
86
|
"release-check": "bun ./scripts/release-check.ts",
|
|
72
87
|
"db:up": "bun ./packages/harness/src/docker-cli.ts up",
|