mirror of https://github.com/immich-app/immich.git
refactor: sql-tools (#19717)
parent
484529e61e
commit
6044663e26
@ -1,47 +1,33 @@
|
||||
import { compareColumns } from 'src/sql-tools/diff/comparers/column.comparer';
|
||||
import { compareConstraints } from 'src/sql-tools/diff/comparers/constraint.comparer';
|
||||
import { compareIndexes } from 'src/sql-tools/diff/comparers/index.comparer';
|
||||
import { compareTriggers } from 'src/sql-tools/diff/comparers/trigger.comparer';
|
||||
import { compareColumns } from 'src/sql-tools/comparers/column.comparer';
|
||||
import { compareConstraints } from 'src/sql-tools/comparers/constraint.comparer';
|
||||
import { compareIndexes } from 'src/sql-tools/comparers/index.comparer';
|
||||
import { compareTriggers } from 'src/sql-tools/comparers/trigger.comparer';
|
||||
import { compare } from 'src/sql-tools/helpers';
|
||||
import { Comparer, DatabaseTable, Reason, SchemaDiff } from 'src/sql-tools/types';
|
||||
|
||||
const newTable = (name: string) => ({
|
||||
name,
|
||||
columns: [],
|
||||
indexes: [],
|
||||
constraints: [],
|
||||
triggers: [],
|
||||
synchronize: true,
|
||||
});
|
||||
|
||||
export const compareTables: Comparer<DatabaseTable> = {
|
||||
onMissing: (source) => [
|
||||
{
|
||||
type: 'table.create',
|
||||
type: 'TableCreate',
|
||||
table: source,
|
||||
reason: Reason.MissingInTarget,
|
||||
},
|
||||
// TODO merge constraints into table create record when possible
|
||||
...compareTable(
|
||||
source,
|
||||
{
|
||||
name: source.name,
|
||||
columns: [],
|
||||
indexes: [],
|
||||
constraints: [],
|
||||
triggers: [],
|
||||
synchronize: true,
|
||||
},
|
||||
|
||||
{ columns: false },
|
||||
),
|
||||
...compareTable(source, newTable(source.name), { columns: false }),
|
||||
],
|
||||
onExtra: (target) => [
|
||||
...compareTable(
|
||||
{
|
||||
name: target.name,
|
||||
columns: [],
|
||||
indexes: [],
|
||||
constraints: [],
|
||||
triggers: [],
|
||||
synchronize: true,
|
||||
},
|
||||
target,
|
||||
{ columns: false },
|
||||
),
|
||||
...compareTable(newTable(target.name), target, { columns: false }),
|
||||
{
|
||||
type: 'table.drop',
|
||||
type: 'TableDrop',
|
||||
tableName: target.name,
|
||||
reason: Reason.MissingInSource,
|
||||
},
|
||||
@ -1,4 +1,4 @@
|
||||
import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/from-code/decorators/trigger-function.decorator';
|
||||
import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/decorators/trigger-function.decorator';
|
||||
|
||||
export const AfterDeleteTrigger = (options: Omit<TriggerFunctionOptions, 'timing' | 'actions'>) =>
|
||||
TriggerFunction({
|
||||
@ -1,4 +1,4 @@
|
||||
import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/from-code/decorators/trigger-function.decorator';
|
||||
import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/decorators/trigger-function.decorator';
|
||||
|
||||
export const AfterInsertTrigger = (options: Omit<TriggerFunctionOptions, 'timing' | 'actions'>) =>
|
||||
TriggerFunction({
|
||||
@ -1,4 +1,4 @@
|
||||
import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/from-code/decorators/trigger-function.decorator';
|
||||
import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/decorators/trigger-function.decorator';
|
||||
|
||||
export const BeforeUpdateTrigger = (options: Omit<TriggerFunctionOptions, 'timing' | 'actions'>) =>
|
||||
TriggerFunction({
|
||||
@ -1,4 +1,4 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { register } from 'src/sql-tools/register';
|
||||
|
||||
export type CheckOptions = {
|
||||
name?: string;
|
||||
@ -1,5 +1,5 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { asOptions } from 'src/sql-tools/helpers';
|
||||
import { register } from 'src/sql-tools/register';
|
||||
import { ColumnStorage, ColumnType, DatabaseEnum } from 'src/sql-tools/types';
|
||||
|
||||
export type ColumnValue = null | boolean | string | number | object | Date | (() => string);
|
||||
@ -1,5 +1,5 @@
|
||||
import { ColumnValue } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { ColumnValue } from 'src/sql-tools/decorators/column.decorator';
|
||||
import { register } from 'src/sql-tools/register';
|
||||
import { ParameterScope } from 'src/sql-tools/types';
|
||||
|
||||
export type ConfigurationParameterOptions = {
|
||||
@ -1,4 +1,4 @@
|
||||
import { Column, ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator';
|
||||
|
||||
export const CreateDateColumn = (options: ColumnOptions = {}): PropertyDecorator => {
|
||||
return Column({
|
||||
@ -1,4 +1,4 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { register } from 'src/sql-tools/register';
|
||||
|
||||
export type DatabaseOptions = {
|
||||
name?: string;
|
||||
@ -1,4 +1,4 @@
|
||||
import { Column, ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator';
|
||||
|
||||
export const DeleteDateColumn = (options: ColumnOptions = {}): PropertyDecorator => {
|
||||
return Column({
|
||||
@ -1,5 +1,5 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { asOptions } from 'src/sql-tools/helpers';
|
||||
import { register } from 'src/sql-tools/register';
|
||||
|
||||
export type ExtensionOptions = {
|
||||
name: string;
|
||||
@ -1,5 +1,5 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { asOptions } from 'src/sql-tools/helpers';
|
||||
import { register } from 'src/sql-tools/register';
|
||||
|
||||
export type ExtensionsOptions = {
|
||||
name: string;
|
||||
@ -0,0 +1,16 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
||||
import { ForeignKeyAction } from 'src/sql-tools//decorators/foreign-key-constraint.decorator';
|
||||
import { ColumnBaseOptions } from 'src/sql-tools/decorators/column.decorator';
|
||||
import { register } from 'src/sql-tools/register';
|
||||
|
||||
export type ForeignKeyColumnOptions = ColumnBaseOptions & {
|
||||
onUpdate?: ForeignKeyAction;
|
||||
onDelete?: ForeignKeyAction;
|
||||
constraintName?: string;
|
||||
};
|
||||
|
||||
export const ForeignKeyColumn = (target: () => Function, options: ForeignKeyColumnOptions): PropertyDecorator => {
|
||||
return (object: object, propertyName: string | symbol) => {
|
||||
register({ type: 'foreignKeyColumn', item: { object, propertyName, options, target } });
|
||||
};
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { register } from 'src/sql-tools/register';
|
||||
|
||||
export type ForeignKeyAction = 'CASCADE' | 'SET NULL' | 'SET DEFAULT' | 'RESTRICT' | 'NO ACTION';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Column, ColumnOptions, ColumnValue } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
import { Column, ColumnOptions, ColumnValue } from 'src/sql-tools/decorators/column.decorator';
|
||||
import { ColumnType } from 'src/sql-tools/types';
|
||||
|
||||
export type GeneratedColumnStrategy = 'uuid' | 'identity';
|
||||
@ -1,5 +1,5 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { asOptions } from 'src/sql-tools/helpers';
|
||||
import { register } from 'src/sql-tools/register';
|
||||
|
||||
export type IndexOptions = {
|
||||
name?: string;
|
||||
@ -0,0 +1,22 @@
|
||||
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';
|
||||
@ -1,3 +1,3 @@
|
||||
import { Column, ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator';
|
||||
|
||||
export const PrimaryColumn = (options: Omit<ColumnOptions, 'primary'> = {}) => Column({ ...options, primary: true });
|
||||
@ -1,4 +1,4 @@
|
||||
import { GenerateColumnOptions, GeneratedColumn } from 'src/sql-tools/from-code/decorators/generated-column.decorator';
|
||||
import { GenerateColumnOptions, GeneratedColumn } from 'src/sql-tools/decorators/generated-column.decorator';
|
||||
|
||||
export const PrimaryGeneratedColumn = (options: Omit<GenerateColumnOptions, 'primary'> = {}) =>
|
||||
GeneratedColumn({ ...options, primary: true });
|
||||
@ -1,5 +1,5 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { asOptions } from 'src/sql-tools/helpers';
|
||||
import { register } from 'src/sql-tools/register';
|
||||
|
||||
export type TableOptions = {
|
||||
name?: string;
|
||||
@ -1,4 +1,4 @@
|
||||
import { Trigger, TriggerOptions } from 'src/sql-tools/from-code/decorators/trigger.decorator';
|
||||
import { Trigger, TriggerOptions } from 'src/sql-tools/decorators/trigger.decorator';
|
||||
import { DatabaseFunction } from 'src/sql-tools/types';
|
||||
|
||||
export type TriggerFunctionOptions = Omit<TriggerOptions, 'functionName'> & { function: DatabaseFunction };
|
||||
@ -1,4 +1,4 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { register } from 'src/sql-tools/register';
|
||||
import { TriggerAction, TriggerScope, TriggerTiming } from 'src/sql-tools/types';
|
||||
|
||||
export type TriggerOptions = {
|
||||
@ -1,4 +1,4 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { register } from 'src/sql-tools/register';
|
||||
|
||||
export type UniqueOptions = {
|
||||
name?: string;
|
||||
@ -1,4 +1,4 @@
|
||||
import { Column, ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
import { Column, ColumnOptions } from 'src/sql-tools/decorators/column.decorator';
|
||||
|
||||
export const UpdateDateColumn = (options: ColumnOptions = {}): PropertyDecorator => {
|
||||
return Column({
|
||||
@ -1,85 +0,0 @@
|
||||
import { compareEnums } from 'src/sql-tools/diff/comparers/enum.comparer';
|
||||
import { compareExtensions } from 'src/sql-tools/diff/comparers/extension.comparer';
|
||||
import { compareFunctions } from 'src/sql-tools/diff/comparers/function.comparer';
|
||||
import { compareParameters } from 'src/sql-tools/diff/comparers/parameter.comparer';
|
||||
import { compareTables } from 'src/sql-tools/diff/comparers/table.comparer';
|
||||
import { compare } from 'src/sql-tools/helpers';
|
||||
import { schemaDiffToSql } from 'src/sql-tools/to-sql';
|
||||
import {
|
||||
DatabaseConstraintType,
|
||||
DatabaseSchema,
|
||||
SchemaDiff,
|
||||
SchemaDiffOptions,
|
||||
SchemaDiffToSqlOptions,
|
||||
} from 'src/sql-tools/types';
|
||||
|
||||
/**
|
||||
* Compute the difference between two database schemas
|
||||
*/
|
||||
export const schemaDiff = (source: DatabaseSchema, target: DatabaseSchema, options: SchemaDiffOptions = {}) => {
|
||||
const items = [
|
||||
...compare(source.parameters, target.parameters, options.parameters, compareParameters),
|
||||
...compare(source.extensions, target.extensions, options.extension, compareExtensions),
|
||||
...compare(source.functions, target.functions, options.functions, compareFunctions),
|
||||
...compare(source.enums, target.enums, options.enums, compareEnums),
|
||||
...compare(source.tables, target.tables, options.tables, compareTables),
|
||||
];
|
||||
|
||||
type SchemaName = SchemaDiff['type'];
|
||||
const itemMap: Record<SchemaName, SchemaDiff[]> = {
|
||||
'enum.create': [],
|
||||
'enum.drop': [],
|
||||
'extension.create': [],
|
||||
'extension.drop': [],
|
||||
'function.create': [],
|
||||
'function.drop': [],
|
||||
'table.create': [],
|
||||
'table.drop': [],
|
||||
'column.add': [],
|
||||
'column.alter': [],
|
||||
'column.drop': [],
|
||||
'constraint.add': [],
|
||||
'constraint.drop': [],
|
||||
'index.create': [],
|
||||
'index.drop': [],
|
||||
'trigger.create': [],
|
||||
'trigger.drop': [],
|
||||
'parameter.set': [],
|
||||
'parameter.reset': [],
|
||||
};
|
||||
|
||||
for (const item of items) {
|
||||
itemMap[item.type].push(item);
|
||||
}
|
||||
|
||||
const constraintAdds = itemMap['constraint.add'].filter((item) => item.type === 'constraint.add');
|
||||
|
||||
const orderedItems = [
|
||||
...itemMap['extension.create'],
|
||||
...itemMap['function.create'],
|
||||
...itemMap['parameter.set'],
|
||||
...itemMap['parameter.reset'],
|
||||
...itemMap['enum.create'],
|
||||
...itemMap['trigger.drop'],
|
||||
...itemMap['index.drop'],
|
||||
...itemMap['constraint.drop'],
|
||||
...itemMap['table.create'],
|
||||
...itemMap['column.alter'],
|
||||
...itemMap['column.add'],
|
||||
...constraintAdds.filter(({ constraint }) => constraint.type === DatabaseConstraintType.PRIMARY_KEY),
|
||||
...constraintAdds.filter(({ constraint }) => constraint.type === DatabaseConstraintType.FOREIGN_KEY),
|
||||
...constraintAdds.filter(({ constraint }) => constraint.type === DatabaseConstraintType.UNIQUE),
|
||||
...constraintAdds.filter(({ constraint }) => constraint.type === DatabaseConstraintType.CHECK),
|
||||
...itemMap['index.create'],
|
||||
...itemMap['trigger.create'],
|
||||
...itemMap['column.drop'],
|
||||
...itemMap['table.drop'],
|
||||
...itemMap['enum.drop'],
|
||||
...itemMap['function.drop'],
|
||||
];
|
||||
|
||||
return {
|
||||
items: orderedItems,
|
||||
asSql: (options?: SchemaDiffToSqlOptions) => schemaDiffToSql(orderedItems, options),
|
||||
};
|
||||
};
|
||||
@ -1,15 +0,0 @@
|
||||
import { ColumnBaseOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
import { ForeignKeyAction } from 'src/sql-tools/from-code/decorators/foreign-key-constraint.decorator';
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
|
||||
export type ForeignKeyColumnOptions = ColumnBaseOptions & {
|
||||
onUpdate?: ForeignKeyAction;
|
||||
onDelete?: ForeignKeyAction;
|
||||
constraintName?: string;
|
||||
};
|
||||
|
||||
export const ForeignKeyColumn = (target: () => object, options: ForeignKeyColumnOptions): PropertyDecorator => {
|
||||
return (object: object, propertyName: string | symbol) => {
|
||||
register({ type: 'foreignKeyColumn', item: { object, propertyName, options, target } });
|
||||
};
|
||||
};
|
||||
@ -1,36 +0,0 @@
|
||||
import { readdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { reset, schemaFromCode } from 'src/sql-tools/from-code';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe(schemaFromCode.name, () => {
|
||||
beforeEach(() => {
|
||||
reset();
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
expect(schemaFromCode()).toEqual({
|
||||
name: 'postgres',
|
||||
schemaName: 'public',
|
||||
functions: [],
|
||||
enums: [],
|
||||
extensions: [],
|
||||
parameters: [],
|
||||
tables: [],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
describe('test files', () => {
|
||||
const files = readdirSync('test/sql-tools', { withFileTypes: true });
|
||||
for (const file of files) {
|
||||
const filePath = join(file.parentPath, file.name);
|
||||
it(filePath, async () => {
|
||||
const module = await import(filePath);
|
||||
expect(module.description).toBeDefined();
|
||||
expect(module.schema).toBeDefined();
|
||||
expect(schemaFromCode(), module.description).toEqual(module.schema);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -1,77 +0,0 @@
|
||||
import 'reflect-metadata';
|
||||
import { processCheckConstraints } from 'src/sql-tools/from-code/processors/check-constraint.processor';
|
||||
import { processColumns } from 'src/sql-tools/from-code/processors/column.processor';
|
||||
import { processConfigurationParameters } from 'src/sql-tools/from-code/processors/configuration-parameter.processor';
|
||||
import { processDatabases } from 'src/sql-tools/from-code/processors/database.processor';
|
||||
import { processEnums } from 'src/sql-tools/from-code/processors/enum.processor';
|
||||
import { processExtensions } from 'src/sql-tools/from-code/processors/extension.processor';
|
||||
import { processForeignKeyColumns } from 'src/sql-tools/from-code/processors/foreign-key-column.processor';
|
||||
import { processForeignKeyConstraints } from 'src/sql-tools/from-code/processors/foreign-key-constraint.processor';
|
||||
import { processFunctions } from 'src/sql-tools/from-code/processors/function.processor';
|
||||
import { processIndexes } from 'src/sql-tools/from-code/processors/index.processor';
|
||||
import { processPrimaryKeyConstraints } from 'src/sql-tools/from-code/processors/primary-key-contraint.processor';
|
||||
import { processTables } from 'src/sql-tools/from-code/processors/table.processor';
|
||||
import { processTriggers } from 'src/sql-tools/from-code/processors/trigger.processor';
|
||||
import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type';
|
||||
import { processUniqueConstraints } from 'src/sql-tools/from-code/processors/unique-constraint.processor';
|
||||
import { getRegisteredItems, resetRegisteredItems } from 'src/sql-tools/from-code/register';
|
||||
import { DatabaseSchema } from 'src/sql-tools/types';
|
||||
|
||||
let initialized = false;
|
||||
let schema: DatabaseSchema;
|
||||
|
||||
export const reset = () => {
|
||||
initialized = false;
|
||||
resetRegisteredItems();
|
||||
};
|
||||
|
||||
const processors: Processor[] = [
|
||||
processDatabases,
|
||||
processConfigurationParameters,
|
||||
processEnums,
|
||||
processExtensions,
|
||||
processFunctions,
|
||||
processTables,
|
||||
processColumns,
|
||||
processForeignKeyColumns,
|
||||
processForeignKeyConstraints,
|
||||
processUniqueConstraints,
|
||||
processCheckConstraints,
|
||||
processPrimaryKeyConstraints,
|
||||
processIndexes,
|
||||
processTriggers,
|
||||
];
|
||||
|
||||
export type SchemaFromCodeOptions = {
|
||||
/** automatically create indexes on foreign key columns */
|
||||
createForeignKeyIndexes?: boolean;
|
||||
};
|
||||
export const schemaFromCode = (options: SchemaFromCodeOptions = {}) => {
|
||||
if (!initialized) {
|
||||
const globalOptions = {
|
||||
createForeignKeyIndexes: options.createForeignKeyIndexes ?? true,
|
||||
};
|
||||
|
||||
const builder: SchemaBuilder = {
|
||||
name: 'postgres',
|
||||
schemaName: 'public',
|
||||
tables: [],
|
||||
functions: [],
|
||||
enums: [],
|
||||
extensions: [],
|
||||
parameters: [],
|
||||
warnings: [],
|
||||
};
|
||||
|
||||
const items = getRegisteredItems();
|
||||
|
||||
for (const processor of processors) {
|
||||
processor(builder, items, globalOptions);
|
||||
}
|
||||
|
||||
schema = { ...builder, tables: builder.tables.map(({ metadata: _, ...table }) => table) };
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
return schema;
|
||||
};
|
||||
@ -1,93 +0,0 @@
|
||||
import { ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||
import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type';
|
||||
import { addWarning, asMetadataKey, fromColumnValue } from 'src/sql-tools/helpers';
|
||||
import { DatabaseColumn } 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 = resolveTable(builder, object.constructor);
|
||||
if (!table) {
|
||||
onMissingTable(builder, 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;
|
||||
}
|
||||
|
||||
const tableName = table.name;
|
||||
|
||||
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;
|
||||
|
||||
const column: DatabaseColumn = {
|
||||
name: columnName,
|
||||
tableName,
|
||||
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,
|
||||
};
|
||||
|
||||
writeMetadata(object, propertyName, { name: column.name, options });
|
||||
|
||||
table.columns.push(column);
|
||||
}
|
||||
};
|
||||
|
||||
type ColumnMetadata = { name: string; options: ColumnOptions };
|
||||
|
||||
export const resolveColumn = (builder: SchemaBuilder, object: object, propertyName: string | symbol) => {
|
||||
const table = resolveTable(builder, object.constructor);
|
||||
if (!table) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const metadata = readMetadata(object, propertyName);
|
||||
if (!metadata) {
|
||||
return { table };
|
||||
}
|
||||
|
||||
const column = table.columns.find((column) => column.name === metadata.name);
|
||||
return { table, column };
|
||||
};
|
||||
|
||||
export const onMissingColumn = (
|
||||
builder: SchemaBuilder,
|
||||
context: string,
|
||||
object: object,
|
||||
propertyName?: symbol | string,
|
||||
) => {
|
||||
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
|
||||
addWarning(builder, context, `Unable to find column (${label})`);
|
||||
};
|
||||
|
||||
const METADATA_KEY = asMetadataKey('table-metadata');
|
||||
|
||||
const writeMetadata = (object: object, propertyName: symbol | string, metadata: ColumnMetadata) =>
|
||||
Reflect.defineMetadata(METADATA_KEY, metadata, object, propertyName);
|
||||
|
||||
const readMetadata = (object: object, propertyName: symbol | string): ColumnMetadata | undefined =>
|
||||
Reflect.getMetadata(METADATA_KEY, object, propertyName);
|
||||
@ -1,58 +0,0 @@
|
||||
import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator';
|
||||
import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type';
|
||||
import { addWarning, asMetadataKey, asSnakeCase } from 'src/sql-tools/helpers';
|
||||
|
||||
export const processTables: Processor = (builder, items) => {
|
||||
for (const {
|
||||
item: { options, object },
|
||||
} of items.filter((item) => item.type === 'table')) {
|
||||
const test = readMetadata(object);
|
||||
if (test) {
|
||||
throw new Error(
|
||||
`Table ${test.name} has already been registered. Does ${object.name} have two @Table() decorators?`,
|
||||
);
|
||||
}
|
||||
|
||||
const tableName = options.name || asSnakeCase(object.name);
|
||||
|
||||
writeMetadata(object, { name: tableName, options });
|
||||
|
||||
builder.tables.push({
|
||||
name: tableName,
|
||||
columns: [],
|
||||
constraints: [],
|
||||
indexes: [],
|
||||
triggers: [],
|
||||
synchronize: options.synchronize ?? true,
|
||||
metadata: { options, object },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const resolveTable = (builder: SchemaBuilder, object: object) => {
|
||||
const metadata = readMetadata(object);
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
return builder.tables.find((table) => table.name === metadata.name);
|
||||
};
|
||||
|
||||
export const onMissingTable = (
|
||||
builder: SchemaBuilder,
|
||||
context: string,
|
||||
object: object,
|
||||
propertyName?: symbol | string,
|
||||
) => {
|
||||
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
|
||||
addWarning(builder, context, `Unable to find table (${label})`);
|
||||
};
|
||||
|
||||
const METADATA_KEY = asMetadataKey('table-metadata');
|
||||
|
||||
type TableMetadata = { name: string; options: TableOptions };
|
||||
|
||||
const readMetadata = (object: object): TableMetadata | undefined => Reflect.getMetadata(METADATA_KEY, object);
|
||||
|
||||
const writeMetadata = (object: object, metadata: TableMetadata): void =>
|
||||
Reflect.defineMetadata(METADATA_KEY, metadata, object);
|
||||
@ -1,10 +0,0 @@
|
||||
import { SchemaFromCodeOptions } from 'src/sql-tools/from-code';
|
||||
import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator';
|
||||
import { RegisterItem } from 'src/sql-tools/from-code/register-item';
|
||||
import { DatabaseSchema, DatabaseTable } from 'src/sql-tools/types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
export type TableWithMetadata = DatabaseTable & { metadata: { options: TableOptions; object: Function } };
|
||||
export type SchemaBuilder = Omit<DatabaseSchema, 'tables'> & { tables: TableWithMetadata[] };
|
||||
|
||||
export type Processor = (builder: SchemaBuilder, items: RegisterItem[], options: SchemaFromCodeOptions) => void;
|
||||
@ -1,54 +0,0 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import { Sql } from 'postgres';
|
||||
import { readColumns } from 'src/sql-tools/from-database/readers/column.reader';
|
||||
import { readComments } from 'src/sql-tools/from-database/readers/comment.reader';
|
||||
import { readConstraints } from 'src/sql-tools/from-database/readers/constraint.reader';
|
||||
import { readExtensions } from 'src/sql-tools/from-database/readers/extension.reader';
|
||||
import { readFunctions } from 'src/sql-tools/from-database/readers/function.reader';
|
||||
import { readIndexes } from 'src/sql-tools/from-database/readers/index.reader';
|
||||
import { readName } from 'src/sql-tools/from-database/readers/name.reader';
|
||||
import { readParameters } from 'src/sql-tools/from-database/readers/parameter.reader';
|
||||
import { readTables } from 'src/sql-tools/from-database/readers/table.reader';
|
||||
import { readTriggers } from 'src/sql-tools/from-database/readers/trigger.reader';
|
||||
import { DatabaseReader } from 'src/sql-tools/from-database/readers/type';
|
||||
import { DatabaseSchema, LoadSchemaOptions, PostgresDB } from 'src/sql-tools/types';
|
||||
|
||||
const readers: DatabaseReader[] = [
|
||||
//
|
||||
readName,
|
||||
readParameters,
|
||||
readExtensions,
|
||||
readFunctions,
|
||||
readTables,
|
||||
readColumns,
|
||||
readIndexes,
|
||||
readConstraints,
|
||||
readTriggers,
|
||||
readComments,
|
||||
];
|
||||
|
||||
/**
|
||||
* Load the database schema from the database
|
||||
*/
|
||||
export const schemaFromDatabase = async (postgres: Sql, options: LoadSchemaOptions = {}): Promise<DatabaseSchema> => {
|
||||
const schema: DatabaseSchema = {
|
||||
name: 'immich',
|
||||
schemaName: options.schemaName || 'public',
|
||||
parameters: [],
|
||||
functions: [],
|
||||
enums: [],
|
||||
extensions: [],
|
||||
tables: [],
|
||||
warnings: [],
|
||||
};
|
||||
|
||||
const db = new Kysely<PostgresDB>({ dialect: new PostgresJSDialect({ postgres }) });
|
||||
for (const reader of readers) {
|
||||
await reader(schema, db);
|
||||
}
|
||||
|
||||
await db.destroy();
|
||||
|
||||
return schema;
|
||||
};
|
||||
@ -1,3 +0,0 @@
|
||||
import { DatabaseClient, DatabaseSchema } from 'src/sql-tools/types';
|
||||
|
||||
export type DatabaseReader = (schema: DatabaseSchema, db: DatabaseClient) => Promise<void>;
|
||||
@ -1,22 +1,20 @@
|
||||
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
import { asKey } from 'src/sql-tools/helpers';
|
||||
import { DatabaseConstraintType } from 'src/sql-tools/types';
|
||||
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 = resolveTable(builder, object);
|
||||
const table = builder.getTableByObject(object);
|
||||
if (!table) {
|
||||
onMissingTable(builder, '@Check', object);
|
||||
builder.warnMissingTable('@Check', object);
|
||||
continue;
|
||||
}
|
||||
|
||||
const tableName = table.name;
|
||||
|
||||
table.constraints.push({
|
||||
type: DatabaseConstraintType.CHECK,
|
||||
type: ConstraintType.CHECK,
|
||||
name: options.name || asCheckConstraintName(tableName, options.expression),
|
||||
tableName,
|
||||
expression: options.expression,
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -1,12 +1,12 @@
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
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.name,
|
||||
databaseName: builder.databaseName,
|
||||
name: options.name,
|
||||
value: fromColumnValue(options.value),
|
||||
scope: options.scope,
|
||||
@ -1,10 +1,10 @@
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
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.name = options.name || asSnakeCase(object.name);
|
||||
builder.databaseName = options.name || asSnakeCase(object.name);
|
||||
}
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
import { Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processEnums: Processor = (builder, items) => {
|
||||
for (const { item } of items.filter((item) => item.type === 'enum')) {
|
||||
@ -1,4 +1,4 @@
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
import { Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processExtensions: Processor = (builder, items) => {
|
||||
for (const {
|
||||
@ -1,4 +1,4 @@
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
import { Processor } from 'src/sql-tools/types';
|
||||
|
||||
export const processFunctions: Processor = (builder, items) => {
|
||||
for (const { item } of items.filter((item) => item.type === 'function')) {
|
||||
@ -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,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,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -1,15 +1,14 @@
|
||||
import { TriggerOptions } from 'src/sql-tools/from-code/decorators/trigger.decorator';
|
||||
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
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 = resolveTable(builder, object);
|
||||
const table = builder.getTableByObject(object);
|
||||
if (!table) {
|
||||
onMissingTable(builder, '@Trigger', object);
|
||||
builder.warnMissingTable('@Trigger', object);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -1,29 +1,7 @@
|
||||
export { schemaDiff } from 'src/sql-tools/diff';
|
||||
export { schemaFromCode } from 'src/sql-tools/from-code';
|
||||
export * from 'src/sql-tools/from-code/decorators/after-delete.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/after-insert.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/before-update.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/check.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/configuration-parameter.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/create-date-column.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/database.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/delete-date-column.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/extension.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/extensions.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/foreign-key-column.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/foreign-key-constraint.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/generated-column.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/index.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/primary-column.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/primary-generated-column.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/table.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/trigger-function.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/trigger.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/unique.decorator';
|
||||
export * from 'src/sql-tools/from-code/decorators/update-date-column.decorator';
|
||||
export * from 'src/sql-tools/from-code/register-enum';
|
||||
export * from 'src/sql-tools/from-code/register-function';
|
||||
export { schemaFromDatabase } from 'src/sql-tools/from-database';
|
||||
export { schemaDiffToSql } from 'src/sql-tools/to-sql';
|
||||
export * from 'src/sql-tools/decorators';
|
||||
export * from 'src/sql-tools/register-enum';
|
||||
export * from 'src/sql-tools/register-function';
|
||||
export { schemaDiff, schemaDiffToSql } from 'src/sql-tools/schema-diff';
|
||||
export { schemaFromCode } from 'src/sql-tools/schema-from-code';
|
||||
export { schemaFromDatabase } from 'src/sql-tools/schema-from-database';
|
||||
export * from 'src/sql-tools/types';
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { sql } from 'kysely';
|
||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||
import { DatabaseReader } from 'src/sql-tools/from-database/readers/type';
|
||||
import { ColumnType, DatabaseColumn } from 'src/sql-tools/types';
|
||||
import { ColumnType, DatabaseColumn, DatabaseReader } from 'src/sql-tools/types';
|
||||
|
||||
export const readColumns: DatabaseReader = async (schema, db) => {
|
||||
const columns = await db
|
||||
@ -1,4 +1,4 @@
|
||||
import { DatabaseReader } from 'src/sql-tools/from-database/readers/type';
|
||||
import { DatabaseReader } from 'src/sql-tools/types';
|
||||
|
||||
export const readComments: DatabaseReader = async (schema, db) => {
|
||||
const comments = await db
|
||||
@ -1,4 +1,4 @@
|
||||
import { DatabaseReader } from 'src/sql-tools/from-database/readers/type';
|
||||
import { DatabaseReader } from 'src/sql-tools/types';
|
||||
|
||||
export const readExtensions: DatabaseReader = async (schema, db) => {
|
||||
const extensions = await db
|
||||
@ -1,5 +1,5 @@
|
||||
import { sql } from 'kysely';
|
||||
import { DatabaseReader } from 'src/sql-tools/from-database/readers/type';
|
||||
import { DatabaseReader } from 'src/sql-tools/types';
|
||||
|
||||
export const readFunctions: DatabaseReader = async (schema, db) => {
|
||||
const routines = await db
|
||||
@ -1,5 +1,5 @@
|
||||
import { sql } from 'kysely';
|
||||
import { DatabaseReader } from 'src/sql-tools/from-database/readers/type';
|
||||
import { DatabaseReader } from 'src/sql-tools/types';
|
||||
|
||||
export const readIndexes: DatabaseReader = async (schema, db) => {
|
||||
const indexes = await db
|
||||
@ -0,0 +1,25 @@
|
||||
import { readColumns } from 'src/sql-tools/readers/column.reader';
|
||||
import { readComments } from 'src/sql-tools/readers/comment.reader';
|
||||
import { readConstraints } from 'src/sql-tools/readers/constraint.reader';
|
||||
import { readExtensions } from 'src/sql-tools/readers/extension.reader';
|
||||
import { readFunctions } from 'src/sql-tools/readers/function.reader';
|
||||
import { readIndexes } from 'src/sql-tools/readers/index.reader';
|
||||
import { readName } from 'src/sql-tools/readers/name.reader';
|
||||
import { readParameters } from 'src/sql-tools/readers/parameter.reader';
|
||||
import { readTables } from 'src/sql-tools/readers/table.reader';
|
||||
import { readTriggers } from 'src/sql-tools/readers/trigger.reader';
|
||||
import { DatabaseReader } from 'src/sql-tools/types';
|
||||
|
||||
export const readers: DatabaseReader[] = [
|
||||
//
|
||||
readName,
|
||||
readParameters,
|
||||
readExtensions,
|
||||
readFunctions,
|
||||
readTables,
|
||||
readColumns,
|
||||
readIndexes,
|
||||
readConstraints,
|
||||
readTriggers,
|
||||
readComments,
|
||||
];
|
||||
@ -1,8 +1,8 @@
|
||||
import { QueryResult, sql } from 'kysely';
|
||||
import { DatabaseReader } from 'src/sql-tools/from-database/readers/type';
|
||||
import { DatabaseReader } from 'src/sql-tools/types';
|
||||
|
||||
export const readName: DatabaseReader = async (schema, db) => {
|
||||
const result = (await sql`SELECT current_database() as name`.execute(db)) as QueryResult<{ name: string }>;
|
||||
|
||||
schema.name = result.rows[0].name;
|
||||
schema.databaseName = result.rows[0].name;
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { sql } from 'kysely';
|
||||
import { DatabaseReader } from 'src/sql-tools/from-database/readers/type';
|
||||
import { DatabaseReader } from 'src/sql-tools/types';
|
||||
|
||||
export const readTables: DatabaseReader = async (schema, db) => {
|
||||
const tables = await db
|
||||
@ -1,4 +1,4 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { register } from 'src/sql-tools/register';
|
||||
import { DatabaseEnum } from 'src/sql-tools/types';
|
||||
|
||||
export type EnumOptions = {
|
||||
@ -1,4 +1,4 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { register } from 'src/sql-tools/register';
|
||||
import { ColumnType, DatabaseFunction } from 'src/sql-tools/types';
|
||||
|
||||
export type FunctionOptions = {
|
||||
@ -1,4 +1,4 @@
|
||||
import { RegisterItem } from 'src/sql-tools/from-code/register-item';
|
||||
import { RegisterItem } from 'src/sql-tools/register-item';
|
||||
|
||||
const items: RegisterItem[] = [];
|
||||
|
||||
@ -0,0 +1,121 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
||||
import { ColumnOptions, TableOptions } from 'src/sql-tools/decorators';
|
||||
import { asKey } from 'src/sql-tools/helpers';
|
||||
import {
|
||||
DatabaseColumn,
|
||||
DatabaseEnum,
|
||||
DatabaseExtension,
|
||||
DatabaseFunction,
|
||||
DatabaseParameter,
|
||||
DatabaseSchema,
|
||||
DatabaseTable,
|
||||
SchemaFromCodeOptions,
|
||||
} from 'src/sql-tools/types';
|
||||
|
||||
type TableMetadata = { options: TableOptions; object: Function; methodToColumn: Map<string | symbol, DatabaseColumn> };
|
||||
|
||||
export class SchemaBuilder {
|
||||
databaseName: string;
|
||||
schemaName: string;
|
||||
tables: DatabaseTable[] = [];
|
||||
functions: DatabaseFunction[] = [];
|
||||
enums: DatabaseEnum[] = [];
|
||||
extensions: DatabaseExtension[] = [];
|
||||
parameters: DatabaseParameter[] = [];
|
||||
warnings: string[] = [];
|
||||
|
||||
classToTable: WeakMap<Function, DatabaseTable> = new WeakMap();
|
||||
tableToMetadata: WeakMap<DatabaseTable, TableMetadata> = new WeakMap();
|
||||
|
||||
constructor(options: SchemaFromCodeOptions) {
|
||||
this.databaseName = options.databaseName ?? 'postgres';
|
||||
this.schemaName = options.schemaName ?? 'public';
|
||||
}
|
||||
|
||||
getTableByObject(object: Function) {
|
||||
return this.classToTable.get(object);
|
||||
}
|
||||
|
||||
getTableByName(name: string) {
|
||||
return this.tables.find((table) => table.name === name);
|
||||
}
|
||||
|
||||
getTableMetadata(table: DatabaseTable) {
|
||||
const metadata = this.tableToMetadata.get(table);
|
||||
if (!metadata) {
|
||||
throw new Error(`Table metadata not found for table: ${table.name}`);
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
addTable(table: DatabaseTable, options: TableOptions, object: Function) {
|
||||
this.tables.push(table);
|
||||
this.classToTable.set(object, table);
|
||||
this.tableToMetadata.set(table, { options, object, methodToColumn: new Map() });
|
||||
}
|
||||
|
||||
getColumnByObjectAndPropertyName(
|
||||
object: object,
|
||||
propertyName: string | symbol,
|
||||
): { table?: DatabaseTable; column?: DatabaseColumn } {
|
||||
const table = this.getTableByObject(object.constructor);
|
||||
if (!table) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const tableMetadata = this.tableToMetadata.get(table);
|
||||
if (!tableMetadata) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const column = tableMetadata.methodToColumn.get(propertyName);
|
||||
|
||||
return { table, column };
|
||||
}
|
||||
|
||||
addColumn(table: DatabaseTable, column: DatabaseColumn, options: ColumnOptions, propertyName: string | symbol) {
|
||||
table.columns.push(column);
|
||||
const tableMetadata = this.getTableMetadata(table);
|
||||
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);
|
||||
}
|
||||
|
||||
warn(context: string, message: string) {
|
||||
this.warnings.push(`[${context}] ${message}`);
|
||||
}
|
||||
|
||||
warnMissingTable(context: string, object: object, propertyName?: symbol | string) {
|
||||
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
|
||||
this.warn(context, `Unable to find table (${label})`);
|
||||
}
|
||||
|
||||
warnMissingColumn(context: string, object: object, propertyName?: symbol | string) {
|
||||
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
|
||||
this.warn(context, `Unable to find column (${label})`);
|
||||
}
|
||||
|
||||
build(): DatabaseSchema {
|
||||
return {
|
||||
databaseName: this.databaseName,
|
||||
schemaName: this.schemaName,
|
||||
tables: this.tables,
|
||||
functions: this.functions,
|
||||
enums: this.enums,
|
||||
extensions: this.extensions,
|
||||
parameters: this.parameters,
|
||||
warnings: this.warnings,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,121 @@
|
||||
import { compareEnums } from 'src/sql-tools/comparers/enum.comparer';
|
||||
import { compareExtensions } from 'src/sql-tools/comparers/extension.comparer';
|
||||
import { compareFunctions } from 'src/sql-tools/comparers/function.comparer';
|
||||
import { compareParameters } from 'src/sql-tools/comparers/parameter.comparer';
|
||||
import { compareTables } from 'src/sql-tools/comparers/table.comparer';
|
||||
import { compare } from 'src/sql-tools/helpers';
|
||||
import { transformers } from 'src/sql-tools/transformers';
|
||||
import {
|
||||
ConstraintType,
|
||||
DatabaseSchema,
|
||||
SchemaDiff,
|
||||
SchemaDiffOptions,
|
||||
SchemaDiffToSqlOptions,
|
||||
} from 'src/sql-tools/types';
|
||||
|
||||
/**
|
||||
* Compute the difference between two database schemas
|
||||
*/
|
||||
export const schemaDiff = (source: DatabaseSchema, target: DatabaseSchema, options: SchemaDiffOptions = {}) => {
|
||||
const items = [
|
||||
...compare(source.parameters, target.parameters, options.parameters, compareParameters),
|
||||
...compare(source.extensions, target.extensions, options.extension, compareExtensions),
|
||||
...compare(source.functions, target.functions, options.functions, compareFunctions),
|
||||
...compare(source.enums, target.enums, options.enums, compareEnums),
|
||||
...compare(source.tables, target.tables, options.tables, compareTables),
|
||||
];
|
||||
|
||||
type SchemaName = SchemaDiff['type'];
|
||||
const itemMap: Record<SchemaName, SchemaDiff[]> = {
|
||||
EnumCreate: [],
|
||||
EnumDrop: [],
|
||||
ExtensionCreate: [],
|
||||
ExtensionDrop: [],
|
||||
FunctionCreate: [],
|
||||
FunctionDrop: [],
|
||||
TableCreate: [],
|
||||
TableDrop: [],
|
||||
ColumnAdd: [],
|
||||
ColumnAlter: [],
|
||||
ColumnDrop: [],
|
||||
ConstraintAdd: [],
|
||||
ConstraintDrop: [],
|
||||
IndexCreate: [],
|
||||
IndexDrop: [],
|
||||
TriggerCreate: [],
|
||||
TriggerDrop: [],
|
||||
ParameterSet: [],
|
||||
ParameterReset: [],
|
||||
};
|
||||
|
||||
for (const item of items) {
|
||||
itemMap[item.type].push(item);
|
||||
}
|
||||
|
||||
const constraintAdds = itemMap.ConstraintAdd.filter((item) => item.type === 'ConstraintAdd');
|
||||
|
||||
const orderedItems = [
|
||||
...itemMap.ExtensionCreate,
|
||||
...itemMap.FunctionCreate,
|
||||
...itemMap.ParameterSet,
|
||||
...itemMap.ParameterReset,
|
||||
...itemMap.EnumCreate,
|
||||
...itemMap.TriggerDrop,
|
||||
...itemMap.IndexDrop,
|
||||
...itemMap.ConstraintDrop,
|
||||
...itemMap.TableCreate,
|
||||
...itemMap.ColumnAlter,
|
||||
...itemMap.ColumnAdd,
|
||||
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.PRIMARY_KEY),
|
||||
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.FOREIGN_KEY),
|
||||
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.UNIQUE),
|
||||
...constraintAdds.filter(({ constraint }) => constraint.type === ConstraintType.CHECK),
|
||||
...itemMap.IndexCreate,
|
||||
...itemMap.TriggerCreate,
|
||||
...itemMap.ColumnDrop,
|
||||
...itemMap.TableDrop,
|
||||
...itemMap.EnumDrop,
|
||||
...itemMap.FunctionDrop,
|
||||
];
|
||||
|
||||
return {
|
||||
items: orderedItems,
|
||||
asSql: (options?: SchemaDiffToSqlOptions) => schemaDiffToSql(orderedItems, options),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert schema diffs into SQL statements
|
||||
*/
|
||||
export const schemaDiffToSql = (items: SchemaDiff[], options: SchemaDiffToSqlOptions = {}): string[] => {
|
||||
return items.flatMap((item) => asSql(item).map((result) => result + withComments(options.comments, item)));
|
||||
};
|
||||
|
||||
const asSql = (item: SchemaDiff): string[] => {
|
||||
for (const transform of transformers) {
|
||||
const result = transform(item);
|
||||
if (!result) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return asArray(result);
|
||||
}
|
||||
|
||||
throw new Error(`Unhandled schema diff type: ${item.type}`);
|
||||
};
|
||||
|
||||
const withComments = (comments: boolean | undefined, item: SchemaDiff): string => {
|
||||
if (!comments) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return ` -- ${item.reason}`;
|
||||
};
|
||||
|
||||
const asArray = <T>(items: T | T[]): T[] => {
|
||||
if (Array.isArray(items)) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return [items];
|
||||
};
|
||||
@ -0,0 +1,46 @@
|
||||
import { readdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { schemaFromCode } from 'src/sql-tools/schema-from-code';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe(schemaFromCode.name, () => {
|
||||
it('should work', () => {
|
||||
expect(schemaFromCode({ reset: true })).toEqual({
|
||||
databaseName: 'postgres',
|
||||
schemaName: 'public',
|
||||
functions: [],
|
||||
enums: [],
|
||||
extensions: [],
|
||||
parameters: [],
|
||||
tables: [],
|
||||
warnings: [],
|
||||
});
|
||||
});
|
||||
|
||||
describe('test files', () => {
|
||||
const errorStubs = readdirSync('test/sql-tools/errors', { withFileTypes: true });
|
||||
for (const file of errorStubs) {
|
||||
const filePath = join(file.parentPath, file.name);
|
||||
it(filePath, async () => {
|
||||
const module = await import(filePath);
|
||||
expect(module.message).toBeDefined();
|
||||
expect(() => schemaFromCode({ reset: true })).toThrowError(module.message);
|
||||
});
|
||||
}
|
||||
|
||||
const stubs = readdirSync('test/sql-tools', { withFileTypes: true });
|
||||
for (const file of stubs) {
|
||||
if (file.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const filePath = join(file.parentPath, file.name);
|
||||
it(filePath, async () => {
|
||||
const module = await import(filePath);
|
||||
expect(module.description).toBeDefined();
|
||||
expect(module.schema).toBeDefined();
|
||||
expect(schemaFromCode({ reset: true }), module.description).toEqual(module.schema);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,29 @@
|
||||
import { processors } from 'src/sql-tools/processors';
|
||||
import { getRegisteredItems, resetRegisteredItems } from 'src/sql-tools/register';
|
||||
import { SchemaBuilder } from 'src/sql-tools/schema-builder';
|
||||
import { SchemaFromCodeOptions } from 'src/sql-tools/types';
|
||||
|
||||
/**
|
||||
* Load schema from code (decorators, etc)
|
||||
*/
|
||||
export const schemaFromCode = (options: SchemaFromCodeOptions = {}) => {
|
||||
try {
|
||||
const globalOptions = {
|
||||
createForeignKeyIndexes: options.createForeignKeyIndexes ?? true,
|
||||
};
|
||||
|
||||
const builder = new SchemaBuilder(options);
|
||||
const items = getRegisteredItems();
|
||||
for (const processor of processors) {
|
||||
processor(builder, items, globalOptions);
|
||||
}
|
||||
|
||||
const newSchema = builder.build();
|
||||
|
||||
return newSchema;
|
||||
} finally {
|
||||
if (options.reset) {
|
||||
resetRegisteredItems();
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import { Sql } from 'postgres';
|
||||
import { readers } from 'src/sql-tools/readers';
|
||||
import { DatabaseSchema, PostgresDB, SchemaFromDatabaseOptions } from 'src/sql-tools/types';
|
||||
|
||||
/**
|
||||
* Load schema from a database url
|
||||
*/
|
||||
export const schemaFromDatabase = async (
|
||||
postgres: Sql,
|
||||
options: SchemaFromDatabaseOptions = {},
|
||||
): Promise<DatabaseSchema> => {
|
||||
const schema: DatabaseSchema = {
|
||||
databaseName: 'immich',
|
||||
schemaName: options.schemaName || 'public',
|
||||
parameters: [],
|
||||
functions: [],
|
||||
enums: [],
|
||||
extensions: [],
|
||||
tables: [],
|
||||
warnings: [],
|
||||
};
|
||||
|
||||
const db = new Kysely<PostgresDB>({ dialect: new PostgresJSDialect({ postgres }) });
|
||||
for (const reader of readers) {
|
||||
await reader(schema, db);
|
||||
}
|
||||
|
||||
await db.destroy();
|
||||
|
||||
return schema;
|
||||
};
|
||||
@ -1,21 +0,0 @@
|
||||
import { schemaDiffToSql } from 'src/sql-tools';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe(schemaDiffToSql.name, () => {
|
||||
describe('comments', () => {
|
||||
it('should include the reason in a SQL comment', () => {
|
||||
expect(
|
||||
schemaDiffToSql(
|
||||
[
|
||||
{
|
||||
type: 'index.drop',
|
||||
indexName: 'IDX_test',
|
||||
reason: 'unknown',
|
||||
},
|
||||
],
|
||||
{ comments: true },
|
||||
),
|
||||
).toEqual([`DROP INDEX "IDX_test"; -- unknown`]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,59 +0,0 @@
|
||||
import { transformColumns } from 'src/sql-tools/to-sql/transformers/column.transformer';
|
||||
import { transformConstraints } from 'src/sql-tools/to-sql/transformers/constraint.transformer';
|
||||
import { transformEnums } from 'src/sql-tools/to-sql/transformers/enum.transformer';
|
||||
import { transformExtensions } from 'src/sql-tools/to-sql/transformers/extension.transformer';
|
||||
import { transformFunctions } from 'src/sql-tools/to-sql/transformers/function.transformer';
|
||||
import { transformIndexes } from 'src/sql-tools/to-sql/transformers/index.transformer';
|
||||
import { transformParameters } from 'src/sql-tools/to-sql/transformers/parameter.transformer';
|
||||
import { transformTables } from 'src/sql-tools/to-sql/transformers/table.transformer';
|
||||
import { transformTriggers } from 'src/sql-tools/to-sql/transformers/trigger.transformer';
|
||||
import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types';
|
||||
import { SchemaDiff, SchemaDiffToSqlOptions } from 'src/sql-tools/types';
|
||||
|
||||
/**
|
||||
* Convert schema diffs into SQL statements
|
||||
*/
|
||||
export const schemaDiffToSql = (items: SchemaDiff[], options: SchemaDiffToSqlOptions = {}): string[] => {
|
||||
return items.flatMap((item) => asSql(item).map((result) => result + withComments(options.comments, item)));
|
||||
};
|
||||
|
||||
const transformers: SqlTransformer[] = [
|
||||
transformColumns,
|
||||
transformConstraints,
|
||||
transformEnums,
|
||||
transformExtensions,
|
||||
transformFunctions,
|
||||
transformIndexes,
|
||||
transformParameters,
|
||||
transformTables,
|
||||
transformTriggers,
|
||||
];
|
||||
|
||||
const asSql = (item: SchemaDiff): string[] => {
|
||||
for (const transform of transformers) {
|
||||
const result = transform(item);
|
||||
if (!result) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return asArray(result);
|
||||
}
|
||||
|
||||
throw new Error(`Unhandled schema diff type: ${item.type}`);
|
||||
};
|
||||
|
||||
const withComments = (comments: boolean | undefined, item: SchemaDiff): string => {
|
||||
if (!comments) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return ` -- ${item.reason}`;
|
||||
};
|
||||
|
||||
const asArray = <T>(items: T | T[]): T[] => {
|
||||
if (Array.isArray(items)) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return [items];
|
||||
};
|
||||
@ -1,18 +1,18 @@
|
||||
import { asColumnComment, getColumnModifiers, getColumnType } from 'src/sql-tools/helpers';
|
||||
import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types';
|
||||
import { SqlTransformer } from 'src/sql-tools/transformers/types';
|
||||
import { ColumnChanges, DatabaseColumn, SchemaDiff } from 'src/sql-tools/types';
|
||||
|
||||
export const transformColumns: SqlTransformer = (item: SchemaDiff) => {
|
||||
switch (item.type) {
|
||||
case 'column.add': {
|
||||
case 'ColumnAdd': {
|
||||
return asColumnAdd(item.column);
|
||||
}
|
||||
|
||||
case 'column.alter': {
|
||||
case 'ColumnAlter': {
|
||||
return asColumnAlter(item.tableName, item.columnName, item.changes);
|
||||
}
|
||||
|
||||
case 'column.drop': {
|
||||
case 'ColumnDrop': {
|
||||
return asColumnDrop(item.tableName, item.columnName);
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types';
|
||||
import { SqlTransformer } from 'src/sql-tools/transformers/types';
|
||||
import { DatabaseEnum, SchemaDiff } from 'src/sql-tools/types';
|
||||
|
||||
export const transformEnums: SqlTransformer = (item: SchemaDiff) => {
|
||||
switch (item.type) {
|
||||
case 'enum.create': {
|
||||
case 'EnumCreate': {
|
||||
return asEnumCreate(item.enum);
|
||||
}
|
||||
|
||||
case 'enum.drop': {
|
||||
case 'EnumDrop': {
|
||||
return asEnumDrop(item.enumName);
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types';
|
||||
import { SqlTransformer } from 'src/sql-tools/transformers/types';
|
||||
import { DatabaseExtension, SchemaDiff } from 'src/sql-tools/types';
|
||||
|
||||
export const transformExtensions: SqlTransformer = (item: SchemaDiff) => {
|
||||
switch (item.type) {
|
||||
case 'extension.create': {
|
||||
case 'ExtensionCreate': {
|
||||
return asExtensionCreate(item.extension);
|
||||
}
|
||||
|
||||
case 'extension.drop': {
|
||||
case 'ExtensionDrop': {
|
||||
return asExtensionDrop(item.extensionName);
|
||||
}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { transformFunctions } from 'src/sql-tools/to-sql/transformers/function.transformer';
|
||||
import { transformFunctions } from 'src/sql-tools/transformers/function.transformer';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe(transformFunctions.name, () => {
|
||||
describe('function.drop', () => {
|
||||
describe('FunctionDrop', () => {
|
||||
it('should work', () => {
|
||||
expect(
|
||||
transformFunctions({
|
||||
type: 'function.drop',
|
||||
type: 'FunctionDrop',
|
||||
functionName: 'test_func',
|
||||
reason: 'unknown',
|
||||
}),
|
||||
@ -1,13 +1,13 @@
|
||||
import { SqlTransformer } from 'src/sql-tools/to-sql/transformers/types';
|
||||
import { SqlTransformer } from 'src/sql-tools/transformers/types';
|
||||
import { DatabaseFunction, SchemaDiff } from 'src/sql-tools/types';
|
||||
|
||||
export const transformFunctions: SqlTransformer = (item: SchemaDiff) => {
|
||||
switch (item.type) {
|
||||
case 'function.create': {
|
||||
case 'FunctionCreate': {
|
||||
return asFunctionCreate(item.function);
|
||||
}
|
||||
|
||||
case 'function.drop': {
|
||||
case 'FunctionDrop': {
|
||||
return asFunctionDrop(item.functionName);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue