Compare commits

...

3 Commits

Author SHA1 Message Date
Matthieu
f82a79b2aa fix: correct DEFAULT_ORIENTATIONS for _ProductConstructeurs
After migration, the table orientation is now A=constructeur, B=product.
Update the fallback orientation to match the new schema.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-22 12:06:08 +01:00
Matthieu
208d49aac8 fix: use DELETE instead of TRUNCATE for migration
Use DELETE instead of TRUNCATE to avoid requiring table ownership.
Wrap in DO block for better error handling and logging.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-22 11:51:57 +01:00
Matthieu
ff278f5549 fix: correct Product-Constructeur join table orientation
The _ProductConstructeurs table was created with wrong column order:
- Before: A=product, B=constructeur
- After: A=constructeur, B=product (alphabetical order expected by Prisma)

This caused Prisma to fail loading constructeurs relations, resulting in empty constructeurs arrays in API responses.

Changes:
- Added migration to swap A/B columns and recreate foreign keys
- Added debug logs in products.service.ts and constructeur-link.util.ts

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-22 11:48:06 +01:00
3 changed files with 77 additions and 2 deletions

View File

@@ -0,0 +1,49 @@
-- Fix _ProductConstructeurs table orientation
-- Issue: Table was created with A=product, B=constructeur
-- But Prisma expects alphabetical order: A=constructeur, B=product
-- This migration swaps the A and B columns to fix the orientation
DO $$
DECLARE
backup_count INTEGER;
BEGIN
-- Step 1: Create temp table with swapped data
CREATE TEMP TABLE IF NOT EXISTS _ProductConstructeurs_backup AS
SELECT "B" as new_A, "A" as new_B FROM "_ProductConstructeurs";
GET DIAGNOSTICS backup_count = ROW_COUNT;
RAISE NOTICE 'Backed up % rows', backup_count;
-- Step 2: Drop foreign key constraints
ALTER TABLE "_ProductConstructeurs" DROP CONSTRAINT IF EXISTS "_ProductConstructeurs_A_fkey";
ALTER TABLE "_ProductConstructeurs" DROP CONSTRAINT IF EXISTS "_ProductConstructeurs_B_fkey";
RAISE NOTICE 'Dropped foreign key constraints';
-- Step 3: Clear the table
DELETE FROM "_ProductConstructeurs";
RAISE NOTICE 'Cleared table';
-- Step 4: Reinsert data with swapped columns
INSERT INTO "_ProductConstructeurs" ("A", "B")
SELECT new_A, new_B FROM _ProductConstructeurs_backup;
GET DIAGNOSTICS backup_count = ROW_COUNT;
RAISE NOTICE 'Reinserted % rows with swapped columns', backup_count;
-- Step 5: Recreate foreign key constraints with correct orientation
ALTER TABLE "_ProductConstructeurs"
ADD CONSTRAINT "_ProductConstructeurs_A_fkey"
FOREIGN KEY ("A") REFERENCES "constructeurs"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "_ProductConstructeurs"
ADD CONSTRAINT "_ProductConstructeurs_B_fkey"
FOREIGN KEY ("B") REFERENCES "products"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
RAISE NOTICE 'Recreated foreign key constraints';
RAISE NOTICE 'Migration completed successfully';
END $$;

View File

@@ -12,7 +12,7 @@ const DEFAULT_ORIENTATIONS: Record<string, LinkOrientation> = {
_MachineConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' }, _MachineConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
_ComposantConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' }, _ComposantConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
_PieceConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' }, _PieceConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
_ProductConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' }, _ProductConstructeurs: { parentColumn: 'B', constructeurColumn: 'A' },
}; };
const sanitizeTableName = (tableName: string): string => { const sanitizeTableName = (tableName: string): string => {
@@ -168,6 +168,8 @@ export async function syncConstructeurLinks(
), ),
); );
let targetConstructeurIds = uniqueConstructeurIds; let targetConstructeurIds = uniqueConstructeurIds;
console.log('[syncConstructeurLinks] Table:', tableName, 'ParentId:', parentId);
console.log('[syncConstructeurLinks] Unique IDs:', uniqueConstructeurIds);
const constructeurDelegate = (executor as any)?.constructeur as const constructeurDelegate = (executor as any)?.constructeur as
| { | {
@@ -183,13 +185,16 @@ export async function syncConstructeurLinks(
where: { id: { in: targetConstructeurIds } }, where: { id: { in: targetConstructeurIds } },
select: { id: true }, select: { id: true },
}); });
console.log('[syncConstructeurLinks] Existing constructeurs in DB:', existing.map(c => c.id));
const existingIds = new Set(existing.map(({ id }) => id)); const existingIds = new Set(existing.map(({ id }) => id));
targetConstructeurIds = targetConstructeurIds.filter((id) => targetConstructeurIds = targetConstructeurIds.filter((id) =>
existingIds.has(id), existingIds.has(id),
); );
console.log('[syncConstructeurLinks] Filtered target IDs:', targetConstructeurIds);
} }
const orientation = await resolveOrientation(executor, tableName); const orientation = await resolveOrientation(executor, tableName);
console.log('[syncConstructeurLinks] Orientation:', orientation);
if (typeof executor.__syncConstructeurLinks === 'function') { if (typeof executor.__syncConstructeurLinks === 'function') {
await executor.__syncConstructeurLinks( await executor.__syncConstructeurLinks(
@@ -200,6 +205,7 @@ export async function syncConstructeurLinks(
return targetConstructeurIds; return targetConstructeurIds;
} }
console.log('[syncConstructeurLinks] Deleting old links...');
await prisma.$executeRaw( await prisma.$executeRaw(
Prisma.sql`DELETE FROM ${table} WHERE ${Prisma.raw( Prisma.sql`DELETE FROM ${table} WHERE ${Prisma.raw(
`"${orientation.parentColumn}"`, `"${orientation.parentColumn}"`,
@@ -207,6 +213,7 @@ export async function syncConstructeurLinks(
); );
if (targetConstructeurIds.length === 0) { if (targetConstructeurIds.length === 0) {
console.log('[syncConstructeurLinks] No IDs to insert, returning empty array');
return []; return [];
} }
@@ -214,6 +221,7 @@ export async function syncConstructeurLinks(
(constructeurId) => Prisma.sql`(${parentId}, ${constructeurId})`, (constructeurId) => Prisma.sql`(${parentId}, ${constructeurId})`,
); );
console.log('[syncConstructeurLinks] Inserting', targetConstructeurIds.length, 'new links...');
await prisma.$executeRaw( await prisma.$executeRaw(
Prisma.sql` Prisma.sql`
INSERT INTO ${table} ( INSERT INTO ${table} (
@@ -224,7 +232,10 @@ export async function syncConstructeurLinks(
ON CONFLICT DO NOTHING ON CONFLICT DO NOTHING
`, `,
); );
return fetchConstructeurIds(executor, tableName, parentId, orientation); console.log('[syncConstructeurLinks] Links inserted, fetching final IDs...');
const finalIds = await fetchConstructeurIds(executor, tableName, parentId, orientation);
console.log('[syncConstructeurLinks] Final fetched IDs:', finalIds);
return finalIds;
} }
export async function fetchConstructeurIds( export async function fetchConstructeurIds(

View File

@@ -124,22 +124,30 @@ export class ProductsService {
const constructeurIds = this.normalizeConstructeurIds( const constructeurIds = this.normalizeConstructeurIds(
createProductDto.constructeurIds, createProductDto.constructeurIds,
); );
console.log('[CREATE] Normalized constructeurIds:', constructeurIds);
const resolvedConstructeurIds = const resolvedConstructeurIds =
await this.resolveExistingConstructeurIds(constructeurIds); await this.resolveExistingConstructeurIds(constructeurIds);
console.log('[CREATE] Resolved constructeurIds:', resolvedConstructeurIds);
const created = await this.prisma.product.create({ const created = await this.prisma.product.create({
data, data,
include: PRODUCT_WITH_RELATIONS_INCLUDE, include: PRODUCT_WITH_RELATIONS_INCLUDE,
}); });
console.log('[CREATE] Product created:', created.id);
let syncedConstructeurIds: string[] = []; let syncedConstructeurIds: string[] = [];
if (resolvedConstructeurIds.length > 0) { if (resolvedConstructeurIds.length > 0) {
console.log('[CREATE] Calling syncConstructeurLinks with:', resolvedConstructeurIds);
syncedConstructeurIds = await syncConstructeurLinks( syncedConstructeurIds = await syncConstructeurLinks(
this.prisma, this.prisma,
'_ProductConstructeurs', '_ProductConstructeurs',
created.id, created.id,
resolvedConstructeurIds, resolvedConstructeurIds,
); );
console.log('[CREATE] Synced constructeurIds:', syncedConstructeurIds);
} else {
console.log('[CREATE] No constructeurIds to sync');
} }
const refreshed = await this.prisma.product.findUnique({ const refreshed = await this.prisma.product.findUnique({
@@ -192,8 +200,10 @@ export class ProductsService {
const constructeurIds = this.normalizeConstructeurIds( const constructeurIds = this.normalizeConstructeurIds(
updateProductDto.constructeurIds, updateProductDto.constructeurIds,
); );
console.log('[UPDATE] Normalized constructeurIds:', constructeurIds);
resolvedConstructeurIds = resolvedConstructeurIds =
await this.resolveExistingConstructeurIds(constructeurIds); await this.resolveExistingConstructeurIds(constructeurIds);
console.log('[UPDATE] Resolved constructeurIds:', resolvedConstructeurIds);
} }
let syncedConstructeurIds: string[] | undefined; let syncedConstructeurIds: string[] | undefined;
@@ -203,14 +213,19 @@ export class ProductsService {
where: { id }, where: { id },
data, data,
}); });
console.log('[UPDATE] Product updated:', id);
if (resolvedConstructeurIds !== undefined) { if (resolvedConstructeurIds !== undefined) {
console.log('[UPDATE] Calling syncConstructeurLinks with:', resolvedConstructeurIds);
syncedConstructeurIds = await syncConstructeurLinks( syncedConstructeurIds = await syncConstructeurLinks(
tx, tx,
'_ProductConstructeurs', '_ProductConstructeurs',
id, id,
resolvedConstructeurIds, resolvedConstructeurIds,
); );
console.log('[UPDATE] Synced constructeurIds:', syncedConstructeurIds);
} else {
console.log('[UPDATE] No constructeurIds to sync');
} }
}); });