@carno.js/orm 0.2.3 → 0.2.5
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/SqlBuilder.d.ts +5 -3
- package/dist/SqlBuilder.js +14 -9
- package/dist/cache/cache-key-generator.d.ts +1 -1
- package/dist/cache/cache-key-generator.js +10 -6
- package/dist/cache/query-cache-manager.d.ts +3 -1
- package/dist/cache/query-cache-manager.js +16 -2
- package/dist/domain/base-entity.d.ts +3 -0
- package/dist/domain/base-entity.js +11 -3
- package/dist/driver/bun-driver.base.d.ts +0 -1
- package/dist/driver/bun-driver.base.js +4 -6
- package/dist/driver/driver.interface.d.ts +4 -0
- package/dist/identity-map/entity-registry.d.ts +2 -1
- package/dist/identity-map/entity-registry.js +16 -5
- package/dist/orm.js +7 -4
- package/dist/query/index-condition-builder.d.ts +0 -2
- package/dist/query/index-condition-builder.js +21 -28
- package/dist/query/model-transformer.d.ts +2 -0
- package/dist/query/model-transformer.js +23 -4
- package/dist/query/sql-condition-builder.d.ts +0 -2
- package/dist/query/sql-condition-builder.js +21 -15
- package/dist/utils/sql-escape.d.ts +11 -0
- package/dist/utils/sql-escape.js +25 -0
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +9 -3
- package/package.json +3 -3
package/dist/SqlBuilder.d.ts
CHANGED
|
@@ -10,12 +10,14 @@ export declare class SqlBuilder<T> {
|
|
|
10
10
|
private updatedColumns;
|
|
11
11
|
private originalColumns;
|
|
12
12
|
private conditionBuilder;
|
|
13
|
-
private modelTransformer;
|
|
14
13
|
private columnManager;
|
|
15
|
-
private joinManager;
|
|
16
14
|
private cacheManager?;
|
|
15
|
+
private _modelTransformer?;
|
|
16
|
+
private _joinManager?;
|
|
17
|
+
private readonly boundGetAlias;
|
|
17
18
|
constructor(model: new () => T);
|
|
18
|
-
private
|
|
19
|
+
private get modelTransformer();
|
|
20
|
+
private get joinManager();
|
|
19
21
|
select(columns?: AutoPath<T, never, '*'>[]): SqlBuilder<T>;
|
|
20
22
|
setStrategy(strategy?: 'joined' | 'select'): SqlBuilder<T>;
|
|
21
23
|
setInstance(instance: T): SqlBuilder<T>;
|
package/dist/SqlBuilder.js
CHANGED
|
@@ -19,10 +19,11 @@ class SqlBuilder {
|
|
|
19
19
|
this.driver = orm.driverInstance;
|
|
20
20
|
this.logger = orm.logger;
|
|
21
21
|
this.entityStorage = entities_1.EntityStorage.getInstance();
|
|
22
|
-
this.
|
|
22
|
+
this.cacheManager = orm.queryCacheManager;
|
|
23
23
|
this.getEntity(model);
|
|
24
24
|
this.statements.hooks = this.entity.hooks;
|
|
25
|
-
|
|
25
|
+
// Pre-bind once
|
|
26
|
+
this.boundGetAlias = this.getAlias.bind(this);
|
|
26
27
|
this.columnManager = new sql_column_manager_1.SqlColumnManager(this.entityStorage, this.statements, this.entity);
|
|
27
28
|
const applyJoinWrapper = (relationship, value, alias) => {
|
|
28
29
|
return this.joinManager.applyJoin(relationship, value, alias);
|
|
@@ -30,16 +31,20 @@ class SqlBuilder {
|
|
|
30
31
|
this.conditionBuilder = new sql_condition_builder_1.SqlConditionBuilder(this.entityStorage, applyJoinWrapper, this.statements);
|
|
31
32
|
const subqueryBuilder = new sql_subquery_builder_1.SqlSubqueryBuilder(this.entityStorage, () => this.conditionBuilder);
|
|
32
33
|
this.conditionBuilder.setSubqueryBuilder(subqueryBuilder);
|
|
33
|
-
this.joinManager = new sql_join_manager_1.SqlJoinManager(this.entityStorage, this.statements, this.entity, this.model, this.driver, this.logger, this.conditionBuilder, this.columnManager, this.modelTransformer, () => this.originalColumns, this.getAlias.bind(this));
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.
|
|
35
|
+
// Lazy getter for modelTransformer - only created when transform is needed
|
|
36
|
+
get modelTransformer() {
|
|
37
|
+
if (!this._modelTransformer) {
|
|
38
|
+
this._modelTransformer = new model_transformer_1.ModelTransformer(this.entityStorage);
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
return this._modelTransformer;
|
|
41
|
+
}
|
|
42
|
+
// Lazy getter for joinManager - only created when joins are needed
|
|
43
|
+
get joinManager() {
|
|
44
|
+
if (!this._joinManager) {
|
|
45
|
+
this._joinManager = new sql_join_manager_1.SqlJoinManager(this.entityStorage, this.statements, this.entity, this.model, this.driver, this.logger, this.conditionBuilder, this.columnManager, this.modelTransformer, () => this.originalColumns, this.boundGetAlias);
|
|
42
46
|
}
|
|
47
|
+
return this._joinManager;
|
|
43
48
|
}
|
|
44
49
|
select(columns) {
|
|
45
50
|
const tableName = this.entity.tableName || this.model.name.toLowerCase();
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CacheKeyGenerator = void 0;
|
|
4
|
-
const
|
|
4
|
+
const FNV_OFFSET_BASIS = 2166136261;
|
|
5
|
+
const FNV_PRIME = 16777619;
|
|
5
6
|
class CacheKeyGenerator {
|
|
6
7
|
generate(statement) {
|
|
7
8
|
const parts = this.buildKeyParts(statement);
|
|
8
9
|
const combined = this.combineKeyParts(parts);
|
|
9
|
-
return this.
|
|
10
|
+
return this.hashFNV1a(combined);
|
|
10
11
|
}
|
|
11
12
|
buildKeyParts(statement) {
|
|
12
13
|
const parts = [];
|
|
@@ -57,10 +58,13 @@ class CacheKeyGenerator {
|
|
|
57
58
|
combineKeyParts(parts) {
|
|
58
59
|
return parts.join('::');
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.
|
|
61
|
+
hashFNV1a(str) {
|
|
62
|
+
let hash = FNV_OFFSET_BASIS;
|
|
63
|
+
for (let i = 0; i < str.length; i++) {
|
|
64
|
+
hash ^= str.charCodeAt(i);
|
|
65
|
+
hash = Math.imul(hash, FNV_PRIME);
|
|
66
|
+
}
|
|
67
|
+
return (hash >>> 0).toString(16);
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
exports.CacheKeyGenerator = CacheKeyGenerator;
|
|
@@ -4,11 +4,13 @@ export declare class QueryCacheManager {
|
|
|
4
4
|
private cacheService;
|
|
5
5
|
private keyGenerator;
|
|
6
6
|
private namespaceKeys;
|
|
7
|
-
|
|
7
|
+
private readonly maxKeysPerNamespace;
|
|
8
|
+
constructor(cacheService: CacheService, maxKeysPerNamespace?: number);
|
|
8
9
|
get<T>(statement: Statement<T>): Promise<any>;
|
|
9
10
|
set<T>(statement: Statement<T>, value: any, ttl?: number): Promise<void>;
|
|
10
11
|
invalidate<T>(statement: Statement<T>): Promise<void>;
|
|
11
12
|
private registerKeyInNamespace;
|
|
13
|
+
private evictOldestKey;
|
|
12
14
|
private getNamespace;
|
|
13
15
|
private generateKey;
|
|
14
16
|
}
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.QueryCacheManager = void 0;
|
|
4
4
|
const cache_key_generator_1 = require("./cache-key-generator");
|
|
5
|
+
const DEFAULT_MAX_KEYS_PER_NAMESPACE = 1000;
|
|
5
6
|
class QueryCacheManager {
|
|
6
|
-
constructor(cacheService) {
|
|
7
|
+
constructor(cacheService, maxKeysPerNamespace = DEFAULT_MAX_KEYS_PER_NAMESPACE) {
|
|
7
8
|
this.cacheService = cacheService;
|
|
8
9
|
this.namespaceKeys = new Map();
|
|
9
10
|
this.keyGenerator = new cache_key_generator_1.CacheKeyGenerator();
|
|
11
|
+
this.maxKeysPerNamespace = maxKeysPerNamespace;
|
|
10
12
|
}
|
|
11
13
|
async get(statement) {
|
|
12
14
|
const key = this.generateKey(statement);
|
|
@@ -32,7 +34,19 @@ class QueryCacheManager {
|
|
|
32
34
|
if (!this.namespaceKeys.has(namespace)) {
|
|
33
35
|
this.namespaceKeys.set(namespace, new Set());
|
|
34
36
|
}
|
|
35
|
-
this.namespaceKeys.get(namespace)
|
|
37
|
+
const keys = this.namespaceKeys.get(namespace);
|
|
38
|
+
if (keys.has(key)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (keys.size >= this.maxKeysPerNamespace) {
|
|
42
|
+
this.evictOldestKey(keys);
|
|
43
|
+
}
|
|
44
|
+
keys.add(key);
|
|
45
|
+
}
|
|
46
|
+
evictOldestKey(keys) {
|
|
47
|
+
const oldest = keys.values().next().value;
|
|
48
|
+
keys.delete(oldest);
|
|
49
|
+
this.cacheService.del(oldest);
|
|
36
50
|
}
|
|
37
51
|
getNamespace(statement) {
|
|
38
52
|
return statement.table || 'unknown';
|
|
@@ -4,7 +4,10 @@ export declare abstract class BaseEntity {
|
|
|
4
4
|
private _oldValues;
|
|
5
5
|
private _changedValues;
|
|
6
6
|
private $_isPersisted;
|
|
7
|
+
private $_isHydrating;
|
|
7
8
|
constructor();
|
|
9
|
+
$_startHydration(): void;
|
|
10
|
+
$_endHydration(): void;
|
|
8
11
|
/**
|
|
9
12
|
* Gets current entity's Repository.
|
|
10
13
|
*/
|
|
@@ -10,26 +10,34 @@ class BaseEntity {
|
|
|
10
10
|
this._oldValues = {};
|
|
11
11
|
this._changedValues = {};
|
|
12
12
|
this.$_isPersisted = false;
|
|
13
|
+
this.$_isHydrating = false;
|
|
13
14
|
return new Proxy(this, {
|
|
14
15
|
set(target, p, newValue) {
|
|
15
16
|
if (p.startsWith('$') || p.startsWith('_')) {
|
|
16
17
|
target[p] = newValue;
|
|
17
18
|
return true;
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
+
if (target.$_isHydrating) {
|
|
21
|
+
target[p] = newValue;
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
20
24
|
if (!(p in target._oldValues)) {
|
|
21
25
|
target._oldValues[p] = newValue;
|
|
22
26
|
}
|
|
23
|
-
// se o valor for diferente do valor antigo, é porque o valor foi alterado
|
|
24
27
|
if (target._oldValues[p] !== newValue) {
|
|
25
28
|
target._changedValues[p] = newValue;
|
|
26
|
-
this.$_isPersisted = false;
|
|
27
29
|
}
|
|
28
30
|
target[p] = newValue;
|
|
29
31
|
return true;
|
|
30
32
|
},
|
|
31
33
|
});
|
|
32
34
|
}
|
|
35
|
+
$_startHydration() {
|
|
36
|
+
this.$_isHydrating = true;
|
|
37
|
+
}
|
|
38
|
+
$_endHydration() {
|
|
39
|
+
this.$_isHydrating = false;
|
|
40
|
+
}
|
|
33
41
|
/**
|
|
34
42
|
* Gets current entity's Repository.
|
|
35
43
|
*/
|
|
@@ -28,7 +28,6 @@ export declare abstract class BunDriverBase implements Partial<DriverInterface>
|
|
|
28
28
|
private getExecutionContext;
|
|
29
29
|
transaction<T>(callback: (tx: SQL) => Promise<T>): Promise<T>;
|
|
30
30
|
protected toDatabaseValue(value: unknown): string | number | boolean;
|
|
31
|
-
protected escapeString(value: string): string;
|
|
32
31
|
protected escapeIdentifier(identifier: string): string;
|
|
33
32
|
protected buildWhereClause(where: string | undefined): string;
|
|
34
33
|
protected buildOrderByClause(orderBy: string[] | undefined): string;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.BunDriverBase = void 0;
|
|
4
4
|
const bun_1 = require("bun");
|
|
5
5
|
const transaction_context_1 = require("../transaction/transaction-context");
|
|
6
|
+
const sql_escape_1 = require("../utils/sql-escape");
|
|
6
7
|
class BunDriverBase {
|
|
7
8
|
constructor(options) {
|
|
8
9
|
this.connectionString = this.buildConnectionString(options);
|
|
@@ -79,20 +80,17 @@ class BunDriverBase {
|
|
|
79
80
|
}
|
|
80
81
|
switch (typeof value) {
|
|
81
82
|
case "string":
|
|
82
|
-
return `'${
|
|
83
|
+
return `'${(0, sql_escape_1.escapeString)(value)}'`;
|
|
83
84
|
case "number":
|
|
84
85
|
return value;
|
|
85
86
|
case "boolean":
|
|
86
87
|
return value;
|
|
87
88
|
case "object":
|
|
88
|
-
return `'${
|
|
89
|
+
return `'${(0, sql_escape_1.escapeString)(JSON.stringify(value))}'`;
|
|
89
90
|
default:
|
|
90
|
-
return `'${
|
|
91
|
+
return `'${(0, sql_escape_1.escapeString)(String(value))}'`;
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
|
-
escapeString(value) {
|
|
94
|
-
return value.replace(/'/g, "''");
|
|
95
|
-
}
|
|
96
94
|
escapeIdentifier(identifier) {
|
|
97
95
|
return `"${identifier}"`;
|
|
98
96
|
}
|
|
@@ -44,6 +44,9 @@ export type SnapshotConstraintInfo = {
|
|
|
44
44
|
consDef: string;
|
|
45
45
|
type: string;
|
|
46
46
|
};
|
|
47
|
+
export interface CacheSettings {
|
|
48
|
+
maxKeysPerTable?: number;
|
|
49
|
+
}
|
|
47
50
|
export interface ConnectionSettings<T extends DriverInterface = DriverInterface> {
|
|
48
51
|
host?: string;
|
|
49
52
|
port?: number;
|
|
@@ -59,6 +62,7 @@ export interface ConnectionSettings<T extends DriverInterface = DriverInterface>
|
|
|
59
62
|
driver: new (options: ConnectionSettings<T>) => T;
|
|
60
63
|
entities?: Function[] | string;
|
|
61
64
|
migrationPath?: string;
|
|
65
|
+
cache?: CacheSettings;
|
|
62
66
|
}
|
|
63
67
|
export type ConditionOperators<T, C> = {
|
|
64
68
|
$ne?: T;
|
|
@@ -4,7 +4,11 @@ exports.EntityRegistry = void 0;
|
|
|
4
4
|
class EntityRegistry {
|
|
5
5
|
constructor() {
|
|
6
6
|
this.store = new Map();
|
|
7
|
-
|
|
7
|
+
// Bun can GC WeakRefs early; keep strong refs there for identity stability.
|
|
8
|
+
this.useWeakRefs = typeof Bun === 'undefined';
|
|
9
|
+
if (this.useWeakRefs) {
|
|
10
|
+
this.setupFinalizationRegistry();
|
|
11
|
+
}
|
|
8
12
|
}
|
|
9
13
|
setupFinalizationRegistry() {
|
|
10
14
|
this.finalizationRegistry = new FinalizationRegistry((key) => {
|
|
@@ -12,11 +16,14 @@ class EntityRegistry {
|
|
|
12
16
|
});
|
|
13
17
|
}
|
|
14
18
|
get(key) {
|
|
15
|
-
const
|
|
16
|
-
if (!
|
|
19
|
+
const value = this.store.get(key);
|
|
20
|
+
if (!value) {
|
|
17
21
|
return undefined;
|
|
18
22
|
}
|
|
19
|
-
|
|
23
|
+
if (!this.useWeakRefs) {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
const entity = value.deref();
|
|
20
27
|
if (!entity) {
|
|
21
28
|
this.store.delete(key);
|
|
22
29
|
return undefined;
|
|
@@ -24,9 +31,13 @@ class EntityRegistry {
|
|
|
24
31
|
return entity;
|
|
25
32
|
}
|
|
26
33
|
set(key, entity) {
|
|
34
|
+
if (!this.useWeakRefs) {
|
|
35
|
+
this.store.set(key, entity);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
27
38
|
const weakRef = new WeakRef(entity);
|
|
28
39
|
this.store.set(key, weakRef);
|
|
29
|
-
this.finalizationRegistry
|
|
40
|
+
this.finalizationRegistry?.register(entity, key, entity);
|
|
30
41
|
}
|
|
31
42
|
has(key) {
|
|
32
43
|
return this.get(key) !== undefined;
|
package/dist/orm.js
CHANGED
|
@@ -16,17 +16,19 @@ const SqlBuilder_1 = require("./SqlBuilder");
|
|
|
16
16
|
const query_cache_manager_1 = require("./cache/query-cache-manager");
|
|
17
17
|
const transaction_context_1 = require("./transaction/transaction-context");
|
|
18
18
|
const orm_session_context_1 = require("./orm-session-context");
|
|
19
|
+
const DEFAULT_MAX_KEYS_PER_TABLE = 10000;
|
|
19
20
|
let Orm = Orm_1 = class Orm {
|
|
20
21
|
constructor(logger, cacheService) {
|
|
21
22
|
this.logger = logger;
|
|
22
23
|
this.cacheService = cacheService;
|
|
23
24
|
Orm_1.instance = this;
|
|
24
|
-
this.initializeQueryCacheManager();
|
|
25
25
|
}
|
|
26
|
-
initializeQueryCacheManager() {
|
|
27
|
-
if (this.cacheService) {
|
|
28
|
-
|
|
26
|
+
initializeQueryCacheManager(cacheSettings) {
|
|
27
|
+
if (!this.cacheService) {
|
|
28
|
+
return;
|
|
29
29
|
}
|
|
30
|
+
const maxKeys = cacheSettings?.maxKeysPerTable ?? DEFAULT_MAX_KEYS_PER_TABLE;
|
|
31
|
+
this.queryCacheManager = new query_cache_manager_1.QueryCacheManager(this.cacheService, maxKeys);
|
|
30
32
|
}
|
|
31
33
|
static getInstance() {
|
|
32
34
|
const scoped = orm_session_context_1.ormSessionContext.getOrm();
|
|
@@ -39,6 +41,7 @@ let Orm = Orm_1 = class Orm {
|
|
|
39
41
|
this.connection = connection;
|
|
40
42
|
// @ts-ignore
|
|
41
43
|
this.driverInstance = new this.connection.driver(connection);
|
|
44
|
+
this.initializeQueryCacheManager(connection.cache);
|
|
42
45
|
}
|
|
43
46
|
createQueryBuilder(model) {
|
|
44
47
|
return new SqlBuilder_1.SqlBuilder(model);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { FilterQuery } from '../driver/driver.interface';
|
|
2
2
|
export declare class IndexConditionBuilder<T> {
|
|
3
3
|
private columnMap;
|
|
4
|
-
private readonly OPERATORS;
|
|
5
4
|
private lastKeyNotOperator;
|
|
6
5
|
constructor(columnMap: Record<string, string>);
|
|
7
6
|
build(condition: FilterQuery<T>): string;
|
|
@@ -29,7 +28,6 @@ export declare class IndexConditionBuilder<T> {
|
|
|
29
28
|
private isPrimitive;
|
|
30
29
|
private formatPrimitive;
|
|
31
30
|
private formatJson;
|
|
32
|
-
private escapeString;
|
|
33
31
|
private isScalarValue;
|
|
34
32
|
private isArrayValue;
|
|
35
33
|
private isLogicalOperator;
|
|
@@ -3,23 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.IndexConditionBuilder = void 0;
|
|
4
4
|
const value_object_1 = require("../common/value-object");
|
|
5
5
|
const utils_1 = require("../utils");
|
|
6
|
+
const sql_escape_1 = require("../utils/sql-escape");
|
|
7
|
+
const OPERATORS_SET = new Set([
|
|
8
|
+
'$eq', '$ne', '$in', '$nin', '$like',
|
|
9
|
+
'$gt', '$gte', '$lt', '$lte',
|
|
10
|
+
'$and', '$or', '$nor',
|
|
11
|
+
]);
|
|
12
|
+
const LOGICAL_OPERATORS_SET = new Set(['$or', '$and']);
|
|
13
|
+
const PRIMITIVES_SET = new Set(['string', 'number', 'boolean', 'bigint']);
|
|
6
14
|
class IndexConditionBuilder {
|
|
7
15
|
constructor(columnMap) {
|
|
8
16
|
this.columnMap = columnMap;
|
|
9
|
-
this.OPERATORS = [
|
|
10
|
-
'$eq',
|
|
11
|
-
'$ne',
|
|
12
|
-
'$in',
|
|
13
|
-
'$nin',
|
|
14
|
-
'$like',
|
|
15
|
-
'$gt',
|
|
16
|
-
'$gte',
|
|
17
|
-
'$lt',
|
|
18
|
-
'$lte',
|
|
19
|
-
'$and',
|
|
20
|
-
'$or',
|
|
21
|
-
'$nor',
|
|
22
|
-
];
|
|
23
17
|
this.lastKeyNotOperator = '';
|
|
24
18
|
}
|
|
25
19
|
build(condition) {
|
|
@@ -82,9 +76,9 @@ class IndexConditionBuilder {
|
|
|
82
76
|
}
|
|
83
77
|
buildOperatorConditions(key, value) {
|
|
84
78
|
const parts = [];
|
|
85
|
-
for (const
|
|
86
|
-
if (
|
|
87
|
-
const condition = this.buildOperatorCondition(key,
|
|
79
|
+
for (const opKey in value) {
|
|
80
|
+
if (OPERATORS_SET.has(opKey)) {
|
|
81
|
+
const condition = this.buildOperatorCondition(key, opKey, value[opKey]);
|
|
88
82
|
parts.push(condition);
|
|
89
83
|
}
|
|
90
84
|
}
|
|
@@ -138,7 +132,8 @@ class IndexConditionBuilder {
|
|
|
138
132
|
}
|
|
139
133
|
buildLikeCondition(key, value) {
|
|
140
134
|
const column = this.resolveColumnName(key);
|
|
141
|
-
|
|
135
|
+
const escaped = (0, sql_escape_1.escapeString)(value);
|
|
136
|
+
return `${column} LIKE '${escaped}'`;
|
|
142
137
|
}
|
|
143
138
|
buildComparisonCondition(key, value, operator) {
|
|
144
139
|
const column = this.resolveColumnName(key);
|
|
@@ -182,28 +177,26 @@ class IndexConditionBuilder {
|
|
|
182
177
|
return value === null || value === undefined;
|
|
183
178
|
}
|
|
184
179
|
isPrimitive(value) {
|
|
185
|
-
return
|
|
180
|
+
return PRIMITIVES_SET.has(typeof value);
|
|
186
181
|
}
|
|
187
182
|
formatPrimitive(value) {
|
|
188
|
-
if (typeof value === 'string')
|
|
189
|
-
return `'${
|
|
183
|
+
if (typeof value === 'string') {
|
|
184
|
+
return `'${(0, sql_escape_1.escapeString)(value)}'`;
|
|
185
|
+
}
|
|
190
186
|
return `${value}`;
|
|
191
187
|
}
|
|
192
188
|
formatJson(value) {
|
|
193
|
-
return `'${
|
|
194
|
-
}
|
|
195
|
-
escapeString(value) {
|
|
196
|
-
return value.replace(/'/g, "''");
|
|
189
|
+
return `'${(0, sql_escape_1.escapeString)(JSON.stringify(value))}'`;
|
|
197
190
|
}
|
|
198
191
|
isScalarValue(value) {
|
|
199
192
|
const isDate = value instanceof Date;
|
|
200
193
|
return typeof value !== 'object' || value === null || isDate;
|
|
201
194
|
}
|
|
202
195
|
isArrayValue(key, value) {
|
|
203
|
-
return !
|
|
196
|
+
return !OPERATORS_SET.has(key) && Array.isArray(value);
|
|
204
197
|
}
|
|
205
198
|
isLogicalOperator(key) {
|
|
206
|
-
return
|
|
199
|
+
return LOGICAL_OPERATORS_SET.has(key);
|
|
207
200
|
}
|
|
208
201
|
isNorOperator(key) {
|
|
209
202
|
return key === '$nor';
|
|
@@ -212,7 +205,7 @@ class IndexConditionBuilder {
|
|
|
212
205
|
return key.toUpperCase().replace('$', '');
|
|
213
206
|
}
|
|
214
207
|
trackLastNonOperatorKey(key) {
|
|
215
|
-
if (!
|
|
208
|
+
if (!OPERATORS_SET.has(key)) {
|
|
216
209
|
this.lastKeyNotOperator = key;
|
|
217
210
|
}
|
|
218
211
|
}
|
|
@@ -4,6 +4,8 @@ export declare class ModelTransformer {
|
|
|
4
4
|
private entityStorage;
|
|
5
5
|
constructor(entityStorage: EntityStorage);
|
|
6
6
|
transform<T>(model: any, statement: Statement<any>, data: any): T;
|
|
7
|
+
private startHydration;
|
|
8
|
+
private endHydration;
|
|
7
9
|
private registerInstancesInIdentityMap;
|
|
8
10
|
private createInstances;
|
|
9
11
|
private createInstance;
|
|
@@ -11,12 +11,30 @@ class ModelTransformer {
|
|
|
11
11
|
transform(model, statement, data) {
|
|
12
12
|
const { instanceMap, cachedAliases } = this.createInstances(model, statement, data);
|
|
13
13
|
const optionsMap = this.buildOptionsMap(instanceMap);
|
|
14
|
+
this.startHydration(instanceMap, cachedAliases);
|
|
14
15
|
this.populateProperties(data, instanceMap, optionsMap, cachedAliases);
|
|
15
16
|
this.linkJoinedEntities(statement, instanceMap, optionsMap);
|
|
16
17
|
this.resetChangedValues(instanceMap, cachedAliases);
|
|
18
|
+
this.endHydration(instanceMap, cachedAliases);
|
|
17
19
|
this.registerInstancesInIdentityMap(instanceMap, cachedAliases);
|
|
18
20
|
return instanceMap[statement.alias];
|
|
19
21
|
}
|
|
22
|
+
startHydration(instanceMap, cachedAliases) {
|
|
23
|
+
for (const [alias, instance] of Object.entries(instanceMap)) {
|
|
24
|
+
if (cachedAliases.has(alias)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
instance.$_startHydration?.();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
endHydration(instanceMap, cachedAliases) {
|
|
31
|
+
for (const [alias, instance] of Object.entries(instanceMap)) {
|
|
32
|
+
if (cachedAliases.has(alias)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
instance.$_endHydration?.();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
20
38
|
registerInstancesInIdentityMap(instanceMap, cachedAliases) {
|
|
21
39
|
Object.entries(instanceMap).forEach(([alias, instance]) => {
|
|
22
40
|
// Skip registering entities that were already in cache
|
|
@@ -42,13 +60,14 @@ class ModelTransformer {
|
|
|
42
60
|
return { instanceMap, cachedAliases };
|
|
43
61
|
}
|
|
44
62
|
createInstance(model, primaryKey) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
if (primaryKey !== undefined && primaryKey !== null) {
|
|
64
|
+
const cached = identity_map_1.IdentityMapIntegration.getEntity(model, primaryKey);
|
|
65
|
+
if (cached) {
|
|
66
|
+
return { instance: cached, wasCached: true };
|
|
67
|
+
}
|
|
48
68
|
}
|
|
49
69
|
const instance = new model();
|
|
50
70
|
instance.$_isPersisted = true;
|
|
51
|
-
// Note: Registration happens later in registerInstancesInIdentityMap after properties are populated
|
|
52
71
|
return { instance, wasCached: false };
|
|
53
72
|
}
|
|
54
73
|
addJoinedInstances(statement, instanceMap, data, cachedAliases) {
|
|
@@ -6,7 +6,6 @@ export declare class SqlConditionBuilder<T> {
|
|
|
6
6
|
private entityStorage;
|
|
7
7
|
private applyJoinCallback;
|
|
8
8
|
private statements;
|
|
9
|
-
private readonly OPERATORS;
|
|
10
9
|
private lastKeyNotOperator;
|
|
11
10
|
private subqueryBuilder?;
|
|
12
11
|
constructor(entityStorage: EntityStorage, applyJoinCallback: ApplyJoinCallback, statements: Statement<T>);
|
|
@@ -36,7 +35,6 @@ export declare class SqlConditionBuilder<T> {
|
|
|
36
35
|
private isPrimitive;
|
|
37
36
|
private formatPrimitive;
|
|
38
37
|
private formatJson;
|
|
39
|
-
private escapeString;
|
|
40
38
|
private findRelationship;
|
|
41
39
|
private isScalarValue;
|
|
42
40
|
private isArrayValue;
|
|
@@ -3,12 +3,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SqlConditionBuilder = void 0;
|
|
4
4
|
const value_object_1 = require("../common/value-object");
|
|
5
5
|
const utils_1 = require("../utils");
|
|
6
|
+
const sql_escape_1 = require("../utils/sql-escape");
|
|
7
|
+
const OPERATORS_SET = new Set([
|
|
8
|
+
'$eq', '$ne', '$in', '$nin', '$like',
|
|
9
|
+
'$gt', '$gte', '$lt', '$lte',
|
|
10
|
+
'$and', '$or', '$exists', '$nexists',
|
|
11
|
+
]);
|
|
12
|
+
const LOGICAL_OPERATORS_SET = new Set(['$or', '$and']);
|
|
13
|
+
const PRIMITIVES_SET = new Set(['string', 'number', 'boolean', 'bigint']);
|
|
6
14
|
class SqlConditionBuilder {
|
|
7
15
|
constructor(entityStorage, applyJoinCallback, statements) {
|
|
8
16
|
this.entityStorage = entityStorage;
|
|
9
17
|
this.applyJoinCallback = applyJoinCallback;
|
|
10
18
|
this.statements = statements;
|
|
11
|
-
this.OPERATORS = ['$eq', '$ne', '$in', '$nin', '$like', '$gt', '$gte', '$lt', '$lte', '$and', '$or', '$exists', '$nexists'];
|
|
12
19
|
this.lastKeyNotOperator = '';
|
|
13
20
|
}
|
|
14
21
|
setSubqueryBuilder(subqueryBuilder) {
|
|
@@ -79,9 +86,9 @@ class SqlConditionBuilder {
|
|
|
79
86
|
}
|
|
80
87
|
buildOperatorConditions(key, value, alias, model) {
|
|
81
88
|
const parts = [];
|
|
82
|
-
for (const
|
|
83
|
-
if (
|
|
84
|
-
const condition = this.buildOperatorCondition(key,
|
|
89
|
+
for (const opKey in value) {
|
|
90
|
+
if (OPERATORS_SET.has(opKey)) {
|
|
91
|
+
const condition = this.buildOperatorCondition(key, opKey, value[opKey], alias, model);
|
|
85
92
|
parts.push(condition);
|
|
86
93
|
}
|
|
87
94
|
}
|
|
@@ -137,7 +144,8 @@ class SqlConditionBuilder {
|
|
|
137
144
|
}
|
|
138
145
|
buildLikeCondition(key, value, alias, model) {
|
|
139
146
|
const column = this.resolveColumnName(key, model);
|
|
140
|
-
|
|
147
|
+
const escaped = (0, sql_escape_1.escapeString)(value);
|
|
148
|
+
return `${alias}.${column} LIKE '${escaped}'`;
|
|
141
149
|
}
|
|
142
150
|
buildComparisonCondition(key, value, alias, operator, model) {
|
|
143
151
|
const column = this.resolveColumnName(key, model);
|
|
@@ -181,18 +189,16 @@ class SqlConditionBuilder {
|
|
|
181
189
|
return `${alias}.${column} IS NULL`;
|
|
182
190
|
}
|
|
183
191
|
isPrimitive(value) {
|
|
184
|
-
return
|
|
192
|
+
return PRIMITIVES_SET.has(typeof value);
|
|
185
193
|
}
|
|
186
194
|
formatPrimitive(value) {
|
|
187
|
-
if (typeof value === 'string')
|
|
188
|
-
return `'${
|
|
195
|
+
if (typeof value === 'string') {
|
|
196
|
+
return `'${(0, sql_escape_1.escapeString)(value)}'`;
|
|
197
|
+
}
|
|
189
198
|
return `${value}`;
|
|
190
199
|
}
|
|
191
200
|
formatJson(value) {
|
|
192
|
-
return `'${
|
|
193
|
-
}
|
|
194
|
-
escapeString(value) {
|
|
195
|
-
return value.replace(/'/g, "''");
|
|
201
|
+
return `'${(0, sql_escape_1.escapeString)(JSON.stringify(value))}'`;
|
|
196
202
|
}
|
|
197
203
|
findRelationship(key, model) {
|
|
198
204
|
const entity = this.entityStorage.get(model);
|
|
@@ -203,16 +209,16 @@ class SqlConditionBuilder {
|
|
|
203
209
|
return typeof value !== 'object' || value === null || isDate;
|
|
204
210
|
}
|
|
205
211
|
isArrayValue(key, value) {
|
|
206
|
-
return !
|
|
212
|
+
return !OPERATORS_SET.has(key) && Array.isArray(value);
|
|
207
213
|
}
|
|
208
214
|
isLogicalOperator(key) {
|
|
209
|
-
return
|
|
215
|
+
return LOGICAL_OPERATORS_SET.has(key);
|
|
210
216
|
}
|
|
211
217
|
extractLogicalOperator(key) {
|
|
212
218
|
return key.toUpperCase().replace('$', '');
|
|
213
219
|
}
|
|
214
220
|
trackLastNonOperatorKey(key, model) {
|
|
215
|
-
if (!
|
|
221
|
+
if (!OPERATORS_SET.has(key)) {
|
|
216
222
|
this.lastKeyNotOperator = key;
|
|
217
223
|
}
|
|
218
224
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL String Escape Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides secure string escaping for SQL queries.
|
|
5
|
+
*
|
|
6
|
+
* SECURITY NOTE: While this escaping is robust, parameterized queries
|
|
7
|
+
* (prepared statements) are the gold standard for SQL injection prevention.
|
|
8
|
+
* This utility should be used only when parameterized queries are not feasible.
|
|
9
|
+
*/
|
|
10
|
+
export declare function escapeString(value: string): string;
|
|
11
|
+
export declare function escapeLikePattern(value: string): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SQL String Escape Utility
|
|
4
|
+
*
|
|
5
|
+
* Provides secure string escaping for SQL queries.
|
|
6
|
+
*
|
|
7
|
+
* SECURITY NOTE: While this escaping is robust, parameterized queries
|
|
8
|
+
* (prepared statements) are the gold standard for SQL injection prevention.
|
|
9
|
+
* This utility should be used only when parameterized queries are not feasible.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.escapeString = escapeString;
|
|
13
|
+
exports.escapeLikePattern = escapeLikePattern;
|
|
14
|
+
const QUOTE_REGEX = /'/g;
|
|
15
|
+
const BACKSLASH_REGEX = /\\/g;
|
|
16
|
+
function escapeString(value) {
|
|
17
|
+
if (value.indexOf('\x00') !== -1) {
|
|
18
|
+
throw new Error('SQL injection attempt detected: null byte in string value');
|
|
19
|
+
}
|
|
20
|
+
return value.replace(QUOTE_REGEX, "''").replace(BACKSLASH_REGEX, '\\\\');
|
|
21
|
+
}
|
|
22
|
+
function escapeLikePattern(value) {
|
|
23
|
+
const escaped = escapeString(value);
|
|
24
|
+
return escaped.replace(/[%_]/g, (char) => '\\' + char);
|
|
25
|
+
}
|
package/dist/utils.d.ts
CHANGED
package/dist/utils.js
CHANGED
|
@@ -6,9 +6,15 @@ exports.extendsFrom = extendsFrom;
|
|
|
6
6
|
function getDefaultLength(type) {
|
|
7
7
|
return null;
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const snakeCaseCache = new Map();
|
|
10
|
+
function toSnakeCase(str) {
|
|
11
|
+
let cached = snakeCaseCache.get(str);
|
|
12
|
+
if (cached) {
|
|
13
|
+
return cached;
|
|
14
|
+
}
|
|
15
|
+
cached = str[0].toLowerCase() + str.slice(1).replace(/([A-Z])/g, '_$1').toLowerCase();
|
|
16
|
+
snakeCaseCache.set(str, cached);
|
|
17
|
+
return cached;
|
|
12
18
|
}
|
|
13
19
|
function extendsFrom(baseClass, instance) {
|
|
14
20
|
if (!instance)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carno.js/orm",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "A simple ORM for Carno.js.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"repository": {
|
|
32
32
|
"type": "git",
|
|
33
|
-
"url": "git+ssh://git@github.com:
|
|
33
|
+
"url": "git+ssh://git@github.com:carnojs/carno.js.git"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/uuid": "^9.0.6",
|
|
@@ -55,5 +55,5 @@
|
|
|
55
55
|
"bun",
|
|
56
56
|
"value-object"
|
|
57
57
|
],
|
|
58
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "2be3063988696f2cd3eb13363e1f679b9f58c2f1"
|
|
59
59
|
}
|