@cheetah.js/orm 0.1.6 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cheetah.js/orm",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "A simple ORM for Cheetah.js",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -29,7 +29,7 @@
29
29
  "url": "git+ssh://git@github.com:mlusca/cheetah.js.git"
30
30
  },
31
31
  "bin": {
32
- "cheetah-orm": "./src/cheetah.ts"
32
+ "cheetah-orm": "./dist/cheetah.js"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/uuid": "^9.0.6",
package/src/SqlBuilder.ts DELETED
@@ -1,573 +0,0 @@
1
- import {
2
- AutoPath,
3
- DriverInterface,
4
- FilterQuery,
5
- QueryOrderMap,
6
- Relationship,
7
- Statement, ValueOrInstance,
8
- } from './driver/driver.interface';
9
- import { EntityStorage, Options } from './domain/entities';
10
- import { Orm } from '../src';
11
- import { LoggerService } from '@cheetah.js/core';
12
- import { ValueObject } from './common/value-object';
13
-
14
- export class SqlBuilder<T> {
15
- private readonly driver: DriverInterface;
16
-
17
- private entityStorage: EntityStorage;
18
- private statements: Statement<T> = {};
19
- private entity!: Options;
20
- private model!: new () => T;
21
-
22
- private aliases: Set<string> = new Set();
23
- private lastKeyNotOperator = '';
24
- private logger: LoggerService;
25
- private updatedColumns: any[] = [];
26
-
27
- constructor(model: new () => T) {
28
- const orm = Orm.getInstance();
29
- this.driver = orm.driverInstance;
30
- this.logger = orm.logger;
31
- this.entityStorage = EntityStorage.getInstance()
32
-
33
- this.getEntity(model);
34
- }
35
-
36
- select(columns?: AutoPath<T, never, '*'>[]): SqlBuilder<T> {
37
- const tableName = this.entity.tableName || (this.model as Function).name.toLowerCase();
38
- const schema = this.entity.schema || 'public';
39
-
40
- this.statements.statement = 'select';
41
-
42
- this.statements.columns = columns;
43
- this.statements.alias = this.getAlias(tableName)
44
- this.statements.table = `"${schema}"."${tableName}"`;
45
- return this;
46
- }
47
-
48
- insert(values: Partial<{ [K in keyof T]: ValueOrInstance<T[K]> }>): SqlBuilder<T> {
49
- const {tableName, schema} = this.getTableName();
50
- for (let value in values) {
51
- if (this.extendsFrom(ValueObject, values[value].constructor.prototype)) {
52
- values[value] = (values[value] as ValueObject<any, any>).getValue();
53
- }
54
- }
55
- this.statements.statement = 'insert';
56
- this.statements.alias = this.getAlias(tableName)
57
- this.statements.table = `"${schema}"."${tableName}"`;
58
- this.statements.values = this.withUpdatedValues(this.withDefaultValues(values, this.entity), this.entity);
59
- return this;
60
- }
61
-
62
- update(values: Partial<{ [K in keyof T]: ValueOrInstance<T[K]> }>): SqlBuilder<T> {
63
- const {tableName, schema} = this.getTableName();
64
- for (let value in values) {
65
- if (this.extendsFrom(ValueObject, values[value].constructor.prototype)) {
66
- values[value] = (values[value] as ValueObject<any, any>).getValue();
67
- }
68
- }
69
-
70
- this.statements.statement = 'update';
71
- this.statements.alias = this.getAlias(tableName)
72
- this.statements.table = `${schema}.${tableName}`;
73
- this.statements.values = this.withUpdatedValues(values, this.entity);
74
- return this;
75
- }
76
-
77
- where(where: FilterQuery<T>): SqlBuilder<T> {
78
- if (typeof where === 'undefined' || Object.entries(where).length === 0) {
79
- return this;
80
- }
81
-
82
- this.statements.where = this.conditionToSql(where, this.statements.alias!);
83
- return this;
84
- }
85
-
86
- orderBy(orderBy: (QueryOrderMap<T> & {
87
- 0?: never;
88
- }) | QueryOrderMap<T>[]): SqlBuilder<T> {
89
- if (typeof orderBy === 'undefined') {
90
- return this;
91
- }
92
-
93
- this.statements.orderBy = this.objectToStringMap(orderBy);
94
- return this;
95
- }
96
-
97
- limit(limit: number | undefined): SqlBuilder<T> {
98
- this.statements.limit = limit;
99
- return this;
100
- }
101
-
102
- offset(offset: number | undefined): SqlBuilder<T> {
103
- this.statements.offset = offset;
104
- return this;
105
- }
106
-
107
- async executeAndReturnFirst(): Promise<T | undefined> {
108
- this.statements.limit = 1;
109
-
110
- const result = await this.execute();
111
-
112
- if (result.query.rows.length === 0) {
113
- return undefined;
114
- }
115
-
116
- return this.transformToModel(result.query.rows[0]);
117
- }
118
-
119
- async executeAndReturnFirstOrFail(): Promise<T> {
120
- this.statements.limit = 1;
121
-
122
- const result = await this.execute();
123
-
124
- if (result.query.rows.length === 0) {
125
- throw new Error('Result not found')
126
- }
127
-
128
- return this.transformToModel(result.query.rows[0]);
129
- }
130
-
131
- // TODO: Corrigir tipagem do retorno de acordo com o driver
132
- async executeAndReturnAll(): Promise<T[]> {
133
- const result = await this.execute()
134
-
135
- if (result.query.rows.length === 0) {
136
- return [];
137
- }
138
-
139
- return result.query.rows.map((row: any) => this.transformToModel(row));
140
- }
141
-
142
- async execute(): Promise<{ query: any, startTime: number, sql: string }> {
143
- if (!this.statements.columns) {
144
- let columns: any[] = [
145
- ...this.getColumnsEntity((this.model as Function), this.statements.alias!),
146
- ];
147
-
148
- if (this.statements.join) {
149
- columns = [
150
- ...columns,
151
- ...this.statements.join.flatMap(join => this.getColumnsEntity(join.joinEntity!, join.joinAlias)),
152
- ]
153
- }
154
-
155
- this.statements.columns = columns
156
- } else {
157
- // @ts-ignore
158
- this.statements.columns = this.statements.columns.map((column: string) => {
159
- return this.discoverColumnAlias(column)
160
- }).flat();
161
- }
162
-
163
- this.statements.columns!.push(...this.updatedColumns)
164
-
165
- const result = await this.driver.executeStatement(this.statements);
166
- // console.log(result.query.rows);
167
- this.logger.debug(`SQL: ${result.sql} [${Date.now() - result.startTime}ms]`)
168
-
169
- return result;
170
- }
171
-
172
- startTransaction(): Promise<void> {
173
- return this.driver.startTransaction();
174
- }
175
-
176
- commit(): Promise<void> {
177
- return this.driver.commitTransaction();
178
- }
179
-
180
- rollback(): Promise<void> {
181
- return this.driver.rollbackTransaction();
182
- }
183
-
184
- // TODO: transform transaction queries into just 1 query
185
- async inTransaction<T>(callback: (builder: SqlBuilder<T>) => Promise<T>): Promise<T> {
186
- await this.startTransaction();
187
- try {
188
- // @ts-ignore
189
- const result = await callback(this);
190
- await this.commit();
191
- return result;
192
- } catch (e) {
193
- await this.rollback();
194
- throw e;
195
- }
196
- }
197
-
198
- private objectToStringMap(obj: any, parentKey: string = ''): string[] {
199
- let result: string[] = [];
200
-
201
- for (let key in obj) {
202
- if (obj.hasOwnProperty(key)) {
203
- let fullKey = parentKey ? `${parentKey}.${key}` : key;
204
- if (typeof obj[key] === 'object' && obj[key] !== null) {
205
- result = result.concat(this.objectToStringMap(obj[key], fullKey));
206
- } else {
207
- result.push(`${this.discoverColumnAlias(fullKey, true)} ${obj[key]}`);
208
- }
209
- }
210
- }
211
-
212
- return result;
213
- }
214
-
215
- private discoverColumnAlias(column: string, onlyAlias = false): string {
216
- if (!column.includes('.')) {
217
- if (onlyAlias) {
218
- return `${this.statements.alias!}."${column}"`
219
- }
220
- return `${this.statements.alias!}."${column}" as ${this.statements.alias!}_${column}`;
221
- }
222
-
223
- if (typeof this.statements.join === 'undefined') {
224
- throw new Error('Join not found');
225
- }
226
-
227
- const entities = column.split('.');
228
- let lastEntity = this.model as Function;
229
- let lastAlias = this.statements.alias!;
230
-
231
- const relationsMap = new Map(this.entity.relations.map(rel => [rel.propertyKey, rel]));
232
- const joinMap = new Map(this.statements.join!.map(join => [join.joinProperty, join]));
233
-
234
- for (let i = 0; i < entities.length; i++) {
235
- if (i === 0) {
236
- const relation = relationsMap.get(entities[i]);
237
- lastEntity = relation?.entity() as Function;
238
- // @ts-ignore
239
- lastAlias = joinMap.get(entities[i])?.joinAlias;
240
- } else {
241
- if ((i + 1) === entities.length) {
242
- if (onlyAlias) {
243
- return `${lastAlias}."${entities[i]}"`
244
- }
245
- return `${lastAlias}."${entities[i]}" as ${lastAlias}_${entities[i]}`;
246
- }
247
-
248
- const lastStatement = joinMap.get(entities[i]);
249
- lastEntity = lastStatement?.joinEntity as Function;
250
- // @ts-ignore
251
- lastAlias = lastStatement?.joinAlias;
252
- }
253
- }
254
-
255
- return '';
256
- }
257
-
258
- private getTableName() {
259
- const tableName = this.entity.tableName || (this.model as Function).name.toLowerCase();
260
- const schema = this.entity.schema || 'public';
261
- return {tableName, schema};
262
- }
263
-
264
- private addSimpleConditionToSql(key: string, value: any, alias: string | null = null, operator: string = '='): string {
265
- const aliasToUse = alias || this.statements.alias;
266
- const valueByType = (typeof value === 'string') ? `'${value}'` : value;
267
-
268
- return `${aliasToUse}.${key} ${operator} ${valueByType}`;
269
- }
270
-
271
- private addInConditionToSql(key: string, values: any[], alias: string | null = null): string {
272
- const aliasToUse = alias || this.statements.alias;
273
- return `${aliasToUse}.${key} IN (${values.map(val => (typeof val === 'string') ? `'${val}'` : val).join(", ")})`;
274
- }
275
-
276
- private addLogicalOperatorToSql(conditions: string[], operator: 'AND' | 'OR'): string {
277
- return `(${conditions.join(` ${operator} `)})`;
278
- }
279
-
280
- private conditionToSql(condition: FilterQuery<T>, alias: string): string {
281
- const sqlParts = [];
282
- const operators = ['$eq', '$ne', '$in', '$nin', '$like', '$gt', '$gte', '$lt', '$lte', '$and', '$or'];
283
-
284
- for (let [key, value] of Object.entries(condition)) {
285
- if (this.extendsFrom(ValueObject, value.constructor.prototype)) {
286
- value = (value as ValueObject<any, any>).getValue();
287
- }
288
-
289
- if (!operators.includes(key)) {
290
- this.lastKeyNotOperator = key;
291
- }
292
-
293
- const relationShip = this.entity.relations?.find(rel => rel.propertyKey === key);
294
- if (relationShip) {
295
- sqlParts.push(this.applyJoin(relationShip, value, alias));
296
- } else if (typeof value !== 'object' || value === null) {
297
- if (key === '$eq') {
298
- sqlParts.push(this.addSimpleConditionToSql(this.lastKeyNotOperator, value, alias, '='));
299
- continue;
300
- }
301
-
302
- sqlParts.push(this.addSimpleConditionToSql(key, value, alias));
303
- } else if (!(operators.includes(key)) && Array.isArray(value)) {
304
- sqlParts.push(this.addInConditionToSql(key, value, alias));
305
- } else {
306
- if (['$or', '$and'].includes(key)) {
307
- sqlParts.push(this.addLogicalOperatorToSql(value.map((cond: any) => this.conditionToSql(cond, alias)), key.toUpperCase().replace('$', '') as 'AND' | 'OR'));
308
- }
309
-
310
- for (const operator of operators) {
311
- if (operator in value) {
312
- switch (operator) {
313
- case '$eq':
314
- sqlParts.push(this.addSimpleConditionToSql(key, value['$eq'], alias, '='));
315
- break;
316
- case '$ne':
317
- sqlParts.push(this.addSimpleConditionToSql(key, value['$ne'], alias, '!='));
318
- break;
319
- case '$in':
320
- sqlParts.push(this.addInConditionToSql(key, value['$in'], alias));
321
- break;
322
- case '$nin':
323
- sqlParts.push(`${alias}.${key} NOT IN (${value['$nin'].map((val: any) => this.t(val)).join(", ")})`);
324
- break;
325
- case '$like':
326
- sqlParts.push(`${alias}.${key} LIKE '${value['$like']}'`);
327
- break;
328
- case '$gt':
329
- sqlParts.push(`${alias}.${key} > ${value['$gt']}`);
330
- break;
331
- case '$gte':
332
- sqlParts.push(`${alias}.${key} >= ${value['$gte']}`);
333
- break;
334
- case '$lt':
335
- sqlParts.push(`${alias}.${key} < ${value['$lt']}`);
336
- break;
337
- case '$lte':
338
- sqlParts.push(`${alias}.${key} <= ${value['$lte']}`);
339
- break;
340
- case '$and':
341
- case '$or':
342
- const parts = value[operator].map((cond: any) => this.conditionToSql(cond, alias))
343
- sqlParts.push(this.addLogicalOperatorToSql(parts, operator.toUpperCase().replace('$', '') as 'AND' | 'OR'));
344
- break;
345
- }
346
- }
347
- }
348
- }
349
- }
350
-
351
- return this.addLogicalOperatorToSql(sqlParts, 'AND');
352
- }
353
-
354
- private t(value: any) {
355
- return (typeof value === 'string') ? `'${value}'` : value;
356
- }
357
-
358
- private applyJoin(relationShip: Relationship<any>, value: FilterQuery<any>, alias: string) {
359
- const {tableName, schema} = this.getTableName();
360
- const {
361
- tableName: joinTableName,
362
- schema: joinSchema,
363
- } = this.entityStorage.get((relationShip.entity() as Function)) || {
364
- tableName: (relationShip.entity() as Function).name.toLowerCase(),
365
- schema: 'public',
366
- };
367
- let originPrimaryKey = 'id';
368
- for (const prop in this.entity.showProperties) {
369
- if (this.entity.showProperties[prop].options.isPrimary) {
370
- originPrimaryKey = prop;
371
- break;
372
- }
373
- }
374
- const joinAlias = `${this.getAlias(joinTableName)}`;
375
- const joinWhere = this.conditionToSql(value, joinAlias)
376
- this.statements.join = this.statements.join || [];
377
- let on = '';
378
-
379
- switch (relationShip.relation) {
380
- case "one-to-many":
381
- on = `${joinAlias}.${this.getFkKey(relationShip)} = ${alias}.${originPrimaryKey}`;
382
- break;
383
- case "many-to-one":
384
- on = `${alias}.${relationShip.propertyKey as string} = ${joinAlias}.${this.getFkKey(relationShip)}`;
385
- break;
386
- }
387
-
388
- this.statements.join.push({
389
- joinAlias: joinAlias,
390
- joinTable: joinTableName,
391
- joinSchema: joinSchema || 'public',
392
- joinWhere: joinWhere,
393
- joinProperty: relationShip.propertyKey as string,
394
- originAlias: alias,
395
- originSchema: schema,
396
- originTable: tableName,
397
- propertyKey: relationShip.propertyKey,
398
- joinEntity: (relationShip.entity() as Function),
399
- type: 'LEFT',
400
- // @ts-ignore
401
- on,
402
- originalEntity: relationShip.originalEntity as Function,
403
- })
404
-
405
- return joinWhere;
406
- }
407
-
408
- private getFkKey(relationShip: Relationship<any>): string | number {
409
- // se for nullable, deverá retornar o primary key da entidade target
410
- if (typeof relationShip.fkKey === 'undefined') {
411
- return 'id'; // TODO: Pegar dinamicamente o primary key da entidade target
412
- }
413
-
414
- // se o fkKey é uma função, ele retornará a propriedade da entidade que é a chave estrangeira
415
- // precisamos pegar o nome dessa propriedade
416
- if (typeof relationShip.fkKey === 'string') {
417
- return relationShip.fkKey;
418
- }
419
-
420
- const match = /\.(?<propriedade>[\w]+)/.exec(relationShip.fkKey.toString());
421
- return match ? match.groups!.propriedade : '';
422
- }
423
-
424
- // private conditionLogicalOperatorToSql<T extends typeof BaseEntity>(conditions: Condition<T>[], operator: 'AND' | 'OR'): string {
425
- // const sqlParts = conditions.map(cond => this.conditionToSql(cond));
426
- // return this.addLogicalOperatorToSql(sqlParts, operator);
427
- // }
428
-
429
-
430
- private getEntity(model: new () => T) {
431
- const entity = this.entityStorage.get((model as Function));
432
- this.model = model;
433
-
434
- if (!entity) {
435
- throw new Error('Entity not found');
436
- }
437
-
438
- this.entity = entity;
439
- }
440
-
441
- private transformToModel<T>(data: any): T {
442
- const instance = new (this.model as any)()
443
-
444
- instance.$_isPersisted = true;
445
- const entitiesByAlias: { [key: string]: Function } = {
446
- [this.statements.alias!]: instance,
447
- }
448
- const entitiesOptions: Map<string, Options> = new Map();
449
- entitiesOptions.set(this.statements.alias!, this.entityStorage.get(instance.constructor)!)
450
-
451
- if (this.statements.join) {
452
- this.statements.join.forEach(join => {
453
- const joinInstance = new (join.joinEntity! as any)();
454
- joinInstance.$_isPersisted = true;
455
- entitiesByAlias[join.joinAlias] = joinInstance;
456
- entitiesOptions.set(join.joinAlias, this.entityStorage.get(joinInstance.constructor)!)
457
- })
458
- }
459
-
460
- Object.entries(data).forEach(([key, value]) => {
461
- const [alias, prop] = key.split('_');
462
- const entity = entitiesByAlias[alias];
463
- if (!entity) {
464
- return;
465
- }
466
-
467
- const entityProperty = entitiesOptions.get(alias)!.showProperties[prop]
468
- if (entityProperty) {
469
-
470
- if (this.extendsFrom(ValueObject, entityProperty.type.prototype)) {
471
- // @ts-ignore
472
- entity[prop] = new entityProperty.type(value);
473
- return;
474
- }
475
- // @ts-ignore
476
- entity[prop] = value;
477
- }
478
- });
479
-
480
- if (this.statements.join) {
481
- this.statements.join.forEach(join => {
482
- const {joinAlias, originAlias, propertyKey} = join;
483
- const originEntity = entitiesByAlias[originAlias];
484
- const joinEntity = entitiesByAlias[joinAlias];
485
- const property = entitiesOptions.get(originAlias)!.relations.find(rel => rel.propertyKey === propertyKey);
486
-
487
- if (!originEntity || !joinEntity) {
488
- return;
489
- }
490
-
491
- // @ts-ignore
492
- originEntity[propertyKey] = (property.type === Array) ? (originEntity[propertyKey]) ? [...originEntity[propertyKey], joinEntity] : [joinEntity] : joinEntity;
493
- })
494
- }
495
-
496
- return instance;
497
- }
498
-
499
- /**
500
- * Retrieves an alias for a given table name.
501
- *
502
- * @param {string} tableName - The name of the table.
503
- * @private
504
- * @returns {string} - The alias for the table name.
505
- */
506
- private getAlias(tableName: string): string {
507
- const alias = tableName.split('').shift() || '';
508
-
509
- let counter = 1;
510
- let uniqueAlias = `${alias}${counter}`;
511
-
512
- while (this.aliases.has(uniqueAlias)) {
513
- counter++;
514
- uniqueAlias = `${alias}${counter}`;
515
- }
516
-
517
- this.aliases.add(uniqueAlias);
518
- return uniqueAlias;
519
- }
520
-
521
- private getColumnsEntity(entity: Function, alias: string): string [] {
522
- const e = this.entityStorage.get(entity);
523
- if (!e) {
524
- throw new Error('Entity not found');
525
- }
526
-
527
- return Object.keys(e.showProperties).map(key => `${alias}."${key}" as "${alias}_${key}"`);
528
- }
529
-
530
- private withDefaultValues(values: any, entityOptions: Options) {
531
- const property = Object.entries(entityOptions.showProperties).filter(([_, value]) => value.options.onInsert);
532
- const defaultProperties = Object.entries(entityOptions.showProperties).filter(([_, value]) => value.options.default);
533
-
534
- for (const [key, property] of defaultProperties) {
535
- if (typeof values[key] === 'undefined') {
536
- if (typeof property.options.default === 'function') {
537
- values[key] = eval(property.options.default());
538
- } else {
539
- values[key] = eval(property.options.default);
540
- }
541
- }
542
- }
543
-
544
- property.forEach(([key, property]) => {
545
- values[key] = property.options.onInsert!();
546
- this.updatedColumns.push(`${this.statements.alias}."${key}" as "${this.statements.alias}_${key}"`)
547
- });
548
-
549
- return values;
550
- }
551
-
552
- private withUpdatedValues(values: any, entityOptions: Options) {
553
- const property = Object.entries(entityOptions.showProperties).filter(([_, value]) => value.options.onUpdate);
554
-
555
- property.forEach(([key, property]) => {
556
- values[key] = property.options.onUpdate!();
557
- this.updatedColumns.push(`${this.statements.alias}."${key}" as "${this.statements.alias}_${key}"`)
558
- });
559
-
560
- return values;
561
- }
562
-
563
- private extendsFrom(baseClass, instance) {
564
- let proto = Object.getPrototypeOf(instance);
565
- while (proto) {
566
- if (proto === baseClass.prototype) {
567
- return true;
568
- }
569
- proto = Object.getPrototypeOf(proto);
570
- }
571
- return false;
572
- }
573
- }
package/src/cheetah.ts DELETED
@@ -1,28 +0,0 @@
1
- import {Migrator} from "./migration/migrator";
2
- import { Command } from 'commander';
3
-
4
- const program = new Command();
5
-
6
- program
7
- .name('cheetah-cli')
8
- .description('CLI to Cheetah.js ORM ')
9
-
10
- program.command('migration:generate')
11
- .description('generate a new migration file with a diff')
12
- .action(async (str, options) => {
13
- const migrator = new Migrator()
14
- await migrator.initConfigFile()
15
- await migrator.createMigration()
16
- process.exit(0)
17
- });
18
-
19
- program.command('migration:run')
20
- .description('run all pending migrations')
21
- .action(async (str, options) => {
22
- const migrator = new Migrator()
23
- await migrator.initConfigFile()
24
- await migrator.migrate()
25
- process.exit(0)
26
- });
27
-
28
- program.parse();
@@ -1,10 +0,0 @@
1
- import { ValueObject } from './value-object';
2
-
3
- const REGEX = /^[a-z0-9.]+@[a-z0-9]+\.[a-z]+(\.[a-z]+)?$/i;
4
-
5
- export class Email extends ValueObject<string, Email> {
6
-
7
- protected validate(value: string): boolean {
8
- return REGEX.test(value);
9
- }
10
- }
@@ -1,8 +0,0 @@
1
- import { ValueObject } from './value-object';
2
-
3
- export class Uuid extends ValueObject<string, Uuid> {
4
-
5
- protected validate(value: string): boolean {
6
- return /^[a-f\d]{8}(-[a-f\d]{4}){4}[a-f\d]{8}$/i.test(value);
7
- }
8
- }