@b4moss/crudian 0.10.0 → 0.11.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,6 +20,7 @@ Install only the peer deps for the adapter you use:
20
20
  | `@b4moss/crudian/drizzle` | Node.js 24+ | sync | `drizzle-orm`, `better-sqlite3` |
21
21
  | `@b4moss/crudian/prisma` | Node.js 24+ | async | `@prisma/client` |
22
22
  | `@b4moss/crudian/libsql` | Node.js 24+ / Bun | async | `@libsql/client` |
23
+ | `@b4moss/crudian/typeorm` | Node.js 24+ / Bun | async | `typeorm` |
23
24
 
24
25
  Node must not import `@b4moss/crudian/bun-sqlite` (resolves to an explicit error stub).
25
26
 
@@ -96,6 +97,7 @@ the client you inject:
96
97
  | Adapter | How to set pool |
97
98
  |---------|-----------------|
98
99
  | **Prisma** | Prisma datasource / connection URL params / engine pool settings (caller-owned). See Prisma docs for your provider. |
100
+ | **TypeORM** | `DataSource` options (caller-owned). |
99
101
  | **bun-sqlite / drizzle (sqlite) / libsql** | No pool settings needed (no-op). |
100
102
 
101
103
  Optional type for docs / future drivers: `PoolOptions` from `@b4moss/crudian`
@@ -119,11 +121,39 @@ await crud.create("items", { name: "alpha", score: 1 })
119
121
 
120
122
  URL / auth token belong on the caller-created client (typically from `.env`).
121
123
 
124
+ ### TypeORM (`DataSource`) — async
125
+
126
+ ```ts
127
+ import "reflect-metadata"
128
+ import { DataSource } from "typeorm"
129
+ import { createCrud } from "@b4moss/crudian/typeorm"
130
+ import { where } from "@b4moss/crudian"
131
+
132
+ const dataSource = new DataSource({
133
+ type: "better-sqlite3",
134
+ database: ":memory:",
135
+ // entities / migrations / pool: caller-owned
136
+ })
137
+ await dataSource.initialize()
138
+
139
+ const crud = createCrud(dataSource)
140
+ await crud.create("items", { name: "alpha", score: 1 })
141
+ await crud.search("items", { where: where().eq("name", "alpha") })
142
+
143
+ // JOIN / QueryBuilder / relation graphs → use crud.db (raw DataSource)
144
+ ```
145
+
146
+ Dialect is inferred from `dataSource.options.type` (`sqlite` / `better-sqlite3` → sqlite, `postgres` → postgres, `mysql` / `mariadb` → mysql). Override with `createCrud(dataSource, { dialect: "postgres" })`.
147
+
148
+ **Boundaries:** callers run migrations; crudian does single-table CRUD by table name. Relations may exist on the schema, but cross-relation CRUD is out of scope (`crud.db`). Subscribers fire only if the write path goes through TypeORM persistence hooks your app configured — treat side effects as caller-owned.
149
+
150
+ Pool / lifetime: configure on the `DataSource` options (same ownership model as Prisma).
151
+
122
152
  ---
123
153
 
124
154
  ## API reference (all methods)
125
155
 
126
- Examples below use the **sync** Bun adapter. For `prisma` / `libsql`, `await` every call (same shapes).
156
+ Examples below use the **sync** Bun adapter. For `prisma` / `libsql` / `typeorm`, `await` every call (same shapes).
127
157
 
128
158
  Assume a table:
129
159
 
@@ -407,9 +437,9 @@ Nestable `and` / `or`. Empty `in([])` is rejected.
407
437
  | Topic | Behavior |
408
438
  |-------|----------|
409
439
  | Entry | `createCrud(db, options?)` — inject a caller-owned client; exposed as `crud.db` |
410
- | Dialects | Shared Dialect layer: SQLite (all adapters), Postgres / MySQL via **Prisma** (`options.dialect`) |
440
+ | Dialects | Shared Dialect layer: SQLite (all adapters), Postgres / MySQL via **Prisma** / **TypeORM** (`options.dialect` or DataSource type) |
411
441
  | Drizzle | **SQLite (better-sqlite3) only** in this line; PG/MySQL subpaths are out of scope |
412
- | Sync vs async | `bun-sqlite` / `drizzle` sync; `prisma` / `libsql` return `Promise`s |
442
+ | Sync vs async | `bun-sqlite` / `drizzle` sync; `prisma` / `libsql` / `typeorm` return `Promise`s |
413
443
  | `read` / `update` / `duplicate` miss | `null` |
414
444
  | `delete` / bulk miss | `0` |
415
445
  | Upsert conflict | application-level on PK (default `id`; no SQL `ON CONFLICT`) |
@@ -417,10 +447,10 @@ Nestable `and` / `or`. Empty `in([])` is rejected.
417
447
  | `columns` | optional on `read` / `search` / `list`; omit → `*` |
418
448
  | `search.total` / `count` | full where count; not page length |
419
449
  | `exists` | boolean presence; same input as `count` |
420
- | Pool | caller-owned; Prisma via datasource settings; SQLite adapters no-op |
450
+ | Pool | caller-owned; Prisma via datasource settings; TypeORM via DataSource options; SQLite adapters no-op |
421
451
  | Errors | minimal `CrudianError`; other errors propagate from the driver |
422
452
  | Identifiers | string required; no format validation |
423
- | Out of scope | relations, migrations, full-text search, ORM models, TypeORM (#43) |
453
+ | Out of scope | relations graph CRUD, migrations ownership, full-text search, Entity-as-first-arg API |
424
454
 
425
455
  ## License
426
456
 
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * - @b4moss/crudian/drizzle
6
6
  * - @b4moss/crudian/prisma
7
7
  * - @b4moss/crudian/libsql
8
+ * - @b4moss/crudian/typeorm
8
9
  */
9
10
  export { CrudianError, assertString } from "./errors.js";
10
11
  export { WhereBuilder, where, isWhereBuilder, type Op, type CondNode, type GroupNode, type WhereNode, } from "./where.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,EACL,YAAY,EACZ,KAAK,EACL,cAAc,EACd,KAAK,EAAE,EACP,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,SAAS,GACf,MAAM,YAAY,CAAA;AACnB,YAAY,EACV,GAAG,EACH,kBAAkB,EAClB,kBAAkB,EAClB,YAAY,EACZ,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,EACV,WAAW,EACX,WAAW,EACX,cAAc,EACd,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,EACL,YAAY,EACZ,KAAK,EACL,cAAc,EACd,KAAK,EAAE,EACP,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,SAAS,GACf,MAAM,YAAY,CAAA;AACnB,YAAY,EACV,GAAG,EACH,kBAAkB,EAClB,kBAAkB,EAClB,YAAY,EACZ,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,EACV,WAAW,EACX,WAAW,EACX,cAAc,EACd,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA"}
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * - @b4moss/crudian/drizzle
6
6
  * - @b4moss/crudian/prisma
7
7
  * - @b4moss/crudian/libsql
8
+ * - @b4moss/crudian/typeorm
8
9
  */
9
10
  export { CrudianError, assertString } from "./errors.js";
10
11
  export { WhereBuilder, where, isWhereBuilder, } from "./where.js";
@@ -0,0 +1,24 @@
1
+ import { type CreateCrudOptions } from "../index.js";
2
+ import { type AsyncSqliteCrud } from "../sqlite/async-crud.js";
3
+ /** Minimal EntityManager / QueryRunner query surface used by the adapter. */
4
+ export type TypeormLikeQuerier = {
5
+ query(query: string, parameters?: unknown[]): Promise<unknown>;
6
+ };
7
+ /**
8
+ * Minimal DataSource surface used by the adapter.
9
+ * Callers own `initialize` / entities / pool configuration.
10
+ */
11
+ export type TypeormLikeDataSource = TypeormLikeQuerier & {
12
+ options: {
13
+ type?: string;
14
+ };
15
+ transaction<T>(runInTransaction: (entityManager: TypeormLikeQuerier) => Promise<T>): Promise<T>;
16
+ };
17
+ export type TypeormCrud = AsyncSqliteCrud<TypeormLikeDataSource>;
18
+ /**
19
+ * Create a Crud bound to a TypeORM DataSource.
20
+ * Methods are async. The injected DataSource is exposed as `crud.db`.
21
+ * Dialect defaults from `dataSource.options.type`; override with `options.dialect`.
22
+ */
23
+ export declare function createCrud(dataSource: TypeormLikeDataSource, options?: CreateCrudOptions): TypeormCrud;
24
+ //# sourceMappingURL=crud.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crud.d.ts","sourceRoot":"","sources":["../../src/typeorm/crud.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAEL,KAAK,eAAe,EACrB,MAAM,yBAAyB,CAAA;AAGhC,6EAA6E;AAC7E,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CAC/D,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG,kBAAkB,GAAG;IACvD,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1B,WAAW,CAAC,CAAC,EACX,gBAAgB,EAAE,CAAC,aAAa,EAAE,kBAAkB,KAAK,OAAO,CAAC,CAAC,CAAC,GAClE,OAAO,CAAC,CAAC,CAAC,CAAA;CACd,CAAA;AAED,MAAM,MAAM,WAAW,GAAG,eAAe,CAAC,qBAAqB,CAAC,CAAA;AA8ChE;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,UAAU,EAAE,qBAAqB,EACjC,OAAO,CAAC,EAAE,iBAAiB,GAC1B,WAAW,CA0Db"}
@@ -0,0 +1,142 @@
1
+ import { CrudianError, } from "../index.js";
2
+ import { resolveDialect } from "../dialect/index.js";
3
+ import { createAsyncCrud, } from "../sqlite/async-crud.js";
4
+ function normalizeRow(row) {
5
+ const out = { ...row };
6
+ for (const [k, v] of Object.entries(out)) {
7
+ if (typeof v === "bigint")
8
+ out[k] = Number(v);
9
+ }
10
+ return out;
11
+ }
12
+ function normalizeQueryResult(result) {
13
+ if (result == null)
14
+ return [];
15
+ if (Array.isArray(result)) {
16
+ return result.filter(isPlainRow);
17
+ }
18
+ if (isPlainRow(result))
19
+ return [result];
20
+ return [];
21
+ }
22
+ function isPlainRow(value) {
23
+ return value != null && typeof value === "object" && !Array.isArray(value);
24
+ }
25
+ function mapDataSourceType(type) {
26
+ switch (type) {
27
+ case "postgres":
28
+ case "aurora-postgres":
29
+ return "postgres";
30
+ case "mysql":
31
+ case "mariadb":
32
+ case "aurora-mysql":
33
+ return "mysql";
34
+ case "sqlite":
35
+ case "better-sqlite3":
36
+ case "capacitor":
37
+ case "cordova":
38
+ case "react-native":
39
+ case "expo":
40
+ case "sqljs":
41
+ case undefined:
42
+ return "sqlite";
43
+ default:
44
+ throw new CrudianError(`unsupported DataSource type: ${String(type)}`);
45
+ }
46
+ }
47
+ /**
48
+ * Create a Crud bound to a TypeORM DataSource.
49
+ * Methods are async. The injected DataSource is exposed as `crud.db`.
50
+ * Dialect defaults from `dataSource.options.type`; override with `options.dialect`.
51
+ */
52
+ export function createCrud(dataSource, options) {
53
+ if (dataSource == null) {
54
+ throw new CrudianError("db is required");
55
+ }
56
+ if (typeof dataSource !== "object" ||
57
+ typeof dataSource.query !== "function" ||
58
+ typeof dataSource.transaction !== "function" ||
59
+ dataSource.options == null ||
60
+ typeof dataSource.options !== "object") {
61
+ throw new CrudianError("db must be a TypeORM DataSource");
62
+ }
63
+ let active = dataSource;
64
+ const dialectName = options?.dialect ?? mapDataSourceType(dataSource.options.type);
65
+ const dialect = resolveDialect(dialectName);
66
+ return createAsyncCrud(dataSource, {
67
+ async run(sql, args = []) {
68
+ if (dialect.name === "postgres") {
69
+ const trimmed = sql.trim();
70
+ if (/^(UPDATE|DELETE)\b/i.test(trimmed)) {
71
+ const wrapped = `WITH _crudian_write AS (${trimmed} RETURNING 1) ` +
72
+ `SELECT COUNT(*)::int AS changes FROM _crudian_write`;
73
+ const rows = normalizeQueryResult(await active.query(wrapped, args));
74
+ return { changes: Number(rows[0]?.changes ?? 0) };
75
+ }
76
+ }
77
+ const result = await active.query(sql, args);
78
+ return { changes: await resolveChanges(active, dialect.name, result) };
79
+ },
80
+ async get(sql, args = []) {
81
+ const rows = normalizeQueryResult(await active.query(sql, args));
82
+ const row = rows[0];
83
+ return row == null ? undefined : normalizeRow(row);
84
+ },
85
+ async all(sql, args = []) {
86
+ return normalizeQueryResult(await active.query(sql, args)).map(normalizeRow);
87
+ },
88
+ async transaction(fn) {
89
+ return dataSource.transaction(async (manager) => {
90
+ const prev = active;
91
+ active = manager;
92
+ try {
93
+ return await fn();
94
+ }
95
+ finally {
96
+ active = prev;
97
+ }
98
+ });
99
+ },
100
+ }, dialect, options);
101
+ }
102
+ async function resolveChanges(querier, dialectName, result) {
103
+ if (dialectName === "sqlite") {
104
+ // better-sqlite3 via TypeORM returns lastInsertRowid for writes, not changes.
105
+ const rows = normalizeQueryResult(await querier.query("SELECT changes() AS changes"));
106
+ return Number(rows[0]?.changes ?? 0);
107
+ }
108
+ if (dialectName === "mysql") {
109
+ const header = mysqlResultHeader(result);
110
+ if (header != null)
111
+ return Number(header.affectedRows ?? 0);
112
+ }
113
+ if (result != null && typeof result === "object" && !Array.isArray(result)) {
114
+ const obj = result;
115
+ if ("affected" in obj)
116
+ return Number(obj.affected ?? 0);
117
+ if ("affectedRows" in obj)
118
+ return Number(obj.affectedRows ?? 0);
119
+ if ("rowCount" in obj)
120
+ return Number(obj.rowCount ?? 0);
121
+ }
122
+ // Postgres (and similar): TypeORM often returns [] and drops rowCount.
123
+ if (dialectName === "postgres" && typeof result === "number") {
124
+ return result;
125
+ }
126
+ return 0;
127
+ }
128
+ function mysqlResultHeader(result) {
129
+ if (result == null)
130
+ return null;
131
+ if (Array.isArray(result)) {
132
+ const first = result[0];
133
+ if (first != null && typeof first === "object" && "affectedRows" in first) {
134
+ return first;
135
+ }
136
+ return null;
137
+ }
138
+ if (typeof result === "object" && "affectedRows" in result) {
139
+ return result;
140
+ }
141
+ return null;
142
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * TypeORM adapter for @b4moss/crudian.
3
+ * Import via: `import { createCrud } from "@b4moss/crudian/typeorm"`
4
+ */
5
+ export { createCrud, type TypeormCrud, type TypeormLikeDataSource, type TypeormLikeQuerier, } from "./crud.js";
6
+ export type { Row, SearchResult, WhereInput, ReadQuery, SearchQuery, CountQuery, DeleteQuery, UpdateQuery, DuplicateQuery, } from "../types.js";
7
+ export { CrudianError, where, WhereBuilder, type Op, type CondNode, type GroupNode, type WhereNode, } from "../index.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/typeorm/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,UAAU,EACV,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,GACxB,MAAM,WAAW,CAAA;AAClB,YAAY,EACV,GAAG,EACH,YAAY,EACZ,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,EACV,WAAW,EACX,WAAW,EACX,cAAc,GACf,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,YAAY,EACZ,KAAK,EACL,YAAY,EACZ,KAAK,EAAE,EACP,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,SAAS,GACf,MAAM,aAAa,CAAA"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * TypeORM adapter for @b4moss/crudian.
3
+ * Import via: `import { createCrud } from "@b4moss/crudian/typeorm"`
4
+ */
5
+ export { createCrud, } from "./crud.js";
6
+ export { CrudianError, where, WhereBuilder, } from "../index.js";
@@ -0,0 +1,13 @@
1
+ import type { DataSource } from "typeorm";
2
+ export type DialectName = "postgres" | "mysql";
3
+ export type TypeormServerDbHarness = {
4
+ dialect: DialectName;
5
+ envKey: "DATABASE_URL_POSTGRES" | "DATABASE_URL_MYSQL";
6
+ createDataSource: (url: string) => DataSource;
7
+ createItemsDDL: string;
8
+ dropItemsDDL: string;
9
+ createAltPkDDL: string;
10
+ dropAltPkDDL: string;
11
+ };
12
+ export declare function registerTypeormServerDialectTests(h: TypeormServerDbHarness): void;
13
+ //# sourceMappingURL=server-dialect-harness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-dialect-harness.d.ts","sourceRoot":"","sources":["../../src/typeorm/server-dialect-harness.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAIzC,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,OAAO,CAAA;AAE9C,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,WAAW,CAAA;IACpB,MAAM,EAAE,uBAAuB,GAAG,oBAAoB,CAAA;IACtD,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,UAAU,CAAA;IAC7C,cAAc,EAAE,MAAM,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,cAAc,EAAE,MAAM,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,wBAAgB,iCAAiC,CAAC,CAAC,EAAE,sBAAsB,QA0I1E"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Shared contract suite for TypeORM against Postgres / MySQL.
3
+ * Skipped automatically when the DATABASE_URL_* env var is unset.
4
+ */
5
+ import assert from "node:assert/strict";
6
+ import { after, before, describe, test } from "node:test";
7
+ import { CrudianError, where } from "../index.js";
8
+ import { createCrud } from "./index.js";
9
+ export function registerTypeormServerDialectTests(h) {
10
+ const url = process.env[h.envKey];
11
+ const suite = describe;
12
+ suite(`typeorm.${h.dialect}`, { skip: !url }, () => {
13
+ let dataSource;
14
+ let crud;
15
+ before(async () => {
16
+ if (!url)
17
+ return;
18
+ dataSource = h.createDataSource(url);
19
+ await dataSource.initialize();
20
+ await dataSource.query(h.dropItemsDDL);
21
+ await dataSource.query(h.createItemsDDL);
22
+ crud = createCrud(dataSource, { dialect: h.dialect });
23
+ });
24
+ after(async () => {
25
+ if (!url || !dataSource?.isInitialized)
26
+ return;
27
+ try {
28
+ await dataSource.query(h.dropItemsDDL);
29
+ await dataSource.query(h.dropAltPkDDL);
30
+ }
31
+ catch {
32
+ /* ignore */
33
+ }
34
+ await dataSource.destroy();
35
+ });
36
+ async function reset() {
37
+ await dataSource.query(h.dropItemsDDL);
38
+ await dataSource.query(h.createItemsDDL);
39
+ }
40
+ test("正常系: create / read / update / delete", async () => {
41
+ await reset();
42
+ const created = await crud.create("items", { name: "alice", score: 10 });
43
+ assert.equal(created.name, "alice");
44
+ assert.ok(created.id != null);
45
+ const got = await crud.read("items", {
46
+ where: where().eq("id", created.id),
47
+ });
48
+ assert.equal(got?.name, "alice");
49
+ const updated = await crud.update("items", { score: 20 }, { where: where().eq("id", created.id) });
50
+ assert.equal(updated?.score, 20);
51
+ const n = await crud.delete("items", {
52
+ where: where().eq("id", created.id),
53
+ });
54
+ assert.equal(n, 1);
55
+ assert.equal(await crud.read("items", { where: where().eq("id", created.id) }), null);
56
+ });
57
+ test("正常系: count / exists / search offset / cursor", async () => {
58
+ await reset();
59
+ await crud.bulkCreate("items", [
60
+ { name: "a", score: 1 },
61
+ { name: "b", score: 2 },
62
+ { name: "c", score: 3 },
63
+ ]);
64
+ assert.equal(await crud.count("items"), 3);
65
+ assert.equal(await crud.exists("items", { where: where().eq("name", "b") }), true);
66
+ assert.equal(await crud.exists("items", { where: where().eq("name", "z") }), false);
67
+ const page = await crud.search("items", { limit: 2, offset: 0 });
68
+ assert.equal(page.items.length, 2);
69
+ assert.equal("total" in page && page.total, 3);
70
+ assert.equal("hasMore" in page && page.hasMore, true);
71
+ const cursorPage = await crud.search("items", {
72
+ paging: "cursor",
73
+ limit: 2,
74
+ });
75
+ assert.equal(cursorPage.items.length, 2);
76
+ assert.ok("nextCursor" in cursorPage);
77
+ });
78
+ test("正常系: upsert / duplicate / transaction", async () => {
79
+ await reset();
80
+ const a = await crud.create("items", { name: "x", score: 1 });
81
+ const up = await crud.upsert("items", {
82
+ id: a.id,
83
+ name: "x",
84
+ score: 9,
85
+ });
86
+ assert.equal(up.score, 9);
87
+ const dup = await crud.duplicate("items", {
88
+ where: where().eq("id", a.id),
89
+ overrides: { name: "y" },
90
+ });
91
+ assert.ok(dup);
92
+ assert.notEqual(dup.id, a.id);
93
+ assert.equal(dup.name, "y");
94
+ await crud.transaction(async () => {
95
+ await crud.create("items", { name: "tx", score: 1 });
96
+ });
97
+ assert.equal(await crud.exists("items", { where: where().eq("name", "tx") }), true);
98
+ });
99
+ test("正常系: 可変 PK", async () => {
100
+ await reset();
101
+ await dataSource.query(h.dropAltPkDDL);
102
+ await dataSource.query(h.createAltPkDDL);
103
+ const alt = createCrud(dataSource, { dialect: h.dialect, pk: "item_id" });
104
+ const row = await alt.create("alt_items", { name: "pk" });
105
+ assert.ok(row.item_id != null);
106
+ const got = await alt.read("alt_items", {
107
+ where: where().eq("item_id", row.item_id),
108
+ });
109
+ assert.equal(got?.name, "pk");
110
+ await dataSource.query(h.dropAltPkDDL);
111
+ });
112
+ test("異常系: 空 in / 不正 dialect は拒否", async () => {
113
+ await reset();
114
+ await assert.rejects(() => crud.search("items", {
115
+ where: where().in("id", []),
116
+ }), CrudianError);
117
+ assert.throws(() => createCrud(dataSource, { dialect: "nope" }), CrudianError);
118
+ });
119
+ });
120
+ }
package/dist/types.d.ts CHANGED
@@ -52,7 +52,7 @@ export type CreateCrudOptions = {
52
52
  pk?: string;
53
53
  /**
54
54
  * SQL dialect. Default: `"sqlite"`.
55
- * Used by Prisma (and future multi-DB adapters). SQLite-only adapters ignore this.
55
+ * Used by Prisma / TypeORM (and other multi-DB adapters). SQLite-only adapters ignore this.
56
56
  */
57
57
  dialect?: CreateCrudDialect;
58
58
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b4moss/crudian",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "CRUD abstraction for DDD repositories",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,6 +30,11 @@
30
30
  "types": "./dist/libsql/index.d.ts",
31
31
  "import": "./dist/libsql/index.js",
32
32
  "default": "./dist/libsql/index.js"
33
+ },
34
+ "./typeorm": {
35
+ "types": "./dist/typeorm/index.d.ts",
36
+ "import": "./dist/typeorm/index.js",
37
+ "default": "./dist/typeorm/index.js"
33
38
  }
34
39
  },
35
40
  "files": [
@@ -43,15 +48,21 @@
43
48
  "prisma:generate": "prisma generate --schema prisma/schema.prisma",
44
49
  "prisma:generate:postgres": "prisma generate --schema prisma/schema.postgres.prisma",
45
50
  "prisma:generate:mysql": "prisma generate --schema prisma/schema.mysql.prisma",
46
- "test": "bun run test:bun-sqlite && bun run test:drizzle && bun run test:prisma && bun run test:libsql",
51
+ "test": "bun run test:bun-sqlite && bun run test:drizzle && bun run test:prisma && bun run test:libsql && bun run test:typeorm && bun run test:dialect",
47
52
  "test:bun-sqlite": "bun test src/bun-sqlite",
48
53
  "test:drizzle": "tsx --test src/drizzle/crud.test.ts src/drizzle/pk.test.ts",
49
54
  "test:prisma": "bun run prisma:generate && tsx --test src/prisma/crud.test.ts src/prisma/pk.test.ts",
50
55
  "test:prisma:postgres": "bun run prisma:generate:postgres && DATABASE_URL_POSTGRES=\"${DATABASE_URL_POSTGRES:-postgresql://crudian:crudian@127.0.0.1:5432/crudian?sslmode=disable}\" tsx --test src/prisma/postgres.test.ts",
51
56
  "test:prisma:mysql": "bun run prisma:generate:mysql && DATABASE_URL_MYSQL=\"${DATABASE_URL_MYSQL:-mysql://crudian:crudian@127.0.0.1:3306/crudian}\" tsx --test src/prisma/mysql.test.ts",
52
- "test:libsql": "tsx --test src/libsql/crud.test.ts src/libsql/pk.test.ts"
53
- },
54
- "engines": {
57
+ "test:libsql": "tsx --test src/libsql/crud.test.ts src/libsql/pk.test.ts",
58
+ "test:dialect": "tsx --test src/dialect/dialect.test.ts",
59
+ "test:node-adapters:coverage": "bun run prisma:generate && c8 --reporter=lcov --reporter=text-summary --report-dir=coverage/node-adapters --exclude='**/*.test.ts' --exclude='prisma/**' --exclude='dist/**' tsx --test src/drizzle/crud.test.ts src/drizzle/pk.test.ts src/prisma/crud.test.ts src/prisma/pk.test.ts src/libsql/crud.test.ts src/libsql/pk.test.ts src/dialect/dialect.test.ts",
60
+ "test:typeorm": "tsx --test src/typeorm/crud.test.ts src/typeorm/pk.test.ts",
61
+ "test:typeorm:bun": "bun test src/typeorm/crud.test.ts src/typeorm/pk.test.ts",
62
+ "test:typeorm:coverage": "c8 --reporter=lcov --reporter=text-summary --report-dir=coverage/typeorm --exclude='**/*.test.ts' --exclude='dist/**' tsx --test src/typeorm/crud.test.ts src/typeorm/pk.test.ts",
63
+ "test:typeorm:postgres": "DATABASE_URL_POSTGRES=\"${DATABASE_URL_POSTGRES:-postgresql://crudian:crudian@127.0.0.1:5432/crudian?sslmode=disable}\" tsx --test src/typeorm/postgres.test.ts",
64
+ "test:typeorm:mysql": "DATABASE_URL_MYSQL=\"${DATABASE_URL_MYSQL:-mysql://crudian:crudian@127.0.0.1:3306/crudian}\" tsx --test src/typeorm/mysql.test.ts"
65
+ }, "engines": {
55
66
  "node": ">=24",
56
67
  "bun": ">=1.0.0"
57
68
  },
@@ -59,7 +70,8 @@
59
70
  "@libsql/client": "^0.14.0 || ^0.15.0 || ^0.17.0",
60
71
  "@prisma/client": "^6.0.0",
61
72
  "better-sqlite3": "^11.0.0",
62
- "drizzle-orm": "^0.39.0 || ^0.40.0 || ^0.41.0 || ^0.44.0"
73
+ "drizzle-orm": "^0.39.0 || ^0.40.0 || ^0.41.0 || ^0.44.0",
74
+ "typeorm": "^0.3.0"
63
75
  },
64
76
  "peerDependenciesMeta": {
65
77
  "@libsql/client": {
@@ -73,6 +85,9 @@
73
85
  },
74
86
  "drizzle-orm": {
75
87
  "optional": true
88
+ },
89
+ "typeorm": {
90
+ "optional": true
76
91
  }
77
92
  },
78
93
  "devDependencies": {
@@ -81,9 +96,14 @@
81
96
  "@types/better-sqlite3": "^7.6.13",
82
97
  "better-sqlite3": "^13",
83
98
  "bun-types": "^1.2.0",
99
+ "c8": "10",
84
100
  "drizzle-orm": "^0.45.2",
101
+ "mysql2": "^3.14.0",
102
+ "pg": "^8.14.0",
85
103
  "prisma": "^6.14.0",
104
+ "reflect-metadata": "^0.2.2",
86
105
  "tsx": "^4.23.12",
106
+ "typeorm": "^0.3.22",
87
107
  "typescript": "^5.8.0"
88
108
  },
89
109
  "keywords": [
@@ -94,6 +114,7 @@
94
114
  "drizzle",
95
115
  "prisma",
96
116
  "libsql",
117
+ "typeorm",
97
118
  "ddd"
98
119
  ],
99
120
  "repository": {