mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
feat: naming strategy (#19848)
* feat: naming strategy * feat: detect renames
This commit is contained in:
parent
1d19d308e2
commit
9e48ae3052
@ -9,7 +9,13 @@ import { ConfigRepository } from 'src/repositories/config.repository';
|
|||||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import 'src/schema';
|
import 'src/schema';
|
||||||
import { schemaDiff, schemaFromCode, schemaFromDatabase } from 'src/sql-tools';
|
import {
|
||||||
|
DefaultNamingStrategy,
|
||||||
|
HashNamingStrategy,
|
||||||
|
schemaDiff,
|
||||||
|
schemaFromCode,
|
||||||
|
schemaFromDatabase,
|
||||||
|
} from 'src/sql-tools';
|
||||||
import { asPostgresConnectionConfig, getKyselyConfig } from 'src/utils/database';
|
import { asPostgresConnectionConfig, getKyselyConfig } from 'src/utils/database';
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
@ -107,7 +113,22 @@ const compare = async () => {
|
|||||||
const { database } = configRepository.getEnv();
|
const { database } = configRepository.getEnv();
|
||||||
const db = postgres(asPostgresConnectionConfig(database.config));
|
const db = postgres(asPostgresConnectionConfig(database.config));
|
||||||
|
|
||||||
const source = schemaFromCode({ overrides: true });
|
const tables = new Set<string>();
|
||||||
|
const preferred = new DefaultNamingStrategy();
|
||||||
|
const fallback = new HashNamingStrategy();
|
||||||
|
|
||||||
|
const source = schemaFromCode({
|
||||||
|
overrides: true,
|
||||||
|
namingStrategy: {
|
||||||
|
getName(item) {
|
||||||
|
if ('tableName' in item && tables.has(item.tableName)) {
|
||||||
|
return preferred.getName(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback.getName(item);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
const target = await schemaFromDatabase(db, {});
|
const target = await schemaFromDatabase(db, {});
|
||||||
|
|
||||||
console.log(source.warnings.join('\n'));
|
console.log(source.warnings.join('\n'));
|
||||||
|
@ -35,7 +35,11 @@ export class StackTable {
|
|||||||
updateId!: Generated<string>;
|
updateId!: Generated<string>;
|
||||||
|
|
||||||
//TODO: Add constraint to ensure primary asset exists in the assets array
|
//TODO: Add constraint to ensure primary asset exists in the assets array
|
||||||
@ForeignKeyColumn(() => AssetTable, { nullable: false, unique: true })
|
@ForeignKeyColumn(() => AssetTable, {
|
||||||
|
nullable: false,
|
||||||
|
unique: true,
|
||||||
|
uniqueConstraintName: 'REL_91704e101438fd0653f582426d',
|
||||||
|
})
|
||||||
primaryAssetId!: string;
|
primaryAssetId!: string;
|
||||||
|
|
||||||
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||||
|
@ -5,6 +5,7 @@ import { describe, expect, it } from 'vitest';
|
|||||||
const testColumn: DatabaseColumn = {
|
const testColumn: DatabaseColumn = {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
|
primary: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
isArray: false,
|
isArray: false,
|
||||||
type: 'character varying',
|
type: 'character varying',
|
||||||
|
@ -1,7 +1,32 @@
|
|||||||
import { getColumnType, isDefaultEqual } from 'src/sql-tools/helpers';
|
import { asRenameKey, getColumnType, isDefaultEqual } from 'src/sql-tools/helpers';
|
||||||
import { Comparer, DatabaseColumn, Reason, SchemaDiff } from 'src/sql-tools/types';
|
import { Comparer, DatabaseColumn, Reason, SchemaDiff } from 'src/sql-tools/types';
|
||||||
|
|
||||||
export const compareColumns: Comparer<DatabaseColumn> = {
|
export const compareColumns = {
|
||||||
|
getRenameKey: (column) => {
|
||||||
|
return asRenameKey([
|
||||||
|
column.tableName,
|
||||||
|
column.type,
|
||||||
|
column.nullable,
|
||||||
|
column.default,
|
||||||
|
column.storage,
|
||||||
|
column.primary,
|
||||||
|
column.isArray,
|
||||||
|
column.length,
|
||||||
|
column.identity,
|
||||||
|
column.enumName,
|
||||||
|
column.numericPrecision,
|
||||||
|
column.numericScale,
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
onRename: (source, target) => [
|
||||||
|
{
|
||||||
|
type: 'ColumnRename',
|
||||||
|
tableName: source.tableName,
|
||||||
|
oldName: target.name,
|
||||||
|
newName: source.name,
|
||||||
|
reason: Reason.Rename,
|
||||||
|
},
|
||||||
|
],
|
||||||
onMissing: (source) => [
|
onMissing: (source) => [
|
||||||
{
|
{
|
||||||
type: 'ColumnAdd',
|
type: 'ColumnAdd',
|
||||||
@ -67,7 +92,7 @@ export const compareColumns: Comparer<DatabaseColumn> = {
|
|||||||
|
|
||||||
return items;
|
return items;
|
||||||
},
|
},
|
||||||
};
|
} satisfies Comparer<DatabaseColumn>;
|
||||||
|
|
||||||
const dropAndRecreateColumn = (source: DatabaseColumn, target: DatabaseColumn, reason: string): SchemaDiff[] => {
|
const dropAndRecreateColumn = (source: DatabaseColumn, target: DatabaseColumn, reason: string): SchemaDiff[] => {
|
||||||
return [
|
return [
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { haveEqualColumns } from 'src/sql-tools/helpers';
|
import { asRenameKey, haveEqualColumns } from 'src/sql-tools/helpers';
|
||||||
import {
|
import {
|
||||||
CompareFunction,
|
CompareFunction,
|
||||||
Comparer,
|
Comparer,
|
||||||
@ -13,6 +13,37 @@ import {
|
|||||||
} from 'src/sql-tools/types';
|
} from 'src/sql-tools/types';
|
||||||
|
|
||||||
export const compareConstraints: Comparer<DatabaseConstraint> = {
|
export const compareConstraints: Comparer<DatabaseConstraint> = {
|
||||||
|
getRenameKey: (constraint) => {
|
||||||
|
switch (constraint.type) {
|
||||||
|
case ConstraintType.PRIMARY_KEY:
|
||||||
|
case ConstraintType.UNIQUE: {
|
||||||
|
return asRenameKey([constraint.type, constraint.tableName, ...constraint.columnNames.toSorted()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
case ConstraintType.FOREIGN_KEY: {
|
||||||
|
return asRenameKey([
|
||||||
|
constraint.type,
|
||||||
|
constraint.tableName,
|
||||||
|
...constraint.columnNames.toSorted(),
|
||||||
|
constraint.referenceTableName,
|
||||||
|
...constraint.referenceColumnNames.toSorted(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
case ConstraintType.CHECK: {
|
||||||
|
return asRenameKey([constraint.type, constraint.tableName, constraint.expression]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRename: (source, target) => [
|
||||||
|
{
|
||||||
|
type: 'ConstraintRename',
|
||||||
|
tableName: target.tableName,
|
||||||
|
oldName: target.name,
|
||||||
|
newName: source.name,
|
||||||
|
reason: Reason.Rename,
|
||||||
|
},
|
||||||
|
],
|
||||||
onMissing: (source) => [
|
onMissing: (source) => [
|
||||||
{
|
{
|
||||||
type: 'ConstraintAdd',
|
type: 'ConstraintAdd',
|
||||||
|
@ -1,7 +1,23 @@
|
|||||||
import { haveEqualColumns } from 'src/sql-tools/helpers';
|
import { asRenameKey, haveEqualColumns } from 'src/sql-tools/helpers';
|
||||||
import { Comparer, DatabaseIndex, Reason } from 'src/sql-tools/types';
|
import { Comparer, DatabaseIndex, Reason } from 'src/sql-tools/types';
|
||||||
|
|
||||||
export const compareIndexes: Comparer<DatabaseIndex> = {
|
export const compareIndexes: Comparer<DatabaseIndex> = {
|
||||||
|
getRenameKey: (index) => {
|
||||||
|
if (index.override) {
|
||||||
|
return index.override.value.sql.replace(index.name, 'INDEX_NAME');
|
||||||
|
}
|
||||||
|
|
||||||
|
return asRenameKey([index.tableName, ...(index.columnNames || []).toSorted(), index.unique]);
|
||||||
|
},
|
||||||
|
onRename: (source, target) => [
|
||||||
|
{
|
||||||
|
type: 'IndexRename',
|
||||||
|
tableName: source.tableName,
|
||||||
|
oldName: target.name,
|
||||||
|
newName: source.name,
|
||||||
|
reason: Reason.Rename,
|
||||||
|
},
|
||||||
|
],
|
||||||
onMissing: (source) => [
|
onMissing: (source) => [
|
||||||
{
|
{
|
||||||
type: 'IndexCreate',
|
type: 'IndexCreate',
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import { DefaultNamingStrategy } from 'src/sql-tools/naming/default.naming';
|
||||||
|
import { HashNamingStrategy } from 'src/sql-tools/naming/hash.naming';
|
||||||
|
import { NamingInterface, NamingItem } from 'src/sql-tools/naming/naming.interface';
|
||||||
import {
|
import {
|
||||||
BaseContextOptions,
|
BaseContextOptions,
|
||||||
DatabaseEnum,
|
DatabaseEnum,
|
||||||
@ -11,6 +14,26 @@ import {
|
|||||||
|
|
||||||
const asOverrideKey = (type: string, name: string) => `${type}:${name}`;
|
const asOverrideKey = (type: string, name: string) => `${type}:${name}`;
|
||||||
|
|
||||||
|
const isNamingInterface = (strategy: any): strategy is NamingInterface => {
|
||||||
|
return typeof strategy === 'object' && typeof strategy.getName === 'function';
|
||||||
|
};
|
||||||
|
|
||||||
|
const asNamingStrategy = (strategy: 'hash' | 'default' | NamingInterface): NamingInterface => {
|
||||||
|
if (isNamingInterface(strategy)) {
|
||||||
|
return strategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (strategy) {
|
||||||
|
case 'hash': {
|
||||||
|
return new HashNamingStrategy();
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return new DefaultNamingStrategy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export class BaseContext {
|
export class BaseContext {
|
||||||
databaseName: string;
|
databaseName: string;
|
||||||
schemaName: string;
|
schemaName: string;
|
||||||
@ -24,10 +47,17 @@ export class BaseContext {
|
|||||||
overrides: DatabaseOverride[] = [];
|
overrides: DatabaseOverride[] = [];
|
||||||
warnings: string[] = [];
|
warnings: string[] = [];
|
||||||
|
|
||||||
|
private namingStrategy: NamingInterface;
|
||||||
|
|
||||||
constructor(options: BaseContextOptions) {
|
constructor(options: BaseContextOptions) {
|
||||||
this.databaseName = options.databaseName ?? 'postgres';
|
this.databaseName = options.databaseName ?? 'postgres';
|
||||||
this.schemaName = options.schemaName ?? 'public';
|
this.schemaName = options.schemaName ?? 'public';
|
||||||
this.overrideTableName = options.overrideTableName ?? 'migration_overrides';
|
this.overrideTableName = options.overrideTableName ?? 'migration_overrides';
|
||||||
|
this.namingStrategy = asNamingStrategy(options.namingStrategy ?? 'hash');
|
||||||
|
}
|
||||||
|
|
||||||
|
getNameFor(item: NamingItem) {
|
||||||
|
return this.namingStrategy.getName(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTableByName(name: string) {
|
getTableByName(name: string) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
||||||
import { BaseContext } from 'src/sql-tools/contexts/base-context';
|
import { BaseContext } from 'src/sql-tools/contexts/base-context';
|
||||||
import { ColumnOptions, TableOptions } from 'src/sql-tools/decorators';
|
import { ColumnOptions } from 'src/sql-tools/decorators/column.decorator';
|
||||||
import { asKey } from 'src/sql-tools/helpers';
|
import { TableOptions } from 'src/sql-tools/decorators/table.decorator';
|
||||||
import { DatabaseColumn, DatabaseTable, SchemaFromCodeOptions } from 'src/sql-tools/types';
|
import { DatabaseColumn, DatabaseTable, SchemaFromCodeOptions } from 'src/sql-tools/types';
|
||||||
|
|
||||||
type TableMetadata = { options: TableOptions; object: Function; methodToColumn: Map<string | symbol, DatabaseColumn> };
|
type TableMetadata = { options: TableOptions; object: Function; methodToColumn: Map<string | symbol, DatabaseColumn> };
|
||||||
@ -59,19 +59,6 @@ export class ProcessorContext extends BaseContext {
|
|||||||
tableMetadata.methodToColumn.set(propertyName, column);
|
tableMetadata.methodToColumn.set(propertyName, column);
|
||||||
}
|
}
|
||||||
|
|
||||||
asIndexName(table: string, columns?: string[], where?: string) {
|
|
||||||
const items: string[] = [];
|
|
||||||
for (const columnName of columns ?? []) {
|
|
||||||
items.push(columnName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (where) {
|
|
||||||
items.push(where);
|
|
||||||
}
|
|
||||||
|
|
||||||
return asKey('IDX_', table, items);
|
|
||||||
}
|
|
||||||
|
|
||||||
warnMissingTable(context: string, object: object, propertyName?: symbol | string) {
|
warnMissingTable(context: string, object: object, propertyName?: symbol | string) {
|
||||||
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
|
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
|
||||||
this.warn(context, `Unable to find table (${label})`);
|
this.warn(context, `Unable to find table (${label})`);
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
export * from 'src/sql-tools/decorators/after-delete.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/after-insert.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/before-update.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/check.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/column.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/configuration-parameter.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/create-date-column.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/database.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/delete-date-column.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/extension.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/extensions.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/foreign-key-column.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/foreign-key-constraint.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/generated-column.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/index.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/primary-column.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/primary-generated-column.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/table.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/trigger-function.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/trigger.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/unique.decorator';
|
|
||||||
export * from 'src/sql-tools/decorators/update-date-column.decorator';
|
|
@ -2,13 +2,6 @@ import { createHash } from 'node:crypto';
|
|||||||
import { ColumnValue } from 'src/sql-tools/decorators/column.decorator';
|
import { ColumnValue } from 'src/sql-tools/decorators/column.decorator';
|
||||||
import { Comparer, DatabaseColumn, DatabaseOverride, IgnoreOptions, SchemaDiff } from 'src/sql-tools/types';
|
import { Comparer, DatabaseColumn, DatabaseOverride, IgnoreOptions, SchemaDiff } from 'src/sql-tools/types';
|
||||||
|
|
||||||
export const asMetadataKey = (name: string) => `sql-tools:${name}`;
|
|
||||||
|
|
||||||
export const asSnakeCase = (name: string): string => name.replaceAll(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
|
|
||||||
// match TypeORM
|
|
||||||
export const asKey = (prefix: string, tableName: string, values: string[]) =>
|
|
||||||
(prefix + sha1(`${tableName}_${values.toSorted().join('_')}`)).slice(0, 30);
|
|
||||||
|
|
||||||
export const asOptions = <T extends { name?: string }>(options: string | T): T => {
|
export const asOptions = <T extends { name?: string }>(options: string | T): T => {
|
||||||
if (typeof options === 'string') {
|
if (typeof options === 'string') {
|
||||||
return { name: options } as T;
|
return { name: options } as T;
|
||||||
@ -79,6 +72,10 @@ export const compare = <T extends { name: string; synchronize: boolean }>(
|
|||||||
const items: SchemaDiff[] = [];
|
const items: SchemaDiff[] = [];
|
||||||
|
|
||||||
const keys = new Set([...Object.keys(sourceMap), ...Object.keys(targetMap)]);
|
const keys = new Set([...Object.keys(sourceMap), ...Object.keys(targetMap)]);
|
||||||
|
const missingKeys = new Set<string>();
|
||||||
|
const extraKeys = new Set<string>();
|
||||||
|
|
||||||
|
// common keys
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const source = sourceMap[key];
|
const source = sourceMap[key];
|
||||||
const target = targetMap[key];
|
const target = targetMap[key];
|
||||||
@ -92,22 +89,63 @@ export const compare = <T extends { name: string; synchronize: boolean }>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (source && !target) {
|
if (source && !target) {
|
||||||
items.push(...comparer.onMissing(source));
|
missingKeys.add(key);
|
||||||
} else if (!source && target) {
|
continue;
|
||||||
items.push(...comparer.onExtra(target));
|
}
|
||||||
} else {
|
|
||||||
if (
|
if (!source && target) {
|
||||||
haveEqualOverrides(
|
extraKeys.add(key);
|
||||||
source as unknown as { override?: DatabaseOverride },
|
continue;
|
||||||
target as unknown as { override?: DatabaseOverride },
|
}
|
||||||
)
|
|
||||||
) {
|
if (
|
||||||
|
haveEqualOverrides(
|
||||||
|
source as unknown as { override?: DatabaseOverride },
|
||||||
|
target as unknown as { override?: DatabaseOverride },
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push(...comparer.onCompare(source, target));
|
||||||
|
}
|
||||||
|
|
||||||
|
// renames
|
||||||
|
if (comparer.getRenameKey && comparer.onRename) {
|
||||||
|
const renameMap: Record<string, string> = {};
|
||||||
|
for (const sourceKey of missingKeys) {
|
||||||
|
const source = sourceMap[sourceKey];
|
||||||
|
const renameKey = comparer.getRenameKey(source);
|
||||||
|
renameMap[renameKey] = sourceKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const targetKey of extraKeys) {
|
||||||
|
const target = targetMap[targetKey];
|
||||||
|
const renameKey = comparer.getRenameKey(target);
|
||||||
|
const sourceKey = renameMap[renameKey];
|
||||||
|
if (!sourceKey) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
items.push(...comparer.onCompare(source, target));
|
|
||||||
|
const source = sourceMap[sourceKey];
|
||||||
|
|
||||||
|
items.push(...comparer.onRename(source, target));
|
||||||
|
|
||||||
|
missingKeys.delete(sourceKey);
|
||||||
|
extraKeys.delete(targetKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// missing
|
||||||
|
for (const key of missingKeys) {
|
||||||
|
items.push(...comparer.onMissing(sourceMap[key]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// extra
|
||||||
|
for (const key of extraKeys) {
|
||||||
|
items.push(...comparer.onExtra(targetMap[key]));
|
||||||
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -186,8 +224,6 @@ export const asColumnComment = (tableName: string, columnName: string, comment:
|
|||||||
|
|
||||||
export const asColumnList = (columns: string[]) => columns.map((column) => `"${column}"`).join(', ');
|
export const asColumnList = (columns: string[]) => columns.map((column) => `"${column}"`).join(', ');
|
||||||
|
|
||||||
export const asForeignKeyConstraintName = (table: string, columns: string[]) => asKey('FK_', table, [...columns]);
|
|
||||||
|
|
||||||
export const asJsonString = (value: unknown): string => {
|
export const asJsonString = (value: unknown): string => {
|
||||||
return `'${escape(JSON.stringify(value))}'::jsonb`;
|
return `'${escape(JSON.stringify(value))}'::jsonb`;
|
||||||
};
|
};
|
||||||
@ -202,3 +238,6 @@ const escape = (value: string) => {
|
|||||||
.replaceAll(/[\r]/g, String.raw`\r`)
|
.replaceAll(/[\r]/g, String.raw`\r`)
|
||||||
.replaceAll(/[\t]/g, String.raw`\t`);
|
.replaceAll(/[\t]/g, String.raw`\t`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const asRenameKey = (values: Array<string | boolean | number | undefined>) =>
|
||||||
|
values.map((value) => value ?? '').join('|');
|
||||||
|
50
server/src/sql-tools/naming/default.naming.ts
Normal file
50
server/src/sql-tools/naming/default.naming.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { sha1 } from 'src/sql-tools/helpers';
|
||||||
|
import { NamingItem } from 'src/sql-tools/naming/naming.interface';
|
||||||
|
|
||||||
|
const asSnakeCase = (name: string): string => name.replaceAll(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
|
||||||
|
|
||||||
|
export class DefaultNamingStrategy {
|
||||||
|
getName(item: NamingItem): string {
|
||||||
|
switch (item.type) {
|
||||||
|
case 'database': {
|
||||||
|
return asSnakeCase(item.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'table': {
|
||||||
|
return asSnakeCase(item.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'column': {
|
||||||
|
return item.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'primaryKey': {
|
||||||
|
return `${item.tableName}_pkey`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'foreignKey': {
|
||||||
|
return `${item.tableName}_${item.columnNames.join('_')}_fkey`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'check': {
|
||||||
|
return `${item.tableName}_${sha1(item.expression).slice(0, 8)}_chk`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'unique': {
|
||||||
|
return `${item.tableName}_${item.columnNames.join('_')}_uq`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'index': {
|
||||||
|
if (item.columnNames) {
|
||||||
|
return `${item.tableName}_${item.columnNames.join('_')}_idx`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${item.tableName}_${sha1(item.expression || item.where || '').slice(0, 8)}_idx`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'trigger': {
|
||||||
|
return `${item.tableName}_${item.functionName}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
server/src/sql-tools/naming/hash.naming.ts
Normal file
51
server/src/sql-tools/naming/hash.naming.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { sha1 } from 'src/sql-tools/helpers';
|
||||||
|
import { DefaultNamingStrategy } from 'src/sql-tools/naming/default.naming';
|
||||||
|
import { NamingInterface, NamingItem } from 'src/sql-tools/naming/naming.interface';
|
||||||
|
|
||||||
|
const fallback = new DefaultNamingStrategy();
|
||||||
|
|
||||||
|
const asKey = (prefix: string, tableName: string, values: string[]) =>
|
||||||
|
(prefix + sha1(`${tableName}_${values.toSorted().join('_')}`)).slice(0, 30);
|
||||||
|
|
||||||
|
export class HashNamingStrategy implements NamingInterface {
|
||||||
|
getName(item: NamingItem): string {
|
||||||
|
switch (item.type) {
|
||||||
|
case 'primaryKey': {
|
||||||
|
return asKey('PK_', item.tableName, item.columnNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'foreignKey': {
|
||||||
|
return asKey('FK_', item.tableName, item.columnNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'check': {
|
||||||
|
return asKey('CHK_', item.tableName, [item.expression]);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'unique': {
|
||||||
|
return asKey('UQ_', item.tableName, item.columnNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'index': {
|
||||||
|
const items: string[] = [];
|
||||||
|
for (const columnName of item.columnNames ?? []) {
|
||||||
|
items.push(columnName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.where) {
|
||||||
|
items.push(item.where);
|
||||||
|
}
|
||||||
|
|
||||||
|
return asKey('IDX_', item.tableName, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'trigger': {
|
||||||
|
return asKey('TR_', item.tableName, [...item.actions, item.scope, item.timing, item.functionName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return fallback.getName(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
server/src/sql-tools/naming/naming.interface.ts
Normal file
59
server/src/sql-tools/naming/naming.interface.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { TriggerAction, TriggerScope, TriggerTiming } from 'src/sql-tools/types';
|
||||||
|
|
||||||
|
export type NamingItem =
|
||||||
|
| {
|
||||||
|
type: 'database';
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'table';
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'column';
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'primaryKey';
|
||||||
|
tableName: string;
|
||||||
|
columnNames: string[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'foreignKey';
|
||||||
|
tableName: string;
|
||||||
|
columnNames: string[];
|
||||||
|
referenceTableName: string;
|
||||||
|
referenceColumnNames: string[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'check';
|
||||||
|
tableName: string;
|
||||||
|
expression: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'unique';
|
||||||
|
tableName: string;
|
||||||
|
columnNames: string[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'index';
|
||||||
|
tableName: string;
|
||||||
|
columnNames?: string[];
|
||||||
|
expression?: string;
|
||||||
|
where?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'trigger';
|
||||||
|
tableName: string;
|
||||||
|
functionName: string;
|
||||||
|
actions: TriggerAction[];
|
||||||
|
scope: TriggerScope;
|
||||||
|
timing: TriggerTiming;
|
||||||
|
columnNames?: string[];
|
||||||
|
expression?: string;
|
||||||
|
where?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface NamingInterface {
|
||||||
|
getName(item: NamingItem): string;
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
import { asKey } from 'src/sql-tools/helpers';
|
|
||||||
import { ConstraintType, Processor } from 'src/sql-tools/types';
|
import { ConstraintType, Processor } from 'src/sql-tools/types';
|
||||||
|
|
||||||
export const processCheckConstraints: Processor = (ctx, items) => {
|
export const processCheckConstraints: Processor = (ctx, items) => {
|
||||||
@ -15,12 +14,10 @@ export const processCheckConstraints: Processor = (ctx, items) => {
|
|||||||
|
|
||||||
table.constraints.push({
|
table.constraints.push({
|
||||||
type: ConstraintType.CHECK,
|
type: ConstraintType.CHECK,
|
||||||
name: options.name || asCheckConstraintName(tableName, options.expression),
|
name: options.name || ctx.getNameFor({ type: 'check', tableName, expression: options.expression }),
|
||||||
tableName,
|
tableName,
|
||||||
expression: options.expression,
|
expression: options.expression,
|
||||||
synchronize: options.synchronize ?? true,
|
synchronize: options.synchronize ?? true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const asCheckConstraintName = (table: string, expression: string) => asKey('CHK_', table, [expression]);
|
|
||||||
|
@ -13,7 +13,7 @@ export const processColumns: Processor = (ctx, items) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnName = options.name ?? String(propertyName);
|
const columnName = options.name ?? ctx.getNameFor({ type: 'column', name: String(propertyName) });
|
||||||
const existingColumn = table.columns.find((column) => column.name === columnName);
|
const existingColumn = table.columns.find((column) => column.name === columnName);
|
||||||
if (existingColumn) {
|
if (existingColumn) {
|
||||||
// TODO log warnings if column name is not unique
|
// TODO log warnings if column name is not unique
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { asSnakeCase } from 'src/sql-tools/helpers';
|
|
||||||
import { Processor } from 'src/sql-tools/types';
|
import { Processor } from 'src/sql-tools/types';
|
||||||
|
|
||||||
export const processDatabases: Processor = (ctx, items) => {
|
export const processDatabases: Processor = (ctx, items) => {
|
||||||
for (const {
|
for (const {
|
||||||
item: { object, options },
|
item: { object, options },
|
||||||
} of items.filter((item) => item.type === 'database')) {
|
} of items.filter((item) => item.type === 'database')) {
|
||||||
ctx.databaseName = options.name || asSnakeCase(object.name);
|
ctx.databaseName = options.name || ctx.getNameFor({ type: 'database', name: object.name });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { asForeignKeyConstraintName, asKey } from 'src/sql-tools/helpers';
|
|
||||||
import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types';
|
import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types';
|
||||||
|
|
||||||
export const processForeignKeyColumns: Processor = (ctx, items) => {
|
export const processForeignKeyColumns: Processor = (ctx, items) => {
|
||||||
@ -31,15 +30,24 @@ export const processForeignKeyColumns: Processor = (ctx, items) => {
|
|||||||
column.type = referenceColumns[0].type;
|
column.type = referenceColumns[0].type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const referenceTableName = referenceTable.name;
|
||||||
const referenceColumnNames = referenceColumns.map((column) => column.name);
|
const referenceColumnNames = referenceColumns.map((column) => column.name);
|
||||||
const name = options.constraintName || asForeignKeyConstraintName(table.name, columnNames);
|
const name =
|
||||||
|
options.constraintName ||
|
||||||
|
ctx.getNameFor({
|
||||||
|
type: 'foreignKey',
|
||||||
|
tableName: table.name,
|
||||||
|
columnNames,
|
||||||
|
referenceTableName,
|
||||||
|
referenceColumnNames,
|
||||||
|
});
|
||||||
|
|
||||||
table.constraints.push({
|
table.constraints.push({
|
||||||
name,
|
name,
|
||||||
tableName: table.name,
|
tableName: table.name,
|
||||||
columnNames,
|
columnNames,
|
||||||
type: ConstraintType.FOREIGN_KEY,
|
type: ConstraintType.FOREIGN_KEY,
|
||||||
referenceTableName: referenceTable.name,
|
referenceTableName,
|
||||||
referenceColumnNames,
|
referenceColumnNames,
|
||||||
onUpdate: options.onUpdate as ActionType,
|
onUpdate: options.onUpdate as ActionType,
|
||||||
onDelete: options.onDelete as ActionType,
|
onDelete: options.onDelete as ActionType,
|
||||||
@ -48,7 +56,7 @@ export const processForeignKeyColumns: Processor = (ctx, items) => {
|
|||||||
|
|
||||||
if (options.unique || options.uniqueConstraintName) {
|
if (options.unique || options.uniqueConstraintName) {
|
||||||
table.constraints.push({
|
table.constraints.push({
|
||||||
name: options.uniqueConstraintName || asRelationKeyConstraintName(table.name, columnNames),
|
name: options.uniqueConstraintName || ctx.getNameFor({ type: 'unique', tableName: table.name, columnNames }),
|
||||||
tableName: table.name,
|
tableName: table.name,
|
||||||
columnNames,
|
columnNames,
|
||||||
type: ConstraintType.UNIQUE,
|
type: ConstraintType.UNIQUE,
|
||||||
@ -57,5 +65,3 @@ export const processForeignKeyColumns: Processor = (ctx, items) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const asRelationKeyConstraintName = (table: string, columns: string[]) => asKey('REL_', table, columns);
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { asForeignKeyConstraintName } from 'src/sql-tools/helpers';
|
|
||||||
import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types';
|
import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types';
|
||||||
|
|
||||||
export const processForeignKeyConstraints: Processor = (ctx, items) => {
|
export const processForeignKeyConstraints: Processor = (ctx, items) => {
|
||||||
@ -46,18 +45,27 @@ export const processForeignKeyConstraints: Processor = (ctx, items) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const referenceColumns =
|
const referenceTableName = referenceTable.name;
|
||||||
|
const referenceColumnNames =
|
||||||
options.referenceColumns || referenceTable.columns.filter(({ primary }) => primary).map(({ name }) => name);
|
options.referenceColumns || referenceTable.columns.filter(({ primary }) => primary).map(({ name }) => name);
|
||||||
|
|
||||||
const name = options.name || asForeignKeyConstraintName(table.name, options.columns);
|
const name =
|
||||||
|
options.name ||
|
||||||
|
ctx.getNameFor({
|
||||||
|
type: 'foreignKey',
|
||||||
|
tableName: table.name,
|
||||||
|
columnNames: options.columns,
|
||||||
|
referenceTableName,
|
||||||
|
referenceColumnNames,
|
||||||
|
});
|
||||||
|
|
||||||
table.constraints.push({
|
table.constraints.push({
|
||||||
type: ConstraintType.FOREIGN_KEY,
|
type: ConstraintType.FOREIGN_KEY,
|
||||||
name,
|
name,
|
||||||
tableName: table.name,
|
tableName: table.name,
|
||||||
columnNames: options.columns,
|
columnNames: options.columns,
|
||||||
referenceTableName: referenceTable.name,
|
referenceTableName,
|
||||||
referenceColumnNames: referenceColumns,
|
referenceColumnNames,
|
||||||
onUpdate: options.onUpdate as ActionType,
|
onUpdate: options.onUpdate as ActionType,
|
||||||
onDelete: options.onDelete as ActionType,
|
onDelete: options.onDelete as ActionType,
|
||||||
synchronize: options.synchronize ?? true,
|
synchronize: options.synchronize ?? true,
|
||||||
@ -68,8 +76,15 @@ export const processForeignKeyConstraints: Processor = (ctx, items) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.index || options.indexName || ctx.options.createForeignKeyIndexes) {
|
if (options.index || options.indexName || ctx.options.createForeignKeyIndexes) {
|
||||||
|
const indexName =
|
||||||
|
options.indexName ||
|
||||||
|
ctx.getNameFor({
|
||||||
|
type: 'index',
|
||||||
|
tableName: table.name,
|
||||||
|
columnNames: options.columns,
|
||||||
|
});
|
||||||
table.indexes.push({
|
table.indexes.push({
|
||||||
name: options.indexName || ctx.asIndexName(table.name, options.columns),
|
name: indexName,
|
||||||
tableName: table.name,
|
tableName: table.name,
|
||||||
columnNames: options.columns,
|
columnNames: options.columns,
|
||||||
unique: false,
|
unique: false,
|
||||||
|
@ -10,8 +10,17 @@ export const processIndexes: Processor = (ctx, items) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const indexName =
|
||||||
|
options.name ||
|
||||||
|
ctx.getNameFor({
|
||||||
|
type: 'index',
|
||||||
|
tableName: table.name,
|
||||||
|
columnNames: options.columns,
|
||||||
|
where: options.where,
|
||||||
|
});
|
||||||
|
|
||||||
table.indexes.push({
|
table.indexes.push({
|
||||||
name: options.name || ctx.asIndexName(table.name, options.columns, options.where),
|
name: indexName,
|
||||||
tableName: table.name,
|
tableName: table.name,
|
||||||
unique: options.unique ?? false,
|
unique: options.unique ?? false,
|
||||||
expression: options.expression,
|
expression: options.expression,
|
||||||
@ -50,7 +59,13 @@ export const processIndexes: Processor = (ctx, items) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexName = options.indexName || ctx.asIndexName(table.name, [column.name]);
|
const indexName =
|
||||||
|
options.indexName ||
|
||||||
|
ctx.getNameFor({
|
||||||
|
type: 'index',
|
||||||
|
tableName: table.name,
|
||||||
|
columnNames: [column.name],
|
||||||
|
});
|
||||||
|
|
||||||
const isIndexPresent = table.indexes.some((index) => index.name === indexName);
|
const isIndexPresent = table.indexes.some((index) => index.name === indexName);
|
||||||
if (isIndexPresent) {
|
if (isIndexPresent) {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { asKey } from 'src/sql-tools/helpers';
|
|
||||||
import { ConstraintType, Processor } from 'src/sql-tools/types';
|
import { ConstraintType, Processor } from 'src/sql-tools/types';
|
||||||
|
|
||||||
export const processPrimaryKeyConstraints: Processor = (ctx) => {
|
export const processPrimaryKeyConstraints: Processor = (ctx) => {
|
||||||
@ -15,7 +14,13 @@ export const processPrimaryKeyConstraints: Processor = (ctx) => {
|
|||||||
const tableMetadata = ctx.getTableMetadata(table);
|
const tableMetadata = ctx.getTableMetadata(table);
|
||||||
table.constraints.push({
|
table.constraints.push({
|
||||||
type: ConstraintType.PRIMARY_KEY,
|
type: ConstraintType.PRIMARY_KEY,
|
||||||
name: tableMetadata.options.primaryConstraintName || asPrimaryKeyConstraintName(table.name, columnNames),
|
name:
|
||||||
|
tableMetadata.options.primaryConstraintName ||
|
||||||
|
ctx.getNameFor({
|
||||||
|
type: 'primaryKey',
|
||||||
|
tableName: table.name,
|
||||||
|
columnNames,
|
||||||
|
}),
|
||||||
tableName: table.name,
|
tableName: table.name,
|
||||||
columnNames,
|
columnNames,
|
||||||
synchronize: tableMetadata.options.synchronize ?? true,
|
synchronize: tableMetadata.options.synchronize ?? true,
|
||||||
@ -23,5 +28,3 @@ export const processPrimaryKeyConstraints: Processor = (ctx) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const asPrimaryKeyConstraintName = (table: string, columns: string[]) => asKey('PK_', table, columns);
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { asSnakeCase } from 'src/sql-tools/helpers';
|
|
||||||
import { Processor } from 'src/sql-tools/types';
|
import { Processor } from 'src/sql-tools/types';
|
||||||
|
|
||||||
export const processTables: Processor = (ctx, items) => {
|
export const processTables: Processor = (ctx, items) => {
|
||||||
@ -14,7 +13,7 @@ export const processTables: Processor = (ctx, items) => {
|
|||||||
|
|
||||||
ctx.addTable(
|
ctx.addTable(
|
||||||
{
|
{
|
||||||
name: options.name || asSnakeCase(object.name),
|
name: options.name || ctx.getNameFor({ type: 'table', name: object.name }),
|
||||||
columns: [],
|
columns: [],
|
||||||
constraints: [],
|
constraints: [],
|
||||||
indexes: [],
|
indexes: [],
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { TriggerOptions } from 'src/sql-tools/decorators/trigger.decorator';
|
|
||||||
import { asKey } from 'src/sql-tools/helpers';
|
|
||||||
import { Processor } from 'src/sql-tools/types';
|
import { Processor } from 'src/sql-tools/types';
|
||||||
|
|
||||||
export const processTriggers: Processor = (ctx, items) => {
|
export const processTriggers: Processor = (ctx, items) => {
|
||||||
@ -12,8 +10,19 @@ export const processTriggers: Processor = (ctx, items) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const triggerName =
|
||||||
|
options.name ||
|
||||||
|
ctx.getNameFor({
|
||||||
|
type: 'trigger',
|
||||||
|
tableName: table.name,
|
||||||
|
actions: options.actions,
|
||||||
|
scope: options.scope,
|
||||||
|
timing: options.timing,
|
||||||
|
functionName: options.functionName,
|
||||||
|
});
|
||||||
|
|
||||||
table.triggers.push({
|
table.triggers.push({
|
||||||
name: options.name || asTriggerName(table.name, options),
|
name: triggerName,
|
||||||
tableName: table.name,
|
tableName: table.name,
|
||||||
timing: options.timing,
|
timing: options.timing,
|
||||||
actions: options.actions,
|
actions: options.actions,
|
||||||
@ -26,6 +35,3 @@ export const processTriggers: Processor = (ctx, items) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const asTriggerName = (table: string, trigger: TriggerOptions) =>
|
|
||||||
asKey('TR_', table, [...trigger.actions, trigger.scope, trigger.timing, trigger.functionName]);
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { asKey } from 'src/sql-tools/helpers';
|
|
||||||
import { ConstraintType, Processor } from 'src/sql-tools/types';
|
import { ConstraintType, Processor } from 'src/sql-tools/types';
|
||||||
|
|
||||||
export const processUniqueConstraints: Processor = (ctx, items) => {
|
export const processUniqueConstraints: Processor = (ctx, items) => {
|
||||||
@ -16,7 +15,7 @@ export const processUniqueConstraints: Processor = (ctx, items) => {
|
|||||||
|
|
||||||
table.constraints.push({
|
table.constraints.push({
|
||||||
type: ConstraintType.UNIQUE,
|
type: ConstraintType.UNIQUE,
|
||||||
name: options.name || asUniqueConstraintName(tableName, columnNames),
|
name: options.name || ctx.getNameFor({ type: 'unique', tableName, columnNames }),
|
||||||
tableName,
|
tableName,
|
||||||
columnNames,
|
columnNames,
|
||||||
synchronize: options.synchronize ?? true,
|
synchronize: options.synchronize ?? true,
|
||||||
@ -41,9 +40,17 @@ export const processUniqueConstraints: Processor = (ctx, items) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'column' && !options.primary && (options.unique || options.uniqueConstraintName)) {
|
if (type === 'column' && !options.primary && (options.unique || options.uniqueConstraintName)) {
|
||||||
|
const uniqueConstraintName =
|
||||||
|
options.uniqueConstraintName ||
|
||||||
|
ctx.getNameFor({
|
||||||
|
type: 'unique',
|
||||||
|
tableName: table.name,
|
||||||
|
columnNames: [column.name],
|
||||||
|
});
|
||||||
|
|
||||||
table.constraints.push({
|
table.constraints.push({
|
||||||
type: ConstraintType.UNIQUE,
|
type: ConstraintType.UNIQUE,
|
||||||
name: options.uniqueConstraintName || asUniqueConstraintName(table.name, [column.name]),
|
name: uniqueConstraintName,
|
||||||
tableName: table.name,
|
tableName: table.name,
|
||||||
columnNames: [column.name],
|
columnNames: [column.name],
|
||||||
synchronize: options.synchronize ?? true,
|
synchronize: options.synchronize ?? true,
|
||||||
@ -51,5 +58,3 @@ export const processUniqueConstraints: Processor = (ctx, items) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const asUniqueConstraintName = (table: string, columns: string[]) => asKey('UQ_', table, columns);
|
|
||||||
|
@ -1,4 +1,28 @@
|
|||||||
export * from 'src/sql-tools/decorators';
|
export * from 'src/sql-tools/decorators/after-delete.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/after-insert.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/before-update.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/check.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/column.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/configuration-parameter.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/create-date-column.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/database.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/delete-date-column.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/extension.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/extensions.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/foreign-key-column.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/foreign-key-constraint.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/generated-column.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/index.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/primary-column.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/primary-generated-column.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/table.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/trigger-function.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/trigger.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/unique.decorator';
|
||||||
|
export * from 'src/sql-tools/decorators/update-date-column.decorator';
|
||||||
|
export * from 'src/sql-tools/naming/default.naming';
|
||||||
|
export * from 'src/sql-tools/naming/hash.naming';
|
||||||
|
export * from 'src/sql-tools/naming/naming.interface';
|
||||||
export * from 'src/sql-tools/register-enum';
|
export * from 'src/sql-tools/register-enum';
|
||||||
export * from 'src/sql-tools/register-function';
|
export * from 'src/sql-tools/register-function';
|
||||||
export { schemaDiff, schemaDiffToSql } from 'src/sql-tools/schema-diff';
|
export { schemaDiff, schemaDiffToSql } from 'src/sql-tools/schema-diff';
|
||||||
|
@ -76,6 +76,8 @@ export const readColumns: Reader = async (ctx, db) => {
|
|||||||
|
|
||||||
const item: DatabaseColumn = {
|
const item: DatabaseColumn = {
|
||||||
type: column.data_type as ColumnType,
|
type: column.data_type as ColumnType,
|
||||||
|
// TODO infer this from PK constraints
|
||||||
|
primary: false,
|
||||||
name: columnName,
|
name: columnName,
|
||||||
tableName: column.table_name,
|
tableName: column.table_name,
|
||||||
nullable: column.is_nullable === 'YES',
|
nullable: column.is_nullable === 'YES',
|
||||||
|
@ -28,6 +28,7 @@ const fromColumn = (column: Partial<Omit<DatabaseColumn, 'tableName'>>): Databas
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'column1',
|
name: 'column1',
|
||||||
|
primary: false,
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
isArray: false,
|
isArray: false,
|
||||||
type: 'character varying',
|
type: 'character varying',
|
||||||
@ -63,6 +64,7 @@ const fromConstraint = (constraint?: DatabaseConstraint): DatabaseSchema => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'column1',
|
name: 'column1',
|
||||||
|
primary: false,
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
isArray: false,
|
isArray: false,
|
||||||
type: 'character varying',
|
type: 'character varying',
|
||||||
@ -97,6 +99,7 @@ const fromIndex = (index?: DatabaseIndex): DatabaseSchema => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'column1',
|
name: 'column1',
|
||||||
|
primary: false,
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
isArray: false,
|
isArray: false,
|
||||||
type: 'character varying',
|
type: 'character varying',
|
||||||
@ -140,6 +143,7 @@ const newSchema = (schema: {
|
|||||||
columns.push({
|
columns.push({
|
||||||
tableName,
|
tableName,
|
||||||
name: columnName,
|
name: columnName,
|
||||||
|
primary: false,
|
||||||
type: column.type || 'character varying',
|
type: column.type || 'character varying',
|
||||||
isArray: column.isArray ?? false,
|
isArray: column.isArray ?? false,
|
||||||
nullable: column.nullable ?? false,
|
nullable: column.nullable ?? false,
|
||||||
@ -182,6 +186,7 @@ describe(schemaDiff.name, () => {
|
|||||||
const column: DatabaseColumn = {
|
const column: DatabaseColumn = {
|
||||||
type: 'character varying',
|
type: 'character varying',
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
|
primary: false,
|
||||||
name: 'column1',
|
name: 'column1',
|
||||||
isArray: false,
|
isArray: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
@ -264,6 +269,7 @@ describe(schemaDiff.name, () => {
|
|||||||
column: {
|
column: {
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
isArray: false,
|
isArray: false,
|
||||||
|
primary: false,
|
||||||
name: 'column2',
|
name: 'column2',
|
||||||
nullable: false,
|
nullable: false,
|
||||||
type: 'character varying',
|
type: 'character varying',
|
||||||
|
@ -40,10 +40,13 @@ export const schemaDiff = (source: DatabaseSchema, target: DatabaseSchema, optio
|
|||||||
TableDrop: [],
|
TableDrop: [],
|
||||||
ColumnAdd: [],
|
ColumnAdd: [],
|
||||||
ColumnAlter: [],
|
ColumnAlter: [],
|
||||||
|
ColumnRename: [],
|
||||||
ColumnDrop: [],
|
ColumnDrop: [],
|
||||||
ConstraintAdd: [],
|
ConstraintAdd: [],
|
||||||
ConstraintDrop: [],
|
ConstraintDrop: [],
|
||||||
|
ConstraintRename: [],
|
||||||
IndexCreate: [],
|
IndexCreate: [],
|
||||||
|
IndexRename: [],
|
||||||
IndexDrop: [],
|
IndexDrop: [],
|
||||||
TriggerCreate: [],
|
TriggerCreate: [],
|
||||||
TriggerDrop: [],
|
TriggerDrop: [],
|
||||||
@ -72,11 +75,14 @@ export const schemaDiff = (source: DatabaseSchema, target: DatabaseSchema, optio
|
|||||||
...itemMap.TableCreate,
|
...itemMap.TableCreate,
|
||||||
...itemMap.ColumnAlter,
|
...itemMap.ColumnAlter,
|
||||||
...itemMap.ColumnAdd,
|
...itemMap.ColumnAdd,
|
||||||
|
...itemMap.ColumnRename,
|
||||||
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.PRIMARY_KEY),
|
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.PRIMARY_KEY),
|
||||||
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.FOREIGN_KEY),
|
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.FOREIGN_KEY),
|
||||||
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.UNIQUE),
|
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.UNIQUE),
|
||||||
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.CHECK),
|
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.CHECK),
|
||||||
|
...itemMap.ConstraintRename,
|
||||||
...itemMap.IndexCreate,
|
...itemMap.IndexCreate,
|
||||||
|
...itemMap.IndexRename,
|
||||||
...itemMap.TriggerCreate,
|
...itemMap.TriggerCreate,
|
||||||
...itemMap.ColumnDrop,
|
...itemMap.ColumnDrop,
|
||||||
...itemMap.TableDrop,
|
...itemMap.TableDrop,
|
||||||
|
@ -20,9 +20,9 @@ export const schemaFromCode = (options: SchemaFromCodeOptions = {}) => {
|
|||||||
name: ctx.overrideTableName,
|
name: ctx.overrideTableName,
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
primary: true,
|
|
||||||
name: 'name',
|
name: 'name',
|
||||||
tableName: ctx.overrideTableName,
|
tableName: ctx.overrideTableName,
|
||||||
|
primary: true,
|
||||||
type: 'character varying',
|
type: 'character varying',
|
||||||
nullable: false,
|
nullable: false,
|
||||||
isArray: false,
|
isArray: false,
|
||||||
@ -31,6 +31,7 @@ export const schemaFromCode = (options: SchemaFromCodeOptions = {}) => {
|
|||||||
{
|
{
|
||||||
name: 'value',
|
name: 'value',
|
||||||
tableName: ctx.overrideTableName,
|
tableName: ctx.overrideTableName,
|
||||||
|
primary: false,
|
||||||
type: 'jsonb',
|
type: 'jsonb',
|
||||||
nullable: false,
|
nullable: false,
|
||||||
isArray: false,
|
isArray: false,
|
||||||
|
@ -13,6 +13,7 @@ describe(transformColumns.name, () => {
|
|||||||
column: {
|
column: {
|
||||||
name: 'column1',
|
name: 'column1',
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
|
primary: false,
|
||||||
type: 'character varying',
|
type: 'character varying',
|
||||||
nullable: false,
|
nullable: false,
|
||||||
isArray: false,
|
isArray: false,
|
||||||
@ -30,6 +31,7 @@ describe(transformColumns.name, () => {
|
|||||||
column: {
|
column: {
|
||||||
name: 'column1',
|
name: 'column1',
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
|
primary: false,
|
||||||
type: 'character varying',
|
type: 'character varying',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
isArray: false,
|
isArray: false,
|
||||||
@ -47,6 +49,7 @@ describe(transformColumns.name, () => {
|
|||||||
column: {
|
column: {
|
||||||
name: 'column1',
|
name: 'column1',
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
|
primary: false,
|
||||||
type: 'character varying',
|
type: 'character varying',
|
||||||
enumName: 'table1_column1_enum',
|
enumName: 'table1_column1_enum',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
@ -65,6 +68,7 @@ describe(transformColumns.name, () => {
|
|||||||
column: {
|
column: {
|
||||||
name: 'column1',
|
name: 'column1',
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
|
primary: false,
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
isArray: true,
|
isArray: true,
|
||||||
|
@ -12,8 +12,12 @@ export const transformColumns: SqlTransformer = (ctx, item) => {
|
|||||||
return asColumnAlter(item.tableName, item.columnName, item.changes);
|
return asColumnAlter(item.tableName, item.columnName, item.changes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'ColumnRename': {
|
||||||
|
return `ALTER TABLE "${item.tableName}" RENAME COLUMN "${item.oldName}" TO "${item.newName}";`;
|
||||||
|
}
|
||||||
|
|
||||||
case 'ColumnDrop': {
|
case 'ColumnDrop': {
|
||||||
return asColumnDrop(item.tableName, item.columnName);
|
return `ALTER TABLE "${item.tableName}" DROP COLUMN "${item.columnName}";`;
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
@ -28,10 +32,6 @@ const asColumnAdd = (column: DatabaseColumn): string => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const asColumnDrop = (tableName: string, columnName: string): string => {
|
|
||||||
return `ALTER TABLE "${tableName}" DROP COLUMN "${columnName}";`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const asColumnAlter = (tableName: string, columnName: string, changes: ColumnChanges): string[] => {
|
export const asColumnAlter = (tableName: string, columnName: string, changes: ColumnChanges): string[] => {
|
||||||
const base = `ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}"`;
|
const base = `ALTER TABLE "${tableName}" ALTER COLUMN "${columnName}"`;
|
||||||
const items: string[] = [];
|
const items: string[] = [];
|
||||||
|
@ -5,11 +5,15 @@ import { ActionType, ConstraintType, DatabaseConstraint } from 'src/sql-tools/ty
|
|||||||
export const transformConstraints: SqlTransformer = (ctx, item) => {
|
export const transformConstraints: SqlTransformer = (ctx, item) => {
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
case 'ConstraintAdd': {
|
case 'ConstraintAdd': {
|
||||||
return asConstraintAdd(item.constraint);
|
return `ALTER TABLE "${item.constraint.tableName}" ADD ${asConstraintBody(item.constraint)};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ConstraintRename': {
|
||||||
|
return `ALTER TABLE "${item.tableName}" RENAME CONSTRAINT "${item.oldName}" TO "${item.newName}";`;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'ConstraintDrop': {
|
case 'ConstraintDrop': {
|
||||||
return asConstraintDrop(item.tableName, item.constraintName);
|
return `ALTER TABLE "${item.tableName}" DROP CONSTRAINT "${item.constraintName}";`;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return false;
|
return false;
|
||||||
@ -52,11 +56,3 @@ export const asConstraintBody = (constraint: DatabaseConstraint): string => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const asConstraintAdd = (constraint: DatabaseConstraint): string | string[] => {
|
|
||||||
return `ALTER TABLE "${constraint.tableName}" ADD ${asConstraintBody(constraint)};`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const asConstraintDrop = (tableName: string, constraintName: string): string => {
|
|
||||||
return `ALTER TABLE "${tableName}" DROP CONSTRAINT "${constraintName}";`;
|
|
||||||
};
|
|
||||||
|
@ -8,8 +8,12 @@ export const transformIndexes: SqlTransformer = (ctx, item) => {
|
|||||||
return asIndexCreate(item.index);
|
return asIndexCreate(item.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'IndexRename': {
|
||||||
|
return `ALTER INDEX "${item.oldName}" RENAME TO "${item.newName}";`;
|
||||||
|
}
|
||||||
|
|
||||||
case 'IndexDrop': {
|
case 'IndexDrop': {
|
||||||
return asIndexDrop(item.indexName);
|
return `DROP INDEX "${item.indexName}";`;
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
@ -50,7 +54,3 @@ export const asIndexCreate = (index: DatabaseIndex): string => {
|
|||||||
|
|
||||||
return sql + ';';
|
return sql + ';';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const asIndexDrop = (indexName: string): string => {
|
|
||||||
return `DROP INDEX "${indexName}";`;
|
|
||||||
};
|
|
||||||
|
@ -19,6 +19,7 @@ const table1: DatabaseTable = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'column2',
|
name: 'column2',
|
||||||
|
primary: false,
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
type: 'character varying',
|
type: 'character varying',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
@ -106,6 +107,7 @@ describe(transformTables.name, () => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
|
primary: false,
|
||||||
name: 'column1',
|
name: 'column1',
|
||||||
type: 'character varying',
|
type: 'character varying',
|
||||||
isArray: false,
|
isArray: false,
|
||||||
@ -137,6 +139,7 @@ describe(transformTables.name, () => {
|
|||||||
{
|
{
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
name: 'column1',
|
name: 'column1',
|
||||||
|
primary: false,
|
||||||
type: 'character varying',
|
type: 'character varying',
|
||||||
isArray: false,
|
isArray: false,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
@ -167,6 +170,7 @@ describe(transformTables.name, () => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
|
primary: false,
|
||||||
name: 'column1',
|
name: 'column1',
|
||||||
type: 'character varying',
|
type: 'character varying',
|
||||||
length: 2,
|
length: 2,
|
||||||
@ -198,6 +202,7 @@ describe(transformTables.name, () => {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
|
primary: false,
|
||||||
name: 'column1',
|
name: 'column1',
|
||||||
type: 'character varying',
|
type: 'character varying',
|
||||||
isArray: true,
|
isArray: true,
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { Kysely, ColumnType as KyselyColumnType } from 'kysely';
|
import { Kysely, ColumnType as KyselyColumnType } from 'kysely';
|
||||||
import { ProcessorContext } from 'src/sql-tools/contexts/processor-context';
|
import { ProcessorContext } from 'src/sql-tools/contexts/processor-context';
|
||||||
import { ReaderContext } from 'src/sql-tools/contexts/reader-context';
|
import { ReaderContext } from 'src/sql-tools/contexts/reader-context';
|
||||||
|
import { NamingInterface } from 'src/sql-tools/naming/naming.interface';
|
||||||
import { RegisterItem } from 'src/sql-tools/register-item';
|
import { RegisterItem } from 'src/sql-tools/register-item';
|
||||||
|
|
||||||
export type BaseContextOptions = {
|
export type BaseContextOptions = {
|
||||||
databaseName?: string;
|
databaseName?: string;
|
||||||
schemaName?: string;
|
schemaName?: string;
|
||||||
overrideTableName?: string;
|
overrideTableName?: string;
|
||||||
|
namingStrategy?: 'default' | 'hash' | NamingInterface;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SchemaFromCodeOptions = BaseContextOptions & {
|
export type SchemaFromCodeOptions = BaseContextOptions & {
|
||||||
@ -386,7 +388,7 @@ export type DatabaseConstraint =
|
|||||||
| DatabaseCheckConstraint;
|
| DatabaseCheckConstraint;
|
||||||
|
|
||||||
export type DatabaseColumn = {
|
export type DatabaseColumn = {
|
||||||
primary?: boolean;
|
primary: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
tableName: string;
|
tableName: string;
|
||||||
comment?: string;
|
comment?: string;
|
||||||
@ -487,11 +489,14 @@ export type SchemaDiff = { reason: string } & (
|
|||||||
| { type: 'TableCreate'; table: DatabaseTable }
|
| { type: 'TableCreate'; table: DatabaseTable }
|
||||||
| { type: 'TableDrop'; tableName: string }
|
| { type: 'TableDrop'; tableName: string }
|
||||||
| { type: 'ColumnAdd'; column: DatabaseColumn }
|
| { type: 'ColumnAdd'; column: DatabaseColumn }
|
||||||
|
| { type: 'ColumnRename'; tableName: string; oldName: string; newName: string }
|
||||||
| { type: 'ColumnAlter'; tableName: string; columnName: string; changes: ColumnChanges }
|
| { type: 'ColumnAlter'; tableName: string; columnName: string; changes: ColumnChanges }
|
||||||
| { type: 'ColumnDrop'; tableName: string; columnName: string }
|
| { type: 'ColumnDrop'; tableName: string; columnName: string }
|
||||||
| { type: 'ConstraintAdd'; constraint: DatabaseConstraint }
|
| { type: 'ConstraintAdd'; constraint: DatabaseConstraint }
|
||||||
|
| { type: 'ConstraintRename'; tableName: string; oldName: string; newName: string }
|
||||||
| { type: 'ConstraintDrop'; tableName: string; constraintName: string }
|
| { type: 'ConstraintDrop'; tableName: string; constraintName: string }
|
||||||
| { type: 'IndexCreate'; index: DatabaseIndex }
|
| { type: 'IndexCreate'; index: DatabaseIndex }
|
||||||
|
| { type: 'IndexRename'; tableName: string; oldName: string; newName: string }
|
||||||
| { type: 'IndexDrop'; indexName: string }
|
| { type: 'IndexDrop'; indexName: string }
|
||||||
| { type: 'TriggerCreate'; trigger: DatabaseTrigger }
|
| { type: 'TriggerCreate'; trigger: DatabaseTrigger }
|
||||||
| { type: 'TriggerDrop'; tableName: string; triggerName: string }
|
| { type: 'TriggerDrop'; tableName: string; triggerName: string }
|
||||||
@ -509,11 +514,15 @@ export type Comparer<T> = {
|
|||||||
onMissing: (source: T) => SchemaDiff[];
|
onMissing: (source: T) => SchemaDiff[];
|
||||||
onExtra: (target: T) => SchemaDiff[];
|
onExtra: (target: T) => SchemaDiff[];
|
||||||
onCompare: CompareFunction<T>;
|
onCompare: CompareFunction<T>;
|
||||||
|
/** if two items have the same key, they are considered identical and can be renamed via `onRename` */
|
||||||
|
getRenameKey?: (item: T) => string;
|
||||||
|
onRename?: (source: T, target: T) => SchemaDiff[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum Reason {
|
export enum Reason {
|
||||||
MissingInSource = 'missing in source',
|
MissingInSource = 'missing in source',
|
||||||
MissingInTarget = 'missing in target',
|
MissingInTarget = 'missing in target',
|
||||||
|
Rename = 'name has changed',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Timestamp = KyselyColumnType<Date, Date | string, Date | string>;
|
export type Timestamp = KyselyColumnType<Date, Date | string, Date | string>;
|
||||||
|
@ -83,7 +83,7 @@ export const schema: DatabaseSchema = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: ConstraintType.UNIQUE,
|
type: ConstraintType.UNIQUE,
|
||||||
name: 'REL_3fcca5cc563abf256fc346e3ff',
|
name: 'UQ_3fcca5cc563abf256fc346e3ff4',
|
||||||
tableName: 'table2',
|
tableName: 'table2',
|
||||||
columnNames: ['parentId'],
|
columnNames: ['parentId'],
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user