Compare commits
23 Commits
dc4a12440b
...
test-ci
| Author | SHA1 | Date | |
|---|---|---|---|
| 88a9b70d53 | |||
| 550882b803 | |||
| be2f359ab6 | |||
| e8fa5f8bc6 | |||
| 68e2823d64 | |||
| e977bd83bd | |||
| 7fe6d0db4b | |||
| ccd3e38346 | |||
| 1cbf066658 | |||
| b732944f7a | |||
| 9044560833 | |||
|
|
e2c7165c8c | ||
|
|
4bfa21d4b3 | ||
|
|
6cf2b566ce | ||
|
|
e81f71e3e7 | ||
|
|
d05b91d7cd | ||
|
|
fe471b9e81 | ||
|
|
9f522a6dbb | ||
|
|
635ea0e84e | ||
|
|
4db64351b7 | ||
|
|
b9c9b2c421 | ||
|
|
16a703a4c3 | ||
|
|
582a6fd7e1 |
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -5,20 +5,30 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- develop
|
- develop
|
||||||
|
- test-ci
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout
|
||||||
- uses: actions/setup-node@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: npm test -- --runInBand
|
run: npm test -- --runInBand
|
||||||
|
|
||||||
- name: Run e2e tests
|
- name: Run e2e tests
|
||||||
run: npm run test:e2e -- --runInBand
|
run: npm run test:e2e -- --runInBand
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Configuration de la base de données
|
# Configuration de la base de données
|
||||||
DATABASE_URL="postgresql://postgres:password@localhost:5432/inventory_db"
|
DATABASE_URL="postgresql://postgres:password@localhost:5432/inventory_db"
|
||||||
|
|
||||||
# Configuration du serveur
|
# Configuration du serveurte
|
||||||
PORT=3000
|
PORT=3000
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
customFields CustomField[] @relation("TypeMachineCustomFields")
|
||||||
componentRequirements TypeMachineComponentRequirement[]
|
componentRequirements TypeMachineComponentRequirement[]
|
||||||
pieceRequirements TypeMachinePieceRequirement[]
|
pieceRequirements TypeMachinePieceRequirement[]
|
||||||
|
productRequirements TypeMachineProductRequirement[]
|
||||||
|
|
||||||
@@map("type_machines")
|
@@map("type_machines")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Machine {
|
model Machine {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String @unique
|
||||||
reference String?
|
reference String?
|
||||||
prix Decimal? @db.Decimal(10, 2)
|
prix Decimal? @db.Decimal(10, 2)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@@ -66,11 +67,11 @@ model Machine {
|
|||||||
typeMachineId String?
|
typeMachineId String?
|
||||||
typeMachine TypeMachine? @relation(fields: [typeMachineId], references: [id])
|
typeMachine TypeMachine? @relation(fields: [typeMachineId], references: [id])
|
||||||
|
|
||||||
constructeurId String?
|
constructeurs Constructeur[] @relation("MachineConstructeurs")
|
||||||
constructeur Constructeur? @relation(fields: [constructeurId], references: [id], onDelete: SetNull)
|
|
||||||
|
|
||||||
componentLinks MachineComponentLink[]
|
componentLinks MachineComponentLink[]
|
||||||
pieceLinks MachinePieceLink[]
|
pieceLinks MachinePieceLink[]
|
||||||
|
productLinks MachineProductLink[]
|
||||||
documents Document[] @relation("MachineDocuments")
|
documents Document[] @relation("MachineDocuments")
|
||||||
customFieldValues CustomFieldValue[] @relation("MachineCustomFieldValues")
|
customFieldValues CustomFieldValue[] @relation("MachineCustomFieldValues")
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ model Machine {
|
|||||||
|
|
||||||
model Composant {
|
model Composant {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String @unique
|
||||||
reference String?
|
reference String?
|
||||||
prix Decimal? @db.Decimal(10, 2)
|
prix Decimal? @db.Decimal(10, 2)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@@ -89,8 +90,10 @@ model Composant {
|
|||||||
typeComposantId String?
|
typeComposantId String?
|
||||||
typeComposant ModelType? @relation("ModelTypeComponentAssignments", fields: [typeComposantId], references: [id])
|
typeComposant ModelType? @relation("ModelTypeComponentAssignments", fields: [typeComposantId], references: [id])
|
||||||
|
|
||||||
constructeurId String?
|
productId String?
|
||||||
constructeur Constructeur? @relation(fields: [constructeurId], references: [id], onDelete: SetNull)
|
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
constructeurs Constructeur[] @relation("ComposantConstructeurs")
|
||||||
|
|
||||||
documents Document[] @relation("ComposantDocuments")
|
documents Document[] @relation("ComposantDocuments")
|
||||||
customFieldValues CustomFieldValue[] @relation("ComposantCustomFieldValues")
|
customFieldValues CustomFieldValue[] @relation("ComposantCustomFieldValues")
|
||||||
@@ -101,7 +104,7 @@ model Composant {
|
|||||||
|
|
||||||
model Piece {
|
model Piece {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String @unique
|
||||||
reference String?
|
reference String?
|
||||||
prix Decimal? @db.Decimal(10, 2)
|
prix Decimal? @db.Decimal(10, 2)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@@ -110,8 +113,10 @@ model Piece {
|
|||||||
typePieceId String?
|
typePieceId String?
|
||||||
typePiece ModelType? @relation("ModelTypePieceAssignments", fields: [typePieceId], references: [id])
|
typePiece ModelType? @relation("ModelTypePieceAssignments", fields: [typePieceId], references: [id])
|
||||||
|
|
||||||
constructeurId String?
|
productId String?
|
||||||
constructeur Constructeur? @relation(fields: [constructeurId], references: [id], onDelete: SetNull)
|
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
constructeurs Constructeur[] @relation("PieceConstructeurs")
|
||||||
|
|
||||||
documents Document[] @relation("PieceDocuments")
|
documents Document[] @relation("PieceDocuments")
|
||||||
customFieldValues CustomFieldValue[] @relation("PieceCustomFieldValues")
|
customFieldValues CustomFieldValue[] @relation("PieceCustomFieldValues")
|
||||||
@@ -120,6 +125,27 @@ model Piece {
|
|||||||
@@map("pieces")
|
@@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 {
|
model MachineComponentLink {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
machineId String
|
machineId String
|
||||||
@@ -138,6 +164,7 @@ model MachineComponentLink {
|
|||||||
childLinks MachineComponentLink[] @relation("MachineComponentLinkHierarchy")
|
childLinks MachineComponentLink[] @relation("MachineComponentLinkHierarchy")
|
||||||
typeMachineComponentRequirement TypeMachineComponentRequirement? @relation("ComponentRequirementLinks", fields: [typeMachineComponentRequirementId], references: [id], onDelete: SetNull)
|
typeMachineComponentRequirement TypeMachineComponentRequirement? @relation("ComponentRequirementLinks", fields: [typeMachineComponentRequirementId], references: [id], onDelete: SetNull)
|
||||||
pieceLinks MachinePieceLink[] @relation("ComponentLinkPieceLinks")
|
pieceLinks MachinePieceLink[] @relation("ComponentLinkPieceLinks")
|
||||||
|
productLinks MachineProductLink[] @relation("ComponentLinkProductLinks")
|
||||||
|
|
||||||
@@map("machine_component_links")
|
@@map("machine_component_links")
|
||||||
}
|
}
|
||||||
@@ -158,13 +185,37 @@ model MachinePieceLink {
|
|||||||
piece Piece @relation(fields: [pieceId], references: [id], onDelete: Cascade)
|
piece Piece @relation(fields: [pieceId], references: [id], onDelete: Cascade)
|
||||||
parentLink MachineComponentLink? @relation("ComponentLinkPieceLinks", fields: [parentLinkId], references: [id], onDelete: Cascade)
|
parentLink MachineComponentLink? @relation("ComponentLinkPieceLinks", fields: [parentLinkId], references: [id], onDelete: Cascade)
|
||||||
typeMachinePieceRequirement TypeMachinePieceRequirement? @relation("PieceRequirementLinks", fields: [typeMachinePieceRequirementId], references: [id], onDelete: SetNull)
|
typeMachinePieceRequirement TypeMachinePieceRequirement? @relation("PieceRequirementLinks", fields: [typeMachinePieceRequirementId], references: [id], onDelete: SetNull)
|
||||||
|
productLinks MachineProductLink[] @relation("PieceLinkProductLinks")
|
||||||
|
|
||||||
@@map("machine_piece_links")
|
@@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 {
|
enum ModelCategory {
|
||||||
COMPONENT
|
COMPONENT
|
||||||
PIECE
|
PIECE
|
||||||
|
PRODUCT
|
||||||
}
|
}
|
||||||
|
|
||||||
model ModelType {
|
model ModelType {
|
||||||
@@ -176,17 +227,21 @@ model ModelType {
|
|||||||
description String? @db.Text
|
description String? @db.Text
|
||||||
componentSkeleton Json?
|
componentSkeleton Json?
|
||||||
pieceSkeleton Json?
|
pieceSkeleton Json?
|
||||||
|
productSkeleton Json?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
composants Composant[] @relation("ModelTypeComponentAssignments")
|
composants Composant[] @relation("ModelTypeComponentAssignments")
|
||||||
componentRequirements TypeMachineComponentRequirement[] @relation("ModelTypeComponentRequirements")
|
componentRequirements TypeMachineComponentRequirement[] @relation("ModelTypeComponentRequirements")
|
||||||
customFields CustomField[] @relation("ModelTypeCustomFields")
|
customFields CustomField[] @relation("ModelTypeCustomFields")
|
||||||
|
productCustomFields CustomField[] @relation("ModelTypeProductCustomFields")
|
||||||
pieceRequirements TypeMachinePieceRequirement[] @relation("ModelTypePieceRequirements")
|
pieceRequirements TypeMachinePieceRequirement[] @relation("ModelTypePieceRequirements")
|
||||||
pieces Piece[] @relation("ModelTypePieceAssignments")
|
pieces Piece[] @relation("ModelTypePieceAssignments")
|
||||||
pieceCustomFields CustomField[] @relation("ModelTypePieceCustomFields")
|
pieceCustomFields CustomField[] @relation("ModelTypePieceCustomFields")
|
||||||
|
products Product[] @relation("ModelTypeProductAssignments")
|
||||||
|
productRequirements TypeMachineProductRequirement[] @relation("ModelTypeProductRequirements")
|
||||||
|
|
||||||
@@index([category, name])
|
@@unique([category, name])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Constructeur {
|
model Constructeur {
|
||||||
@@ -197,9 +252,10 @@ model Constructeur {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
machines Machine[]
|
machines Machine[] @relation("MachineConstructeurs")
|
||||||
composants Composant[]
|
composants Composant[] @relation("ComposantConstructeurs")
|
||||||
pieces Piece[]
|
pieces Piece[] @relation("PieceConstructeurs")
|
||||||
|
products Product[] @relation("ProductConstructeurs")
|
||||||
|
|
||||||
@@map("constructeurs")
|
@@map("constructeurs")
|
||||||
}
|
}
|
||||||
@@ -235,6 +291,9 @@ model Document {
|
|||||||
pieceId String?
|
pieceId String?
|
||||||
piece Piece? @relation("PieceDocuments", fields: [pieceId], references: [id], onDelete: Cascade)
|
piece Piece? @relation("PieceDocuments", fields: [pieceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
productId String?
|
||||||
|
product Product? @relation("ProductDocuments", fields: [productId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
siteId String?
|
siteId String?
|
||||||
site Site? @relation("SiteDocuments", fields: [siteId], references: [id], onDelete: Cascade)
|
site Site? @relation("SiteDocuments", fields: [siteId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@ -248,6 +307,7 @@ model CustomField {
|
|||||||
required Boolean @default(false)
|
required Boolean @default(false)
|
||||||
defaultValue String?
|
defaultValue String?
|
||||||
options String[] // Pour les champs de type SELECT
|
options String[] // Pour les champs de type SELECT
|
||||||
|
orderIndex Int @default(0)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@ -261,6 +321,9 @@ model CustomField {
|
|||||||
typePieceId String?
|
typePieceId String?
|
||||||
typePiece ModelType? @relation("ModelTypePieceCustomFields", fields: [typePieceId], references: [id], onDelete: Cascade)
|
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
|
// Relations avec les valeurs
|
||||||
customFieldValues CustomFieldValue[]
|
customFieldValues CustomFieldValue[]
|
||||||
|
|
||||||
@@ -286,6 +349,9 @@ model CustomFieldValue {
|
|||||||
pieceId String?
|
pieceId String?
|
||||||
piece Piece? @relation("PieceCustomFieldValues", fields: [pieceId], references: [id], onDelete: Cascade)
|
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")
|
@@map("custom_field_values")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,6 +362,7 @@ model TypeMachineComponentRequirement {
|
|||||||
maxCount Int?
|
maxCount Int?
|
||||||
required Boolean @default(true)
|
required Boolean @default(true)
|
||||||
allowNewModels Boolean @default(true)
|
allowNewModels Boolean @default(true)
|
||||||
|
orderIndex Int @default(0)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@ -317,6 +384,7 @@ model TypeMachinePieceRequirement {
|
|||||||
maxCount Int?
|
maxCount Int?
|
||||||
required Boolean @default(false)
|
required Boolean @default(false)
|
||||||
allowNewModels Boolean @default(true)
|
allowNewModels Boolean @default(true)
|
||||||
|
orderIndex Int @default(0)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@ -330,3 +398,24 @@ model TypeMachinePieceRequirement {
|
|||||||
|
|
||||||
@@map("type_machine_piece_requirements")
|
@@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.machinePieceLink.deleteMany();
|
||||||
await prisma.machine.deleteMany();
|
await prisma.machine.deleteMany();
|
||||||
await prisma.customFieldValue.deleteMany();
|
await prisma.customFieldValue.deleteMany();
|
||||||
|
await prisma.product.deleteMany();
|
||||||
await prisma.composant.deleteMany();
|
await prisma.composant.deleteMany();
|
||||||
await prisma.piece.deleteMany();
|
await prisma.piece.deleteMany();
|
||||||
|
await prisma.typeMachineProductRequirement.deleteMany();
|
||||||
|
|
||||||
await prisma.modelType.deleteMany({
|
await prisma.modelType.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -22,6 +24,7 @@ async function deleteExistingData() {
|
|||||||
'cooling-module',
|
'cooling-module',
|
||||||
'structural-frame',
|
'structural-frame',
|
||||||
'hydraulic-power-unit',
|
'hydraulic-power-unit',
|
||||||
|
'hydraulic-product',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -120,7 +123,7 @@ async function createPiece(options: {
|
|||||||
name: string;
|
name: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
price: number;
|
price: number;
|
||||||
constructeurId?: string | null;
|
constructeurIds?: string[] | null;
|
||||||
typeId: string;
|
typeId: string;
|
||||||
fieldValues: Record<string, string>;
|
fieldValues: Record<string, string>;
|
||||||
}) {
|
}) {
|
||||||
@@ -143,17 +146,35 @@ async function createPiece(options: {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return prisma.piece.create({
|
const constructeurIds = Array.isArray(options.constructeurIds)
|
||||||
data: {
|
? Array.from(
|
||||||
name: options.name,
|
new Set(
|
||||||
reference: options.reference,
|
options.constructeurIds
|
||||||
prix: new Prisma.Decimal(options.price),
|
.filter((value): value is string => typeof value === 'string')
|
||||||
typePieceId: options.typeId,
|
.map((value) => value.trim())
|
||||||
constructeurId: options.constructeurId ?? null,
|
.filter((value) => value.length > 0),
|
||||||
customFieldValues: {
|
),
|
||||||
create: customFieldValues,
|
)
|
||||||
},
|
: [];
|
||||||
|
|
||||||
|
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;
|
name: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
price: number;
|
price: number;
|
||||||
constructeurId?: string | null;
|
constructeurIds?: string[] | null;
|
||||||
typeId: string;
|
typeId: string;
|
||||||
fieldValues: Record<string, string>;
|
fieldValues: Record<string, string>;
|
||||||
structure?: Prisma.InputJsonValue;
|
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({
|
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: {
|
data: {
|
||||||
name: options.name,
|
name,
|
||||||
reference: options.reference,
|
code,
|
||||||
prix: new Prisma.Decimal(options.price),
|
category: 'PRODUCT',
|
||||||
typeComposantId: options.typeId,
|
description,
|
||||||
constructeurId: options.constructeurId ?? null,
|
productSkeleton: skeleton
|
||||||
structure:
|
? (skeleton as Prisma.InputJsonValue)
|
||||||
options.structure === undefined
|
: Prisma.JsonNull,
|
||||||
? Prisma.JsonNull
|
productCustomFields: {
|
||||||
: options.structure ?? Prisma.JsonNull,
|
create: fields.map((field, index) => ({
|
||||||
customFieldValues: {
|
name: field.name,
|
||||||
create: customFieldValues,
|
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() {
|
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…');
|
console.log('Création des types de composants…');
|
||||||
const coolingComponentFields: {
|
const coolingComponentFields: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -473,6 +698,15 @@ async function main() {
|
|||||||
} as Prisma.InputJsonValue,
|
} as Prisma.InputJsonValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await prisma.composant.update({
|
||||||
|
where: { id: coolingModule.id },
|
||||||
|
data: {
|
||||||
|
product: {
|
||||||
|
connect: { id: coolingProduct.id },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const structuralFrame = await createComponent({
|
const structuralFrame = await createComponent({
|
||||||
name: 'Châssis structurel XC-800',
|
name: 'Châssis structurel XC-800',
|
||||||
reference: 'FRAME-XC800',
|
reference: 'FRAME-XC800',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { ConstructeursModule } from './constructeurs/constructeurs.module';
|
|||||||
import { ProfilesModule } from './profiles/profiles.module';
|
import { ProfilesModule } from './profiles/profiles.module';
|
||||||
import { SessionModule } from './session/session.module';
|
import { SessionModule } from './session/session.module';
|
||||||
import { ModelTypeModule } from './model-type/model-type.module';
|
import { ModelTypeModule } from './model-type/model-type.module';
|
||||||
|
import { ProductsModule } from './products/products.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -32,6 +33,7 @@ import { ModelTypeModule } from './model-type/model-type.module';
|
|||||||
ProfilesModule,
|
ProfilesModule,
|
||||||
SessionModule,
|
SessionModule,
|
||||||
ModelTypeModule,
|
ModelTypeModule,
|
||||||
|
ProductsModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
|
|||||||
@@ -6,20 +6,34 @@ const CUSTOM_FIELD_SELECT = {
|
|||||||
type: true,
|
type: true,
|
||||||
required: true,
|
required: true,
|
||||||
options: true,
|
options: true,
|
||||||
|
orderIndex: true,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const COMPONENT_WITH_RELATIONS_INCLUDE = {
|
export const COMPONENT_WITH_RELATIONS_INCLUDE = {
|
||||||
typeComposant: {
|
typeComposant: {
|
||||||
include: {
|
include: {
|
||||||
customFields: true,
|
customFields: {
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
constructeur: true,
|
constructeurs: true,
|
||||||
customFieldValues: {
|
customFieldValues: {
|
||||||
include: {
|
include: {
|
||||||
customField: { select: CUSTOM_FIELD_SELECT },
|
customField: { select: CUSTOM_FIELD_SELECT },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
product: {
|
||||||
|
include: {
|
||||||
|
constructeurs: true,
|
||||||
|
customFieldValues: {
|
||||||
|
include: {
|
||||||
|
customField: { select: CUSTOM_FIELD_SELECT },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
documents: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
machineLinks: {
|
machineLinks: {
|
||||||
include: {
|
include: {
|
||||||
machine: true,
|
machine: true,
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export const CUSTOM_FIELD_SELECT = {
|
|||||||
type: true,
|
type: true,
|
||||||
required: true,
|
required: true,
|
||||||
options: true,
|
options: true,
|
||||||
|
orderIndex: true,
|
||||||
} as const;
|
} 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',
|
description: 'Desc',
|
||||||
notes: '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({
|
expect((input as any).componentSkeleton).toEqual({
|
||||||
pieces: [
|
pieces: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,12 +12,18 @@ import type {
|
|||||||
import { CUSTOM_FIELD_SELECT } from '../constants/custom-field.constant';
|
import { CUSTOM_FIELD_SELECT } from '../constants/custom-field.constant';
|
||||||
|
|
||||||
export const COMPONENT_TYPE_INCLUDE: Prisma.ModelTypeInclude = {
|
export const COMPONENT_TYPE_INCLUDE: Prisma.ModelTypeInclude = {
|
||||||
customFields: { select: CUSTOM_FIELD_SELECT },
|
customFields: {
|
||||||
|
select: CUSTOM_FIELD_SELECT,
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
composants: true,
|
composants: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PIECE_TYPE_INCLUDE: Prisma.ModelTypeInclude = {
|
export const PIECE_TYPE_INCLUDE: Prisma.ModelTypeInclude = {
|
||||||
pieceCustomFields: { select: CUSTOM_FIELD_SELECT },
|
pieceCustomFields: {
|
||||||
|
select: CUSTOM_FIELD_SELECT,
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
pieceRequirements: true,
|
pieceRequirements: true,
|
||||||
pieces: true,
|
pieces: true,
|
||||||
};
|
};
|
||||||
@@ -42,11 +48,12 @@ export class ModelTypeMapper {
|
|||||||
notes: description ?? null,
|
notes: description ?? null,
|
||||||
customFields: customFields
|
customFields: customFields
|
||||||
? {
|
? {
|
||||||
create: customFields.map((field) => ({
|
create: customFields.map((field, index) => ({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
required: field.required ?? false,
|
required: field.required ?? false,
|
||||||
options: field.options,
|
options: field.options,
|
||||||
|
orderIndex: field.orderIndex ?? index,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -97,11 +104,12 @@ export class ModelTypeMapper {
|
|||||||
notes: description ?? null,
|
notes: description ?? null,
|
||||||
pieceCustomFields: customFields
|
pieceCustomFields: customFields
|
||||||
? {
|
? {
|
||||||
create: customFields.map((field) => ({
|
create: customFields.map((field, index) => ({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
required: field.required ?? false,
|
required: field.required ?? false,
|
||||||
options: field.options,
|
options: field.options,
|
||||||
|
orderIndex: field.orderIndex ?? index,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -165,11 +173,12 @@ export class ModelTypeMapper {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields.map((field) => ({
|
return fields.map((field, index) => ({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
required: field.required ?? false,
|
required: field.required ?? false,
|
||||||
options: field.options,
|
options: field.options,
|
||||||
|
orderIndex: field.orderIndex ?? index,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,11 +189,12 @@ export class ModelTypeMapper {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields.map((field) => ({
|
return fields.map((field, index) => ({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
required: field.required ?? false,
|
required: field.required ?? false,
|
||||||
options: field.options,
|
options: field.options,
|
||||||
|
orderIndex: field.orderIndex ?? index,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,16 @@ const baseDto = {
|
|||||||
typePieceId: 'piece-id',
|
typePieceId: 'piece-id',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
productRequirements: [
|
||||||
|
{
|
||||||
|
label: 'Product',
|
||||||
|
minCount: 1,
|
||||||
|
maxCount: 3,
|
||||||
|
required: true,
|
||||||
|
allowNewModels: true,
|
||||||
|
typeProductId: 'product-id',
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('TypeMachineMapper', () => {
|
describe('TypeMachineMapper', () => {
|
||||||
@@ -33,12 +43,16 @@ describe('TypeMachineMapper', () => {
|
|||||||
const input = TypeMachineMapper.toCreateInput(baseDto as any);
|
const input = TypeMachineMapper.toCreateInput(baseDto as any);
|
||||||
|
|
||||||
expect(input.customFields?.create).toHaveLength(1);
|
expect(input.customFields?.create).toHaveLength(1);
|
||||||
|
expect(input.customFields?.create?.[0]).toMatchObject({
|
||||||
|
orderIndex: 0,
|
||||||
|
});
|
||||||
expect(input.componentRequirements?.create?.[0]).toMatchObject({
|
expect(input.componentRequirements?.create?.[0]).toMatchObject({
|
||||||
label: 'Comp',
|
label: 'Comp',
|
||||||
minCount: 2,
|
minCount: 2,
|
||||||
maxCount: 4,
|
maxCount: 4,
|
||||||
required: true,
|
required: true,
|
||||||
allowNewModels: false,
|
allowNewModels: false,
|
||||||
|
orderIndex: 0,
|
||||||
});
|
});
|
||||||
expect(input.pieceRequirements?.create?.[0]).toMatchObject({
|
expect(input.pieceRequirements?.create?.[0]).toMatchObject({
|
||||||
label: 'Piece',
|
label: 'Piece',
|
||||||
@@ -46,6 +60,15 @@ describe('TypeMachineMapper', () => {
|
|||||||
maxCount: 2,
|
maxCount: 2,
|
||||||
required: false,
|
required: false,
|
||||||
allowNewModels: true,
|
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',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
options: ['a'],
|
options: ['a'],
|
||||||
|
orderIndex: 0,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -70,16 +94,27 @@ describe('TypeMachineMapper', () => {
|
|||||||
const piece = TypeMachineMapper.mapPieceRequirementInputs(
|
const piece = TypeMachineMapper.mapPieceRequirementInputs(
|
||||||
baseDto.pieceRequirements as any,
|
baseDto.pieceRequirements as any,
|
||||||
);
|
);
|
||||||
|
const product = TypeMachineMapper.mapProductRequirementInputs(
|
||||||
|
baseDto.productRequirements as any,
|
||||||
|
);
|
||||||
|
|
||||||
expect(component[0]).toMatchObject({
|
expect(component[0]).toMatchObject({
|
||||||
typeComposantId: 'comp-id',
|
typeComposantId: 'comp-id',
|
||||||
minCount: 2,
|
minCount: 2,
|
||||||
maxCount: 4,
|
maxCount: 4,
|
||||||
|
orderIndex: 0,
|
||||||
});
|
});
|
||||||
expect(piece[0]).toMatchObject({
|
expect(piece[0]).toMatchObject({
|
||||||
typePieceId: 'piece-id',
|
typePieceId: 'piece-id',
|
||||||
minCount: 0,
|
minCount: 0,
|
||||||
maxCount: 2,
|
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;
|
allowNewModels?: boolean | null;
|
||||||
typeComposantId?: string;
|
typeComposantId?: string;
|
||||||
typePieceId?: string;
|
typePieceId?: string;
|
||||||
|
typeProductId?: string;
|
||||||
|
orderIndex?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TYPE_MACHINE_DEFAULT_INCLUDE: Prisma.TypeMachineInclude = {
|
export const TYPE_MACHINE_DEFAULT_INCLUDE: Prisma.TypeMachineInclude = {
|
||||||
customFields: { select: CUSTOM_FIELD_SELECT },
|
customFields: {
|
||||||
|
select: CUSTOM_FIELD_SELECT,
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
componentRequirements: {
|
componentRequirements: {
|
||||||
include: { typeComposant: true },
|
include: { typeComposant: true },
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
},
|
},
|
||||||
pieceRequirements: {
|
pieceRequirements: {
|
||||||
include: { typePiece: true },
|
include: { typePiece: true },
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
|
productRequirements: {
|
||||||
|
include: { typeProduct: true },
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,8 +45,13 @@ export class TypeMachineMapper {
|
|||||||
static toCreateInput(
|
static toCreateInput(
|
||||||
dto: CreateTypeMachineDto,
|
dto: CreateTypeMachineDto,
|
||||||
): Prisma.TypeMachineCreateInput {
|
): Prisma.TypeMachineCreateInput {
|
||||||
const { customFields, componentRequirements, pieceRequirements, ...data } =
|
const {
|
||||||
dto;
|
customFields,
|
||||||
|
componentRequirements,
|
||||||
|
pieceRequirements,
|
||||||
|
productRequirements,
|
||||||
|
...data
|
||||||
|
} = dto;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
@@ -44,14 +60,20 @@ export class TypeMachineMapper {
|
|||||||
componentRequirements,
|
componentRequirements,
|
||||||
),
|
),
|
||||||
pieceRequirements: this.mapPieceRequirements(pieceRequirements),
|
pieceRequirements: this.mapPieceRequirements(pieceRequirements),
|
||||||
|
productRequirements: this.mapProductRequirements(productRequirements),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static toUpdateData(
|
static toUpdateData(
|
||||||
dto: UpdateTypeMachineDto,
|
dto: UpdateTypeMachineDto,
|
||||||
): Prisma.TypeMachineUpdateInput {
|
): Prisma.TypeMachineUpdateInput {
|
||||||
const { customFields, componentRequirements, pieceRequirements, ...data } =
|
const {
|
||||||
dto;
|
customFields,
|
||||||
|
componentRequirements,
|
||||||
|
pieceRequirements,
|
||||||
|
productRequirements,
|
||||||
|
...data
|
||||||
|
} = dto;
|
||||||
|
|
||||||
const payload: Prisma.TypeMachineUpdateInput = { ...data };
|
const payload: Prisma.TypeMachineUpdateInput = { ...data };
|
||||||
|
|
||||||
@@ -67,6 +89,10 @@ export class TypeMachineMapper {
|
|||||||
payload.pieceRequirements = undefined;
|
payload.pieceRequirements = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (productRequirements !== undefined) {
|
||||||
|
payload.productRequirements = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,11 +104,12 @@ export class TypeMachineMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
create: fields.map((field) => ({
|
create: fields.map((field, index) => ({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
required: field.required ?? false,
|
required: field.required ?? false,
|
||||||
options: field.options,
|
options: field.options,
|
||||||
|
orderIndex: field.orderIndex ?? index,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -92,11 +119,12 @@ export class TypeMachineMapper {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields.map((field) => ({
|
return fields.map((field, index) => ({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
required: field.required ?? false,
|
required: field.required ?? false,
|
||||||
options: field.options,
|
options: field.options,
|
||||||
|
orderIndex: field.orderIndex ?? index,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,12 +138,13 @@ export class TypeMachineMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
create: requirements.map((requirement) => ({
|
create: requirements.map((requirement, index) => ({
|
||||||
label: requirement.label ?? null,
|
label: requirement.label ?? null,
|
||||||
minCount: requirement.minCount ?? 1,
|
minCount: requirement.minCount ?? 1,
|
||||||
maxCount: requirement.maxCount ?? null,
|
maxCount: requirement.maxCount ?? null,
|
||||||
required: requirement.required ?? true,
|
required: requirement.required ?? true,
|
||||||
allowNewModels: requirement.allowNewModels ?? true,
|
allowNewModels: requirement.allowNewModels ?? true,
|
||||||
|
orderIndex: requirement.orderIndex ?? index,
|
||||||
typeComposant: requirement.typeComposantId
|
typeComposant: requirement.typeComposantId
|
||||||
? {
|
? {
|
||||||
connect: { id: requirement.typeComposantId },
|
connect: { id: requirement.typeComposantId },
|
||||||
@@ -134,12 +163,13 @@ export class TypeMachineMapper {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return requirements.map((requirement) => ({
|
return requirements.map((requirement, index) => ({
|
||||||
label: requirement.label ?? null,
|
label: requirement.label ?? null,
|
||||||
minCount: requirement.minCount ?? 1,
|
minCount: requirement.minCount ?? 1,
|
||||||
maxCount: requirement.maxCount ?? null,
|
maxCount: requirement.maxCount ?? null,
|
||||||
required: requirement.required ?? true,
|
required: requirement.required ?? true,
|
||||||
allowNewModels: requirement.allowNewModels ?? true,
|
allowNewModels: requirement.allowNewModels ?? true,
|
||||||
|
orderIndex: requirement.orderIndex ?? index,
|
||||||
typeComposantId: requirement.typeComposantId!,
|
typeComposantId: requirement.typeComposantId!,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -154,12 +184,13 @@ export class TypeMachineMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
create: requirements.map((requirement) => ({
|
create: requirements.map((requirement, index) => ({
|
||||||
label: requirement.label ?? null,
|
label: requirement.label ?? null,
|
||||||
minCount: requirement.minCount ?? 0,
|
minCount: requirement.minCount ?? 0,
|
||||||
maxCount: requirement.maxCount ?? null,
|
maxCount: requirement.maxCount ?? null,
|
||||||
required: requirement.required ?? false,
|
required: requirement.required ?? false,
|
||||||
allowNewModels: requirement.allowNewModels ?? true,
|
allowNewModels: requirement.allowNewModels ?? true,
|
||||||
|
orderIndex: requirement.orderIndex ?? index,
|
||||||
typePiece: requirement.typePieceId
|
typePiece: requirement.typePieceId
|
||||||
? {
|
? {
|
||||||
connect: { id: requirement.typePieceId },
|
connect: { id: requirement.typePieceId },
|
||||||
@@ -178,13 +209,60 @@ export class TypeMachineMapper {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return requirements.map((requirement) => ({
|
return requirements.map((requirement, index) => ({
|
||||||
label: requirement.label ?? null,
|
label: requirement.label ?? null,
|
||||||
minCount: requirement.minCount ?? 0,
|
minCount: requirement.minCount ?? 0,
|
||||||
maxCount: requirement.maxCount ?? null,
|
maxCount: requirement.maxCount ?? null,
|
||||||
required: requirement.required ?? false,
|
required: requirement.required ?? false,
|
||||||
allowNewModels: requirement.allowNewModels ?? true,
|
allowNewModels: requirement.allowNewModels ?? true,
|
||||||
|
orderIndex: requirement.orderIndex ?? index,
|
||||||
typePieceId: requirement.typePieceId!,
|
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',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
options: [],
|
options: [],
|
||||||
|
orderIndex: 0,
|
||||||
typeComposantId: 'comp-id',
|
typeComposantId: 'comp-id',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -63,6 +64,7 @@ describe('ModelTypesRepository', () => {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
required: false,
|
required: false,
|
||||||
options: [],
|
options: [],
|
||||||
|
orderIndex: 0,
|
||||||
typePieceId: 'piece-id',
|
typePieceId: 'piece-id',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ describe('TypeMachinesRepository', () => {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
options: [],
|
options: [],
|
||||||
|
orderIndex: 0,
|
||||||
typeMachineId: 'machine-id',
|
typeMachineId: 'machine-id',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ type PieceRequirementInput = Omit<
|
|||||||
'id' | 'typeMachineId'
|
'id' | 'typeMachineId'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
type ProductRequirementInput = Omit<
|
||||||
|
Prisma.TypeMachineProductRequirementCreateManyInput,
|
||||||
|
'id' | 'typeMachineId'
|
||||||
|
>;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TypeMachinesRepository {
|
export class TypeMachinesRepository {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
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) {
|
async findMachinesUsingType(typeMachineId: string) {
|
||||||
return this.client.machine.findMany({
|
return this.client.machine.findMany({
|
||||||
where: { typeMachineId },
|
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(
|
const rawSubcomponents = toArray(
|
||||||
(structure as any)?.subcomponents ?? (structure as any)?.subComponents,
|
(structure as any)?.subcomponents ?? (structure as any)?.subComponents,
|
||||||
);
|
);
|
||||||
@@ -115,6 +168,7 @@ export function normalizeComponentModelStructure(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
pieces,
|
pieces,
|
||||||
|
products,
|
||||||
customFields,
|
customFields,
|
||||||
subcomponents,
|
subcomponents,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { ComposantsService } from './composants.service';
|
import { ComposantsService } from './composants.service';
|
||||||
import { PrismaService } from '../prisma/prisma.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', () => {
|
describe('ComposantsService', () => {
|
||||||
let service: ComposantsService;
|
let service: ComposantsService;
|
||||||
@@ -32,6 +35,7 @@ describe('ComposantsService', () => {
|
|||||||
const dto: CreateComposantDto = {
|
const dto: CreateComposantDto = {
|
||||||
name: 'Comp A',
|
name: 'Comp A',
|
||||||
typeComposantId: 'type-1',
|
typeComposantId: 'type-1',
|
||||||
|
productId: ' product-1 ',
|
||||||
};
|
};
|
||||||
|
|
||||||
prisma.composant.create.mockResolvedValue({ id: 'comp-1', name: dto.name });
|
prisma.composant.create.mockResolvedValue({ id: 'comp-1', name: dto.name });
|
||||||
@@ -39,16 +43,25 @@ describe('ComposantsService', () => {
|
|||||||
const result = await service.create(dto);
|
const result = await service.create(dto);
|
||||||
|
|
||||||
expect(prisma.composant.create).toHaveBeenCalled();
|
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' });
|
expect(result).toMatchObject({ id: 'comp-1' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates a component', async () => {
|
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);
|
await service.update('comp-1', dto);
|
||||||
|
|
||||||
expect(prisma.composant.update).toHaveBeenCalled();
|
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 { Prisma } from '@prisma/client';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import {
|
import {
|
||||||
@@ -9,14 +9,18 @@ import {
|
|||||||
COMPONENT_WITH_RELATIONS_INCLUDE,
|
COMPONENT_WITH_RELATIONS_INCLUDE,
|
||||||
ComposantWithRelations,
|
ComposantWithRelations,
|
||||||
} from '../common/constants/component-includes';
|
} from '../common/constants/component-includes';
|
||||||
|
import { syncConstructeurLinks } from '../common/utils/constructeur-link.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ComposantsService {
|
export class ComposantsService {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
private buildCreateInput(
|
private async buildCreateInput(
|
||||||
createComposantDto: CreateComposantDto,
|
createComposantDto: CreateComposantDto,
|
||||||
): Prisma.ComposantCreateInput {
|
): Promise<{
|
||||||
|
data: Prisma.ComposantCreateInput;
|
||||||
|
constructeurIds: string[];
|
||||||
|
}> {
|
||||||
const data: Prisma.ComposantCreateInput = {
|
const data: Prisma.ComposantCreateInput = {
|
||||||
name: createComposantDto.name,
|
name: createComposantDto.name,
|
||||||
reference: createComposantDto.reference ?? null,
|
reference: createComposantDto.reference ?? null,
|
||||||
@@ -24,11 +28,11 @@ export class ComposantsService {
|
|||||||
createComposantDto.prix !== undefined ? createComposantDto.prix : null,
|
createComposantDto.prix !== undefined ? createComposantDto.prix : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (createComposantDto.constructeurId) {
|
const constructeurIds = this.normalizeConstructeurIds(
|
||||||
data.constructeur = {
|
createComposantDto.constructeurIds,
|
||||||
connect: { id: createComposantDto.constructeurId },
|
);
|
||||||
};
|
const resolvedConstructeurIds =
|
||||||
}
|
await this.resolveExistingConstructeurIds(constructeurIds);
|
||||||
|
|
||||||
if (createComposantDto.typeComposantId) {
|
if (createComposantDto.typeComposantId) {
|
||||||
data.typeComposant = {
|
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) {
|
if (createComposantDto.structure !== undefined) {
|
||||||
data.structure = createComposantDto.structure as Prisma.InputJsonValue;
|
data.structure = createComposantDto.structure as Prisma.InputJsonValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return { data, constructeurIds: resolvedConstructeurIds };
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(createComposantDto: CreateComposantDto) {
|
async create(createComposantDto: CreateComposantDto) {
|
||||||
const created = await this.prisma.composant.create({
|
try {
|
||||||
data: this.buildCreateInput(createComposantDto),
|
const { data, constructeurIds } =
|
||||||
include: COMPONENT_WITH_RELATIONS_INCLUDE,
|
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() {
|
async findAll() {
|
||||||
@@ -81,10 +123,13 @@ export class ComposantsService {
|
|||||||
data.prix = updateComposantDto.prix;
|
data.prix = updateComposantDto.prix;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateComposantDto.constructeurId !== undefined) {
|
let resolvedConstructeurIds: string[] | undefined;
|
||||||
data.constructeur = updateComposantDto.constructeurId
|
if (updateComposantDto.constructeurIds !== undefined) {
|
||||||
? { connect: { id: updateComposantDto.constructeurId } }
|
const constructeurIds = this.normalizeConstructeurIds(
|
||||||
: { disconnect: true };
|
updateComposantDto.constructeurIds,
|
||||||
|
);
|
||||||
|
resolvedConstructeurIds =
|
||||||
|
await this.resolveExistingConstructeurIds(constructeurIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateComposantDto.typeComposantId !== undefined) {
|
if (updateComposantDto.typeComposantId !== undefined) {
|
||||||
@@ -93,20 +138,156 @@ export class ComposantsService {
|
|||||||
: { disconnect: true };
|
: { 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) {
|
if (updateComposantDto.structure !== undefined) {
|
||||||
data.structure = updateComposantDto.structure as Prisma.InputJsonValue;
|
data.structure = updateComposantDto.structure as Prisma.InputJsonValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (await this.prisma.composant.update({
|
let syncedConstructeurIds: string[] | undefined;
|
||||||
where: { id },
|
try {
|
||||||
data,
|
await this.prisma.$transaction(async (tx) => {
|
||||||
include: COMPONENT_WITH_RELATIONS_INCLUDE,
|
await tx.composant.update({
|
||||||
})) as ComposantWithRelations;
|
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) {
|
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({
|
return this.prisma.composant.delete({
|
||||||
where: { id },
|
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;
|
return 'composantId' as const;
|
||||||
case CustomFieldEntityType.PIECE:
|
case CustomFieldEntityType.PIECE:
|
||||||
return 'pieceId' as const;
|
return 'pieceId' as const;
|
||||||
|
case CustomFieldEntityType.PRODUCT:
|
||||||
|
return 'productId' as const;
|
||||||
default:
|
default:
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"Type d'entité de champ personnalisé invalide.",
|
"Type d'entité de champ personnalisé invalide.",
|
||||||
@@ -114,6 +116,28 @@ export class CustomFieldsService {
|
|||||||
valueKey: 'pieceId' as const,
|
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:
|
default:
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"Type d'entité de champ personnalisé invalide.",
|
"Type d'entité de champ personnalisé invalide.",
|
||||||
@@ -209,12 +233,20 @@ export class CustomFieldsService {
|
|||||||
if (existingField) {
|
if (existingField) {
|
||||||
targetCustomFieldId = existingField.id;
|
targetCustomFieldId = existingField.id;
|
||||||
} else {
|
} 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({
|
const createdField = await this.prisma.customField.create({
|
||||||
data: {
|
data: {
|
||||||
name: normalizedName,
|
name: normalizedName,
|
||||||
type: (customFieldType || 'text').trim() || 'text',
|
type: normalizedType,
|
||||||
required: !!customFieldRequired,
|
required: normalizedRequired,
|
||||||
options: normalizedOptions,
|
options: normalizedOptions,
|
||||||
|
orderIndex: nextOrderIndex,
|
||||||
[customFieldTypeField]: typeId,
|
[customFieldTypeField]: typeId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ export class DocumentsController {
|
|||||||
return this.documentsService.findByPiece(pieceId);
|
return this.documentsService.findByPiece(pieceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('product/:productId')
|
||||||
|
findByProduct(@Param('productId') productId: string) {
|
||||||
|
return this.documentsService.findByProduct(productId);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('site/:siteId')
|
@Get('site/:siteId')
|
||||||
findBySite(@Param('siteId') siteId: string) {
|
findBySite(@Param('siteId') siteId: string) {
|
||||||
return this.documentsService.findBySite(siteId);
|
return this.documentsService.findBySite(siteId);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export class DocumentsService {
|
|||||||
machine: true,
|
machine: true,
|
||||||
composant: true,
|
composant: true,
|
||||||
piece: true,
|
piece: true,
|
||||||
|
product: true,
|
||||||
site: true,
|
site: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -27,6 +28,7 @@ export class DocumentsService {
|
|||||||
machine: true,
|
machine: true,
|
||||||
composant: true,
|
composant: true,
|
||||||
piece: true,
|
piece: true,
|
||||||
|
product: true,
|
||||||
site: true,
|
site: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -39,6 +41,7 @@ export class DocumentsService {
|
|||||||
machine: true,
|
machine: true,
|
||||||
composant: true,
|
composant: true,
|
||||||
piece: true,
|
piece: true,
|
||||||
|
product: true,
|
||||||
site: true,
|
site: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -51,6 +54,7 @@ export class DocumentsService {
|
|||||||
machine: true,
|
machine: true,
|
||||||
composant: true,
|
composant: true,
|
||||||
piece: true,
|
piece: true,
|
||||||
|
product: true,
|
||||||
site: true,
|
site: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -63,6 +67,20 @@ export class DocumentsService {
|
|||||||
machine: true,
|
machine: true,
|
||||||
composant: true,
|
composant: true,
|
||||||
piece: 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,
|
site: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -75,6 +93,7 @@ export class DocumentsService {
|
|||||||
machine: true,
|
machine: true,
|
||||||
composant: true,
|
composant: true,
|
||||||
piece: true,
|
piece: true,
|
||||||
|
product: true,
|
||||||
site: true,
|
site: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -59,11 +59,10 @@ describe('MachinesService', () => {
|
|||||||
prix: null,
|
prix: null,
|
||||||
createdAt: timestamp,
|
createdAt: timestamp,
|
||||||
updatedAt: timestamp,
|
updatedAt: timestamp,
|
||||||
constructeurId: null,
|
constructeurs: [],
|
||||||
typePieceId: null,
|
typePieceId: null,
|
||||||
documents: [],
|
documents: [],
|
||||||
customFieldValues: [],
|
customFieldValues: [],
|
||||||
constructeur: null,
|
|
||||||
typePiece: null,
|
typePiece: null,
|
||||||
},
|
},
|
||||||
typeMachinePieceRequirement: null,
|
typeMachinePieceRequirement: null,
|
||||||
@@ -81,7 +80,7 @@ describe('MachinesService', () => {
|
|||||||
id: 'piece-root',
|
id: 'piece-root',
|
||||||
name: 'Root piece',
|
name: 'Root piece',
|
||||||
},
|
},
|
||||||
} as any;
|
};
|
||||||
|
|
||||||
const componentChildLink = {
|
const componentChildLink = {
|
||||||
id: 'component-child',
|
id: 'component-child',
|
||||||
@@ -101,11 +100,10 @@ describe('MachinesService', () => {
|
|||||||
prix: null,
|
prix: null,
|
||||||
createdAt: timestamp,
|
createdAt: timestamp,
|
||||||
updatedAt: timestamp,
|
updatedAt: timestamp,
|
||||||
constructeurId: null,
|
constructeurs: [],
|
||||||
typeComposantId: null,
|
typeComposantId: null,
|
||||||
documents: [],
|
documents: [],
|
||||||
customFieldValues: [],
|
customFieldValues: [],
|
||||||
constructeur: null,
|
|
||||||
typeComposant: null,
|
typeComposant: null,
|
||||||
},
|
},
|
||||||
typeMachineComponentRequirement: null,
|
typeMachineComponentRequirement: null,
|
||||||
@@ -125,7 +123,7 @@ describe('MachinesService', () => {
|
|||||||
name: 'Root component',
|
name: 'Root component',
|
||||||
},
|
},
|
||||||
pieceLinks: [componentPieceLink],
|
pieceLinks: [componentPieceLink],
|
||||||
} as any;
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: 'machine-1',
|
id: 'machine-1',
|
||||||
@@ -135,11 +133,10 @@ describe('MachinesService', () => {
|
|||||||
createdAt: timestamp,
|
createdAt: timestamp,
|
||||||
updatedAt: timestamp,
|
updatedAt: timestamp,
|
||||||
typeMachineId: null,
|
typeMachineId: null,
|
||||||
constructeurId: null,
|
constructeurs: [],
|
||||||
siteId: 'site-1',
|
siteId: 'site-1',
|
||||||
site: null,
|
site: null,
|
||||||
typeMachine: null,
|
typeMachine: null,
|
||||||
constructeur: null,
|
|
||||||
componentLinks: [componentRootLink, componentChildLink],
|
componentLinks: [componentRootLink, componentChildLink],
|
||||||
pieceLinks: [rootPieceLink, componentPieceLink],
|
pieceLinks: [rootPieceLink, componentPieceLink],
|
||||||
customFieldValues: [],
|
customFieldValues: [],
|
||||||
@@ -165,9 +162,7 @@ describe('MachinesService', () => {
|
|||||||
expect(rootLink.pieceLinks[0].parent?.overrides.name).toBe(
|
expect(rootLink.pieceLinks[0].parent?.overrides.name).toBe(
|
||||||
'Root component override',
|
'Root component override',
|
||||||
);
|
);
|
||||||
expect(rootLink.pieceLinks[0].originalPiece.name).toBe(
|
expect(rootLink.pieceLinks[0].originalPiece.name).toBe('Piece component');
|
||||||
'Piece component',
|
|
||||||
);
|
|
||||||
expect(rootLink.pieceLinks[0].piece.name).toBe('Component piece name');
|
expect(rootLink.pieceLinks[0].piece.name).toBe('Component piece name');
|
||||||
expect(rootLink.pieceLinks[0].overrides.reference).toBe('CP-001');
|
expect(rootLink.pieceLinks[0].overrides.reference).toBe('CP-001');
|
||||||
expect(rootLink.overrides.name).toBe('Root component override');
|
expect(rootLink.overrides.name).toBe('Root component override');
|
||||||
@@ -193,9 +188,7 @@ describe('MachinesService', () => {
|
|||||||
const root = result?.componentLinks[0];
|
const root = result?.componentLinks[0];
|
||||||
expect(root?.childLinks[0].parent?.id).toBe('component-root');
|
expect(root?.childLinks[0].parent?.id).toBe('component-root');
|
||||||
expect(root?.childLinks[0].parent?.composantId).toBe('component-root');
|
expect(root?.childLinks[0].parent?.composantId).toBe('component-root');
|
||||||
expect(root?.childLinks[0].originalComposant.name).toBe(
|
expect(root?.childLinks[0].originalComposant.name).toBe('Child component');
|
||||||
'Child component',
|
|
||||||
);
|
|
||||||
expect(root?.childLinks[0].composant.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?.id).toBe('component-root');
|
||||||
expect(root?.pieceLinks[0].parent?.composantId).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].piece.name).toBe('Root piece name');
|
||||||
expect(result?.pieceLinks[0].overrides.reference).toBe('RP-001');
|
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 {
|
export enum ModelCategory {
|
||||||
COMPONENT = 'COMPONENT',
|
COMPONENT = 'COMPONENT',
|
||||||
PIECE = 'PIECE',
|
PIECE = 'PIECE',
|
||||||
|
PRODUCT = 'PRODUCT',
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateModelTypeDto {
|
export class CreateModelTypeDto {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { UpdateModelTypeDto } from './dto/update-model-type.dto';
|
|||||||
import {
|
import {
|
||||||
ComponentModelStructureSchema,
|
ComponentModelStructureSchema,
|
||||||
PieceModelStructureSchema,
|
PieceModelStructureSchema,
|
||||||
|
ProductModelStructureSchema,
|
||||||
} from '../shared/schemas/inventory';
|
} from '../shared/schemas/inventory';
|
||||||
|
|
||||||
type SortField = 'name' | 'code' | 'createdAt';
|
type SortField = 'name' | 'code' | 'createdAt';
|
||||||
@@ -112,12 +113,22 @@ export class ModelTypeService {
|
|||||||
if (normalizedStructure !== undefined) {
|
if (normalizedStructure !== undefined) {
|
||||||
const skeletonValue =
|
const skeletonValue =
|
||||||
normalizedStructure === null ? Prisma.JsonNull : normalizedStructure;
|
normalizedStructure === null ? Prisma.JsonNull : normalizedStructure;
|
||||||
if (rest.category === ModelCategory.COMPONENT) {
|
switch (rest.category) {
|
||||||
data.componentSkeleton = skeletonValue;
|
case ModelCategory.COMPONENT:
|
||||||
data.pieceSkeleton = Prisma.JsonNull;
|
data.componentSkeleton = skeletonValue;
|
||||||
} else {
|
data.pieceSkeleton = Prisma.JsonNull;
|
||||||
data.pieceSkeleton = skeletonValue;
|
data.productSkeleton = Prisma.JsonNull;
|
||||||
data.componentSkeleton = 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) {
|
if (normalizedStructure !== undefined) {
|
||||||
const skeletonValue =
|
const skeletonValue =
|
||||||
normalizedStructure === null ? Prisma.JsonNull : normalizedStructure;
|
normalizedStructure === null ? Prisma.JsonNull : normalizedStructure;
|
||||||
if (targetCategory === ModelCategory.COMPONENT) {
|
switch (targetCategory) {
|
||||||
data.componentSkeleton = skeletonValue;
|
case ModelCategory.COMPONENT:
|
||||||
data.pieceSkeleton = Prisma.JsonNull;
|
data.componentSkeleton = skeletonValue;
|
||||||
} else {
|
data.pieceSkeleton = Prisma.JsonNull;
|
||||||
data.pieceSkeleton = skeletonValue;
|
data.productSkeleton = Prisma.JsonNull;
|
||||||
data.componentSkeleton = 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 {
|
private handlePrismaError(error: unknown): never {
|
||||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
if (error.code === 'P2002' && this.isUniqueCodeConstraint(error)) {
|
if (error.code === 'P2002') {
|
||||||
throw new ConflictException('Ce code est déjà utilisé.');
|
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') {
|
if (error.code === 'P2025') {
|
||||||
@@ -235,6 +262,17 @@ export class ModelTypeService {
|
|||||||
return false;
|
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(
|
private normalizeStructure(
|
||||||
category: ModelCategory,
|
category: ModelCategory,
|
||||||
structure: unknown,
|
structure: unknown,
|
||||||
@@ -253,7 +291,12 @@ export class ModelTypeService {
|
|||||||
structure,
|
structure,
|
||||||
) as Prisma.InputJsonValue;
|
) as Prisma.InputJsonValue;
|
||||||
}
|
}
|
||||||
return PieceModelStructureSchema.parse(
|
if (category === ModelCategory.PIECE) {
|
||||||
|
return PieceModelStructureSchema.parse(
|
||||||
|
structure,
|
||||||
|
) as Prisma.InputJsonValue;
|
||||||
|
}
|
||||||
|
return ProductModelStructureSchema.parse(
|
||||||
structure,
|
structure,
|
||||||
) as Prisma.InputJsonValue;
|
) as Prisma.InputJsonValue;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -264,10 +307,24 @@ export class ModelTypeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private mapModelType(modelType: PrismaModelType) {
|
private mapModelType(modelType: PrismaModelType) {
|
||||||
const structure =
|
let structure: Prisma.InputJsonValue | null;
|
||||||
modelType.category === ModelCategory.COMPONENT
|
switch (modelType.category as ModelCategory) {
|
||||||
? (modelType.componentSkeleton ?? null)
|
case ModelCategory.COMPONENT:
|
||||||
: (modelType.pieceSkeleton ?? null);
|
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 {
|
return {
|
||||||
...modelType,
|
...modelType,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ describe('PiecesService', () => {
|
|||||||
customField: {
|
customField: {
|
||||||
findMany: jest.fn(),
|
findMany: jest.fn(),
|
||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
},
|
},
|
||||||
customFieldValue: {
|
customFieldValue: {
|
||||||
findMany: jest.fn(),
|
findMany: jest.fn(),
|
||||||
@@ -27,10 +28,7 @@ describe('PiecesService', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [
|
providers: [PiecesService, { provide: PrismaService, useValue: prisma }],
|
||||||
PiecesService,
|
|
||||||
{ provide: PrismaService, useValue: prisma },
|
|
||||||
],
|
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<PiecesService>(PiecesService);
|
service = module.get<PiecesService>(PiecesService);
|
||||||
@@ -40,29 +38,45 @@ describe('PiecesService', () => {
|
|||||||
const dto: CreatePieceDto = {
|
const dto: CreatePieceDto = {
|
||||||
name: 'Piece A',
|
name: 'Piece A',
|
||||||
typePieceId: 'type-piece-1',
|
typePieceId: 'type-piece-1',
|
||||||
|
productId: ' product-1 ',
|
||||||
};
|
};
|
||||||
|
|
||||||
prisma.piece.create.mockResolvedValue({ id: 'piece-1', name: dto.name });
|
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.customField.findMany.mockResolvedValue([]);
|
||||||
prisma.customFieldValue.findMany.mockResolvedValue([]);
|
prisma.customFieldValue.findMany.mockResolvedValue([]);
|
||||||
|
|
||||||
const result = await service.create(dto);
|
const result = await service.create(dto);
|
||||||
|
|
||||||
expect(prisma.piece.create).toHaveBeenCalled();
|
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' });
|
expect(result).toMatchObject({ id: 'piece-1' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates a piece', async () => {
|
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.update.mockResolvedValue({
|
||||||
prisma.piece.findUnique.mockResolvedValue({ id: 'piece-1', name: 'Updated piece' });
|
id: 'piece-1',
|
||||||
|
name: 'Updated piece',
|
||||||
|
});
|
||||||
|
prisma.piece.findUnique.mockResolvedValue({
|
||||||
|
id: 'piece-1',
|
||||||
|
name: 'Updated piece',
|
||||||
|
});
|
||||||
prisma.customField.findMany.mockResolvedValue([]);
|
prisma.customField.findMany.mockResolvedValue([]);
|
||||||
prisma.customFieldValue.findMany.mockResolvedValue([]);
|
prisma.customFieldValue.findMany.mockResolvedValue([]);
|
||||||
|
|
||||||
await service.update('piece-1', dto);
|
await service.update('piece-1', dto);
|
||||||
|
|
||||||
expect(prisma.piece.update).toHaveBeenCalled();
|
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 { Prisma } from '@prisma/client';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { syncConstructeurLinks } from '../common/utils/constructeur-link.util';
|
||||||
import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto';
|
import { CreatePieceDto, UpdatePieceDto } from '../shared/dto/piece.dto';
|
||||||
import { PieceModelStructureSchema } from '../shared/schemas/inventory';
|
import { PieceModelStructureSchema } from '../shared/schemas/inventory';
|
||||||
import type { PieceModelStructure } from '../shared/types/inventory';
|
import type { PieceModelStructure } from '../shared/types/inventory';
|
||||||
@@ -8,16 +9,30 @@ import type { PieceModelStructure } from '../shared/types/inventory';
|
|||||||
const PIECE_WITH_RELATIONS_INCLUDE = {
|
const PIECE_WITH_RELATIONS_INCLUDE = {
|
||||||
typePiece: {
|
typePiece: {
|
||||||
include: {
|
include: {
|
||||||
pieceCustomFields: true,
|
pieceCustomFields: {
|
||||||
|
orderBy: { orderIndex: 'asc' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
constructeur: true,
|
constructeurs: true,
|
||||||
documents: true,
|
documents: true,
|
||||||
customFieldValues: {
|
customFieldValues: {
|
||||||
include: {
|
include: {
|
||||||
customField: true,
|
customField: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
product: {
|
||||||
|
include: {
|
||||||
|
typeProduct: true,
|
||||||
|
constructeurs: true,
|
||||||
|
customFieldValues: {
|
||||||
|
include: {
|
||||||
|
customField: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
documents: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
machineLinks: {
|
machineLinks: {
|
||||||
include: {
|
include: {
|
||||||
machine: true,
|
machine: true,
|
||||||
@@ -31,18 +46,20 @@ const PIECE_WITH_RELATIONS_INCLUDE = {
|
|||||||
export class PiecesService {
|
export class PiecesService {
|
||||||
constructor(private prisma: PrismaService) {}
|
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 = {
|
const data: Prisma.PieceCreateInput = {
|
||||||
name: createPieceDto.name,
|
name: createPieceDto.name,
|
||||||
reference: createPieceDto.reference ?? null,
|
reference: createPieceDto.reference ?? null,
|
||||||
prix: createPieceDto.prix !== undefined ? createPieceDto.prix : null,
|
prix: createPieceDto.prix !== undefined ? createPieceDto.prix : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (createPieceDto.constructeurId) {
|
const constructeurIds = this.normalizeConstructeurIds(
|
||||||
data.constructeur = {
|
createPieceDto.constructeurIds,
|
||||||
connect: { id: createPieceDto.constructeurId },
|
);
|
||||||
};
|
const resolvedConstructeurIds =
|
||||||
}
|
await this.resolveExistingConstructeurIds(constructeurIds);
|
||||||
|
|
||||||
if (createPieceDto.typePieceId) {
|
if (createPieceDto.typePieceId) {
|
||||||
data.typePiece = {
|
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) {
|
async create(createPieceDto: CreatePieceDto) {
|
||||||
const created = await this.prisma.piece.create({
|
try {
|
||||||
data: this.buildCreateInput(createPieceDto),
|
const { data, constructeurIds } =
|
||||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
await this.buildCreateInput(createPieceDto);
|
||||||
});
|
|
||||||
|
|
||||||
await this.applyPieceSkeleton({
|
const { pieceId, syncedConstructeurIds } = await this.prisma.$transaction(
|
||||||
pieceId: created.id,
|
async (tx) => {
|
||||||
typePiece: created.typePiece as PieceTypeWithSkeleton | null,
|
const created = await tx.piece.create({
|
||||||
});
|
data,
|
||||||
|
include: PIECE_WITH_RELATIONS_INCLUDE,
|
||||||
|
});
|
||||||
|
|
||||||
return this.prisma.piece.findUnique({
|
let synced: string[] = [];
|
||||||
where: { id: created.id },
|
if (constructeurIds.length > 0) {
|
||||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
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() {
|
async findAll() {
|
||||||
@@ -99,10 +161,13 @@ export class PiecesService {
|
|||||||
data.prix = updatePieceDto.prix;
|
data.prix = updatePieceDto.prix;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatePieceDto.constructeurId !== undefined) {
|
let resolvedConstructeurIds: string[] | undefined;
|
||||||
data.constructeur = updatePieceDto.constructeurId
|
if (updatePieceDto.constructeurIds !== undefined) {
|
||||||
? { connect: { id: updatePieceDto.constructeurId } }
|
const constructeurIds = this.normalizeConstructeurIds(
|
||||||
: { disconnect: true };
|
updatePieceDto.constructeurIds,
|
||||||
|
);
|
||||||
|
resolvedConstructeurIds =
|
||||||
|
await this.resolveExistingConstructeurIds(constructeurIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatePieceDto.typePieceId !== undefined) {
|
if (updatePieceDto.typePieceId !== undefined) {
|
||||||
@@ -111,24 +176,112 @@ export class PiecesService {
|
|||||||
: { disconnect: true };
|
: { disconnect: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = await this.prisma.piece.update({
|
if (updatePieceDto.productId !== undefined) {
|
||||||
where: { id },
|
const normalizedProductId =
|
||||||
data,
|
typeof updatePieceDto.productId === 'string'
|
||||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
? updatePieceDto.productId.trim()
|
||||||
});
|
: null;
|
||||||
|
data.product = normalizedProductId
|
||||||
|
? { connect: { id: normalizedProductId } }
|
||||||
|
: { disconnect: true };
|
||||||
|
}
|
||||||
|
|
||||||
await this.applyPieceSkeleton({
|
let syncedConstructeurIds: string[] | undefined;
|
||||||
pieceId: updated.id,
|
try {
|
||||||
typePiece: updated.typePiece as PieceTypeWithSkeleton | null,
|
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({
|
if (resolvedConstructeurIds !== undefined) {
|
||||||
where: { id: updated.id },
|
syncedConstructeurIds = await syncConstructeurLinks(
|
||||||
include: PIECE_WITH_RELATIONS_INCLUDE,
|
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) {
|
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({
|
return this.prisma.piece.delete({
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
@@ -137,9 +290,16 @@ export class PiecesService {
|
|||||||
private async applyPieceSkeleton({
|
private async applyPieceSkeleton({
|
||||||
pieceId,
|
pieceId,
|
||||||
typePiece,
|
typePiece,
|
||||||
|
product,
|
||||||
|
prisma,
|
||||||
}: {
|
}: {
|
||||||
pieceId: string;
|
pieceId: string;
|
||||||
typePiece: PieceTypeWithSkeleton | null;
|
typePiece: PieceTypeWithSkeleton | null;
|
||||||
|
product: {
|
||||||
|
typeProductId: string | null;
|
||||||
|
typeProduct?: { code: string | null } | null;
|
||||||
|
} | null;
|
||||||
|
prisma: Prisma.TransactionClient | PrismaService;
|
||||||
}) {
|
}) {
|
||||||
if (!typePiece?.id) {
|
if (!typePiece?.id) {
|
||||||
return;
|
return;
|
||||||
@@ -155,13 +315,142 @@ export class PiecesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customFields = skeleton.customFields ?? [];
|
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(
|
await this.createPieceCustomFieldValues(
|
||||||
|
prisma,
|
||||||
pieceId,
|
pieceId,
|
||||||
typePiece.id,
|
typePiece.id,
|
||||||
customFields,
|
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 {
|
private parsePieceSkeleton(value: unknown): PieceModelStructure | null {
|
||||||
@@ -177,6 +466,7 @@ export class PiecesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async ensurePieceCustomFieldDefinitions(
|
private async ensurePieceCustomFieldDefinitions(
|
||||||
|
prisma: Prisma.TransactionClient | PrismaService,
|
||||||
typePieceId: string,
|
typePieceId: string,
|
||||||
customFields: PieceModelStructure['customFields'],
|
customFields: PieceModelStructure['customFields'],
|
||||||
) {
|
) {
|
||||||
@@ -188,25 +478,37 @@ export class PiecesService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = await this.prisma.customField.findMany({
|
const existing = await prisma.customField.findMany({
|
||||||
where: { typePieceId },
|
where: { typePieceId },
|
||||||
select: { id: true, name: true },
|
select: { id: true, name: true, orderIndex: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingByName = new Map(
|
const existingByName = new Map(
|
||||||
existing.map((field) => [
|
existing.map((field) => [
|
||||||
this.normalizeIdentifier(field.name) ?? field.name,
|
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) {
|
if (!field) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = this.normalizeIdentifier(field.name);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,22 +516,24 @@ export class PiecesService {
|
|||||||
const required = Boolean(field.required);
|
const required = Boolean(field.required);
|
||||||
const options = this.normalizeOptions(field);
|
const options = this.normalizeOptions(field);
|
||||||
|
|
||||||
const created = await this.prisma.customField.create({
|
const created = await prisma.customField.create({
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
required,
|
required,
|
||||||
options,
|
options,
|
||||||
|
orderIndex: index,
|
||||||
typePieceId,
|
typePieceId,
|
||||||
},
|
},
|
||||||
select: { id: true },
|
select: { id: true, name: true, orderIndex: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
existingByName.set(name, created.id);
|
existingByName.set(name, created);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createPieceCustomFieldValues(
|
private async createPieceCustomFieldValues(
|
||||||
|
prisma: Prisma.TransactionClient | PrismaService,
|
||||||
pieceId: string,
|
pieceId: string,
|
||||||
typePieceId: string,
|
typePieceId: string,
|
||||||
customFields: PieceModelStructure['customFields'],
|
customFields: PieceModelStructure['customFields'],
|
||||||
@@ -242,7 +546,7 @@ export class PiecesService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const definitions = await this.prisma.customField.findMany({
|
const definitions = await prisma.customField.findMany({
|
||||||
where: { typePieceId },
|
where: { typePieceId },
|
||||||
select: { id: true, name: true },
|
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 },
|
where: { pieceId },
|
||||||
select: { customFieldId: true },
|
select: { customFieldId: true },
|
||||||
});
|
});
|
||||||
@@ -282,7 +586,7 @@ export class PiecesService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prisma.customFieldValue.create({
|
await prisma.customFieldValue.create({
|
||||||
data: {
|
data: {
|
||||||
customFieldId: definitionId,
|
customFieldId: definitionId,
|
||||||
pieceId,
|
pieceId,
|
||||||
@@ -343,10 +647,37 @@ export class PiecesService {
|
|||||||
|
|
||||||
return JSON.stringify(value);
|
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<{
|
type PieceTypeWithSkeleton = Prisma.ModelTypeGetPayload<{
|
||||||
include: { pieceCustomFields: true };
|
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';
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
export class CreateComposantDto {
|
export class CreateComposantDto {
|
||||||
@@ -18,8 +24,10 @@ export class CreateComposantDto {
|
|||||||
reference?: string;
|
reference?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsOptional()
|
||||||
constructeurId?: string;
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
constructeurIds?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(({ value }) => (value === '' ? null : value))
|
@Transform(({ value }) => (value === '' ? null : value))
|
||||||
@@ -37,6 +45,10 @@ export class CreateComposantDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
structure?: Record<string, any>;
|
structure?: Record<string, any>;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
productId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateComposantDto {
|
export class UpdateComposantDto {
|
||||||
@@ -49,8 +61,9 @@ export class UpdateComposantDto {
|
|||||||
reference?: string;
|
reference?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsArray()
|
||||||
constructeurId?: string;
|
@IsString({ each: true })
|
||||||
|
constructeurIds?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(({ value }) => (value === '' ? null : value))
|
@Transform(({ value }) => (value === '' ? null : value))
|
||||||
@@ -64,4 +77,9 @@ export class UpdateComposantDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
structure?: Record<string, any>;
|
structure?: Record<string, any>;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => (value === '' ? null : value))
|
||||||
|
@IsString()
|
||||||
|
productId?: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export enum CustomFieldEntityType {
|
|||||||
MACHINE = 'machine',
|
MACHINE = 'machine',
|
||||||
COMPOSANT = 'composant',
|
COMPOSANT = 'composant',
|
||||||
PIECE = 'piece',
|
PIECE = 'piece',
|
||||||
|
PRODUCT = 'product',
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CustomFieldEntityParamsDto {
|
export class CustomFieldEntityParamsDto {
|
||||||
@@ -76,6 +77,10 @@ export class CreateCustomFieldValueDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
pieceId?: string;
|
pieceId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
productId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateCustomFieldValueDto {
|
export class UpdateCustomFieldValueDto {
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ export class CreateDocumentDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
siteId?: string;
|
siteId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
productId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateDocumentDto {
|
export class UpdateDocumentDto {
|
||||||
@@ -57,4 +61,20 @@ export class UpdateDocumentDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
siteId?: string;
|
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()
|
@IsString()
|
||||||
composantId?: string;
|
composantId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
productId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
componentId?: string;
|
componentId?: string;
|
||||||
@@ -97,6 +101,10 @@ export class MachinePieceLinkPayloadDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
composantId?: string;
|
composantId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
productId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
parentLinkId?: string;
|
parentLinkId?: string;
|
||||||
@@ -142,6 +150,59 @@ export class MachinePieceLinkPayloadDto {
|
|||||||
overrides?: Record<string, unknown>;
|
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 {
|
export class CreateMachineDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
name: string;
|
name: string;
|
||||||
@@ -154,8 +215,9 @@ export class CreateMachineDto {
|
|||||||
reference?: string;
|
reference?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsArray()
|
||||||
constructeurId?: string;
|
@IsString({ each: true })
|
||||||
|
constructeurIds?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsDecimal()
|
@IsDecimal()
|
||||||
@@ -176,6 +238,12 @@ export class CreateMachineDto {
|
|||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => MachinePieceLinkPayloadDto)
|
@Type(() => MachinePieceLinkPayloadDto)
|
||||||
pieceLinks?: MachinePieceLinkPayloadDto[];
|
pieceLinks?: MachinePieceLinkPayloadDto[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => MachineProductLinkPayloadDto)
|
||||||
|
productLinks?: MachineProductLinkPayloadDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateMachineDto {
|
export class UpdateMachineDto {
|
||||||
@@ -188,8 +256,9 @@ export class UpdateMachineDto {
|
|||||||
reference?: string;
|
reference?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsArray()
|
||||||
constructeurId?: string;
|
@IsString({ each: true })
|
||||||
|
constructeurIds?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsDecimal()
|
@IsDecimal()
|
||||||
@@ -212,7 +281,14 @@ export class ReconfigureMachineDto {
|
|||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => MachinePieceLinkPayloadDto)
|
@Type(() => MachinePieceLinkPayloadDto)
|
||||||
pieceLinks?: MachinePieceLinkPayloadDto[];
|
pieceLinks?: MachinePieceLinkPayloadDto[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => MachineProductLinkPayloadDto)
|
||||||
|
productLinks?: MachineProductLinkPayloadDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MachineComponentLinkInput = MachineComponentLinkPayloadDto;
|
export type MachineComponentLinkInput = MachineComponentLinkPayloadDto;
|
||||||
export type MachinePieceLinkInput = MachinePieceLinkPayloadDto;
|
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';
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
export class CreatePieceDto {
|
export class CreatePieceDto {
|
||||||
@@ -18,8 +18,10 @@ export class CreatePieceDto {
|
|||||||
reference?: string;
|
reference?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsOptional()
|
||||||
constructeurId?: string;
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
constructeurIds?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(({ value }) => (value === '' ? null : value))
|
@Transform(({ value }) => (value === '' ? null : value))
|
||||||
@@ -33,6 +35,10 @@ export class CreatePieceDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
typeMachinePieceRequirementId?: string;
|
typeMachinePieceRequirementId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
productId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdatePieceDto {
|
export class UpdatePieceDto {
|
||||||
@@ -45,8 +51,10 @@ export class UpdatePieceDto {
|
|||||||
reference?: string;
|
reference?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsOptional()
|
||||||
constructeurId?: string;
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
constructeurIds?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(({ value }) => (value === '' ? null : value))
|
@Transform(({ value }) => (value === '' ? null : value))
|
||||||
@@ -56,4 +64,9 @@ export class UpdatePieceDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
typePieceId?: string;
|
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()
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
options?: string[]; // Pour les champs de type SELECT
|
options?: string[]; // Pour les champs de type SELECT
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
orderIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateCustomFieldDto {
|
export class UpdateCustomFieldDto {
|
||||||
@@ -54,6 +58,10 @@ export class UpdateCustomFieldDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
options?: string[];
|
options?: string[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
orderIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TypeMachineComponentRequirementDto {
|
export class TypeMachineComponentRequirementDto {
|
||||||
@@ -79,6 +87,10 @@ export class TypeMachineComponentRequirementDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
allowNewModels?: boolean;
|
allowNewModels?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
orderIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TypeMachinePieceRequirementDto {
|
export class TypeMachinePieceRequirementDto {
|
||||||
@@ -104,6 +116,39 @@ export class TypeMachinePieceRequirementDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
allowNewModels?: boolean;
|
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 {
|
export class CreateTypeMachineDto {
|
||||||
@@ -145,6 +190,12 @@ export class CreateTypeMachineDto {
|
|||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => TypeMachinePieceRequirementDto)
|
@Type(() => TypeMachinePieceRequirementDto)
|
||||||
pieceRequirements?: TypeMachinePieceRequirementDto[];
|
pieceRequirements?: TypeMachinePieceRequirementDto[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => TypeMachineProductRequirementDto)
|
||||||
|
productRequirements?: TypeMachineProductRequirementDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateTypeMachineDto {
|
export class UpdateTypeMachineDto {
|
||||||
@@ -187,6 +238,12 @@ export class UpdateTypeMachineDto {
|
|||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => TypeMachinePieceRequirementDto)
|
@Type(() => TypeMachinePieceRequirementDto)
|
||||||
pieceRequirements?: TypeMachinePieceRequirementDto[];
|
pieceRequirements?: TypeMachinePieceRequirementDto[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => TypeMachineProductRequirementDto)
|
||||||
|
productRequirements?: TypeMachineProductRequirementDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateTypeComposantDto {
|
export class CreateTypeComposantDto {
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { normalizeComponentModelStructure } from '../../component-models/structu
|
|||||||
import type {
|
import type {
|
||||||
ComponentModelStructure,
|
ComponentModelStructure,
|
||||||
PieceModelCustomField,
|
PieceModelCustomField,
|
||||||
|
PieceModelProduct,
|
||||||
PieceModelStructure,
|
PieceModelStructure,
|
||||||
|
ProductModelStructure,
|
||||||
} from '../types/inventory';
|
} from '../types/inventory';
|
||||||
|
|
||||||
export class ComponentModelStructureValidationError extends Error {
|
export class ComponentModelStructureValidationError extends Error {
|
||||||
@@ -28,6 +30,67 @@ function sanitizeOptionalString(value: unknown): string | undefined {
|
|||||||
return String(value);
|
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(
|
function validatePieces(
|
||||||
pieces: ComponentModelStructure['pieces'],
|
pieces: ComponentModelStructure['pieces'],
|
||||||
): ComponentModelStructure['pieces'] {
|
): ComponentModelStructure['pieces'] {
|
||||||
@@ -148,6 +211,7 @@ export const ComponentModelStructureSchema = {
|
|||||||
const normalized = normalizeComponentModelStructure(input);
|
const normalized = normalizeComponentModelStructure(input);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
products: validateProducts(normalized.products),
|
||||||
pieces: validatePieces(normalized.pieces),
|
pieces: validatePieces(normalized.pieces),
|
||||||
customFields: validateCustomFields(normalized.customFields),
|
customFields: validateCustomFields(normalized.customFields),
|
||||||
subcomponents: validateSubcomponents(normalized.subcomponents),
|
subcomponents: validateSubcomponents(normalized.subcomponents),
|
||||||
@@ -230,10 +294,57 @@ function normalizePieceModelCustomFields(
|
|||||||
return normalized;
|
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 = {
|
export const PieceModelStructureSchema = {
|
||||||
parse(input: unknown): PieceModelStructure {
|
parse(input: unknown): PieceModelStructure {
|
||||||
if (input === undefined || input === null) {
|
if (input === undefined || input === null) {
|
||||||
return { customFields: [] };
|
return { customFields: [], products: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof input !== 'object' || Array.isArray(input)) {
|
if (typeof input !== 'object' || Array.isArray(input)) {
|
||||||
@@ -250,6 +361,11 @@ export const PieceModelStructureSchema = {
|
|||||||
structure.customFields = customFields;
|
structure.customFields = customFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const products = normalizePieceModelProducts(record.products);
|
||||||
|
if (products.length > 0 || 'products' in record) {
|
||||||
|
structure.products = products;
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedTypePiece = toStringOrNull(record.typePieceId);
|
const normalizedTypePiece = toStringOrNull(record.typePieceId);
|
||||||
if (normalizedTypePiece) {
|
if (normalizedTypePiece) {
|
||||||
structure.typePieceId = normalizedTypePiece;
|
structure.typePieceId = normalizedTypePiece;
|
||||||
@@ -260,3 +376,34 @@ export const PieceModelStructureSchema = {
|
|||||||
return structure;
|
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).
|
* Valeurs par défaut au niveau "modèle" (libres, mais clé obligatoire).
|
||||||
*/
|
*/
|
||||||
@@ -48,7 +62,25 @@ export type PieceModelCustomField = {
|
|||||||
options?: unknown;
|
options?: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PieceModelProduct =
|
||||||
|
| {
|
||||||
|
familyCode: string;
|
||||||
|
role?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
typeProductId: string;
|
||||||
|
role?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type PieceModelStructure = {
|
export type PieceModelStructure = {
|
||||||
customFields?: PieceModelCustomField[];
|
customFields?: PieceModelCustomField[];
|
||||||
|
products?: PieceModelProduct[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProductModelCustomField = PieceModelCustomField;
|
||||||
|
|
||||||
|
export type ProductModelStructure = {
|
||||||
|
customFields?: ProductModelCustomField[];
|
||||||
[key: string]: unknown;
|
[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 { TypeMachinesRepository } from '../../common/repositories/type-machines.repository';
|
||||||
import {
|
import {
|
||||||
TYPE_MACHINE_DEFAULT_INCLUDE,
|
TYPE_MACHINE_DEFAULT_INCLUDE,
|
||||||
@@ -17,7 +18,12 @@ export class TypeMachineService {
|
|||||||
async create(dto: CreateTypeMachineDto) {
|
async create(dto: CreateTypeMachineDto) {
|
||||||
const data = TypeMachineMapper.toCreateInput(dto);
|
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() {
|
async findAll() {
|
||||||
@@ -53,7 +59,24 @@ export class TypeMachineService {
|
|||||||
await this.repository.createPieceRequirements(id, requirements);
|
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) {
|
async remove(id: string) {
|
||||||
@@ -69,4 +92,19 @@ export class TypeMachineService {
|
|||||||
await this.repository.deleteCustomFields(id);
|
await this.repository.deleteCustomFields(id);
|
||||||
return this.repository.delete(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;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
reference: Nullable<string>;
|
reference: Nullable<string>;
|
||||||
constructeurId: Nullable<string>;
|
constructeurIds: string[];
|
||||||
prix: Nullable<string>;
|
prix: Nullable<string>;
|
||||||
siteId: string;
|
siteId: string;
|
||||||
typeMachineId: Nullable<string>;
|
typeMachineId: Nullable<string>;
|
||||||
@@ -90,7 +90,7 @@ type ComposantRecord = {
|
|||||||
parentComposantId: Nullable<string>;
|
parentComposantId: Nullable<string>;
|
||||||
typeComposantId: Nullable<string>;
|
typeComposantId: Nullable<string>;
|
||||||
typeMachineComponentRequirementId: Nullable<string>;
|
typeMachineComponentRequirementId: Nullable<string>;
|
||||||
constructeurId: Nullable<string>;
|
constructeurIds: string[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
};
|
};
|
||||||
@@ -104,7 +104,16 @@ type PieceRecord = {
|
|||||||
composantId: Nullable<string>;
|
composantId: Nullable<string>;
|
||||||
typePieceId: Nullable<string>;
|
typePieceId: Nullable<string>;
|
||||||
typeMachinePieceRequirementId: 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;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
};
|
};
|
||||||
@@ -202,6 +211,15 @@ class InMemoryPrismaService {
|
|||||||
private customFields: CustomFieldRecord[] = [];
|
private customFields: CustomFieldRecord[] = [];
|
||||||
private customFieldValues: CustomFieldValueRecord[] = [];
|
private customFieldValues: CustomFieldValueRecord[] = [];
|
||||||
private profiles: ProfileRecord[] = [];
|
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 onModuleInit() {}
|
||||||
async onModuleDestroy() {}
|
async onModuleDestroy() {}
|
||||||
@@ -229,6 +247,7 @@ class InMemoryPrismaService {
|
|||||||
this.customFields = [];
|
this.customFields = [];
|
||||||
this.customFieldValues = [];
|
this.customFieldValues = [];
|
||||||
this.profiles = [];
|
this.profiles = [];
|
||||||
|
this.constructeurs = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly modelTypeDelegate = {
|
private readonly modelTypeDelegate = {
|
||||||
@@ -638,11 +657,12 @@ class InMemoryPrismaService {
|
|||||||
machine = {
|
machine = {
|
||||||
create: async ({ data, include }: any) => {
|
create: async ({ data, include }: any) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const constructeurIds = this.extractConstructeurIds(data.constructeurs);
|
||||||
const record: MachineRecord = {
|
const record: MachineRecord = {
|
||||||
id: generateId('machine'),
|
id: generateId('machine'),
|
||||||
name: data.name,
|
name: data.name,
|
||||||
reference: data.reference ?? null,
|
reference: data.reference ?? null,
|
||||||
constructeurId: data.constructeurId ?? null,
|
constructeurIds,
|
||||||
prix: data.prix ?? null,
|
prix: data.prix ?? null,
|
||||||
siteId: data.siteId,
|
siteId: data.siteId,
|
||||||
typeMachineId: data.typeMachineId ?? null,
|
typeMachineId: data.typeMachineId ?? null,
|
||||||
@@ -683,6 +703,7 @@ class InMemoryPrismaService {
|
|||||||
composant = {
|
composant = {
|
||||||
create: async ({ data }: any) => {
|
create: async ({ data }: any) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const constructeurIds = this.extractConstructeurIds(data.constructeurs);
|
||||||
const record: ComposantRecord = {
|
const record: ComposantRecord = {
|
||||||
id: generateId('component'),
|
id: generateId('component'),
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -693,7 +714,7 @@ class InMemoryPrismaService {
|
|||||||
typeComposantId: data.typeComposantId ?? null,
|
typeComposantId: data.typeComposantId ?? null,
|
||||||
typeMachineComponentRequirementId:
|
typeMachineComponentRequirementId:
|
||||||
data.typeMachineComponentRequirementId ?? null,
|
data.typeMachineComponentRequirementId ?? null,
|
||||||
constructeurId: data.constructeurId ?? null,
|
constructeurIds,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
@@ -719,6 +740,7 @@ class InMemoryPrismaService {
|
|||||||
piece = {
|
piece = {
|
||||||
create: async ({ data }: any) => {
|
create: async ({ data }: any) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const constructeurIds = this.extractConstructeurIds(data.constructeurs);
|
||||||
const record: PieceRecord = {
|
const record: PieceRecord = {
|
||||||
id: generateId('piece'),
|
id: generateId('piece'),
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -729,7 +751,7 @@ class InMemoryPrismaService {
|
|||||||
typePieceId: data.typePieceId ?? null,
|
typePieceId: data.typePieceId ?? null,
|
||||||
typeMachinePieceRequirementId:
|
typeMachinePieceRequirementId:
|
||||||
data.typeMachinePieceRequirementId ?? null,
|
data.typeMachinePieceRequirementId ?? null,
|
||||||
constructeurId: data.constructeurId ?? null,
|
constructeurIds,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
@@ -767,7 +789,7 @@ class InMemoryPrismaService {
|
|||||||
prixOverride:
|
prixOverride:
|
||||||
data.prixOverride !== undefined && data.prixOverride !== null
|
data.prixOverride !== undefined && data.prixOverride !== null
|
||||||
? String(data.prixOverride)
|
? String(data.prixOverride)
|
||||||
: data.prixOverride ?? null,
|
: (data.prixOverride ?? null),
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
@@ -819,7 +841,7 @@ class InMemoryPrismaService {
|
|||||||
prixOverride:
|
prixOverride:
|
||||||
data.prixOverride !== undefined && data.prixOverride !== null
|
data.prixOverride !== undefined && data.prixOverride !== null
|
||||||
? String(data.prixOverride)
|
? String(data.prixOverride)
|
||||||
: data.prixOverride ?? null,
|
: (data.prixOverride ?? null),
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
@@ -848,7 +870,9 @@ class InMemoryPrismaService {
|
|||||||
(link) => link.parentLinkId === where.parentLinkId,
|
(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 = {
|
profile = {
|
||||||
count: async ({ where }: any) => {
|
count: async ({ where }: any) => {
|
||||||
return this.profiles.filter((profile) => {
|
return this.profiles.filter((profile) => {
|
||||||
@@ -1279,6 +1417,71 @@ class InMemoryPrismaService {
|
|||||||
return base;
|
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) {
|
private buildMachine(machine: MachineRecord, include: any) {
|
||||||
const base: any = { ...machine };
|
const base: any = { ...machine };
|
||||||
|
|
||||||
@@ -1309,8 +1512,8 @@ class InMemoryPrismaService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (include?.constructeur) {
|
if (include?.constructeurs) {
|
||||||
base.constructeur = null;
|
base.constructeurs = this.mapConstructeurs(machine.constructeurIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (include?.componentLinks) {
|
if (include?.componentLinks) {
|
||||||
@@ -1389,19 +1592,19 @@ class InMemoryPrismaService {
|
|||||||
|
|
||||||
if (include?.typeMachineComponentRequirement) {
|
if (include?.typeMachineComponentRequirement) {
|
||||||
const requirement = link.typeMachineComponentRequirementId
|
const requirement = link.typeMachineComponentRequirementId
|
||||||
? this.typeMachineComponentRequirements.find(
|
? (this.typeMachineComponentRequirements.find(
|
||||||
(item) => item.id === link.typeMachineComponentRequirementId,
|
(item) => item.id === link.typeMachineComponentRequirementId,
|
||||||
) ?? null
|
) ?? null)
|
||||||
: null;
|
: null;
|
||||||
base.typeMachineComponentRequirement = requirement
|
base.typeMachineComponentRequirement = requirement
|
||||||
? {
|
? {
|
||||||
...requirement,
|
...requirement,
|
||||||
typeComposant:
|
typeComposant: include.typeMachineComponentRequirement.include
|
||||||
include.typeMachineComponentRequirement.include?.typeComposant
|
?.typeComposant
|
||||||
? this.typeComposants.find(
|
? (this.typeComposants.find(
|
||||||
(item) => item.id === requirement.typeComposantId,
|
(item) => item.id === requirement.typeComposantId,
|
||||||
) ?? null
|
) ?? null)
|
||||||
: undefined,
|
: undefined,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
@@ -1419,14 +1622,12 @@ class InMemoryPrismaService {
|
|||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildMachinePieceLink(
|
private buildMachinePieceLink(link: MachinePieceLinkRecord, include: any) {
|
||||||
link: MachinePieceLinkRecord,
|
|
||||||
include: any,
|
|
||||||
) {
|
|
||||||
const base: any = { ...link };
|
const base: any = { ...link };
|
||||||
|
|
||||||
if (include?.piece) {
|
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
|
base.piece = piece
|
||||||
? this.buildPiece(piece, include.piece.include ?? {})
|
? this.buildPiece(piece, include.piece.include ?? {})
|
||||||
: null;
|
: null;
|
||||||
@@ -1434,19 +1635,18 @@ class InMemoryPrismaService {
|
|||||||
|
|
||||||
if (include?.typeMachinePieceRequirement) {
|
if (include?.typeMachinePieceRequirement) {
|
||||||
const requirement = link.typeMachinePieceRequirementId
|
const requirement = link.typeMachinePieceRequirementId
|
||||||
? this.typeMachinePieceRequirements.find(
|
? (this.typeMachinePieceRequirements.find(
|
||||||
(item) => item.id === link.typeMachinePieceRequirementId,
|
(item) => item.id === link.typeMachinePieceRequirementId,
|
||||||
) ?? null
|
) ?? null)
|
||||||
: null;
|
: null;
|
||||||
base.typeMachinePieceRequirement = requirement
|
base.typeMachinePieceRequirement = requirement
|
||||||
? {
|
? {
|
||||||
...requirement,
|
...requirement,
|
||||||
typePiece:
|
typePiece: include.typeMachinePieceRequirement.include?.typePiece
|
||||||
include.typeMachinePieceRequirement.include?.typePiece
|
? (this.typePieces.find(
|
||||||
? this.typePieces.find(
|
(item) => item.id === requirement.typePieceId,
|
||||||
(item) => item.id === requirement.typePieceId,
|
) ?? null)
|
||||||
) ?? null
|
: undefined,
|
||||||
: undefined,
|
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
@@ -1505,8 +1705,8 @@ class InMemoryPrismaService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (include?.constructeur) {
|
if (include?.constructeurs) {
|
||||||
base.constructeur = null;
|
base.constructeurs = this.mapConstructeurs(component.constructeurIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (include?.pieces) {
|
if (include?.pieces) {
|
||||||
@@ -1536,8 +1736,8 @@ class InMemoryPrismaService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (include?.constructeur) {
|
if (include?.constructeurs) {
|
||||||
base.constructeur = null;
|
base.constructeurs = this.mapConstructeurs(piece.constructeurIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (include?.typeMachinePieceRequirement) {
|
if (include?.typeMachinePieceRequirement) {
|
||||||
|
|||||||
Reference in New Issue
Block a user