@b4moss/crudian 0.9.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.
Files changed (55) hide show
  1. package/README.md +64 -10
  2. package/dist/bun-sqlite/sql.d.ts +5 -1
  3. package/dist/bun-sqlite/sql.d.ts.map +1 -1
  4. package/dist/bun-sqlite/sql.js +7 -1
  5. package/dist/dialect/index.d.ts +7 -0
  6. package/dist/dialect/index.d.ts.map +1 -0
  7. package/dist/dialect/index.js +19 -0
  8. package/dist/dialect/mysql.d.ts +4 -0
  9. package/dist/dialect/mysql.d.ts.map +1 -0
  10. package/dist/dialect/mysql.js +24 -0
  11. package/dist/dialect/postgres.d.ts +4 -0
  12. package/dist/dialect/postgres.d.ts.map +1 -0
  13. package/dist/dialect/postgres.js +25 -0
  14. package/dist/dialect/sqlite.d.ts +4 -0
  15. package/dist/dialect/sqlite.d.ts.map +1 -0
  16. package/dist/dialect/sqlite.js +23 -0
  17. package/dist/dialect/types.d.ts +25 -0
  18. package/dist/dialect/types.d.ts.map +1 -0
  19. package/dist/dialect/types.js +5 -0
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/pool.d.ts +18 -0
  24. package/dist/pool.d.ts.map +1 -0
  25. package/dist/pool.js +1 -0
  26. package/dist/prisma/crud.d.ts +2 -1
  27. package/dist/prisma/crud.d.ts.map +1 -1
  28. package/dist/prisma/crud.js +7 -4
  29. package/dist/prisma/server-dialect-harness.d.ts +16 -0
  30. package/dist/prisma/server-dialect-harness.d.ts.map +1 -0
  31. package/dist/prisma/server-dialect-harness.js +120 -0
  32. package/dist/sqlite/async-crud.d.ts +4 -0
  33. package/dist/sqlite/async-crud.d.ts.map +1 -1
  34. package/dist/sqlite/async-crud.js +57 -36
  35. package/dist/sqlite/pk.d.ts +6 -13
  36. package/dist/sqlite/pk.d.ts.map +1 -1
  37. package/dist/sqlite/pk.js +13 -6
  38. package/dist/sqlite/sql.d.ts +10 -1
  39. package/dist/sqlite/sql.d.ts.map +1 -1
  40. package/dist/sqlite/sql.js +60 -23
  41. package/dist/sqlite/sync-crud.d.ts +4 -0
  42. package/dist/sqlite/sync-crud.d.ts.map +1 -1
  43. package/dist/sqlite/sync-crud.js +48 -35
  44. package/dist/typeorm/crud.d.ts +24 -0
  45. package/dist/typeorm/crud.d.ts.map +1 -0
  46. package/dist/typeorm/crud.js +142 -0
  47. package/dist/typeorm/index.d.ts +8 -0
  48. package/dist/typeorm/index.d.ts.map +1 -0
  49. package/dist/typeorm/index.js +6 -0
  50. package/dist/typeorm/server-dialect-harness.d.ts +13 -0
  51. package/dist/typeorm/server-dialect-harness.d.ts.map +1 -0
  52. package/dist/typeorm/server-dialect-harness.js +120 -0
  53. package/dist/types.d.ts +7 -0
  54. package/dist/types.d.ts.map +1 -1
  55. package/package.json +31 -6
@@ -1,15 +1,16 @@
1
1
  import { CrudianError, assertString, } from "../index.js";
2
+ import { sqliteDialect } from "../dialect/sqlite.js";
2
3
  import { createPkGuard, resolvePk } from "./pk.js";
3
- import { compileWhere, quoteIdent, resolveWhere } from "./sql.js";
4
+ import { compileWhere, resolveWhere } from "./sql.js";
4
5
  function requireWhere(query, label) {
5
6
  if (query.where === undefined || query.where === null) {
6
7
  throw new CrudianError(`${label} requires where`);
7
8
  }
8
9
  }
9
- function selectColumns(columns) {
10
+ function selectColumns(d, columns) {
10
11
  if (!columns || columns.length === 0)
11
12
  return "*";
12
- return columns.map((c) => quoteIdent(c)).join(", ");
13
+ return columns.map((c) => d.quoteIdent(c)).join(", ");
13
14
  }
14
15
  function rowFromObject(value) {
15
16
  if (value === null || typeof value !== "object") {
@@ -17,9 +18,11 @@ function rowFromObject(value) {
17
18
  }
18
19
  return { ...value };
19
20
  }
20
- export function createSyncSqliteCrud(db, ex, options) {
21
+ /** Dialect-aware sync CRUD factory. */
22
+ export function createSyncCrud(db, ex, dialect, options) {
21
23
  const pk = resolvePk(options);
22
- const ensurePkColumn = createPkGuard(pk, ex.get, ex.all);
24
+ const ensurePkColumn = createPkGuard(pk, dialect, ex.all);
25
+ const d = dialect;
23
26
  const crud = {
24
27
  db,
25
28
  create(table, cols) {
@@ -32,23 +35,24 @@ export function createSyncSqliteCrud(db, ex, options) {
32
35
  if (keys.length === 0) {
33
36
  throw new CrudianError("cols must not be empty");
34
37
  }
35
- const tbl = quoteIdent(table);
36
- const colSql = keys.map((k) => quoteIdent(k)).join(", ");
37
- const placeholders = keys.map(() => "?").join(", ");
38
+ const tbl = d.quoteIdent(table);
39
+ const colSql = keys.map((k) => d.quoteIdent(k)).join(", ");
40
+ let idx = 1;
41
+ const placeholders = keys.map(() => d.placeholder(idx++)).join(", ");
38
42
  const args = keys.map((k) => cols[k]);
39
43
  ex.run(`INSERT INTO ${tbl} (${colSql}) VALUES (${placeholders})`, args);
40
44
  const pkValue = Object.prototype.hasOwnProperty.call(cols, pk)
41
45
  ? cols[pk]
42
- : Number(ex.get("SELECT last_insert_rowid() AS id")?.id);
43
- const row = ex.get(`SELECT * FROM ${tbl} WHERE ${quoteIdent(pk)} = ?`, [pkValue]);
46
+ : Number(ex.get(d.lastInsertIdSql())?.id);
47
+ const row = ex.get(`SELECT * FROM ${tbl} WHERE ${d.quoteIdent(pk)} = ${d.placeholder(1)}`, [pkValue]);
44
48
  return rowFromObject(row);
45
49
  },
46
50
  read(table, query = {}) {
47
51
  assertString(table, "table");
48
52
  ensurePkColumn(table);
49
- const tbl = quoteIdent(table);
50
- const where = compileWhere(resolveWhere(query.where));
51
- const sql = `SELECT ${selectColumns(query.columns)} FROM ${tbl}` +
53
+ const tbl = d.quoteIdent(table);
54
+ const where = compileWhere(d, resolveWhere(query.where));
55
+ const sql = `SELECT ${selectColumns(d, query.columns)} FROM ${tbl}` +
52
56
  (where.sql ? ` WHERE ${where.sql}` : "") +
53
57
  ` LIMIT 1`;
54
58
  const row = ex.get(sql, where.args);
@@ -65,25 +69,27 @@ export function createSyncSqliteCrud(db, ex, options) {
65
69
  if (keys.length === 0) {
66
70
  throw new CrudianError("cols must not be empty");
67
71
  }
68
- const tbl = quoteIdent(table);
69
- const where = compileWhere(resolveWhere(query.where));
72
+ const tbl = d.quoteIdent(table);
73
+ let idx = 1;
74
+ const sets = keys.map((k) => `${d.quoteIdent(k)} = ${d.placeholder(idx++)}`).join(", ");
75
+ const where = compileWhere(d, resolveWhere(query.where), idx);
70
76
  if (!where.sql) {
71
77
  throw new CrudianError("update requires where");
72
78
  }
73
- const sets = keys.map((k) => `${quoteIdent(k)} = ?`).join(", ");
74
79
  const args = [...keys.map((k) => cols[k]), ...where.args];
75
80
  const result = ex.run(`UPDATE ${tbl} SET ${sets} WHERE ${where.sql}`, args);
76
81
  if (result.changes === 0)
77
82
  return null;
78
- const row = ex.get(`SELECT * FROM ${tbl} WHERE ${where.sql} LIMIT 1`, where.args);
83
+ const fetchWhere = compileWhere(d, resolveWhere(query.where));
84
+ const row = ex.get(`SELECT * FROM ${tbl} WHERE ${fetchWhere.sql} LIMIT 1`, fetchWhere.args);
79
85
  return row == null ? null : rowFromObject(row);
80
86
  },
81
87
  delete(table, query) {
82
88
  assertString(table, "table");
83
89
  ensurePkColumn(table);
84
90
  requireWhere(query, "delete");
85
- const tbl = quoteIdent(table);
86
- const where = compileWhere(resolveWhere(query.where));
91
+ const tbl = d.quoteIdent(table);
92
+ const where = compileWhere(d, resolveWhere(query.where));
87
93
  if (!where.sql) {
88
94
  throw new CrudianError("delete requires where");
89
95
  }
@@ -112,8 +118,8 @@ export function createSyncSqliteCrud(db, ex, options) {
112
118
  typeof query.cursor !== "string") {
113
119
  throw new CrudianError("cursor must be a number, string, or null");
114
120
  }
115
- const tbl = quoteIdent(table);
116
- const where = compileWhere(resolveWhere(query.where));
121
+ const tbl = d.quoteIdent(table);
122
+ const where = compileWhere(d, resolveWhere(query.where));
117
123
  const total = crud.count(table, { where: query.where });
118
124
  if (paging === "offset") {
119
125
  const offset = query.offset ?? 0;
@@ -121,10 +127,11 @@ export function createSyncSqliteCrud(db, ex, options) {
121
127
  throw new CrudianError("offset must be a non-negative number");
122
128
  }
123
129
  const args = [...where.args];
130
+ let idx = where.nextIndex;
124
131
  const whereSql = where.sql ? ` WHERE ${where.sql}` : "";
125
- const sql = `SELECT ${selectColumns(query.columns)} FROM ${tbl}` +
132
+ const sql = `SELECT ${selectColumns(d, query.columns)} FROM ${tbl}` +
126
133
  whereSql +
127
- ` ORDER BY ${quoteIdent(pk)} ASC LIMIT ? OFFSET ?`;
134
+ ` ORDER BY ${d.quoteIdent(pk)} ASC LIMIT ${d.placeholder(idx++)} OFFSET ${d.placeholder(idx++)}`;
128
135
  args.push(limit, offset);
129
136
  const items = ex.all(sql, args).map((r) => rowFromObject(r));
130
137
  return {
@@ -136,17 +143,18 @@ export function createSyncSqliteCrud(db, ex, options) {
136
143
  };
137
144
  }
138
145
  const args = [...where.args];
146
+ let idx = where.nextIndex;
139
147
  const parts = [];
140
148
  if (where.sql)
141
149
  parts.push(`(${where.sql})`);
142
150
  if (query.cursor != null) {
143
- parts.push(`${quoteIdent(pk)} > ?`);
151
+ parts.push(`${d.quoteIdent(pk)} > ${d.placeholder(idx++)}`);
144
152
  args.push(query.cursor);
145
153
  }
146
154
  const whereSql = parts.length > 0 ? ` WHERE ${parts.join(" AND ")}` : "";
147
- const sql = `SELECT ${selectColumns(query.columns)} FROM ${tbl}` +
155
+ const sql = `SELECT ${selectColumns(d, query.columns)} FROM ${tbl}` +
148
156
  whereSql +
149
- ` ORDER BY ${quoteIdent(pk)} ASC LIMIT ?`;
157
+ ` ORDER BY ${d.quoteIdent(pk)} ASC LIMIT ${d.placeholder(idx++)}`;
150
158
  args.push(limit + 1);
151
159
  const rows = ex.all(sql, args).map((r) => rowFromObject(r));
152
160
  const hasMore = rows.length > limit;
@@ -164,9 +172,9 @@ export function createSyncSqliteCrud(db, ex, options) {
164
172
  count(table, query = {}) {
165
173
  assertString(table, "table");
166
174
  ensurePkColumn(table);
167
- const tbl = quoteIdent(table);
168
- const where = compileWhere(resolveWhere(query.where));
169
- const sql = `SELECT COUNT(*) AS ${quoteIdent("row_count")} FROM ${tbl}` +
175
+ const tbl = d.quoteIdent(table);
176
+ const where = compileWhere(d, resolveWhere(query.where));
177
+ const sql = `SELECT COUNT(*) AS ${d.quoteIdent("row_count")} FROM ${tbl}` +
170
178
  (where.sql ? ` WHERE ${where.sql}` : "");
171
179
  const row = ex.get(sql, where.args);
172
180
  return Number(row?.row_count ?? 0);
@@ -174,9 +182,9 @@ export function createSyncSqliteCrud(db, ex, options) {
174
182
  exists(table, query = {}) {
175
183
  assertString(table, "table");
176
184
  ensurePkColumn(table);
177
- const tbl = quoteIdent(table);
178
- const where = compileWhere(resolveWhere(query.where));
179
- const sql = `SELECT 1 AS ${quoteIdent("ok")} FROM ${tbl}` +
185
+ const tbl = d.quoteIdent(table);
186
+ const where = compileWhere(d, resolveWhere(query.where));
187
+ const sql = `SELECT 1 AS ${d.quoteIdent("ok")} FROM ${tbl}` +
180
188
  (where.sql ? ` WHERE ${where.sql}` : "") +
181
189
  ` LIMIT 1`;
182
190
  return ex.get(sql, where.args) != null;
@@ -260,12 +268,13 @@ export function createSyncSqliteCrud(db, ex, options) {
260
268
  if (keys.length === 0) {
261
269
  throw new CrudianError("cols must not be empty");
262
270
  }
263
- const tbl = quoteIdent(table);
264
- const where = compileWhere(resolveWhere(query.where));
271
+ const tbl = d.quoteIdent(table);
272
+ let idx = 1;
273
+ const sets = keys.map((k) => `${d.quoteIdent(k)} = ${d.placeholder(idx++)}`).join(", ");
274
+ const where = compileWhere(d, resolveWhere(query.where), idx);
265
275
  if (!where.sql) {
266
276
  throw new CrudianError("bulkUpdate requires where");
267
277
  }
268
- const sets = keys.map((k) => `${quoteIdent(k)} = ?`).join(", ");
269
278
  const args = [...keys.map((k) => cols[k]), ...where.args];
270
279
  const result = ex.run(`UPDATE ${tbl} SET ${sets} WHERE ${where.sql}`, args);
271
280
  return Number(result.changes ?? 0);
@@ -303,3 +312,7 @@ export function createSyncSqliteCrud(db, ex, options) {
303
312
  };
304
313
  return crud;
305
314
  }
315
+ /** SQLite sync CRUD (compat). */
316
+ export function createSyncSqliteCrud(db, ex, options) {
317
+ return createSyncCrud(db, ex, sqliteDialect, options);
318
+ }
@@ -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
@@ -44,9 +44,16 @@ export type DuplicateQuery = {
44
44
  where: WhereInput;
45
45
  overrides?: Record<string, unknown>;
46
46
  };
47
+ /** SQL dialect for adapters that support more than SQLite (e.g. Prisma). */
48
+ export type CreateCrudDialect = "sqlite" | "postgres" | "mysql";
47
49
  /** Options for `createCrud(db, options?)`. */
48
50
  export type CreateCrudOptions = {
49
51
  /** Primary key column name. Default: `"id"`. */
50
52
  pk?: string;
53
+ /**
54
+ * SQL dialect. Default: `"sqlite"`.
55
+ * Used by Prisma / TypeORM (and other multi-DB adapters). SQLite-only adapters ignore this.
56
+ */
57
+ dialect?: CreateCrudDialect;
51
58
  };
52
59
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEzD,MAAM,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEzC,MAAM,MAAM,kBAAkB,CAAC,CAAC,GAAG,GAAG,IAAI;IACxC,KAAK,EAAE,CAAC,EAAE,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,kBAAkB,CAAC,CAAC,GAAG,GAAG,IAAI;IACxC,KAAK,EAAE,CAAC,EAAE,CAAA;IACV,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,GAAG,IAAI,kBAAkB,CAAC,CAAC,CAAC,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAA;AAEjF,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,SAAS,CAAA;AAEjD,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,KAAK,CAAC,EAAE,UAAU,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,4CAA4C;IAC5C,MAAM,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC5B,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CAChC,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,CAAC,EAAE,UAAU,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,UAAU,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,UAAU,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,UAAU,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC,CAAA;AAED,8CAA8C;AAC9C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,gDAAgD;IAChD,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAEzD,MAAM,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEzC,MAAM,MAAM,kBAAkB,CAAC,CAAC,GAAG,GAAG,IAAI;IACxC,KAAK,EAAE,CAAC,EAAE,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,kBAAkB,CAAC,CAAC,GAAG,GAAG,IAAI;IACxC,KAAK,EAAE,CAAC,EAAE,CAAA;IACV,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,oDAAoD;IACpD,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,GAAG,IAAI,kBAAkB,CAAC,CAAC,CAAC,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAA;AAEjF,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,SAAS,CAAA;AAEjD,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,KAAK,CAAC,EAAE,UAAU,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,4CAA4C;IAC5C,MAAM,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC5B,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CAChC,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,CAAC,EAAE,UAAU,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,UAAU,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,UAAU,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,UAAU,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC,CAAA;AAED,4EAA4E;AAC5E,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAA;AAE/D,8CAA8C;AAC9C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,gDAAgD;IAChD,EAAE,CAAC,EAAE,MAAM,CAAA;IACX;;;OAGG;IACH,OAAO,CAAC,EAAE,iBAAiB,CAAA;CAC5B,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b4moss/crudian",
3
- "version": "0.9.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": [
@@ -41,13 +46,23 @@
41
46
  "build": "tsc -p tsconfig.json",
42
47
  "prepublishOnly": "npm run build",
43
48
  "prisma:generate": "prisma generate --schema prisma/schema.prisma",
44
- "test": "bun run test:bun-sqlite && bun run test:drizzle && bun run test:prisma && bun run test:libsql",
49
+ "prisma:generate:postgres": "prisma generate --schema prisma/schema.postgres.prisma",
50
+ "prisma:generate:mysql": "prisma generate --schema prisma/schema.mysql.prisma",
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",
45
52
  "test:bun-sqlite": "bun test src/bun-sqlite",
46
53
  "test:drizzle": "tsx --test src/drizzle/crud.test.ts src/drizzle/pk.test.ts",
47
54
  "test:prisma": "bun run prisma:generate && tsx --test src/prisma/crud.test.ts src/prisma/pk.test.ts",
48
- "test:libsql": "tsx --test src/libsql/crud.test.ts src/libsql/pk.test.ts"
49
- },
50
- "engines": {
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",
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",
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": {
51
66
  "node": ">=24",
52
67
  "bun": ">=1.0.0"
53
68
  },
@@ -55,7 +70,8 @@
55
70
  "@libsql/client": "^0.14.0 || ^0.15.0 || ^0.17.0",
56
71
  "@prisma/client": "^6.0.0",
57
72
  "better-sqlite3": "^11.0.0",
58
- "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"
59
75
  },
60
76
  "peerDependenciesMeta": {
61
77
  "@libsql/client": {
@@ -69,6 +85,9 @@
69
85
  },
70
86
  "drizzle-orm": {
71
87
  "optional": true
88
+ },
89
+ "typeorm": {
90
+ "optional": true
72
91
  }
73
92
  },
74
93
  "devDependencies": {
@@ -77,9 +96,14 @@
77
96
  "@types/better-sqlite3": "^7.6.13",
78
97
  "better-sqlite3": "^13",
79
98
  "bun-types": "^1.2.0",
99
+ "c8": "10",
80
100
  "drizzle-orm": "^0.45.2",
101
+ "mysql2": "^3.14.0",
102
+ "pg": "^8.14.0",
81
103
  "prisma": "^6.14.0",
104
+ "reflect-metadata": "^0.2.2",
82
105
  "tsx": "^4.23.12",
106
+ "typeorm": "^0.3.22",
83
107
  "typescript": "^5.8.0"
84
108
  },
85
109
  "keywords": [
@@ -90,6 +114,7 @@
90
114
  "drizzle",
91
115
  "prisma",
92
116
  "libsql",
117
+ "typeorm",
93
118
  "ddd"
94
119
  ],
95
120
  "repository": {