feat: extension, triggers, functions, comments, parameters management in sql-tools (#17269)

feat: sql-tools extension, triggers, functions, comments, parameters
This commit is contained in:
Jason Rasmussen
2025-04-07 15:12:12 -04:00
committed by GitHub
parent 51c2c60231
commit e7a5b96ed0
170 changed files with 5205 additions and 2295 deletions

View File

@@ -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,
});

View File

@@ -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,
});

View 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 } });
};

View File

@@ -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) } });
};

View File

@@ -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) } });
};

View File

@@ -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 } });
};

View File

@@ -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,
});
};

View File

@@ -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 } });
};

View File

@@ -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,
});
};

View File

@@ -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) } });
};

View File

@@ -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) } });
}
};
};

View File

@@ -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 } });
};
};

View File

@@ -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,
});
};

View 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) } });
};

View File

@@ -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 });

View File

@@ -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 });

View 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) } });
};

View File

@@ -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 });

View File

@@ -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 } });
};

View File

@@ -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 } });
};

View File

@@ -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,
});
};

View 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);
});
}
});
});

View 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;
};

View File

@@ -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,
});
}
};

View File

@@ -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,
});
}
};

View 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);

View File

@@ -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,
});
}
};

View File

@@ -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);
}
};

View File

@@ -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);
}
};

View File

@@ -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,
});
}
};

View File

@@ -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,
});
}
}
};

View File

@@ -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);
}
};

View 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,
});
}
};

View File

@@ -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,
});
}
}
};

View 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);

View File

@@ -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,
});
}
};

View 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;

View 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 { 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,
});
}
};

View 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;
};

View 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;
};

View 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'];

View 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;
};