@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.
@@ -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 initializeCacheManager;
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>;
@@ -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.initializeCacheManager();
22
+ this.cacheManager = orm.queryCacheManager;
23
23
  this.getEntity(model);
24
24
  this.statements.hooks = this.entity.hooks;
25
- this.modelTransformer = new model_transformer_1.ModelTransformer(this.entityStorage);
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
- initializeCacheManager() {
36
- try {
37
- const orm = orm_1.Orm.getInstance();
38
- this.cacheManager = orm.queryCacheManager;
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
- catch (error) {
41
- this.cacheManager = undefined;
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();
@@ -9,5 +9,5 @@ export declare class CacheKeyGenerator {
9
9
  private addLimits;
10
10
  private addJoins;
11
11
  private combineKeyParts;
12
- private hashKey;
12
+ private hashFNV1a;
13
13
  }
@@ -1,12 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CacheKeyGenerator = void 0;
4
- const crypto_1 = require("crypto");
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.hashKey(combined);
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
- hashKey(key) {
61
- return (0, crypto_1.createHash)('md5')
62
- .update(key)
63
- .digest('hex');
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
- constructor(cacheService: CacheService);
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).add(key);
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
- // se oldvalue não existir, é porque é a primeira vez que o atributo está sendo setado
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 `'${this.escapeString(value)}'`;
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 `'${this.escapeString(JSON.stringify(value))}'`;
89
+ return `'${(0, sql_escape_1.escapeString)(JSON.stringify(value))}'`;
89
90
  default:
90
- return `'${this.escapeString(String(value))}'`;
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;
@@ -1,6 +1,7 @@
1
1
  export declare class EntityRegistry {
2
2
  private store;
3
- private finalizationRegistry;
3
+ private finalizationRegistry?;
4
+ private readonly useWeakRefs;
4
5
  constructor();
5
6
  private setupFinalizationRegistry;
6
7
  get<T>(key: string): T | undefined;
@@ -4,7 +4,11 @@ exports.EntityRegistry = void 0;
4
4
  class EntityRegistry {
5
5
  constructor() {
6
6
  this.store = new Map();
7
- this.setupFinalizationRegistry();
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 weakRef = this.store.get(key);
16
- if (!weakRef) {
19
+ const value = this.store.get(key);
20
+ if (!value) {
17
21
  return undefined;
18
22
  }
19
- const entity = weakRef.deref();
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.register(entity, key, entity);
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
- this.queryCacheManager = new query_cache_manager_1.QueryCacheManager(this.cacheService);
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 operator of this.OPERATORS) {
86
- if (operator in value) {
87
- const condition = this.buildOperatorCondition(key, operator, value[operator]);
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
- return `${column} LIKE '${value}'`;
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 ['string', 'number', 'boolean', 'bigint'].includes(typeof value);
180
+ return PRIMITIVES_SET.has(typeof value);
186
181
  }
187
182
  formatPrimitive(value) {
188
- if (typeof value === 'string')
189
- return `'${this.escapeString(value)}'`;
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 `'${this.escapeString(JSON.stringify(value))}'`;
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 !this.OPERATORS.includes(key) && Array.isArray(value);
196
+ return !OPERATORS_SET.has(key) && Array.isArray(value);
204
197
  }
205
198
  isLogicalOperator(key) {
206
- return ['$or', '$and'].includes(key);
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 (!this.OPERATORS.includes(key)) {
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
- const cached = identity_map_1.IdentityMapIntegration.getEntity(model, primaryKey);
46
- if (cached) {
47
- return { instance: cached, wasCached: true };
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 operator of this.OPERATORS) {
83
- if (operator in value) {
84
- const condition = this.buildOperatorCondition(key, operator, value[operator], alias, model);
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
- return `${alias}.${column} LIKE '${value}'`;
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 ['string', 'number', 'boolean', 'bigint'].includes(typeof value);
192
+ return PRIMITIVES_SET.has(typeof value);
185
193
  }
186
194
  formatPrimitive(value) {
187
- if (typeof value === 'string')
188
- return `'${this.escapeString(value)}'`;
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 `'${this.escapeString(JSON.stringify(value))}'`;
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 !this.OPERATORS.includes(key) && Array.isArray(value);
212
+ return !OPERATORS_SET.has(key) && Array.isArray(value);
207
213
  }
208
214
  isLogicalOperator(key) {
209
- return ['$or', '$and'].includes(key);
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 (!this.OPERATORS.includes(key)) {
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
@@ -1,3 +1,3 @@
1
1
  export declare function getDefaultLength(type: string): number;
2
- export declare function toSnakeCase(propertyKey1: string): string;
2
+ export declare function toSnakeCase(str: string): string;
3
3
  export declare function extendsFrom(baseClass: any, instance: any): boolean;
package/dist/utils.js CHANGED
@@ -6,9 +6,15 @@ exports.extendsFrom = extendsFrom;
6
6
  function getDefaultLength(type) {
7
7
  return null;
8
8
  }
9
- function toSnakeCase(propertyKey1) {
10
- propertyKey1 = propertyKey1[0].toLowerCase() + propertyKey1.slice(1);
11
- return propertyKey1.replace(/([A-Z])/g, '_$1').toLowerCase();
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",
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:mlusca/carno.js.git"
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": "179037d66f41ab7014a008b141afce3c9232190e"
58
+ "gitHead": "2be3063988696f2cd3eb13363e1f679b9f58c2f1"
59
59
  }