mirror of
https://github.com/immich-app/immich.git
synced 2025-12-24 01:11:32 +03:00
refactor: sql-tools (#19717)
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import { asKey } from 'src/sql-tools/helpers';
|
||||
import { ConstraintType, Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processCheckConstraints: Processor = (builder, items) => {
|
||||
for (const {
|
||||
item: { object, options },
|
||||
} of items.filter((item) => item.type === 'checkConstraint')) {
|
||||
const table = builder.getTableByObject(object);
|
||||
if (!table) {
|
||||
builder.warnMissingTable('@Check', object);
|
||||
continue;
|
||||
}
|
||||
|
||||
const tableName = table.name;
|
||||
|
||||
table.constraints.push({
|
||||
type: ConstraintType.CHECK,
|
||||
name: options.name || asCheckConstraintName(tableName, options.expression),
|
||||
tableName,
|
||||
expression: options.expression,
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const asCheckConstraintName = (table: string, expression: string) => asKey('CHK_', table, [expression]);
|
||||
55
server/src/sql-tools/processors/column.processor.ts
Normal file
55
server/src/sql-tools/processors/column.processor.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { ColumnOptions } from 'src/sql-tools/decorators/column.decorator';
|
||||
import { fromColumnValue } from 'src/sql-tools/helpers';
|
||||
import { Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processColumns: Processor = (builder, items) => {
|
||||
for (const {
|
||||
type,
|
||||
item: { object, propertyName, options },
|
||||
} of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) {
|
||||
const table = builder.getTableByObject(object.constructor);
|
||||
if (!table) {
|
||||
builder.warnMissingTable(type === 'column' ? '@Column' : '@ForeignKeyColumn', object, propertyName);
|
||||
continue;
|
||||
}
|
||||
|
||||
const columnName = options.name ?? String(propertyName);
|
||||
const existingColumn = table.columns.find((column) => column.name === columnName);
|
||||
if (existingColumn) {
|
||||
// TODO log warnings if column name is not unique
|
||||
continue;
|
||||
}
|
||||
|
||||
let defaultValue = fromColumnValue(options.default);
|
||||
let nullable = options.nullable ?? false;
|
||||
|
||||
// map `{ default: null }` to `{ nullable: true }`
|
||||
if (defaultValue === null) {
|
||||
nullable = true;
|
||||
defaultValue = undefined;
|
||||
}
|
||||
|
||||
const isEnum = !!(options as ColumnOptions).enum;
|
||||
|
||||
builder.addColumn(
|
||||
table,
|
||||
{
|
||||
name: columnName,
|
||||
tableName: table.name,
|
||||
primary: options.primary ?? false,
|
||||
default: defaultValue,
|
||||
nullable,
|
||||
isArray: (options as ColumnOptions).array ?? false,
|
||||
length: options.length,
|
||||
type: isEnum ? 'enum' : options.type || 'character varying',
|
||||
enumName: isEnum ? (options as ColumnOptions).enum!.name : undefined,
|
||||
comment: options.comment,
|
||||
storage: options.storage,
|
||||
identity: options.identity,
|
||||
synchronize: options.synchronize ?? true,
|
||||
},
|
||||
options,
|
||||
propertyName,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { fromColumnValue } from 'src/sql-tools/helpers';
|
||||
import { Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processConfigurationParameters: Processor = (builder, items) => {
|
||||
for (const {
|
||||
item: { options },
|
||||
} of items.filter((item) => item.type === 'configurationParameter')) {
|
||||
builder.parameters.push({
|
||||
databaseName: builder.databaseName,
|
||||
name: options.name,
|
||||
value: fromColumnValue(options.value),
|
||||
scope: options.scope,
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
};
|
||||
10
server/src/sql-tools/processors/database.processor.ts
Normal file
10
server/src/sql-tools/processors/database.processor.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { asSnakeCase } from 'src/sql-tools/helpers';
|
||||
import { Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processDatabases: Processor = (builder, items) => {
|
||||
for (const {
|
||||
item: { object, options },
|
||||
} of items.filter((item) => item.type === 'database')) {
|
||||
builder.databaseName = options.name || asSnakeCase(object.name);
|
||||
}
|
||||
};
|
||||
8
server/src/sql-tools/processors/enum.processor.ts
Normal file
8
server/src/sql-tools/processors/enum.processor.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processEnums: Processor = (builder, items) => {
|
||||
for (const { item } of items.filter((item) => item.type === 'enum')) {
|
||||
// TODO log warnings if enum name is not unique
|
||||
builder.enums.push(item);
|
||||
}
|
||||
};
|
||||
12
server/src/sql-tools/processors/extension.processor.ts
Normal file
12
server/src/sql-tools/processors/extension.processor.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processExtensions: Processor = (builder, items) => {
|
||||
for (const {
|
||||
item: { options },
|
||||
} of items.filter((item) => item.type === 'extension')) {
|
||||
builder.extensions.push({
|
||||
name: options.name,
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { asForeignKeyConstraintName, asKey } from 'src/sql-tools/helpers';
|
||||
import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processForeignKeyColumns: Processor = (builder, items) => {
|
||||
for (const {
|
||||
item: { object, propertyName, options, target },
|
||||
} of items.filter((item) => item.type === 'foreignKeyColumn')) {
|
||||
const { table, column } = builder.getColumnByObjectAndPropertyName(object, propertyName);
|
||||
if (!table) {
|
||||
builder.warnMissingTable('@ForeignKeyColumn', object);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!column) {
|
||||
// should be impossible since they are pre-created in `column.processor.ts`
|
||||
builder.warnMissingColumn('@ForeignKeyColumn', object, propertyName);
|
||||
continue;
|
||||
}
|
||||
|
||||
const referenceTable = builder.getTableByObject(target());
|
||||
if (!referenceTable) {
|
||||
builder.warnMissingTable('@ForeignKeyColumn', object, propertyName);
|
||||
continue;
|
||||
}
|
||||
|
||||
const columnNames = [column.name];
|
||||
const referenceColumns = referenceTable.columns.filter((column) => column.primary);
|
||||
|
||||
// infer FK column type from reference table
|
||||
if (referenceColumns.length === 1) {
|
||||
column.type = referenceColumns[0].type;
|
||||
}
|
||||
|
||||
const referenceColumnNames = referenceColumns.map((column) => column.name);
|
||||
const name = options.constraintName || asForeignKeyConstraintName(table.name, columnNames);
|
||||
|
||||
table.constraints.push({
|
||||
name,
|
||||
tableName: table.name,
|
||||
columnNames,
|
||||
type: ConstraintType.FOREIGN_KEY,
|
||||
referenceTableName: referenceTable.name,
|
||||
referenceColumnNames,
|
||||
onUpdate: options.onUpdate as ActionType,
|
||||
onDelete: options.onDelete as ActionType,
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
|
||||
if (options.unique || options.uniqueConstraintName) {
|
||||
table.constraints.push({
|
||||
name: options.uniqueConstraintName || asRelationKeyConstraintName(table.name, columnNames),
|
||||
tableName: table.name,
|
||||
columnNames,
|
||||
type: ConstraintType.UNIQUE,
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const asRelationKeyConstraintName = (table: string, columns: string[]) => asKey('REL_', table, columns);
|
||||
@@ -0,0 +1,80 @@
|
||||
import { asForeignKeyConstraintName } from 'src/sql-tools/helpers';
|
||||
import { ActionType, ConstraintType, Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processForeignKeyConstraints: Processor = (builder, items, config) => {
|
||||
for (const {
|
||||
item: { object, options },
|
||||
} of items.filter((item) => item.type === 'foreignKeyConstraint')) {
|
||||
const table = builder.getTableByObject(object);
|
||||
if (!table) {
|
||||
builder.warnMissingTable('@ForeignKeyConstraint', { name: 'referenceTable' });
|
||||
continue;
|
||||
}
|
||||
|
||||
const referenceTable = builder.getTableByObject(options.referenceTable());
|
||||
if (!referenceTable) {
|
||||
const referenceTableName = options.referenceTable()?.name;
|
||||
builder.warn(
|
||||
'@ForeignKeyConstraint.referenceTable',
|
||||
`Unable to find table` + (referenceTableName ? ` (${referenceTableName})` : ''),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let missingColumn = false;
|
||||
|
||||
for (const columnName of options.columns) {
|
||||
if (!table.columns.some(({ name }) => name === columnName)) {
|
||||
const metadata = builder.getTableMetadata(table);
|
||||
builder.warn('@ForeignKeyConstraint.columns', `Unable to find column (${metadata.object.name}.${columnName})`);
|
||||
missingColumn = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const columnName of options.referenceColumns || []) {
|
||||
if (!referenceTable.columns.some(({ name }) => name === columnName)) {
|
||||
const metadata = builder.getTableMetadata(referenceTable);
|
||||
builder.warn(
|
||||
'@ForeignKeyConstraint.referenceColumns',
|
||||
`Unable to find column (${metadata.object.name}.${columnName})`,
|
||||
);
|
||||
missingColumn = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (missingColumn) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const referenceColumns =
|
||||
options.referenceColumns || referenceTable.columns.filter(({ primary }) => primary).map(({ name }) => name);
|
||||
|
||||
const name = options.name || asForeignKeyConstraintName(table.name, options.columns);
|
||||
|
||||
table.constraints.push({
|
||||
type: ConstraintType.FOREIGN_KEY,
|
||||
name,
|
||||
tableName: table.name,
|
||||
columnNames: options.columns,
|
||||
referenceTableName: referenceTable.name,
|
||||
referenceColumnNames: referenceColumns,
|
||||
onUpdate: options.onUpdate as ActionType,
|
||||
onDelete: options.onDelete as ActionType,
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
|
||||
if (options.index === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (options.index || options.indexName || config.createForeignKeyIndexes) {
|
||||
table.indexes.push({
|
||||
name: options.indexName || builder.asIndexName(table.name, options.columns),
|
||||
tableName: table.name,
|
||||
columnNames: options.columns,
|
||||
unique: false,
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
8
server/src/sql-tools/processors/function.processor.ts
Normal file
8
server/src/sql-tools/processors/function.processor.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processFunctions: Processor = (builder, items) => {
|
||||
for (const { item } of items.filter((item) => item.type === 'function')) {
|
||||
// TODO log warnings if function name is not unique
|
||||
builder.functions.push(item);
|
||||
}
|
||||
};
|
||||
74
server/src/sql-tools/processors/index.processor.ts
Normal file
74
server/src/sql-tools/processors/index.processor.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processIndexes: Processor = (builder, items, config) => {
|
||||
for (const {
|
||||
item: { object, options },
|
||||
} of items.filter((item) => item.type === 'index')) {
|
||||
const table = builder.getTableByObject(object);
|
||||
if (!table) {
|
||||
builder.warnMissingTable('@Check', object);
|
||||
continue;
|
||||
}
|
||||
|
||||
table.indexes.push({
|
||||
name: options.name || builder.asIndexName(table.name, options.columns, options.where),
|
||||
tableName: table.name,
|
||||
unique: options.unique ?? false,
|
||||
expression: options.expression,
|
||||
using: options.using,
|
||||
with: options.with,
|
||||
where: options.where,
|
||||
columnNames: options.columns,
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
|
||||
// column indexes
|
||||
for (const {
|
||||
type,
|
||||
item: { object, propertyName, options },
|
||||
} of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) {
|
||||
const { table, column } = builder.getColumnByObjectAndPropertyName(object, propertyName);
|
||||
if (!table) {
|
||||
builder.warnMissingTable('@Column', object);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!column) {
|
||||
// should be impossible since they are created in `column.processor.ts`
|
||||
builder.warnMissingColumn('@Column', object, propertyName);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (options.index === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isIndexRequested =
|
||||
options.indexName || options.index || (type === 'foreignKeyColumn' && config.createForeignKeyIndexes);
|
||||
if (!isIndexRequested) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const indexName = options.indexName || builder.asIndexName(table.name, [column.name]);
|
||||
|
||||
const isIndexPresent = table.indexes.some((index) => index.name === indexName);
|
||||
if (isIndexPresent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isOnlyPrimaryColumn = options.primary && table.columns.filter(({ primary }) => primary === true).length === 1;
|
||||
if (isOnlyPrimaryColumn) {
|
||||
// will have an index created by the primary key constraint
|
||||
continue;
|
||||
}
|
||||
|
||||
table.indexes.push({
|
||||
name: indexName,
|
||||
tableName: table.name,
|
||||
unique: false,
|
||||
columnNames: [column.name],
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
};
|
||||
32
server/src/sql-tools/processors/index.ts
Normal file
32
server/src/sql-tools/processors/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { processCheckConstraints } from 'src/sql-tools/processors/check-constraint.processor';
|
||||
import { processColumns } from 'src/sql-tools/processors/column.processor';
|
||||
import { processConfigurationParameters } from 'src/sql-tools/processors/configuration-parameter.processor';
|
||||
import { processDatabases } from 'src/sql-tools/processors/database.processor';
|
||||
import { processEnums } from 'src/sql-tools/processors/enum.processor';
|
||||
import { processExtensions } from 'src/sql-tools/processors/extension.processor';
|
||||
import { processForeignKeyColumns } from 'src/sql-tools/processors/foreign-key-column.processor';
|
||||
import { processForeignKeyConstraints } from 'src/sql-tools/processors/foreign-key-constraint.processor';
|
||||
import { processFunctions } from 'src/sql-tools/processors/function.processor';
|
||||
import { processIndexes } from 'src/sql-tools/processors/index.processor';
|
||||
import { processPrimaryKeyConstraints } from 'src/sql-tools/processors/primary-key-contraint.processor';
|
||||
import { processTables } from 'src/sql-tools/processors/table.processor';
|
||||
import { processTriggers } from 'src/sql-tools/processors/trigger.processor';
|
||||
import { processUniqueConstraints } from 'src/sql-tools/processors/unique-constraint.processor';
|
||||
import { Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processors: Processor[] = [
|
||||
processDatabases,
|
||||
processConfigurationParameters,
|
||||
processEnums,
|
||||
processExtensions,
|
||||
processFunctions,
|
||||
processTables,
|
||||
processColumns,
|
||||
processForeignKeyColumns,
|
||||
processForeignKeyConstraints,
|
||||
processUniqueConstraints,
|
||||
processCheckConstraints,
|
||||
processPrimaryKeyConstraints,
|
||||
processIndexes,
|
||||
processTriggers,
|
||||
];
|
||||
@@ -0,0 +1,27 @@
|
||||
import { asKey } from 'src/sql-tools/helpers';
|
||||
import { ConstraintType, Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processPrimaryKeyConstraints: Processor = (builder) => {
|
||||
for (const table of builder.tables) {
|
||||
const columnNames: string[] = [];
|
||||
|
||||
for (const column of table.columns) {
|
||||
if (column.primary) {
|
||||
columnNames.push(column.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (columnNames.length > 0) {
|
||||
const tableMetadata = builder.getTableMetadata(table);
|
||||
table.constraints.push({
|
||||
type: ConstraintType.PRIMARY_KEY,
|
||||
name: tableMetadata.options.primaryConstraintName || asPrimaryKeyConstraintName(table.name, columnNames),
|
||||
tableName: table.name,
|
||||
columnNames,
|
||||
synchronize: tableMetadata.options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const asPrimaryKeyConstraintName = (table: string, columns: string[]) => asKey('PK_', table, columns);
|
||||
28
server/src/sql-tools/processors/table.processor.ts
Normal file
28
server/src/sql-tools/processors/table.processor.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { asSnakeCase } from 'src/sql-tools/helpers';
|
||||
import { Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processTables: Processor = (builder, items) => {
|
||||
for (const {
|
||||
item: { options, object },
|
||||
} of items.filter((item) => item.type === 'table')) {
|
||||
const test = builder.getTableByObject(object);
|
||||
if (test) {
|
||||
throw new Error(
|
||||
`Table ${test.name} has already been registered. Does ${object.name} have two @Table() decorators?`,
|
||||
);
|
||||
}
|
||||
|
||||
builder.addTable(
|
||||
{
|
||||
name: options.name || asSnakeCase(object.name),
|
||||
columns: [],
|
||||
constraints: [],
|
||||
indexes: [],
|
||||
triggers: [],
|
||||
synchronize: options.synchronize ?? true,
|
||||
},
|
||||
options,
|
||||
object,
|
||||
);
|
||||
}
|
||||
};
|
||||
31
server/src/sql-tools/processors/trigger.processor.ts
Normal file
31
server/src/sql-tools/processors/trigger.processor.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { TriggerOptions } from 'src/sql-tools/decorators/trigger.decorator';
|
||||
import { asKey } from 'src/sql-tools/helpers';
|
||||
import { Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processTriggers: Processor = (builder, items) => {
|
||||
for (const {
|
||||
item: { object, options },
|
||||
} of items.filter((item) => item.type === 'trigger')) {
|
||||
const table = builder.getTableByObject(object);
|
||||
if (!table) {
|
||||
builder.warnMissingTable('@Trigger', object);
|
||||
continue;
|
||||
}
|
||||
|
||||
table.triggers.push({
|
||||
name: options.name || asTriggerName(table.name, options),
|
||||
tableName: table.name,
|
||||
timing: options.timing,
|
||||
actions: options.actions,
|
||||
when: options.when,
|
||||
scope: options.scope,
|
||||
referencingNewTableAs: options.referencingNewTableAs,
|
||||
referencingOldTableAs: options.referencingOldTableAs,
|
||||
functionName: options.functionName,
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const asTriggerName = (table: string, trigger: TriggerOptions) =>
|
||||
asKey('TR_', table, [...trigger.actions, trigger.scope, trigger.timing, trigger.functionName]);
|
||||
@@ -0,0 +1,55 @@
|
||||
import { asKey } from 'src/sql-tools/helpers';
|
||||
import { ConstraintType, Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processUniqueConstraints: Processor = (builder, items) => {
|
||||
for (const {
|
||||
item: { object, options },
|
||||
} of items.filter((item) => item.type === 'uniqueConstraint')) {
|
||||
const table = builder.getTableByObject(object);
|
||||
if (!table) {
|
||||
builder.warnMissingTable('@Unique', object);
|
||||
continue;
|
||||
}
|
||||
|
||||
const tableName = table.name;
|
||||
const columnNames = options.columns;
|
||||
|
||||
table.constraints.push({
|
||||
type: ConstraintType.UNIQUE,
|
||||
name: options.name || asUniqueConstraintName(tableName, columnNames),
|
||||
tableName,
|
||||
columnNames,
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
|
||||
// column level constraints
|
||||
for (const {
|
||||
type,
|
||||
item: { object, propertyName, options },
|
||||
} of items.filter((item) => item.type === 'column' || item.type === 'foreignKeyColumn')) {
|
||||
const { table, column } = builder.getColumnByObjectAndPropertyName(object, propertyName);
|
||||
if (!table) {
|
||||
builder.warnMissingTable('@Column', object);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!column) {
|
||||
// should be impossible since they are created in `column.processor.ts`
|
||||
builder.warnMissingColumn('@Column', object, propertyName);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type === 'column' && !options.primary && (options.unique || options.uniqueConstraintName)) {
|
||||
table.constraints.push({
|
||||
type: ConstraintType.UNIQUE,
|
||||
name: options.uniqueConstraintName || asUniqueConstraintName(table.name, [column.name]),
|
||||
tableName: table.name,
|
||||
columnNames: [column.name],
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const asUniqueConstraintName = (table: string, columns: string[]) => asKey('UQ_', table, columns);
|
||||
Reference in New Issue
Block a user