oakbun 0.2.2 → 0.4.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/dist/index.js CHANGED
@@ -20,6 +20,188 @@ import {
20
20
  __toCommonJS
21
21
  } from "./chunk-Z6ZWNWWR.js";
22
22
 
23
+ // src/schema/table.ts
24
+ function defineTable(name, schema) {
25
+ return new TableBuilder(name, schema);
26
+ }
27
+ function sqlColName(jsKey, col) {
28
+ return col.def.columnName ?? jsKey;
29
+ }
30
+ function toCreateTableSql(table) {
31
+ const cols = Object.entries(table.schema).map(([jsKey, col]) => {
32
+ const c = col;
33
+ const sqlName = c.def.columnName ?? jsKey;
34
+ let def = `"${sqlName}" `;
35
+ switch (c.def.type) {
36
+ case "INTEGER":
37
+ def += "INTEGER";
38
+ break;
39
+ case "TEXT":
40
+ case "UUID":
41
+ def += "TEXT";
42
+ break;
43
+ case "REAL":
44
+ def += "REAL";
45
+ break;
46
+ case "BOOLEAN":
47
+ def += "INTEGER";
48
+ break;
49
+ // SQLite has no BOOLEAN
50
+ case "TIMESTAMP":
51
+ def += "TEXT";
52
+ break;
53
+ // ISO string in SQLite
54
+ case "JSON":
55
+ def += "TEXT";
56
+ break;
57
+ case "BLOB":
58
+ def += "BLOB";
59
+ break;
60
+ }
61
+ if (c.def.primaryKey) def += " PRIMARY KEY";
62
+ if (c.def.autoIncrement && c.def.type === "INTEGER") def += " AUTOINCREMENT";
63
+ if (!c.def.nullable && !c.def.primaryKey) def += " NOT NULL";
64
+ if (c.def.unique) def += " UNIQUE";
65
+ return def;
66
+ });
67
+ return `CREATE TABLE IF NOT EXISTS "${table.name}" (${cols.join(", ")})`;
68
+ }
69
+ var TableBuilder;
70
+ var init_table = __esm({
71
+ "src/schema/table.ts"() {
72
+ "use strict";
73
+ TableBuilder = class _TableBuilder {
74
+ constructor(_name, _schema) {
75
+ this._name = _name;
76
+ this._schema = _schema;
77
+ }
78
+ _name;
79
+ _schema;
80
+ _hooks = [];
81
+ _events = {};
82
+ _relations = {};
83
+ _softDeleteColumn = null;
84
+ // Register table-level hook (no ctx)
85
+ hook(handlers) {
86
+ this._hooks.push(handlers);
87
+ return this;
88
+ }
89
+ /**
90
+ * Designate a column as the soft-delete timestamp.
91
+ * Once set, all SELECTs automatically add `WHERE "col" IS NULL`.
92
+ * Use `.withDeleted()` on the query to opt out.
93
+ *
94
+ * The column must exist in the schema (validated in `build()`).
95
+ *
96
+ * @example
97
+ * const usersTable = defineTable('users', {
98
+ * id: column.integer().primaryKey(),
99
+ * deletedAt: column.timestamp().nullable(),
100
+ * }).withSoftDelete('deletedAt').build()
101
+ */
102
+ withSoftDelete(col) {
103
+ this._softDeleteColumn = col;
104
+ return this;
105
+ }
106
+ emits(map) {
107
+ const next = new _TableBuilder(this._name, this._schema);
108
+ for (const h of this._hooks) next._hooks.push(h);
109
+ next._events = map;
110
+ for (const [k, v] of Object.entries(this._relations)) {
111
+ ;
112
+ next._relations[k] = v;
113
+ }
114
+ ;
115
+ next._softDeleteColumn = this._softDeleteColumn;
116
+ return next;
117
+ }
118
+ /**
119
+ * Declare a belongs-to relation — FK lives on this table.
120
+ * Returns a new builder with the relation type added to TRelations.
121
+ *
122
+ * @example
123
+ * const postsTable = defineTable('posts', { authorId: column.integer() })
124
+ * .belongsTo('author', () => usersTable, 'authorId')
125
+ * .build()
126
+ */
127
+ belongsTo(name, getTable, foreignKey) {
128
+ if (name in this._relations) {
129
+ throw new Error(`Relation '${name}' is already defined on table '${this._name}'`);
130
+ }
131
+ const rel = { kind: "belongsTo", name, getTable, foreignKey };
132
+ this._relations[name] = rel;
133
+ return this;
134
+ }
135
+ /**
136
+ * Declare a has-many relation — FK lives on the foreign table.
137
+ * Returns a new builder with the relation type added to TRelations.
138
+ *
139
+ * @example
140
+ * const usersTable = defineTable('users', { id: column.integer().primaryKey() })
141
+ * .hasMany('posts', () => postsTable, 'authorId')
142
+ * .build()
143
+ */
144
+ hasMany(name, getTable, foreignKey) {
145
+ if (name in this._relations) {
146
+ throw new Error(`Relation '${name}' is already defined on table '${this._name}'`);
147
+ }
148
+ const rel = { kind: "hasMany", name, getTable, foreignKey };
149
+ this._relations[name] = rel;
150
+ return this;
151
+ }
152
+ /**
153
+ * Declare a many-to-many relation via a pivot table.
154
+ *
155
+ * @example
156
+ * const postsTable = defineTable('posts', { ... })
157
+ * .manyToMany('tags', () => tagsTable, postTagsTable, 'postId', 'tagId')
158
+ * .build()
159
+ */
160
+ manyToMany(name, getTable, pivotTable, localKey, foreignKey) {
161
+ if (name in this._relations) {
162
+ throw new Error(`Relation '${name}' is already defined on table '${this._name}'`);
163
+ }
164
+ this._relations[name] = {
165
+ kind: "manyToMany",
166
+ name,
167
+ getTable,
168
+ foreignKey,
169
+ pivot: { table: pivotTable, localKey, foreignKey }
170
+ };
171
+ return this;
172
+ }
173
+ build() {
174
+ if (this._softDeleteColumn !== null && !(this._softDeleteColumn in this._schema)) {
175
+ throw new Error(
176
+ `withSoftDelete: column '${this._softDeleteColumn}' is not defined in table '${this._name}'. Add it to the schema: column.timestamp().nullable()`
177
+ );
178
+ }
179
+ return {
180
+ name: this._name,
181
+ schema: this._schema,
182
+ primaryKey: this._findPrimaryKey(),
183
+ hooks: [...this._hooks],
184
+ // copy — immutable after build
185
+ events: { ...this._events },
186
+ // _eventMap is typed as InferTableEvents<T, TEvents> — the concrete shape.
187
+ // At runtime it's an empty object (events hold only the string names, not payloads).
188
+ // The field exists solely so TypeScript can infer TMap in onEvent() without
189
+ // recomputing the conditional InferTableEvents each time.
190
+ _eventMap: {},
191
+ relations: { ...this._relations },
192
+ softDeleteColumn: this._softDeleteColumn
193
+ };
194
+ }
195
+ _findPrimaryKey() {
196
+ for (const [key, col] of Object.entries(this._schema)) {
197
+ if (col.def.primaryKey) return key;
198
+ }
199
+ return "id";
200
+ }
201
+ };
202
+ }
203
+ });
204
+
23
205
  // src/events/index.ts
24
206
  var events_exports = {};
25
207
  __export(events_exports, {
@@ -517,13 +699,14 @@ function buildDelete(tableName, pk, pkValue) {
517
699
  }
518
700
  function deserializeRow(table, row) {
519
701
  const result = {};
520
- for (const [key, col] of Object.entries(table.schema)) {
702
+ for (const [jsKey, col] of Object.entries(table.schema)) {
521
703
  const c = col;
522
- const raw = row[key];
704
+ const sqlName = sqlColName(jsKey, c);
705
+ const raw = row[sqlName] !== void 0 ? row[sqlName] : row[jsKey];
523
706
  if (c.def.type === "TIMESTAMP" && raw !== null && raw !== void 0) {
524
- result[key] = new Date(raw);
707
+ result[jsKey] = new Date(raw);
525
708
  } else {
526
- result[key] = raw;
709
+ result[jsKey] = raw;
527
710
  }
528
711
  }
529
712
  return result;
@@ -621,6 +804,7 @@ var ON_CLAUSE_PATTERN;
621
804
  var init_sql = __esm({
622
805
  "src/db/sql.ts"() {
623
806
  "use strict";
807
+ init_table();
624
808
  init_errors();
625
809
  ON_CLAUSE_PATTERN = /^([\w]+)\.([\w]+)\s*=\s*([\w]+)\.([\w]+)$/;
626
810
  }
@@ -638,6 +822,27 @@ __export(db_exports, {
638
822
  UnionBuilder: () => UnionBuilder,
639
823
  VelnDB: () => VelnDB
640
824
  });
825
+ function mapWhere(conditions, schema) {
826
+ if (typeof conditions !== "object" || conditions === null || Array.isArray(conditions)) {
827
+ return conditions;
828
+ }
829
+ const result = {};
830
+ for (const [jsKey, val] of Object.entries(conditions)) {
831
+ const col = schema[jsKey];
832
+ const sqlName = col?.def.columnName ?? jsKey;
833
+ result[sqlName] = val;
834
+ }
835
+ return result;
836
+ }
837
+ function mapDataToSql(data, schema) {
838
+ const result = {};
839
+ for (const [jsKey, val] of Object.entries(data)) {
840
+ const col = schema[jsKey];
841
+ const sqlName = col?.def.columnName ?? jsKey;
842
+ result[sqlName] = val;
843
+ }
844
+ return result;
845
+ }
641
846
  function mergeWhereAnd(a, b) {
642
847
  const aIsPlain = !("OR" in a) && !("AND" in a);
643
848
  const bIsPlain = !("OR" in b) && !("AND" in b);
@@ -1055,7 +1260,7 @@ var init_db = __esm({
1055
1260
  return mergeWhereAnd(this.conditions, softFilter);
1056
1261
  }
1057
1262
  _buildSelectSQL() {
1058
- const conditions = this._effectiveConditions();
1263
+ const conditions = mapWhere(this._effectiveConditions(), this.table.schema);
1059
1264
  if (this._rawWhere.length === 0) {
1060
1265
  return buildSelect(
1061
1266
  this.table.name,
@@ -1167,7 +1372,7 @@ var init_db = __esm({
1167
1372
  const alias = "_agg";
1168
1373
  const colExpr = col ? `"${col}"` : "*";
1169
1374
  const { sql: whereSql, params } = buildWhere(
1170
- this._effectiveConditions(),
1375
+ mapWhere(this._effectiveConditions(), this.table.schema),
1171
1376
  this._dialect
1172
1377
  );
1173
1378
  let sqlStr;
@@ -1193,7 +1398,7 @@ var init_db = __esm({
1193
1398
  async select() {
1194
1399
  let finalSql;
1195
1400
  let finalParams;
1196
- const effectiveConditions = this._effectiveConditions();
1401
+ const effectiveConditions = mapWhere(this._effectiveConditions(), this.table.schema);
1197
1402
  if (this._rawWhere.length === 0) {
1198
1403
  const { sql, params } = buildSelect(
1199
1404
  this.table.name,
@@ -1353,10 +1558,11 @@ var init_db = __esm({
1353
1558
  const finalPatch = await this.hooks.runBeforeUpdate(this.table, this.ctx, current, patch);
1354
1559
  const pk = this.table.primaryKey;
1355
1560
  const pkValue = current[pk];
1561
+ const pkSqlName = this.table.schema[pk]?.def.columnName ?? pk;
1356
1562
  const { sql, params } = buildUpdate(
1357
1563
  this.table.name,
1358
- finalPatch,
1359
- pk,
1564
+ mapDataToSql(finalPatch, this.table.schema),
1565
+ pkSqlName,
1360
1566
  pkValue
1361
1567
  );
1362
1568
  await this.adapter.execute(sql, params);
@@ -1392,7 +1598,8 @@ var init_db = __esm({
1392
1598
  `updateMany: row is missing primary key "${pk}" \u2014 every row must include the PK`
1393
1599
  );
1394
1600
  }
1395
- const selectSql = `SELECT * FROM "${this.table.name}" WHERE "${pk}" = ?`;
1601
+ const pkSqlName = this.table.schema[pk]?.def.columnName ?? pk;
1602
+ const selectSql = `SELECT * FROM "${this.table.name}" WHERE "${pkSqlName}" = ?`;
1396
1603
  const currentRows = await txAdapter.query(selectSql, [pkValue]);
1397
1604
  if (currentRows.length === 0) {
1398
1605
  throw new Error(`updateMany: record with ${pk}=${String(pkValue)} not found`);
@@ -1403,7 +1610,7 @@ var init_db = __esm({
1403
1610
  const finalPatch = await this.hooks.runBeforeUpdate(this.table, this.ctx, current, patch);
1404
1611
  const { sql, params } = buildUpdate(
1405
1612
  this.table.name,
1406
- finalPatch,
1613
+ mapDataToSql(finalPatch, this.table.schema),
1407
1614
  pk,
1408
1615
  pkValue
1409
1616
  );
@@ -1432,7 +1639,8 @@ var init_db = __esm({
1432
1639
  await this.hooks.runBeforeDelete(this.table, this.ctx, current);
1433
1640
  const pk = this.table.primaryKey;
1434
1641
  const pkValue = current[pk];
1435
- const { sql, params } = buildDelete(this.table.name, pk, pkValue);
1642
+ const pkSqlName = this.table.schema[pk]?.def.columnName ?? pk;
1643
+ const { sql, params } = buildDelete(this.table.name, pkSqlName, pkValue);
1436
1644
  await this.adapter.execute(sql, params);
1437
1645
  await this.hooks.runAfterDelete(this.table, this.ctx, current, this.queue);
1438
1646
  return current;
@@ -1571,11 +1779,12 @@ var init_db = __esm({
1571
1779
  `softDelete() called on table '${this.table.name}' which has no soft delete column. Add .withSoftDelete('deletedAt') to the table definition.`
1572
1780
  );
1573
1781
  }
1782
+ const colSqlName = this.table.schema[col]?.def.columnName ?? col;
1574
1783
  const { sql, params } = buildSoftDeleteUpdate(
1575
1784
  this.table.name,
1576
- col,
1785
+ colSqlName,
1577
1786
  this._value,
1578
- this._conditions,
1787
+ mapWhere(this._conditions, this.table.schema),
1579
1788
  this._dialect
1580
1789
  );
1581
1790
  await this.adapter.execute(sql, params);
@@ -1741,12 +1950,14 @@ var init_db = __esm({
1741
1950
  }
1742
1951
  return results;
1743
1952
  }
1744
- /** Serialize values for storage. Date → ISO string. Drops undefined values. */
1953
+ /** Serialize values for storage. Maps JS keys → SQL column names. Date → ISO string. Drops undefined. */
1745
1954
  _serializeForInsert(data) {
1746
1955
  const result = {};
1747
- for (const [key, val] of Object.entries(data)) {
1956
+ for (const [jsKey, val] of Object.entries(data)) {
1748
1957
  if (val === void 0) continue;
1749
- result[key] = val instanceof Date ? val.toISOString() : val;
1958
+ const col = this.table.schema[jsKey];
1959
+ const sqlName = col?.def.columnName ?? jsKey;
1960
+ result[sqlName] = val instanceof Date ? val.toISOString() : val;
1750
1961
  }
1751
1962
  return result;
1752
1963
  }
@@ -1921,6 +2132,17 @@ var Column = class _Column {
1921
2132
  defaultFn(fn) {
1922
2133
  return new _Column({ ...this.def, defaultFn: fn });
1923
2134
  }
2135
+ /**
2136
+ * Set an explicit SQL column name, independent of the JS property key.
2137
+ * Use this to map camelCase TypeScript keys to snake_case SQL columns.
2138
+ *
2139
+ * @example
2140
+ * passwordHash: column.text().name('password_hash')
2141
+ * // INSERT uses "password_hash", SELECT returns { passwordHash: ... }
2142
+ */
2143
+ name(columnName) {
2144
+ return new _Column({ ...this.def, columnName });
2145
+ }
1924
2146
  };
1925
2147
  var base = (type) => ({
1926
2148
  type,
@@ -1940,179 +2162,11 @@ var column = {
1940
2162
  json: () => new Column({ ...base("JSON") })
1941
2163
  };
1942
2164
 
1943
- // src/schema/table.ts
1944
- var TableBuilder = class _TableBuilder {
1945
- constructor(_name, _schema) {
1946
- this._name = _name;
1947
- this._schema = _schema;
1948
- }
1949
- _name;
1950
- _schema;
1951
- _hooks = [];
1952
- _events = {};
1953
- _relations = {};
1954
- _softDeleteColumn = null;
1955
- // Register table-level hook (no ctx)
1956
- hook(handlers) {
1957
- this._hooks.push(handlers);
1958
- return this;
1959
- }
1960
- /**
1961
- * Designate a column as the soft-delete timestamp.
1962
- * Once set, all SELECTs automatically add `WHERE "col" IS NULL`.
1963
- * Use `.withDeleted()` on the query to opt out.
1964
- *
1965
- * The column must exist in the schema (validated in `build()`).
1966
- *
1967
- * @example
1968
- * const usersTable = defineTable('users', {
1969
- * id: column.integer().primaryKey(),
1970
- * deletedAt: column.timestamp().nullable(),
1971
- * }).withSoftDelete('deletedAt').build()
1972
- */
1973
- withSoftDelete(col) {
1974
- this._softDeleteColumn = col;
1975
- return this;
1976
- }
1977
- emits(map) {
1978
- const next = new _TableBuilder(this._name, this._schema);
1979
- for (const h of this._hooks) next._hooks.push(h);
1980
- next._events = map;
1981
- for (const [k, v] of Object.entries(this._relations)) {
1982
- ;
1983
- next._relations[k] = v;
1984
- }
1985
- ;
1986
- next._softDeleteColumn = this._softDeleteColumn;
1987
- return next;
1988
- }
1989
- /**
1990
- * Declare a belongs-to relation — FK lives on this table.
1991
- * Returns a new builder with the relation type added to TRelations.
1992
- *
1993
- * @example
1994
- * const postsTable = defineTable('posts', { authorId: column.integer() })
1995
- * .belongsTo('author', () => usersTable, 'authorId')
1996
- * .build()
1997
- */
1998
- belongsTo(name, getTable, foreignKey) {
1999
- if (name in this._relations) {
2000
- throw new Error(`Relation '${name}' is already defined on table '${this._name}'`);
2001
- }
2002
- const rel = { kind: "belongsTo", name, getTable, foreignKey };
2003
- this._relations[name] = rel;
2004
- return this;
2005
- }
2006
- /**
2007
- * Declare a has-many relation — FK lives on the foreign table.
2008
- * Returns a new builder with the relation type added to TRelations.
2009
- *
2010
- * @example
2011
- * const usersTable = defineTable('users', { id: column.integer().primaryKey() })
2012
- * .hasMany('posts', () => postsTable, 'authorId')
2013
- * .build()
2014
- */
2015
- hasMany(name, getTable, foreignKey) {
2016
- if (name in this._relations) {
2017
- throw new Error(`Relation '${name}' is already defined on table '${this._name}'`);
2018
- }
2019
- const rel = { kind: "hasMany", name, getTable, foreignKey };
2020
- this._relations[name] = rel;
2021
- return this;
2022
- }
2023
- /**
2024
- * Declare a many-to-many relation via a pivot table.
2025
- *
2026
- * @example
2027
- * const postsTable = defineTable('posts', { ... })
2028
- * .manyToMany('tags', () => tagsTable, postTagsTable, 'postId', 'tagId')
2029
- * .build()
2030
- */
2031
- manyToMany(name, getTable, pivotTable, localKey, foreignKey) {
2032
- if (name in this._relations) {
2033
- throw new Error(`Relation '${name}' is already defined on table '${this._name}'`);
2034
- }
2035
- this._relations[name] = {
2036
- kind: "manyToMany",
2037
- name,
2038
- getTable,
2039
- foreignKey,
2040
- pivot: { table: pivotTable, localKey, foreignKey }
2041
- };
2042
- return this;
2043
- }
2044
- build() {
2045
- if (this._softDeleteColumn !== null && !(this._softDeleteColumn in this._schema)) {
2046
- throw new Error(
2047
- `withSoftDelete: column '${this._softDeleteColumn}' is not defined in table '${this._name}'. Add it to the schema: column.timestamp().nullable()`
2048
- );
2049
- }
2050
- return {
2051
- name: this._name,
2052
- schema: this._schema,
2053
- primaryKey: this._findPrimaryKey(),
2054
- hooks: [...this._hooks],
2055
- // copy — immutable after build
2056
- events: { ...this._events },
2057
- // _eventMap is typed as InferTableEvents<T, TEvents> — the concrete shape.
2058
- // At runtime it's an empty object (events hold only the string names, not payloads).
2059
- // The field exists solely so TypeScript can infer TMap in onEvent() without
2060
- // recomputing the conditional InferTableEvents each time.
2061
- _eventMap: {},
2062
- relations: { ...this._relations },
2063
- softDeleteColumn: this._softDeleteColumn
2064
- };
2065
- }
2066
- _findPrimaryKey() {
2067
- for (const [key, col] of Object.entries(this._schema)) {
2068
- if (col.def.primaryKey) return key;
2069
- }
2070
- return "id";
2071
- }
2072
- };
2073
- function defineTable(name, schema) {
2074
- return new TableBuilder(name, schema);
2075
- }
2076
- function toCreateTableSql(table) {
2077
- const cols = Object.entries(table.schema).map(([name, col]) => {
2078
- const c = col;
2079
- let def = `"${name}" `;
2080
- switch (c.def.type) {
2081
- case "INTEGER":
2082
- def += "INTEGER";
2083
- break;
2084
- case "TEXT":
2085
- case "UUID":
2086
- def += "TEXT";
2087
- break;
2088
- case "REAL":
2089
- def += "REAL";
2090
- break;
2091
- case "BOOLEAN":
2092
- def += "INTEGER";
2093
- break;
2094
- // SQLite has no BOOLEAN
2095
- case "TIMESTAMP":
2096
- def += "TEXT";
2097
- break;
2098
- // ISO string in SQLite
2099
- case "JSON":
2100
- def += "TEXT";
2101
- break;
2102
- case "BLOB":
2103
- def += "BLOB";
2104
- break;
2105
- }
2106
- if (c.def.primaryKey) def += " PRIMARY KEY";
2107
- if (c.def.autoIncrement && c.def.type === "INTEGER") def += " AUTOINCREMENT";
2108
- if (!c.def.nullable && !c.def.primaryKey) def += " NOT NULL";
2109
- if (c.def.unique) def += " UNIQUE";
2110
- return def;
2111
- });
2112
- return `CREATE TABLE IF NOT EXISTS "${table.name}" (${cols.join(", ")})`;
2113
- }
2165
+ // src/index.ts
2166
+ init_table();
2114
2167
 
2115
2168
  // src/schema/audit.ts
2169
+ init_table();
2116
2170
  var _baseAuditFields = {
2117
2171
  id: column.integer().primaryKey(),
2118
2172
  tableName: column.text(),