peta-orm 0.4.1 → 0.6.0

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
@@ -20,24 +20,67 @@ const page = await Post.query().with("author").orderBy("id", "asc").paginate(1,
20
20
  bun add peta-orm arktype kysely @libsql/kysely-libsql @libsql/client
21
21
  ```
22
22
 
23
+ ### Simple setup (examples, scripts)
24
+
23
25
  ```ts
24
26
  import { createClient } from "@libsql/client"
25
27
  import { LibsqlDialect } from "@libsql/kysely-libsql"
26
28
  import { createORM, defineModel, t } from "peta-orm"
27
29
 
28
- const orm = createORM({
29
- dialect: new LibsqlDialect({ url: "file:my-app.db" }),
30
+ const User = defineModel("users", {
31
+ columns: { id: t.integer().primaryKey(), name: t.string(255), email: t.text().unique() },
30
32
  })
31
33
 
34
+ // Eager init — fine for scripts, one-off tasks
35
+ const client = createClient({ url: "file::memory:?cache=shared" })
36
+ await client.execute(
37
+ "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE)",
38
+ )
39
+
40
+ const orm = createORM({ dialect: new LibsqlDialect({ client }), models: { User } })
41
+
42
+ const user = await User.insert({ name: "Alice", email: "alice@test.com" })
43
+ ```
44
+
45
+ ### Production setup (apps, servers) — no module-level side effects
46
+
47
+ Module-level side effects (database connections, schema init, ORM setup at import time) cause problems with testing, HMR, and error recovery. Use `createDb()` for lazy, safe initialization:
48
+
49
+ ```ts
50
+ import { createClient } from "@libsql/client"
51
+ import { LibsqlDialect } from "@libsql/kysely-libsql"
52
+ import { createDb, createORM, defineModel, t } from "peta-orm"
53
+
32
54
  const User = defineModel("users", {
33
55
  columns: { id: t.integer().primaryKey(), name: t.string(255), email: t.text().unique() },
34
56
  })
35
57
 
36
- orm.registerAll(User)
58
+ async function setup() {
59
+ const client = createClient({ url: "file:my-app.db" })
60
+ await client.execute(
61
+ "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE)",
62
+ )
63
+ const orm = createORM({ dialect: new LibsqlDialect({ client }) })
64
+ orm.registerAll(User)
65
+ return orm
66
+ }
37
67
 
38
- const user = await User.insert({ name: "Alice", email: "alice@test.com" })
68
+ /** Lazy singleton first call creates the connection, subsequent calls reuse it. */
69
+ export const db = createDb(setup)
70
+
71
+ // In route handlers:
72
+ // const orm = await db()
73
+ // const users = await User.query().execute()
39
74
  ```
40
75
 
76
+ The factory function runs **once** on the first `await db()` call. Importing models has zero side effects — no connection, no schema init, no unhandled promises.
77
+
78
+ > [!TIP]
79
+ > For an existing Kysely instance (e.g. from a migration runner), pass it via the `kysely` config option:
80
+ > ```ts
81
+ > const orm = createORM({ kysely: existingKysely })
82
+ > ```
83
+
41
84
  > [!TIP]
42
85
  > See the 32 [runnable examples](./examples) for every feature. Run them with `bun run examples/XX-*.ts`.
43
86
 
@@ -70,7 +113,6 @@ const user = await User.insert({ name: "Alice", email: "alice@test.com" })
70
113
  Column definitions double as validation schemas — no separate validation step needed.
71
114
 
72
115
  ```ts
73
- const t = columnTypes({ schema: createArkTypeSchemaConfig() })
74
116
 
75
117
  const User = defineModel("users", {
76
118
  columns: {
@@ -268,25 +310,10 @@ const posts = await Post.query()
268
310
 
269
311
  ## Migrations
270
312
 
271
- Generate and run migrations from model definitions:
313
+ See the [peta-migrate](../migrate/README.md) package for migration generation and running.
272
314
 
273
315
  ```ts
274
- import { createMigrationRunner, createMigrationGenerator } from "peta-orm/migrator"
275
-
276
- const runner = createMigrationRunner(kysely)
277
- const gen = createMigrationGenerator()
278
-
279
- const code = gen.generateInitialMigration(models)
280
- await runner.up(migrationFiles)
281
- ```
282
-
283
- Or via the CLI:
284
-
285
- ```bash
286
- bun run bin/peta migrate:init
287
- bun run bin/peta migrate:generate
288
- bun run bin/peta migrate:up
289
- bun run bin/peta migrate:status
316
+ import { createMigrationRunner, createMigrationGenerator } from "peta-migrate"
290
317
  ```
291
318
 
292
319
  ---
@@ -352,7 +379,16 @@ cd packages/orm
352
379
  bun test test/integration/
353
380
  ```
354
381
 
355
- Set `INTEGRATION_SKIP_PG=1` or `INTEGRATION_SKIP_MYSQL=1` to skip specific databases.
382
+ ### Environment Variables
383
+
384
+ | Variable | Default | Description |
385
+ |----------|---------|-------------|
386
+ | `INTEGRATION_PG_URL` | `postgres://postgres:postgres@localhost:5432/peta_orm_test` | PostgreSQL connection string |
387
+ | `INTEGRATION_MYSQL_URL` | `mysql://root:mysqlroot@localhost:3306/peta_orm_test` | MySQL connection string |
388
+ | `INTEGRATION_SKIP_PG` | — | Set to `1` to skip PostgreSQL integration tests |
389
+ | `INTEGRATION_SKIP_MYSQL` | — | Set to `1` to skip MySQL integration tests |
390
+
391
+ See [`.env.example`](./.env.example) for a copyable template.
356
392
 
357
393
  ---
358
394
 
@@ -44,7 +44,7 @@ function createCollection(items) {
44
44
  return data.map((d) => d.get(key));
45
45
  },
46
46
  pluck(key) {
47
- return data.map((d) => d.get(key));
47
+ return this.get(key);
48
48
  },
49
49
  groupBy(key) {
50
50
  const result = {};
@@ -72,10 +72,6 @@ function createCollection(items) {
72
72
  forEach(fn) {
73
73
  data.forEach(fn);
74
74
  },
75
- each(fn) {
76
- data.forEach(fn);
77
- return collection;
78
- },
79
75
  unique(key) {
80
76
  if (!key) {
81
77
  const seen = /* @__PURE__ */ new Set();
@@ -152,11 +148,10 @@ function createCollection(items) {
152
148
  },
153
149
  async load(...relations) {
154
150
  if (data.length === 0) return collection;
155
- const { EagerLoader } = await import("./index.mjs").then((n) => n.i);
156
- const { getModelDefFromInstance } = await import("./factory-BBvIMQuc.mjs").then((n) => n.n);
157
- const { getModelDef } = await import("./index.mjs").then((n) => n.n);
151
+ const { EagerLoader } = await import("./index.mjs").then((n) => n.n);
152
+ const { getModelDefFromInstance } = await import("./factory-_JPR5fVl.mjs").then((n) => n.n);
158
153
  const first = data[0];
159
- const def = getModelDefFromInstance(first) ?? getModelDef(first);
154
+ const def = getModelDefFromInstance(first);
160
155
  if (def) {
161
156
  const loader = new EagerLoader();
162
157
  for (const rel of relations) await loader.loadRelated(data, { name: rel }, def);
@@ -1,4 +1,6 @@
1
- import { r as DatabaseError, s as RelationNotFoundError } from "./errors-sfFJolfu.mjs";
1
+ import { c as RelationNotFoundError, i as DatabaseError, n as isUniqueConstraintError, r as normalizeError } from "./errors-i-gCZnlW.mjs";
2
+ import { t as getDb } from "./model-helpers-BBpD3qdv.mjs";
3
+ import { i as resolveTargetId, n as getPivotInfo, t as findRelated } from "./helpers-CcxHFhtz.mjs";
2
4
  //#region src/relations/crud.ts
3
5
  function extractRelationData(def, data) {
4
6
  const columnData = {};
@@ -12,10 +14,6 @@ function extractRelationData(def, data) {
12
14
  relationOps
13
15
  };
14
16
  }
15
- function getDb(def) {
16
- if (!def._orm) throw new Error("Model not registered");
17
- return def._orm.kysely;
18
- }
19
17
  /**
20
18
  * Process relation operations after the parent model has been created.
21
19
  * For belongsTo, the related model must be created FIRST, then its ID
@@ -77,25 +75,10 @@ async function processManyToManyCreate(_instance, relation, op, pkValue) {
77
75
  [foreignPivotKey]: pkValue,
78
76
  [relatedPivotKey]: relatedId
79
77
  }).execute();
80
- } catch {}
78
+ } catch (e) {
79
+ if (!isUniqueConstraintError(e)) throw normalizeError(e, throughTable);
80
+ }
81
81
  }
82
82
  }
83
- function getPivotInfo(relation) {
84
- if (relation.type !== "manyToMany" || !relation.throughTable) throw new Error("Not a many-to-many relation");
85
- return {
86
- throughTable: relation.throughTable,
87
- foreignPivotKey: relation.foreignPivotKey ?? "",
88
- relatedPivotKey: relation.relatedPivotKey ?? ""
89
- };
90
- }
91
- async function findRelated(def, conditions) {
92
- const key = Object.keys(conditions)[0];
93
- return def.query().where(key, "=", conditions[key]).executeTakeFirst();
94
- }
95
- async function resolveTargetId(def, target) {
96
- if (typeof target === "number" || typeof target === "string") return target;
97
- const found = await findRelated(def, target);
98
- if (found) return found.get("id");
99
- }
100
83
  //#endregion
101
84
  export { extractRelationData, processCreateRelations };
@@ -62,8 +62,21 @@ function normalizeError(e, table) {
62
62
  if (raw.code === "ER_BAD_NULL_ERROR" || raw.errno === 1048) return new DatabaseError(`Not null constraint violation on ${table}: ${msg}`, "NOT_NULL_CONSTRAINT", table, msg);
63
63
  return new DatabaseError(msg || "Unknown database error", "UNKNOWN", table, msg);
64
64
  }
65
+ function isUniqueConstraintError(error) {
66
+ if (error instanceof Error) {
67
+ const msg = error.message.toLowerCase();
68
+ if (msg.includes("unique") || msg.includes("sqlite_constraint")) return true;
69
+ if (msg.includes("23505")) return true;
70
+ if (msg.includes("1062") || msg.includes("duplicate entry")) return true;
71
+ }
72
+ if (error && typeof error === "object" && "code" in error) {
73
+ const code = error.code;
74
+ if (code === "23505" || code === "ER_DUP_ENTRY" || code === "SQLITE_CONSTRAINT_UNIQUE") return true;
75
+ }
76
+ return false;
77
+ }
65
78
  //#endregion
66
79
  //#region src/errors.ts
67
80
  var errors_exports = /* @__PURE__ */ __exportAll({ normalizeError: () => normalizeError });
68
81
  //#endregion
69
- export { ModelNotRegisteredError as a, ValidationError as c, ModelNotFoundError as i, normalizeError as n, RelationNotAllowedError as o, DatabaseError as r, RelationNotFoundError as s, errors_exports as t };
82
+ export { ModelNotFoundError as a, RelationNotFoundError as c, DatabaseError as i, ValidationError as l, isUniqueConstraintError as n, ModelNotRegisteredError as o, normalizeError as r, RelationNotAllowedError as s, errors_exports as t };
@@ -39,10 +39,10 @@ function castForSet(value, type) {
39
39
  default: return value;
40
40
  }
41
41
  }
42
- function applyCastsToData(config, data, mode) {
42
+ function applyCastsToData(config, data) {
43
43
  if (!config.casts) return { ...data };
44
44
  const result = { ...data };
45
- for (const [key, type] of Object.entries(config.casts)) if (key in result) result[key] = mode === "get" ? castValue(result[key], type) : prepareForDb(result[key], type);
45
+ for (const [key, type] of Object.entries(config.casts)) if (key in result) result[key] = castValue(result[key], type);
46
46
  return result;
47
47
  }
48
48
  //#endregion
@@ -77,6 +77,7 @@ function getModelDefFromInstance(instance) {
77
77
  return instanceDefs.get(instance);
78
78
  }
79
79
  function createInstance(def, config, data, exists) {
80
+ const validColumns = new Set(Object.keys(def.columns));
80
81
  const instance = {
81
82
  get exists() {
82
83
  return getExists(instance);
@@ -106,7 +107,7 @@ function createInstance(def, config, data, exists) {
106
107
  },
107
108
  fill(data) {
108
109
  const safe = {};
109
- for (const [key, value] of Object.entries(data)) if (!FORBIDDEN_KEYS.has(key)) {
110
+ for (const [key, value] of Object.entries(data)) if (!FORBIDDEN_KEYS.has(key) && validColumns.has(key)) {
110
111
  let v = value;
111
112
  const attrDef = config.attributes?.[key];
112
113
  if (attrDef?.set) v = attrDef.set(v, instance);
@@ -160,7 +161,7 @@ function createInstance(def, config, data, exists) {
160
161
  return getRuntime().modelToJSON(def, instance);
161
162
  }
162
163
  };
163
- if (exists) initState(instance, applyCastsToData(config, data || {}, "get"), true);
164
+ if (exists) initState(instance, applyCastsToData(config, data || {}), true);
164
165
  else {
165
166
  initState(instance, {}, false);
166
167
  if (data && Object.keys(data).length > 0) instance.fill(data);
@@ -0,0 +1,40 @@
1
+ //#region src/relations/helpers.ts
2
+ const THUNK_CACHE = /* @__PURE__ */ new WeakMap();
3
+ function resolveThunk(thunk) {
4
+ let cls = THUNK_CACHE.get(thunk);
5
+ if (!cls) {
6
+ cls = thunk();
7
+ THUNK_CACHE.set(thunk, cls);
8
+ }
9
+ return cls;
10
+ }
11
+ function groupByArray(items, key) {
12
+ const result = {};
13
+ for (const item of items) {
14
+ const v = item.get(key);
15
+ if (v == null) continue;
16
+ const k = String(v);
17
+ if (!result[k]) result[k] = [];
18
+ result[k].push(item);
19
+ }
20
+ return result;
21
+ }
22
+ function getPivotInfo(relation) {
23
+ if (relation.type !== "manyToMany" || !relation.throughTable) throw new Error("Not a many-to-many relation");
24
+ return {
25
+ throughTable: relation.throughTable,
26
+ foreignPivotKey: relation.foreignPivotKey ?? "",
27
+ relatedPivotKey: relation.relatedPivotKey ?? ""
28
+ };
29
+ }
30
+ async function findRelated(def, conditions) {
31
+ const key = Object.keys(conditions)[0];
32
+ return def.query().where(key, "=", conditions[key]).executeTakeFirst();
33
+ }
34
+ async function resolveTargetId(def, target) {
35
+ if (typeof target === "number" || typeof target === "string") return target;
36
+ const found = await findRelated(def, target);
37
+ if (found) return found.get("id");
38
+ }
39
+ //#endregion
40
+ export { resolveThunk as a, resolveTargetId as i, getPivotInfo as n, groupByArray as r, findRelated as t };
@@ -1,4 +1,3 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
1
  //#region src/hooks/index.ts
3
2
  function createHookManager() {
4
3
  const listeners = /* @__PURE__ */ new Map();
@@ -22,27 +21,14 @@ function createHookManager() {
22
21
  const cbs = listeners.get(event);
23
22
  if (cbs) for (const cb of cbs) await cb(model);
24
23
  }
25
- function clone() {
26
- const cloned = createHookManager();
27
- for (const [event, cbs] of listeners) for (const cb of cbs) cloned.on(event, cb);
28
- return cloned;
29
- }
30
24
  return {
31
25
  on,
32
26
  off,
33
- trigger,
34
- clone
27
+ trigger
35
28
  };
36
29
  }
37
30
  //#endregion
38
31
  //#region src/model/hooks.ts
39
- var hooks_exports = /* @__PURE__ */ __exportAll({
40
- getHooksFor: () => getHooksFor,
41
- getSoftDeleteConfig: () => getSoftDeleteConfig,
42
- hasSoftDelete: () => hasSoftDelete,
43
- registerSoftDeletesFor: () => registerSoftDeletesFor,
44
- registerTimestampsFor: () => registerTimestampsFor
45
- });
46
32
  const hookManagers = /* @__PURE__ */ new WeakMap();
47
33
  function getHooksFor(def) {
48
34
  let hm = hookManagers.get(def);
@@ -74,4 +60,4 @@ function registerTimestampsFor(def, createdAtCol = "createdAt", updatedAtCol = "
74
60
  });
75
61
  }
76
62
  //#endregion
77
- export { registerSoftDeletesFor as a, hooks_exports as i, getSoftDeleteConfig as n, registerTimestampsFor as o, hasSoftDelete as r, createHookManager as s, getHooksFor as t };
63
+ export { registerTimestampsFor as a, registerSoftDeletesFor as i, getSoftDeleteConfig as n, createHookManager as o, hasSoftDelete as r, getHooksFor as t };