@bunnykit/orm 0.1.21 → 0.1.22

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.
@@ -111,6 +111,9 @@ export class Connection {
111
111
  if (this.driverName !== "postgres") {
112
112
  return await this.transaction(callback);
113
113
  }
114
+ if (!/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(setting)) {
115
+ throw new Error(`Invalid PostgreSQL setting name: ${setting}`);
116
+ }
114
117
  return await this.transaction(async (connection) => {
115
118
  await connection.run(`SET LOCAL ${setting} = ${connection.getGrammar().placeholder(1)}`, [tenantId]);
116
119
  return await callback(connection);
@@ -134,7 +134,7 @@ export declare class Builder<T = Record<string, any>> {
134
134
  private addBinding;
135
135
  private compileWhereClause;
136
136
  private compileWheres;
137
- private compileNestedWheres;
137
+ private compileWhereClauses;
138
138
  private compileOrders;
139
139
  private compileGroups;
140
140
  private compileHavings;
@@ -161,6 +161,8 @@ export declare class Builder<T = Record<string, any>> {
161
161
  chunk(count: number, callback: (items: T[]) => void | Promise<void>): Promise<void>;
162
162
  each(count: number, callback: (item: T) => void | Promise<void>): Promise<void>;
163
163
  cursor(chunkSize?: number): AsyncGenerator<T>;
164
+ private getResultAccessColumn;
165
+ private compileCursorWheres;
164
166
  lazy(count?: number): AsyncGenerator<T>;
165
167
  insert(data: ModelAttributeInput<T> | ModelAttributeInput<T>[]): Promise<any>;
166
168
  insertGetId(data: ModelAttributeInput<T>, idColumn?: ModelColumn<T>): Promise<any>;
@@ -65,8 +65,7 @@ export class Builder {
65
65
  const nested = new Builder(this.connection, this.tableName);
66
66
  callback(nested);
67
67
  if (nested.wheres.length > 0) {
68
- const sql = this.compileNestedWheres(nested);
69
- this.wheres.push({ type: "raw", column: `(${sql})`, boolean, scope: undefined });
68
+ this.wheres.push({ type: "nested", column: "", query: nested.wheres, boolean, scope: undefined });
70
69
  }
71
70
  return this;
72
71
  }
@@ -484,6 +483,10 @@ export class Builder {
484
483
  else if (where.type === "raw") {
485
484
  return `${prefix} ${where.column}`;
486
485
  }
486
+ else if (where.type === "nested") {
487
+ const sql = this.compileWhereClauses(where.query || [], "");
488
+ return `${prefix} (${sql})`;
489
+ }
487
490
  else if (where.type === "like") {
488
491
  const sql = this.grammar.compileLike(this.grammar.wrap(where.column), where.value, !!where.not, this.parameterize ? (v) => this.addBinding(v) : undefined);
489
492
  return `${prefix} ${sql}`;
@@ -540,20 +543,15 @@ export class Builder {
540
543
  return "";
541
544
  }
542
545
  compileWheres() {
543
- if (this.wheres.length === 0)
544
- return "";
545
- const clauses = this.wheres.map((where, index) => {
546
- const prefix = index === 0 ? "WHERE" : where.boolean.toUpperCase();
547
- return this.compileWhereClause(where, prefix);
548
- });
549
- return clauses.join(" ");
546
+ return this.compileWhereClauses(this.wheres, "WHERE");
550
547
  }
551
- compileNestedWheres(builder) {
552
- if (builder.wheres.length === 0)
548
+ compileWhereClauses(wheres, firstPrefix) {
549
+ if (wheres.length === 0)
553
550
  return "";
554
- const clauses = builder.wheres.map((where, index) => {
555
- const prefix = index === 0 ? "" : where.boolean.toUpperCase();
556
- return this.compileWhereClause(where, prefix);
551
+ const clauses = wheres.map((where, index) => {
552
+ const prefix = index === 0 ? "WHERE" : where.boolean.toUpperCase();
553
+ const adjustedPrefix = index === 0 ? firstPrefix : prefix;
554
+ return this.compileWhereClause(where, adjustedPrefix);
557
555
  });
558
556
  return clauses.join(" ").trim();
559
557
  }
@@ -781,9 +779,7 @@ export class Builder {
781
779
  }
782
780
  const orderColumn = this.orders[0]?.column || primaryKey;
783
781
  const orderDirection = this.orders[0]?.direction || "asc";
784
- // Use unqualified column name for property access on model instances
785
- const accessColumn = orderColumn.includes(".") ? orderColumn.split(".")[1] : orderColumn;
786
- let lastValue = undefined;
782
+ let lastValues = undefined;
787
783
  while (true) {
788
784
  const builder = this.clone();
789
785
  // Preserve multi-column ORDER BY, appending PK tie-breaker if not present
@@ -794,23 +790,15 @@ export class Builder {
794
790
  }
795
791
  builder.offsetValue = undefined;
796
792
  builder.limitValue = chunkSize;
797
- if (lastValue !== undefined) {
798
- const op = orderDirection === "asc" ? ">" : "<";
793
+ if (lastValues !== undefined) {
799
794
  // Parenthesize existing wheres when appending cursor condition to preserve OR precedence
800
795
  if (builder.wheres.length > 0) {
801
796
  const hasOr = builder.wheres.some((w) => w.boolean === "or");
802
797
  if (hasOr) {
803
- builder.wheres = [{ type: "raw", column: `(${builder.compileWheres().replace(/^WHERE /, "")})`, boolean: "and", scope: undefined }];
798
+ builder.wheres = [{ type: "nested", column: "", query: builder.wheres, boolean: "and", scope: undefined }];
804
799
  }
805
800
  }
806
- builder.wheres.push({
807
- type: "basic",
808
- column: orderColumn,
809
- operator: op,
810
- value: lastValue,
811
- boolean: "and",
812
- scope: undefined,
813
- });
801
+ builder.wheres.push({ type: "nested", column: "", query: this.compileCursorWheres(builder.orders, lastValues), boolean: "and", scope: undefined });
814
802
  }
815
803
  const items = await builder.get();
816
804
  if (items.length === 0)
@@ -821,11 +809,46 @@ export class Builder {
821
809
  if (items.length < chunkSize)
822
810
  break;
823
811
  const lastItem = items[items.length - 1];
824
- lastValue = lastItem && typeof lastItem === "object"
825
- ? lastItem[accessColumn]
812
+ lastValues = lastItem && typeof lastItem === "object"
813
+ ? builder.orders.map((order) => lastItem[this.getResultAccessColumn(order.column)])
826
814
  : undefined;
827
815
  }
828
816
  }
817
+ getResultAccessColumn(column) {
818
+ return column.includes(".") ? column.split(".").at(-1) : column;
819
+ }
820
+ compileCursorWheres(orders, values, index = 0) {
821
+ const order = orders[index];
822
+ const op = order.direction === "asc" ? ">" : "<";
823
+ const clauses = [{
824
+ type: "basic",
825
+ column: order.column,
826
+ operator: op,
827
+ value: values[index],
828
+ boolean: "and",
829
+ scope: undefined,
830
+ }];
831
+ if (index < orders.length - 1) {
832
+ clauses.push({
833
+ type: "nested",
834
+ column: "",
835
+ query: [
836
+ {
837
+ type: "basic",
838
+ column: order.column,
839
+ operator: "=",
840
+ value: values[index],
841
+ boolean: "and",
842
+ scope: undefined,
843
+ },
844
+ ...this.compileCursorWheres(orders, values, index + 1),
845
+ ],
846
+ boolean: "or",
847
+ scope: undefined,
848
+ });
849
+ }
850
+ return clauses;
851
+ }
829
852
  async *lazy(count = 1000) {
830
853
  let page = 1;
831
854
  while (true) {
@@ -135,48 +135,57 @@ export class Schema {
135
135
  const driver = connection.getDriverName();
136
136
  const schema = connection.getSchema() || "public";
137
137
  let sql;
138
+ let bindings = [];
138
139
  if (driver === "sqlite") {
139
- sql = `SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`;
140
+ sql = "SELECT name FROM sqlite_master WHERE type='table' AND name = ?";
141
+ bindings = [table];
140
142
  }
141
143
  else if (driver === "mysql") {
142
- sql = `SHOW TABLES LIKE '${table}'`;
144
+ sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?";
145
+ bindings = [table];
143
146
  }
144
147
  else {
145
- sql = `SELECT * FROM information_schema.tables WHERE table_schema = '${schema}' AND table_name = '${table}'`;
148
+ sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2";
149
+ bindings = [schema, table];
146
150
  }
147
- const result = await connection.query(sql);
151
+ const result = await connection.query(sql, bindings);
148
152
  return result.length > 0;
149
153
  }
150
154
  static async hasColumn(table, column) {
151
155
  const connection = this.getConnection();
152
156
  const driver = connection.getDriverName();
153
157
  const schema = connection.getSchema() || "public";
158
+ const grammar = this.getGrammar();
154
159
  let sql;
160
+ let bindings = [];
155
161
  if (driver === "sqlite") {
156
- sql = `PRAGMA table_info(${table})`;
162
+ sql = `PRAGMA table_info(${grammar.wrap(table)})`;
157
163
  const result = await connection.query(sql);
158
164
  return result.some((row) => row.name === column);
159
165
  }
160
166
  else if (driver === "mysql") {
161
- sql = `SHOW COLUMNS FROM ${table} LIKE '${column}'`;
167
+ sql = "SELECT column_name FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?";
168
+ bindings = [table, column];
162
169
  }
163
170
  else {
164
- sql = `SELECT column_name FROM information_schema.columns WHERE table_schema = '${schema}' AND table_name = '${table}' AND column_name = '${column}'`;
171
+ sql = "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 AND column_name = $3";
172
+ bindings = [schema, table, column];
165
173
  }
166
- const result = await connection.query(sql);
174
+ const result = await connection.query(sql, bindings);
167
175
  return result.length > 0;
168
176
  }
169
177
  static async getColumn(table, column) {
170
178
  const connection = this.getConnection();
171
179
  const driver = connection.getDriverName();
172
180
  const schema = connection.getSchema() || "public";
181
+ const grammar = this.getGrammar();
173
182
  if (driver === "sqlite") {
174
- const rows = await connection.query(`PRAGMA table_info(${table})`);
183
+ const rows = await connection.query(`PRAGMA table_info(${grammar.wrap(table)})`);
175
184
  const row = rows.find((item) => item.name === column);
176
185
  return row ? { name: row.name, type: row.type, primary: row.pk > 0, autoIncrement: false } : null;
177
186
  }
178
187
  if (driver === "mysql") {
179
- const rows = await connection.query(`SHOW COLUMNS FROM ${table} LIKE '${column}'`);
188
+ const rows = await connection.query("SELECT column_name AS Field, column_type AS Type, column_key AS `Key`, extra AS Extra FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?", [table, column]);
180
189
  const row = rows[0];
181
190
  return row ? { name: row.Field, type: row.Type, primary: row.Key === "PRI", autoIncrement: String(row.Extra || "").toLowerCase().includes("auto_increment") } : null;
182
191
  }
@@ -189,9 +198,9 @@ export class Schema {
189
198
  LEFT JOIN information_schema.table_constraints tc
190
199
  ON kcu.table_schema = tc.table_schema
191
200
  AND kcu.constraint_name = tc.constraint_name
192
- WHERE c.table_schema = '${schema}'
193
- AND c.table_name = '${table}'
194
- AND c.column_name = '${column}'`);
201
+ WHERE c.table_schema = $1
202
+ AND c.table_name = $2
203
+ AND c.column_name = $3`, [schema, table, column]);
195
204
  const row = rows[0];
196
205
  return row ? { name: row.column_name, type: row.data_type, primary: !!row.primary_key, autoIncrement: false } : null;
197
206
  }
@@ -5,7 +5,7 @@ export class Grammar {
5
5
  return value.split(".").map((v) => this.wrap(v)).join(".");
6
6
  }
7
7
  const { prefix, suffix } = this.wrappers;
8
- return `${prefix}${value}${suffix}`;
8
+ return `${prefix}${value.replaceAll(suffix, `${suffix}${suffix}`)}${suffix}`;
9
9
  }
10
10
  wrapArray(values) {
11
11
  return values.map((v) => this.wrap(v));
@@ -29,7 +29,7 @@ export interface ForeignKeyDefinition {
29
29
  onUpdate?: string;
30
30
  }
31
31
  export interface WhereClause {
32
- type: "basic" | "in" | "null" | "raw" | "between" | "column" | "exists" | "like" | "regexp" | "fulltext" | "json_contains" | "json_length" | "date" | "all" | "any";
32
+ type: "basic" | "in" | "null" | "raw" | "nested" | "between" | "column" | "exists" | "like" | "regexp" | "fulltext" | "json_contains" | "json_length" | "date" | "all" | "any";
33
33
  column: string;
34
34
  columns?: string[];
35
35
  operator?: string;
@@ -38,6 +38,7 @@ export interface WhereClause {
38
38
  scope?: string;
39
39
  not?: boolean;
40
40
  dateType?: string;
41
+ query?: WhereClause[];
41
42
  }
42
43
  export interface OrderClause {
43
44
  column: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunnykit/orm",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "An Eloquent-inspired ORM for Bun's native SQL client supporting SQLite, MySQL, and PostgreSQL.",
5
5
  "license": "MIT",
6
6
  "packageManager": "bun@1.3.12",