fix: diff logic (#927)

This commit is contained in:
Guy Ben-Aharon
2025-09-20 19:40:04 +03:00
committed by GitHub
parent 93d72a896b
commit 1b8d51b73c
5 changed files with 1629 additions and 215 deletions

View File

@@ -0,0 +1,70 @@
import { z } from 'zod';
import type { Area } from '../area';
export type AreaDiffAttribute = keyof Pick<Area, 'name' | 'color'>;
const areaDiffAttributeSchema: z.ZodType<AreaDiffAttribute> = z.union([
z.literal('name'),
z.literal('color'),
]);
export interface AreaDiffChanged {
object: 'area';
type: 'changed';
areaId: string;
attribute: AreaDiffAttribute;
oldValue?: string | null;
newValue?: string | null;
}
export const AreaDiffChangedSchema: z.ZodType<AreaDiffChanged> = z.object({
object: z.literal('area'),
type: z.literal('changed'),
areaId: z.string(),
attribute: areaDiffAttributeSchema,
oldValue: z.string().or(z.null()).optional(),
newValue: z.string().or(z.null()).optional(),
});
export interface AreaDiffRemoved {
object: 'area';
type: 'removed';
areaId: string;
}
export const AreaDiffRemovedSchema: z.ZodType<AreaDiffRemoved> = z.object({
object: z.literal('area'),
type: z.literal('removed'),
areaId: z.string(),
});
export interface AreaDiffAdded<T = Area> {
object: 'area';
type: 'added';
areaAdded: T;
}
export const createAreaDiffAddedSchema = <T = Area>(
areaSchema: z.ZodType<T>
): z.ZodType<AreaDiffAdded<T>> => {
return z.object({
object: z.literal('area'),
type: z.literal('added'),
areaAdded: areaSchema,
}) as z.ZodType<AreaDiffAdded<T>>;
};
export type AreaDiff<T = Area> =
| AreaDiffChanged
| AreaDiffRemoved
| AreaDiffAdded<T>;
export const createAreaDiffSchema = <T = Area>(
areaSchema: z.ZodType<T>
): z.ZodType<AreaDiff<T>> => {
return z.union([
AreaDiffChangedSchema,
AreaDiffRemovedSchema,
createAreaDiffAddedSchema(areaSchema),
]) as z.ZodType<AreaDiff<T>>;
};

View File

@@ -2,7 +2,14 @@ import type { Diagram } from '@/lib/domain/diagram';
import type { DBField } from '@/lib/domain/db-field';
import type { DBIndex } from '@/lib/domain/db-index';
import type { ChartDBDiff, DiffMap, DiffObject } from '@/lib/domain/diff/diff';
import type { FieldDiffAttribute } from '@/lib/domain/diff/field-diff';
import type {
FieldDiff,
FieldDiffAttribute,
} from '@/lib/domain/diff/field-diff';
import type { TableDiff, TableDiffAttribute } from '../table-diff';
import type { AreaDiff, AreaDiffAttribute } from '../area-diff';
import type { IndexDiff } from '../index-diff';
import type { RelationshipDiff } from '../relationship-diff';
export function getDiffMapKey({
diffObject,
@@ -18,23 +25,64 @@ export function getDiffMapKey({
: `${diffObject}-${objectId}`;
}
export interface GenerateDiffOptions {
includeTables?: boolean;
includeFields?: boolean;
includeIndexes?: boolean;
includeRelationships?: boolean;
includeAreas?: boolean;
attributes?: {
tables?: TableDiffAttribute[];
fields?: FieldDiffAttribute[];
areas?: AreaDiffAttribute[];
};
changeTypes?: {
tables?: TableDiff['type'][];
fields?: FieldDiff['type'][];
indexes?: IndexDiff['type'][];
relationships?: RelationshipDiff['type'][];
areas?: AreaDiff['type'][];
};
}
export function generateDiff({
diagram,
newDiagram,
options = {
includeTables: true,
includeFields: true,
includeIndexes: true,
includeRelationships: true,
includeAreas: false,
attributes: {},
changeTypes: {},
},
}: {
diagram: Diagram;
newDiagram: Diagram;
options?: GenerateDiffOptions;
}): {
diffMap: DiffMap;
changedTables: Map<string, boolean>;
changedFields: Map<string, boolean>;
changedAreas: Map<string, boolean>;
} {
const newDiffs = new Map<string, ChartDBDiff>();
const changedTables = new Map<string, boolean>();
const changedFields = new Map<string, boolean>();
const changedAreas = new Map<string, boolean>();
// Compare tables
compareTables({ diagram, newDiagram, diffMap: newDiffs, changedTables });
if (options.includeTables) {
compareTables({
diagram,
newDiagram,
diffMap: newDiffs,
changedTables,
attributes: options.attributes?.tables,
changeTypes: options.changeTypes?.tables,
});
}
// Compare fields and indexes for matching tables
compareTableContents({
@@ -43,12 +91,32 @@ export function generateDiff({
diffMap: newDiffs,
changedTables,
changedFields,
options,
});
// Compare relationships
compareRelationships({ diagram, newDiagram, diffMap: newDiffs });
if (options.includeRelationships) {
compareRelationships({
diagram,
newDiagram,
diffMap: newDiffs,
changeTypes: options.changeTypes?.relationships,
});
}
return { diffMap: newDiffs, changedTables, changedFields };
// Compare areas if enabled
if (options.includeAreas) {
compareAreas({
diagram,
newDiagram,
diffMap: newDiffs,
changedAreas,
attributes: options.attributes?.areas,
changeTypes: options.changeTypes?.areas,
});
}
return { diffMap: newDiffs, changedTables, changedFields, changedAreas };
}
// Compare tables between diagrams
@@ -57,112 +125,150 @@ function compareTables({
newDiagram,
diffMap,
changedTables,
attributes,
changeTypes,
}: {
diagram: Diagram;
newDiagram: Diagram;
diffMap: DiffMap;
changedTables: Map<string, boolean>;
attributes?: TableDiffAttribute[];
changeTypes?: TableDiff['type'][];
}) {
const oldTables = diagram.tables || [];
const newTables = newDiagram.tables || [];
// If changeTypes is empty array, don't check any changes
if (changeTypes && changeTypes.length === 0) {
return;
}
// If changeTypes is undefined, check all types
const typesToCheck = changeTypes ?? ['added', 'removed', 'changed'];
// Check for added tables
for (const newTable of newTables) {
if (!oldTables.find((t) => t.id === newTable.id)) {
diffMap.set(
getDiffMapKey({ diffObject: 'table', objectId: newTable.id }),
{
object: 'table',
type: 'added',
tableAdded: newTable,
}
);
changedTables.set(newTable.id, true);
if (typesToCheck.includes('added')) {
for (const newTable of newTables) {
if (!oldTables.find((t) => t.id === newTable.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'table',
objectId: newTable.id,
}),
{
object: 'table',
type: 'added',
tableAdded: newTable,
}
);
changedTables.set(newTable.id, true);
}
}
}
// Check for removed tables
for (const oldTable of oldTables) {
if (!newTables.find((t) => t.id === oldTable.id)) {
diffMap.set(
getDiffMapKey({ diffObject: 'table', objectId: oldTable.id }),
{
object: 'table',
type: 'removed',
tableId: oldTable.id,
}
);
changedTables.set(oldTable.id, true);
if (typesToCheck.includes('removed')) {
for (const oldTable of oldTables) {
if (!newTables.find((t) => t.id === oldTable.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'table',
objectId: oldTable.id,
}),
{
object: 'table',
type: 'removed',
tableId: oldTable.id,
}
);
changedTables.set(oldTable.id, true);
}
}
}
// Check for table name, comments and color changes
for (const oldTable of oldTables) {
const newTable = newTables.find((t) => t.id === oldTable.id);
if (typesToCheck.includes('changed')) {
for (const oldTable of oldTables) {
const newTable = newTables.find((t) => t.id === oldTable.id);
if (!newTable) continue;
if (!newTable) continue;
if (oldTable.name !== newTable.name) {
diffMap.set(
getDiffMapKey({
diffObject: 'table',
objectId: oldTable.id,
attribute: 'name',
}),
{
object: 'table',
type: 'changed',
tableId: oldTable.id,
attribute: 'name',
newValue: newTable.name,
oldValue: oldTable.name,
}
);
// If attributes are specified, only check those attributes
const attributesToCheck: TableDiffAttribute[] = attributes ?? [
'name',
'comments',
'color',
];
changedTables.set(oldTable.id, true);
}
if (
attributesToCheck.includes('name') &&
oldTable.name !== newTable.name
) {
diffMap.set(
getDiffMapKey({
diffObject: 'table',
objectId: oldTable.id,
attribute: 'name',
}),
{
object: 'table',
type: 'changed',
tableId: oldTable.id,
attribute: 'name',
newValue: newTable.name,
oldValue: oldTable.name,
}
);
if (
(oldTable.comments || newTable.comments) &&
oldTable.comments !== newTable.comments
) {
diffMap.set(
getDiffMapKey({
diffObject: 'table',
objectId: oldTable.id,
attribute: 'comments',
}),
{
object: 'table',
type: 'changed',
tableId: oldTable.id,
attribute: 'comments',
newValue: newTable.comments,
oldValue: oldTable.comments,
}
);
changedTables.set(oldTable.id, true);
}
changedTables.set(oldTable.id, true);
}
if (
attributesToCheck.includes('comments') &&
(oldTable.comments || newTable.comments) &&
oldTable.comments !== newTable.comments
) {
diffMap.set(
getDiffMapKey({
diffObject: 'table',
objectId: oldTable.id,
attribute: 'comments',
}),
{
object: 'table',
type: 'changed',
tableId: oldTable.id,
attribute: 'comments',
newValue: newTable.comments,
oldValue: oldTable.comments,
}
);
if (oldTable.color !== newTable.color) {
diffMap.set(
getDiffMapKey({
diffObject: 'table',
objectId: oldTable.id,
attribute: 'color',
}),
{
object: 'table',
type: 'changed',
tableId: oldTable.id,
attribute: 'color',
newValue: newTable.color,
oldValue: oldTable.color,
}
);
changedTables.set(oldTable.id, true);
}
changedTables.set(oldTable.id, true);
if (
attributesToCheck.includes('color') &&
oldTable.color !== newTable.color
) {
diffMap.set(
getDiffMapKey({
diffObject: 'table',
objectId: oldTable.id,
attribute: 'color',
}),
{
object: 'table',
type: 'changed',
tableId: oldTable.id,
attribute: 'color',
newValue: newTable.color,
oldValue: oldTable.color,
}
);
changedTables.set(oldTable.id, true);
}
}
}
}
@@ -174,12 +280,14 @@ function compareTableContents({
diffMap,
changedTables,
changedFields,
options,
}: {
diagram: Diagram;
newDiagram: Diagram;
diffMap: DiffMap;
changedTables: Map<string, boolean>;
changedFields: Map<string, boolean>;
options?: GenerateDiffOptions;
}) {
const oldTables = diagram.tables || [];
const newTables = newDiagram.tables || [];
@@ -190,23 +298,30 @@ function compareTableContents({
if (!newTable) continue;
// Compare fields
compareFields({
tableId: oldTable.id,
oldFields: oldTable.fields,
newFields: newTable.fields,
diffMap,
changedTables,
changedFields,
});
if (options?.includeFields) {
compareFields({
tableId: oldTable.id,
oldFields: oldTable.fields,
newFields: newTable.fields,
diffMap,
changedTables,
changedFields,
attributes: options?.attributes?.fields,
changeTypes: options?.changeTypes?.fields,
});
}
// Compare indexes
compareIndexes({
tableId: oldTable.id,
oldIndexes: oldTable.indexes,
newIndexes: newTable.indexes,
diffMap,
changedTables,
});
if (options?.includeIndexes) {
compareIndexes({
tableId: oldTable.id,
oldIndexes: oldTable.indexes,
newIndexes: newTable.indexes,
diffMap,
changedTables,
changeTypes: options?.changeTypes?.indexes,
});
}
}
}
@@ -218,6 +333,8 @@ function compareFields({
diffMap,
changedTables,
changedFields,
attributes,
changeTypes,
}: {
tableId: string;
oldFields: DBField[];
@@ -225,62 +342,78 @@ function compareFields({
diffMap: DiffMap;
changedTables: Map<string, boolean>;
changedFields: Map<string, boolean>;
attributes?: FieldDiffAttribute[];
changeTypes?: FieldDiff['type'][];
}) {
// If changeTypes is empty array, don't check any changes
if (changeTypes && changeTypes.length === 0) {
return;
}
// If changeTypes is undefined, check all types
const typesToCheck = changeTypes ?? ['added', 'removed', 'changed'];
// Check for added fields
for (const newField of newFields) {
if (!oldFields.find((f) => f.id === newField.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'field',
objectId: newField.id,
}),
{
object: 'field',
type: 'added',
newField,
tableId,
}
);
changedTables.set(tableId, true);
changedFields.set(newField.id, true);
if (typesToCheck.includes('added')) {
for (const newField of newFields) {
if (!oldFields.find((f) => f.id === newField.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'field',
objectId: newField.id,
}),
{
object: 'field',
type: 'added',
newField,
tableId,
}
);
changedTables.set(tableId, true);
changedFields.set(newField.id, true);
}
}
}
// Check for removed fields
for (const oldField of oldFields) {
if (!newFields.find((f) => f.id === oldField.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'field',
objectId: oldField.id,
}),
{
object: 'field',
type: 'removed',
fieldId: oldField.id,
tableId,
}
);
if (typesToCheck.includes('removed')) {
for (const oldField of oldFields) {
if (!newFields.find((f) => f.id === oldField.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'field',
objectId: oldField.id,
}),
{
object: 'field',
type: 'removed',
fieldId: oldField.id,
tableId,
}
);
changedTables.set(tableId, true);
changedFields.set(oldField.id, true);
changedTables.set(tableId, true);
changedFields.set(oldField.id, true);
}
}
}
// Check for field changes
for (const oldField of oldFields) {
const newField = newFields.find((f) => f.id === oldField.id);
if (!newField) continue;
if (typesToCheck.includes('changed')) {
for (const oldField of oldFields) {
const newField = newFields.find((f) => f.id === oldField.id);
if (!newField) continue;
// Compare basic field properties
compareFieldProperties({
tableId,
oldField,
newField,
diffMap,
changedTables,
changedFields,
});
// Compare basic field properties
compareFieldProperties({
tableId,
oldField,
newField,
diffMap,
changedTables,
changedFields,
attributes,
});
}
}
}
@@ -292,6 +425,7 @@ function compareFieldProperties({
diffMap,
changedTables,
changedFields,
attributes,
}: {
tableId: string;
oldField: DBField;
@@ -299,30 +433,57 @@ function compareFieldProperties({
diffMap: DiffMap;
changedTables: Map<string, boolean>;
changedFields: Map<string, boolean>;
attributes?: FieldDiffAttribute[];
}) {
// If attributes are specified, only check those attributes
const attributesToCheck: FieldDiffAttribute[] = attributes ?? [
'name',
'type',
'primaryKey',
'unique',
'nullable',
'comments',
'characterMaximumLength',
'scale',
'precision',
];
const changedAttributes: FieldDiffAttribute[] = [];
if (oldField.name !== newField.name) {
if (attributesToCheck.includes('name') && oldField.name !== newField.name) {
changedAttributes.push('name');
}
if (oldField.type.id !== newField.type.id) {
if (
attributesToCheck.includes('type') &&
oldField.type.id !== newField.type.id
) {
changedAttributes.push('type');
}
if (oldField.primaryKey !== newField.primaryKey) {
if (
attributesToCheck.includes('primaryKey') &&
oldField.primaryKey !== newField.primaryKey
) {
changedAttributes.push('primaryKey');
}
if (oldField.unique !== newField.unique) {
if (
attributesToCheck.includes('unique') &&
oldField.unique !== newField.unique
) {
changedAttributes.push('unique');
}
if (oldField.nullable !== newField.nullable) {
if (
attributesToCheck.includes('nullable') &&
oldField.nullable !== newField.nullable
) {
changedAttributes.push('nullable');
}
if (
attributesToCheck.includes('comments') &&
(newField.comments || oldField.comments) &&
oldField.comments !== newField.comments
) {
@@ -330,6 +491,7 @@ function compareFieldProperties({
}
if (
attributesToCheck.includes('characterMaximumLength') &&
(newField.characterMaximumLength || oldField.characterMaximumLength) &&
oldField.characterMaximumLength !== newField.characterMaximumLength
) {
@@ -337,6 +499,7 @@ function compareFieldProperties({
}
if (
attributesToCheck.includes('scale') &&
(newField.scale || oldField.scale) &&
oldField.scale !== newField.scale
) {
@@ -344,6 +507,7 @@ function compareFieldProperties({
}
if (
attributesToCheck.includes('precision') &&
(newField.precision || oldField.precision) &&
oldField.precision !== newField.precision
) {
@@ -381,48 +545,61 @@ function compareIndexes({
newIndexes,
diffMap,
changedTables,
changeTypes,
}: {
tableId: string;
oldIndexes: DBIndex[];
newIndexes: DBIndex[];
diffMap: DiffMap;
changedTables: Map<string, boolean>;
changeTypes?: IndexDiff['type'][];
}) {
// If changeTypes is empty array, don't check any changes
if (changeTypes && changeTypes.length === 0) {
return;
}
// If changeTypes is undefined, check all types
const typesToCheck = changeTypes ?? ['added', 'removed'];
// Check for added indexes
for (const newIndex of newIndexes) {
if (!oldIndexes.find((i) => i.id === newIndex.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'index',
objectId: newIndex.id,
}),
{
object: 'index',
type: 'added',
newIndex,
tableId,
}
);
changedTables.set(tableId, true);
if (typesToCheck.includes('added')) {
for (const newIndex of newIndexes) {
if (!oldIndexes.find((i) => i.id === newIndex.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'index',
objectId: newIndex.id,
}),
{
object: 'index',
type: 'added',
newIndex,
tableId,
}
);
changedTables.set(tableId, true);
}
}
}
// Check for removed indexes
for (const oldIndex of oldIndexes) {
if (!newIndexes.find((i) => i.id === oldIndex.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'index',
objectId: oldIndex.id,
}),
{
object: 'index',
type: 'removed',
indexId: oldIndex.id,
tableId,
}
);
changedTables.set(tableId, true);
if (typesToCheck.includes('removed')) {
for (const oldIndex of oldIndexes) {
if (!newIndexes.find((i) => i.id === oldIndex.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'index',
objectId: oldIndex.id,
}),
{
object: 'index',
type: 'removed',
indexId: oldIndex.id,
tableId,
}
);
changedTables.set(tableId, true);
}
}
}
}
@@ -432,45 +609,185 @@ function compareRelationships({
diagram,
newDiagram,
diffMap,
changeTypes,
}: {
diagram: Diagram;
newDiagram: Diagram;
diffMap: DiffMap;
changeTypes?: RelationshipDiff['type'][];
}) {
// If changeTypes is empty array, don't check any changes
if (changeTypes && changeTypes.length === 0) {
return;
}
// If changeTypes is undefined, check all types
const typesToCheck = changeTypes ?? ['added', 'removed'];
const oldRelationships = diagram.relationships || [];
const newRelationships = newDiagram.relationships || [];
// Check for added relationships
for (const newRelationship of newRelationships) {
if (!oldRelationships.find((r) => r.id === newRelationship.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'relationship',
objectId: newRelationship.id,
}),
{
object: 'relationship',
type: 'added',
newRelationship,
}
);
if (typesToCheck.includes('added')) {
for (const newRelationship of newRelationships) {
if (!oldRelationships.find((r) => r.id === newRelationship.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'relationship',
objectId: newRelationship.id,
}),
{
object: 'relationship',
type: 'added',
newRelationship,
}
);
}
}
}
// Check for removed relationships
for (const oldRelationship of oldRelationships) {
if (!newRelationships.find((r) => r.id === oldRelationship.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'relationship',
objectId: oldRelationship.id,
}),
{
object: 'relationship',
type: 'removed',
relationshipId: oldRelationship.id,
}
);
if (typesToCheck.includes('removed')) {
for (const oldRelationship of oldRelationships) {
if (!newRelationships.find((r) => r.id === oldRelationship.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'relationship',
objectId: oldRelationship.id,
}),
{
object: 'relationship',
type: 'removed',
relationshipId: oldRelationship.id,
}
);
}
}
}
}
// Compare areas between diagrams
function compareAreas({
diagram,
newDiagram,
diffMap,
changedAreas,
attributes,
changeTypes,
}: {
diagram: Diagram;
newDiagram: Diagram;
diffMap: DiffMap;
changedAreas: Map<string, boolean>;
attributes?: AreaDiffAttribute[];
changeTypes?: AreaDiff['type'][];
}) {
const oldAreas = diagram.areas || [];
const newAreas = newDiagram.areas || [];
// If changeTypes is empty array, don't check any changes
if (changeTypes && changeTypes.length === 0) {
return;
}
// If changeTypes is undefined, check all types
const typesToCheck = changeTypes ?? ['added', 'removed', 'changed'];
// Check for added areas
if (typesToCheck.includes('added')) {
for (const newArea of newAreas) {
if (!oldAreas.find((a) => a.id === newArea.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'area',
objectId: newArea.id,
}),
{
object: 'area',
type: 'added',
areaAdded: newArea,
}
);
changedAreas.set(newArea.id, true);
}
}
}
// Check for removed areas
if (typesToCheck.includes('removed')) {
for (const oldArea of oldAreas) {
if (!newAreas.find((a) => a.id === oldArea.id)) {
diffMap.set(
getDiffMapKey({
diffObject: 'area',
objectId: oldArea.id,
}),
{
object: 'area',
type: 'removed',
areaId: oldArea.id,
}
);
changedAreas.set(oldArea.id, true);
}
}
}
// Check for area name and color changes
if (typesToCheck.includes('changed')) {
for (const oldArea of oldAreas) {
const newArea = newAreas.find((a) => a.id === oldArea.id);
if (!newArea) continue;
// If attributes are specified, only check those attributes
const attributesToCheck: AreaDiffAttribute[] = attributes ?? [
'name',
'color',
];
if (
attributesToCheck.includes('name') &&
oldArea.name !== newArea.name
) {
diffMap.set(
getDiffMapKey({
diffObject: 'area',
objectId: oldArea.id,
attribute: 'name',
}),
{
object: 'area',
type: 'changed',
areaId: oldArea.id,
attribute: 'name',
newValue: newArea.name,
oldValue: oldArea.name,
}
);
changedAreas.set(oldArea.id, true);
}
if (
attributesToCheck.includes('color') &&
oldArea.color !== newArea.color
) {
diffMap.set(
getDiffMapKey({
diffObject: 'area',
objectId: oldArea.id,
attribute: 'color',
}),
{
object: 'area',
type: 'changed',
areaId: oldArea.id,
attribute: 'color',
newValue: newArea.color,
oldValue: oldArea.color,
}
);
changedAreas.set(oldArea.id, true);
}
}
}
}

View File

@@ -8,36 +8,43 @@ import type { RelationshipDiff } from './relationship-diff';
import { createRelationshipDiffSchema } from './relationship-diff';
import type { TableDiff } from './table-diff';
import { createTableDiffSchema } from './table-diff';
import type { DBField, DBIndex, DBRelationship, DBTable } from '..';
import type { AreaDiff } from './area-diff';
import { createAreaDiffSchema } from './area-diff';
import type { DBField, DBIndex, DBRelationship, DBTable, Area } from '..';
export type ChartDBDiff<
TTable = DBTable,
TField = DBField,
TIndex = DBIndex,
TRelationship = DBRelationship,
TArea = Area,
> =
| TableDiff<TTable>
| FieldDiff<TField>
| IndexDiff<TIndex>
| RelationshipDiff<TRelationship>;
| RelationshipDiff<TRelationship>
| AreaDiff<TArea>;
export const createChartDBDiffSchema = <
TTable = DBTable,
TField = DBField,
TIndex = DBIndex,
TRelationship = DBRelationship,
TArea = Area,
>(
tableSchema: z.ZodType<TTable>,
fieldSchema: z.ZodType<TField>,
indexSchema: z.ZodType<TIndex>,
relationshipSchema: z.ZodType<TRelationship>
): z.ZodType<ChartDBDiff<TTable, TField, TIndex, TRelationship>> => {
relationshipSchema: z.ZodType<TRelationship>,
areaSchema: z.ZodType<TArea>
): z.ZodType<ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea>> => {
return z.union([
createTableDiffSchema(tableSchema),
createFieldDiffSchema(fieldSchema),
createIndexDiffSchema(indexSchema),
createRelationshipDiffSchema(relationshipSchema),
]) as z.ZodType<ChartDBDiff<TTable, TField, TIndex, TRelationship>>;
createAreaDiffSchema(areaSchema),
]) as z.ZodType<ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea>>;
};
export type DiffMap<
@@ -45,18 +52,21 @@ export type DiffMap<
TField = DBField,
TIndex = DBIndex,
TRelationship = DBRelationship,
> = Map<string, ChartDBDiff<TTable, TField, TIndex, TRelationship>>;
TArea = Area,
> = Map<string, ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea>>;
export type DiffObject<
TTable = DBTable,
TField = DBField,
TIndex = DBIndex,
TRelationship = DBRelationship,
TArea = Area,
> =
| TableDiff<TTable>['object']
| FieldDiff<TField>['object']
| IndexDiff<TIndex>['object']
| RelationshipDiff<TRelationship>['object'];
| RelationshipDiff<TRelationship>['object']
| AreaDiff<TArea>['object'];
type ExtractDiffKind<T> = T extends { object: infer O; type: infer Type }
? T extends { attribute: infer A }
@@ -69,16 +79,18 @@ export type DiffKind<
TField = DBField,
TIndex = DBIndex,
TRelationship = DBRelationship,
> = ExtractDiffKind<ChartDBDiff<TTable, TField, TIndex, TRelationship>>;
TArea = Area,
> = ExtractDiffKind<ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea>>;
export const isDiffOfKind = <
TTable = DBTable,
TField = DBField,
TIndex = DBIndex,
TRelationship = DBRelationship,
TArea = Area,
>(
diff: ChartDBDiff<TTable, TField, TIndex, TRelationship>,
kind: DiffKind<TTable, TField, TIndex, TRelationship>
diff: ChartDBDiff<TTable, TField, TIndex, TRelationship, TArea>,
kind: DiffKind<TTable, TField, TIndex, TRelationship, TArea>
): boolean => {
if ('attribute' in kind) {
return (

View File

@@ -0,0 +1,687 @@
import { describe, expect, it } from 'vitest';
import { applyIds } from '../apply-ids';
import {
DatabaseType,
DBCustomTypeKind,
type Diagram,
type DBTable,
type DBField,
type DBIndex,
type DBRelationship,
type DBDependency,
type DBCustomType,
} from '../../domain';
describe('applyIds', () => {
const createBaseDiagram = (overrides?: Partial<Diagram>): Diagram => ({
id: 'diagram1',
name: 'Test Diagram',
databaseType: DatabaseType.POSTGRESQL,
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
});
const createTable = (overrides: Partial<DBTable>): DBTable => ({
id: 'table-1',
name: 'table',
schema: 'public',
x: 0,
y: 0,
fields: [],
indexes: [],
color: '#000000',
comments: null,
isView: false,
createdAt: Date.now(),
...overrides,
});
const createField = (overrides: Partial<DBField>): DBField => ({
id: 'field-1',
name: 'field',
type: { id: 'integer', name: 'integer' },
primaryKey: false,
nullable: false,
unique: false,
comments: null,
collation: null,
createdAt: Date.now(),
...overrides,
});
const createIndex = (overrides: Partial<DBIndex>): DBIndex => ({
id: 'index-1',
name: 'index',
unique: false,
fieldIds: [],
createdAt: Date.now(),
...overrides,
});
const createRelationship = (
overrides: Partial<DBRelationship>
): DBRelationship => ({
id: 'rel-1',
name: 'relationship',
sourceTableId: 'table-1',
sourceFieldId: 'field-1',
targetTableId: 'table-2',
targetFieldId: 'field-2',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
...overrides,
});
const createDependency = (
overrides: Partial<DBDependency>
): DBDependency => ({
id: 'dep-1',
tableId: 'table-1',
dependentTableId: 'table-2',
createdAt: Date.now(),
...overrides,
});
const createCustomType = (
overrides: Partial<DBCustomType>
): DBCustomType => ({
id: 'type-1',
name: 'custom_type',
schema: 'public',
kind: DBCustomTypeKind.enum,
values: [],
...overrides,
});
describe('table ID mapping', () => {
it('should preserve table IDs when tables match by name and schema', () => {
const sourceDiagram = createBaseDiagram({
tables: [
createTable({
id: 'source-table-1',
name: 'users',
schema: 'public',
}),
createTable({
id: 'source-table-2',
name: 'posts',
schema: 'public',
}),
],
});
const targetDiagram = createBaseDiagram({
tables: [
createTable({
id: 'target-table-1',
name: 'users',
schema: 'public',
x: 100,
y: 100,
color: '#ff0000',
comments: 'Users table',
}),
createTable({
id: 'target-table-2',
name: 'posts',
schema: 'public',
x: 200,
y: 200,
color: '#00ff00',
}),
],
});
const result = applyIds({ sourceDiagram, targetDiagram });
expect(result.tables).toHaveLength(2);
expect(result.tables?.[0].id).toBe('source-table-1');
expect(result.tables?.[0].name).toBe('users');
expect(result.tables?.[0].x).toBe(100); // Should keep target's position
expect(result.tables?.[0].color).toBe('#ff0000'); // Should keep target's color
expect(result.tables?.[1].id).toBe('source-table-2');
expect(result.tables?.[1].name).toBe('posts');
});
it('should keep target table IDs when no matching source table exists', () => {
const sourceDiagram = createBaseDiagram({
tables: [
createTable({
id: 'source-table-1',
name: 'users',
schema: 'public',
}),
],
});
const targetDiagram = createBaseDiagram({
tables: [
createTable({
id: 'target-table-1',
name: 'orders',
schema: 'public',
x: 100,
y: 100,
color: '#ff0000',
}),
],
});
const result = applyIds({ sourceDiagram, targetDiagram });
expect(result.tables).toHaveLength(1);
expect(result.tables?.[0].id).toBe('target-table-1'); // Should keep target ID
expect(result.tables?.[0].name).toBe('orders');
});
});
describe('field ID mapping', () => {
it('should preserve field IDs when fields match by name within the same table', () => {
const sourceDiagram = createBaseDiagram({
tables: [
createTable({
id: 'source-table-1',
name: 'users',
schema: 'public',
fields: [
createField({
id: 'source-field-1',
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
nullable: false,
unique: true,
}),
createField({
id: 'source-field-2',
name: 'email',
type: { id: 'varchar', name: 'varchar' },
primaryKey: false,
nullable: false,
unique: true,
}),
],
}),
],
});
const targetDiagram = createBaseDiagram({
tables: [
createTable({
id: 'target-table-1',
name: 'users',
schema: 'public',
x: 100,
y: 100,
color: '#ff0000',
fields: [
createField({
id: 'target-field-1',
name: 'id',
type: { id: 'bigint', name: 'bigint' },
primaryKey: true,
nullable: false,
unique: true,
comments: 'Primary key',
}),
createField({
id: 'target-field-2',
name: 'email',
type: { id: 'text', name: 'text' },
primaryKey: false,
nullable: true,
unique: false,
}),
],
}),
],
});
const result = applyIds({ sourceDiagram, targetDiagram });
expect(result.tables?.[0].fields).toHaveLength(2);
expect(result.tables?.[0].fields[0].id).toBe('source-field-1');
expect(result.tables?.[0].fields[0].name).toBe('id');
expect(result.tables?.[0].fields[0].type.id).toBe('bigint'); // Should keep target's type
expect(result.tables?.[0].fields[1].id).toBe('source-field-2');
expect(result.tables?.[0].fields[1].name).toBe('email');
expect(result.tables?.[0].fields[1].nullable).toBe(true); // Should keep target's nullable
});
});
describe('index ID mapping', () => {
it('should preserve index IDs and update field references', () => {
const sourceDiagram = createBaseDiagram({
tables: [
createTable({
id: 'source-table-1',
name: 'users',
schema: 'public',
fields: [
createField({
id: 'source-field-1',
name: 'email',
type: { id: 'varchar', name: 'varchar' },
}),
],
indexes: [
createIndex({
id: 'source-index-1',
name: 'idx_email',
unique: true,
fieldIds: ['source-field-1'],
}),
],
}),
],
});
const targetDiagram = createBaseDiagram({
tables: [
createTable({
id: 'target-table-1',
name: 'users',
schema: 'public',
x: 100,
y: 100,
color: '#ff0000',
fields: [
createField({
id: 'target-field-1',
name: 'email',
type: { id: 'text', name: 'text' },
}),
],
indexes: [
createIndex({
id: 'target-index-1',
name: 'idx_email',
unique: false,
fieldIds: ['target-field-1'],
}),
],
}),
],
});
const result = applyIds({ sourceDiagram, targetDiagram });
expect(result.tables?.[0].indexes).toHaveLength(1);
expect(result.tables?.[0].indexes[0].id).toBe('source-index-1');
expect(result.tables?.[0].indexes[0].fieldIds).toEqual([
'source-field-1',
]); // Should update field reference
expect(result.tables?.[0].indexes[0].unique).toBe(false); // Should keep target's unique setting
});
});
describe('relationship ID mapping', () => {
it('should preserve relationship IDs and update table/field references', () => {
const sourceDiagram = createBaseDiagram({
tables: [
createTable({
id: 'source-table-1',
name: 'users',
schema: 'public',
fields: [
createField({
id: 'source-field-1',
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
unique: true,
}),
],
}),
createTable({
id: 'source-table-2',
name: 'posts',
schema: 'public',
fields: [
createField({
id: 'source-field-2',
name: 'user_id',
type: { id: 'integer', name: 'integer' },
}),
],
}),
],
relationships: [
createRelationship({
id: 'source-rel-1',
name: 'fk_posts_users',
sourceTableId: 'source-table-2',
sourceFieldId: 'source-field-2',
targetTableId: 'source-table-1',
targetFieldId: 'source-field-1',
}),
],
});
const targetDiagram = createBaseDiagram({
tables: [
createTable({
id: 'target-table-1',
name: 'users',
schema: 'public',
x: 100,
y: 100,
color: '#ff0000',
fields: [
createField({
id: 'target-field-1',
name: 'id',
type: { id: 'bigint', name: 'bigint' },
primaryKey: true,
unique: true,
}),
],
}),
createTable({
id: 'target-table-2',
name: 'posts',
schema: 'public',
x: 200,
y: 200,
color: '#00ff00',
fields: [
createField({
id: 'target-field-2',
name: 'user_id',
type: { id: 'bigint', name: 'bigint' },
nullable: true,
}),
],
}),
],
relationships: [
createRelationship({
id: 'target-rel-1',
name: 'fk_posts_users',
sourceTableId: 'target-table-2',
sourceFieldId: 'target-field-2',
targetTableId: 'target-table-1',
targetFieldId: 'target-field-1',
}),
],
});
const result = applyIds({ sourceDiagram, targetDiagram });
expect(result.relationships).toHaveLength(1);
expect(result.relationships?.[0].id).toBe('source-rel-1');
expect(result.relationships?.[0].sourceTableId).toBe(
'source-table-2'
);
expect(result.relationships?.[0].sourceFieldId).toBe(
'source-field-2'
);
expect(result.relationships?.[0].targetTableId).toBe(
'source-table-1'
);
expect(result.relationships?.[0].targetFieldId).toBe(
'source-field-1'
);
});
});
describe('dependency ID mapping', () => {
it('should preserve dependency IDs and update table references', () => {
const sourceDiagram = createBaseDiagram({
tables: [
createTable({
id: 'source-table-1',
name: 'users',
schema: 'public',
}),
createTable({
id: 'source-table-2',
name: 'user_view',
schema: 'public',
isView: true,
}),
],
dependencies: [
createDependency({
id: 'source-dep-1',
tableId: 'source-table-2',
dependentTableId: 'source-table-1',
}),
],
});
const targetDiagram = createBaseDiagram({
tables: [
createTable({
id: 'target-table-1',
name: 'users',
schema: 'public',
x: 100,
y: 100,
color: '#ff0000',
}),
createTable({
id: 'target-table-2',
name: 'user_view',
schema: 'public',
x: 200,
y: 200,
color: '#00ff00',
isView: true,
}),
],
dependencies: [
createDependency({
id: 'target-dep-1',
tableId: 'target-table-2',
dependentTableId: 'target-table-1',
}),
],
});
const result = applyIds({ sourceDiagram, targetDiagram });
expect(result.dependencies).toHaveLength(1);
expect(result.dependencies?.[0].id).toBe('source-dep-1');
expect(result.dependencies?.[0].tableId).toBe('source-table-2');
expect(result.dependencies?.[0].dependentTableId).toBe(
'source-table-1'
);
});
});
describe('custom type ID mapping', () => {
it('should preserve custom type IDs when types match by name and schema', () => {
const sourceDiagram = createBaseDiagram({
customTypes: [
createCustomType({
id: 'source-type-1',
name: 'user_role',
schema: 'public',
values: ['admin', 'user', 'guest'],
}),
],
});
const targetDiagram = createBaseDiagram({
customTypes: [
createCustomType({
id: 'target-type-1',
name: 'user_role',
schema: 'public',
values: ['admin', 'user', 'guest', 'moderator'],
}),
],
});
const result = applyIds({ sourceDiagram, targetDiagram });
expect(result.customTypes).toHaveLength(1);
expect(result.customTypes?.[0].id).toBe('source-type-1');
expect(result.customTypes?.[0].values).toEqual([
'admin',
'user',
'guest',
'moderator',
]); // Should keep target's values
});
});
describe('complex scenarios', () => {
it('should handle partial matches correctly', () => {
const sourceDiagram = createBaseDiagram({
tables: [
createTable({
id: 'source-table-1',
name: 'users',
schema: 'public',
fields: [
createField({
id: 'source-field-1',
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
unique: true,
}),
createField({
id: 'source-field-2',
name: 'email',
type: { id: 'varchar', name: 'varchar' },
unique: true,
}),
],
}),
],
});
const targetDiagram = createBaseDiagram({
tables: [
createTable({
id: 'target-table-1',
name: 'users',
schema: 'public',
x: 100,
y: 100,
color: '#ff0000',
fields: [
createField({
id: 'target-field-1',
name: 'id',
type: { id: 'bigint', name: 'bigint' },
primaryKey: true,
unique: true,
}),
createField({
id: 'target-field-3',
name: 'username',
type: { id: 'varchar', name: 'varchar' },
unique: true,
}),
],
}),
createTable({
id: 'target-table-2',
name: 'posts',
schema: 'public',
x: 200,
y: 200,
color: '#00ff00',
}),
],
});
const result = applyIds({ sourceDiagram, targetDiagram });
expect(result.tables).toHaveLength(2);
expect(result.tables?.[0].id).toBe('source-table-1');
expect(result.tables?.[0].fields).toHaveLength(2);
expect(result.tables?.[0].fields[0].id).toBe('source-field-1'); // Matched field
expect(result.tables?.[0].fields[1].id).toBe('target-field-3'); // Unmatched field keeps target ID
expect(result.tables?.[1].id).toBe('target-table-2'); // Unmatched table keeps target ID
});
it('should handle different schemas correctly', () => {
const sourceDiagram = createBaseDiagram({
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: 'source-table-1',
name: 'users',
schema: 'auth',
}),
],
});
const targetDiagram = createBaseDiagram({
databaseType: DatabaseType.POSTGRESQL,
tables: [
createTable({
id: 'target-table-1',
name: 'users',
schema: 'public',
x: 100,
y: 100,
color: '#ff0000',
}),
],
});
const result = applyIds({ sourceDiagram, targetDiagram });
expect(result.tables?.[0].id).toBe('target-table-1'); // Different schemas, no match
});
it('should handle empty diagrams', () => {
const sourceDiagram = createBaseDiagram();
const targetDiagram = createBaseDiagram();
const result = applyIds({ sourceDiagram, targetDiagram });
expect(result).toEqual(targetDiagram);
});
it('should return target diagram unchanged when source has no matching entities', () => {
const sourceDiagram = createBaseDiagram({
tables: [
createTable({
id: 'source-table-1',
name: 'products',
schema: 'inventory',
}),
],
});
const targetDiagram = createBaseDiagram({
tables: [
createTable({
id: 'target-table-1',
name: 'users',
schema: 'public',
x: 100,
y: 100,
color: '#ff0000',
fields: [
createField({
id: 'target-field-1',
name: 'id',
type: { id: 'integer', name: 'integer' },
primaryKey: true,
unique: true,
}),
],
}),
],
});
const result = applyIds({ sourceDiagram, targetDiagram });
// Should keep all target IDs since nothing matches
expect(result.tables?.[0].id).toBe('target-table-1');
expect(result.tables?.[0].fields[0].id).toBe('target-field-1');
expect(result.tables?.[0].name).toBe('users');
expect(result.tables?.[0].schema).toBe('public');
});
});
});

328
src/lib/utils/apply-ids.ts Normal file
View File

@@ -0,0 +1,328 @@
import { defaultSchemas } from '../data/default-schemas';
import type { DBCustomType, DBTable, Diagram } from '../domain';
const createTableKey = ({
table,
defaultSchema,
}: {
table: DBTable;
defaultSchema?: string;
}) => {
return `${table.schema ?? defaultSchema ?? ''}::${table.name}`;
};
const createFieldKey = ({
table,
fieldName,
defaultSchema,
}: {
table: DBTable;
fieldName: string;
defaultSchema?: string;
}) => {
return `${table.schema ?? defaultSchema ?? ''}::${table.name}::${fieldName}`;
};
const createIndexKey = ({
table,
indexName,
defaultSchema,
}: {
table: DBTable;
indexName: string;
defaultSchema?: string;
}) => {
return `${table.schema ?? defaultSchema ?? ''}::${table.name}::${indexName}`;
};
const createRelationshipKey = ({
relationshipName,
defaultSchema,
}: {
relationshipName: string;
defaultSchema?: string;
}) => {
return `${defaultSchema ?? ''}::${relationshipName}`;
};
const createDependencyKey = ({
table,
dependentTable,
defaultSchema,
}: {
table: DBTable;
dependentTable: DBTable;
defaultSchema?: string;
}) => {
return `${table.schema ?? defaultSchema ?? ''}::${table.name}::${dependentTable.schema ?? defaultSchema ?? ''}::${dependentTable.name}`;
};
const createCustomTypeKey = ({
customType,
defaultSchema,
}: {
customType: DBCustomType;
defaultSchema?: string;
}) => {
return `${customType.schema ?? defaultSchema ?? ''}::${customType.name}`;
};
export const applyIds = ({
sourceDiagram,
targetDiagram,
}: {
sourceDiagram: Diagram;
targetDiagram: Diagram;
}): Diagram => {
// Create a mapping of old IDs to new IDs
const tablesIdMapping = new Map<string, string>();
const fieldsIdMapping = new Map<string, string>();
const indexesIdMapping = new Map<string, string>();
const relationshipsIdMapping = new Map<string, string>();
const dependenciesIdMapping = new Map<string, string>();
const customTypesIdMapping = new Map<string, string>();
const sourceDefaultSchema = defaultSchemas[sourceDiagram.databaseType];
const targetDefaultSchema = defaultSchemas[targetDiagram.databaseType];
// build idMapping
sourceDiagram?.tables?.forEach((sourceTable) => {
const sourceKey = createTableKey({
table: sourceTable,
defaultSchema: sourceDefaultSchema,
});
tablesIdMapping.set(sourceKey, sourceTable.id);
sourceTable.fields.forEach((field) => {
const fieldKey = createFieldKey({
table: sourceTable,
fieldName: field.name,
defaultSchema: sourceDefaultSchema,
});
fieldsIdMapping.set(fieldKey, field.id);
});
sourceTable.indexes.forEach((index) => {
const indexKey = createIndexKey({
table: sourceTable,
indexName: index.name,
defaultSchema: sourceDefaultSchema,
});
indexesIdMapping.set(indexKey, index.id);
});
});
sourceDiagram.relationships?.forEach((relationship) => {
const relationshipKey = createRelationshipKey({
relationshipName: relationship.name,
defaultSchema: sourceDefaultSchema,
});
relationshipsIdMapping.set(relationshipKey, relationship.id);
});
sourceDiagram.dependencies?.forEach((dependency) => {
const table = sourceDiagram.tables?.find(
(t) => t.id === dependency.tableId
);
const dependentTable = sourceDiagram.tables?.find(
(t) => t.id === dependency.dependentTableId
);
if (!table || !dependentTable) return;
const dependencyKey = createDependencyKey({
table,
dependentTable,
defaultSchema: sourceDefaultSchema,
});
dependenciesIdMapping.set(dependencyKey, dependency.id);
});
sourceDiagram.customTypes?.forEach((customType) => {
const customTypeKey = createCustomTypeKey({
customType,
defaultSchema: sourceDefaultSchema,
});
customTypesIdMapping.set(customTypeKey, customType.id);
});
// Map current ID -> new ID for target diagram entities
const targetTableIdMapping = new Map<string, string>();
const targetFieldIdMapping = new Map<string, string>();
const targetIndexIdMapping = new Map<string, string>();
const targetRelationshipIdMapping = new Map<string, string>();
const targetDependencyIdMapping = new Map<string, string>();
const targetCustomTypeIdMapping = new Map<string, string>();
targetDiagram?.tables?.forEach((targetTable) => {
const targetKey = createTableKey({
table: targetTable,
defaultSchema: targetDefaultSchema,
});
const newId = tablesIdMapping.get(targetKey);
if (newId) {
targetTableIdMapping.set(targetTable.id, newId);
}
targetTable.fields.forEach((field) => {
const fieldKey = createFieldKey({
table: targetTable,
fieldName: field.name,
defaultSchema: targetDefaultSchema,
});
const newFieldId = fieldsIdMapping.get(fieldKey);
if (newFieldId) {
targetFieldIdMapping.set(field.id, newFieldId);
}
});
targetTable.indexes.forEach((index) => {
const indexKey = createIndexKey({
table: targetTable,
indexName: index.name,
defaultSchema: targetDefaultSchema,
});
const newIndexId = indexesIdMapping.get(indexKey);
if (newIndexId) {
targetIndexIdMapping.set(index.id, newIndexId);
}
});
});
targetDiagram.relationships?.forEach((relationship) => {
const relationshipKey = createRelationshipKey({
relationshipName: relationship.name,
defaultSchema: targetDefaultSchema,
});
const newId = relationshipsIdMapping.get(relationshipKey);
if (newId) {
targetRelationshipIdMapping.set(relationship.id, newId);
}
});
targetDiagram.dependencies?.forEach((dependency) => {
const table = targetDiagram.tables?.find(
(t) => t.id === dependency.tableId
);
const dependentTable = targetDiagram.tables?.find(
(t) => t.id === dependency.dependentTableId
);
if (!table || !dependentTable) return;
const dependencyKey = createDependencyKey({
table,
dependentTable,
defaultSchema: targetDefaultSchema,
});
const newId = dependenciesIdMapping.get(dependencyKey);
if (newId) {
targetDependencyIdMapping.set(dependency.id, newId);
}
});
targetDiagram.customTypes?.forEach((customType) => {
const customTypeKey = createCustomTypeKey({
customType,
defaultSchema: targetDefaultSchema,
});
const newId = customTypesIdMapping.get(customTypeKey);
if (newId) {
targetCustomTypeIdMapping.set(customType.id, newId);
}
});
// Apply the ID mappings to create the final diagram
const result: Diagram = {
...targetDiagram,
tables: targetDiagram.tables?.map((table) => {
const newTableId = targetTableIdMapping.get(table.id) ?? table.id;
return {
...table,
id: newTableId,
fields: table.fields.map((field) => {
const newFieldId =
targetFieldIdMapping.get(field.id) ?? field.id;
return {
...field,
id: newFieldId,
};
}),
indexes: table.indexes.map((index) => {
const newIndexId =
targetIndexIdMapping.get(index.id) ?? index.id;
// Update field IDs in index
const updatedFieldIds = index.fieldIds.map((fieldId) => {
return targetFieldIdMapping.get(fieldId) ?? fieldId;
});
return {
...index,
id: newIndexId,
fieldIds: updatedFieldIds,
};
}),
};
}),
relationships: targetDiagram.relationships?.map((relationship) => {
const newRelationshipId =
targetRelationshipIdMapping.get(relationship.id) ??
relationship.id;
// Update table and field IDs in relationships
const newSourceTableId =
targetTableIdMapping.get(relationship.sourceTableId) ??
relationship.sourceTableId;
const newTargetTableId =
targetTableIdMapping.get(relationship.targetTableId) ??
relationship.targetTableId;
const newSourceFieldId =
targetFieldIdMapping.get(relationship.sourceFieldId) ??
relationship.sourceFieldId;
const newTargetFieldId =
targetFieldIdMapping.get(relationship.targetFieldId) ??
relationship.targetFieldId;
return {
...relationship,
id: newRelationshipId,
sourceTableId: newSourceTableId,
targetTableId: newTargetTableId,
sourceFieldId: newSourceFieldId,
targetFieldId: newTargetFieldId,
};
}),
dependencies: targetDiagram.dependencies?.map((dependency) => {
const newDependencyId =
targetDependencyIdMapping.get(dependency.id) ?? dependency.id;
const newTableId =
targetTableIdMapping.get(dependency.tableId) ??
dependency.tableId;
const newDependentTableId =
targetTableIdMapping.get(dependency.dependentTableId) ??
dependency.dependentTableId;
return {
...dependency,
id: newDependencyId,
tableId: newTableId,
dependentTableId: newDependentTableId,
};
}),
customTypes: targetDiagram.customTypes?.map((customType) => {
const newCustomTypeId =
targetCustomTypeIdMapping.get(customType.id) ?? customType.id;
return {
...customType,
id: newCustomTypeId,
};
}),
};
return result;
};