@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 +13 -0
- package/dist/database/index.d.ts +2 -2
- package/dist/database/index.d.ts.map +1 -1
- package/dist/database/index.js +2 -2
- package/dist/database/index.js.map +1 -1
- package/dist/database/query-builder.d.ts +30 -6
- package/dist/database/query-builder.d.ts.map +1 -1
- package/dist/database/query-builder.js +193 -34
- package/dist/database/query-builder.js.map +1 -1
- package/dist/database/sqlite.d.ts +5 -0
- package/dist/database/sqlite.d.ts.map +1 -1
- package/dist/database/sqlite.js +21 -0
- package/dist/database/sqlite.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/security/hash.d.ts +4 -0
- package/dist/security/hash.d.ts.map +1 -1
- package/dist/security/hash.js +9 -0
- package/dist/security/hash.js.map +1 -1
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js.map +1 -1
- package/dist/security/jwt.d.ts +6 -4
- package/dist/security/jwt.d.ts.map +1 -1
- package/dist/security/jwt.js +68 -11
- package/dist/security/jwt.js.map +1 -1
- package/dist/ws/websocket.d.ts +18 -0
- package/dist/ws/websocket.d.ts.map +1 -1
- package/dist/ws/websocket.js +125 -42
- package/dist/ws/websocket.js.map +1 -1
- package/package.json +1 -1
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)
|
package/dist/database/index.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/database/index.js
CHANGED
|
@@ -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,
|
|
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,
|
|
87
|
-
leftJoin(table: string, first: string,
|
|
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(
|
|
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":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGpE,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,KAAK,GAAG,IAAI,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,YAAY,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW;
|
|
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,
|
|
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: "${
|
|
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,
|
|
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: "${
|
|
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
|
-
|
|
193
|
-
|
|
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:
|
|
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}${
|
|
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}${
|
|
371
|
+
return `${prefix}${col} ${clause.operator} ? AND ?`;
|
|
240
372
|
}
|
|
241
373
|
if (clause.value === null) {
|
|
242
|
-
return `${prefix}${
|
|
374
|
+
return `${prefix}${col} ${clause.operator} NULL`;
|
|
243
375
|
}
|
|
244
376
|
bindings.push(clause.value);
|
|
245
|
-
return `${prefix}${
|
|
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
|
-
|
|
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
|
|
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 +=
|
|
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
|
-
|
|
309
|
-
if (
|
|
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(
|
|
334
|
-
|
|
335
|
-
|
|
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();
|