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 CHANGED
@@ -1,44 +1,246 @@
1
1
  # OKModel
2
2
 
3
- Catalog-first TypeScript ORM for PostgreSQL. Version 0.1.
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
- postgres.js and PGlite are optional peers. Install the one you connect with.
33
+ PGlite is the in-process driver.
12
34
 
13
35
  ```sh
14
- bun add postgres
15
- # or
16
- bun add @electric-sql/pglite
36
+ bun add okmodel @electric-sql/pglite
17
37
  ```
18
38
 
19
- The repository docs have the [quickstart](docs/quickstart.md), the [production checklist](docs/production.md), and the [known limits](docs/known-limits.md). Those files are not in the npm tarball.
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
- export const notes = table("notes", {
26
- id: t.uuid(),
27
- title: t.text(),
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
- export const app = schema({ tables: [notes] });
91
+ const tables = [authors, notes];
31
92
 
32
- export default defineConfig({ schema: "./schema.ts" });
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
- `table` and `index` for a schema come from `okmodel/pg`. Stable, experimental, and internal exports are listed in `tests/fixtures/api-surface.json` in the repository. `okmodel/internal` has no stability promise.
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
- A production target sets `protected: true`. A production `connect` sets `requireMeta: true`. `prepared: "named"` is not for a transaction-mode pooler.
242
+ ## Docs
43
243
 
44
- Apache-2.0.
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)
@@ -6,7 +6,7 @@ import {
6
6
  } from "../../shared/fgqgsb5z.js";
7
7
  import {
8
8
  createClient
9
- } from "../../shared/f5thc1s0.js";
9
+ } from "../../shared/rgs0st28.js";
10
10
  import {
11
11
  PGLITE_CAPABILITIES,
12
12
  readCapability2,
@@ -3,10 +3,10 @@ import {
3
3
  } from "../../shared/hd293j9g.js";
4
4
  import {
5
5
  open2
6
- } from "../../shared/7ck0dtry.js";
6
+ } from "../../shared/dkn3y60a.js";
7
7
  import {
8
8
  createClient
9
- } from "../../shared/f5thc1s0.js";
9
+ } from "../../shared/rgs0st28.js";
10
10
  import {
11
11
  POSTGRESJS_CAPABILITIES,
12
12
  readCapability2,
@@ -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
- /** Closes the pool when this client opened it. */
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.identity) === stable(after.definition.identity) && stable(before.definition.generated) === stable(after.definition.generated);
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 === "sequence" && identitySequence(object, source))
924
+ if (object.kind !== "sequence")
925
+ return false;
926
+ if (identitySequence(object, source))
898
927
  return true;
899
- return false;
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 "./7ck0dtry.js";
3
+ } from "./dkn3y60a.js";
4
4
  import"./y9m15dfv.js";
5
5
  import {
6
6
  loadMigrations
7
- } from "./4j72449d.js";
7
+ } from "./jjb7yht9.js";
8
8
  import {
9
9
  assertTargetPolicy,
10
10
  listTargets,
11
11
  loadConfig
12
- } from "./kynx7vbm.js";
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 sql = postgres(config.url, {
135
+ const plain = config.ssl === undefined;
136
+ const sockets = new PoolSockets;
137
+ const sql = postgres(config.url, driverOptions(config, {
24
138
  max,
25
- prepare: named,
26
- idle_timeout: 30,
27
- connect_timeout: 5,
28
- types: WIRE_TYPES,
29
- ...config.ssl !== undefined ? { ssl: config.ssl } : {},
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
- const { checkout } = await import("./tb4ze1tr.js");
47
- const connection = await checkout(held);
48
- try {
49
- return await connection.batch(statements, options);
50
- } finally {
51
- await connection.release();
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: () => import("./tb4ze1tr.js").then((mod) => mod.checkout(held)),
55
- describe: (text, params) => import("./tb4ze1tr.js").then((mod) => mod.describe(sql, text, params)),
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
- async listen(channel, onNotify) {
60
- const listening = await sql.listen(channel, onNotify);
61
- return async () => {
62
- await listening.unlisten();
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("./tb4ze1tr.js");
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 "./f5thc1s0.js";
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
- const selected = pk === undefined ? -1 : columns.findIndex((column) => column.field === pk);
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: pk === undefined ? undefined : table.columns.get(pk), anchor: columns.length };
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);
@@ -4,7 +4,7 @@ import {
4
4
  } from "./y9m15dfv.js";
5
5
  import {
6
6
  parsePlan
7
- } from "./kynx7vbm.js";
7
+ } from "./05w44d8g.js";
8
8
  import {
9
9
  OkmError2
10
10
  } from "./ycwz42az.js";
@@ -2,7 +2,7 @@ import"./y9m15dfv.js";
2
2
  import {
3
3
  listTargets,
4
4
  loadConfig
5
- } from "./kynx7vbm.js";
5
+ } from "./05w44d8g.js";
6
6
 
7
7
  // src/tooling/migrate/dev.ts
8
8
  import { mkdirSync } from "node:fs";
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  open2
3
- } from "./7ck0dtry.js";
3
+ } from "./dkn3y60a.js";
4
4
  import {
5
5
  catalogHash2
6
6
  } from "./y9m15dfv.js";
7
7
  import {
8
8
  loadMigrations
9
- } from "./4j72449d.js";
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 "./kynx7vbm.js";
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
- return options.ownsPool ? pool.close() : Promise.resolve();
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("./ckb1p0a4.js");
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("./px90pf9p.js")).decodeIncluded(plan, result.rows, table);
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("./px90pf9p.js")).decodeIncluded;
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("./px90pf9p.js").then((mod) => mod.includeHooks);
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 "./f5thc1s0.js";
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("./79ggp945.js");
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 = [];
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PgSession
3
- } from "./7ck0dtry.js";
3
+ } from "./dkn3y60a.js";
4
4
  import {
5
5
  driverErrors,
6
6
  rejectClosed,
@@ -4,7 +4,7 @@ import {
4
4
  list,
5
5
  isRecord,
6
6
  fail
7
- } from "./f5thc1s0.js";
7
+ } from "./rgs0st28.js";
8
8
  import {
9
9
  throwNamed2
10
10
  } from "./ycwz42az.js";
@@ -5,7 +5,7 @@ import {
5
5
  generateProject,
6
6
  planProject,
7
7
  checkProject
8
- } from "../shared/kynx7vbm.js";
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/vkm3q4x1.js");
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/g2v6gnc8.js");
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/g2v6gnc8.js");
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/ax16zpvm.js");
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.0",
4
- "description": "Catalog-first TypeScript ORM: typed queries, safe migrations and replica-aware routing. PostgreSQL first, zero runtime dependencies.",
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
- "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 compiler-api && bun run bundle-purity && bun run build && bun test && bun run publint && bun run attw && bun run size",
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",