@shohaghinfo/aerojs 0.4.2 → 0.4.3

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,19 @@
20
20
 
21
21
  ---
22
22
 
23
+ ## 🚀 Official Starter Kits & Real-World Showcases
24
+
25
+ Jumpstart your next enterprise application with production-ready starter templates built on **AeroJS**:
26
+
27
+ | Starter Application | Stack | Architecture | Key Features |
28
+ | :--- | :--- | :--- | :--- |
29
+ | **[AeroSaaS-React](https://github.com/atifsoftware/AeroSaaS-React)** | React 18 + Inertia.js + Tailwind CSS | Single Page App (SPA) | Kanban board, Executive Dashboard, Scrypt Auth, Signed Invites |
30
+ | **[AeroSaaS-Vue](https://github.com/atifsoftware/AeroSaaS-Vue)** | Vue 3 (Composition API) + Inertia.js + Tailwind | Single Page App (SPA) | Optimistic UI updates, Kanban board, Swagger docs, Native SQLite |
31
+ | **[AeroSaaS-Edge](https://github.com/atifsoftware/AeroSaaS-Edge)** | AdonisJS Edge.js 6 + Tailwind CSS | Server-Side Rendered (MPA) | Instant FCP, 100% SEO score, In-memory AST caching, Zero client JS |
32
+ | **[AeroSaaS-EJS](https://github.com/atifsoftware/AeroSaaS-EJS)** | EJS Templates + Tailwind CSS | Server-Side Rendered (MPA) | Classic async node streaming, Full dark theme, OpenAPI Swagger |
33
+
34
+ ---
35
+
23
36
  ## 📑 Table of Contents
24
37
 
25
38
  - [Overview & Philosophy](#-overview--philosophy)
@@ -3,7 +3,7 @@
3
3
  * @description AeroJS Database & Active Record ORM Layer.
4
4
  */
5
5
  export { Database, DB, MemoryDatabaseAdapter, type DatabaseAdapter, type DatabaseRow, } from './connection.js';
6
- export { QueryBuilder, type WhereClause, type JoinClause, type PaginationResult, } from './query-builder.js';
6
+ export { QueryBuilder, validateIdentifier, quoteIdentifier, type WhereClause, type JoinClause, type PaginationResult, } from './query-builder.js';
7
7
  export { Model, Relation, computed, encrypted, type RelationDefinition, type ManyToManyOptions, type HasManyThroughOptions, } from './model.js';
8
8
  export { getHookRegistry, registerHook, ModelHookRegistry, beforeCreate, afterCreate, beforeSave, afterSave, beforeUpdate, afterUpdate, beforeDelete, afterDelete, beforeFind, afterFind, beforeFetch, afterFetch, type ModelHookEvent, type ModelHookHandler, } from './model-hooks.js';
9
9
  export { Schema, TableBlueprint, type ColumnDefinition, } from './schema.js';
@@ -13,5 +13,5 @@ export { ModelFactory, type FactoryDefinition, } from './factory.js';
13
13
  export { KnexDatabaseAdapter, useKnex, } from './knex.js';
14
14
  export { usePrisma, type PrismaIntegrationOptions, } from './prisma.js';
15
15
  export { useDrizzle, } from './drizzle.js';
16
- export { SqliteDatabaseAdapter, useSqlite, } from './sqlite.js';
16
+ export { SqliteDatabaseAdapter, useSqlite, isSqliteSupported, } from './sqlite.js';
17
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/database/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,QAAQ,EACR,EAAE,EACF,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,WAAW,GACjB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,GAC3B,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,UAAU,EACV,SAAS,EACT,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,WAAW,EACX,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,EACV,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,MAAM,EACN,cAAc,EACd,KAAK,gBAAgB,GACtB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,QAAQ,EACR,KAAK,SAAS,GACf,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,MAAM,EACN,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,YAAY,EACZ,KAAK,iBAAiB,GACvB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,mBAAmB,EACnB,OAAO,GACR,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,SAAS,EACT,KAAK,wBAAwB,GAC9B,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,UAAU,GACX,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,qBAAqB,EACrB,SAAS,GACV,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/database/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,QAAQ,EACR,EAAE,EACF,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,WAAW,GACjB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,GAC3B,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,UAAU,EACV,SAAS,EACT,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,WAAW,EACX,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,EACV,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,MAAM,EACN,cAAc,EACd,KAAK,gBAAgB,GACtB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,QAAQ,EACR,KAAK,SAAS,GACf,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,MAAM,EACN,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,YAAY,EACZ,KAAK,iBAAiB,GACvB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,mBAAmB,EACnB,OAAO,GACR,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,SAAS,EACT,KAAK,wBAAwB,GAC9B,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,UAAU,GACX,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,qBAAqB,EACrB,SAAS,EACT,iBAAiB,GAClB,MAAM,aAAa,CAAC"}
@@ -3,7 +3,7 @@
3
3
  * @description AeroJS Database & Active Record ORM Layer.
4
4
  */
5
5
  export { Database, DB, MemoryDatabaseAdapter, } from './connection.js';
6
- export { QueryBuilder, } from './query-builder.js';
6
+ export { QueryBuilder, validateIdentifier, quoteIdentifier, } from './query-builder.js';
7
7
  export { Model, Relation, computed, encrypted, } from './model.js';
8
8
  export { getHookRegistry, registerHook, ModelHookRegistry, beforeCreate, afterCreate, beforeSave, afterSave, beforeUpdate, afterUpdate, beforeDelete, afterDelete, beforeFind, afterFind, beforeFetch, afterFetch, } from './model-hooks.js';
9
9
  export { Schema, TableBlueprint, } from './schema.js';
@@ -13,5 +13,5 @@ export { ModelFactory, } from './factory.js';
13
13
  export { KnexDatabaseAdapter, useKnex, } from './knex.js';
14
14
  export { usePrisma, } from './prisma.js';
15
15
  export { useDrizzle, } from './drizzle.js';
16
- export { SqliteDatabaseAdapter, useSqlite, } from './sqlite.js';
16
+ export { SqliteDatabaseAdapter, useSqlite, isSqliteSupported, } from './sqlite.js';
17
17
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/database/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,QAAQ,EACR,EAAE,EACF,qBAAqB,GAGtB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,YAAY,GAIb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,SAAS,GAIV,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,UAAU,EACV,SAAS,EACT,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,WAAW,EACX,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,GAGX,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,MAAM,EACN,cAAc,GAEf,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,QAAQ,GAET,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,MAAM,EACN,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,YAAY,GAEb,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,mBAAmB,EACnB,OAAO,GACR,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,SAAS,GAEV,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,UAAU,GACX,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,qBAAqB,EACrB,SAAS,GACV,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/database/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,QAAQ,EACR,EAAE,EACF,qBAAqB,GAGtB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,eAAe,GAIhB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,SAAS,GAIV,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,UAAU,EACV,SAAS,EACT,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,WAAW,EACX,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,GAGX,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,MAAM,EACN,cAAc,GAEf,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,QAAQ,GAET,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,MAAM,EACN,cAAc,GACf,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,YAAY,GAEb,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,mBAAmB,EACnB,OAAO,GACR,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,SAAS,GAEV,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,UAAU,GACX,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,qBAAqB,EACrB,SAAS,EACT,iBAAiB,GAClB,MAAM,aAAa,CAAC"}
@@ -4,6 +4,17 @@
4
4
  * Supports chainable filtering, joins, eager loading, pagination, and SQL compilation.
5
5
  */
6
6
  import type { DatabaseAdapter, DatabaseRow } from './connection.js';
7
+ /**
8
+ * Validates a SQL identifier (table name, column name, or alias) to prevent SQL injection.
9
+ * Supports /^[a-zA-Z0-9_.*]+$/ and alias 'as' patterns.
10
+ */
11
+ export declare function validateIdentifier(identifier: string): string;
12
+ /**
13
+ * Dialect-aware column and table identifier quoting to prevent injection and keyword collisions.
14
+ * MySQL: `backtick`
15
+ * PostgreSQL / SQLite: "double quote"
16
+ */
17
+ export declare function quoteIdentifier(identifier: string, dialect?: string): string;
7
18
  export interface WhereClause {
8
19
  type: 'and' | 'or';
9
20
  column: string;
@@ -26,6 +37,8 @@ export interface PaginationResult<T> {
26
37
  hasMore: boolean;
27
38
  }
28
39
  export declare class QueryBuilder<T extends DatabaseRow = DatabaseRow> {
40
+ static validateIdentifier: typeof validateIdentifier;
41
+ static quoteIdentifier: typeof quoteIdentifier;
29
42
  tableName: string;
30
43
  protected adapter: DatabaseAdapter;
31
44
  protected columns: string[];
@@ -47,7 +60,14 @@ export declare class QueryBuilder<T extends DatabaseRow = DatabaseRow> {
47
60
  _eagerLoads: string[];
48
61
  protected preferWriteConnection: boolean;
49
62
  protected customReadAdapter?: DatabaseAdapter;
63
+ protected shouldQuote: boolean;
50
64
  constructor(tableName: string, adapter: DatabaseAdapter);
65
+ /**
66
+ * Enables or disables automatic dialect-aware identifier quoting for this query.
67
+ */
68
+ quoteIdentifiers(enable?: boolean): this;
69
+ validateIdentifier(identifier: string): string;
70
+ quoteIdentifier(identifier: string, dialect?: string): string;
51
71
  /**
52
72
  * Forces this query to execute against the primary write connection (for read-your-own-writes consistency).
53
73
  */
@@ -83,8 +103,8 @@ export declare class QueryBuilder<T extends DatabaseRow = DatabaseRow> {
83
103
  distinct(): this;
84
104
  groupBy(...columns: string[]): this;
85
105
  having(column: string, operator: string, value: unknown): this;
86
- join(table: string, first: string, operator: string, second: string): this;
87
- leftJoin(table: string, first: string, operator: string, second: string): this;
106
+ join(table: string, first: string, operatorOrSecond: string, maybeSecond?: string): this;
107
+ leftJoin(table: string, first: string, operatorOrSecond: string, maybeSecond?: string): this;
88
108
  orderBy(column: string, direction?: 'asc' | 'desc' | 'ASC' | 'DESC'): this;
89
109
  limit(count: number): this;
90
110
  offset(count: number): this;
@@ -93,21 +113,25 @@ export declare class QueryBuilder<T extends DatabaseRow = DatabaseRow> {
93
113
  /**
94
114
  * Compiles where clauses into a standalone SQL WHERE fragment and parameter bindings.
95
115
  */
96
- compileWhere(): {
116
+ compileWhere(options?: {
117
+ quote?: boolean;
118
+ }): {
97
119
  sql: string;
98
120
  bindings: unknown[];
99
121
  };
100
122
  /**
101
123
  * Compiles the current builder state to raw SQL and bindings.
102
124
  */
103
- toSQL(): {
125
+ toSQL(options?: {
126
+ quote?: boolean;
127
+ }): {
104
128
  sql: string;
105
129
  bindings: unknown[];
106
130
  };
107
131
  /**
108
132
  * Executes query and returns array of records.
109
133
  */
110
- get(): Promise<T[]>;
134
+ get(adapterOrName?: DatabaseAdapter | string): Promise<T[]>;
111
135
  /**
112
136
  * Executes query and returns first record, or null.
113
137
  */
@@ -119,7 +143,7 @@ export declare class QueryBuilder<T extends DatabaseRow = DatabaseRow> {
119
143
  /**
120
144
  * Counts the matching records.
121
145
  */
122
- count(column?: string): Promise<number>;
146
+ count(columnOrAdapter?: string | unknown): Promise<number>;
123
147
  /**
124
148
  * Sums the given column.
125
149
  */
@@ -1 +1 @@
1
- {"version":3,"file":"query-builder.d.ts","sourceRoot":"","sources":["../../src/database/query-builder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"query-builder.d.ts","sourceRoot":"","sources":["../../src/database/query-builder.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,79 @@
4
4
  * Supports chainable filtering, joins, eager loading, pagination, and SQL compilation.
5
5
  */
6
6
  import { Database, MemoryDatabaseAdapter } from './connection.js';
7
+ /**
8
+ * Validates a SQL identifier (table name, column name, or alias) to prevent SQL injection.
9
+ * Supports /^[a-zA-Z0-9_.*]+$/ and alias 'as' patterns.
10
+ */
11
+ export function validateIdentifier(identifier) {
12
+ const trimmed = String(identifier || '').trim();
13
+ if (!trimmed) {
14
+ throw new Error('Security Violation: SQL identifier cannot be empty');
15
+ }
16
+ // Handle alias pattern e.g., "column as alias" or "table.column AS alias"
17
+ const asMatch = trimmed.match(/^(.+?)\s+as\s+(.+)$/i);
18
+ if (asMatch) {
19
+ const expr = asMatch[1].trim();
20
+ const alias = asMatch[2].trim();
21
+ validateIdentifierPart(expr, true);
22
+ validateIdentifierPart(alias, false);
23
+ return trimmed;
24
+ }
25
+ validateIdentifierPart(trimmed, true);
26
+ return trimmed;
27
+ }
28
+ function validateIdentifierPart(part, allowStar) {
29
+ // Allow segments separated by dots: e.g., table.column or schema.table.column
30
+ const segments = part.split('.');
31
+ for (let i = 0; i < segments.length; i++) {
32
+ let seg = segments[i].trim();
33
+ if ((seg.startsWith('`') && seg.endsWith('`')) || (seg.startsWith('"') && seg.endsWith('"'))) {
34
+ seg = seg.slice(1, -1);
35
+ }
36
+ if (allowStar && seg === '*') {
37
+ continue;
38
+ }
39
+ if (!/^[a-zA-Z0-9_]+$/.test(seg)) {
40
+ throw new Error(`Security Violation: Invalid identifier: "${part}"`);
41
+ }
42
+ }
43
+ }
44
+ /**
45
+ * Dialect-aware column and table identifier quoting to prevent injection and keyword collisions.
46
+ * MySQL: `backtick`
47
+ * PostgreSQL / SQLite: "double quote"
48
+ */
49
+ export function quoteIdentifier(identifier, dialect) {
50
+ validateIdentifier(identifier);
51
+ const d = String(dialect || '').toLowerCase();
52
+ const isMysql = d.includes('mysql');
53
+ const isPgOrSqlite = d.includes('pg') || d.includes('postgres') || d.includes('sqlite');
54
+ if (!isMysql && !isPgOrSqlite) {
55
+ return identifier;
56
+ }
57
+ const quoteChar = isMysql ? '`' : '"';
58
+ // Alias pattern: "col as alias" -> "`col` AS `alias`" or `"col" AS "alias"`
59
+ const asMatch = identifier.trim().match(/^(.+?)\s+as\s+(.+)$/i);
60
+ if (asMatch) {
61
+ const expr = quoteIdentifier(asMatch[1].trim(), dialect);
62
+ const alias = quoteIdentifier(asMatch[2].trim(), dialect);
63
+ return `${expr} AS ${alias}`;
64
+ }
65
+ const segments = identifier.trim().split('.');
66
+ const quoted = segments.map((seg) => {
67
+ let s = seg.trim();
68
+ if ((s.startsWith('`') && s.endsWith('`')) || (s.startsWith('"') && s.endsWith('"'))) {
69
+ s = s.slice(1, -1);
70
+ }
71
+ if (s === '*')
72
+ return '*';
73
+ return `${quoteChar}${s}${quoteChar}`;
74
+ });
75
+ return quoted.join('.');
76
+ }
7
77
  export class QueryBuilder {
78
+ static validateIdentifier = validateIdentifier;
79
+ static quoteIdentifier = quoteIdentifier;
8
80
  tableName;
9
81
  adapter;
10
82
  columns = ['*'];
@@ -19,10 +91,25 @@ export class QueryBuilder {
19
91
  _eagerLoads = [];
20
92
  preferWriteConnection = false;
21
93
  customReadAdapter;
94
+ shouldQuote = false;
22
95
  constructor(tableName, adapter) {
96
+ validateIdentifier(tableName);
23
97
  this.tableName = tableName;
24
98
  this.adapter = adapter;
25
99
  }
100
+ /**
101
+ * Enables or disables automatic dialect-aware identifier quoting for this query.
102
+ */
103
+ quoteIdentifiers(enable = true) {
104
+ this.shouldQuote = enable;
105
+ return this;
106
+ }
107
+ validateIdentifier(identifier) {
108
+ return validateIdentifier(identifier);
109
+ }
110
+ quoteIdentifier(identifier, dialect) {
111
+ return quoteIdentifier(identifier, dialect || this.resolveReadAdapter().dialect);
112
+ }
26
113
  /**
27
114
  * Forces this query to execute against the primary write connection (for read-your-own-writes consistency).
28
115
  */
@@ -61,6 +148,7 @@ export class QueryBuilder {
61
148
  copy.preferWriteConnection = this.preferWriteConnection;
62
149
  copy.customReadAdapter = this.customReadAdapter;
63
150
  copy.lockMode = this.lockMode;
151
+ copy.shouldQuote = this.shouldQuote;
64
152
  return copy;
65
153
  }
66
154
  /**
@@ -87,17 +175,28 @@ export class QueryBuilder {
87
175
  }
88
176
  select(...columns) {
89
177
  if (columns.length > 0) {
178
+ columns.forEach(validateIdentifier);
90
179
  this.columns = columns;
91
180
  }
92
181
  return this;
93
182
  }
94
183
  where(column, operatorOrValue, value) {
184
+ validateIdentifier(column);
95
185
  let operator = '=';
96
186
  let val = operatorOrValue;
97
187
  if (value !== undefined) {
98
- operator = String(operatorOrValue).toUpperCase();
188
+ operator = String(operatorOrValue).toUpperCase().trim();
99
189
  val = value;
100
190
  }
191
+ const validOperators = [
192
+ '=', '!=', '<>', '>', '<', '>=', '<=',
193
+ 'LIKE', 'NOT LIKE', 'ILIKE', 'NOT ILIKE',
194
+ 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN',
195
+ 'IS', 'IS NOT', '=='
196
+ ];
197
+ if (!validOperators.includes(operator)) {
198
+ throw new Error(`Security Violation: Unsupported operator in where: "${operator}"`);
199
+ }
101
200
  this.whereClauses.push({
102
201
  type: 'and',
103
202
  column,
@@ -107,12 +206,22 @@ export class QueryBuilder {
107
206
  return this;
108
207
  }
109
208
  orWhere(column, operatorOrValue, value) {
209
+ validateIdentifier(column);
110
210
  let operator = '=';
111
211
  let val = operatorOrValue;
112
212
  if (value !== undefined) {
113
- operator = String(operatorOrValue).toUpperCase();
213
+ operator = String(operatorOrValue).toUpperCase().trim();
114
214
  val = value;
115
215
  }
216
+ const validOperators = [
217
+ '=', '!=', '<>', '>', '<', '>=', '<=',
218
+ 'LIKE', 'NOT LIKE', 'ILIKE', 'NOT ILIKE',
219
+ 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN',
220
+ 'IS', 'IS NOT', '=='
221
+ ];
222
+ if (!validOperators.includes(operator)) {
223
+ throw new Error(`Security Violation: Unsupported operator in orWhere: "${operator}"`);
224
+ }
116
225
  this.whereClauses.push({
117
226
  type: 'or',
118
227
  column,
@@ -156,10 +265,12 @@ export class QueryBuilder {
156
265
  return this;
157
266
  }
158
267
  groupBy(...columns) {
268
+ columns.forEach(validateIdentifier);
159
269
  this.groupClauses.push(...columns);
160
270
  return this;
161
271
  }
162
272
  having(column, operator, value) {
273
+ validateIdentifier(column);
163
274
  const validOperators = ['=', '!=', '<>', '>', '<', '>=', '<=', 'LIKE', 'NOT LIKE'];
164
275
  const op = operator.toUpperCase().trim();
165
276
  if (!validOperators.includes(op)) {
@@ -168,33 +279,50 @@ export class QueryBuilder {
168
279
  this.havingClauses.push({ column, operator: op, value });
169
280
  return this;
170
281
  }
171
- join(table, first, operator, second) {
282
+ join(table, first, operatorOrSecond, maybeSecond) {
283
+ validateIdentifier(table);
284
+ validateIdentifier(first);
285
+ let op = '=';
286
+ let second = operatorOrSecond;
287
+ if (maybeSecond !== undefined) {
288
+ op = operatorOrSecond.trim();
289
+ second = maybeSecond;
290
+ }
291
+ validateIdentifier(second);
172
292
  const validOperators = ['=', '!=', '<>', '>', '<', '>=', '<='];
173
- const op = operator.trim();
174
293
  if (!validOperators.includes(op)) {
175
- throw new Error(`Security Violation: Unsupported operator in join: "${operator}"`);
294
+ throw new Error(`Security Violation: Unsupported operator in join: "${op}"`);
176
295
  }
177
296
  this.joinClauses.push({ type: 'inner', table, first, operator: op, second });
178
297
  return this;
179
298
  }
180
- leftJoin(table, first, operator, second) {
299
+ leftJoin(table, first, operatorOrSecond, maybeSecond) {
300
+ validateIdentifier(table);
301
+ validateIdentifier(first);
302
+ let op = '=';
303
+ let second = operatorOrSecond;
304
+ if (maybeSecond !== undefined) {
305
+ op = operatorOrSecond.trim();
306
+ second = maybeSecond;
307
+ }
308
+ validateIdentifier(second);
181
309
  const validOperators = ['=', '!=', '<>', '>', '<', '>=', '<='];
182
- const op = operator.trim();
183
310
  if (!validOperators.includes(op)) {
184
- throw new Error(`Security Violation: Unsupported operator in leftJoin: "${operator}"`);
311
+ throw new Error(`Security Violation: Unsupported operator in leftJoin: "${op}"`);
185
312
  }
186
313
  this.joinClauses.push({ type: 'left', table, first, operator: op, second });
187
314
  return this;
188
315
  }
189
316
  orderBy(column, direction = 'ASC') {
190
- const cleanDir = String(direction).toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
191
317
  const trimmedCol = column.trim();
192
- if (!/^[a-zA-Z0-9_.]+$/.test(trimmedCol) && !/^`[a-zA-Z0-9_.]+`$/.test(trimmedCol) && !/^"[a-zA-Z0-9_.]+"$/.test(trimmedCol)) {
193
- throw new Error(`Security Violation: Invalid column identifier in orderBy: "${column}"`);
318
+ validateIdentifier(trimmedCol);
319
+ const dirUpper = String(direction || 'ASC').trim().toUpperCase();
320
+ if (dirUpper !== 'ASC' && dirUpper !== 'DESC') {
321
+ throw new Error(`Security Violation: Invalid order direction in orderBy: "${direction}". Allowed directions are ASC or DESC.`);
194
322
  }
195
323
  this.orderClauses.push({
196
324
  column: trimmedCol,
197
- direction: cleanDir,
325
+ direction: dirUpper,
198
326
  });
199
327
  return this;
200
328
  }
@@ -216,11 +344,14 @@ export class QueryBuilder {
216
344
  /**
217
345
  * Compiles where clauses into a standalone SQL WHERE fragment and parameter bindings.
218
346
  */
219
- compileWhere() {
347
+ compileWhere(options) {
220
348
  const bindings = [];
221
349
  if (this.whereClauses.length === 0) {
222
350
  return { sql: '', bindings };
223
351
  }
352
+ const quote = options?.quote ?? this.shouldQuote;
353
+ const dialect = this.resolveReadAdapter().dialect;
354
+ const q = (id) => (quote ? this.quoteIdentifier(id, dialect) : id);
224
355
  const parts = this.whereClauses.map((clause, idx) => {
225
356
  const prefix = idx > 0 ? `${clause.type.toUpperCase()} ` : '';
226
357
  if (clause.operator === 'RAW') {
@@ -229,56 +360,60 @@ export class QueryBuilder {
229
360
  }
230
361
  return `${prefix}${clause.column}`;
231
362
  }
363
+ const col = q(clause.column);
232
364
  if ((clause.operator === 'IN' || clause.operator === 'NOT IN') && Array.isArray(clause.value)) {
233
365
  const placeholders = clause.value.map(() => '?').join(', ');
234
366
  bindings.push(...clause.value);
235
- return `${prefix}${clause.column} ${clause.operator} (${placeholders})`;
367
+ return `${prefix}${col} ${clause.operator} (${placeholders})`;
236
368
  }
237
369
  if ((clause.operator === 'BETWEEN' || clause.operator === 'NOT BETWEEN') && Array.isArray(clause.value)) {
238
370
  bindings.push(clause.value[0], clause.value[1]);
239
- return `${prefix}${clause.column} ${clause.operator} ? AND ?`;
371
+ return `${prefix}${col} ${clause.operator} ? AND ?`;
240
372
  }
241
373
  if (clause.value === null) {
242
- return `${prefix}${clause.column} ${clause.operator} NULL`;
374
+ return `${prefix}${col} ${clause.operator} NULL`;
243
375
  }
244
376
  bindings.push(clause.value);
245
- return `${prefix}${clause.column} ${clause.operator} ?`;
377
+ return `${prefix}${col} ${clause.operator} ?`;
246
378
  });
247
379
  return { sql: ` WHERE ${parts.join(' ')}`, bindings };
248
380
  }
249
381
  /**
250
382
  * Compiles the current builder state to raw SQL and bindings.
251
383
  */
252
- toSQL() {
384
+ toSQL(options) {
253
385
  const bindings = [];
254
- let sql = `SELECT ${this.isDistinct ? 'DISTINCT ' : ''}${this.columns.join(', ')} FROM ${this.tableName}`;
386
+ const quote = options?.quote ?? this.shouldQuote;
387
+ const dialect = this.resolveReadAdapter().dialect;
388
+ const q = (id) => (quote ? this.quoteIdentifier(id, dialect) : id);
389
+ let sql = `SELECT ${this.isDistinct ? 'DISTINCT ' : ''}${this.columns.map(q).join(', ')} FROM ${q(this.tableName)}`;
255
390
  // Joins
256
391
  for (const join of this.joinClauses) {
257
392
  const joinType = join.type === 'left' ? 'LEFT JOIN' : 'INNER JOIN';
258
- sql += ` ${joinType} ${join.table} ON ${join.first} ${join.operator} ${join.second}`;
393
+ sql += ` ${joinType} ${q(join.table)} ON ${q(join.first)} ${join.operator} ${q(join.second)}`;
259
394
  }
260
395
  // Wheres
261
- const where = this.compileWhere();
396
+ const where = this.compileWhere({ quote });
262
397
  if (where.sql) {
263
398
  sql += where.sql;
264
399
  bindings.push(...where.bindings);
265
400
  }
266
401
  // Group By
267
402
  if (this.groupClauses.length > 0) {
268
- sql += ` GROUP BY ${this.groupClauses.join(', ')}`;
403
+ sql += ` GROUP BY ${this.groupClauses.map(q).join(', ')}`;
269
404
  }
270
405
  // Having
271
406
  if (this.havingClauses.length > 0) {
272
407
  const havingParts = this.havingClauses.map((h, i) => {
273
408
  const pfx = i > 0 ? 'AND ' : '';
274
409
  bindings.push(h.value);
275
- return `${pfx}${h.column} ${h.operator} ?`;
410
+ return `${pfx}${q(h.column)} ${h.operator} ?`;
276
411
  });
277
412
  sql += ` HAVING ${havingParts.join(' ')}`;
278
413
  }
279
414
  // Order By
280
415
  if (this.orderClauses.length > 0) {
281
- const orders = this.orderClauses.map((o) => `${o.column} ${o.direction}`).join(', ');
416
+ const orders = this.orderClauses.map((o) => `${q(o.column)} ${o.direction}`).join(', ');
282
417
  sql += ` ORDER BY ${orders}`;
283
418
  }
284
419
  // Limit and Offset
@@ -291,12 +426,12 @@ export class QueryBuilder {
291
426
  // Pessimistic Locking
292
427
  if (this.lockMode) {
293
428
  const adapter = this.resolveReadAdapter();
294
- const dialect = String(adapter.dialect || '').toLowerCase();
429
+ const d = String(adapter.dialect || '').toLowerCase();
295
430
  if (this.lockMode === 'FOR UPDATE') {
296
431
  sql += ' FOR UPDATE';
297
432
  }
298
433
  else if (this.lockMode === 'SHARE') {
299
- sql += dialect.includes('mysql') ? ' LOCK IN SHARE MODE' : ' FOR SHARE';
434
+ sql += d.includes('mysql') ? ' LOCK IN SHARE MODE' : ' FOR SHARE';
300
435
  }
301
436
  }
302
437
  return { sql, bindings };
@@ -304,9 +439,12 @@ export class QueryBuilder {
304
439
  /**
305
440
  * Executes query and returns array of records.
306
441
  */
307
- async get() {
308
- const adapter = this.resolveReadAdapter();
309
- if (adapter instanceof MemoryDatabaseAdapter) {
442
+ async get(adapterOrName) {
443
+ let adapter = this.resolveReadAdapter();
444
+ if (adapterOrName) {
445
+ adapter = typeof adapterOrName === 'string' ? Database.getAdapter(adapterOrName) : adapterOrName;
446
+ }
447
+ if (adapter instanceof MemoryDatabaseAdapter || adapter.getTableData) {
310
448
  return this.executeInMemory(adapter);
311
449
  }
312
450
  const { sql, bindings } = this.toSQL();
@@ -330,9 +468,23 @@ export class QueryBuilder {
330
468
  /**
331
469
  * Counts the matching records.
332
470
  */
333
- async count(column = '*') {
334
- const adapter = this.resolveReadAdapter();
335
- if (adapter instanceof MemoryDatabaseAdapter) {
471
+ async count(columnOrAdapter = '*') {
472
+ let column = '*';
473
+ let targetAdapter;
474
+ if (typeof columnOrAdapter === 'object' && columnOrAdapter !== null) {
475
+ targetAdapter = columnOrAdapter;
476
+ }
477
+ else if (typeof columnOrAdapter === 'string') {
478
+ if (columnOrAdapter !== '*' && Database.getAdapter(columnOrAdapter) && Database.getAdapter(columnOrAdapter) !== Database.getAdapter('default')) {
479
+ targetAdapter = Database.getAdapter(columnOrAdapter);
480
+ }
481
+ else {
482
+ column = columnOrAdapter;
483
+ validateIdentifier(column);
484
+ }
485
+ }
486
+ const adapter = targetAdapter || this.resolveReadAdapter();
487
+ if (adapter instanceof MemoryDatabaseAdapter || adapter.getTableData) {
336
488
  const rows = this.filterMemoryRows(adapter.getTableData(this.tableName));
337
489
  return rows.length;
338
490
  }
@@ -350,6 +502,7 @@ export class QueryBuilder {
350
502
  * Sums the given column.
351
503
  */
352
504
  async sum(column) {
505
+ validateIdentifier(column);
353
506
  const adapter = this.resolveReadAdapter();
354
507
  if (adapter instanceof MemoryDatabaseAdapter) {
355
508
  const rows = this.filterMemoryRows(adapter.getTableData(this.tableName));
@@ -369,6 +522,7 @@ export class QueryBuilder {
369
522
  * Calculates the average of the given column.
370
523
  */
371
524
  async avg(column) {
525
+ validateIdentifier(column);
372
526
  const adapter = this.resolveReadAdapter();
373
527
  if (adapter instanceof MemoryDatabaseAdapter) {
374
528
  const rows = this.filterMemoryRows(adapter.getTableData(this.tableName));
@@ -391,6 +545,7 @@ export class QueryBuilder {
391
545
  * Finds the minimum value of the given column.
392
546
  */
393
547
  async min(column) {
548
+ validateIdentifier(column);
394
549
  const adapter = this.resolveReadAdapter();
395
550
  if (adapter instanceof MemoryDatabaseAdapter) {
396
551
  const rows = this.filterMemoryRows(adapter.getTableData(this.tableName));
@@ -412,6 +567,7 @@ export class QueryBuilder {
412
567
  * Finds the maximum value of the given column.
413
568
  */
414
569
  async max(column) {
570
+ validateIdentifier(column);
415
571
  const adapter = this.resolveReadAdapter();
416
572
  if (adapter instanceof MemoryDatabaseAdapter) {
417
573
  const rows = this.filterMemoryRows(adapter.getTableData(this.tableName));
@@ -442,6 +598,7 @@ export class QueryBuilder {
442
598
  * Plucks an array of single column values from matching records.
443
599
  */
444
600
  async pluck(column) {
601
+ validateIdentifier(String(column));
445
602
  const qb = this.clone();
446
603
  qb.columns = [String(column)];
447
604
  const rows = await qb.get();
@@ -455,6 +612,8 @@ export class QueryBuilder {
455
612
  if (records.length === 0)
456
613
  return { affectedRows: 0 };
457
614
  const adapter = this.resolveWriteAdapter();
615
+ const keys = Object.keys(records[0]);
616
+ keys.forEach(validateIdentifier);
458
617
  if (adapter instanceof MemoryDatabaseAdapter) {
459
618
  const tableRows = adapter.getTableData(this.tableName);
460
619
  const defaults = Database.tableDefaults.get(this.tableName) || {};
@@ -467,7 +626,6 @@ export class QueryBuilder {
467
626
  }
468
627
  return { insertId: lastId, affectedRows: records.length };
469
628
  }
470
- const keys = Object.keys(records[0]);
471
629
  const cols = keys.join(', ');
472
630
  const rowPlaceholders = `(${keys.map(() => '?').join(', ')})`;
473
631
  const allPlaceholders = records.map(() => rowPlaceholders).join(', ');
@@ -489,6 +647,8 @@ export class QueryBuilder {
489
647
  */
490
648
  async update(data) {
491
649
  const adapter = this.resolveWriteAdapter();
650
+ const keys = Object.keys(data);
651
+ keys.forEach(validateIdentifier);
492
652
  if (adapter instanceof MemoryDatabaseAdapter) {
493
653
  const tableRows = adapter.getTableData(this.tableName);
494
654
  let updatedCount = 0;
@@ -500,7 +660,6 @@ export class QueryBuilder {
500
660
  }
501
661
  return updatedCount;
502
662
  }
503
- const keys = Object.keys(data);
504
663
  const setClause = keys.map((k) => `${k} = ?`).join(', ');
505
664
  const setBindings = keys.map((k) => data[k]);
506
665
  const { sql: whereSql, bindings: whereBindings } = this.compileWhere();