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