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 +59 -23
- package/dist/{collection-PFmrQHyM.mjs → collection-D9YZn2mL.mjs} +4 -9
- package/dist/{crud-BCWvg5MI.mjs → crud-Di2nvpjB.mjs} +6 -23
- package/dist/{errors-sfFJolfu.mjs → errors-i-gCZnlW.mjs} +14 -1
- package/dist/{factory-BBvIMQuc.mjs → factory-_JPR5fVl.mjs} +5 -4
- package/dist/helpers-CcxHFhtz.mjs +40 -0
- package/dist/{hooks-BD0xy7uw.mjs → hooks-D508wLQg.mjs} +2 -16
- package/dist/index.d.mts +315 -202
- package/dist/index.mjs +782 -397
- package/dist/model-helpers-BBpD3qdv.mjs +14 -0
- package/package.json +3 -7
- package/dist/save-D5UKXvqC.mjs +0 -331
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
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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.
|
|
156
|
-
const { getModelDefFromInstance } = await import("./factory-
|
|
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)
|
|
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 {
|
|
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 {
|
|
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
|
|
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] =
|
|
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 || {}
|
|
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 {
|
|
63
|
+
export { registerTimestampsFor as a, registerSoftDeletesFor as i, getSoftDeleteConfig as n, createHookManager as o, hasSoftDelete as r, getHooksFor as t };
|