mirror of
https://github.com/immich-app/immich.git
synced 2025-12-22 09:15:34 +03:00
feat: extension, triggers, functions, comments, parameters management in sql-tools (#17269)
feat: sql-tools extension, triggers, functions, comments, parameters
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/from-code/decorators/trigger-function.decorator';
|
||||
|
||||
export const AfterDeleteTrigger = (options: Omit<TriggerFunctionOptions, 'timing' | 'actions'>) =>
|
||||
TriggerFunction({
|
||||
timing: 'after',
|
||||
actions: ['delete'],
|
||||
...options,
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { TriggerFunction, TriggerFunctionOptions } from 'src/sql-tools/from-code/decorators/trigger-function.decorator';
|
||||
|
||||
export const BeforeUpdateTrigger = (options: Omit<TriggerFunctionOptions, 'timing' | 'actions'>) =>
|
||||
TriggerFunction({
|
||||
timing: 'before',
|
||||
actions: ['update'],
|
||||
...options,
|
||||
});
|
||||
11
server/src/sql-tools/from-code/decorators/check.decorator.ts
Normal file
11
server/src/sql-tools/from-code/decorators/check.decorator.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
|
||||
export type CheckOptions = {
|
||||
name?: string;
|
||||
expression: string;
|
||||
synchronize?: boolean;
|
||||
};
|
||||
export const Check = (options: CheckOptions): ClassDecorator => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
return (object: Function) => void register({ type: 'checkConstraint', item: { object, options } });
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { asOptions } from 'src/sql-tools/helpers';
|
||||
|
||||
export type ColumnIndexOptions = {
|
||||
name?: string;
|
||||
unique?: boolean;
|
||||
expression?: string;
|
||||
using?: string;
|
||||
with?: string;
|
||||
where?: string;
|
||||
synchronize?: boolean;
|
||||
};
|
||||
export const ColumnIndex = (options: string | ColumnIndexOptions = {}): PropertyDecorator => {
|
||||
return (object: object, propertyName: string | symbol) =>
|
||||
void register({ type: 'columnIndex', item: { object, propertyName, options: asOptions(options) } });
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { asOptions } from 'src/sql-tools/helpers';
|
||||
import { ColumnStorage, ColumnType, DatabaseEnum } from 'src/sql-tools/types';
|
||||
|
||||
export type ColumnValue = null | boolean | string | number | object | Date | (() => string);
|
||||
|
||||
export type ColumnBaseOptions = {
|
||||
name?: string;
|
||||
primary?: boolean;
|
||||
type?: ColumnType;
|
||||
nullable?: boolean;
|
||||
length?: number;
|
||||
default?: ColumnValue;
|
||||
comment?: string;
|
||||
synchronize?: boolean;
|
||||
storage?: ColumnStorage;
|
||||
identity?: boolean;
|
||||
};
|
||||
|
||||
export type ColumnOptions = ColumnBaseOptions & {
|
||||
enum?: DatabaseEnum;
|
||||
array?: boolean;
|
||||
unique?: boolean;
|
||||
uniqueConstraintName?: string;
|
||||
};
|
||||
|
||||
export const Column = (options: string | ColumnOptions = {}): PropertyDecorator => {
|
||||
return (object: object, propertyName: string | symbol) =>
|
||||
void register({ type: 'column', item: { object, propertyName, options: asOptions(options) } });
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ColumnValue } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { ParameterScope } from 'src/sql-tools/types';
|
||||
|
||||
export type ConfigurationParameterOptions = {
|
||||
name: string;
|
||||
value: ColumnValue;
|
||||
scope: ParameterScope;
|
||||
synchronize?: boolean;
|
||||
};
|
||||
export const ConfigurationParameter = (options: ConfigurationParameterOptions): ClassDecorator => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
return (object: Function) => void register({ type: 'configurationParameter', item: { object, options } });
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Column, ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
|
||||
export const CreateDateColumn = (options: ColumnOptions = {}): PropertyDecorator => {
|
||||
return Column({
|
||||
type: 'timestamp with time zone',
|
||||
default: () => 'now()',
|
||||
...options,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
|
||||
export type DatabaseOptions = {
|
||||
name?: string;
|
||||
synchronize?: boolean;
|
||||
};
|
||||
export const Database = (options: DatabaseOptions): ClassDecorator => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
return (object: Function) => void register({ type: 'database', item: { object, options } });
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Column, ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
|
||||
export const DeleteDateColumn = (options: ColumnOptions = {}): PropertyDecorator => {
|
||||
return Column({
|
||||
type: 'timestamp with time zone',
|
||||
nullable: true,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { asOptions } from 'src/sql-tools/helpers';
|
||||
|
||||
export type ExtensionOptions = {
|
||||
name: string;
|
||||
synchronize?: boolean;
|
||||
};
|
||||
export const Extension = (options: string | ExtensionOptions): ClassDecorator => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
return (object: Function) => void register({ type: 'extension', item: { object, options: asOptions(options) } });
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { asOptions } from 'src/sql-tools/helpers';
|
||||
|
||||
export type ExtensionsOptions = {
|
||||
name: string;
|
||||
synchronize?: boolean;
|
||||
};
|
||||
export const Extensions = (options: Array<string | ExtensionsOptions>): ClassDecorator => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
return (object: Function) => {
|
||||
for (const option of options) {
|
||||
register({ type: 'extension', item: { object, options: asOptions(option) } });
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ColumnBaseOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
|
||||
type Action = 'CASCADE' | 'SET NULL' | 'SET DEFAULT' | 'RESTRICT' | 'NO ACTION';
|
||||
|
||||
export type ForeignKeyColumnOptions = ColumnBaseOptions & {
|
||||
onUpdate?: Action;
|
||||
onDelete?: Action;
|
||||
constraintName?: string;
|
||||
unique?: boolean;
|
||||
uniqueConstraintName?: string;
|
||||
};
|
||||
|
||||
export const ForeignKeyColumn = (target: () => object, options: ForeignKeyColumnOptions): PropertyDecorator => {
|
||||
return (object: object, propertyName: string | symbol) => {
|
||||
register({ type: 'foreignKeyColumn', item: { object, propertyName, options, target } });
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Column, ColumnOptions, ColumnValue } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
import { ColumnType } from 'src/sql-tools/types';
|
||||
|
||||
export type GeneratedColumnStrategy = 'uuid' | 'identity';
|
||||
|
||||
export type GenerateColumnOptions = Omit<ColumnOptions, 'type'> & {
|
||||
strategy?: GeneratedColumnStrategy;
|
||||
};
|
||||
|
||||
export const GeneratedColumn = ({ strategy = 'uuid', ...options }: GenerateColumnOptions): PropertyDecorator => {
|
||||
let columnType: ColumnType | undefined;
|
||||
let columnDefault: ColumnValue | undefined;
|
||||
|
||||
switch (strategy) {
|
||||
case 'uuid': {
|
||||
columnType = 'uuid';
|
||||
columnDefault = () => 'uuid_generate_v4()';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'identity': {
|
||||
columnType = 'integer';
|
||||
options.identity = true;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error(`Unsupported strategy for @GeneratedColumn ${strategy}`);
|
||||
}
|
||||
}
|
||||
|
||||
return Column({
|
||||
type: columnType,
|
||||
default: columnDefault,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
12
server/src/sql-tools/from-code/decorators/index.decorator.ts
Normal file
12
server/src/sql-tools/from-code/decorators/index.decorator.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ColumnIndexOptions } from 'src/sql-tools/from-code/decorators/column-index.decorator';
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { asOptions } from 'src/sql-tools/helpers';
|
||||
|
||||
export type IndexOptions = ColumnIndexOptions & {
|
||||
columns?: string[];
|
||||
synchronize?: boolean;
|
||||
};
|
||||
export const Index = (options: string | IndexOptions = {}): ClassDecorator => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
return (object: Function) => void register({ type: 'index', item: { object, options: asOptions(options) } });
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Column, ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
|
||||
export const PrimaryColumn = (options: Omit<ColumnOptions, 'primary'> = {}) => Column({ ...options, primary: true });
|
||||
@@ -0,0 +1,4 @@
|
||||
import { GenerateColumnOptions, GeneratedColumn } from 'src/sql-tools/from-code/decorators/generated-column.decorator';
|
||||
|
||||
export const PrimaryGeneratedColumn = (options: Omit<GenerateColumnOptions, 'primary'> = {}) =>
|
||||
GeneratedColumn({ ...options, primary: true });
|
||||
14
server/src/sql-tools/from-code/decorators/table.decorator.ts
Normal file
14
server/src/sql-tools/from-code/decorators/table.decorator.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { asOptions } from 'src/sql-tools/helpers';
|
||||
|
||||
export type TableOptions = {
|
||||
name?: string;
|
||||
primaryConstraintName?: string;
|
||||
synchronize?: boolean;
|
||||
};
|
||||
|
||||
/** Table comments here */
|
||||
export const Table = (options: string | TableOptions = {}): ClassDecorator => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
return (object: Function) => void register({ type: 'table', item: { object, options: asOptions(options) } });
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Trigger, TriggerOptions } from 'src/sql-tools/from-code/decorators/trigger.decorator';
|
||||
import { DatabaseFunction } from 'src/sql-tools/types';
|
||||
|
||||
export type TriggerFunctionOptions = Omit<TriggerOptions, 'functionName'> & { function: DatabaseFunction };
|
||||
export const TriggerFunction = (options: TriggerFunctionOptions) =>
|
||||
Trigger({ ...options, functionName: options.function.name });
|
||||
@@ -0,0 +1,19 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { TriggerAction, TriggerScope, TriggerTiming } from 'src/sql-tools/types';
|
||||
|
||||
export type TriggerOptions = {
|
||||
name?: string;
|
||||
timing: TriggerTiming;
|
||||
actions: TriggerAction[];
|
||||
scope: TriggerScope;
|
||||
functionName: string;
|
||||
referencingNewTableAs?: string;
|
||||
referencingOldTableAs?: string;
|
||||
when?: string;
|
||||
synchronize?: boolean;
|
||||
};
|
||||
|
||||
export const Trigger = (options: TriggerOptions): ClassDecorator => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
return (object: Function) => void register({ type: 'trigger', item: { object, options } });
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
|
||||
export type UniqueOptions = {
|
||||
name?: string;
|
||||
columns: string[];
|
||||
synchronize?: boolean;
|
||||
};
|
||||
export const Unique = (options: UniqueOptions): ClassDecorator => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
return (object: Function) => void register({ type: 'uniqueConstraint', item: { object, options } });
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Column, ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
|
||||
export const UpdateDateColumn = (options: ColumnOptions = {}): PropertyDecorator => {
|
||||
return Column({
|
||||
type: 'timestamp with time zone',
|
||||
default: () => 'now()',
|
||||
...options,
|
||||
});
|
||||
};
|
||||
36
server/src/sql-tools/from-code/index.spec.ts
Normal file
36
server/src/sql-tools/from-code/index.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
69
server/src/sql-tools/from-code/index.ts
Normal file
69
server/src/sql-tools/from-code/index.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'reflect-metadata';
|
||||
import { processCheckConstraints } from 'src/sql-tools/from-code/processors/check-constraint.processor';
|
||||
import { processColumnIndexes } from 'src/sql-tools/from-code/processors/column-index.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 { processForeignKeyConstraints } from 'src/sql-tools/from-code/processors/foreign-key-constriant.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,
|
||||
processUniqueConstraints,
|
||||
processCheckConstraints,
|
||||
processPrimaryKeyConstraints,
|
||||
processIndexes,
|
||||
processColumnIndexes,
|
||||
processForeignKeyConstraints,
|
||||
processTriggers,
|
||||
];
|
||||
|
||||
export const schemaFromCode = () => {
|
||||
if (!initialized) {
|
||||
const builder: SchemaBuilder = {
|
||||
name: 'postgres',
|
||||
schemaName: 'public',
|
||||
tables: [],
|
||||
functions: [],
|
||||
enums: [],
|
||||
extensions: [],
|
||||
parameters: [],
|
||||
warnings: [],
|
||||
};
|
||||
|
||||
const items = getRegisteredItems();
|
||||
|
||||
for (const processor of processors) {
|
||||
processor(builder, items);
|
||||
}
|
||||
|
||||
schema = { ...builder, tables: builder.tables.map(({ metadata: _, ...table }) => table) };
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
return schema;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
import { asCheckConstraintName } from 'src/sql-tools/helpers';
|
||||
import { DatabaseConstraintType } 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);
|
||||
if (!table) {
|
||||
onMissingTable(builder, '@Check', object);
|
||||
continue;
|
||||
}
|
||||
|
||||
const tableName = table.name;
|
||||
|
||||
table.constraints.push({
|
||||
type: DatabaseConstraintType.CHECK,
|
||||
name: options.name || asCheckConstraintName(tableName, options.expression),
|
||||
tableName,
|
||||
expression: options.expression,
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor';
|
||||
import { onMissingTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
import { asIndexName } from 'src/sql-tools/helpers';
|
||||
|
||||
export const processColumnIndexes: Processor = (builder, items) => {
|
||||
for (const {
|
||||
item: { object, propertyName, options },
|
||||
} of items.filter((item) => item.type === 'columnIndex')) {
|
||||
const { table, column } = resolveColumn(builder, object, propertyName);
|
||||
if (!table) {
|
||||
onMissingTable(builder, '@ColumnIndex', object);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!column) {
|
||||
onMissingColumn(builder, `@ColumnIndex`, object, propertyName);
|
||||
continue;
|
||||
}
|
||||
|
||||
table.indexes.push({
|
||||
name: options.name || asIndexName(table.name, [column.name], options.where),
|
||||
tableName: table.name,
|
||||
unique: options.unique ?? false,
|
||||
expression: options.expression,
|
||||
using: options.using,
|
||||
where: options.where,
|
||||
columnNames: [column.name],
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
};
|
||||
103
server/src/sql-tools/from-code/processors/column.processor.ts
Normal file
103
server/src/sql-tools/from-code/processors/column.processor.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
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 { asMetadataKey, asUniqueConstraintName, fromColumnValue } from 'src/sql-tools/helpers';
|
||||
import { DatabaseColumn, DatabaseConstraintType } 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);
|
||||
|
||||
if (type === 'column' && !options.primary && options.unique) {
|
||||
table.constraints.push({
|
||||
type: DatabaseConstraintType.UNIQUE,
|
||||
name: options.uniqueConstraintName || asUniqueConstraintName(table.name, [column.name]),
|
||||
tableName: table.name,
|
||||
columnNames: [column.name],
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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) : '');
|
||||
builder.warnings.push(`[${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);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
import { fromColumnValue } from 'src/sql-tools/helpers';
|
||||
|
||||
export const processConfigurationParameters: Processor = (builder, items) => {
|
||||
for (const {
|
||||
item: { options },
|
||||
} of items.filter((item) => item.type === 'configurationParameter')) {
|
||||
builder.parameters.push({
|
||||
databaseName: builder.name,
|
||||
name: options.name,
|
||||
value: fromColumnValue(options.value),
|
||||
scope: options.scope,
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
import { asSnakeCase } from 'src/sql-tools/helpers';
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
|
||||
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,59 @@
|
||||
import { onMissingColumn, resolveColumn } from 'src/sql-tools/from-code/processors/column.processor';
|
||||
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
import { asForeignKeyConstraintName, asRelationKeyConstraintName } from 'src/sql-tools/helpers';
|
||||
import { DatabaseActionType, DatabaseConstraintType } from 'src/sql-tools/types';
|
||||
|
||||
export const processForeignKeyConstraints: Processor = (builder, items) => {
|
||||
for (const {
|
||||
item: { object, propertyName, options, target },
|
||||
} of items.filter((item) => item.type === 'foreignKeyColumn')) {
|
||||
const { table, column } = resolveColumn(builder, object, propertyName);
|
||||
if (!table) {
|
||||
onMissingTable(builder, '@ForeignKeyColumn', object);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!column) {
|
||||
// should be impossible since they are pre-created in `column.processor.ts`
|
||||
onMissingColumn(builder, '@ForeignKeyColumn', object, propertyName);
|
||||
continue;
|
||||
}
|
||||
|
||||
const referenceTable = resolveTable(builder, target());
|
||||
if (!referenceTable) {
|
||||
onMissingTable(builder, '@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;
|
||||
}
|
||||
|
||||
table.constraints.push({
|
||||
name: options.constraintName || asForeignKeyConstraintName(table.name, columnNames),
|
||||
tableName: table.name,
|
||||
columnNames,
|
||||
type: DatabaseConstraintType.FOREIGN_KEY,
|
||||
referenceTableName: referenceTable.name,
|
||||
referenceColumnNames: referenceColumns.map((column) => column.name),
|
||||
onUpdate: options.onUpdate as DatabaseActionType,
|
||||
onDelete: options.onDelete as DatabaseActionType,
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
|
||||
if (options.unique) {
|
||||
table.constraints.push({
|
||||
name: options.uniqueConstraintName || asRelationKeyConstraintName(table.name, columnNames),
|
||||
tableName: table.name,
|
||||
columnNames,
|
||||
type: DatabaseConstraintType.UNIQUE,
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
27
server/src/sql-tools/from-code/processors/index.processor.ts
Normal file
27
server/src/sql-tools/from-code/processors/index.processor.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
import { asIndexName } from 'src/sql-tools/helpers';
|
||||
|
||||
export const processIndexes: Processor = (builder, items) => {
|
||||
for (const {
|
||||
item: { object, options },
|
||||
} of items.filter((item) => item.type === 'index')) {
|
||||
const table = resolveTable(builder, object);
|
||||
if (!table) {
|
||||
onMissingTable(builder, '@Check', object);
|
||||
continue;
|
||||
}
|
||||
|
||||
table.indexes.push({
|
||||
name: options.name || 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
import { asPrimaryKeyConstraintName } from 'src/sql-tools/helpers';
|
||||
import { DatabaseConstraintType } 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) {
|
||||
table.constraints.push({
|
||||
type: DatabaseConstraintType.PRIMARY_KEY,
|
||||
name: table.metadata.options.primaryConstraintName || asPrimaryKeyConstraintName(table.name, columnNames),
|
||||
tableName: table.name,
|
||||
columnNames,
|
||||
synchronize: table.metadata.options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
51
server/src/sql-tools/from-code/processors/table.processor.ts
Normal file
51
server/src/sql-tools/from-code/processors/table.processor.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator';
|
||||
import { Processor, SchemaBuilder } from 'src/sql-tools/from-code/processors/type';
|
||||
import { 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 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) : '');
|
||||
builder.warnings.push(`[${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);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
import { asTriggerName } from 'src/sql-tools/helpers';
|
||||
|
||||
export const processTriggers: Processor = (builder, items) => {
|
||||
for (const {
|
||||
item: { object, options },
|
||||
} of items.filter((item) => item.type === 'trigger')) {
|
||||
const table = resolveTable(builder, object);
|
||||
if (!table) {
|
||||
onMissingTable(builder, '@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,
|
||||
});
|
||||
}
|
||||
};
|
||||
9
server/src/sql-tools/from-code/processors/type.ts
Normal file
9
server/src/sql-tools/from-code/processors/type.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
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[]) => void;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
|
||||
import { Processor } from 'src/sql-tools/from-code/processors/type';
|
||||
import { asUniqueConstraintName } from 'src/sql-tools/helpers';
|
||||
import { DatabaseConstraintType } 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 = resolveTable(builder, object);
|
||||
if (!table) {
|
||||
onMissingTable(builder, '@Unique', object);
|
||||
continue;
|
||||
}
|
||||
|
||||
const tableName = table.name;
|
||||
const columnNames = options.columns;
|
||||
|
||||
table.constraints.push({
|
||||
type: DatabaseConstraintType.UNIQUE,
|
||||
name: options.name || asUniqueConstraintName(tableName, columnNames),
|
||||
tableName,
|
||||
columnNames,
|
||||
synchronize: options.synchronize ?? true,
|
||||
});
|
||||
}
|
||||
};
|
||||
20
server/src/sql-tools/from-code/register-enum.ts
Normal file
20
server/src/sql-tools/from-code/register-enum.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { DatabaseEnum } from 'src/sql-tools/types';
|
||||
|
||||
export type EnumOptions = {
|
||||
name: string;
|
||||
values: string[];
|
||||
synchronize?: boolean;
|
||||
};
|
||||
|
||||
export const registerEnum = (options: EnumOptions) => {
|
||||
const item: DatabaseEnum = {
|
||||
name: options.name,
|
||||
values: options.values,
|
||||
synchronize: options.synchronize ?? true,
|
||||
};
|
||||
|
||||
register({ type: 'enum', item });
|
||||
|
||||
return item;
|
||||
};
|
||||
29
server/src/sql-tools/from-code/register-function.ts
Normal file
29
server/src/sql-tools/from-code/register-function.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { register } from 'src/sql-tools/from-code/register';
|
||||
import { asFunctionExpression } from 'src/sql-tools/helpers';
|
||||
import { ColumnType, DatabaseFunction } from 'src/sql-tools/types';
|
||||
|
||||
export type FunctionOptions = {
|
||||
name: string;
|
||||
arguments?: string[];
|
||||
returnType: ColumnType | string;
|
||||
language?: 'SQL' | 'PLPGSQL';
|
||||
behavior?: 'immutable' | 'stable' | 'volatile';
|
||||
parallel?: 'safe' | 'unsafe' | 'restricted';
|
||||
strict?: boolean;
|
||||
synchronize?: boolean;
|
||||
} & ({ body: string } | { return: string });
|
||||
|
||||
export const registerFunction = (options: FunctionOptions) => {
|
||||
const name = options.name;
|
||||
const expression = asFunctionExpression(options);
|
||||
|
||||
const item: DatabaseFunction = {
|
||||
name,
|
||||
expression,
|
||||
synchronize: options.synchronize ?? true,
|
||||
};
|
||||
|
||||
register({ type: 'function', item });
|
||||
|
||||
return item;
|
||||
};
|
||||
31
server/src/sql-tools/from-code/register-item.ts
Normal file
31
server/src/sql-tools/from-code/register-item.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { CheckOptions } from 'src/sql-tools/from-code/decorators/check.decorator';
|
||||
import { ColumnIndexOptions } from 'src/sql-tools/from-code/decorators/column-index.decorator';
|
||||
import { ColumnOptions } from 'src/sql-tools/from-code/decorators/column.decorator';
|
||||
import { ConfigurationParameterOptions } from 'src/sql-tools/from-code/decorators/configuration-parameter.decorator';
|
||||
import { DatabaseOptions } from 'src/sql-tools/from-code/decorators/database.decorator';
|
||||
import { ExtensionOptions } from 'src/sql-tools/from-code/decorators/extension.decorator';
|
||||
import { ForeignKeyColumnOptions } from 'src/sql-tools/from-code/decorators/foreign-key-column.decorator';
|
||||
import { IndexOptions } from 'src/sql-tools/from-code/decorators/index.decorator';
|
||||
import { TableOptions } from 'src/sql-tools/from-code/decorators/table.decorator';
|
||||
import { TriggerOptions } from 'src/sql-tools/from-code/decorators/trigger.decorator';
|
||||
import { UniqueOptions } from 'src/sql-tools/from-code/decorators/unique.decorator';
|
||||
import { DatabaseEnum, DatabaseFunction } from 'src/sql-tools/types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
export type ClassBased<T> = { object: Function } & T;
|
||||
export type PropertyBased<T> = { object: object; propertyName: string | symbol } & T;
|
||||
export type RegisterItem =
|
||||
| { type: 'database'; item: ClassBased<{ options: DatabaseOptions }> }
|
||||
| { type: 'table'; item: ClassBased<{ options: TableOptions }> }
|
||||
| { type: 'index'; item: ClassBased<{ options: IndexOptions }> }
|
||||
| { type: 'uniqueConstraint'; item: ClassBased<{ options: UniqueOptions }> }
|
||||
| { type: 'checkConstraint'; item: ClassBased<{ options: CheckOptions }> }
|
||||
| { type: 'column'; item: PropertyBased<{ options: ColumnOptions }> }
|
||||
| { type: 'columnIndex'; item: PropertyBased<{ options: ColumnIndexOptions }> }
|
||||
| { type: 'function'; item: DatabaseFunction }
|
||||
| { type: 'enum'; item: DatabaseEnum }
|
||||
| { type: 'trigger'; item: ClassBased<{ options: TriggerOptions }> }
|
||||
| { type: 'extension'; item: ClassBased<{ options: ExtensionOptions }> }
|
||||
| { type: 'configurationParameter'; item: ClassBased<{ options: ConfigurationParameterOptions }> }
|
||||
| { type: 'foreignKeyColumn'; item: PropertyBased<{ options: ForeignKeyColumnOptions; target: () => object }> };
|
||||
export type RegisterItemType<T extends RegisterItem['type']> = Extract<RegisterItem, { type: T }>['item'];
|
||||
11
server/src/sql-tools/from-code/register.ts
Normal file
11
server/src/sql-tools/from-code/register.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { RegisterItem } from 'src/sql-tools/from-code/register-item';
|
||||
|
||||
const items: RegisterItem[] = [];
|
||||
|
||||
export const register = (item: RegisterItem) => void items.push(item);
|
||||
|
||||
export const getRegisteredItems = () => items;
|
||||
|
||||
export const resetRegisteredItems = () => {
|
||||
items.length = 0;
|
||||
};
|
||||
Reference in New Issue
Block a user