feat(server): sql-tools support for class level composite fk (#19242)

* feat: support for class level composite fk

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
SGT 2025-06-18 15:30:39 -03:00 committed by GitHub
parent de81006367
commit 0a9a520ed2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 865 additions and 30 deletions

View file

@ -1,7 +1,7 @@
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, fromColumnValue } from 'src/sql-tools/helpers';
import { addWarning, asMetadataKey, fromColumnValue } from 'src/sql-tools/helpers';
import { DatabaseColumn } from 'src/sql-tools/types';
export const processColumns: Processor = (builder, items) => {
@ -81,7 +81,7 @@ export const onMissingColumn = (
propertyName?: symbol | string,
) => {
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
builder.warnings.push(`[${context}] Unable to find column (${label})`);
addWarning(builder, context, `Unable to find column (${label})`);
};
const METADATA_KEY = asMetadataKey('table-metadata');

View file

@ -1,10 +1,10 @@
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 { asKey } from 'src/sql-tools/helpers';
import { asForeignKeyConstraintName, asKey } from 'src/sql-tools/helpers';
import { DatabaseActionType, DatabaseConstraintType } from 'src/sql-tools/types';
export const processForeignKeyConstraints: Processor = (builder, items) => {
export const processForeignKeyColumns: Processor = (builder, items) => {
for (const {
item: { object, propertyName, options, target },
} of items.filter((item) => item.type === 'foreignKeyColumn')) {
@ -34,13 +34,16 @@ export const processForeignKeyConstraints: Processor = (builder, items) => {
column.type = referenceColumns[0].type;
}
const referenceColumnNames = referenceColumns.map((column) => column.name);
const name = options.constraintName || asForeignKeyConstraintName(table.name, columnNames);
table.constraints.push({
name: options.constraintName || asForeignKeyConstraintName(table.name, columnNames),
name,
tableName: table.name,
columnNames,
type: DatabaseConstraintType.FOREIGN_KEY,
referenceTableName: referenceTable.name,
referenceColumnNames: referenceColumns.map((column) => column.name),
referenceColumnNames,
onUpdate: options.onUpdate as DatabaseActionType,
onDelete: options.onDelete as DatabaseActionType,
synchronize: options.synchronize ?? true,
@ -58,5 +61,4 @@ export const processForeignKeyConstraints: Processor = (builder, items) => {
}
};
const asForeignKeyConstraintName = (table: string, columns: string[]) => asKey('FK_', table, columns);
const asRelationKeyConstraintName = (table: string, columns: string[]) => asKey('REL_', table, columns);

View file

@ -0,0 +1,86 @@
import { onMissingTable, resolveTable } from 'src/sql-tools/from-code/processors/table.processor';
import { Processor } from 'src/sql-tools/from-code/processors/type';
import { addWarning, asForeignKeyConstraintName, asIndexName } from 'src/sql-tools/helpers';
import { DatabaseActionType, DatabaseConstraintType } from 'src/sql-tools/types';
export const processForeignKeyConstraints: Processor = (builder, items, config) => {
for (const {
item: { object, options },
} of items.filter((item) => item.type === 'foreignKeyConstraint')) {
const table = resolveTable(builder, object);
if (!table) {
onMissingTable(builder, '@ForeignKeyConstraint', { name: 'referenceTable' });
continue;
}
const referenceTable = resolveTable(builder, options.referenceTable());
if (!referenceTable) {
const referenceTableName = options.referenceTable()?.name;
addWarning(
builder,
'@ForeignKeyConstraint.referenceTable',
`Unable to find table` + (referenceTableName ? ` (${referenceTableName})` : ''),
);
continue;
}
let missingColumn = false;
for (const columnName of options.columns) {
if (!table.columns.some(({ name }) => name === columnName)) {
addWarning(
builder,
'@ForeignKeyConstraint.columns',
`Unable to find column (${table.metadata.object.name}.${columnName})`,
);
missingColumn = true;
}
}
for (const columnName of options.referenceColumns || []) {
if (!referenceTable.columns.some(({ name }) => name === columnName)) {
addWarning(
builder,
'@ForeignKeyConstraint.referenceColumns',
`Unable to find column (${referenceTable.metadata.object.name}.${columnName})`,
);
missingColumn = true;
}
}
if (missingColumn) {
continue;
}
const referenceColumns =
options.referenceColumns || referenceTable.columns.filter(({ primary }) => primary).map(({ name }) => name);
const name = options.name || asForeignKeyConstraintName(table.name, options.columns);
table.constraints.push({
type: DatabaseConstraintType.FOREIGN_KEY,
name,
tableName: table.name,
columnNames: options.columns,
referenceTableName: referenceTable.name,
referenceColumnNames: referenceColumns,
onUpdate: options.onUpdate as DatabaseActionType,
onDelete: options.onDelete as DatabaseActionType,
synchronize: options.synchronize ?? true,
});
if (options.index === false) {
continue;
}
if (options.index || options.indexName || config.createForeignKeyIndexes) {
table.indexes.push({
name: options.indexName || asIndexName(table.name, options.columns),
tableName: table.name,
columnNames: options.columns,
unique: false,
synchronize: options.synchronize ?? true,
});
}
}
};

View file

@ -1,7 +1,7 @@
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 { asKey } from 'src/sql-tools/helpers';
import { asIndexName } from 'src/sql-tools/helpers';
export const processIndexes: Processor = (builder, items, config) => {
for (const {
@ -75,16 +75,3 @@ export const processIndexes: Processor = (builder, items, config) => {
});
}
};
const 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);
};

View file

@ -1,6 +1,6 @@
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';
import { addWarning, asMetadataKey, asSnakeCase } from 'src/sql-tools/helpers';
export const processTables: Processor = (builder, items) => {
for (const {
@ -45,7 +45,7 @@ export const onMissingTable = (
propertyName?: symbol | string,
) => {
const label = object.constructor.name + (propertyName ? '.' + String(propertyName) : '');
builder.warnings.push(`[${context}] Unable to find table (${label})`);
addWarning(builder, context, `Unable to find table (${label})`);
};
const METADATA_KEY = asMetadataKey('table-metadata');