Skip to content

Commit

Permalink
fix: detect and log broken relationship (#760)
Browse files Browse the repository at this point in the history
  • Loading branch information
SkyHuss authored Aug 31, 2023
1 parent 8dc8e1f commit 996899b
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 16 deletions.
69 changes: 53 additions & 16 deletions packages/datasource-sql/src/introspection/introspector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Logger } from '@forestadmin/datasource-toolkit';
import { Dialect, Sequelize } from 'sequelize';
import { Dialect, QueryTypes, Sequelize } from 'sequelize';

import DefaultValueParser from './helpers/default-value-parser';
import SqlTypeConverter from './helpers/sql-type-converter';
Expand Down Expand Up @@ -36,25 +36,33 @@ export default class Introspector {
logger: Logger,
tableName: string,
): Promise<Table> {
// Load columns descriptions, indexes and references of the current table.
const queryInterface = sequelize.getQueryInterface() as QueryInterfaceExt;
const [columnDescriptions, tableIndexes, tableReferences] = await Promise.all([
queryInterface.describeTable(tableName),
queryInterface.showIndex(tableName),
queryInterface.getForeignKeyReferencesForTable(tableName),
]);

// Create columns
const columns = Object.entries(columnDescriptions).map(([name, description]) => {
const references = tableReferences.filter(r => r.columnName === name);
const options = { name, description, references };

return this.getColumn(sequelize, logger, tableName, options);
});
const [columnDescriptions, tableIndexes, tableReferences, constraintNamesForForeignKey] =
await Promise.all([
queryInterface.describeTable(tableName),
queryInterface.showIndex(tableName),
queryInterface.getForeignKeyReferencesForTable(tableName),
sequelize.query(
`SELECT constraint_name, table_name from information_schema.table_constraints
where table_name = :tableName and constraint_type = 'FOREIGN KEY';`,
{ replacements: { tableName }, type: QueryTypes.SELECT },
),
]);

const columns = await Promise.all(
Object.entries(columnDescriptions).map(async ([name, description]) => {
const references = tableReferences.filter(r => r.columnName === name);
const options = { name, description, references };

return this.getColumn(sequelize, logger, tableName, options);
}),
);

this.detectBrokenRelationship(constraintNamesForForeignKey, tableReferences, logger);

return {
name: tableName,
columns: (await Promise.all(columns)).filter(Boolean),
columns: columns.filter(Boolean),
unique: tableIndexes
.filter(i => i.unique || i.primary)
.map(i => i.fields.map(f => f.attribute)),
Expand Down Expand Up @@ -126,4 +134,33 @@ export default class Introspector {
}
}
}

private static detectBrokenRelationship(
constraintNamesForForeignKey: unknown[],
tableReferences: SequelizeReference[],
logger: Logger,
) {
if (constraintNamesForForeignKey.length !== tableReferences.length) {
const constraintNames = new Set(
(constraintNamesForForeignKey as [{ constraint_name: string; table_name: string }]).map(
c => ({ constraint_name: c.constraint_name, table_name: c.table_name }),
),
);
tableReferences.forEach(({ constraintName }) => {
constraintNames.forEach(obj => {
if (obj.constraint_name === constraintName) {
constraintNames.delete(obj);
}
});
});

constraintNames.forEach(obj => {
logger?.(
'Error',
// eslint-disable-next-line max-len
`Failed to load constraints on relation '${obj.constraint_name}' on table '${obj.table_name}'. The relation will be ignored.`,
);
});
}
}
}
84 changes: 84 additions & 0 deletions packages/datasource-sql/test/introspection/intropector.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { QueryTypes, Sequelize } from 'sequelize';

import Introspector from '../../src/introspection/introspector';

// Mock Sequelize and Logger for testing
const mockSequelize: Sequelize = jest.createMockFromModule('sequelize');
const logger = jest.fn();
// Mock the necessary Sequelize methods and their return values
const mockGetQueryInterface = jest.fn();
const mockQueryInterface = {
showAllTables: jest.fn().mockResolvedValue([{ tableName: 'table1' }]),
describeTable: jest.fn().mockResolvedValue({
column1: { type: 'INTEGER', allowNull: false, primaryKey: true },
column2: { type: 'STRING', allowNull: true, primaryKey: false },
}),
showIndex: jest.fn().mockResolvedValue([
{ fields: ['column1'], unique: true },
{ fields: ['column2'], unique: false },
]),
getForeignKeyReferencesForTable: jest.fn().mockResolvedValue([
{
columnName: 'column1',
constraintName: 'fk_column1',
},
{
columnName: 'column2',
constraintName: 'fk_column2',
},
]),
};
(mockSequelize as Sequelize).getQueryInterface =
mockGetQueryInterface.mockReturnValue(mockQueryInterface);

describe('Introspector', () => {
afterEach(() => {
jest.clearAllMocks();
});

describe('getTable', () => {
it('should log errors for missing constraint names', async () => {
// Mock the Sequelize methods
const mockDescribeTable = jest.fn().mockResolvedValue({
column1: { type: 'INTEGER', allowNull: false, primaryKey: true },
});
const mockShowIndex = jest.fn().mockResolvedValue([{ fields: ['column1'], unique: true }]);
const mockQuery = jest.fn().mockResolvedValue([
{
table_name: 'table1',
constraint_name: 'fk_column1',
},
{
table_name: 'table1',
constraint_name: 'fk_column2',
},
{
table_name: 'table1',
constraint_name: 'fk_unknown_column',
},
]);
mockQueryInterface.describeTable = mockDescribeTable;
mockQueryInterface.showIndex = mockShowIndex;
mockSequelize.query = mockQuery;
mockSequelize.getDialect = jest.fn().mockResolvedValue('postgres');

await Introspector.introspect(mockSequelize, logger);

// Assert the Sequelize method calls
expect(mockDescribeTable).toHaveBeenCalledWith('table1');
expect(mockShowIndex).toHaveBeenCalledWith('table1');
expect(mockQuery).toHaveBeenCalledWith(
`SELECT constraint_name, table_name from information_schema.table_constraints
where table_name = :tableName and constraint_type = 'FOREIGN KEY';`,
{ replacements: { tableName: 'table1' }, type: QueryTypes.SELECT },
);

// Assert the logger call
expect(logger).toHaveBeenCalledWith(
'Error',
// eslint-disable-next-line max-len
"Failed to load constraints on relation 'fk_unknown_column' on table 'table1'. The relation will be ignored.",
);
});
});
});

0 comments on commit 996899b

Please sign in to comment.