Compare commits
10 Commits
dc4a12440b
...
6cf2b566ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cf2b566ce | ||
|
|
e81f71e3e7 | ||
|
|
d05b91d7cd | ||
|
|
fe471b9e81 | ||
|
|
9f522a6dbb | ||
|
|
635ea0e84e | ||
|
|
4db64351b7 | ||
|
|
b9c9b2c421 | ||
|
|
16a703a4c3 | ||
|
|
582a6fd7e1 |
@@ -0,0 +1,16 @@
|
||||
-- Drop previous non-unique index to replace it with a unique constraint
|
||||
DROP INDEX IF EXISTS "ModelType_category_name_idx";
|
||||
|
||||
-- Ensure unique names for machines, components, and pieces
|
||||
ALTER TABLE "machines"
|
||||
ADD CONSTRAINT "machines_name_key" UNIQUE ("name");
|
||||
|
||||
ALTER TABLE "composants"
|
||||
ADD CONSTRAINT "composants_name_key" UNIQUE ("name");
|
||||
|
||||
ALTER TABLE "pieces"
|
||||
ADD CONSTRAINT "pieces_name_key" UNIQUE ("name");
|
||||
|
||||
-- Enforce unique category/name pairs for model types (component & piece categories)
|
||||
ALTER TABLE "ModelType"
|
||||
ADD CONSTRAINT "ModelType_category_name_key" UNIQUE ("category", "name");
|
||||
@@ -0,0 +1,27 @@
|
||||
ALTER TABLE "type_machine_component_requirements"
|
||||
ADD COLUMN "orderIndex" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE "type_machine_piece_requirements"
|
||||
ADD COLUMN "orderIndex" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
WITH ordered_component_requirements AS (
|
||||
SELECT
|
||||
id,
|
||||
ROW_NUMBER() OVER (PARTITION BY "typeMachineId" ORDER BY "createdAt") - 1 AS idx
|
||||
FROM "type_machine_component_requirements"
|
||||
)
|
||||
UPDATE "type_machine_component_requirements" t
|
||||
SET "orderIndex" = ordered_component_requirements.idx
|
||||
FROM ordered_component_requirements
|
||||
WHERE ordered_component_requirements.id = t.id;
|
||||
|
||||
WITH ordered_piece_requirements AS (
|
||||
SELECT
|
||||
id,
|
||||
ROW_NUMBER() OVER (PARTITION BY "typeMachineId" ORDER BY "createdAt") - 1 AS idx
|
||||
FROM "type_machine_piece_requirements"
|
||||
)
|
||||
UPDATE "type_machine_piece_requirements" t
|
||||
SET "orderIndex" = ordered_piece_requirements.idx
|
||||
FROM ordered_piece_requirements
|
||||
WHERE ordered_piece_requirements.id = t.id;
|
||||
@@ -0,0 +1,64 @@
|
||||
-- Convert single constructeur relation to many-to-many for machines, composants et pièces
|
||||
|
||||
-- Machines → Constructeurs
|
||||
ALTER TABLE "machines" DROP CONSTRAINT IF EXISTS "machines_constructeurId_fkey";
|
||||
|
||||
CREATE TABLE "_MachineConstructeurs" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
CONSTRAINT "_MachineConstructeurs_A_fkey" FOREIGN KEY ("A") REFERENCES "machines"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "_MachineConstructeurs_B_fkey" FOREIGN KEY ("B") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "_MachineConstructeurs_AB_unique" ON "_MachineConstructeurs"("A", "B");
|
||||
CREATE INDEX "_MachineConstructeurs_B_index" ON "_MachineConstructeurs"("B");
|
||||
|
||||
INSERT INTO "_MachineConstructeurs" ("A", "B")
|
||||
SELECT "id", "constructeurId"
|
||||
FROM "machines"
|
||||
WHERE "constructeurId" IS NOT NULL
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
ALTER TABLE "machines" DROP COLUMN IF EXISTS "constructeurId";
|
||||
|
||||
-- Composants → Constructeurs
|
||||
ALTER TABLE "composants" DROP CONSTRAINT IF EXISTS "composants_constructeurId_fkey";
|
||||
|
||||
CREATE TABLE "_ComposantConstructeurs" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
CONSTRAINT "_ComposantConstructeurs_A_fkey" FOREIGN KEY ("A") REFERENCES "composants"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "_ComposantConstructeurs_B_fkey" FOREIGN KEY ("B") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "_ComposantConstructeurs_AB_unique" ON "_ComposantConstructeurs"("A", "B");
|
||||
CREATE INDEX "_ComposantConstructeurs_B_index" ON "_ComposantConstructeurs"("B");
|
||||
|
||||
INSERT INTO "_ComposantConstructeurs" ("A", "B")
|
||||
SELECT "id", "constructeurId"
|
||||
FROM "composants"
|
||||
WHERE "constructeurId" IS NOT NULL
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
ALTER TABLE "composants" DROP COLUMN IF EXISTS "constructeurId";
|
||||
|
||||
-- Pièces → Constructeurs
|
||||
ALTER TABLE "pieces" DROP CONSTRAINT IF EXISTS "pieces_constructeurId_fkey";
|
||||
|
||||
CREATE TABLE "_PieceConstructeurs" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
CONSTRAINT "_PieceConstructeurs_A_fkey" FOREIGN KEY ("A") REFERENCES "pieces"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "_PieceConstructeurs_B_fkey" FOREIGN KEY ("B") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "_PieceConstructeurs_AB_unique" ON "_PieceConstructeurs"("A", "B");
|
||||
CREATE INDEX "_PieceConstructeurs_B_index" ON "_PieceConstructeurs"("B");
|
||||
|
||||
INSERT INTO "_PieceConstructeurs" ("A", "B")
|
||||
SELECT "id", "constructeurId"
|
||||
FROM "pieces"
|
||||
WHERE "constructeurId" IS NOT NULL
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
ALTER TABLE "pieces" DROP COLUMN IF EXISTS "constructeurId";
|
||||
@@ -0,0 +1,66 @@
|
||||
-- Fix the orientation of implicit many-to-many join tables between constructeurs
|
||||
-- and machines/composants/pièces so that Prisma inserts target IDs into the
|
||||
-- matching foreign key columns.
|
||||
|
||||
-- Machines ↔ Constructeurs
|
||||
CREATE TABLE "_MachineConstructeurs_new" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
CONSTRAINT "_MachineConstructeurs_new_A_fkey" FOREIGN KEY ("A") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "_MachineConstructeurs_new_B_fkey" FOREIGN KEY ("B") REFERENCES "machines"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "_MachineConstructeurs_new" ("A", "B")
|
||||
SELECT "B", "A"
|
||||
FROM "_MachineConstructeurs";
|
||||
|
||||
DROP TABLE "_MachineConstructeurs";
|
||||
|
||||
ALTER TABLE "_MachineConstructeurs_new" RENAME TO "_MachineConstructeurs";
|
||||
ALTER TABLE "_MachineConstructeurs" RENAME CONSTRAINT "_MachineConstructeurs_new_A_fkey" TO "_MachineConstructeurs_A_fkey";
|
||||
ALTER TABLE "_MachineConstructeurs" RENAME CONSTRAINT "_MachineConstructeurs_new_B_fkey" TO "_MachineConstructeurs_B_fkey";
|
||||
|
||||
CREATE UNIQUE INDEX "_MachineConstructeurs_AB_unique" ON "_MachineConstructeurs"("A", "B");
|
||||
CREATE INDEX "_MachineConstructeurs_B_index" ON "_MachineConstructeurs"("B");
|
||||
|
||||
-- Composants ↔ Constructeurs
|
||||
CREATE TABLE "_ComposantConstructeurs_new" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
CONSTRAINT "_ComposantConstructeurs_new_A_fkey" FOREIGN KEY ("A") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "_ComposantConstructeurs_new_B_fkey" FOREIGN KEY ("B") REFERENCES "composants"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "_ComposantConstructeurs_new" ("A", "B")
|
||||
SELECT "B", "A"
|
||||
FROM "_ComposantConstructeurs";
|
||||
|
||||
DROP TABLE "_ComposantConstructeurs";
|
||||
|
||||
ALTER TABLE "_ComposantConstructeurs_new" RENAME TO "_ComposantConstructeurs";
|
||||
ALTER TABLE "_ComposantConstructeurs" RENAME CONSTRAINT "_ComposantConstructeurs_new_A_fkey" TO "_ComposantConstructeurs_A_fkey";
|
||||
ALTER TABLE "_ComposantConstructeurs" RENAME CONSTRAINT "_ComposantConstructeurs_new_B_fkey" TO "_ComposantConstructeurs_B_fkey";
|
||||
|
||||
CREATE UNIQUE INDEX "_ComposantConstructeurs_AB_unique" ON "_ComposantConstructeurs"("A", "B");
|
||||
CREATE INDEX "_ComposantConstructeurs_B_index" ON "_ComposantConstructeurs"("B");
|
||||
|
||||
-- Pièces ↔ Constructeurs
|
||||
CREATE TABLE "_PieceConstructeurs_new" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
CONSTRAINT "_PieceConstructeurs_new_A_fkey" FOREIGN KEY ("A") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "_PieceConstructeurs_new_B_fkey" FOREIGN KEY ("B") REFERENCES "pieces"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "_PieceConstructeurs_new" ("A", "B")
|
||||
SELECT "B", "A"
|
||||
FROM "_PieceConstructeurs";
|
||||
|
||||
DROP TABLE "_PieceConstructeurs";
|
||||
|
||||
ALTER TABLE "_PieceConstructeurs_new" RENAME TO "_PieceConstructeurs";
|
||||
ALTER TABLE "_PieceConstructeurs" RENAME CONSTRAINT "_PieceConstructeurs_new_A_fkey" TO "_PieceConstructeurs_A_fkey";
|
||||
ALTER TABLE "_PieceConstructeurs" RENAME CONSTRAINT "_PieceConstructeurs_new_B_fkey" TO "_PieceConstructeurs_B_fkey";
|
||||
|
||||
CREATE UNIQUE INDEX "_PieceConstructeurs_AB_unique" ON "_PieceConstructeurs"("A", "B");
|
||||
CREATE INDEX "_PieceConstructeurs_B_index" ON "_PieceConstructeurs"("B");
|
||||
@@ -0,0 +1,45 @@
|
||||
-- Restore the original orientation of the machine/composant ↔ constructeur
|
||||
-- implicit join tables after the previous corrective migration, while keeping
|
||||
-- the pièce ↔ constructeur table aligned with Prisma's expectations.
|
||||
|
||||
-- Machines ↔ Constructeurs must map column A → machine, B → constructeur
|
||||
CREATE TABLE "_MachineConstructeurs_restored" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
CONSTRAINT "_MachineConstructeurs_restored_A_fkey" FOREIGN KEY ("A") REFERENCES "machines"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "_MachineConstructeurs_restored_B_fkey" FOREIGN KEY ("B") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "_MachineConstructeurs_restored" ("A", "B")
|
||||
SELECT "B", "A"
|
||||
FROM "_MachineConstructeurs";
|
||||
|
||||
DROP TABLE "_MachineConstructeurs";
|
||||
|
||||
ALTER TABLE "_MachineConstructeurs_restored" RENAME TO "_MachineConstructeurs";
|
||||
ALTER TABLE "_MachineConstructeurs" RENAME CONSTRAINT "_MachineConstructeurs_restored_A_fkey" TO "_MachineConstructeurs_A_fkey";
|
||||
ALTER TABLE "_MachineConstructeurs" RENAME CONSTRAINT "_MachineConstructeurs_restored_B_fkey" TO "_MachineConstructeurs_B_fkey";
|
||||
|
||||
CREATE UNIQUE INDEX "_MachineConstructeurs_AB_unique" ON "_MachineConstructeurs"("A", "B");
|
||||
CREATE INDEX "_MachineConstructeurs_B_index" ON "_MachineConstructeurs"("B");
|
||||
|
||||
-- Composants ↔ Constructeurs must map column A → composant, B → constructeur
|
||||
CREATE TABLE "_ComposantConstructeurs_restored" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
CONSTRAINT "_ComposantConstructeurs_restored_A_fkey" FOREIGN KEY ("A") REFERENCES "composants"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "_ComposantConstructeurs_restored_B_fkey" FOREIGN KEY ("B") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "_ComposantConstructeurs_restored" ("A", "B")
|
||||
SELECT "B", "A"
|
||||
FROM "_ComposantConstructeurs";
|
||||
|
||||
DROP TABLE "_ComposantConstructeurs";
|
||||
|
||||
ALTER TABLE "_ComposantConstructeurs_restored" RENAME TO "_ComposantConstructeurs";
|
||||
ALTER TABLE "_ComposantConstructeurs" RENAME CONSTRAINT "_ComposantConstructeurs_restored_A_fkey" TO "_ComposantConstructeurs_A_fkey";
|
||||
ALTER TABLE "_ComposantConstructeurs" RENAME CONSTRAINT "_ComposantConstructeurs_restored_B_fkey" TO "_ComposantConstructeurs_B_fkey";
|
||||
|
||||
CREATE UNIQUE INDEX "_ComposantConstructeurs_AB_unique" ON "_ComposantConstructeurs"("A", "B");
|
||||
CREATE INDEX "_ComposantConstructeurs_B_index" ON "_ComposantConstructeurs"("B");
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Introduce an order index for custom fields so their ordering can be persisted.
|
||||
|
||||
ALTER TABLE "custom_fields"
|
||||
ADD COLUMN "orderIndex" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
WITH ranked AS (
|
||||
SELECT
|
||||
"id",
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY "typeMachineId", "typeComposantId", "typePieceId"
|
||||
ORDER BY "createdAt", "id"
|
||||
) - 1 AS rn
|
||||
FROM "custom_fields"
|
||||
)
|
||||
UPDATE "custom_fields"
|
||||
SET "orderIndex" = ranked.rn
|
||||
FROM ranked
|
||||
WHERE ranked."id" = "custom_fields"."id";
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Remove constructeur link rows that reference missing parents or constructeur records.
|
||||
-- This prevents foreign key violations when the orientation of the join tables is normalized.
|
||||
|
||||
DELETE FROM "_MachineConstructeurs"
|
||||
WHERE "A" NOT IN (SELECT "id" FROM "machines")
|
||||
OR "B" NOT IN (SELECT "id" FROM "constructeurs");
|
||||
|
||||
DELETE FROM "_ComposantConstructeurs"
|
||||
WHERE "A" NOT IN (SELECT "id" FROM "composants")
|
||||
OR "B" NOT IN (SELECT "id" FROM "constructeurs");
|
||||
|
||||
DELETE FROM "_PieceConstructeurs"
|
||||
WHERE "A" NOT IN (SELECT "id" FROM "pieces")
|
||||
OR "B" NOT IN (SELECT "id" FROM "constructeurs");
|
||||
@@ -0,0 +1,72 @@
|
||||
-- Restore the implicit many-to-many join table orientation between constructeurs
|
||||
-- and machines/composants/pièces so Prisma nested writes keep using the expected
|
||||
-- foreign key columns (A → constructeur, B → parent record).
|
||||
|
||||
-- Machines ↔ Constructeurs
|
||||
CREATE TABLE "_MachineConstructeurs_new" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
CONSTRAINT "_MachineConstructeurs_new_A_fkey" FOREIGN KEY ("A") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "_MachineConstructeurs_new_B_fkey" FOREIGN KEY ("B") REFERENCES "machines"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "_MachineConstructeurs_new" ("A", "B")
|
||||
SELECT mc."B", mc."A"
|
||||
FROM "_MachineConstructeurs" mc
|
||||
JOIN "machines" m ON mc."B" = m."id"
|
||||
JOIN "constructeurs" c ON mc."A" = c."id";
|
||||
|
||||
DROP TABLE "_MachineConstructeurs";
|
||||
|
||||
ALTER TABLE "_MachineConstructeurs_new" RENAME TO "_MachineConstructeurs";
|
||||
ALTER TABLE "_MachineConstructeurs" RENAME CONSTRAINT "_MachineConstructeurs_new_A_fkey" TO "_MachineConstructeurs_A_fkey";
|
||||
ALTER TABLE "_MachineConstructeurs" RENAME CONSTRAINT "_MachineConstructeurs_new_B_fkey" TO "_MachineConstructeurs_B_fkey";
|
||||
|
||||
CREATE UNIQUE INDEX "_MachineConstructeurs_AB_unique" ON "_MachineConstructeurs"("A", "B");
|
||||
CREATE INDEX "_MachineConstructeurs_B_index" ON "_MachineConstructeurs"("B");
|
||||
|
||||
-- Composants ↔ Constructeurs
|
||||
CREATE TABLE "_ComposantConstructeurs_new" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
CONSTRAINT "_ComposantConstructeurs_new_A_fkey" FOREIGN KEY ("A") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "_ComposantConstructeurs_new_B_fkey" FOREIGN KEY ("B") REFERENCES "composants"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "_ComposantConstructeurs_new" ("A", "B")
|
||||
SELECT cc."B", cc."A"
|
||||
FROM "_ComposantConstructeurs" cc
|
||||
JOIN "composants" comp ON cc."B" = comp."id"
|
||||
JOIN "constructeurs" c ON cc."A" = c."id";
|
||||
|
||||
DROP TABLE "_ComposantConstructeurs";
|
||||
|
||||
ALTER TABLE "_ComposantConstructeurs_new" RENAME TO "_ComposantConstructeurs";
|
||||
ALTER TABLE "_ComposantConstructeurs" RENAME CONSTRAINT "_ComposantConstructeurs_new_A_fkey" TO "_ComposantConstructeurs_A_fkey";
|
||||
ALTER TABLE "_ComposantConstructeurs" RENAME CONSTRAINT "_ComposantConstructeurs_new_B_fkey" TO "_ComposantConstructeurs_B_fkey";
|
||||
|
||||
CREATE UNIQUE INDEX "_ComposantConstructeurs_AB_unique" ON "_ComposantConstructeurs"("A", "B");
|
||||
CREATE INDEX "_ComposantConstructeurs_B_index" ON "_ComposantConstructeurs"("B");
|
||||
|
||||
-- Pièces ↔ Constructeurs
|
||||
CREATE TABLE "_PieceConstructeurs_new" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
CONSTRAINT "_PieceConstructeurs_new_A_fkey" FOREIGN KEY ("A") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "_PieceConstructeurs_new_B_fkey" FOREIGN KEY ("B") REFERENCES "pieces"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "_PieceConstructeurs_new" ("A", "B")
|
||||
SELECT pc."B", pc."A"
|
||||
FROM "_PieceConstructeurs" pc
|
||||
JOIN "pieces" p ON pc."B" = p."id"
|
||||
JOIN "constructeurs" c ON pc."A" = c."id";
|
||||
|
||||
DROP TABLE "_PieceConstructeurs";
|
||||
|
||||
ALTER TABLE "_PieceConstructeurs_new" RENAME TO "_PieceConstructeurs";
|
||||
ALTER TABLE "_PieceConstructeurs" RENAME CONSTRAINT "_PieceConstructeurs_new_A_fkey" TO "_PieceConstructeurs_A_fkey";
|
||||
ALTER TABLE "_PieceConstructeurs" RENAME CONSTRAINT "_PieceConstructeurs_new_B_fkey" TO "_PieceConstructeurs_B_fkey";
|
||||
|
||||
CREATE UNIQUE INDEX "_PieceConstructeurs_AB_unique" ON "_PieceConstructeurs"("A", "B");
|
||||
CREATE INDEX "_PieceConstructeurs_B_index" ON "_PieceConstructeurs"("B");
|
||||
@@ -0,0 +1,73 @@
|
||||
-- Ensure implicit many-to-many join tables between machines/composants/pièces
|
||||
-- and constructeurs follow Prisma's expected orientation:
|
||||
-- A -> parent model (machines/composants/pièces)
|
||||
-- B -> constructeurs
|
||||
|
||||
-- Machines ↔ Constructeurs
|
||||
CREATE TABLE "_MachineConstructeurs_new" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
CONSTRAINT "_MachineConstructeurs_new_A_fkey" FOREIGN KEY ("A") REFERENCES "machines"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "_MachineConstructeurs_new_B_fkey" FOREIGN KEY ("B") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "_MachineConstructeurs_new" ("A", "B")
|
||||
SELECT mc."B", mc."A"
|
||||
FROM "_MachineConstructeurs" mc
|
||||
JOIN "machines" m ON mc."B" = m."id"
|
||||
JOIN "constructeurs" c ON mc."A" = c."id";
|
||||
|
||||
DROP TABLE "_MachineConstructeurs";
|
||||
|
||||
ALTER TABLE "_MachineConstructeurs_new" RENAME TO "_MachineConstructeurs";
|
||||
ALTER TABLE "_MachineConstructeurs" RENAME CONSTRAINT "_MachineConstructeurs_new_A_fkey" TO "_MachineConstructeurs_A_fkey";
|
||||
ALTER TABLE "_MachineConstructeurs" RENAME CONSTRAINT "_MachineConstructeurs_new_B_fkey" TO "_MachineConstructeurs_B_fkey";
|
||||
|
||||
CREATE UNIQUE INDEX "_MachineConstructeurs_AB_unique" ON "_MachineConstructeurs"("A", "B");
|
||||
CREATE INDEX "_MachineConstructeurs_B_index" ON "_MachineConstructeurs"("B");
|
||||
|
||||
-- Composants ↔ Constructeurs
|
||||
CREATE TABLE "_ComposantConstructeurs_new" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
CONSTRAINT "_ComposantConstructeurs_new_A_fkey" FOREIGN KEY ("A") REFERENCES "composants"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "_ComposantConstructeurs_new_B_fkey" FOREIGN KEY ("B") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "_ComposantConstructeurs_new" ("A", "B")
|
||||
SELECT cc."B", cc."A"
|
||||
FROM "_ComposantConstructeurs" cc
|
||||
JOIN "composants" comp ON cc."B" = comp."id"
|
||||
JOIN "constructeurs" c ON cc."A" = c."id";
|
||||
|
||||
DROP TABLE "_ComposantConstructeurs";
|
||||
|
||||
ALTER TABLE "_ComposantConstructeurs_new" RENAME TO "_ComposantConstructeurs";
|
||||
ALTER TABLE "_ComposantConstructeurs" RENAME CONSTRAINT "_ComposantConstructeurs_new_A_fkey" TO "_ComposantConstructeurs_A_fkey";
|
||||
ALTER TABLE "_ComposantConstructeurs" RENAME CONSTRAINT "_ComposantConstructeurs_new_B_fkey" TO "_ComposantConstructeurs_B_fkey";
|
||||
|
||||
CREATE UNIQUE INDEX "_ComposantConstructeurs_AB_unique" ON "_ComposantConstructeurs"("A", "B");
|
||||
CREATE INDEX "_ComposantConstructeurs_B_index" ON "_ComposantConstructeurs"("B");
|
||||
|
||||
-- Pièces ↔ Constructeurs
|
||||
CREATE TABLE "_PieceConstructeurs_new" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL,
|
||||
CONSTRAINT "_PieceConstructeurs_new_A_fkey" FOREIGN KEY ("A") REFERENCES "pieces"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "_PieceConstructeurs_new_B_fkey" FOREIGN KEY ("B") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO "_PieceConstructeurs_new" ("A", "B")
|
||||
SELECT pc."B", pc."A"
|
||||
FROM "_PieceConstructeurs" pc
|
||||
JOIN "pieces" p ON pc."B" = p."id"
|
||||
JOIN "constructeurs" c ON pc."A" = c."id";
|
||||
|
||||
DROP TABLE "_PieceConstructeurs";
|
||||
|
||||
ALTER TABLE "_PieceConstructeurs_new" RENAME TO "_PieceConstructeurs";
|
||||
ALTER TABLE "_PieceConstructeurs" RENAME CONSTRAINT "_PieceConstructeurs_new_A_fkey" TO "_PieceConstructeurs_A_fkey";
|
||||
ALTER TABLE "_PieceConstructeurs" RENAME CONSTRAINT "_PieceConstructeurs_new_B_fkey" TO "_PieceConstructeurs_B_fkey";
|
||||
|
||||
CREATE UNIQUE INDEX "_PieceConstructeurs_AB_unique" ON "_PieceConstructeurs"("A", "B");
|
||||
CREATE INDEX "_PieceConstructeurs_B_index" ON "_PieceConstructeurs"("B");
|
||||
75
prisma/migrations/20251108120000_add_products/migration.sql
Normal file
75
prisma/migrations/20251108120000_add_products/migration.sql
Normal file
@@ -0,0 +1,75 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "ModelCategory" ADD VALUE IF NOT EXISTS 'PRODUCT';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ModelType" ADD COLUMN "productSkeleton" JSONB;
|
||||
|
||||
ALTER TABLE "composants" ADD COLUMN "productId" TEXT;
|
||||
|
||||
ALTER TABLE "pieces" ADD COLUMN "productId" TEXT;
|
||||
|
||||
ALTER TABLE "documents" ADD COLUMN "productId" TEXT;
|
||||
|
||||
ALTER TABLE "custom_fields" ADD COLUMN "typeProductId" TEXT;
|
||||
|
||||
ALTER TABLE "custom_field_values" ADD COLUMN "productId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "products" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"reference" TEXT,
|
||||
"supplierPrice" DECIMAL(10,2),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"typeProductId" TEXT,
|
||||
|
||||
CONSTRAINT "products_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "type_machine_product_requirements" (
|
||||
"id" TEXT NOT NULL,
|
||||
"label" TEXT,
|
||||
"minCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"maxCount" INTEGER,
|
||||
"required" BOOLEAN NOT NULL DEFAULT false,
|
||||
"allowNewModels" BOOLEAN NOT NULL DEFAULT true,
|
||||
"orderIndex" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"typeMachineId" TEXT NOT NULL,
|
||||
"typeProductId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "type_machine_product_requirements_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "_ProductConstructeurs" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "products_name_key" ON "products"("name");
|
||||
|
||||
CREATE UNIQUE INDEX "_ProductConstructeurs_AB_unique" ON "_ProductConstructeurs"("A", "B");
|
||||
CREATE INDEX "_ProductConstructeurs_B_index" ON "_ProductConstructeurs"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "products" ADD CONSTRAINT "products_typeProductId_fkey" FOREIGN KEY ("typeProductId") REFERENCES "ModelType"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "composants" ADD CONSTRAINT "composants_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "pieces" ADD CONSTRAINT "pieces_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "documents" ADD CONSTRAINT "documents_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "custom_fields" ADD CONSTRAINT "custom_fields_typeProductId_fkey" FOREIGN KEY ("typeProductId") REFERENCES "ModelType"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "custom_field_values" ADD CONSTRAINT "custom_field_values_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "type_machine_product_requirements" ADD CONSTRAINT "type_machine_product_requirements_typeMachineId_fkey" FOREIGN KEY ("typeMachineId") REFERENCES "type_machines"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "type_machine_product_requirements" ADD CONSTRAINT "type_machine_product_requirements_typeProductId_fkey" FOREIGN KEY ("typeProductId") REFERENCES "ModelType"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "_ProductConstructeurs" ADD CONSTRAINT "_ProductConstructeurs_A_fkey" FOREIGN KEY ("A") REFERENCES "products"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "_ProductConstructeurs" ADD CONSTRAINT "_ProductConstructeurs_B_fkey" FOREIGN KEY ("B") REFERENCES "constructeurs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,26 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "machine_product_links" (
|
||||
"id" TEXT NOT NULL,
|
||||
"machineId" TEXT NOT NULL,
|
||||
"productId" TEXT NOT NULL,
|
||||
"typeMachineProductRequirementId" TEXT,
|
||||
"parentLinkId" TEXT,
|
||||
"parentComponentLinkId" TEXT,
|
||||
"parentPieceLinkId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "machine_product_links_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "machine_product_links" ADD CONSTRAINT "machine_product_links_machineId_fkey" FOREIGN KEY ("machineId") REFERENCES "machines"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "machine_product_links" ADD CONSTRAINT "machine_product_links_productId_fkey" FOREIGN KEY ("productId") REFERENCES "products"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "machine_product_links" ADD CONSTRAINT "machine_product_links_typeMachineProductRequirementId_fkey" FOREIGN KEY ("typeMachineProductRequirementId") REFERENCES "type_machine_product_requirements"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
ALTER TABLE "machine_product_links" ADD CONSTRAINT "machine_product_links_parentLinkId_fkey" FOREIGN KEY ("parentLinkId") REFERENCES "machine_product_links"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "machine_product_links" ADD CONSTRAINT "machine_product_links_parentComponentLinkId_fkey" FOREIGN KEY ("parentComponentLinkId") REFERENCES "machine_component_links"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "machine_product_links" ADD CONSTRAINT "machine_product_links_parentPieceLinkId_fkey" FOREIGN KEY ("parentPieceLinkId") REFERENCES "machine_piece_links"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "machine_product_links_machineId_idx" ON "machine_product_links"("machineId");
|
||||
CREATE INDEX "machine_product_links_productId_idx" ON "machine_product_links"("productId");
|
||||
@@ -47,13 +47,14 @@ model TypeMachine {
|
||||
customFields CustomField[] @relation("TypeMachineCustomFields")
|
||||
componentRequirements TypeMachineComponentRequirement[]
|
||||
pieceRequirements TypeMachinePieceRequirement[]
|
||||
productRequirements TypeMachineProductRequirement[]
|
||||
|
||||
@@map("type_machines")
|
||||
}
|
||||
|
||||
model Machine {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
name String @unique
|
||||
reference String?
|
||||
prix Decimal? @db.Decimal(10, 2)
|
||||
createdAt DateTime @default(now())
|
||||
@@ -66,11 +67,11 @@ model Machine {
|
||||
typeMachineId String?
|
||||
typeMachine TypeMachine? @relation(fields: [typeMachineId], references: [id])
|
||||
|
||||
constructeurId String?
|
||||
constructeur Constructeur? @relation(fields: [constructeurId], references: [id], onDelete: SetNull)
|
||||
constructeurs Constructeur[] @relation("MachineConstructeurs")
|
||||
|
||||
componentLinks MachineComponentLink[]
|
||||
pieceLinks MachinePieceLink[]
|
||||
productLinks MachineProductLink[]
|
||||
documents Document[] @relation("MachineDocuments")
|
||||
customFieldValues CustomFieldValue[] @relation("MachineCustomFieldValues")
|
||||
|
||||
@@ -79,7 +80,7 @@ model Machine {
|
||||
|
||||
model Composant {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
name String @unique
|
||||
reference String?
|
||||
prix Decimal? @db.Decimal(10, 2)
|
||||
createdAt DateTime @default(now())
|
||||
@@ -89,8 +90,10 @@ model Composant {
|
||||
typeComposantId String?
|
||||
typeComposant ModelType? @relation("ModelTypeComponentAssignments", fields: [typeComposantId], references: [id])
|
||||
|
||||
constructeurId String?
|
||||
constructeur Constructeur? @relation(fields: [constructeurId], references: [id], onDelete: SetNull)
|
||||
productId String?
|
||||
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
|
||||
|
||||
constructeurs Constructeur[] @relation("ComposantConstructeurs")
|
||||
|
||||
documents Document[] @relation("ComposantDocuments")
|
||||
customFieldValues CustomFieldValue[] @relation("ComposantCustomFieldValues")
|
||||
@@ -101,7 +104,7 @@ model Composant {
|
||||
|
||||
model Piece {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
name String @unique
|
||||
reference String?
|
||||
prix Decimal? @db.Decimal(10, 2)
|
||||
createdAt DateTime @default(now())
|
||||
@@ -110,8 +113,10 @@ model Piece {
|
||||
typePieceId String?
|
||||
typePiece ModelType? @relation("ModelTypePieceAssignments", fields: [typePieceId], references: [id])
|
||||
|
||||
constructeurId String?
|
||||
constructeur Constructeur? @relation(fields: [constructeurId], references: [id], onDelete: SetNull)
|
||||
productId String?
|
||||
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
|
||||
|
||||
constructeurs Constructeur[] @relation("PieceConstructeurs")
|
||||
|
||||
documents Document[] @relation("PieceDocuments")
|
||||
customFieldValues CustomFieldValue[] @relation("PieceCustomFieldValues")
|
||||
@@ -120,6 +125,27 @@ model Piece {
|
||||
@@map("pieces")
|
||||
}
|
||||
|
||||
model Product {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
reference String?
|
||||
supplierPrice Decimal? @db.Decimal(10, 2)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
typeProductId String?
|
||||
typeProduct ModelType? @relation("ModelTypeProductAssignments", fields: [typeProductId], references: [id])
|
||||
|
||||
constructeurs Constructeur[] @relation("ProductConstructeurs")
|
||||
documents Document[] @relation("ProductDocuments")
|
||||
customFieldValues CustomFieldValue[] @relation("ProductCustomFieldValues")
|
||||
pieces Piece[]
|
||||
composants Composant[]
|
||||
machineLinks MachineProductLink[]
|
||||
|
||||
@@map("products")
|
||||
}
|
||||
|
||||
model MachineComponentLink {
|
||||
id String @id @default(cuid())
|
||||
machineId String
|
||||
@@ -138,6 +164,7 @@ model MachineComponentLink {
|
||||
childLinks MachineComponentLink[] @relation("MachineComponentLinkHierarchy")
|
||||
typeMachineComponentRequirement TypeMachineComponentRequirement? @relation("ComponentRequirementLinks", fields: [typeMachineComponentRequirementId], references: [id], onDelete: SetNull)
|
||||
pieceLinks MachinePieceLink[] @relation("ComponentLinkPieceLinks")
|
||||
productLinks MachineProductLink[] @relation("ComponentLinkProductLinks")
|
||||
|
||||
@@map("machine_component_links")
|
||||
}
|
||||
@@ -158,13 +185,37 @@ model MachinePieceLink {
|
||||
piece Piece @relation(fields: [pieceId], references: [id], onDelete: Cascade)
|
||||
parentLink MachineComponentLink? @relation("ComponentLinkPieceLinks", fields: [parentLinkId], references: [id], onDelete: Cascade)
|
||||
typeMachinePieceRequirement TypeMachinePieceRequirement? @relation("PieceRequirementLinks", fields: [typeMachinePieceRequirementId], references: [id], onDelete: SetNull)
|
||||
productLinks MachineProductLink[] @relation("PieceLinkProductLinks")
|
||||
|
||||
@@map("machine_piece_links")
|
||||
}
|
||||
|
||||
model MachineProductLink {
|
||||
id String @id @default(cuid())
|
||||
machineId String
|
||||
productId String
|
||||
typeMachineProductRequirementId String?
|
||||
parentLinkId String?
|
||||
parentComponentLinkId String?
|
||||
parentPieceLinkId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
machine Machine @relation(fields: [machineId], references: [id], onDelete: Cascade)
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
typeMachineProductRequirement TypeMachineProductRequirement? @relation("ProductRequirementLinks", fields: [typeMachineProductRequirementId], references: [id], onDelete: SetNull)
|
||||
parentLink MachineProductLink? @relation("MachineProductLinkHierarchy", fields: [parentLinkId], references: [id], onDelete: Cascade)
|
||||
childLinks MachineProductLink[] @relation("MachineProductLinkHierarchy")
|
||||
parentComponentLink MachineComponentLink? @relation("ComponentLinkProductLinks", fields: [parentComponentLinkId], references: [id], onDelete: Cascade)
|
||||
parentPieceLink MachinePieceLink? @relation("PieceLinkProductLinks", fields: [parentPieceLinkId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("machine_product_links")
|
||||
}
|
||||
|
||||
enum ModelCategory {
|
||||
COMPONENT
|
||||
PIECE
|
||||
PRODUCT
|
||||
}
|
||||
|
||||
model ModelType {
|
||||
@@ -176,17 +227,21 @@ model ModelType {
|
||||
description String? @db.Text
|
||||
componentSkeleton Json?
|
||||
pieceSkeleton Json?
|
||||
productSkeleton Json?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
composants Composant[] @relation("ModelTypeComponentAssignments")
|
||||
componentRequirements TypeMachineComponentRequirement[] @relation("ModelTypeComponentRequirements")
|
||||
customFields CustomField[] @relation("ModelTypeCustomFields")
|
||||
productCustomFields CustomField[] @relation("ModelTypeProductCustomFields")
|
||||
pieceRequirements TypeMachinePieceRequirement[] @relation("ModelTypePieceRequirements")
|
||||
pieces Piece[] @relation("ModelTypePieceAssignments")
|
||||
pieceCustomFields CustomField[] @relation("ModelTypePieceCustomFields")
|
||||
products Product[] @relation("ModelTypeProductAssignments")
|
||||
productRequirements TypeMachineProductRequirement[] @relation("ModelTypeProductRequirements")
|
||||
|
||||
@@index([category, name])
|
||||
@@unique([category, name])
|
||||
}
|
||||
|
||||
model Constructeur {
|
||||
@@ -197,9 +252,10 @@ model Constructeur {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
machines Machine[]
|
||||
composants Composant[]
|
||||
pieces Piece[]
|
||||
machines Machine[] @relation("MachineConstructeurs")
|
||||
composants Composant[] @relation("ComposantConstructeurs")
|
||||
pieces Piece[] @relation("PieceConstructeurs")
|
||||
products Product[] @relation("ProductConstructeurs")
|
||||
|
||||
@@map("constructeurs")
|
||||
}
|
||||
@@ -235,6 +291,9 @@ model Document {
|
||||
pieceId String?
|
||||
piece Piece? @relation("PieceDocuments", fields: [pieceId], references: [id], onDelete: Cascade)
|
||||
|
||||
productId String?
|
||||
product Product? @relation("ProductDocuments", fields: [productId], references: [id], onDelete: Cascade)
|
||||
|
||||
siteId String?
|
||||
site Site? @relation("SiteDocuments", fields: [siteId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@ -248,6 +307,7 @@ model CustomField {
|
||||
required Boolean @default(false)
|
||||
defaultValue String?
|
||||
options String[] // Pour les champs de type SELECT
|
||||
orderIndex Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -261,6 +321,9 @@ model CustomField {
|
||||
typePieceId String?
|
||||
typePiece ModelType? @relation("ModelTypePieceCustomFields", fields: [typePieceId], references: [id], onDelete: Cascade)
|
||||
|
||||
typeProductId String?
|
||||
typeProduct ModelType? @relation("ModelTypeProductCustomFields", fields: [typeProductId], references: [id], onDelete: Cascade)
|
||||
|
||||
// Relations avec les valeurs
|
||||
customFieldValues CustomFieldValue[]
|
||||
|
||||
@@ -286,6 +349,9 @@ model CustomFieldValue {
|
||||
pieceId String?
|
||||
piece Piece? @relation("PieceCustomFieldValues", fields: [pieceId], references: [id], onDelete: Cascade)
|
||||
|
||||
productId String?
|
||||
product Product? @relation("ProductCustomFieldValues", fields: [productId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("custom_field_values")
|
||||
}
|
||||
|
||||
@@ -296,6 +362,7 @@ model TypeMachineComponentRequirement {
|
||||
maxCount Int?
|
||||
required Boolean @default(true)
|
||||
allowNewModels Boolean @default(true)
|
||||
orderIndex Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -317,6 +384,7 @@ model TypeMachinePieceRequirement {
|
||||
maxCount Int?
|
||||
required Boolean @default(false)
|
||||
allowNewModels Boolean @default(true)
|
||||
orderIndex Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -330,3 +398,24 @@ model TypeMachinePieceRequirement {
|
||||
|
||||
@@map("type_machine_piece_requirements")
|
||||
}
|
||||
|
||||
model TypeMachineProductRequirement {
|
||||
id String @id @default(cuid())
|
||||
label String?
|
||||
minCount Int @default(0)
|
||||
maxCount Int?
|
||||
required Boolean @default(false)
|
||||
allowNewModels Boolean @default(true)
|
||||
orderIndex Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
typeMachineId String
|
||||
typeMachine TypeMachine @relation(fields: [typeMachineId], references: [id], onDelete: Cascade)
|
||||
|
||||
typeProductId String
|
||||
typeProduct ModelType @relation("ModelTypeProductRequirements", fields: [typeProductId], references: [id])
|
||||
machineProductLinks MachineProductLink[] @relation("ProductRequirementLinks")
|
||||
|
||||
@@map("type_machine_product_requirements")
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ async function deleteExistingData() {
|
||||
await prisma.machinePieceLink.deleteMany();
|
||||
await prisma.machine.deleteMany();
|
||||
await prisma.customFieldValue.deleteMany();
|
||||
await prisma.product.deleteMany();
|
||||
await prisma.composant.deleteMany();
|
||||
await prisma.piece.deleteMany();
|
||||
await prisma.typeMachineProductRequirement.deleteMany();
|
||||
|
||||
await prisma.modelType.deleteMany({
|
||||
where: {
|
||||
@@ -22,6 +24,7 @@ async function deleteExistingData() {
|
||||
'cooling-module',
|
||||
'structural-frame',
|
||||
'hydraulic-power-unit',
|
||||
'hydraulic-product',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -120,7 +123,7 @@ async function createPiece(options: {
|
||||
name: string;
|
||||
reference: string;
|
||||
price: number;
|
||||
constructeurId?: string | null;
|
||||
constructeurIds?: string[] | null;
|
||||
typeId: string;
|
||||
fieldValues: Record<string, string>;
|
||||
}) {
|
||||
@@ -143,17 +146,35 @@ async function createPiece(options: {
|
||||
},
|
||||
);
|
||||
|
||||
return prisma.piece.create({
|
||||
data: {
|
||||
name: options.name,
|
||||
reference: options.reference,
|
||||
prix: new Prisma.Decimal(options.price),
|
||||
typePieceId: options.typeId,
|
||||
constructeurId: options.constructeurId ?? null,
|
||||
customFieldValues: {
|
||||
create: customFieldValues,
|
||||
},
|
||||
const constructeurIds = Array.isArray(options.constructeurIds)
|
||||
? Array.from(
|
||||
new Set(
|
||||
options.constructeurIds
|
||||
.filter((value): value is string => typeof value === 'string')
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value.length > 0),
|
||||
),
|
||||
)
|
||||
: [];
|
||||
|
||||
const data: any = {
|
||||
name: options.name,
|
||||
reference: options.reference,
|
||||
prix: new Prisma.Decimal(options.price),
|
||||
typePieceId: options.typeId,
|
||||
customFieldValues: {
|
||||
create: customFieldValues,
|
||||
},
|
||||
};
|
||||
|
||||
if (constructeurIds.length) {
|
||||
data.constructeurs = {
|
||||
connect: constructeurIds.map((id) => ({ id })),
|
||||
};
|
||||
}
|
||||
|
||||
return prisma.piece.create({
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -161,7 +182,7 @@ async function createComponent(options: {
|
||||
name: string;
|
||||
reference: string;
|
||||
price: number;
|
||||
constructeurId?: string | null;
|
||||
constructeurIds?: string[] | null;
|
||||
typeId: string;
|
||||
fieldValues: Record<string, string>;
|
||||
structure?: Prisma.InputJsonValue;
|
||||
@@ -185,22 +206,169 @@ async function createComponent(options: {
|
||||
},
|
||||
);
|
||||
|
||||
const constructeurIds = Array.isArray(options.constructeurIds)
|
||||
? Array.from(
|
||||
new Set(
|
||||
options.constructeurIds
|
||||
.filter((value): value is string => typeof value === 'string')
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value.length > 0),
|
||||
),
|
||||
)
|
||||
: [];
|
||||
|
||||
const data: any = {
|
||||
name: options.name,
|
||||
reference: options.reference,
|
||||
prix: new Prisma.Decimal(options.price),
|
||||
typeComposantId: options.typeId,
|
||||
structure:
|
||||
options.structure === undefined
|
||||
? Prisma.JsonNull
|
||||
: options.structure ?? Prisma.JsonNull,
|
||||
customFieldValues: {
|
||||
create: customFieldValues,
|
||||
},
|
||||
};
|
||||
|
||||
if (constructeurIds.length) {
|
||||
data.constructeurs = {
|
||||
connect: constructeurIds.map((id) => ({ id })),
|
||||
};
|
||||
}
|
||||
|
||||
return prisma.composant.create({
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
async function createProductType(
|
||||
name: string,
|
||||
code: string,
|
||||
description: string,
|
||||
fields: Array<{
|
||||
name: string;
|
||||
type: 'text' | 'number' | 'select' | 'boolean' | 'date';
|
||||
required?: boolean;
|
||||
options?: string[];
|
||||
}>,
|
||||
skeleton?: Record<string, unknown>,
|
||||
) {
|
||||
const type = await prisma.modelType.create({
|
||||
data: {
|
||||
name: options.name,
|
||||
reference: options.reference,
|
||||
prix: new Prisma.Decimal(options.price),
|
||||
typeComposantId: options.typeId,
|
||||
constructeurId: options.constructeurId ?? null,
|
||||
structure:
|
||||
options.structure === undefined
|
||||
? Prisma.JsonNull
|
||||
: options.structure ?? Prisma.JsonNull,
|
||||
customFieldValues: {
|
||||
create: customFieldValues,
|
||||
name,
|
||||
code,
|
||||
category: 'PRODUCT',
|
||||
description,
|
||||
productSkeleton: skeleton
|
||||
? (skeleton as Prisma.InputJsonValue)
|
||||
: Prisma.JsonNull,
|
||||
productCustomFields: {
|
||||
create: fields.map((field, index) => ({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
required: field.required ?? false,
|
||||
options: field.options ?? [],
|
||||
orderIndex: index,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const customFields = await prisma.customField.findMany({
|
||||
where: { typeProductId: type.id },
|
||||
});
|
||||
|
||||
const fieldMap: CreatedFields = {};
|
||||
customFields.forEach((field) => {
|
||||
fieldMap[field.name] = field.id;
|
||||
});
|
||||
|
||||
return { type, fieldMap };
|
||||
}
|
||||
|
||||
async function createProduct(options: {
|
||||
name: string;
|
||||
reference?: string;
|
||||
supplierPrice?: number | null;
|
||||
constructeurIds?: string[] | null;
|
||||
typeId?: string | null;
|
||||
fieldValues?: Record<string, string>;
|
||||
}) {
|
||||
const fieldValues = options.fieldValues ?? {};
|
||||
|
||||
const customFields = options.typeId
|
||||
? await prisma.customField.findMany({
|
||||
where: { typeProductId: options.typeId },
|
||||
})
|
||||
: [];
|
||||
|
||||
const customFieldValues = Object.entries(fieldValues).flatMap(
|
||||
([fieldName, value]) => {
|
||||
if (typeof value !== 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const target = customFields.find((field) => field.name === fieldName);
|
||||
if (!target) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
value,
|
||||
customFieldId: target.id,
|
||||
},
|
||||
];
|
||||
},
|
||||
);
|
||||
|
||||
const constructeurIds = Array.isArray(options.constructeurIds)
|
||||
? Array.from(
|
||||
new Set(
|
||||
options.constructeurIds
|
||||
.filter((value): value is string => typeof value === 'string')
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value.length > 0),
|
||||
),
|
||||
)
|
||||
: [];
|
||||
|
||||
const data: Prisma.ProductCreateInput = {
|
||||
name: options.name,
|
||||
reference: options.reference ?? null,
|
||||
supplierPrice:
|
||||
options.supplierPrice === undefined || options.supplierPrice === null
|
||||
? null
|
||||
: new Prisma.Decimal(options.supplierPrice),
|
||||
};
|
||||
|
||||
if (options.typeId) {
|
||||
data.typeProduct = {
|
||||
connect: { id: options.typeId },
|
||||
};
|
||||
}
|
||||
|
||||
if (constructeurIds.length) {
|
||||
data.constructeurs = {
|
||||
connect: constructeurIds.map((id) => ({ id })),
|
||||
};
|
||||
}
|
||||
|
||||
if (customFieldValues.length) {
|
||||
data.customFieldValues = {
|
||||
create: customFieldValues.map((entry) => ({
|
||||
value: entry.value,
|
||||
customField: {
|
||||
connect: { id: entry.customFieldId },
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return prisma.product.create({
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -335,6 +503,63 @@ async function main() {
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Création des types de produits…');
|
||||
const hydraulicProductFields: {
|
||||
name: string;
|
||||
type: 'text' | 'number' | 'select' | 'boolean' | 'date';
|
||||
required?: boolean;
|
||||
options?: string[];
|
||||
}[] = [
|
||||
{ name: 'Fournisseur', type: 'text', required: true },
|
||||
{ name: 'Garantie (mois)', type: 'number', required: true },
|
||||
{
|
||||
name: 'Délai d’approvisionnement (jours)',
|
||||
type: 'number',
|
||||
},
|
||||
];
|
||||
|
||||
const hydraulicProductType = await createProductType(
|
||||
'Produit hydraulique standard',
|
||||
'hydraulic-product',
|
||||
'Produits compatibles avec les centrales hydrauliques',
|
||||
hydraulicProductFields,
|
||||
);
|
||||
|
||||
console.log('Création des produits…');
|
||||
const pumpProduct = await createProduct({
|
||||
name: 'Pompe PX-300 Fournisseur A',
|
||||
reference: 'PRD-PX-300-A',
|
||||
supplierPrice: 1520,
|
||||
typeId: hydraulicProductType.type.id,
|
||||
fieldValues: {
|
||||
Fournisseur: 'HydrauParts',
|
||||
'Garantie (mois)': '24',
|
||||
'Délai d’approvisionnement (jours)': '21',
|
||||
},
|
||||
});
|
||||
|
||||
const coolingProduct = await createProduct({
|
||||
name: 'Module de refroidissement AC-50 - OEM',
|
||||
reference: 'PRD-AC-50',
|
||||
supplierPrice: 1980,
|
||||
typeId: hydraulicProductType.type.id,
|
||||
fieldValues: {
|
||||
Fournisseur: 'ThermoTech',
|
||||
'Garantie (mois)': '18',
|
||||
'Délai d’approvisionnement (jours)': '28',
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Association des produits aux pièces…');
|
||||
await prisma.piece.update({
|
||||
where: { id: pumpPiece.id },
|
||||
data: {
|
||||
product: {
|
||||
connect: { id: pumpProduct.id },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Création des types de composants…');
|
||||
const coolingComponentFields: {
|
||||
name: string;
|
||||
@@ -473,6 +698,15 @@ async function main() {
|
||||
} as Prisma.InputJsonValue,
|
||||
});
|
||||
|
||||
await prisma.composant.update({
|
||||
where: { id: coolingModule.id },
|
||||
data: {
|
||||
product: {
|
||||
connect: { id: coolingProduct.id },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const structuralFrame = await createComponent({
|
||||
name: 'Châssis structurel XC-800',
|
||||
reference: 'FRAME-XC800',
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ConstructeursModule } from './constructeurs/constructeurs.module';
|
||||
import { ProfilesModule } from './profiles/profiles.module';
|
||||
import { SessionModule } from './session/session.module';
|
||||
import { ModelTypeModule } from './model-type/model-type.module';
|
||||
import { ProductsModule } from './products/products.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -32,6 +33,7 @@ import { ModelTypeModule } from './model-type/model-type.module';
|
||||
ProfilesModule,
|
||||
SessionModule,
|
||||
ModelTypeModule,
|
||||
ProductsModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
||||
@@ -6,20 +6,34 @@ const CUSTOM_FIELD_SELECT = {
|
||||
type: true,
|
||||
required: true,
|
||||
options: true,
|
||||
orderIndex: true,
|
||||
} as const;
|
||||
|
||||
export const COMPONENT_WITH_RELATIONS_INCLUDE = {
|
||||
typeComposant: {
|
||||
include: {
|
||||
customFields: true,
|
||||
customFields: {
|
||||
orderBy: { orderIndex: 'asc' },
|
||||
},
|
||||
},
|
||||
},
|
||||
constructeur: true,
|
||||
constructeurs: true,
|
||||
customFieldValues: {
|
||||
include: {
|
||||
customField: { select: CUSTOM_FIELD_SELECT },
|
||||
},
|
||||
},
|
||||
product: {
|
||||
include: {
|
||||
constructeurs: true,
|
||||
customFieldValues: {
|
||||
include: {
|
||||
customField: { select: CUSTOM_FIELD_SELECT },
|
||||
},
|
||||
},
|
||||
documents: true,
|
||||
},
|
||||
},
|
||||
machineLinks: {
|
||||
include: {
|
||||
machine: true,
|
||||
|
||||
@@ -4,4 +4,5 @@ export const CUSTOM_FIELD_SELECT = {
|
||||
type: true,
|
||||
required: true,
|
||||
options: true,
|
||||
orderIndex: true,
|
||||
} as const;
|
||||
|
||||
32
src/common/constants/product-includes.ts
Normal file
32
src/common/constants/product-includes.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
export const PRODUCT_WITH_RELATIONS_INCLUDE = {
|
||||
typeProduct: {
|
||||
include: {
|
||||
productCustomFields: {
|
||||
orderBy: { orderIndex: 'asc' },
|
||||
},
|
||||
},
|
||||
},
|
||||
constructeurs: true,
|
||||
documents: true,
|
||||
customFieldValues: {
|
||||
include: {
|
||||
customField: true,
|
||||
},
|
||||
},
|
||||
pieces: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
reference: true,
|
||||
},
|
||||
},
|
||||
composants: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
reference: true,
|
||||
},
|
||||
},
|
||||
} satisfies Prisma.ProductInclude;
|
||||
@@ -43,7 +43,10 @@ describe('ModelTypeMapper', () => {
|
||||
description: 'Desc',
|
||||
notes: 'Desc',
|
||||
});
|
||||
expect(input.customFields?.create?.[0]).toMatchObject({ name: 'Field' });
|
||||
expect(input.customFields?.create?.[0]).toMatchObject({
|
||||
name: 'Field',
|
||||
orderIndex: 0,
|
||||
});
|
||||
expect((input as any).componentSkeleton).toEqual({
|
||||
pieces: [
|
||||
{
|
||||
|
||||
@@ -12,12 +12,18 @@ import type {
|
||||
import { CUSTOM_FIELD_SELECT } from '../constants/custom-field.constant';
|
||||
|
||||
export const COMPONENT_TYPE_INCLUDE: Prisma.ModelTypeInclude = {
|
||||
customFields: { select: CUSTOM_FIELD_SELECT },
|
||||
customFields: {
|
||||
select: CUSTOM_FIELD_SELECT,
|
||||
orderBy: { orderIndex: 'asc' },
|
||||
},
|
||||
composants: true,
|
||||
};
|
||||
|
||||
export const PIECE_TYPE_INCLUDE: Prisma.ModelTypeInclude = {
|
||||
pieceCustomFields: { select: CUSTOM_FIELD_SELECT },
|
||||
pieceCustomFields: {
|
||||
select: CUSTOM_FIELD_SELECT,
|
||||
orderBy: { orderIndex: 'asc' },
|
||||
},
|
||||
pieceRequirements: true,
|
||||
pieces: true,
|
||||
};
|
||||
@@ -42,11 +48,12 @@ export class ModelTypeMapper {
|
||||
notes: description ?? null,
|
||||
customFields: customFields
|
||||
? {
|
||||
create: customFields.map((field) => ({
|
||||
create: customFields.map((field, index) => ({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
required: field.required ?? false,
|
||||
options: field.options,
|
||||
orderIndex: field.orderIndex ?? index,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
@@ -97,11 +104,12 @@ export class ModelTypeMapper {
|
||||
notes: description ?? null,
|
||||
pieceCustomFields: customFields
|
||||
? {
|
||||
create: customFields.map((field) => ({
|
||||
create: customFields.map((field, index) => ({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
required: field.required ?? false,
|
||||
options: field.options,
|
||||
orderIndex: field.orderIndex ?? index,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
@@ -165,11 +173,12 @@ export class ModelTypeMapper {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fields.map((field) => ({
|
||||
return fields.map((field, index) => ({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
required: field.required ?? false,
|
||||
options: field.options,
|
||||
orderIndex: field.orderIndex ?? index,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -180,11 +189,12 @@ export class ModelTypeMapper {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fields.map((field) => ({
|
||||
return fields.map((field, index) => ({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
required: field.required ?? false,
|
||||
options: field.options,
|
||||
orderIndex: field.orderIndex ?? index,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,16 @@ const baseDto = {
|
||||
typePieceId: 'piece-id',
|
||||
},
|
||||
],
|
||||
productRequirements: [
|
||||
{
|
||||
label: 'Product',
|
||||
minCount: 1,
|
||||
maxCount: 3,
|
||||
required: true,
|
||||
allowNewModels: true,
|
||||
typeProductId: 'product-id',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('TypeMachineMapper', () => {
|
||||
@@ -33,12 +43,16 @@ describe('TypeMachineMapper', () => {
|
||||
const input = TypeMachineMapper.toCreateInput(baseDto as any);
|
||||
|
||||
expect(input.customFields?.create).toHaveLength(1);
|
||||
expect(input.customFields?.create?.[0]).toMatchObject({
|
||||
orderIndex: 0,
|
||||
});
|
||||
expect(input.componentRequirements?.create?.[0]).toMatchObject({
|
||||
label: 'Comp',
|
||||
minCount: 2,
|
||||
maxCount: 4,
|
||||
required: true,
|
||||
allowNewModels: false,
|
||||
orderIndex: 0,
|
||||
});
|
||||
expect(input.pieceRequirements?.create?.[0]).toMatchObject({
|
||||
label: 'Piece',
|
||||
@@ -46,6 +60,15 @@ describe('TypeMachineMapper', () => {
|
||||
maxCount: 2,
|
||||
required: false,
|
||||
allowNewModels: true,
|
||||
orderIndex: 0,
|
||||
});
|
||||
expect(input.productRequirements?.create?.[0]).toMatchObject({
|
||||
label: 'Product',
|
||||
minCount: 1,
|
||||
maxCount: 3,
|
||||
required: true,
|
||||
allowNewModels: true,
|
||||
orderIndex: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,6 +82,7 @@ describe('TypeMachineMapper', () => {
|
||||
type: 'string',
|
||||
required: true,
|
||||
options: ['a'],
|
||||
orderIndex: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -70,16 +94,27 @@ describe('TypeMachineMapper', () => {
|
||||
const piece = TypeMachineMapper.mapPieceRequirementInputs(
|
||||
baseDto.pieceRequirements as any,
|
||||
);
|
||||
const product = TypeMachineMapper.mapProductRequirementInputs(
|
||||
baseDto.productRequirements as any,
|
||||
);
|
||||
|
||||
expect(component[0]).toMatchObject({
|
||||
typeComposantId: 'comp-id',
|
||||
minCount: 2,
|
||||
maxCount: 4,
|
||||
orderIndex: 0,
|
||||
});
|
||||
expect(piece[0]).toMatchObject({
|
||||
typePieceId: 'piece-id',
|
||||
minCount: 0,
|
||||
maxCount: 2,
|
||||
orderIndex: 0,
|
||||
});
|
||||
expect(product[0]).toMatchObject({
|
||||
typeProductId: 'product-id',
|
||||
minCount: 1,
|
||||
maxCount: 3,
|
||||
orderIndex: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,15 +13,26 @@ type RequirementDto = {
|
||||
allowNewModels?: boolean | null;
|
||||
typeComposantId?: string;
|
||||
typePieceId?: string;
|
||||
typeProductId?: string;
|
||||
orderIndex?: number | null;
|
||||
};
|
||||
|
||||
export const TYPE_MACHINE_DEFAULT_INCLUDE: Prisma.TypeMachineInclude = {
|
||||
customFields: { select: CUSTOM_FIELD_SELECT },
|
||||
customFields: {
|
||||
select: CUSTOM_FIELD_SELECT,
|
||||
orderBy: { orderIndex: 'asc' },
|
||||
},
|
||||
componentRequirements: {
|
||||
include: { typeComposant: true },
|
||||
orderBy: { orderIndex: 'asc' },
|
||||
},
|
||||
pieceRequirements: {
|
||||
include: { typePiece: true },
|
||||
orderBy: { orderIndex: 'asc' },
|
||||
},
|
||||
productRequirements: {
|
||||
include: { typeProduct: true },
|
||||
orderBy: { orderIndex: 'asc' },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -34,8 +45,13 @@ export class TypeMachineMapper {
|
||||
static toCreateInput(
|
||||
dto: CreateTypeMachineDto,
|
||||
): Prisma.TypeMachineCreateInput {
|
||||
const { customFields, componentRequirements, pieceRequirements, ...data } =
|
||||
dto;
|
||||
const {
|
||||
customFields,
|
||||
componentRequirements,
|
||||
pieceRequirements,
|
||||
productRequirements,
|
||||
...data
|
||||
} = dto;
|
||||
|
||||
return {
|
||||
...data,
|
||||
@@ -44,14 +60,20 @@ export class TypeMachineMapper {
|
||||
componentRequirements,
|
||||
),
|
||||
pieceRequirements: this.mapPieceRequirements(pieceRequirements),
|
||||
productRequirements: this.mapProductRequirements(productRequirements),
|
||||
};
|
||||
}
|
||||
|
||||
static toUpdateData(
|
||||
dto: UpdateTypeMachineDto,
|
||||
): Prisma.TypeMachineUpdateInput {
|
||||
const { customFields, componentRequirements, pieceRequirements, ...data } =
|
||||
dto;
|
||||
const {
|
||||
customFields,
|
||||
componentRequirements,
|
||||
pieceRequirements,
|
||||
productRequirements,
|
||||
...data
|
||||
} = dto;
|
||||
|
||||
const payload: Prisma.TypeMachineUpdateInput = { ...data };
|
||||
|
||||
@@ -67,6 +89,10 @@ export class TypeMachineMapper {
|
||||
payload.pieceRequirements = undefined;
|
||||
}
|
||||
|
||||
if (productRequirements !== undefined) {
|
||||
payload.productRequirements = undefined;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
@@ -78,11 +104,12 @@ export class TypeMachineMapper {
|
||||
}
|
||||
|
||||
return {
|
||||
create: fields.map((field) => ({
|
||||
create: fields.map((field, index) => ({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
required: field.required ?? false,
|
||||
options: field.options,
|
||||
orderIndex: field.orderIndex ?? index,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -92,11 +119,12 @@ export class TypeMachineMapper {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fields.map((field) => ({
|
||||
return fields.map((field, index) => ({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
required: field.required ?? false,
|
||||
options: field.options,
|
||||
orderIndex: field.orderIndex ?? index,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -110,12 +138,13 @@ export class TypeMachineMapper {
|
||||
}
|
||||
|
||||
return {
|
||||
create: requirements.map((requirement) => ({
|
||||
create: requirements.map((requirement, index) => ({
|
||||
label: requirement.label ?? null,
|
||||
minCount: requirement.minCount ?? 1,
|
||||
maxCount: requirement.maxCount ?? null,
|
||||
required: requirement.required ?? true,
|
||||
allowNewModels: requirement.allowNewModels ?? true,
|
||||
orderIndex: requirement.orderIndex ?? index,
|
||||
typeComposant: requirement.typeComposantId
|
||||
? {
|
||||
connect: { id: requirement.typeComposantId },
|
||||
@@ -134,12 +163,13 @@ export class TypeMachineMapper {
|
||||
return [];
|
||||
}
|
||||
|
||||
return requirements.map((requirement) => ({
|
||||
return requirements.map((requirement, index) => ({
|
||||
label: requirement.label ?? null,
|
||||
minCount: requirement.minCount ?? 1,
|
||||
maxCount: requirement.maxCount ?? null,
|
||||
required: requirement.required ?? true,
|
||||
allowNewModels: requirement.allowNewModels ?? true,
|
||||
orderIndex: requirement.orderIndex ?? index,
|
||||
typeComposantId: requirement.typeComposantId!,
|
||||
}));
|
||||
}
|
||||
@@ -154,12 +184,13 @@ export class TypeMachineMapper {
|
||||
}
|
||||
|
||||
return {
|
||||
create: requirements.map((requirement) => ({
|
||||
create: requirements.map((requirement, index) => ({
|
||||
label: requirement.label ?? null,
|
||||
minCount: requirement.minCount ?? 0,
|
||||
maxCount: requirement.maxCount ?? null,
|
||||
required: requirement.required ?? false,
|
||||
allowNewModels: requirement.allowNewModels ?? true,
|
||||
orderIndex: requirement.orderIndex ?? index,
|
||||
typePiece: requirement.typePieceId
|
||||
? {
|
||||
connect: { id: requirement.typePieceId },
|
||||
@@ -178,13 +209,60 @@ export class TypeMachineMapper {
|
||||
return [];
|
||||
}
|
||||
|
||||
return requirements.map((requirement) => ({
|
||||
return requirements.map((requirement, index) => ({
|
||||
label: requirement.label ?? null,
|
||||
minCount: requirement.minCount ?? 0,
|
||||
maxCount: requirement.maxCount ?? null,
|
||||
required: requirement.required ?? false,
|
||||
allowNewModels: requirement.allowNewModels ?? true,
|
||||
orderIndex: requirement.orderIndex ?? index,
|
||||
typePieceId: requirement.typePieceId!,
|
||||
}));
|
||||
}
|
||||
|
||||
static mapProductRequirements(
|
||||
requirements?: RequirementDto[] | null,
|
||||
):
|
||||
| Prisma.TypeMachineProductRequirementCreateNestedManyWithoutTypeMachineInput
|
||||
| undefined {
|
||||
if (!requirements || requirements.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
create: requirements.map((requirement, index) => ({
|
||||
label: requirement.label ?? null,
|
||||
minCount: requirement.minCount ?? 0,
|
||||
maxCount: requirement.maxCount ?? null,
|
||||
required: requirement.required ?? false,
|
||||
allowNewModels: requirement.allowNewModels ?? true,
|
||||
orderIndex: requirement.orderIndex ?? index,
|
||||
typeProduct: requirement.typeProductId
|
||||
? {
|
||||
connect: { id: requirement.typeProductId },
|
||||
}
|
||||
: (() => {
|
||||
throw new Error(
|
||||
'typeProductId est requis pour créer une contrainte produit.',
|
||||
);
|
||||
})(),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
static mapProductRequirementInputs(requirements?: RequirementDto[] | null) {
|
||||
if (!requirements || requirements.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return requirements.map((requirement, index) => ({
|
||||
label: requirement.label ?? null,
|
||||
minCount: requirement.minCount ?? 0,
|
||||
maxCount: requirement.maxCount ?? null,
|
||||
required: requirement.required ?? false,
|
||||
allowNewModels: requirement.allowNewModels ?? true,
|
||||
orderIndex: requirement.orderIndex ?? index,
|
||||
typeProductId: requirement.typeProductId!,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ describe('ModelTypesRepository', () => {
|
||||
type: 'string',
|
||||
required: true,
|
||||
options: [],
|
||||
orderIndex: 0,
|
||||
typeComposantId: 'comp-id',
|
||||
},
|
||||
],
|
||||
@@ -63,6 +64,7 @@ describe('ModelTypesRepository', () => {
|
||||
type: 'string',
|
||||
required: false,
|
||||
options: [],
|
||||
orderIndex: 0,
|
||||
typePieceId: 'piece-id',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -44,6 +44,7 @@ describe('TypeMachinesRepository', () => {
|
||||
type: 'string',
|
||||
required: true,
|
||||
options: [],
|
||||
orderIndex: 0,
|
||||
typeMachineId: 'machine-id',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -17,6 +17,11 @@ type PieceRequirementInput = Omit<
|
||||
'id' | 'typeMachineId'
|
||||
>;
|
||||
|
||||
type ProductRequirementInput = Omit<
|
||||
Prisma.TypeMachineProductRequirementCreateManyInput,
|
||||
'id' | 'typeMachineId'
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
export class TypeMachinesRepository {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
@@ -132,6 +137,28 @@ export class TypeMachinesRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteProductRequirements(typeMachineId: string) {
|
||||
await this.client.typeMachineProductRequirement.deleteMany({
|
||||
where: { typeMachineId },
|
||||
});
|
||||
}
|
||||
|
||||
async createProductRequirements(
|
||||
typeMachineId: string,
|
||||
requirements: ProductRequirementInput[],
|
||||
) {
|
||||
if (!requirements.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.client.typeMachineProductRequirement.createMany({
|
||||
data: requirements.map((requirement) => ({
|
||||
...requirement,
|
||||
typeMachineId,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
async findMachinesUsingType(typeMachineId: string) {
|
||||
return this.client.machine.findMany({
|
||||
where: { typeMachineId },
|
||||
|
||||
254
src/common/utils/constructeur-link.util.ts
Normal file
254
src/common/utils/constructeur-link.util.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
import type { PrismaService } from '../../prisma/prisma.service';
|
||||
|
||||
type PrismaExecutor = Prisma.TransactionClient | PrismaService;
|
||||
|
||||
type LinkOrientation = {
|
||||
parentColumn: 'A' | 'B';
|
||||
constructeurColumn: 'A' | 'B';
|
||||
};
|
||||
|
||||
const DEFAULT_ORIENTATIONS: Record<string, LinkOrientation> = {
|
||||
_MachineConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
|
||||
_ComposantConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
|
||||
_PieceConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
|
||||
_ProductConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
|
||||
};
|
||||
|
||||
const sanitizeTableName = (tableName: string): string => {
|
||||
if (!/^_[A-Za-z0-9]+$/.test(tableName)) {
|
||||
throw new Error(`Invalid constructeur link table name: ${tableName}`);
|
||||
}
|
||||
return `"${tableName}"`;
|
||||
};
|
||||
|
||||
const ORIENTATION_CACHE = new Map<string, LinkOrientation>();
|
||||
const KNOWN_PARENT_TABLES = new Set([
|
||||
'machines',
|
||||
'composants',
|
||||
'pieces',
|
||||
'products',
|
||||
]);
|
||||
const oppositeColumn = (column: 'A' | 'B'): 'A' | 'B' =>
|
||||
column === 'A' ? 'B' : 'A';
|
||||
|
||||
async function resolveOrientation(
|
||||
prisma: PrismaExecutor & {
|
||||
__getConstructeurLinkOrientation?: (
|
||||
table: string,
|
||||
) => LinkOrientation | Promise<LinkOrientation>;
|
||||
},
|
||||
tableName: string,
|
||||
): Promise<LinkOrientation> {
|
||||
const cached = ORIENTATION_CACHE.get(tableName);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (typeof prisma.__getConstructeurLinkOrientation === 'function') {
|
||||
const orientation =
|
||||
await prisma.__getConstructeurLinkOrientation(tableName);
|
||||
ORIENTATION_CACHE.set(tableName, orientation);
|
||||
return orientation;
|
||||
}
|
||||
|
||||
const rows = await prisma.$queryRaw<
|
||||
Array<{ column_name: string; foreign_table_name: string }>
|
||||
>(Prisma.sql`
|
||||
SELECT
|
||||
kcu.column_name,
|
||||
ccu.table_name AS foreign_table_name
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
JOIN information_schema.constraint_column_usage ccu
|
||||
ON tc.constraint_name = ccu.constraint_name
|
||||
AND tc.table_schema = ccu.table_schema
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_schema = 'public'
|
||||
AND tc.table_name = ${tableName}
|
||||
`);
|
||||
|
||||
let parentColumn: 'A' | 'B' | null = null;
|
||||
let constructeurColumn: 'A' | 'B' | null = null;
|
||||
|
||||
for (const row of rows) {
|
||||
const column = row.column_name?.toUpperCase?.() as 'A' | 'B' | undefined;
|
||||
if (column !== 'A' && column !== 'B') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (row.foreign_table_name === 'constructeurs') {
|
||||
constructeurColumn = column;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (KNOWN_PARENT_TABLES.has(row.foreign_table_name)) {
|
||||
parentColumn = column;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!parentColumn) {
|
||||
parentColumn = column;
|
||||
}
|
||||
}
|
||||
|
||||
if (parentColumn && !constructeurColumn) {
|
||||
constructeurColumn = oppositeColumn(parentColumn);
|
||||
} else if (!parentColumn && constructeurColumn) {
|
||||
parentColumn = oppositeColumn(constructeurColumn);
|
||||
}
|
||||
|
||||
if (!parentColumn || !constructeurColumn) {
|
||||
const fallback = DEFAULT_ORIENTATIONS[tableName];
|
||||
if (fallback) {
|
||||
parentColumn ??= fallback.parentColumn;
|
||||
constructeurColumn ??= fallback.constructeurColumn;
|
||||
}
|
||||
}
|
||||
|
||||
if (!parentColumn || !constructeurColumn) {
|
||||
const columns = rows
|
||||
.map((row) => row.column_name?.toUpperCase?.() as 'A' | 'B' | undefined)
|
||||
.filter(
|
||||
(column): column is 'A' | 'B' => column === 'A' || column === 'B',
|
||||
);
|
||||
|
||||
if (columns.length === 2) {
|
||||
if (!parentColumn) {
|
||||
parentColumn = columns[0];
|
||||
}
|
||||
if (!constructeurColumn) {
|
||||
const alternative = columns.find((column) => column !== parentColumn);
|
||||
if (alternative) {
|
||||
constructeurColumn = alternative;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!parentColumn || !constructeurColumn) {
|
||||
throw new Error(
|
||||
`Impossible de déterminer l'orientation de la table ${tableName}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const orientation: LinkOrientation = {
|
||||
parentColumn,
|
||||
constructeurColumn,
|
||||
};
|
||||
ORIENTATION_CACHE.set(tableName, orientation);
|
||||
return orientation;
|
||||
}
|
||||
|
||||
export async function syncConstructeurLinks(
|
||||
prisma: PrismaExecutor,
|
||||
tableName: string,
|
||||
parentId: string,
|
||||
constructeurIds: string[],
|
||||
): Promise<string[]> {
|
||||
const executor = prisma as PrismaExecutor & {
|
||||
__syncConstructeurLinks?: (
|
||||
table: string,
|
||||
parent: string,
|
||||
ids: string[],
|
||||
) => Promise<void> | void;
|
||||
__getConstructeurLinkOrientation?: (
|
||||
table: string,
|
||||
) => LinkOrientation | Promise<LinkOrientation>;
|
||||
};
|
||||
|
||||
const table = Prisma.raw(sanitizeTableName(tableName));
|
||||
const uniqueConstructeurIds = Array.from(
|
||||
new Set(
|
||||
constructeurIds
|
||||
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
||||
.filter((value) => value.length > 0),
|
||||
),
|
||||
);
|
||||
let targetConstructeurIds = uniqueConstructeurIds;
|
||||
|
||||
const constructeurDelegate = (executor as any)?.constructeur as
|
||||
| {
|
||||
findMany?: (args: {
|
||||
where: { id: { in: string[] } };
|
||||
select: { id: boolean };
|
||||
}) => Promise<Array<{ id: string }>>;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
if (targetConstructeurIds.length > 0 && constructeurDelegate?.findMany) {
|
||||
const existing = await constructeurDelegate.findMany({
|
||||
where: { id: { in: targetConstructeurIds } },
|
||||
select: { id: true },
|
||||
});
|
||||
const existingIds = new Set(existing.map(({ id }) => id));
|
||||
targetConstructeurIds = targetConstructeurIds.filter((id) =>
|
||||
existingIds.has(id),
|
||||
);
|
||||
}
|
||||
|
||||
const orientation = await resolveOrientation(executor, tableName);
|
||||
|
||||
if (typeof executor.__syncConstructeurLinks === 'function') {
|
||||
await executor.__syncConstructeurLinks(
|
||||
tableName,
|
||||
parentId,
|
||||
targetConstructeurIds,
|
||||
);
|
||||
return targetConstructeurIds;
|
||||
}
|
||||
|
||||
await prisma.$executeRaw(
|
||||
Prisma.sql`DELETE FROM ${table} WHERE ${Prisma.raw(
|
||||
`"${orientation.parentColumn}"`,
|
||||
)} = ${parentId}`,
|
||||
);
|
||||
|
||||
if (targetConstructeurIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const valueTuples = targetConstructeurIds.map(
|
||||
(constructeurId) => Prisma.sql`(${parentId}, ${constructeurId})`,
|
||||
);
|
||||
|
||||
await prisma.$executeRaw(
|
||||
Prisma.sql`
|
||||
INSERT INTO ${table} (
|
||||
${Prisma.raw(`"${orientation.parentColumn}"`)},
|
||||
${Prisma.raw(`"${orientation.constructeurColumn}"`)}
|
||||
)
|
||||
VALUES ${Prisma.join(valueTuples)}
|
||||
ON CONFLICT DO NOTHING
|
||||
`,
|
||||
);
|
||||
return fetchConstructeurIds(executor, tableName, parentId, orientation);
|
||||
}
|
||||
|
||||
export async function fetchConstructeurIds(
|
||||
prisma: PrismaExecutor,
|
||||
tableName: string,
|
||||
parentId: string,
|
||||
orientationOverride?: LinkOrientation,
|
||||
): Promise<string[]> {
|
||||
const orientation =
|
||||
orientationOverride ?? (await resolveOrientation(prisma as any, tableName));
|
||||
const table = Prisma.raw(sanitizeTableName(tableName));
|
||||
const rows = await prisma.$queryRaw<Array<{ constructeurId: string | null }>>(
|
||||
Prisma.sql`
|
||||
SELECT ${Prisma.raw(
|
||||
`"${orientation.constructeurColumn}"`,
|
||||
)} AS "constructeurId"
|
||||
FROM ${table}
|
||||
WHERE ${Prisma.raw(`"${orientation.parentColumn}"`)} = ${parentId}
|
||||
`,
|
||||
);
|
||||
|
||||
return rows
|
||||
.map((row) =>
|
||||
typeof row.constructeurId === 'string' ? row.constructeurId : null,
|
||||
)
|
||||
.filter((id): id is string => Boolean(id));
|
||||
}
|
||||
@@ -76,6 +76,59 @@ export function normalizeComponentModelStructure(
|
||||
},
|
||||
);
|
||||
|
||||
const products = toArray((structure as any)?.products).map((product) => {
|
||||
const candidate = product as Record<string, unknown> | null | undefined;
|
||||
|
||||
if (candidate?.typeProductId) {
|
||||
const normalized: ComponentModelStructure['products'][number] = {
|
||||
typeProductId:
|
||||
ensureString(candidate.typeProductId).trim() || 'UNKNOWN',
|
||||
role: sanitizeRole(candidate.role),
|
||||
};
|
||||
|
||||
if (candidate?.familyCode) {
|
||||
const familyCode = ensureString(candidate.familyCode).trim();
|
||||
if (familyCode) {
|
||||
(normalized as Record<string, unknown>).familyCode = familyCode;
|
||||
}
|
||||
}
|
||||
|
||||
if (candidate?.typeProductLabel) {
|
||||
const label = ensureString(candidate.typeProductLabel).trim();
|
||||
if (label) {
|
||||
(normalized as Record<string, unknown>).typeProductLabel = label;
|
||||
}
|
||||
}
|
||||
|
||||
if (candidate?.reference) {
|
||||
const reference = ensureString(candidate.reference).trim();
|
||||
if (reference) {
|
||||
(normalized as Record<string, unknown>).reference = reference;
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
if (candidate?.familyCode) {
|
||||
return {
|
||||
familyCode: ensureString(candidate.familyCode).trim() || 'UNKNOWN',
|
||||
role: sanitizeRole(candidate.role),
|
||||
} as ComponentModelStructure['products'][number];
|
||||
}
|
||||
|
||||
return {
|
||||
familyCode:
|
||||
ensureString(
|
||||
candidate?.familyCode ??
|
||||
candidate?.name ??
|
||||
candidate?.typeProductLabel ??
|
||||
'UNKNOWN',
|
||||
).trim() || 'UNKNOWN',
|
||||
role: sanitizeRole(candidate?.role),
|
||||
} as ComponentModelStructure['products'][number];
|
||||
});
|
||||
|
||||
const rawSubcomponents = toArray(
|
||||
(structure as any)?.subcomponents ?? (structure as any)?.subComponents,
|
||||
);
|
||||
@@ -115,6 +168,7 @@ export function normalizeComponentModelStructure(
|
||||
|
||||
return {
|
||||
pieces,
|
||||
products,
|
||||
customFields,
|
||||
subcomponents,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ComposantsService } from './composants.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateComposantDto, UpdateComposantDto } from '../shared/dto/composant.dto';
|
||||
import {
|
||||
CreateComposantDto,
|
||||
UpdateComposantDto,
|
||||
} from '../shared/dto/composant.dto';
|
||||
|
||||
describe('ComposantsService', () => {
|
||||
let service: ComposantsService;
|
||||
@@ -32,6 +35,7 @@ describe('ComposantsService', () => {
|
||||
const dto: CreateComposantDto = {
|
||||
name: 'Comp A',
|
||||
typeComposantId: 'type-1',
|
||||
productId: ' product-1 ',
|
||||
};
|
||||
|
||||
prisma.composant.create.mockResolvedValue({ id: 'comp-1', name: dto.name });
|
||||
@@ -39,16 +43,25 @@ describe('ComposantsService', () => {
|
||||
const result = await service.create(dto);
|
||||
|
||||
expect(prisma.composant.create).toHaveBeenCalled();
|
||||
expect(prisma.composant.create.mock.calls[0][0].data.product).toEqual({
|
||||
connect: { id: 'product-1' },
|
||||
});
|
||||
expect(result).toMatchObject({ id: 'comp-1' });
|
||||
});
|
||||
|
||||
it('updates a component', async () => {
|
||||
const dto: UpdateComposantDto = { name: 'Updated' };
|
||||
const dto: UpdateComposantDto = { name: 'Updated', productId: '' };
|
||||
|
||||
prisma.composant.update.mockResolvedValue({ id: 'comp-1', name: 'Updated' });
|
||||
prisma.composant.update.mockResolvedValue({
|
||||
id: 'comp-1',
|
||||
name: 'Updated',
|
||||
});
|
||||
|
||||
await service.update('comp-1', dto);
|
||||
|
||||
expect(prisma.composant.update).toHaveBeenCalled();
|
||||
expect(prisma.composant.update.mock.calls[0][0].data.product).toEqual({
|
||||
disconnect: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConflictException, Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import {
|
||||
@@ -9,14 +9,18 @@ import {
|
||||
COMPONENT_WITH_RELATIONS_INCLUDE,
|
||||
ComposantWithRelations,
|
||||
} from '../common/constants/component-includes';
|
||||
import { syncConstructeurLinks } from '../common/utils/constructeur-link.util';
|
||||
|
||||
@Injectable()
|
||||
export class ComposantsService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
private buildCreateInput(
|
||||
private async buildCreateInput(
|
||||
createComposantDto: CreateComposantDto,
|
||||
): Prisma.ComposantCreateInput {
|
||||
): Promise<{
|
||||
data: Prisma.ComposantCreateInput;
|
||||
constructeurIds: string[];
|
||||
}> {
|
||||
const data: Prisma.ComposantCreateInput = {
|
||||
name: createComposantDto.name,
|
||||
reference: createComposantDto.reference ?? null,
|
||||
@@ -24,11 +28,11 @@ export class ComposantsService {
|
||||
createComposantDto.prix !== undefined ? createComposantDto.prix : null,
|
||||
};
|
||||
|
||||
if (createComposantDto.constructeurId) {
|
||||
data.constructeur = {
|
||||
connect: { id: createComposantDto.constructeurId },
|
||||
};
|
||||
}
|
||||
const constructeurIds = this.normalizeConstructeurIds(
|
||||
createComposantDto.constructeurIds,
|
||||
);
|
||||
const resolvedConstructeurIds =
|
||||
await this.resolveExistingConstructeurIds(constructeurIds);
|
||||
|
||||
if (createComposantDto.typeComposantId) {
|
||||
data.typeComposant = {
|
||||
@@ -36,20 +40,58 @@ export class ComposantsService {
|
||||
};
|
||||
}
|
||||
|
||||
if (createComposantDto.productId) {
|
||||
const normalizedProductId = createComposantDto.productId.trim();
|
||||
if (normalizedProductId) {
|
||||
data.product = {
|
||||
connect: { id: normalizedProductId },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (createComposantDto.structure !== undefined) {
|
||||
data.structure = createComposantDto.structure as Prisma.InputJsonValue;
|
||||
}
|
||||
|
||||
return data;
|
||||
return { data, constructeurIds: resolvedConstructeurIds };
|
||||
}
|
||||
|
||||
async create(createComposantDto: CreateComposantDto) {
|
||||
const created = await this.prisma.composant.create({
|
||||
data: this.buildCreateInput(createComposantDto),
|
||||
include: COMPONENT_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
try {
|
||||
const { data, constructeurIds } =
|
||||
await this.buildCreateInput(createComposantDto);
|
||||
const created = await this.prisma.composant.create({
|
||||
data,
|
||||
include: COMPONENT_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
|
||||
return created as ComposantWithRelations;
|
||||
let syncedConstructeurIds: string[] = [];
|
||||
if (constructeurIds.length > 0) {
|
||||
syncedConstructeurIds = await syncConstructeurLinks(
|
||||
this.prisma,
|
||||
'_ComposantConstructeurs',
|
||||
created.id,
|
||||
constructeurIds,
|
||||
);
|
||||
}
|
||||
|
||||
const refreshed = (await this.prisma.composant.findUnique({
|
||||
where: { id: created.id },
|
||||
include: COMPONENT_WITH_RELATIONS_INCLUDE,
|
||||
})) as ComposantWithRelations | null;
|
||||
|
||||
if (refreshed && syncedConstructeurIds.length > 0) {
|
||||
(
|
||||
refreshed as ComposantWithRelations & {
|
||||
constructeurIds?: string[];
|
||||
}
|
||||
).constructeurIds = [...syncedConstructeurIds];
|
||||
}
|
||||
|
||||
return refreshed;
|
||||
} catch (error) {
|
||||
this.handlePrismaError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
@@ -81,10 +123,13 @@ export class ComposantsService {
|
||||
data.prix = updateComposantDto.prix;
|
||||
}
|
||||
|
||||
if (updateComposantDto.constructeurId !== undefined) {
|
||||
data.constructeur = updateComposantDto.constructeurId
|
||||
? { connect: { id: updateComposantDto.constructeurId } }
|
||||
: { disconnect: true };
|
||||
let resolvedConstructeurIds: string[] | undefined;
|
||||
if (updateComposantDto.constructeurIds !== undefined) {
|
||||
const constructeurIds = this.normalizeConstructeurIds(
|
||||
updateComposantDto.constructeurIds,
|
||||
);
|
||||
resolvedConstructeurIds =
|
||||
await this.resolveExistingConstructeurIds(constructeurIds);
|
||||
}
|
||||
|
||||
if (updateComposantDto.typeComposantId !== undefined) {
|
||||
@@ -93,20 +138,156 @@ export class ComposantsService {
|
||||
: { disconnect: true };
|
||||
}
|
||||
|
||||
if (updateComposantDto.productId !== undefined) {
|
||||
const normalizedProductId =
|
||||
typeof updateComposantDto.productId === 'string'
|
||||
? updateComposantDto.productId.trim()
|
||||
: null;
|
||||
data.product = normalizedProductId
|
||||
? { connect: { id: normalizedProductId } }
|
||||
: { disconnect: true };
|
||||
}
|
||||
|
||||
if (updateComposantDto.structure !== undefined) {
|
||||
data.structure = updateComposantDto.structure as Prisma.InputJsonValue;
|
||||
}
|
||||
|
||||
return (await this.prisma.composant.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: COMPONENT_WITH_RELATIONS_INCLUDE,
|
||||
})) as ComposantWithRelations;
|
||||
let syncedConstructeurIds: string[] | undefined;
|
||||
try {
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.composant.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
|
||||
if (resolvedConstructeurIds !== undefined) {
|
||||
syncedConstructeurIds = await syncConstructeurLinks(
|
||||
tx,
|
||||
'_ComposantConstructeurs',
|
||||
id,
|
||||
resolvedConstructeurIds,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const refreshed = (await this.prisma.composant.findUnique({
|
||||
where: { id },
|
||||
include: COMPONENT_WITH_RELATIONS_INCLUDE,
|
||||
})) as ComposantWithRelations | null;
|
||||
|
||||
if (refreshed && syncedConstructeurIds) {
|
||||
(
|
||||
refreshed as ComposantWithRelations & {
|
||||
constructeurIds?: string[];
|
||||
}
|
||||
).constructeurIds = [...syncedConstructeurIds];
|
||||
}
|
||||
|
||||
return refreshed;
|
||||
} catch (error) {
|
||||
this.handlePrismaError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
const [machineLinksCount, documentsCount, customFieldValuesCount] =
|
||||
await Promise.all([
|
||||
this.prisma.machineComponentLink.count({
|
||||
where: { composantId: id },
|
||||
}),
|
||||
this.prisma.document.count({
|
||||
where: { composantId: id },
|
||||
}),
|
||||
this.prisma.customFieldValue.count({
|
||||
where: { composantId: id },
|
||||
}),
|
||||
]);
|
||||
|
||||
const blockingReasons: string[] = [];
|
||||
|
||||
if (machineLinksCount > 0) {
|
||||
blockingReasons.push(
|
||||
`${machineLinksCount} liaison${machineLinksCount > 1 ? 's' : ''} machine`,
|
||||
);
|
||||
}
|
||||
if (documentsCount > 0) {
|
||||
blockingReasons.push(
|
||||
`${documentsCount} document${documentsCount > 1 ? 's' : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (blockingReasons.length > 0) {
|
||||
const messageParts = [
|
||||
`Impossible de supprimer ce composant car il possède encore: ${blockingReasons.join(
|
||||
', ',
|
||||
)}.`,
|
||||
];
|
||||
|
||||
if (customFieldValuesCount > 0) {
|
||||
messageParts.push(
|
||||
`Les ${customFieldValuesCount} valeur${
|
||||
customFieldValuesCount > 1 ? 's' : ''
|
||||
} de champ personnalisé seront supprimées automatiquement une fois ces éléments détachés.`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new ConflictException(
|
||||
`${messageParts.join(' ')} Supprimez ou détachez les éléments indiqués avant de réessayer.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (customFieldValuesCount > 0) {
|
||||
await this.prisma.customFieldValue.deleteMany({
|
||||
where: { composantId: id },
|
||||
});
|
||||
}
|
||||
|
||||
return this.prisma.composant.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
private normalizeConstructeurIds(ids?: string[] | null): string[] {
|
||||
if (!Array.isArray(ids)) {
|
||||
return [];
|
||||
}
|
||||
const cleaned = ids
|
||||
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
||||
.filter((item) => item.length > 0);
|
||||
return Array.from(new Set(cleaned));
|
||||
}
|
||||
|
||||
private handlePrismaError(error: unknown): never {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (error.code === 'P2002' && this.isNameConstraint(error)) {
|
||||
throw new ConflictException('Un composant avec ce nom existe déjà.');
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
private isNameConstraint(error: Prisma.PrismaClientKnownRequestError) {
|
||||
const { target } = error.meta ?? {};
|
||||
if (Array.isArray(target)) {
|
||||
return target.includes('name');
|
||||
}
|
||||
if (typeof target === 'string') {
|
||||
return target === 'name';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private async resolveExistingConstructeurIds(
|
||||
ids: string[],
|
||||
): Promise<string[]> {
|
||||
if (!ids.length) {
|
||||
return [];
|
||||
}
|
||||
const existing = await this.prisma.constructeur.findMany({
|
||||
where: { id: { in: ids } },
|
||||
select: { id: true },
|
||||
});
|
||||
const existingIds = new Set(existing.map(({ id }) => id));
|
||||
return ids.filter((id) => existingIds.has(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ export class CustomFieldsService {
|
||||
return 'composantId' as const;
|
||||
case CustomFieldEntityType.PIECE:
|
||||
return 'pieceId' as const;
|
||||
case CustomFieldEntityType.PRODUCT:
|
||||
return 'productId' as const;
|
||||
default:
|
||||
throw new BadRequestException(
|
||||
"Type d'entité de champ personnalisé invalide.",
|
||||
@@ -114,6 +116,28 @@ export class CustomFieldsService {
|
||||
valueKey: 'pieceId' as const,
|
||||
};
|
||||
}
|
||||
case CustomFieldEntityType.PRODUCT: {
|
||||
const product = await this.prisma.product.findUnique({
|
||||
where: { id: entityId },
|
||||
select: { typeProductId: true },
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
throw new NotFoundException('Produit introuvable.');
|
||||
}
|
||||
|
||||
if (!product.typeProductId) {
|
||||
throw new BadRequestException(
|
||||
'Le produit ne possède pas de type associé pour les champs personnalisés.',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
typeId: product.typeProductId,
|
||||
customFieldTypeField: 'typeProductId' as const,
|
||||
valueKey: 'productId' as const,
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new BadRequestException(
|
||||
"Type d'entité de champ personnalisé invalide.",
|
||||
@@ -209,12 +233,20 @@ export class CustomFieldsService {
|
||||
if (existingField) {
|
||||
targetCustomFieldId = existingField.id;
|
||||
} else {
|
||||
const normalizedType = (customFieldType || 'text').trim() || 'text';
|
||||
const normalizedRequired = !!customFieldRequired;
|
||||
const orderScope = { [customFieldTypeField]: typeId } as const;
|
||||
const nextOrderIndex = await this.prisma.customField.count({
|
||||
where: orderScope,
|
||||
});
|
||||
|
||||
const createdField = await this.prisma.customField.create({
|
||||
data: {
|
||||
name: normalizedName,
|
||||
type: (customFieldType || 'text').trim() || 'text',
|
||||
required: !!customFieldRequired,
|
||||
type: normalizedType,
|
||||
required: normalizedRequired,
|
||||
options: normalizedOptions,
|
||||
orderIndex: nextOrderIndex,
|
||||
[customFieldTypeField]: typeId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -42,6 +42,11 @@ export class DocumentsController {
|
||||
return this.documentsService.findByPiece(pieceId);
|
||||
}
|
||||
|
||||
@Get('product/:productId')
|
||||
findByProduct(@Param('productId') productId: string) {
|
||||
return this.documentsService.findByProduct(productId);
|
||||
}
|
||||
|
||||
@Get('site/:siteId')
|
||||
findBySite(@Param('siteId') siteId: string) {
|
||||
return this.documentsService.findBySite(siteId);
|
||||
|
||||
@@ -16,6 +16,7 @@ export class DocumentsService {
|
||||
machine: true,
|
||||
composant: true,
|
||||
piece: true,
|
||||
product: true,
|
||||
site: true,
|
||||
},
|
||||
});
|
||||
@@ -27,6 +28,7 @@ export class DocumentsService {
|
||||
machine: true,
|
||||
composant: true,
|
||||
piece: true,
|
||||
product: true,
|
||||
site: true,
|
||||
},
|
||||
});
|
||||
@@ -39,6 +41,7 @@ export class DocumentsService {
|
||||
machine: true,
|
||||
composant: true,
|
||||
piece: true,
|
||||
product: true,
|
||||
site: true,
|
||||
},
|
||||
});
|
||||
@@ -51,6 +54,7 @@ export class DocumentsService {
|
||||
machine: true,
|
||||
composant: true,
|
||||
piece: true,
|
||||
product: true,
|
||||
site: true,
|
||||
},
|
||||
});
|
||||
@@ -63,6 +67,20 @@ export class DocumentsService {
|
||||
machine: true,
|
||||
composant: true,
|
||||
piece: true,
|
||||
product: true,
|
||||
site: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findByProduct(productId: string) {
|
||||
return this.prisma.document.findMany({
|
||||
where: { productId },
|
||||
include: {
|
||||
machine: true,
|
||||
composant: true,
|
||||
piece: true,
|
||||
product: true,
|
||||
site: true,
|
||||
},
|
||||
});
|
||||
@@ -75,6 +93,7 @@ export class DocumentsService {
|
||||
machine: true,
|
||||
composant: true,
|
||||
piece: true,
|
||||
product: true,
|
||||
site: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -59,11 +59,10 @@ describe('MachinesService', () => {
|
||||
prix: null,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
constructeurId: null,
|
||||
constructeurs: [],
|
||||
typePieceId: null,
|
||||
documents: [],
|
||||
customFieldValues: [],
|
||||
constructeur: null,
|
||||
typePiece: null,
|
||||
},
|
||||
typeMachinePieceRequirement: null,
|
||||
@@ -81,7 +80,7 @@ describe('MachinesService', () => {
|
||||
id: 'piece-root',
|
||||
name: 'Root piece',
|
||||
},
|
||||
} as any;
|
||||
};
|
||||
|
||||
const componentChildLink = {
|
||||
id: 'component-child',
|
||||
@@ -101,11 +100,10 @@ describe('MachinesService', () => {
|
||||
prix: null,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
constructeurId: null,
|
||||
constructeurs: [],
|
||||
typeComposantId: null,
|
||||
documents: [],
|
||||
customFieldValues: [],
|
||||
constructeur: null,
|
||||
typeComposant: null,
|
||||
},
|
||||
typeMachineComponentRequirement: null,
|
||||
@@ -125,7 +123,7 @@ describe('MachinesService', () => {
|
||||
name: 'Root component',
|
||||
},
|
||||
pieceLinks: [componentPieceLink],
|
||||
} as any;
|
||||
};
|
||||
|
||||
return {
|
||||
id: 'machine-1',
|
||||
@@ -135,11 +133,10 @@ describe('MachinesService', () => {
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
typeMachineId: null,
|
||||
constructeurId: null,
|
||||
constructeurs: [],
|
||||
siteId: 'site-1',
|
||||
site: null,
|
||||
typeMachine: null,
|
||||
constructeur: null,
|
||||
componentLinks: [componentRootLink, componentChildLink],
|
||||
pieceLinks: [rootPieceLink, componentPieceLink],
|
||||
customFieldValues: [],
|
||||
@@ -165,9 +162,7 @@ describe('MachinesService', () => {
|
||||
expect(rootLink.pieceLinks[0].parent?.overrides.name).toBe(
|
||||
'Root component override',
|
||||
);
|
||||
expect(rootLink.pieceLinks[0].originalPiece.name).toBe(
|
||||
'Piece component',
|
||||
);
|
||||
expect(rootLink.pieceLinks[0].originalPiece.name).toBe('Piece component');
|
||||
expect(rootLink.pieceLinks[0].piece.name).toBe('Component piece name');
|
||||
expect(rootLink.pieceLinks[0].overrides.reference).toBe('CP-001');
|
||||
expect(rootLink.overrides.name).toBe('Root component override');
|
||||
@@ -193,9 +188,7 @@ describe('MachinesService', () => {
|
||||
const root = result?.componentLinks[0];
|
||||
expect(root?.childLinks[0].parent?.id).toBe('component-root');
|
||||
expect(root?.childLinks[0].parent?.composantId).toBe('component-root');
|
||||
expect(root?.childLinks[0].originalComposant.name).toBe(
|
||||
'Child component',
|
||||
);
|
||||
expect(root?.childLinks[0].originalComposant.name).toBe('Child component');
|
||||
expect(root?.childLinks[0].composant.name).toBe('Child component');
|
||||
expect(root?.pieceLinks[0].parent?.id).toBe('component-root');
|
||||
expect(root?.pieceLinks[0].parent?.composantId).toBe('component-root');
|
||||
@@ -209,4 +202,52 @@ describe('MachinesService', () => {
|
||||
expect(result?.pieceLinks[0].piece.name).toBe('Root piece name');
|
||||
expect(result?.pieceLinks[0].overrides.reference).toBe('RP-001');
|
||||
});
|
||||
|
||||
describe('validateProductRequirements', () => {
|
||||
const buildRequirement = (overrides: Partial<any> = {}) =>
|
||||
({
|
||||
id: 'req-1',
|
||||
label: 'Hydraulic kits',
|
||||
minCount: 1,
|
||||
maxCount: 2,
|
||||
required: true,
|
||||
allowNewModels: true,
|
||||
typeProductId: 'product-type-1',
|
||||
typeProduct: { name: 'Hydraulic kit' },
|
||||
...overrides,
|
||||
}) as any;
|
||||
|
||||
const callValidate = (
|
||||
requirement: any,
|
||||
componentUsage: Record<string, number>,
|
||||
pieceUsage: Record<string, number>,
|
||||
) => {
|
||||
const map = new Map([[requirement.id, requirement]]);
|
||||
const componentMap = new Map(Object.entries(componentUsage));
|
||||
const pieceMap = new Map(Object.entries(pieceUsage));
|
||||
(service as any).validateProductRequirements(map, componentMap, pieceMap);
|
||||
};
|
||||
|
||||
it('does nothing when usage satisfies min and max constraints', () => {
|
||||
expect(() =>
|
||||
callValidate(buildRequirement(), { 'product-type-1': 1 }, {}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('throws when minimum requirement is not met', () => {
|
||||
expect(() => callValidate(buildRequirement(), {}, {})).toThrow(
|
||||
/requiert au moins 1 sélection/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when usage exceeds maximum', () => {
|
||||
expect(() =>
|
||||
callValidate(
|
||||
buildRequirement({ maxCount: 2 }),
|
||||
{ 'product-type-1': 2 },
|
||||
{ 'product-type-1': 1 },
|
||||
),
|
||||
).toThrow(/ne peut pas dépasser 2 sélection/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ import { IsEnum, IsOptional, IsString, Length, Matches } from 'class-validator';
|
||||
export enum ModelCategory {
|
||||
COMPONENT = 'COMPONENT',
|
||||
PIECE = 'PIECE',
|
||||
PRODUCT = 'PRODUCT',
|
||||
}
|
||||
|
||||
export class CreateModelTypeDto {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { UpdateModelTypeDto } from './dto/update-model-type.dto';
|
||||
import {
|
||||
ComponentModelStructureSchema,
|
||||
PieceModelStructureSchema,
|
||||
ProductModelStructureSchema,
|
||||
} from '../shared/schemas/inventory';
|
||||
|
||||
type SortField = 'name' | 'code' | 'createdAt';
|
||||
@@ -112,12 +113,22 @@ export class ModelTypeService {
|
||||
if (normalizedStructure !== undefined) {
|
||||
const skeletonValue =
|
||||
normalizedStructure === null ? Prisma.JsonNull : normalizedStructure;
|
||||
if (rest.category === ModelCategory.COMPONENT) {
|
||||
data.componentSkeleton = skeletonValue;
|
||||
data.pieceSkeleton = Prisma.JsonNull;
|
||||
} else {
|
||||
data.pieceSkeleton = skeletonValue;
|
||||
data.componentSkeleton = Prisma.JsonNull;
|
||||
switch (rest.category) {
|
||||
case ModelCategory.COMPONENT:
|
||||
data.componentSkeleton = skeletonValue;
|
||||
data.pieceSkeleton = Prisma.JsonNull;
|
||||
data.productSkeleton = Prisma.JsonNull;
|
||||
break;
|
||||
case ModelCategory.PIECE:
|
||||
data.pieceSkeleton = skeletonValue;
|
||||
data.componentSkeleton = Prisma.JsonNull;
|
||||
data.productSkeleton = Prisma.JsonNull;
|
||||
break;
|
||||
case ModelCategory.PRODUCT:
|
||||
data.productSkeleton = skeletonValue;
|
||||
data.componentSkeleton = Prisma.JsonNull;
|
||||
data.pieceSkeleton = Prisma.JsonNull;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,12 +183,22 @@ export class ModelTypeService {
|
||||
if (normalizedStructure !== undefined) {
|
||||
const skeletonValue =
|
||||
normalizedStructure === null ? Prisma.JsonNull : normalizedStructure;
|
||||
if (targetCategory === ModelCategory.COMPONENT) {
|
||||
data.componentSkeleton = skeletonValue;
|
||||
data.pieceSkeleton = Prisma.JsonNull;
|
||||
} else {
|
||||
data.pieceSkeleton = skeletonValue;
|
||||
data.componentSkeleton = Prisma.JsonNull;
|
||||
switch (targetCategory) {
|
||||
case ModelCategory.COMPONENT:
|
||||
data.componentSkeleton = skeletonValue;
|
||||
data.pieceSkeleton = Prisma.JsonNull;
|
||||
data.productSkeleton = Prisma.JsonNull;
|
||||
break;
|
||||
case ModelCategory.PIECE:
|
||||
data.pieceSkeleton = skeletonValue;
|
||||
data.componentSkeleton = Prisma.JsonNull;
|
||||
data.productSkeleton = Prisma.JsonNull;
|
||||
break;
|
||||
case ModelCategory.PRODUCT:
|
||||
data.productSkeleton = skeletonValue;
|
||||
data.componentSkeleton = Prisma.JsonNull;
|
||||
data.pieceSkeleton = Prisma.JsonNull;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,8 +233,14 @@ export class ModelTypeService {
|
||||
|
||||
private handlePrismaError(error: unknown): never {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (error.code === 'P2002' && this.isUniqueCodeConstraint(error)) {
|
||||
throw new ConflictException('Ce code est déjà utilisé.');
|
||||
if (error.code === 'P2002') {
|
||||
if (this.isUniqueCodeConstraint(error)) {
|
||||
throw new ConflictException('Ce code est déjà utilisé.');
|
||||
}
|
||||
|
||||
if (this.isUniqueNameConstraint(error)) {
|
||||
throw new ConflictException('Une catégorie avec ce nom existe déjà.');
|
||||
}
|
||||
}
|
||||
|
||||
if (error.code === 'P2025') {
|
||||
@@ -235,6 +262,17 @@ export class ModelTypeService {
|
||||
return false;
|
||||
}
|
||||
|
||||
private isUniqueNameConstraint(error: Prisma.PrismaClientKnownRequestError) {
|
||||
const { target } = error.meta ?? {};
|
||||
if (Array.isArray(target)) {
|
||||
return target.includes('name') && target.includes('category');
|
||||
}
|
||||
if (typeof target === 'string') {
|
||||
return target.includes('name') && target.includes('category');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private normalizeStructure(
|
||||
category: ModelCategory,
|
||||
structure: unknown,
|
||||
@@ -253,7 +291,12 @@ export class ModelTypeService {
|
||||
structure,
|
||||
) as Prisma.InputJsonValue;
|
||||
}
|
||||
return PieceModelStructureSchema.parse(
|
||||
if (category === ModelCategory.PIECE) {
|
||||
return PieceModelStructureSchema.parse(
|
||||
structure,
|
||||
) as Prisma.InputJsonValue;
|
||||
}
|
||||
return ProductModelStructureSchema.parse(
|
||||
structure,
|
||||
) as Prisma.InputJsonValue;
|
||||
} catch (error) {
|
||||
@@ -264,10 +307,24 @@ export class ModelTypeService {
|
||||
}
|
||||
|
||||
private mapModelType(modelType: PrismaModelType) {
|
||||
const structure =
|
||||
modelType.category === ModelCategory.COMPONENT
|
||||
? (modelType.componentSkeleton ?? null)
|
||||
: (modelType.pieceSkeleton ?? null);
|
||||
let structure: Prisma.InputJsonValue | null;
|
||||
switch (modelType.category as ModelCategory) {
|
||||
case ModelCategory.COMPONENT:
|
||||
structure = (modelType.componentSkeleton ??
|
||||
null) as Prisma.InputJsonValue | null;
|
||||
break;
|
||||
case ModelCategory.PIECE:
|
||||
structure = (modelType.pieceSkeleton ??
|
||||
null) as Prisma.InputJsonValue | null;
|
||||
break;
|
||||
case ModelCategory.PRODUCT:
|
||||
structure = (modelType.productSkeleton ??
|
||||
null) as Prisma.InputJsonValue | null;
|
||||
break;
|
||||
default:
|
||||
structure = null;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
...modelType,
|
||||
|
||||
@@ -19,6 +19,7 @@ describe('PiecesService', () => {
|
||||
customField: {
|
||||
findMany: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
customFieldValue: {
|
||||
findMany: jest.fn(),
|
||||
@@ -27,10 +28,7 @@ describe('PiecesService', () => {
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
PiecesService,
|
||||
{ provide: PrismaService, useValue: prisma },
|
||||
],
|
||||
providers: [PiecesService, { provide: PrismaService, useValue: prisma }],
|
||||
}).compile();
|
||||
|
||||
service = module.get<PiecesService>(PiecesService);
|
||||
@@ -40,29 +38,45 @@ describe('PiecesService', () => {
|
||||
const dto: CreatePieceDto = {
|
||||
name: 'Piece A',
|
||||
typePieceId: 'type-piece-1',
|
||||
productId: ' product-1 ',
|
||||
};
|
||||
|
||||
prisma.piece.create.mockResolvedValue({ id: 'piece-1', name: dto.name });
|
||||
prisma.piece.findUnique.mockResolvedValue({ id: 'piece-1', name: dto.name });
|
||||
prisma.piece.findUnique.mockResolvedValue({
|
||||
id: 'piece-1',
|
||||
name: dto.name,
|
||||
});
|
||||
prisma.customField.findMany.mockResolvedValue([]);
|
||||
prisma.customFieldValue.findMany.mockResolvedValue([]);
|
||||
|
||||
const result = await service.create(dto);
|
||||
|
||||
expect(prisma.piece.create).toHaveBeenCalled();
|
||||
expect(prisma.piece.create.mock.calls[0][0].data.product).toEqual({
|
||||
connect: { id: 'product-1' },
|
||||
});
|
||||
expect(result).toMatchObject({ id: 'piece-1' });
|
||||
});
|
||||
|
||||
it('updates a piece', async () => {
|
||||
const dto: UpdatePieceDto = { name: 'Updated piece' };
|
||||
const dto: UpdatePieceDto = { name: 'Updated piece', productId: '' };
|
||||
|
||||
prisma.piece.update.mockResolvedValue({ id: 'piece-1', name: 'Updated piece' });
|
||||
prisma.piece.findUnique.mockResolvedValue({ id: 'piece-1', name: 'Updated piece' });
|
||||
prisma.piece.update.mockResolvedValue({
|
||||
id: 'piece-1',
|
||||
name: 'Updated piece',
|
||||
});
|
||||
prisma.piece.findUnique.mockResolvedValue({
|
||||
id: 'piece-1',
|
||||
name: 'Updated piece',
|
||||
});
|
||||
prisma.customField.findMany.mockResolvedValue([]);
|
||||
prisma.customFieldValue.findMany.mockResolvedValue([]);
|
||||
|
||||
await service.update('piece-1', dto);
|
||||
|
||||
expect(prisma.piece.update).toHaveBeenCalled();
|
||||
expect(prisma.piece.update.mock.calls[0][0].data.product).toEqual({
|
||||
disconnect: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConflictException, Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { syncConstructeurLinks } from '../common/utils/constructeur-link.util';
|
||||
import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto';
|
||||
import { PieceModelStructureSchema } from '../shared/schemas/inventory';
|
||||
import type { PieceModelStructure } from '../shared/types/inventory';
|
||||
@@ -8,16 +9,30 @@ import type { PieceModelStructure } from '../shared/types/inventory';
|
||||
const PIECE_WITH_RELATIONS_INCLUDE = {
|
||||
typePiece: {
|
||||
include: {
|
||||
pieceCustomFields: true,
|
||||
pieceCustomFields: {
|
||||
orderBy: { orderIndex: 'asc' },
|
||||
},
|
||||
},
|
||||
},
|
||||
constructeur: true,
|
||||
constructeurs: true,
|
||||
documents: true,
|
||||
customFieldValues: {
|
||||
include: {
|
||||
customField: true,
|
||||
},
|
||||
},
|
||||
product: {
|
||||
include: {
|
||||
typeProduct: true,
|
||||
constructeurs: true,
|
||||
customFieldValues: {
|
||||
include: {
|
||||
customField: true,
|
||||
},
|
||||
},
|
||||
documents: true,
|
||||
},
|
||||
},
|
||||
machineLinks: {
|
||||
include: {
|
||||
machine: true,
|
||||
@@ -31,18 +46,20 @@ const PIECE_WITH_RELATIONS_INCLUDE = {
|
||||
export class PiecesService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
private buildCreateInput(createPieceDto: CreatePieceDto): Prisma.PieceCreateInput {
|
||||
private async buildCreateInput(
|
||||
createPieceDto: CreatePieceDto,
|
||||
): Promise<{ data: Prisma.PieceCreateInput; constructeurIds: string[] }> {
|
||||
const data: Prisma.PieceCreateInput = {
|
||||
name: createPieceDto.name,
|
||||
reference: createPieceDto.reference ?? null,
|
||||
prix: createPieceDto.prix !== undefined ? createPieceDto.prix : null,
|
||||
};
|
||||
|
||||
if (createPieceDto.constructeurId) {
|
||||
data.constructeur = {
|
||||
connect: { id: createPieceDto.constructeurId },
|
||||
};
|
||||
}
|
||||
const constructeurIds = this.normalizeConstructeurIds(
|
||||
createPieceDto.constructeurIds,
|
||||
);
|
||||
const resolvedConstructeurIds =
|
||||
await this.resolveExistingConstructeurIds(constructeurIds);
|
||||
|
||||
if (createPieceDto.typePieceId) {
|
||||
data.typePiece = {
|
||||
@@ -50,24 +67,69 @@ export class PiecesService {
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
if (createPieceDto.productId) {
|
||||
const normalizedProductId = createPieceDto.productId.trim();
|
||||
if (normalizedProductId) {
|
||||
data.product = {
|
||||
connect: { id: normalizedProductId },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { data, constructeurIds: resolvedConstructeurIds };
|
||||
}
|
||||
|
||||
async create(createPieceDto: CreatePieceDto) {
|
||||
const created = await this.prisma.piece.create({
|
||||
data: this.buildCreateInput(createPieceDto),
|
||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
try {
|
||||
const { data, constructeurIds } =
|
||||
await this.buildCreateInput(createPieceDto);
|
||||
|
||||
await this.applyPieceSkeleton({
|
||||
pieceId: created.id,
|
||||
typePiece: created.typePiece as PieceTypeWithSkeleton | null,
|
||||
});
|
||||
const { pieceId, syncedConstructeurIds } = await this.prisma.$transaction(
|
||||
async (tx) => {
|
||||
const created = await tx.piece.create({
|
||||
data,
|
||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
|
||||
return this.prisma.piece.findUnique({
|
||||
where: { id: created.id },
|
||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
let synced: string[] = [];
|
||||
if (constructeurIds.length > 0) {
|
||||
synced = await syncConstructeurLinks(
|
||||
tx,
|
||||
'_PieceConstructeurs',
|
||||
created.id,
|
||||
constructeurIds,
|
||||
);
|
||||
}
|
||||
|
||||
await this.applyPieceSkeleton({
|
||||
pieceId: created.id,
|
||||
typePiece: created.typePiece as PieceTypeWithSkeleton | null,
|
||||
product: created.product,
|
||||
prisma: tx,
|
||||
});
|
||||
|
||||
return {
|
||||
pieceId: created.id,
|
||||
syncedConstructeurIds: synced,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const refreshed = await this.prisma.piece.findUnique({
|
||||
where: { id: pieceId },
|
||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
|
||||
if (refreshed && syncedConstructeurIds.length > 0) {
|
||||
(
|
||||
refreshed as typeof refreshed & { constructeurIds?: string[] }
|
||||
).constructeurIds = [...syncedConstructeurIds];
|
||||
}
|
||||
|
||||
return refreshed;
|
||||
} catch (error) {
|
||||
this.handlePrismaError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
@@ -99,10 +161,13 @@ export class PiecesService {
|
||||
data.prix = updatePieceDto.prix;
|
||||
}
|
||||
|
||||
if (updatePieceDto.constructeurId !== undefined) {
|
||||
data.constructeur = updatePieceDto.constructeurId
|
||||
? { connect: { id: updatePieceDto.constructeurId } }
|
||||
: { disconnect: true };
|
||||
let resolvedConstructeurIds: string[] | undefined;
|
||||
if (updatePieceDto.constructeurIds !== undefined) {
|
||||
const constructeurIds = this.normalizeConstructeurIds(
|
||||
updatePieceDto.constructeurIds,
|
||||
);
|
||||
resolvedConstructeurIds =
|
||||
await this.resolveExistingConstructeurIds(constructeurIds);
|
||||
}
|
||||
|
||||
if (updatePieceDto.typePieceId !== undefined) {
|
||||
@@ -111,24 +176,112 @@ export class PiecesService {
|
||||
: { disconnect: true };
|
||||
}
|
||||
|
||||
const updated = await this.prisma.piece.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
if (updatePieceDto.productId !== undefined) {
|
||||
const normalizedProductId =
|
||||
typeof updatePieceDto.productId === 'string'
|
||||
? updatePieceDto.productId.trim()
|
||||
: null;
|
||||
data.product = normalizedProductId
|
||||
? { connect: { id: normalizedProductId } }
|
||||
: { disconnect: true };
|
||||
}
|
||||
|
||||
await this.applyPieceSkeleton({
|
||||
pieceId: updated.id,
|
||||
typePiece: updated.typePiece as PieceTypeWithSkeleton | null,
|
||||
});
|
||||
let syncedConstructeurIds: string[] | undefined;
|
||||
try {
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
const updated = await tx.piece.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
|
||||
return this.prisma.piece.findUnique({
|
||||
where: { id: updated.id },
|
||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
if (resolvedConstructeurIds !== undefined) {
|
||||
syncedConstructeurIds = await syncConstructeurLinks(
|
||||
tx,
|
||||
'_PieceConstructeurs',
|
||||
id,
|
||||
resolvedConstructeurIds,
|
||||
);
|
||||
}
|
||||
|
||||
await this.applyPieceSkeleton({
|
||||
pieceId: updated.id,
|
||||
typePiece: updated.typePiece as PieceTypeWithSkeleton | null,
|
||||
product: updated.product,
|
||||
prisma: tx,
|
||||
});
|
||||
});
|
||||
|
||||
const refreshed = await this.prisma.piece.findUnique({
|
||||
where: { id },
|
||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
|
||||
if (refreshed && syncedConstructeurIds) {
|
||||
(
|
||||
refreshed as typeof refreshed & { constructeurIds?: string[] }
|
||||
).constructeurIds = [...syncedConstructeurIds];
|
||||
}
|
||||
|
||||
return refreshed;
|
||||
} catch (error) {
|
||||
this.handlePrismaError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
const [machineLinksCount, documentsCount, customFieldValuesCount] =
|
||||
await Promise.all([
|
||||
this.prisma.machinePieceLink.count({
|
||||
where: { pieceId: id },
|
||||
}),
|
||||
this.prisma.document.count({
|
||||
where: { pieceId: id },
|
||||
}),
|
||||
this.prisma.customFieldValue.count({
|
||||
where: { pieceId: id },
|
||||
}),
|
||||
]);
|
||||
|
||||
const blockingReasons: string[] = [];
|
||||
|
||||
if (machineLinksCount > 0) {
|
||||
blockingReasons.push(
|
||||
`${machineLinksCount} liaison${machineLinksCount > 1 ? 's' : ''} machine`,
|
||||
);
|
||||
}
|
||||
if (documentsCount > 0) {
|
||||
blockingReasons.push(
|
||||
`${documentsCount} document${documentsCount > 1 ? 's' : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (blockingReasons.length > 0) {
|
||||
const messageParts = [
|
||||
`Impossible de supprimer cette pièce car elle possède encore: ${blockingReasons.join(
|
||||
', ',
|
||||
)}.`,
|
||||
];
|
||||
|
||||
if (customFieldValuesCount > 0) {
|
||||
messageParts.push(
|
||||
`Les ${customFieldValuesCount} valeur${
|
||||
customFieldValuesCount > 1 ? 's' : ''
|
||||
} de champ personnalisé seront supprimées automatiquement une fois ces éléments détachés.`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new ConflictException(
|
||||
`${messageParts.join(' ')} Supprimez ou détachez les éléments indiqués avant de réessayer.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (customFieldValuesCount > 0) {
|
||||
await this.prisma.customFieldValue.deleteMany({
|
||||
where: { pieceId: id },
|
||||
});
|
||||
}
|
||||
|
||||
return this.prisma.piece.delete({
|
||||
where: { id },
|
||||
});
|
||||
@@ -137,9 +290,16 @@ export class PiecesService {
|
||||
private async applyPieceSkeleton({
|
||||
pieceId,
|
||||
typePiece,
|
||||
product,
|
||||
prisma,
|
||||
}: {
|
||||
pieceId: string;
|
||||
typePiece: PieceTypeWithSkeleton | null;
|
||||
product: {
|
||||
typeProductId: string | null;
|
||||
typeProduct?: { code: string | null } | null;
|
||||
} | null;
|
||||
prisma: Prisma.TransactionClient | PrismaService;
|
||||
}) {
|
||||
if (!typePiece?.id) {
|
||||
return;
|
||||
@@ -155,13 +315,142 @@ export class PiecesService {
|
||||
}
|
||||
|
||||
const customFields = skeleton.customFields ?? [];
|
||||
const productRequirements: PieceProductRequirement[] = Array.isArray(
|
||||
skeleton.products,
|
||||
)
|
||||
? skeleton.products.filter(
|
||||
(entry): entry is PieceProductRequirement => !!entry,
|
||||
)
|
||||
: [];
|
||||
|
||||
await this.ensurePieceCustomFieldDefinitions(typePiece.id, customFields);
|
||||
await this.ensurePieceCustomFieldDefinitions(
|
||||
prisma,
|
||||
typePiece.id,
|
||||
customFields,
|
||||
);
|
||||
await this.createPieceCustomFieldValues(
|
||||
prisma,
|
||||
pieceId,
|
||||
typePiece.id,
|
||||
customFields,
|
||||
);
|
||||
|
||||
if (productRequirements.length > 0) {
|
||||
await this.ensurePieceProductCompliance({
|
||||
prisma,
|
||||
pieceId,
|
||||
product,
|
||||
requirements: productRequirements,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async ensurePieceProductCompliance({
|
||||
prisma,
|
||||
pieceId,
|
||||
product,
|
||||
requirements,
|
||||
}: {
|
||||
prisma: Prisma.TransactionClient | PrismaService;
|
||||
pieceId: string;
|
||||
product: {
|
||||
typeProductId: string | null;
|
||||
typeProduct?: { code: string | null } | null;
|
||||
} | null;
|
||||
requirements: PieceProductRequirement[];
|
||||
}) {
|
||||
const effectiveProduct =
|
||||
product ??
|
||||
(
|
||||
await prisma.piece.findUnique({
|
||||
where: { id: pieceId },
|
||||
select: {
|
||||
product: {
|
||||
select: {
|
||||
typeProductId: true,
|
||||
typeProduct: {
|
||||
select: { code: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)?.product;
|
||||
|
||||
if (!effectiveProduct) {
|
||||
throw new ConflictException(
|
||||
'Ce type de pièce impose la sélection d’un produit catalogue.',
|
||||
);
|
||||
}
|
||||
|
||||
const matches = requirements.some((requirement) =>
|
||||
this.doesProductMatchRequirement(effectiveProduct, requirement),
|
||||
);
|
||||
|
||||
if (!matches) {
|
||||
throw new ConflictException(
|
||||
'Le produit associé ne respecte pas les exigences définies par le squelette.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private doesProductMatchRequirement(
|
||||
product: {
|
||||
typeProductId: string | null;
|
||||
typeProduct?: { code: string | null } | null;
|
||||
},
|
||||
requirement: PieceProductRequirement,
|
||||
): boolean {
|
||||
if (!requirement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('typeProductId' in requirement && requirement.typeProductId) {
|
||||
const expectedId = requirement.typeProductId.trim();
|
||||
if (!expectedId) {
|
||||
return false;
|
||||
}
|
||||
const currentId = product.typeProductId
|
||||
? product.typeProductId.trim()
|
||||
: '';
|
||||
return currentId === expectedId;
|
||||
}
|
||||
|
||||
if ('familyCode' in requirement && requirement.familyCode) {
|
||||
const expectedCode = requirement.familyCode.trim().toLowerCase();
|
||||
if (!expectedCode) {
|
||||
return false;
|
||||
}
|
||||
const productCode =
|
||||
product.typeProduct?.code?.trim().toLowerCase() ?? null;
|
||||
return productCode === expectedCode;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private normalizeConstructeurIds(ids?: string[] | null): string[] {
|
||||
if (!Array.isArray(ids)) {
|
||||
return [];
|
||||
}
|
||||
const cleaned = ids
|
||||
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
||||
.filter((item) => item.length > 0);
|
||||
return Array.from(new Set(cleaned));
|
||||
}
|
||||
|
||||
private async resolveExistingConstructeurIds(
|
||||
ids: string[],
|
||||
): Promise<string[]> {
|
||||
if (!ids.length) {
|
||||
return [];
|
||||
}
|
||||
const existing = await this.prisma.constructeur.findMany({
|
||||
where: { id: { in: ids } },
|
||||
select: { id: true },
|
||||
});
|
||||
const existingIds = new Set(existing.map(({ id }) => id));
|
||||
return ids.filter((id) => existingIds.has(id));
|
||||
}
|
||||
|
||||
private parsePieceSkeleton(value: unknown): PieceModelStructure | null {
|
||||
@@ -177,6 +466,7 @@ export class PiecesService {
|
||||
}
|
||||
|
||||
private async ensurePieceCustomFieldDefinitions(
|
||||
prisma: Prisma.TransactionClient | PrismaService,
|
||||
typePieceId: string,
|
||||
customFields: PieceModelStructure['customFields'],
|
||||
) {
|
||||
@@ -188,25 +478,37 @@ export class PiecesService {
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = await this.prisma.customField.findMany({
|
||||
const existing = await prisma.customField.findMany({
|
||||
where: { typePieceId },
|
||||
select: { id: true, name: true },
|
||||
select: { id: true, name: true, orderIndex: true },
|
||||
});
|
||||
|
||||
const existingByName = new Map(
|
||||
existing.map((field) => [
|
||||
this.normalizeIdentifier(field.name) ?? field.name,
|
||||
field.id,
|
||||
field,
|
||||
]),
|
||||
);
|
||||
|
||||
for (const field of customFields) {
|
||||
for (let index = 0; index < customFields.length; index += 1) {
|
||||
const field = customFields[index];
|
||||
if (!field) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = this.normalizeIdentifier(field.name);
|
||||
if (!name || existingByName.has(name)) {
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingField = existingByName.get(name);
|
||||
if (existingField) {
|
||||
if (existingField.orderIndex !== index) {
|
||||
await prisma.customField.update({
|
||||
where: { id: existingField.id },
|
||||
data: { orderIndex: index },
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -214,22 +516,24 @@ export class PiecesService {
|
||||
const required = Boolean(field.required);
|
||||
const options = this.normalizeOptions(field);
|
||||
|
||||
const created = await this.prisma.customField.create({
|
||||
const created = await prisma.customField.create({
|
||||
data: {
|
||||
name,
|
||||
type,
|
||||
required,
|
||||
options,
|
||||
orderIndex: index,
|
||||
typePieceId,
|
||||
},
|
||||
select: { id: true },
|
||||
select: { id: true, name: true, orderIndex: true },
|
||||
});
|
||||
|
||||
existingByName.set(name, created.id);
|
||||
existingByName.set(name, created);
|
||||
}
|
||||
}
|
||||
|
||||
private async createPieceCustomFieldValues(
|
||||
prisma: Prisma.TransactionClient | PrismaService,
|
||||
pieceId: string,
|
||||
typePieceId: string,
|
||||
customFields: PieceModelStructure['customFields'],
|
||||
@@ -242,7 +546,7 @@ export class PiecesService {
|
||||
return;
|
||||
}
|
||||
|
||||
const definitions = await this.prisma.customField.findMany({
|
||||
const definitions = await prisma.customField.findMany({
|
||||
where: { typePieceId },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
@@ -258,7 +562,7 @@ export class PiecesService {
|
||||
]),
|
||||
);
|
||||
|
||||
const existingValues = await this.prisma.customFieldValue.findMany({
|
||||
const existingValues = await prisma.customFieldValue.findMany({
|
||||
where: { pieceId },
|
||||
select: { customFieldId: true },
|
||||
});
|
||||
@@ -282,7 +586,7 @@ export class PiecesService {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.prisma.customFieldValue.create({
|
||||
await prisma.customFieldValue.create({
|
||||
data: {
|
||||
customFieldId: definitionId,
|
||||
pieceId,
|
||||
@@ -343,10 +647,37 @@ export class PiecesService {
|
||||
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
private handlePrismaError(error: unknown): never {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (error.code === 'P2002' && this.isNameConstraint(error)) {
|
||||
throw new ConflictException('Une pièce avec ce nom existe déjà.');
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
private isNameConstraint(error: Prisma.PrismaClientKnownRequestError) {
|
||||
const { target } = error.meta ?? {};
|
||||
if (Array.isArray(target)) {
|
||||
return target.includes('name');
|
||||
}
|
||||
if (typeof target === 'string') {
|
||||
return target === 'name';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
type PieceTypeWithSkeleton = Prisma.ModelTypeGetPayload<{
|
||||
include: { pieceCustomFields: true };
|
||||
}>;
|
||||
|
||||
type PieceCustomFieldEntry = NonNullable<PieceModelStructure['customFields']>[number];
|
||||
type PieceCustomFieldEntry = NonNullable<
|
||||
PieceModelStructure['customFields']
|
||||
>[number];
|
||||
|
||||
type PieceProductRequirement = NonNullable<
|
||||
PieceModelStructure['products']
|
||||
>[number];
|
||||
|
||||
49
src/products/dto/list-products.dto.ts
Normal file
49
src/products/dto/list-products.dto.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { IsEnum, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export enum ProductSortField {
|
||||
NAME = 'name',
|
||||
REFERENCE = 'reference',
|
||||
CREATED_AT = 'createdAt',
|
||||
SUPPLIER_PRICE = 'supplierPrice',
|
||||
}
|
||||
|
||||
export enum SortDirection {
|
||||
ASC = 'asc',
|
||||
DESC = 'desc',
|
||||
}
|
||||
|
||||
export class ListProductsQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
q?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
typeProductId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
constructeurId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
limit?: number;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
offset?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(ProductSortField)
|
||||
sort?: ProductSortField;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(SortDirection)
|
||||
dir?: SortDirection;
|
||||
}
|
||||
43
src/products/products.controller.ts
Normal file
43
src/products/products.controller.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { ProductsService } from './products.service';
|
||||
import { CreateProductDto, UpdateProductDto } from '../shared/dto/product.dto';
|
||||
import { ListProductsQueryDto } from './dto/list-products.dto';
|
||||
|
||||
@Controller('products')
|
||||
export class ProductsController {
|
||||
constructor(private readonly productsService: ProductsService) {}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: ListProductsQueryDto) {
|
||||
return this.productsService.list(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.productsService.findOne(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() createProductDto: CreateProductDto) {
|
||||
return this.productsService.create(createProductDto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() updateProductDto: UpdateProductDto) {
|
||||
return this.productsService.update(id, updateProductDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.productsService.remove(id);
|
||||
}
|
||||
}
|
||||
9
src/products/products.module.ts
Normal file
9
src/products/products.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ProductsController } from './products.controller';
|
||||
import { ProductsService } from './products.service';
|
||||
|
||||
@Module({
|
||||
controllers: [ProductsController],
|
||||
providers: [ProductsService],
|
||||
})
|
||||
export class ProductsModule {}
|
||||
240
src/products/products.service.spec.ts
Normal file
240
src/products/products.service.spec.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConflictException } from '@nestjs/common';
|
||||
import { Prisma, Product } from '@prisma/client';
|
||||
import { ProductsService } from './products.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { syncConstructeurLinks } from '../common/utils/constructeur-link.util';
|
||||
import { CreateProductDto, UpdateProductDto } from '../shared/dto/product.dto';
|
||||
import { ProductSortField, SortDirection } from './dto/list-products.dto';
|
||||
|
||||
jest.mock('../common/utils/constructeur-link.util', () => ({
|
||||
syncConstructeurLinks: jest.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
describe('ProductsService', () => {
|
||||
let service: ProductsService;
|
||||
let prisma: {
|
||||
product: any;
|
||||
constructeur: any;
|
||||
piece: any;
|
||||
composant: any;
|
||||
document: any;
|
||||
$transaction: jest.Mock;
|
||||
};
|
||||
const mockSyncConstructeurLinks = syncConstructeurLinks as jest.Mock;
|
||||
|
||||
beforeEach(async () => {
|
||||
prisma = {
|
||||
product: {
|
||||
findMany: jest.fn(),
|
||||
count: jest.fn(),
|
||||
findUnique: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
constructeur: {
|
||||
findMany: jest.fn(),
|
||||
},
|
||||
piece: {
|
||||
count: jest.fn(),
|
||||
},
|
||||
composant: {
|
||||
count: jest.fn(),
|
||||
},
|
||||
document: {
|
||||
count: jest.fn(),
|
||||
},
|
||||
$transaction: jest.fn((arg: any) => {
|
||||
if (Array.isArray(arg)) {
|
||||
return Promise.all(arg);
|
||||
}
|
||||
if (typeof arg === 'function') {
|
||||
return arg(prisma);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ProductsService,
|
||||
{ provide: PrismaService, useValue: prisma },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ProductsService>(ProductsService);
|
||||
mockSyncConstructeurLinks.mockClear();
|
||||
});
|
||||
|
||||
describe('list', () => {
|
||||
it('returns products with mapped constructeur ids and pagination meta', async () => {
|
||||
const product: Product & {
|
||||
constructeurs: Array<{ id: string }>;
|
||||
documents: any[];
|
||||
customFieldValues: any[];
|
||||
pieces: any[];
|
||||
composants: any[];
|
||||
typeProduct: null;
|
||||
} = {
|
||||
id: 'prod-1',
|
||||
name: 'Product 1',
|
||||
reference: 'P-001',
|
||||
supplierPrice: new Prisma.Decimal(120),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
typeProductId: null,
|
||||
constructeurs: [{ id: 'const-1' }],
|
||||
documents: [],
|
||||
customFieldValues: [],
|
||||
pieces: [],
|
||||
composants: [],
|
||||
typeProduct: null,
|
||||
};
|
||||
|
||||
prisma.product.findMany.mockResolvedValue([product]);
|
||||
prisma.product.count.mockResolvedValue(1);
|
||||
|
||||
const result = await service.list({
|
||||
q: ' Product ',
|
||||
limit: 200,
|
||||
sort: ProductSortField.NAME,
|
||||
dir: SortDirection.ASC,
|
||||
});
|
||||
|
||||
expect(prisma.product.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
OR: expect.any(Array),
|
||||
}),
|
||||
take: 100, // capped
|
||||
orderBy: { name: 'asc' },
|
||||
}),
|
||||
);
|
||||
expect(result.total).toBe(1);
|
||||
expect(result.items[0]).toMatchObject({
|
||||
id: 'prod-1',
|
||||
constructeurIds: ['const-1'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('persists a product and synchronizes constructeurs', async () => {
|
||||
const dto: CreateProductDto = {
|
||||
name: 'New Product',
|
||||
supplierPrice: 150.5,
|
||||
constructeurIds: ['const-1', 'const-1', ''],
|
||||
typeProductId: 'type-1',
|
||||
};
|
||||
|
||||
prisma.constructeur.findMany.mockResolvedValue([{ id: 'const-1' }]);
|
||||
prisma.product.create.mockResolvedValue({
|
||||
id: 'prod-1',
|
||||
});
|
||||
prisma.product.findUnique.mockResolvedValue({
|
||||
id: 'prod-1',
|
||||
name: dto.name,
|
||||
reference: null,
|
||||
supplierPrice: new Prisma.Decimal(150.5),
|
||||
typeProductId: dto.typeProductId,
|
||||
constructeurs: [{ id: 'const-1' }],
|
||||
documents: [],
|
||||
customFieldValues: [],
|
||||
pieces: [],
|
||||
composants: [],
|
||||
typeProduct: null,
|
||||
});
|
||||
mockSyncConstructeurLinks.mockResolvedValue(['const-1']);
|
||||
|
||||
const created = await service.create(dto);
|
||||
|
||||
expect(prisma.product.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
name: 'New Product',
|
||||
supplierPrice: expect.any(Prisma.Decimal),
|
||||
typeProduct: { connect: { id: 'type-1' } },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
String(prisma.product.create.mock.calls[0][0].data.supplierPrice),
|
||||
).toBe('150.5');
|
||||
expect(mockSyncConstructeurLinks).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
'_ProductConstructeurs',
|
||||
'prod-1',
|
||||
['const-1'],
|
||||
);
|
||||
expect(created.constructeurIds).toEqual(['const-1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('updates product fields and synchronizes constructeurs when provided', async () => {
|
||||
const dto: UpdateProductDto = {
|
||||
supplierPrice: null,
|
||||
constructeurIds: ['const-2'],
|
||||
typeProductId: '',
|
||||
};
|
||||
|
||||
prisma.constructeur.findMany.mockResolvedValue([{ id: 'const-2' }]);
|
||||
prisma.product.findUnique.mockResolvedValue({
|
||||
id: 'prod-1',
|
||||
name: 'Existing product',
|
||||
reference: null,
|
||||
supplierPrice: null,
|
||||
typeProductId: null,
|
||||
constructeurs: [{ id: 'const-2' }],
|
||||
documents: [],
|
||||
customFieldValues: [],
|
||||
pieces: [],
|
||||
composants: [],
|
||||
typeProduct: null,
|
||||
});
|
||||
|
||||
await service.update('prod-1', dto);
|
||||
|
||||
expect(prisma.product.update).toHaveBeenCalledWith({
|
||||
where: { id: 'prod-1' },
|
||||
data: expect.objectContaining({
|
||||
supplierPrice: null,
|
||||
typeProduct: { disconnect: true },
|
||||
}),
|
||||
});
|
||||
expect(mockSyncConstructeurLinks).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
'_ProductConstructeurs',
|
||||
'prod-1',
|
||||
['const-2'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('throws when product is still referenced', async () => {
|
||||
prisma.piece.count.mockResolvedValue(1);
|
||||
prisma.composant.count.mockResolvedValue(0);
|
||||
prisma.document.count.mockResolvedValue(2);
|
||||
|
||||
await expect(service.remove('prod-1')).rejects.toBeInstanceOf(
|
||||
ConflictException,
|
||||
);
|
||||
expect(prisma.product.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('deletes product when no references remain', async () => {
|
||||
prisma.piece.count.mockResolvedValue(0);
|
||||
prisma.composant.count.mockResolvedValue(0);
|
||||
prisma.document.count.mockResolvedValue(0);
|
||||
prisma.product.delete.mockResolvedValue(undefined);
|
||||
|
||||
await service.remove('prod-1');
|
||||
|
||||
expect(prisma.product.delete).toHaveBeenCalledWith({
|
||||
where: { id: 'prod-1' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
322
src/products/products.service.ts
Normal file
322
src/products/products.service.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateProductDto, UpdateProductDto } from '../shared/dto/product.dto';
|
||||
import { PRODUCT_WITH_RELATIONS_INCLUDE } from '../common/constants/product-includes';
|
||||
import { syncConstructeurLinks } from '../common/utils/constructeur-link.util';
|
||||
import {
|
||||
ListProductsQueryDto,
|
||||
ProductSortField,
|
||||
SortDirection,
|
||||
} from './dto/list-products.dto';
|
||||
|
||||
type ProductWithRelations = Prisma.ProductGetPayload<{
|
||||
include: typeof PRODUCT_WITH_RELATIONS_INCLUDE;
|
||||
}>;
|
||||
|
||||
@Injectable()
|
||||
export class ProductsService {
|
||||
private readonly allowedSortFields: ProductSortField[] = [
|
||||
ProductSortField.CREATED_AT,
|
||||
ProductSortField.NAME,
|
||||
ProductSortField.REFERENCE,
|
||||
ProductSortField.SUPPLIER_PRICE,
|
||||
];
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(params: ListProductsQueryDto) {
|
||||
const {
|
||||
q,
|
||||
typeProductId,
|
||||
constructeurId,
|
||||
limit = 20,
|
||||
offset = 0,
|
||||
sort = ProductSortField.CREATED_AT,
|
||||
dir = SortDirection.DESC,
|
||||
} = params;
|
||||
|
||||
const cappedLimit = Math.min(Math.max(limit, 1), 100);
|
||||
const safeOffset = Math.max(offset, 0);
|
||||
|
||||
const orderByField = this.allowedSortFields.includes(sort)
|
||||
? sort
|
||||
: ProductSortField.CREATED_AT;
|
||||
const orderByDir: SortDirection =
|
||||
dir === SortDirection.ASC ? SortDirection.ASC : SortDirection.DESC;
|
||||
|
||||
const where: Prisma.ProductWhereInput = {};
|
||||
|
||||
if (q?.trim()) {
|
||||
const term = q.trim();
|
||||
where.OR = [
|
||||
{ name: { contains: term, mode: 'insensitive' } },
|
||||
{ reference: { contains: term, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
|
||||
if (typeProductId) {
|
||||
where.typeProductId = typeProductId;
|
||||
}
|
||||
|
||||
if (constructeurId) {
|
||||
where.constructeurs = {
|
||||
some: { id: constructeurId },
|
||||
};
|
||||
}
|
||||
|
||||
const [items, total] = await this.prisma.$transaction([
|
||||
this.prisma.product.findMany({
|
||||
where,
|
||||
include: PRODUCT_WITH_RELATIONS_INCLUDE,
|
||||
orderBy: {
|
||||
[orderByField]: orderByDir,
|
||||
},
|
||||
skip: safeOffset,
|
||||
take: cappedLimit,
|
||||
}),
|
||||
this.prisma.product.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
items: items.map((item) => this.mapProduct(item)),
|
||||
total,
|
||||
offset: safeOffset,
|
||||
limit: cappedLimit,
|
||||
};
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const product = await this.prisma.product.findUnique({
|
||||
where: { id },
|
||||
include: PRODUCT_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
throw new NotFoundException('Produit introuvable.');
|
||||
}
|
||||
|
||||
return this.mapProduct(product);
|
||||
}
|
||||
|
||||
async create(createProductDto: CreateProductDto) {
|
||||
try {
|
||||
const data: Prisma.ProductCreateInput = {
|
||||
name: createProductDto.name,
|
||||
reference: createProductDto.reference ?? null,
|
||||
supplierPrice:
|
||||
createProductDto.supplierPrice === undefined ||
|
||||
createProductDto.supplierPrice === null
|
||||
? null
|
||||
: new Prisma.Decimal(createProductDto.supplierPrice),
|
||||
};
|
||||
|
||||
if (createProductDto.typeProductId) {
|
||||
data.typeProduct = {
|
||||
connect: { id: createProductDto.typeProductId },
|
||||
};
|
||||
}
|
||||
|
||||
const constructeurIds = this.normalizeConstructeurIds(
|
||||
createProductDto.constructeurIds,
|
||||
);
|
||||
const resolvedConstructeurIds =
|
||||
await this.resolveExistingConstructeurIds(constructeurIds);
|
||||
|
||||
const created = await this.prisma.product.create({
|
||||
data,
|
||||
include: PRODUCT_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
|
||||
let syncedConstructeurIds: string[] = [];
|
||||
if (resolvedConstructeurIds.length > 0) {
|
||||
syncedConstructeurIds = await syncConstructeurLinks(
|
||||
this.prisma,
|
||||
'_ProductConstructeurs',
|
||||
created.id,
|
||||
resolvedConstructeurIds,
|
||||
);
|
||||
}
|
||||
|
||||
const refreshed = await this.prisma.product.findUnique({
|
||||
where: { id: created.id },
|
||||
include: PRODUCT_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
|
||||
if (!refreshed) {
|
||||
return this.mapProduct(created);
|
||||
}
|
||||
|
||||
const mapped = this.mapProduct(refreshed);
|
||||
if (syncedConstructeurIds.length > 0) {
|
||||
mapped.constructeurIds = [...syncedConstructeurIds];
|
||||
}
|
||||
|
||||
return mapped;
|
||||
} catch (error) {
|
||||
this.handlePrismaError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: string, updateProductDto: UpdateProductDto) {
|
||||
try {
|
||||
const data: Prisma.ProductUpdateInput = {};
|
||||
|
||||
if (updateProductDto.name !== undefined) {
|
||||
data.name = updateProductDto.name;
|
||||
}
|
||||
|
||||
if (updateProductDto.reference !== undefined) {
|
||||
data.reference = updateProductDto.reference;
|
||||
}
|
||||
|
||||
if (updateProductDto.supplierPrice !== undefined) {
|
||||
data.supplierPrice =
|
||||
updateProductDto.supplierPrice === null
|
||||
? null
|
||||
: new Prisma.Decimal(updateProductDto.supplierPrice);
|
||||
}
|
||||
|
||||
if (updateProductDto.typeProductId !== undefined) {
|
||||
data.typeProduct = updateProductDto.typeProductId
|
||||
? { connect: { id: updateProductDto.typeProductId } }
|
||||
: { disconnect: true };
|
||||
}
|
||||
|
||||
let resolvedConstructeurIds: string[] | undefined;
|
||||
if (updateProductDto.constructeurIds !== undefined) {
|
||||
const constructeurIds = this.normalizeConstructeurIds(
|
||||
updateProductDto.constructeurIds,
|
||||
);
|
||||
resolvedConstructeurIds =
|
||||
await this.resolveExistingConstructeurIds(constructeurIds);
|
||||
}
|
||||
|
||||
let syncedConstructeurIds: string[] | undefined;
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.product.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
|
||||
if (resolvedConstructeurIds !== undefined) {
|
||||
syncedConstructeurIds = await syncConstructeurLinks(
|
||||
tx,
|
||||
'_ProductConstructeurs',
|
||||
id,
|
||||
resolvedConstructeurIds,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const refreshed = await this.prisma.product.findUnique({
|
||||
where: { id },
|
||||
include: PRODUCT_WITH_RELATIONS_INCLUDE,
|
||||
});
|
||||
|
||||
if (!refreshed) {
|
||||
throw new NotFoundException('Produit introuvable.');
|
||||
}
|
||||
|
||||
const mapped = this.mapProduct(refreshed);
|
||||
if (syncedConstructeurIds) {
|
||||
mapped.constructeurIds = [...syncedConstructeurIds];
|
||||
}
|
||||
|
||||
return mapped;
|
||||
} catch (error) {
|
||||
this.handlePrismaError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
const [pieceCount, componentCount, documentCount] = await Promise.all([
|
||||
this.prisma.piece.count({
|
||||
where: { productId: id },
|
||||
}),
|
||||
this.prisma.composant.count({
|
||||
where: { productId: id },
|
||||
}),
|
||||
this.prisma.document.count({
|
||||
where: { productId: id },
|
||||
}),
|
||||
]);
|
||||
|
||||
const blockingReasons: string[] = [];
|
||||
if (pieceCount > 0) {
|
||||
blockingReasons.push(`${pieceCount} pièce${pieceCount > 1 ? 's' : ''}`);
|
||||
}
|
||||
if (componentCount > 0) {
|
||||
blockingReasons.push(
|
||||
`${componentCount} composant${componentCount > 1 ? 's' : ''}`,
|
||||
);
|
||||
}
|
||||
if (documentCount > 0) {
|
||||
blockingReasons.push(
|
||||
`${documentCount} document${documentCount > 1 ? 's' : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (blockingReasons.length > 0) {
|
||||
throw new ConflictException(
|
||||
`Impossible de supprimer ce produit car il est encore lié à ${blockingReasons.join(
|
||||
', ',
|
||||
)}.`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.prisma.product.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
private normalizeConstructeurIds(ids?: string[] | null): string[] {
|
||||
if (!Array.isArray(ids)) {
|
||||
return [];
|
||||
}
|
||||
return Array.from(
|
||||
new Set(
|
||||
ids
|
||||
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
||||
.filter((value) => value.length > 0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async resolveExistingConstructeurIds(ids: string[]) {
|
||||
if (ids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const existing = await this.prisma.constructeur.findMany({
|
||||
where: { id: { in: ids } },
|
||||
select: { id: true },
|
||||
});
|
||||
const existingIds = new Set(existing.map(({ id }) => id));
|
||||
return ids.filter((id) => existingIds.has(id));
|
||||
}
|
||||
|
||||
private mapProduct(product: ProductWithRelations) {
|
||||
return {
|
||||
...product,
|
||||
constructeurIds: product.constructeurs.map((item) => item.id),
|
||||
};
|
||||
}
|
||||
|
||||
private handlePrismaError(error: unknown): never {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (error.code === 'P2002') {
|
||||
throw new ConflictException('Un produit avec ce nom existe déjà.');
|
||||
}
|
||||
if (error.code === 'P2025') {
|
||||
throw new NotFoundException('Produit introuvable.');
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
import { IsString, IsOptional, IsNumber, IsObject } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
IsArray,
|
||||
} from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export class CreateComposantDto {
|
||||
@@ -18,8 +24,10 @@ export class CreateComposantDto {
|
||||
reference?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
constructeurId?: string;
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
constructeurIds?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => (value === '' ? null : value))
|
||||
@@ -37,6 +45,10 @@ export class CreateComposantDto {
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
structure?: Record<string, any>;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
productId?: string;
|
||||
}
|
||||
|
||||
export class UpdateComposantDto {
|
||||
@@ -49,8 +61,9 @@ export class UpdateComposantDto {
|
||||
reference?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
constructeurId?: string;
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
constructeurIds?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => (value === '' ? null : value))
|
||||
@@ -64,4 +77,9 @@ export class UpdateComposantDto {
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
structure?: Record<string, any>;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => (value === '' ? null : value))
|
||||
@IsString()
|
||||
productId?: string | null;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export enum CustomFieldEntityType {
|
||||
MACHINE = 'machine',
|
||||
COMPOSANT = 'composant',
|
||||
PIECE = 'piece',
|
||||
PRODUCT = 'product',
|
||||
}
|
||||
|
||||
export class CustomFieldEntityParamsDto {
|
||||
@@ -76,6 +77,10 @@ export class CreateCustomFieldValueDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
pieceId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
productId?: string;
|
||||
}
|
||||
|
||||
export class UpdateCustomFieldValueDto {
|
||||
|
||||
@@ -31,6 +31,10 @@ export class CreateDocumentDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
siteId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
productId?: string;
|
||||
}
|
||||
|
||||
export class UpdateDocumentDto {
|
||||
@@ -57,4 +61,20 @@ export class UpdateDocumentDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
siteId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
machineId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
composantId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
pieceId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
productId?: string;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ export class MachineComponentLinkPayloadDto {
|
||||
@IsString()
|
||||
composantId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
productId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
componentId?: string;
|
||||
@@ -97,6 +101,10 @@ export class MachinePieceLinkPayloadDto {
|
||||
@IsString()
|
||||
composantId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
productId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentLinkId?: string;
|
||||
@@ -142,6 +150,59 @@ export class MachinePieceLinkPayloadDto {
|
||||
overrides?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class MachineProductLinkPayloadDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
id?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
linkId?: string;
|
||||
|
||||
@IsString()
|
||||
requirementId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
productId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
typeProductId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentLinkId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentComponentLinkId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentPieceLinkId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentRequirementId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentComponentRequirementId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentPieceRequirementId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentMachineComponentRequirementId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
parentMachinePieceRequirementId?: string;
|
||||
}
|
||||
|
||||
export class CreateMachineDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
@@ -154,8 +215,9 @@ export class CreateMachineDto {
|
||||
reference?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
constructeurId?: string;
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
constructeurIds?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsDecimal()
|
||||
@@ -176,6 +238,12 @@ export class CreateMachineDto {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => MachinePieceLinkPayloadDto)
|
||||
pieceLinks?: MachinePieceLinkPayloadDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => MachineProductLinkPayloadDto)
|
||||
productLinks?: MachineProductLinkPayloadDto[];
|
||||
}
|
||||
|
||||
export class UpdateMachineDto {
|
||||
@@ -188,8 +256,9 @@ export class UpdateMachineDto {
|
||||
reference?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
constructeurId?: string;
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
constructeurIds?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsDecimal()
|
||||
@@ -212,7 +281,14 @@ export class ReconfigureMachineDto {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => MachinePieceLinkPayloadDto)
|
||||
pieceLinks?: MachinePieceLinkPayloadDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => MachineProductLinkPayloadDto)
|
||||
productLinks?: MachineProductLinkPayloadDto[];
|
||||
}
|
||||
|
||||
export type MachineComponentLinkInput = MachineComponentLinkPayloadDto;
|
||||
export type MachinePieceLinkInput = MachinePieceLinkPayloadDto;
|
||||
export type MachineProductLinkInput = MachineProductLinkPayloadDto;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsString, IsOptional, IsNumber } from 'class-validator';
|
||||
import { IsString, IsOptional, IsNumber, IsArray } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export class CreatePieceDto {
|
||||
@@ -18,8 +18,10 @@ export class CreatePieceDto {
|
||||
reference?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
constructeurId?: string;
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
constructeurIds?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => (value === '' ? null : value))
|
||||
@@ -33,6 +35,10 @@ export class CreatePieceDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
typeMachinePieceRequirementId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
productId?: string;
|
||||
}
|
||||
|
||||
export class UpdatePieceDto {
|
||||
@@ -45,8 +51,10 @@ export class UpdatePieceDto {
|
||||
reference?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
constructeurId?: string;
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
constructeurIds?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => (value === '' ? null : value))
|
||||
@@ -56,4 +64,9 @@ export class UpdatePieceDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
typePieceId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => (value === '' ? null : value))
|
||||
@IsString()
|
||||
productId?: string | null;
|
||||
}
|
||||
|
||||
28
src/shared/dto/product.dto.ts
Normal file
28
src/shared/dto/product.dto.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { IsArray, IsNumber, IsOptional, IsString } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
|
||||
export class CreateProductDto {
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
reference?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => (value === '' ? null : value))
|
||||
@IsNumber({}, { message: 'supplierPrice must be a valid number' })
|
||||
supplierPrice?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
typeProductId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
constructeurIds?: string[];
|
||||
}
|
||||
|
||||
export class UpdateProductDto extends PartialType(CreateProductDto) {}
|
||||
@@ -36,6 +36,10 @@ export class CreateCustomFieldDto {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
options?: string[]; // Pour les champs de type SELECT
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
orderIndex?: number;
|
||||
}
|
||||
|
||||
export class UpdateCustomFieldDto {
|
||||
@@ -54,6 +58,10 @@ export class UpdateCustomFieldDto {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
options?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
orderIndex?: number;
|
||||
}
|
||||
|
||||
export class TypeMachineComponentRequirementDto {
|
||||
@@ -79,6 +87,10 @@ export class TypeMachineComponentRequirementDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
allowNewModels?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
orderIndex?: number;
|
||||
}
|
||||
|
||||
export class TypeMachinePieceRequirementDto {
|
||||
@@ -104,6 +116,39 @@ export class TypeMachinePieceRequirementDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
allowNewModels?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
orderIndex?: number;
|
||||
}
|
||||
|
||||
export class TypeMachineProductRequirementDto {
|
||||
@IsString()
|
||||
typeProductId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
label?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
minCount?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
maxCount?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
required?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
allowNewModels?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
orderIndex?: number;
|
||||
}
|
||||
|
||||
export class CreateTypeMachineDto {
|
||||
@@ -145,6 +190,12 @@ export class CreateTypeMachineDto {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => TypeMachinePieceRequirementDto)
|
||||
pieceRequirements?: TypeMachinePieceRequirementDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => TypeMachineProductRequirementDto)
|
||||
productRequirements?: TypeMachineProductRequirementDto[];
|
||||
}
|
||||
|
||||
export class UpdateTypeMachineDto {
|
||||
@@ -187,6 +238,12 @@ export class UpdateTypeMachineDto {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => TypeMachinePieceRequirementDto)
|
||||
pieceRequirements?: TypeMachinePieceRequirementDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => TypeMachineProductRequirementDto)
|
||||
productRequirements?: TypeMachineProductRequirementDto[];
|
||||
}
|
||||
|
||||
export class CreateTypeComposantDto {
|
||||
|
||||
@@ -2,7 +2,9 @@ import { normalizeComponentModelStructure } from '../../component-models/structu
|
||||
import type {
|
||||
ComponentModelStructure,
|
||||
PieceModelCustomField,
|
||||
PieceModelProduct,
|
||||
PieceModelStructure,
|
||||
ProductModelStructure,
|
||||
} from '../types/inventory';
|
||||
|
||||
export class ComponentModelStructureValidationError extends Error {
|
||||
@@ -28,6 +30,67 @@ function sanitizeOptionalString(value: unknown): string | undefined {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function validateProducts(
|
||||
products: ComponentModelStructure['products'],
|
||||
): ComponentModelStructure['products'] {
|
||||
return products.map((product, index) => {
|
||||
if ('typeProductId' in product) {
|
||||
const typeProductId = assertString(
|
||||
product.typeProductId,
|
||||
`products[${index}].typeProductId`,
|
||||
).trim();
|
||||
if (!typeProductId) {
|
||||
throw new ComponentModelStructureValidationError(
|
||||
`products[${index}].typeProductId ne peut pas être vide`,
|
||||
);
|
||||
}
|
||||
const payload: ComponentModelStructure['products'][number] = {
|
||||
typeProductId,
|
||||
role: sanitizeOptionalString(product.role),
|
||||
};
|
||||
if ('familyCode' in product && product.familyCode) {
|
||||
const familyCode = assertString(
|
||||
product.familyCode,
|
||||
`products[${index}].familyCode`,
|
||||
).trim();
|
||||
if (familyCode) {
|
||||
(payload as Record<string, unknown>).familyCode = familyCode;
|
||||
}
|
||||
}
|
||||
if ('reference' in product && product.reference) {
|
||||
(payload as Record<string, unknown>).reference = sanitizeOptionalString(
|
||||
product.reference,
|
||||
);
|
||||
}
|
||||
if ('typeProductLabel' in product && product.typeProductLabel) {
|
||||
(payload as Record<string, unknown>).typeProductLabel =
|
||||
sanitizeOptionalString(product.typeProductLabel);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
if ('familyCode' in product) {
|
||||
const familyCode = assertString(
|
||||
product.familyCode,
|
||||
`products[${index}].familyCode`,
|
||||
).trim();
|
||||
if (!familyCode) {
|
||||
throw new ComponentModelStructureValidationError(
|
||||
`products[${index}].familyCode ne peut pas être vide`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
familyCode,
|
||||
role: sanitizeOptionalString(product.role),
|
||||
};
|
||||
}
|
||||
|
||||
throw new ComponentModelStructureValidationError(
|
||||
`products[${index}] doit définir "familyCode" ou "typeProductId"`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function validatePieces(
|
||||
pieces: ComponentModelStructure['pieces'],
|
||||
): ComponentModelStructure['pieces'] {
|
||||
@@ -148,6 +211,7 @@ export const ComponentModelStructureSchema = {
|
||||
const normalized = normalizeComponentModelStructure(input);
|
||||
|
||||
return {
|
||||
products: validateProducts(normalized.products),
|
||||
pieces: validatePieces(normalized.pieces),
|
||||
customFields: validateCustomFields(normalized.customFields),
|
||||
subcomponents: validateSubcomponents(normalized.subcomponents),
|
||||
@@ -230,10 +294,57 @@ function normalizePieceModelCustomFields(
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizePieceModelProducts(products: unknown): PieceModelProduct[] {
|
||||
if (!Array.isArray(products)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return products.map((entry, index) => {
|
||||
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
||||
throw new PieceModelStructureValidationError(
|
||||
`products[${index}] doit être un objet`,
|
||||
);
|
||||
}
|
||||
|
||||
const record = entry as Record<string, unknown>;
|
||||
|
||||
const rawTypeProductId =
|
||||
typeof record.typeProductId === 'string'
|
||||
? record.typeProductId
|
||||
: typeof (record.typeProduct as { id?: unknown })?.id === 'string'
|
||||
? (record.typeProduct as { id: string }).id
|
||||
: undefined;
|
||||
const typeProductId = rawTypeProductId ? rawTypeProductId.trim() : '';
|
||||
|
||||
const rawFamilyCode =
|
||||
typeof record.familyCode === 'string'
|
||||
? record.familyCode
|
||||
: typeof (record.typeProduct as { code?: unknown })?.code === 'string'
|
||||
? (record.typeProduct as { code: string }).code
|
||||
: undefined;
|
||||
const familyCode = rawFamilyCode ? rawFamilyCode.trim() : '';
|
||||
|
||||
const rawRole = typeof record.role === 'string' ? record.role.trim() : '';
|
||||
const role = rawRole ? rawRole : undefined;
|
||||
|
||||
if (typeProductId) {
|
||||
return role ? { typeProductId, role } : { typeProductId };
|
||||
}
|
||||
|
||||
if (familyCode) {
|
||||
return role ? { familyCode, role } : { familyCode };
|
||||
}
|
||||
|
||||
throw new PieceModelStructureValidationError(
|
||||
`products[${index}] doit définir "familyCode" ou "typeProductId"`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export const PieceModelStructureSchema = {
|
||||
parse(input: unknown): PieceModelStructure {
|
||||
if (input === undefined || input === null) {
|
||||
return { customFields: [] };
|
||||
return { customFields: [], products: [] };
|
||||
}
|
||||
|
||||
if (typeof input !== 'object' || Array.isArray(input)) {
|
||||
@@ -250,6 +361,11 @@ export const PieceModelStructureSchema = {
|
||||
structure.customFields = customFields;
|
||||
}
|
||||
|
||||
const products = normalizePieceModelProducts(record.products);
|
||||
if (products.length > 0 || 'products' in record) {
|
||||
structure.products = products;
|
||||
}
|
||||
|
||||
const normalizedTypePiece = toStringOrNull(record.typePieceId);
|
||||
if (normalizedTypePiece) {
|
||||
structure.typePieceId = normalizedTypePiece;
|
||||
@@ -260,3 +376,34 @@ export const PieceModelStructureSchema = {
|
||||
return structure;
|
||||
},
|
||||
};
|
||||
|
||||
export class ProductModelStructureValidationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ProductModelStructureValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
export const ProductModelStructureSchema = {
|
||||
parse(input: unknown): ProductModelStructure {
|
||||
if (input === undefined || input === null) {
|
||||
return { customFields: [] };
|
||||
}
|
||||
|
||||
if (typeof input !== 'object' || Array.isArray(input)) {
|
||||
throw new ProductModelStructureValidationError(
|
||||
'La structure de produit doit être un objet JSON.',
|
||||
);
|
||||
}
|
||||
|
||||
const record = input as Record<string, unknown>;
|
||||
const structure: ProductModelStructure = { ...record };
|
||||
const customFields = normalizePieceModelCustomFields(record.customFields);
|
||||
|
||||
if (customFields.length > 0 || 'customFields' in record) {
|
||||
structure.customFields = customFields;
|
||||
}
|
||||
|
||||
return structure;
|
||||
},
|
||||
};
|
||||
|
||||
6
src/shared/types/constructeur-summary.type.ts
Normal file
6
src/shared/types/constructeur-summary.type.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type ConstructeurSummary = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
phone: string | null;
|
||||
};
|
||||
@@ -16,6 +16,20 @@ export type ComponentModelStructure = {
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Familles de produits autorisées (ou identifiant de famille) — pas de quantité ici.
|
||||
*/
|
||||
products: Array<
|
||||
| {
|
||||
familyCode: string;
|
||||
role?: string;
|
||||
}
|
||||
| {
|
||||
typeProductId: string;
|
||||
role?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Valeurs par défaut au niveau "modèle" (libres, mais clé obligatoire).
|
||||
*/
|
||||
@@ -48,7 +62,25 @@ export type PieceModelCustomField = {
|
||||
options?: unknown;
|
||||
};
|
||||
|
||||
export type PieceModelProduct =
|
||||
| {
|
||||
familyCode: string;
|
||||
role?: string;
|
||||
}
|
||||
| {
|
||||
typeProductId: string;
|
||||
role?: string;
|
||||
};
|
||||
|
||||
export type PieceModelStructure = {
|
||||
customFields?: PieceModelCustomField[];
|
||||
products?: PieceModelProduct[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type ProductModelCustomField = PieceModelCustomField;
|
||||
|
||||
export type ProductModelStructure = {
|
||||
customFields?: ProductModelCustomField[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConflictException, Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { TypeMachinesRepository } from '../../common/repositories/type-machines.repository';
|
||||
import {
|
||||
TYPE_MACHINE_DEFAULT_INCLUDE,
|
||||
@@ -17,7 +18,12 @@ export class TypeMachineService {
|
||||
async create(dto: CreateTypeMachineDto) {
|
||||
const data = TypeMachineMapper.toCreateInput(dto);
|
||||
|
||||
return this.repository.create(data, TYPE_MACHINE_DEFAULT_INCLUDE);
|
||||
try {
|
||||
return await this.repository.create(data, TYPE_MACHINE_DEFAULT_INCLUDE);
|
||||
} catch (error) {
|
||||
this.handlePrismaError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
@@ -53,7 +59,24 @@ export class TypeMachineService {
|
||||
await this.repository.createPieceRequirements(id, requirements);
|
||||
}
|
||||
|
||||
return this.repository.update(id, updateData, TYPE_MACHINE_DEFAULT_INCLUDE);
|
||||
if (dto.productRequirements !== undefined) {
|
||||
await this.repository.deleteProductRequirements(id);
|
||||
const requirements = TypeMachineMapper.mapProductRequirementInputs(
|
||||
dto.productRequirements,
|
||||
);
|
||||
await this.repository.createProductRequirements(id, requirements);
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.repository.update(
|
||||
id,
|
||||
updateData,
|
||||
TYPE_MACHINE_DEFAULT_INCLUDE,
|
||||
);
|
||||
} catch (error) {
|
||||
this.handlePrismaError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
@@ -69,4 +92,19 @@ export class TypeMachineService {
|
||||
await this.repository.deleteCustomFields(id);
|
||||
return this.repository.delete(id);
|
||||
}
|
||||
|
||||
private handlePrismaError(error: unknown): never {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (
|
||||
error.code === 'P2002' &&
|
||||
Array.isArray(error.meta?.target) &&
|
||||
error.meta.target.includes('name')
|
||||
) {
|
||||
throw new ConflictException(
|
||||
'Nom déjà utilisé pour un type de machine.',
|
||||
);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ type MachineRecord = {
|
||||
id: string;
|
||||
name: string;
|
||||
reference: Nullable<string>;
|
||||
constructeurId: Nullable<string>;
|
||||
constructeurIds: string[];
|
||||
prix: Nullable<string>;
|
||||
siteId: string;
|
||||
typeMachineId: Nullable<string>;
|
||||
@@ -90,7 +90,7 @@ type ComposantRecord = {
|
||||
parentComposantId: Nullable<string>;
|
||||
typeComposantId: Nullable<string>;
|
||||
typeMachineComponentRequirementId: Nullable<string>;
|
||||
constructeurId: Nullable<string>;
|
||||
constructeurIds: string[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
@@ -104,7 +104,16 @@ type PieceRecord = {
|
||||
composantId: Nullable<string>;
|
||||
typePieceId: Nullable<string>;
|
||||
typeMachinePieceRequirementId: Nullable<string>;
|
||||
constructeurId: Nullable<string>;
|
||||
constructeurIds: string[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
type ConstructeurRecord = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: Nullable<string>;
|
||||
phone: Nullable<string>;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
@@ -202,6 +211,15 @@ class InMemoryPrismaService {
|
||||
private customFields: CustomFieldRecord[] = [];
|
||||
private customFieldValues: CustomFieldValueRecord[] = [];
|
||||
private profiles: ProfileRecord[] = [];
|
||||
private constructeurs: ConstructeurRecord[] = [];
|
||||
private readonly constructeurLinkOrientation: Record<
|
||||
string,
|
||||
{ parentColumn: 'A' | 'B'; constructeurColumn: 'A' | 'B' }
|
||||
> = {
|
||||
_MachineConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
|
||||
_ComposantConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
|
||||
_PieceConstructeurs: { parentColumn: 'A', constructeurColumn: 'B' },
|
||||
};
|
||||
|
||||
async onModuleInit() {}
|
||||
async onModuleDestroy() {}
|
||||
@@ -229,6 +247,7 @@ class InMemoryPrismaService {
|
||||
this.customFields = [];
|
||||
this.customFieldValues = [];
|
||||
this.profiles = [];
|
||||
this.constructeurs = [];
|
||||
}
|
||||
|
||||
private readonly modelTypeDelegate = {
|
||||
@@ -638,11 +657,12 @@ class InMemoryPrismaService {
|
||||
machine = {
|
||||
create: async ({ data, include }: any) => {
|
||||
const now = new Date();
|
||||
const constructeurIds = this.extractConstructeurIds(data.constructeurs);
|
||||
const record: MachineRecord = {
|
||||
id: generateId('machine'),
|
||||
name: data.name,
|
||||
reference: data.reference ?? null,
|
||||
constructeurId: data.constructeurId ?? null,
|
||||
constructeurIds,
|
||||
prix: data.prix ?? null,
|
||||
siteId: data.siteId,
|
||||
typeMachineId: data.typeMachineId ?? null,
|
||||
@@ -683,6 +703,7 @@ class InMemoryPrismaService {
|
||||
composant = {
|
||||
create: async ({ data }: any) => {
|
||||
const now = new Date();
|
||||
const constructeurIds = this.extractConstructeurIds(data.constructeurs);
|
||||
const record: ComposantRecord = {
|
||||
id: generateId('component'),
|
||||
name: data.name,
|
||||
@@ -693,7 +714,7 @@ class InMemoryPrismaService {
|
||||
typeComposantId: data.typeComposantId ?? null,
|
||||
typeMachineComponentRequirementId:
|
||||
data.typeMachineComponentRequirementId ?? null,
|
||||
constructeurId: data.constructeurId ?? null,
|
||||
constructeurIds,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
@@ -719,6 +740,7 @@ class InMemoryPrismaService {
|
||||
piece = {
|
||||
create: async ({ data }: any) => {
|
||||
const now = new Date();
|
||||
const constructeurIds = this.extractConstructeurIds(data.constructeurs);
|
||||
const record: PieceRecord = {
|
||||
id: generateId('piece'),
|
||||
name: data.name,
|
||||
@@ -729,7 +751,7 @@ class InMemoryPrismaService {
|
||||
typePieceId: data.typePieceId ?? null,
|
||||
typeMachinePieceRequirementId:
|
||||
data.typeMachinePieceRequirementId ?? null,
|
||||
constructeurId: data.constructeurId ?? null,
|
||||
constructeurIds,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
@@ -767,7 +789,7 @@ class InMemoryPrismaService {
|
||||
prixOverride:
|
||||
data.prixOverride !== undefined && data.prixOverride !== null
|
||||
? String(data.prixOverride)
|
||||
: data.prixOverride ?? null,
|
||||
: (data.prixOverride ?? null),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
@@ -819,7 +841,7 @@ class InMemoryPrismaService {
|
||||
prixOverride:
|
||||
data.prixOverride !== undefined && data.prixOverride !== null
|
||||
? String(data.prixOverride)
|
||||
: data.prixOverride ?? null,
|
||||
: (data.prixOverride ?? null),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
@@ -848,7 +870,9 @@ class InMemoryPrismaService {
|
||||
(link) => link.parentLinkId === where.parentLinkId,
|
||||
);
|
||||
}
|
||||
return links.map((link) => this.buildMachinePieceLink(link, include ?? {}));
|
||||
return links.map((link) =>
|
||||
this.buildMachinePieceLink(link, include ?? {}),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -979,6 +1003,120 @@ class InMemoryPrismaService {
|
||||
},
|
||||
};
|
||||
|
||||
constructeur = {
|
||||
findMany: async ({ where, select, orderBy }: any = {}) => {
|
||||
let records = [...this.constructeurs];
|
||||
|
||||
if (where?.id?.in) {
|
||||
const ids = Array.isArray(where.id.in) ? where.id.in : [];
|
||||
records = records.filter((item) => ids.includes(item.id));
|
||||
}
|
||||
|
||||
if (where?.OR && Array.isArray(where.OR)) {
|
||||
const termCandidate = where.OR.find(
|
||||
(clause: any) => clause?.name?.contains,
|
||||
);
|
||||
const term =
|
||||
termCandidate?.name?.contains ??
|
||||
termCandidate?.email?.contains ??
|
||||
termCandidate?.phone?.contains ??
|
||||
'';
|
||||
const normalized = term.toLowerCase().trim();
|
||||
if (normalized) {
|
||||
records = records.filter((record) => {
|
||||
const nameMatch = record.name.toLowerCase().includes(normalized);
|
||||
const emailMatch =
|
||||
record.email?.toLowerCase().includes(normalized) ?? false;
|
||||
const phoneMatch =
|
||||
record.phone?.toLowerCase().includes(normalized) ?? false;
|
||||
return nameMatch || emailMatch || phoneMatch;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (orderBy?.name) {
|
||||
records = [...records].sort((a, b) =>
|
||||
orderBy.name === 'desc'
|
||||
? b.name.localeCompare(a.name)
|
||||
: a.name.localeCompare(b.name),
|
||||
);
|
||||
}
|
||||
|
||||
return records.map((record) => this.applySelect(record, select));
|
||||
},
|
||||
|
||||
findUnique: async ({ where, select }: any) => {
|
||||
const record = this.constructeurs.find((item) => item.id === where?.id);
|
||||
return record ? this.applySelect(record, select) : null;
|
||||
},
|
||||
|
||||
findFirst: async ({ where, select }: any = {}) => {
|
||||
if (where?.name?.equals) {
|
||||
const target = where.name.equals.toLowerCase();
|
||||
const record = this.constructeurs.find(
|
||||
(item) => item.name.toLowerCase() === target,
|
||||
);
|
||||
return record ? this.applySelect(record, select) : null;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
create: async ({ data, select }: any) => {
|
||||
const now = new Date();
|
||||
const record: ConstructeurRecord = {
|
||||
id: data.id ?? generateId('constructeur'),
|
||||
name: data.name?.trim?.() ?? '',
|
||||
email: data.email?.trim?.() ?? null,
|
||||
phone: data.phone?.trim?.() ?? null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
this.constructeurs.push(record);
|
||||
return this.applySelect(record, select);
|
||||
},
|
||||
|
||||
update: async ({ where, data, select }: any) => {
|
||||
const record = this.constructeurs.find((item) => item.id === where?.id);
|
||||
if (!record) {
|
||||
throw new Error('Constructeur not found');
|
||||
}
|
||||
if (data.name !== undefined) {
|
||||
record.name =
|
||||
this.applyUpdateValue<string>(data.name)?.trim?.() ?? record.name;
|
||||
}
|
||||
if (data.email !== undefined) {
|
||||
record.email =
|
||||
this.applyUpdateValue<string | null>(data.email)?.trim?.() ?? null;
|
||||
}
|
||||
if (data.phone !== undefined) {
|
||||
record.phone =
|
||||
this.applyUpdateValue<string | null>(data.phone)?.trim?.() ?? null;
|
||||
}
|
||||
record.updatedAt = new Date();
|
||||
return this.applySelect(record, select);
|
||||
},
|
||||
|
||||
delete: async ({ where, select }: any) => {
|
||||
const index = this.constructeurs.findIndex(
|
||||
(item) => item.id === where?.id,
|
||||
);
|
||||
if (index === -1) {
|
||||
throw new Error('Constructeur not found');
|
||||
}
|
||||
const [deleted] = this.constructeurs.splice(index, 1);
|
||||
return this.applySelect(deleted, select);
|
||||
},
|
||||
};
|
||||
|
||||
async __getConstructeurLinkOrientation(table: string) {
|
||||
return (
|
||||
this.constructeurLinkOrientation[table] ?? {
|
||||
parentColumn: 'A',
|
||||
constructeurColumn: 'B',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
profile = {
|
||||
count: async ({ where }: any) => {
|
||||
return this.profiles.filter((profile) => {
|
||||
@@ -1279,6 +1417,71 @@ class InMemoryPrismaService {
|
||||
return base;
|
||||
}
|
||||
|
||||
private extractConstructeurIds(input: any): string[] {
|
||||
if (!input) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const source = Array.isArray(input.set)
|
||||
? input.set
|
||||
: Array.isArray(input.connect)
|
||||
? input.connect
|
||||
: [];
|
||||
|
||||
return source
|
||||
.map((entry: any) => (typeof entry?.id === 'string' ? entry.id : null))
|
||||
.filter((id: string | null): id is string => Boolean(id));
|
||||
}
|
||||
|
||||
private mapConstructeurs(ids: string[] = []) {
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return ids
|
||||
.map((id) =>
|
||||
this.constructeurs.find((constructeur) => constructeur.id === id),
|
||||
)
|
||||
.filter((item): item is (typeof this.constructeurs)[number] =>
|
||||
Boolean(item),
|
||||
)
|
||||
.map((item) => ({ ...item }));
|
||||
}
|
||||
|
||||
async __syncConstructeurLinks(
|
||||
table: string,
|
||||
parentId: string,
|
||||
ids: string[],
|
||||
): Promise<void> {
|
||||
const filtered = ids.filter((id) =>
|
||||
this.constructeurs.some((item) => item.id === id),
|
||||
);
|
||||
|
||||
switch (table) {
|
||||
case '_MachineConstructeurs':
|
||||
this.assignConstructeurs(this.machines, parentId, filtered);
|
||||
break;
|
||||
case '_ComposantConstructeurs':
|
||||
this.assignConstructeurs(this.composants, parentId, filtered);
|
||||
break;
|
||||
case '_PieceConstructeurs':
|
||||
this.assignConstructeurs(this.pieces, parentId, filtered);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported constructeur link table: ${table}`);
|
||||
}
|
||||
}
|
||||
|
||||
private assignConstructeurs<
|
||||
T extends { id: string; constructeurIds: string[] },
|
||||
>(collection: T[], parentId: string, ids: string[]) {
|
||||
const record = collection.find((item) => item.id === parentId);
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
record.constructeurIds = [...ids];
|
||||
}
|
||||
|
||||
private buildMachine(machine: MachineRecord, include: any) {
|
||||
const base: any = { ...machine };
|
||||
|
||||
@@ -1309,8 +1512,8 @@ class InMemoryPrismaService {
|
||||
}
|
||||
}
|
||||
|
||||
if (include?.constructeur) {
|
||||
base.constructeur = null;
|
||||
if (include?.constructeurs) {
|
||||
base.constructeurs = this.mapConstructeurs(machine.constructeurIds);
|
||||
}
|
||||
|
||||
if (include?.componentLinks) {
|
||||
@@ -1389,19 +1592,19 @@ class InMemoryPrismaService {
|
||||
|
||||
if (include?.typeMachineComponentRequirement) {
|
||||
const requirement = link.typeMachineComponentRequirementId
|
||||
? this.typeMachineComponentRequirements.find(
|
||||
? (this.typeMachineComponentRequirements.find(
|
||||
(item) => item.id === link.typeMachineComponentRequirementId,
|
||||
) ?? null
|
||||
) ?? null)
|
||||
: null;
|
||||
base.typeMachineComponentRequirement = requirement
|
||||
? {
|
||||
...requirement,
|
||||
typeComposant:
|
||||
include.typeMachineComponentRequirement.include?.typeComposant
|
||||
? this.typeComposants.find(
|
||||
(item) => item.id === requirement.typeComposantId,
|
||||
) ?? null
|
||||
: undefined,
|
||||
typeComposant: include.typeMachineComponentRequirement.include
|
||||
?.typeComposant
|
||||
? (this.typeComposants.find(
|
||||
(item) => item.id === requirement.typeComposantId,
|
||||
) ?? null)
|
||||
: undefined,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -1419,14 +1622,12 @@ class InMemoryPrismaService {
|
||||
return base;
|
||||
}
|
||||
|
||||
private buildMachinePieceLink(
|
||||
link: MachinePieceLinkRecord,
|
||||
include: any,
|
||||
) {
|
||||
private buildMachinePieceLink(link: MachinePieceLinkRecord, include: any) {
|
||||
const base: any = { ...link };
|
||||
|
||||
if (include?.piece) {
|
||||
const piece = this.pieces.find((item) => item.id === link.pieceId) ?? null;
|
||||
const piece =
|
||||
this.pieces.find((item) => item.id === link.pieceId) ?? null;
|
||||
base.piece = piece
|
||||
? this.buildPiece(piece, include.piece.include ?? {})
|
||||
: null;
|
||||
@@ -1434,19 +1635,18 @@ class InMemoryPrismaService {
|
||||
|
||||
if (include?.typeMachinePieceRequirement) {
|
||||
const requirement = link.typeMachinePieceRequirementId
|
||||
? this.typeMachinePieceRequirements.find(
|
||||
? (this.typeMachinePieceRequirements.find(
|
||||
(item) => item.id === link.typeMachinePieceRequirementId,
|
||||
) ?? null
|
||||
) ?? null)
|
||||
: null;
|
||||
base.typeMachinePieceRequirement = requirement
|
||||
? {
|
||||
...requirement,
|
||||
typePiece:
|
||||
include.typeMachinePieceRequirement.include?.typePiece
|
||||
? this.typePieces.find(
|
||||
(item) => item.id === requirement.typePieceId,
|
||||
) ?? null
|
||||
: undefined,
|
||||
typePiece: include.typeMachinePieceRequirement.include?.typePiece
|
||||
? (this.typePieces.find(
|
||||
(item) => item.id === requirement.typePieceId,
|
||||
) ?? null)
|
||||
: undefined,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -1505,8 +1705,8 @@ class InMemoryPrismaService {
|
||||
);
|
||||
}
|
||||
|
||||
if (include?.constructeur) {
|
||||
base.constructeur = null;
|
||||
if (include?.constructeurs) {
|
||||
base.constructeurs = this.mapConstructeurs(component.constructeurIds);
|
||||
}
|
||||
|
||||
if (include?.pieces) {
|
||||
@@ -1536,8 +1736,8 @@ class InMemoryPrismaService {
|
||||
);
|
||||
}
|
||||
|
||||
if (include?.constructeur) {
|
||||
base.constructeur = null;
|
||||
if (include?.constructeurs) {
|
||||
base.constructeurs = this.mapConstructeurs(piece.constructeurIds);
|
||||
}
|
||||
|
||||
if (include?.typeMachinePieceRequirement) {
|
||||
|
||||
Reference in New Issue
Block a user