@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.
Files changed (93) hide show
  1. package/README.md +228 -0
  2. package/build.ts +14 -0
  3. package/cheetah.config.ts +14 -0
  4. package/dist/SqlBuilder.d.ts +57 -0
  5. package/dist/SqlBuilder.js +436 -0
  6. package/dist/SqlBuilder.js.map +1 -0
  7. package/dist/bun/index.d.ts +13 -0
  8. package/dist/bun/index.js +224319 -0
  9. package/dist/bun/index.js.map +306 -0
  10. package/dist/cheetah.d.ts +1 -0
  11. package/dist/cheetah.js +24 -0
  12. package/dist/cheetah.js.map +1 -0
  13. package/dist/constants.d.ts +4 -0
  14. package/dist/constants.js +5 -0
  15. package/dist/constants.js.map +1 -0
  16. package/dist/decorators/entity.decorator.d.ts +3 -0
  17. package/dist/decorators/entity.decorator.js +10 -0
  18. package/dist/decorators/entity.decorator.js.map +1 -0
  19. package/dist/decorators/index.decorator.d.ts +3 -0
  20. package/dist/decorators/index.decorator.js +17 -0
  21. package/dist/decorators/index.decorator.js.map +1 -0
  22. package/dist/decorators/one-many.decorator.d.ts +3 -0
  23. package/dist/decorators/one-many.decorator.js +17 -0
  24. package/dist/decorators/one-many.decorator.js.map +1 -0
  25. package/dist/decorators/primary-key.decorator.d.ts +2 -0
  26. package/dist/decorators/primary-key.decorator.js +6 -0
  27. package/dist/decorators/primary-key.decorator.js.map +1 -0
  28. package/dist/decorators/property.decorator.d.ts +15 -0
  29. package/dist/decorators/property.decorator.js +25 -0
  30. package/dist/decorators/property.decorator.js.map +1 -0
  31. package/dist/domain/base-entity.d.ts +42 -0
  32. package/dist/domain/base-entity.js +106 -0
  33. package/dist/domain/base-entity.js.map +1 -0
  34. package/dist/domain/collection.d.ts +6 -0
  35. package/dist/domain/collection.js +11 -0
  36. package/dist/domain/collection.js.map +1 -0
  37. package/dist/domain/entities.d.ts +40 -0
  38. package/dist/domain/entities.js +137 -0
  39. package/dist/domain/entities.js.map +1 -0
  40. package/dist/domain/reference.d.ts +4 -0
  41. package/dist/domain/reference.js +7 -0
  42. package/dist/domain/reference.js.map +1 -0
  43. package/dist/driver/driver.interface.d.ts +270 -0
  44. package/dist/driver/driver.interface.js +2 -0
  45. package/dist/driver/driver.interface.js.map +1 -0
  46. package/dist/driver/pg-driver.d.ts +43 -0
  47. package/dist/driver/pg-driver.js +255 -0
  48. package/dist/driver/pg-driver.js.map +1 -0
  49. package/dist/index.d.ts +13 -0
  50. package/dist/index.js +16 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/migration/diff-calculator.d.ts +17 -0
  53. package/dist/migration/diff-calculator.js +230 -0
  54. package/dist/migration/diff-calculator.js.map +1 -0
  55. package/dist/migration/migrator.d.ts +18 -0
  56. package/dist/migration/migrator.js +233 -0
  57. package/dist/migration/migrator.js.map +1 -0
  58. package/dist/orm.d.ts +14 -0
  59. package/dist/orm.js +23 -0
  60. package/dist/orm.js.map +1 -0
  61. package/dist/orm.service.d.ts +8 -0
  62. package/dist/orm.service.js +115 -0
  63. package/dist/orm.service.js.map +1 -0
  64. package/dist/utils.d.ts +1 -0
  65. package/dist/utils.js +16 -0
  66. package/dist/utils.js.map +1 -0
  67. package/package.json +50 -0
  68. package/src/SqlBuilder.ts +542 -0
  69. package/src/cheetah.ts +28 -0
  70. package/src/constants.ts +4 -0
  71. package/src/decorators/entity.decorator.ts +10 -0
  72. package/src/decorators/index.decorator.ts +18 -0
  73. package/src/decorators/one-many.decorator.ts +19 -0
  74. package/src/decorators/primary-key.decorator.ts +6 -0
  75. package/src/decorators/property.decorator.ts +41 -0
  76. package/src/domain/base-entity.ts +149 -0
  77. package/src/domain/collection.ts +10 -0
  78. package/src/domain/entities.ts +159 -0
  79. package/src/domain/reference.ts +5 -0
  80. package/src/driver/driver.interface.ts +331 -0
  81. package/src/driver/pg-driver.ts +308 -0
  82. package/src/index.ts +17 -0
  83. package/src/migration/diff-calculator.ts +258 -0
  84. package/src/migration/migrator.ts +278 -0
  85. package/src/orm.service.ts +115 -0
  86. package/src/orm.ts +30 -0
  87. package/src/utils.ts +18 -0
  88. package/test/domain/base-entity.spec.ts +463 -0
  89. package/test/migration/.sql +5 -0
  90. package/test/migration/cheetah.config.ts +13 -0
  91. package/test/migration/migator.spec.ts +251 -0
  92. package/test/migration/test.sql +5 -0
  93. package/test/node-database.ts +32 -0
@@ -0,0 +1,278 @@
1
+ import 'reflect-metadata';
2
+ import { ColDiff, ConnectionSettings, DriverInterface, SnapshotTable } from '@cheetah.js/orm/driver/driver.interface';
3
+ import { globby } from 'globby'
4
+ import { Orm, OrmService } from '@cheetah.js/orm';
5
+ import { InjectorService, LoggerService } from '@cheetah.js/core';
6
+ import { EntityStorage } from '@cheetah.js/orm/domain/entities';
7
+ import * as path from 'path';
8
+ import * as fs from 'fs';
9
+ import { DiffCalculator } from './diff-calculator';
10
+
11
+ export class Migrator {
12
+ config: ConnectionSettings<any>;
13
+ orm: Orm<any>;
14
+ entities: EntityStorage = new EntityStorage();
15
+
16
+ constructor() {
17
+ }
18
+
19
+ async initConfigFile(basePath: string = process.cwd()) {
20
+ const paths = await globby(['cheetah.config.ts'], {absolute: true, cwd: basePath})
21
+
22
+ if (paths.length === 0) {
23
+ throw new Error('Config file not found');
24
+ }
25
+
26
+ const config = await import(paths[0]);
27
+ this.config = config.default;
28
+
29
+ if (typeof this.config.entities === 'string') {
30
+ const paths = await globby(this.config.entities, {absolute: true, cwd: basePath})
31
+ for (const path of paths) {
32
+ await import(path);
33
+ }
34
+ }
35
+
36
+ this.orm = new Orm(this.config, new LoggerService(new InjectorService()));
37
+ await this.orm.connect();
38
+ }
39
+
40
+ async initMigration() {
41
+ this.entities = new EntityStorage();
42
+ const serv = new OrmService(this.entities)
43
+ serv.onInit();
44
+ const snapshotBd = await this.snapshotBd();
45
+ const snapshotEntities = await this.snapshotEntities();
46
+ const calculator = new DiffCalculator(this.entities);
47
+
48
+ return calculator.diff(snapshotBd, snapshotEntities);
49
+ }
50
+
51
+ private async snapshotBd(): Promise<SnapshotTable[]> {
52
+ const snapshot = []
53
+ for (let [_, values] of this.entities.entries()) {
54
+ const bd = await this.orm.driverInstance.snapshot(values.tableName);
55
+ if (!bd) {
56
+ continue;
57
+ }
58
+ snapshot.push(bd)
59
+ }
60
+ return snapshot;
61
+ }
62
+
63
+ private async snapshotEntities() {
64
+ const snapshot = []
65
+ for (let [_, values] of this.entities.entries()) {
66
+ snapshot.push(await this.entities.snapshot(values))
67
+ }
68
+ return snapshot;
69
+ }
70
+
71
+
72
+ async createMigration(forceName: string | null = null): Promise<void> {
73
+ const diff = await this.initMigration();
74
+ const migrationDirectory = path.join(process.cwd(), this.config.migrationPath ?? 'database/migrations');
75
+ const migrationFileName = (forceName ?? `migration_${new Date().toISOString().replace(/[^\d]/g, '')}`) + `.sql`;
76
+ const migrationFilePath = path.join(migrationDirectory, migrationFileName);
77
+
78
+ if (!fs.existsSync(migrationDirectory)) {
79
+ fs.mkdirSync(migrationDirectory, {recursive: true});
80
+ }
81
+
82
+ let sqlInstructions: string[] = [];
83
+
84
+ diff.forEach(tableDiff => {
85
+ const tableName = tableDiff.tableName;
86
+ const schema = tableDiff.schema;
87
+
88
+ if (tableDiff.newTable) {
89
+ const indexes = tableDiff.colDiffs.filter(colDiff => colDiff.actionType === 'INDEX');
90
+ const creates = tableDiff.colDiffs.filter(colDiff => colDiff.actionType === 'CREATE');
91
+ const fks = tableDiff.colDiffs.filter(colDiff => colDiff.colChanges?.foreignKeys);
92
+
93
+ sqlInstructions.push(this.orm.driverInstance.getCreateTableInstruction(schema, tableName, creates));
94
+
95
+ indexes.forEach(colDiff => {
96
+ colDiff.indexTables = colDiff.indexTables?.filter(index => index.name !== `${tableName}_pkey`);
97
+ });
98
+
99
+ fks.forEach(colDiff => {
100
+ colDiff.colChanges?.foreignKeys?.forEach(fk => {
101
+ sqlInstructions.push(this.orm.driverInstance.getAlterTableFkInstruction(schema, tableName, colDiff, fk));
102
+ });
103
+ });
104
+
105
+ sqlInstructions = sqlInstructions.map(sql => sql.replace(/' '/g, ''));
106
+ indexes.forEach(colDiff => {
107
+ if (colDiff.indexTables) {
108
+ colDiff.indexTables.forEach(index => {
109
+ if (index.properties) {
110
+ sqlInstructions.push(this.orm.driverInstance.getCreateIndex(index, schema, tableName));
111
+ }
112
+ });
113
+ }
114
+ })
115
+ return;
116
+ }
117
+
118
+ // Agrupar alterações por coluna
119
+ const colChangesMap = new Map<string, ColDiff[]>();
120
+
121
+ tableDiff.colDiffs.reverse().forEach(colDiff => {
122
+ const colName = colDiff.colName;
123
+
124
+ if (!colChangesMap.has(colName)) {
125
+ colChangesMap.set(colName, []);
126
+ }
127
+
128
+ colChangesMap.get(colName)?.push(colDiff);
129
+ });
130
+
131
+ // Gerar instruções SQL agrupadas por coluna
132
+ colChangesMap.forEach((colDiffs, colName) => {
133
+ const colDiffInstructions: string[] = [];
134
+
135
+ colDiffs.forEach(colDiff => {
136
+ switch (colDiff.actionType) {
137
+ case 'CREATE':
138
+ (this.orm.driverInstance as DriverInterface).getAddColumn(schema, tableName, colName, colDiff, colDiffInstructions);
139
+ break;
140
+ case 'DELETE':
141
+ (this.orm.driverInstance as DriverInterface).getDropColumn(colDiffInstructions, schema, tableName, colName);
142
+ break;
143
+ case 'ALTER':
144
+ this.applyColumnChanges(colDiff, colDiffInstructions, schema, tableName, colName);
145
+ break;
146
+ case "INDEX":
147
+ if (colDiff.indexTables) {
148
+ colDiff.indexTables.forEach(index => {
149
+ // if already exists instruction for this index, skip
150
+ if (colDiffInstructions.find(instruction => instruction.includes(index.name))) {
151
+ return;
152
+ }
153
+
154
+ if (this.alreadyConstraint(sqlInstructions, index.name)) {
155
+ return;
156
+ }
157
+
158
+ if (index.properties) {
159
+ colDiffInstructions.push((this.orm.driverInstance as DriverInterface).getCreateIndex(index, schema, tableName));
160
+ } else {
161
+ colDiffInstructions.push((this.orm.driverInstance as DriverInterface).getDropIndex(index, schema, tableName));
162
+ }
163
+ });
164
+ }
165
+ break;
166
+ // Adicione lógica para outros tipos de ação, se necessário
167
+ }
168
+ });
169
+
170
+ // Se houver instruções para a coluna, agrupe-as
171
+ if (colDiffInstructions.length > 0) {
172
+ sqlInstructions.push(colDiffInstructions.join('\n'));
173
+ }
174
+ });
175
+ });
176
+ const migrationContent = sqlInstructions.join('\n');
177
+
178
+ if (migrationContent.length === 0) {
179
+ console.log('No changes detected');
180
+ return;
181
+ } else {
182
+ fs.writeFileSync(migrationFilePath, migrationContent);
183
+
184
+ console.log(`Migration file created: ${migrationFilePath}`);
185
+ }
186
+ }
187
+
188
+ async migrate() {
189
+ const migrationTable = 'cheetah_migrations';
190
+ const migrationDirectory = path.join(process.cwd(), this.config.migrationPath ?? 'database/migrations');
191
+ const migrationFiles = fs
192
+ .readdirSync(migrationDirectory)
193
+ .filter(file => file.endsWith('.sql'))
194
+ .sort();
195
+
196
+ if (migrationFiles.length === 0) {
197
+ console.log('No migration files found');
198
+ return;
199
+ }
200
+
201
+ this.orm.driverInstance.executeSql(`CREATE TABLE IF NOT EXISTS "${migrationTable}" ("migration_file" character varying(255) NOT NULL PRIMARY KEY UNIQUE);`);
202
+ // get the migration fil
203
+ const migrated = await this.orm.driverInstance.executeSql(`SELECT * FROM "${migrationTable}" ORDER BY "migration_file" ASC;`);
204
+ const lastMigration = migrated.rows[migrated.rows.length - 1];
205
+ const lastMigrationIndex = migrationFiles.indexOf(lastMigration?.migration_file ?? '');
206
+ const migrationsToExecute = migrationFiles.slice(lastMigrationIndex + 1);
207
+
208
+ if (migrationsToExecute.length === 0) {
209
+ console.log('Database is up to date');
210
+ return;
211
+ }
212
+
213
+ for (const migrationFile of migrationsToExecute) {
214
+ const migrationFilePath = path.join(migrationDirectory, migrationFile);
215
+ const migrationContent = fs.readFileSync(migrationFilePath, {encoding: 'utf-8'});
216
+ const sqlInstructions = migrationContent.split(';').filter(sql => sql.trim().length > 0);
217
+
218
+ for (const sqlInstruction of sqlInstructions) {
219
+ await this.orm.driverInstance.executeSql(sqlInstruction);
220
+ }
221
+
222
+ await this.orm.driverInstance.executeSql(`INSERT INTO "${migrationTable}" ("migration_file") VALUES ('${migrationFile}');`);
223
+
224
+ console.log(`Migration executed: ${migrationFile}`);
225
+ }
226
+ }
227
+
228
+ private applyColumnChanges(colDiff: ColDiff, sqlInstructions: string[], schema: string | undefined, tableName: string, colName: string) {
229
+ if (colDiff.colType) {
230
+ sqlInstructions.push( (this.orm.driverInstance as DriverInterface).getAlterTableType(schema, tableName, colName, colDiff));
231
+ }
232
+
233
+ if (colDiff.colChanges) {
234
+ if (colDiff.colChanges.default !== undefined) {
235
+ sqlInstructions.push((this.orm.driverInstance as DriverInterface).getAlterTableDefaultInstruction(schema, tableName, colName, colDiff));
236
+ }
237
+
238
+ if (colDiff.colChanges.primary !== undefined) {
239
+ if (colDiff.colChanges.primary) {
240
+ sqlInstructions.push((this.orm.driverInstance as DriverInterface).getAlterTablePrimaryKeyInstruction(schema, tableName, colName, colDiff));
241
+ } else {
242
+ sqlInstructions.push((this.orm.driverInstance as DriverInterface).getDropConstraint({name: `${tableName}_pkey`}, schema, tableName));
243
+ }
244
+ }
245
+
246
+ if (colDiff.colChanges.unique !== undefined && !this.alreadyConstraint(sqlInstructions, `${tableName}_${colName}_key`)) {
247
+ if (colDiff.colChanges.unique) {
248
+ sqlInstructions.push((this.orm.driverInstance as DriverInterface).getAddUniqueConstraint(schema, tableName, colName));
249
+ } else {
250
+ sqlInstructions.push((this.orm.driverInstance as DriverInterface).getDropConstraint({name: `${tableName}_${colName}_key`}, schema, tableName));
251
+ }
252
+ }
253
+
254
+ if(colDiff.colChanges.nullable !== undefined) {
255
+ if (colDiff.colChanges.nullable) {
256
+ sqlInstructions.push((this.orm.driverInstance as DriverInterface).getAlterTableDropNullInstruction(schema, tableName, colName, colDiff));
257
+ } else {
258
+ sqlInstructions.push((this.orm.driverInstance as DriverInterface).getAlterTableDropNotNullInstruction(schema, tableName, colName, colDiff));
259
+ }
260
+ }
261
+
262
+ if (colDiff.colChanges.foreignKeys !== undefined) {
263
+ if (colDiff.colChanges.foreignKeys.length === 0) {
264
+ sqlInstructions.push((this.orm.driverInstance as DriverInterface).getDropConstraint({name: `${tableName}_${colName}_fk`}, schema, tableName));
265
+ }
266
+
267
+ colDiff.colChanges.foreignKeys.forEach(fk => {
268
+ sqlInstructions.push((this.orm.driverInstance as DriverInterface).getAlterTableFkInstruction(schema, tableName, colDiff, fk))
269
+ });
270
+ }
271
+ // Adicione lógica para outras alterações necessárias
272
+ }
273
+ }
274
+
275
+ private alreadyConstraint(sqlInstructions: string[], s: string): boolean {
276
+ return sqlInstructions.some(sql => sql.includes(`"${s}"`));
277
+ }
278
+ }
@@ -0,0 +1,115 @@
1
+ import { Metadata, OnApplicationInit, Service } from '@cheetah.js/core';
2
+ import { EntityStorage, Property } from './domain/entities';
3
+ import { ENTITIES, PROPERTIES_METADATA, PROPERTIES_RELATIONS } from './constants';
4
+ import { globbySync } from 'globby';
5
+ import { Project, SyntaxKind } from 'ts-morph';
6
+ import { Orm } from '@cheetah.js/orm/orm';
7
+ import { ConnectionSettings } from '@cheetah.js/orm/driver/driver.interface';
8
+
9
+ @Service()
10
+ export class OrmService {
11
+ private allEntities = new Map<string, { nullables: string[], defaults: { [key: string]: any } }>();
12
+
13
+ constructor(private storage: EntityStorage, entityFile: string | undefined = undefined) {
14
+ console.log('Preparing entities...')
15
+ const files = new Project({skipLoadingLibFiles: true}).addSourceFilesAtPaths(entityFile ?? this.getSourceFilePaths())
16
+ files.forEach(file => {
17
+ file.getClasses().forEach(classDeclaration => {
18
+ if (classDeclaration.getDecorator('Entity')) {
19
+
20
+ const properties = classDeclaration.getProperties();
21
+ const nullables: string[] = [];
22
+ const defaults: { [key: string]: any } = {};
23
+
24
+ properties.forEach(property => {
25
+ const propertyName = property.getName();
26
+ const isNullable = property.hasQuestionToken();
27
+ const initializer = property.getInitializer();
28
+ if (isNullable) {
29
+ nullables.push(propertyName);
30
+ }
31
+ if (initializer) {
32
+ const initializerKind = initializer.getKind();
33
+
34
+ switch (initializerKind) {
35
+ case SyntaxKind.StringLiteral:
36
+ defaults[propertyName] = initializer.getText();
37
+ break;
38
+ case SyntaxKind.NumericLiteral:
39
+ defaults[propertyName] = parseFloat(initializer.getText());
40
+ break;
41
+ default:
42
+ defaults[propertyName] = () => initializer.getText();
43
+ break;
44
+ }
45
+ }
46
+
47
+ this.allEntities.set(classDeclaration.getName() as string, {nullables, defaults});
48
+ });
49
+ }
50
+ });
51
+ })
52
+ }
53
+
54
+ @OnApplicationInit()
55
+ async onInit() {
56
+ const configFile = globbySync('cheetah.config.ts', {absolute: true});
57
+ if (configFile.length === 0) {
58
+ console.log('No config file found!')
59
+ return;
60
+ }
61
+ const config = await import(configFile[0]);
62
+
63
+ if (typeof config.default.entities === 'string') {
64
+ const files = globbySync([config.default.entities, '!node_modules'], {gitignore: true, absolute: true})
65
+
66
+ for (const file of files) {
67
+ await import(file)
68
+ }
69
+ }
70
+
71
+ const entities = Metadata.get(ENTITIES, Reflect);
72
+
73
+ if (!entities) {
74
+ console.log('No entities found!')
75
+ return;
76
+ }
77
+
78
+ for (const entity of entities) {
79
+ const nullableDefaultEntity = this.allEntities.get(entity.target.name);
80
+ const properties: { [key: string]: Property } = Metadata.get(PROPERTIES_METADATA, entity.target);
81
+ const relationship = Metadata.get(PROPERTIES_RELATIONS, entity.target);
82
+
83
+ for (const property in properties) {
84
+ if (nullableDefaultEntity?.nullables.includes(property)) {
85
+ properties[property].options.nullable = true;
86
+ }
87
+ if (nullableDefaultEntity?.defaults[property]) {
88
+ properties[property].options.default = nullableDefaultEntity?.defaults[property];
89
+ }
90
+ }
91
+
92
+ this.storage.add(entity, properties, relationship);
93
+ }
94
+ console.log('Entities prepared!')
95
+ }
96
+
97
+
98
+ private getSourceFilePaths(): string[] {
99
+ const projectRoot = process.cwd(); // Ajuste conforme a estrutura do seu projeto
100
+
101
+ const getAllFiles = (dir: string): string[] => {
102
+ const patterns = [`${dir}/**/*.(ts|js)`, "!**/node_modules/**"];
103
+
104
+ try {
105
+ return globbySync(patterns, {gitignore: true});
106
+ } catch (error) {
107
+ console.error('Erro ao obter arquivos:', error);
108
+ return [];
109
+ }
110
+ }
111
+
112
+ // Filtra os arquivos pelo padrão de nomenclatura
113
+ return getAllFiles(projectRoot);
114
+ }
115
+ }
package/src/orm.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { ConnectionSettings, DriverInterface, InstanceOf } from './driver/driver.interface';
2
+ import { LoggerService } from '@cheetah.js/core';
3
+ import { SqlBuilder } from './SqlBuilder';
4
+
5
+ export class Orm<T extends DriverInterface = DriverInterface> {
6
+ driverInstance: T;
7
+ static instance: Orm<any>
8
+
9
+ static getInstance() {
10
+ return Orm.instance
11
+ }
12
+
13
+ constructor(public connection: ConnectionSettings<T>, public logger: LoggerService) {
14
+ // @ts-ignore
15
+ this.driverInstance = new this.connection.driver(connection)
16
+ Orm.instance = this
17
+ }
18
+
19
+ createQueryBuilder<Model>(model: new() => Model): SqlBuilder<Model> {
20
+ return new SqlBuilder<Model>(model)
21
+ }
22
+
23
+ connect(): Promise<void> {
24
+ return this.driverInstance.connect()
25
+ }
26
+
27
+ disconnect(): Promise<void> {
28
+ return this.driverInstance.disconnect()
29
+ }
30
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,18 @@
1
+ export function getDefaultLength(type: string): number {
2
+ if (type === 'String') {
3
+ return 255;
4
+ }
5
+
6
+ if (type === 'Number') {
7
+ return 11;
8
+ }
9
+
10
+ if (type === 'Boolean') {
11
+ return 1;
12
+ }
13
+
14
+ if (type === 'Date') {
15
+ return 6;
16
+ }
17
+ return 255;
18
+ }