@uql/core 3.1.1 → 3.1.2
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/CHANGELOG.md +134 -187
- package/package.json +31 -26
- package/dist/CHANGELOG.md +0 -186
- package/dist/package.json +0 -131
- package/src/@types/index.d.ts +0 -1
- package/src/@types/jest.d.ts +0 -6
- package/src/browser/http/bus.spec.ts +0 -22
- package/src/browser/http/bus.ts +0 -17
- package/src/browser/http/http.spec.ts +0 -70
- package/src/browser/http/http.ts +0 -55
- package/src/browser/http/index.ts +0 -2
- package/src/browser/index.ts +0 -4
- package/src/browser/options.spec.ts +0 -37
- package/src/browser/options.ts +0 -18
- package/src/browser/querier/genericClientRepository.spec.ts +0 -105
- package/src/browser/querier/genericClientRepository.ts +0 -49
- package/src/browser/querier/httpQuerier.ts +0 -82
- package/src/browser/querier/index.ts +0 -3
- package/src/browser/querier/querier.util.spec.ts +0 -35
- package/src/browser/querier/querier.util.ts +0 -18
- package/src/browser/type/clientQuerier.ts +0 -45
- package/src/browser/type/clientQuerierPool.ts +0 -5
- package/src/browser/type/clientRepository.ts +0 -22
- package/src/browser/type/index.ts +0 -4
- package/src/browser/type/request.ts +0 -25
- package/src/dialect/abstractDialect.ts +0 -28
- package/src/dialect/abstractSqlDialect-spec.ts +0 -1309
- package/src/dialect/abstractSqlDialect.ts +0 -805
- package/src/dialect/index.ts +0 -3
- package/src/dialect/namingStrategy.spec.ts +0 -52
- package/src/dialect/queryContext.ts +0 -69
- package/src/entity/decorator/definition.spec.ts +0 -736
- package/src/entity/decorator/definition.ts +0 -265
- package/src/entity/decorator/entity.ts +0 -8
- package/src/entity/decorator/field.ts +0 -9
- package/src/entity/decorator/id.ts +0 -9
- package/src/entity/decorator/index.ts +0 -5
- package/src/entity/decorator/relation.spec.ts +0 -41
- package/src/entity/decorator/relation.ts +0 -34
- package/src/entity/index.ts +0 -1
- package/src/express/@types/express.d.ts +0 -8
- package/src/express/@types/index.d.ts +0 -1
- package/src/express/index.ts +0 -2
- package/src/express/querierMiddleware.ts +0 -217
- package/src/express/query.util.spec.ts +0 -40
- package/src/express/query.util.ts +0 -21
- package/src/index.ts +0 -9
- package/src/maria/index.ts +0 -3
- package/src/maria/mariaDialect.spec.ts +0 -207
- package/src/maria/mariaDialect.ts +0 -42
- package/src/maria/mariaQuerierPool.test.ts +0 -23
- package/src/maria/mariadbQuerier.test.ts +0 -23
- package/src/maria/mariadbQuerier.ts +0 -45
- package/src/maria/mariadbQuerierPool.ts +0 -21
- package/src/migrate/cli.ts +0 -301
- package/src/migrate/generator/index.ts +0 -4
- package/src/migrate/generator/mongoSchemaGenerator.spec.ts +0 -112
- package/src/migrate/generator/mongoSchemaGenerator.ts +0 -115
- package/src/migrate/generator/mysqlSchemaGenerator.spec.ts +0 -34
- package/src/migrate/generator/mysqlSchemaGenerator.ts +0 -92
- package/src/migrate/generator/postgresSchemaGenerator.spec.ts +0 -44
- package/src/migrate/generator/postgresSchemaGenerator.ts +0 -127
- package/src/migrate/generator/sqliteSchemaGenerator.spec.ts +0 -33
- package/src/migrate/generator/sqliteSchemaGenerator.ts +0 -81
- package/src/migrate/index.ts +0 -41
- package/src/migrate/introspection/index.ts +0 -4
- package/src/migrate/introspection/mongoIntrospector.spec.ts +0 -75
- package/src/migrate/introspection/mongoIntrospector.ts +0 -47
- package/src/migrate/introspection/mysqlIntrospector.spec.ts +0 -113
- package/src/migrate/introspection/mysqlIntrospector.ts +0 -278
- package/src/migrate/introspection/postgresIntrospector.spec.ts +0 -112
- package/src/migrate/introspection/postgresIntrospector.ts +0 -329
- package/src/migrate/introspection/sqliteIntrospector.spec.ts +0 -112
- package/src/migrate/introspection/sqliteIntrospector.ts +0 -296
- package/src/migrate/migrator-mongo.test.ts +0 -54
- package/src/migrate/migrator.spec.ts +0 -255
- package/src/migrate/migrator.test.ts +0 -94
- package/src/migrate/migrator.ts +0 -719
- package/src/migrate/namingStrategy.spec.ts +0 -22
- package/src/migrate/schemaGenerator-advanced.spec.ts +0 -138
- package/src/migrate/schemaGenerator.spec.ts +0 -190
- package/src/migrate/schemaGenerator.ts +0 -478
- package/src/migrate/storage/databaseStorage.spec.ts +0 -69
- package/src/migrate/storage/databaseStorage.ts +0 -100
- package/src/migrate/storage/index.ts +0 -2
- package/src/migrate/storage/jsonStorage.ts +0 -58
- package/src/migrate/type.ts +0 -1
- package/src/mongo/index.ts +0 -3
- package/src/mongo/mongoDialect.spec.ts +0 -251
- package/src/mongo/mongoDialect.ts +0 -238
- package/src/mongo/mongodbQuerier.test.ts +0 -45
- package/src/mongo/mongodbQuerier.ts +0 -256
- package/src/mongo/mongodbQuerierPool.test.ts +0 -25
- package/src/mongo/mongodbQuerierPool.ts +0 -24
- package/src/mysql/index.ts +0 -3
- package/src/mysql/mysql2Querier.test.ts +0 -20
- package/src/mysql/mysql2Querier.ts +0 -49
- package/src/mysql/mysql2QuerierPool.test.ts +0 -20
- package/src/mysql/mysql2QuerierPool.ts +0 -21
- package/src/mysql/mysqlDialect.spec.ts +0 -20
- package/src/mysql/mysqlDialect.ts +0 -16
- package/src/namingStrategy/defaultNamingStrategy.ts +0 -18
- package/src/namingStrategy/index.spec.ts +0 -36
- package/src/namingStrategy/index.ts +0 -2
- package/src/namingStrategy/snakeCaseNamingStrategy.ts +0 -15
- package/src/options.spec.ts +0 -41
- package/src/options.ts +0 -18
- package/src/postgres/index.ts +0 -3
- package/src/postgres/manual-types.d.ts +0 -4
- package/src/postgres/pgQuerier.test.ts +0 -25
- package/src/postgres/pgQuerier.ts +0 -45
- package/src/postgres/pgQuerierPool.test.ts +0 -28
- package/src/postgres/pgQuerierPool.ts +0 -21
- package/src/postgres/postgresDialect.spec.ts +0 -428
- package/src/postgres/postgresDialect.ts +0 -144
- package/src/querier/abstractQuerier-test.ts +0 -584
- package/src/querier/abstractQuerier.ts +0 -353
- package/src/querier/abstractQuerierPool-test.ts +0 -20
- package/src/querier/abstractQuerierPool.ts +0 -18
- package/src/querier/abstractSqlQuerier-spec.ts +0 -979
- package/src/querier/abstractSqlQuerier-test.ts +0 -21
- package/src/querier/abstractSqlQuerier.ts +0 -138
- package/src/querier/decorator/index.ts +0 -3
- package/src/querier/decorator/injectQuerier.spec.ts +0 -74
- package/src/querier/decorator/injectQuerier.ts +0 -45
- package/src/querier/decorator/serialized.spec.ts +0 -98
- package/src/querier/decorator/serialized.ts +0 -13
- package/src/querier/decorator/transactional.spec.ts +0 -240
- package/src/querier/decorator/transactional.ts +0 -56
- package/src/querier/index.ts +0 -4
- package/src/repository/genericRepository.spec.ts +0 -111
- package/src/repository/genericRepository.ts +0 -74
- package/src/repository/index.ts +0 -1
- package/src/sqlite/index.ts +0 -3
- package/src/sqlite/manual-types.d.ts +0 -4
- package/src/sqlite/sqliteDialect.spec.ts +0 -155
- package/src/sqlite/sqliteDialect.ts +0 -76
- package/src/sqlite/sqliteQuerier.spec.ts +0 -36
- package/src/sqlite/sqliteQuerier.test.ts +0 -21
- package/src/sqlite/sqliteQuerier.ts +0 -37
- package/src/sqlite/sqliteQuerierPool.test.ts +0 -12
- package/src/sqlite/sqliteQuerierPool.ts +0 -38
- package/src/test/entityMock.ts +0 -375
- package/src/test/index.ts +0 -3
- package/src/test/it.util.ts +0 -69
- package/src/test/spec.util.ts +0 -57
- package/src/type/entity.ts +0 -218
- package/src/type/index.ts +0 -9
- package/src/type/migration.ts +0 -241
- package/src/type/namingStrategy.ts +0 -17
- package/src/type/querier.ts +0 -143
- package/src/type/querierPool.ts +0 -26
- package/src/type/query.ts +0 -506
- package/src/type/repository.ts +0 -142
- package/src/type/universalQuerier.ts +0 -133
- package/src/type/utility.ts +0 -21
- package/src/util/dialect.util-extra.spec.ts +0 -96
- package/src/util/dialect.util.spec.ts +0 -23
- package/src/util/dialect.util.ts +0 -134
- package/src/util/index.ts +0 -5
- package/src/util/object.util.spec.ts +0 -29
- package/src/util/object.util.ts +0 -27
- package/src/util/raw.ts +0 -11
- package/src/util/sql.util-extra.spec.ts +0 -17
- package/src/util/sql.util.spec.ts +0 -208
- package/src/util/sql.util.ts +0 -104
- package/src/util/string.util.spec.ts +0 -46
- package/src/util/string.util.ts +0 -35
- package/tsconfig.build.json +0 -5
- package/tsconfig.json +0 -8
- /package/{dist/README.md → README.md} +0 -0
|
@@ -1,478 +0,0 @@
|
|
|
1
|
-
import { AbstractDialect } from '../dialect/index.js';
|
|
2
|
-
import { getMeta } from '../entity/index.js';
|
|
3
|
-
import type {
|
|
4
|
-
ColumnSchema,
|
|
5
|
-
ColumnType,
|
|
6
|
-
EntityMeta,
|
|
7
|
-
FieldKey,
|
|
8
|
-
FieldOptions,
|
|
9
|
-
IndexSchema,
|
|
10
|
-
NamingStrategy,
|
|
11
|
-
SchemaDiff,
|
|
12
|
-
SchemaGenerator,
|
|
13
|
-
TableSchema,
|
|
14
|
-
Type,
|
|
15
|
-
} from '../type/index.js';
|
|
16
|
-
import { escapeSqlId, getKeys } from '../util/index.js';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Abstract base class for SQL schema generation
|
|
20
|
-
*/
|
|
21
|
-
export abstract class AbstractSchemaGenerator extends AbstractDialect implements SchemaGenerator {
|
|
22
|
-
/**
|
|
23
|
-
* Primary key type for auto-increment integer IDs
|
|
24
|
-
*/
|
|
25
|
-
protected abstract readonly serialPrimaryKeyType: string;
|
|
26
|
-
|
|
27
|
-
constructor(
|
|
28
|
-
namingStrategy?: NamingStrategy,
|
|
29
|
-
protected readonly escapeIdChar: '`' | '"' = '`',
|
|
30
|
-
) {
|
|
31
|
-
super(namingStrategy);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Escape an identifier (table name, column name, etc.)
|
|
36
|
-
*/
|
|
37
|
-
protected escapeId(identifier: string): string {
|
|
38
|
-
return escapeSqlId(identifier, this.escapeIdChar);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
generateCreateTable<E>(entity: Type<E>, options: { ifNotExists?: boolean } = {}): string {
|
|
42
|
-
const meta = getMeta(entity);
|
|
43
|
-
const tableName = this.resolveTableName(entity, meta);
|
|
44
|
-
const columns = this.generateColumnDefinitions(meta);
|
|
45
|
-
const constraints = this.generateTableConstraints(meta);
|
|
46
|
-
|
|
47
|
-
const ifNotExists = options.ifNotExists ? 'IF NOT EXISTS ' : '';
|
|
48
|
-
let sql = `CREATE TABLE ${ifNotExists}${this.escapeId(tableName)} (\n`;
|
|
49
|
-
sql += columns.map((col) => ` ${col}`).join(',\n');
|
|
50
|
-
|
|
51
|
-
if (constraints.length > 0) {
|
|
52
|
-
sql += ',\n';
|
|
53
|
-
sql += constraints.map((c: any) => ` ${c}`).join(',\n');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
sql += '\n)';
|
|
57
|
-
sql += this.getTableOptions(meta);
|
|
58
|
-
sql += ';';
|
|
59
|
-
|
|
60
|
-
return sql;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
generateDropTable<E>(entity: Type<E>): string {
|
|
64
|
-
const meta = getMeta(entity);
|
|
65
|
-
const tableName = this.resolveTableName(entity, meta);
|
|
66
|
-
return `DROP TABLE IF EXISTS ${this.escapeId(tableName)};`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
generateAlterTable(diff: SchemaDiff): string[] {
|
|
70
|
-
const statements: string[] = [];
|
|
71
|
-
const tableName = this.escapeId(diff.tableName);
|
|
72
|
-
|
|
73
|
-
// Add new columns
|
|
74
|
-
if (diff.columnsToAdd?.length) {
|
|
75
|
-
for (const column of diff.columnsToAdd) {
|
|
76
|
-
const colDef = this.generateColumnDefinitionFromSchema(column);
|
|
77
|
-
statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${colDef};`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Alter existing columns
|
|
82
|
-
if (diff.columnsToAlter?.length) {
|
|
83
|
-
for (const { to } of diff.columnsToAlter) {
|
|
84
|
-
const colDef = this.generateColumnDefinitionFromSchema(to);
|
|
85
|
-
const colStatements = this.generateAlterColumnStatements(diff.tableName, to, colDef);
|
|
86
|
-
statements.push(...colStatements);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Drop columns
|
|
91
|
-
if (diff.columnsToDrop?.length) {
|
|
92
|
-
for (const columnName of diff.columnsToDrop) {
|
|
93
|
-
statements.push(`ALTER TABLE ${tableName} DROP COLUMN ${this.escapeId(columnName)};`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Add indexes
|
|
98
|
-
if (diff.indexesToAdd?.length) {
|
|
99
|
-
for (const index of diff.indexesToAdd) {
|
|
100
|
-
statements.push(this.generateCreateIndex(diff.tableName, index));
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Drop indexes
|
|
105
|
-
if (diff.indexesToDrop?.length) {
|
|
106
|
-
for (const indexName of diff.indexesToDrop) {
|
|
107
|
-
statements.push(this.generateDropIndex(diff.tableName, indexName));
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return statements;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
generateAlterTableDown(diff: SchemaDiff): string[] {
|
|
115
|
-
const statements: string[] = [];
|
|
116
|
-
const tableName = this.escapeId(diff.tableName);
|
|
117
|
-
|
|
118
|
-
// Rollback additions by dropping columns
|
|
119
|
-
if (diff.columnsToAdd?.length) {
|
|
120
|
-
for (const column of diff.columnsToAdd) {
|
|
121
|
-
statements.push(`ALTER TABLE ${tableName} DROP COLUMN ${this.escapeId(column.name)};`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return statements;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
generateCreateIndex(tableName: string, index: IndexSchema): string {
|
|
129
|
-
const unique = index.unique ? 'UNIQUE ' : '';
|
|
130
|
-
const columns = index.columns.map((c: any) => this.escapeId(c)).join(', ');
|
|
131
|
-
return `CREATE ${unique}INDEX ${this.escapeId(index.name)} ON ${this.escapeId(tableName)} (${columns});`;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
generateDropIndex(tableName: string, indexName: string): string {
|
|
135
|
-
return `DROP INDEX IF EXISTS ${this.escapeId(indexName)};`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Generate column definitions from entity metadata
|
|
140
|
-
*/
|
|
141
|
-
public generateColumnDefinitions<E>(meta: EntityMeta<E>): string[] {
|
|
142
|
-
const columns: string[] = [];
|
|
143
|
-
const fieldKeys = getKeys(meta.fields) as FieldKey<E>[];
|
|
144
|
-
|
|
145
|
-
for (const key of fieldKeys) {
|
|
146
|
-
const field = meta.fields[key];
|
|
147
|
-
if (field?.virtual) continue; // Skip virtual fields
|
|
148
|
-
|
|
149
|
-
const colDef = this.generateColumnDefinition(key as string, field, meta);
|
|
150
|
-
columns.push(colDef);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return columns;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Generate a single column definition
|
|
158
|
-
*/
|
|
159
|
-
public generateColumnDefinition<E>(fieldKey: string, field: FieldOptions, meta: EntityMeta<E>): string {
|
|
160
|
-
const columnName = this.escapeId(this.resolveColumnName(fieldKey, field));
|
|
161
|
-
const isId = field.isId === true;
|
|
162
|
-
const isPrimaryKey = isId && meta.id === fieldKey;
|
|
163
|
-
|
|
164
|
-
// Determine SQL type
|
|
165
|
-
let sqlType: string;
|
|
166
|
-
if (isPrimaryKey && field.autoIncrement !== false && !field.onInsert) {
|
|
167
|
-
// Auto-increment primary key
|
|
168
|
-
sqlType = this.serialPrimaryKeyType;
|
|
169
|
-
} else {
|
|
170
|
-
sqlType = this.getSqlType(field, field.type);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
let definition = `${columnName} ${sqlType}`;
|
|
174
|
-
|
|
175
|
-
// PRIMARY KEY constraint (for non-serial types)
|
|
176
|
-
if (isPrimaryKey && !sqlType.includes('PRIMARY KEY')) {
|
|
177
|
-
definition += ' PRIMARY KEY';
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// NULL/NOT NULL
|
|
181
|
-
if (!isPrimaryKey) {
|
|
182
|
-
const nullable = field.nullable ?? true;
|
|
183
|
-
if (!nullable) {
|
|
184
|
-
definition += ' NOT NULL';
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// UNIQUE constraint
|
|
189
|
-
if (field.unique && !isPrimaryKey) {
|
|
190
|
-
definition += ' UNIQUE';
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// DEFAULT value
|
|
194
|
-
if (field.defaultValue !== undefined) {
|
|
195
|
-
definition += ` DEFAULT ${this.formatDefaultValue(field.defaultValue)}`;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// COMMENT (if supported)
|
|
199
|
-
if (field.comment) {
|
|
200
|
-
definition += this.generateColumnComment(
|
|
201
|
-
this.resolveTableName(meta.entity, meta),
|
|
202
|
-
this.resolveColumnName(fieldKey, field),
|
|
203
|
-
field.comment,
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return definition;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Generate column definition from a ColumnSchema object
|
|
212
|
-
*/
|
|
213
|
-
public generateColumnDefinitionFromSchema(column: ColumnSchema): string {
|
|
214
|
-
const columnName = this.escapeId(column.name);
|
|
215
|
-
let type = column.type;
|
|
216
|
-
|
|
217
|
-
if (column.length && !type.includes('(')) {
|
|
218
|
-
type = `${type}(${column.length})`;
|
|
219
|
-
} else if (column.precision !== undefined && !type.includes('(')) {
|
|
220
|
-
if (column.scale !== undefined) {
|
|
221
|
-
type = `${type}(${column.precision}, ${column.scale})`;
|
|
222
|
-
} else {
|
|
223
|
-
type = `${type}(${column.precision})`;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
let definition = `${columnName} ${type}`;
|
|
228
|
-
|
|
229
|
-
if (column.isPrimaryKey) {
|
|
230
|
-
definition += ' PRIMARY KEY';
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (!column.nullable && !column.isPrimaryKey) {
|
|
234
|
-
definition += ' NOT NULL';
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (column.isUnique && !column.isPrimaryKey) {
|
|
238
|
-
definition += ' UNIQUE';
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (column.defaultValue !== undefined) {
|
|
242
|
-
definition += ` DEFAULT ${this.formatDefaultValue(column.defaultValue)}`;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return definition;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Generate table constraints (indexes, foreign keys, etc.)
|
|
250
|
-
*/
|
|
251
|
-
public generateTableConstraints<E>(meta: EntityMeta<E>): string[] {
|
|
252
|
-
const constraints: string[] = [];
|
|
253
|
-
const fieldKeys = getKeys(meta.fields) as FieldKey<E>[];
|
|
254
|
-
const tableName = this.resolveTableName(meta.entity, meta);
|
|
255
|
-
|
|
256
|
-
// Generate indexes from field options
|
|
257
|
-
for (const key of fieldKeys) {
|
|
258
|
-
const field = meta.fields[key];
|
|
259
|
-
if (field?.index) {
|
|
260
|
-
const columnName = this.resolveColumnName(key as string, field);
|
|
261
|
-
const indexName = typeof field.index === 'string' ? field.index : `idx_${tableName}_${columnName}`;
|
|
262
|
-
constraints.push(`INDEX ${this.escapeId(indexName)} (${this.escapeId(columnName)})`);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Generate foreign key constraints from references
|
|
267
|
-
for (const key of fieldKeys) {
|
|
268
|
-
const field = meta.fields[key];
|
|
269
|
-
if (field?.reference) {
|
|
270
|
-
const refEntity = field.reference();
|
|
271
|
-
const refMeta = getMeta(refEntity);
|
|
272
|
-
const refIdField = refMeta.fields[refMeta.id];
|
|
273
|
-
const columnName = this.resolveColumnName(key as string, field);
|
|
274
|
-
const refTableName = this.resolveTableName(refEntity, refMeta);
|
|
275
|
-
const refColumnName = this.resolveColumnName(refMeta.id, refIdField);
|
|
276
|
-
const fkName = `fk_${tableName}_${columnName}`;
|
|
277
|
-
|
|
278
|
-
constraints.push(
|
|
279
|
-
`CONSTRAINT ${this.escapeId(fkName)} FOREIGN KEY (${this.escapeId(columnName)}) ` +
|
|
280
|
-
`REFERENCES ${this.escapeId(refTableName)} (${this.escapeId(refColumnName)})`,
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return constraints;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
getSqlType(field: FieldOptions, fieldType?: unknown): string {
|
|
289
|
-
// Use explicit column type if specified
|
|
290
|
-
if (field.columnType) {
|
|
291
|
-
return this.mapColumnType(field.columnType, field);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Handle special types
|
|
295
|
-
if (field.type === 'json' || field.type === 'jsonb') {
|
|
296
|
-
return this.mapColumnType(field.type as ColumnType, field);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (field.type === 'vector') {
|
|
300
|
-
return this.mapColumnType('vector', field);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Infer from TypeScript type
|
|
304
|
-
const type = fieldType ?? field.type;
|
|
305
|
-
|
|
306
|
-
if (type === Number || type === 'number') {
|
|
307
|
-
return field.precision ? this.mapColumnType('decimal', field) : 'BIGINT';
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (type === String || type === 'string') {
|
|
311
|
-
const length = field.length ?? 255;
|
|
312
|
-
return `VARCHAR(${length})`;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (type === Boolean || type === 'boolean') {
|
|
316
|
-
return this.getBooleanType();
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (type === Date || type === 'date') {
|
|
320
|
-
return 'TIMESTAMP';
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (type === BigInt || type === 'bigint') {
|
|
324
|
-
return 'BIGINT';
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Default to VARCHAR
|
|
328
|
-
return `VARCHAR(${field.length ?? 255})`;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Map uql column type to database-specific SQL type
|
|
333
|
-
*/
|
|
334
|
-
public abstract mapColumnType(columnType: ColumnType, field: FieldOptions): string;
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Get the boolean type for this database
|
|
338
|
-
*/
|
|
339
|
-
public abstract getBooleanType(): string;
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Generate ALTER COLUMN statements (database-specific)
|
|
343
|
-
*/
|
|
344
|
-
public abstract generateAlterColumnStatements(
|
|
345
|
-
tableName: string,
|
|
346
|
-
column: ColumnSchema,
|
|
347
|
-
newDefinition: string,
|
|
348
|
-
): string[];
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Get table options (e.g., ENGINE for MySQL)
|
|
352
|
-
*/
|
|
353
|
-
getTableOptions<E>(meta: EntityMeta<E>): string {
|
|
354
|
-
return '';
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Generate column comment clause (if supported)
|
|
359
|
-
*/
|
|
360
|
-
public abstract generateColumnComment(tableName: string, columnName: string, comment: string): string;
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Format a default value for SQL
|
|
364
|
-
*/
|
|
365
|
-
public formatDefaultValue(value: unknown): string {
|
|
366
|
-
if (value === null) {
|
|
367
|
-
return 'NULL';
|
|
368
|
-
}
|
|
369
|
-
if (typeof value === 'string') {
|
|
370
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
371
|
-
}
|
|
372
|
-
if (typeof value === 'boolean') {
|
|
373
|
-
return value ? 'TRUE' : 'FALSE';
|
|
374
|
-
}
|
|
375
|
-
if (typeof value === 'number' || typeof value === 'bigint') {
|
|
376
|
-
return String(value);
|
|
377
|
-
}
|
|
378
|
-
if (value instanceof Date) {
|
|
379
|
-
return `'${value.toISOString()}'`;
|
|
380
|
-
}
|
|
381
|
-
return String(value);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Compare two schemas and return the differences
|
|
386
|
-
*/
|
|
387
|
-
diffSchema<E>(entity: Type<E>, currentSchema: TableSchema | undefined): SchemaDiff | undefined {
|
|
388
|
-
const meta = getMeta(entity);
|
|
389
|
-
|
|
390
|
-
if (!currentSchema) {
|
|
391
|
-
// Table doesn't exist, need to create
|
|
392
|
-
return {
|
|
393
|
-
tableName: this.resolveTableName(entity, meta),
|
|
394
|
-
type: 'create',
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const columnsToAdd: ColumnSchema[] = [];
|
|
399
|
-
const columnsToAlter: { from: ColumnSchema; to: ColumnSchema }[] = [];
|
|
400
|
-
const columnsToDrop: string[] = [];
|
|
401
|
-
|
|
402
|
-
const currentColumns = new Map(currentSchema.columns.map((c: any) => [c.name, c]));
|
|
403
|
-
const fieldKeys = getKeys(meta.fields) as FieldKey<E>[];
|
|
404
|
-
|
|
405
|
-
// Check for new or altered columns
|
|
406
|
-
for (const key of fieldKeys) {
|
|
407
|
-
const field = meta.fields[key];
|
|
408
|
-
if (field?.virtual) continue;
|
|
409
|
-
|
|
410
|
-
const columnName = this.resolveColumnName(key as string, field);
|
|
411
|
-
const currentColumn = currentColumns.get(columnName);
|
|
412
|
-
|
|
413
|
-
if (!currentColumn) {
|
|
414
|
-
// Column needs to be added
|
|
415
|
-
columnsToAdd.push(this.fieldToColumnSchema(key as string, field, meta));
|
|
416
|
-
} else {
|
|
417
|
-
// Check if column needs alteration
|
|
418
|
-
const desiredColumn = this.fieldToColumnSchema(key as string, field, meta);
|
|
419
|
-
if (this.columnsNeedAlteration(currentColumn, desiredColumn)) {
|
|
420
|
-
columnsToAlter.push({ from: currentColumn, to: desiredColumn });
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
currentColumns.delete(columnName);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// Remaining columns in currentColumns should be dropped
|
|
427
|
-
for (const [name] of currentColumns) {
|
|
428
|
-
columnsToDrop.push(name);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (columnsToAdd.length === 0 && columnsToAlter.length === 0 && columnsToDrop.length === 0) {
|
|
432
|
-
return undefined; // No changes needed
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return {
|
|
436
|
-
tableName: this.resolveTableName(entity, meta),
|
|
437
|
-
type: 'alter',
|
|
438
|
-
columnsToAdd: columnsToAdd.length > 0 ? columnsToAdd : undefined,
|
|
439
|
-
columnsToAlter: columnsToAlter.length > 0 ? columnsToAlter : undefined,
|
|
440
|
-
columnsToDrop: columnsToDrop.length > 0 ? columnsToDrop : undefined,
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Convert field options to ColumnSchema
|
|
446
|
-
*/
|
|
447
|
-
protected fieldToColumnSchema<E>(fieldKey: string, field: FieldOptions, meta: EntityMeta<E>): ColumnSchema {
|
|
448
|
-
const isId = field.isId === true;
|
|
449
|
-
const isPrimaryKey = isId && meta.id === fieldKey;
|
|
450
|
-
|
|
451
|
-
return {
|
|
452
|
-
name: this.resolveColumnName(fieldKey, field),
|
|
453
|
-
type: this.getSqlType(field, field.type),
|
|
454
|
-
nullable: field.nullable ?? !isPrimaryKey,
|
|
455
|
-
defaultValue: field.defaultValue,
|
|
456
|
-
isPrimaryKey,
|
|
457
|
-
isAutoIncrement: isPrimaryKey && field.autoIncrement !== false && !field.onInsert,
|
|
458
|
-
isUnique: field.unique ?? false,
|
|
459
|
-
length: field.length,
|
|
460
|
-
precision: field.precision,
|
|
461
|
-
scale: field.scale,
|
|
462
|
-
comment: field.comment,
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* Check if two columns differ enough to require alteration
|
|
468
|
-
*/
|
|
469
|
-
protected columnsNeedAlteration(current: ColumnSchema, desired: ColumnSchema): boolean {
|
|
470
|
-
// Compare relevant properties
|
|
471
|
-
return (
|
|
472
|
-
current.type.toLowerCase() !== desired.type.toLowerCase() ||
|
|
473
|
-
current.nullable !== desired.nullable ||
|
|
474
|
-
current.isUnique !== desired.isUnique ||
|
|
475
|
-
JSON.stringify(current.defaultValue) !== JSON.stringify(desired.defaultValue)
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, jest } from 'bun:test';
|
|
2
|
-
import type { QuerierPool, SqlQuerier } from '../../type/index.js';
|
|
3
|
-
import { DatabaseMigrationStorage } from './databaseStorage.js';
|
|
4
|
-
|
|
5
|
-
describe('DatabaseMigrationStorage', () => {
|
|
6
|
-
let storage: DatabaseMigrationStorage;
|
|
7
|
-
let pool: QuerierPool;
|
|
8
|
-
let querier: SqlQuerier;
|
|
9
|
-
let mockAll: ReturnType<typeof jest.fn>;
|
|
10
|
-
let mockRun: ReturnType<typeof jest.fn>;
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
mockAll = jest.fn<any>().mockResolvedValue([]);
|
|
14
|
-
mockRun = jest.fn<any>().mockResolvedValue({});
|
|
15
|
-
querier = {
|
|
16
|
-
all: mockAll,
|
|
17
|
-
run: mockRun,
|
|
18
|
-
release: jest.fn<any>().mockResolvedValue(undefined),
|
|
19
|
-
dialect: {
|
|
20
|
-
escapeId: (id: string) => `"${id}"`,
|
|
21
|
-
placeholder: (index: number) => `$${index}`,
|
|
22
|
-
escapeIdChar: '"',
|
|
23
|
-
},
|
|
24
|
-
} as any;
|
|
25
|
-
|
|
26
|
-
pool = {
|
|
27
|
-
getQuerier: jest.fn<any>().mockResolvedValue(querier) as any,
|
|
28
|
-
} as any;
|
|
29
|
-
|
|
30
|
-
storage = new DatabaseMigrationStorage(pool);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('ensureStorage should create table', async () => {
|
|
34
|
-
await storage.ensureStorage();
|
|
35
|
-
|
|
36
|
-
expect(querier.run).toHaveBeenCalledWith(expect.stringContaining('CREATE TABLE IF NOT EXISTS "uql_migrations"'));
|
|
37
|
-
expect(querier.release).toHaveBeenCalled();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('executed should return migration names', async () => {
|
|
41
|
-
// 1. ensureStorage
|
|
42
|
-
mockRun.mockResolvedValueOnce({} as any);
|
|
43
|
-
// 2. executed query
|
|
44
|
-
mockAll.mockResolvedValueOnce([{ name: 'm1' }, { name: 'm2' }]);
|
|
45
|
-
|
|
46
|
-
const executed = await storage.executed();
|
|
47
|
-
|
|
48
|
-
expect(executed).toEqual(['m1', 'm2']);
|
|
49
|
-
expect(querier.all).toHaveBeenCalledWith(expect.stringContaining('SELECT "name" FROM "uql_migrations"'));
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('logWithQuerier should insert record', async () => {
|
|
53
|
-
await storage.logWithQuerier(querier, 'm3');
|
|
54
|
-
|
|
55
|
-
expect(querier.run).toHaveBeenCalledWith(
|
|
56
|
-
expect.stringContaining('INSERT INTO "uql_migrations" ("name") VALUES ($1)'),
|
|
57
|
-
['m3'],
|
|
58
|
-
);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('unlogWithQuerier should delete record', async () => {
|
|
62
|
-
await storage.unlogWithQuerier(querier, 'm3');
|
|
63
|
-
|
|
64
|
-
expect(querier.run).toHaveBeenCalledWith(
|
|
65
|
-
expect.stringContaining('DELETE FROM "uql_migrations" WHERE "name" = $1'),
|
|
66
|
-
['m3'],
|
|
67
|
-
);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import type { MigrationStorage, QuerierPool, SqlQuerier } from '../../type/index.js';
|
|
2
|
-
import { isSqlQuerier } from '../../type/index.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Migration metadata stored in the database
|
|
6
|
-
*/
|
|
7
|
-
interface MigrationRecord {
|
|
8
|
-
name: string;
|
|
9
|
-
executed_at: Date | number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Stores migration state in a database table.
|
|
14
|
-
* Uses the querier's dialect for escaping and placeholders.
|
|
15
|
-
*/
|
|
16
|
-
export class DatabaseMigrationStorage implements MigrationStorage {
|
|
17
|
-
private readonly tableName: string;
|
|
18
|
-
private storageInitialized = false;
|
|
19
|
-
|
|
20
|
-
constructor(
|
|
21
|
-
private readonly querierPool: QuerierPool,
|
|
22
|
-
options: {
|
|
23
|
-
tableName?: string;
|
|
24
|
-
} = {},
|
|
25
|
-
) {
|
|
26
|
-
this.tableName = options.tableName ?? 'uql_migrations';
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async ensureStorage(): Promise<void> {
|
|
30
|
-
if (this.storageInitialized) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const querier = await this.querierPool.getQuerier();
|
|
35
|
-
|
|
36
|
-
if (!isSqlQuerier(querier)) {
|
|
37
|
-
await querier.release();
|
|
38
|
-
throw new Error('DatabaseMigrationStorage requires a SQL-based querier');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
await this.createTableIfNotExists(querier);
|
|
43
|
-
this.storageInitialized = true;
|
|
44
|
-
} finally {
|
|
45
|
-
await querier.release();
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private async createTableIfNotExists(querier: SqlQuerier): Promise<void> {
|
|
50
|
-
const { escapeId } = querier.dialect;
|
|
51
|
-
const sql = `
|
|
52
|
-
CREATE TABLE IF NOT EXISTS ${escapeId(this.tableName)} (
|
|
53
|
-
${escapeId('name')} VARCHAR(255) PRIMARY KEY,
|
|
54
|
-
${escapeId('executed_at')} TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
55
|
-
)
|
|
56
|
-
`;
|
|
57
|
-
|
|
58
|
-
await querier.run(sql);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async executed(): Promise<string[]> {
|
|
62
|
-
await this.ensureStorage();
|
|
63
|
-
|
|
64
|
-
const querier = await this.querierPool.getQuerier();
|
|
65
|
-
|
|
66
|
-
if (!isSqlQuerier(querier)) {
|
|
67
|
-
await querier.release();
|
|
68
|
-
throw new Error('DatabaseMigrationStorage requires a SQL-based querier');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
const { escapeId } = querier.dialect;
|
|
73
|
-
const sql = `SELECT ${escapeId('name')} FROM ${escapeId(this.tableName)} ORDER BY ${escapeId('name')} ASC`;
|
|
74
|
-
const results = await querier.all<MigrationRecord>(sql);
|
|
75
|
-
return results.map((r: any) => r.name);
|
|
76
|
-
} finally {
|
|
77
|
-
await querier.release();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Log a migration as executed - uses provided querier (within transaction)
|
|
83
|
-
*/
|
|
84
|
-
async logWithQuerier(querier: SqlQuerier, migrationName: string): Promise<void> {
|
|
85
|
-
await this.ensureStorage();
|
|
86
|
-
const { escapeId, placeholder } = querier.dialect;
|
|
87
|
-
const sql = `INSERT INTO ${escapeId(this.tableName)} (${escapeId('name')}) VALUES (${placeholder(1)})`;
|
|
88
|
-
await querier.run(sql, [migrationName]);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Unlog a migration - uses provided querier (within transaction)
|
|
93
|
-
*/
|
|
94
|
-
async unlogWithQuerier(querier: SqlQuerier, migrationName: string): Promise<void> {
|
|
95
|
-
await this.ensureStorage();
|
|
96
|
-
const { escapeId, placeholder } = querier.dialect;
|
|
97
|
-
const sql = `DELETE FROM ${escapeId(this.tableName)} WHERE ${escapeId('name')} = ${placeholder(1)}`;
|
|
98
|
-
await querier.run(sql, [migrationName]);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { dirname } from 'node:path';
|
|
3
|
-
import type { MigrationStorage, SqlQuerier } from '../../type/index.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Stores migration state in a JSON file (useful for development/testing)
|
|
7
|
-
*/
|
|
8
|
-
export class JsonMigrationStorage implements MigrationStorage {
|
|
9
|
-
private readonly filePath: string;
|
|
10
|
-
private cache: string[] | null = null;
|
|
11
|
-
|
|
12
|
-
constructor(filePath = './migrations/.uql-migrations.json') {
|
|
13
|
-
this.filePath = filePath;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async ensureStorage(): Promise<void> {
|
|
17
|
-
try {
|
|
18
|
-
await this.load();
|
|
19
|
-
} catch {
|
|
20
|
-
// File doesn't exist, create it
|
|
21
|
-
await this.save([]);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async executed(): Promise<string[]> {
|
|
26
|
-
await this.ensureStorage();
|
|
27
|
-
return this.cache ?? [];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async logWithQuerier(_querier: SqlQuerier, migrationName: string): Promise<void> {
|
|
31
|
-
const executed = await this.executed();
|
|
32
|
-
if (!executed.includes(migrationName)) {
|
|
33
|
-
executed.push(migrationName);
|
|
34
|
-
executed.sort();
|
|
35
|
-
await this.save(executed);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async unlogWithQuerier(_querier: SqlQuerier, migrationName: string): Promise<void> {
|
|
40
|
-
const executed = await this.executed();
|
|
41
|
-
const index = executed.indexOf(migrationName);
|
|
42
|
-
if (index !== -1) {
|
|
43
|
-
executed.splice(index, 1);
|
|
44
|
-
await this.save(executed);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
private async load(): Promise<void> {
|
|
49
|
-
const content = await readFile(this.filePath, 'utf-8');
|
|
50
|
-
this.cache = JSON.parse(content);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
private async save(migrations: string[]): Promise<void> {
|
|
54
|
-
await mkdir(dirname(this.filePath), { recursive: true });
|
|
55
|
-
await writeFile(this.filePath, JSON.stringify(migrations, null, 2), 'utf-8');
|
|
56
|
-
this.cache = migrations;
|
|
57
|
-
}
|
|
58
|
-
}
|