Compare commits

...

22 Commits

Author SHA1 Message Date
8d83076be0 chore(release) : v1.2.0 2026-01-29 19:55:29 +01:00
Matthieu
997a3ae822 chore(frontend): update submodule to history UI 2026-01-25 21:22:24 +01:00
Matthieu
034c193e4b feat(audit): add history tracking and bump version to 1.1.2 2026-01-25 21:19:42 +01:00
Matthieu
4acc8d1c01 chore(release) : v1.1.1
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:07:45 +01:00
Matthieu
49ff15f18d fix : correct commit message format in release script
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:07:08 +01:00
Matthieu
7a02617d48 chore : add qpdf to Docker image for PDF compression
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:06:08 +01:00
Matthieu
e52eef0491 docs : add PDF compression documentation to README
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:03:36 +01:00
Matthieu
a5118305d3 feat : automatic PDF compression on upload
- Add PdfCompressorService for lossless compression with qpdf
- Add DocumentPdfCompressorListener for automatic compression on persist/update
- Add app:compress-pdf command for batch compression of existing PDFs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:02:26 +01:00
Matthieu
b51671b1d4 chore(release) : v1.1.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 15:58:09 +01:00
Matthieu
1643dcf8c2 fix : case-insensitive search filters for all entities 2026-01-25 15:54:07 +01:00
Matthieu
17ab4cdd16 chore : update fixtures with current database data 2026-01-25 15:44:22 +01:00
Matthieu
d9182131d9 chore : reset migrations to single initial schema + add deployment guide 2026-01-25 15:41:06 +01:00
Matthieu
26a7fe64be chore : update frontend submodule to master v1.0.0 2026-01-25 12:07:15 +01:00
Matthieu
4669dec359 chore : add versioning system and release script 2026-01-25 12:02:20 +01:00
Matthieu
3f05fe753e Update frontend submodule for piece requirements and component selections 2026-01-25 11:40:48 +01:00
Matthieu
a502acd234 Support multiple product requirements on pieces 2026-01-25 11:40:41 +01:00
Matthieu
69b199b6dc Update frontend submodule: overview machines + lighter PDF previews 2026-01-25 09:46:23 +01:00
Matthieu
d5f6749f9e wip: api filters and migration helpers 2026-01-23 23:30:05 +01:00
ad7918c993 chore(frontend) : met a jour le sous-module 2026-01-23 19:35:36 +01:00
86447000b1 feat(api) : ajoute pagination et filtres pour les catalogues 2026-01-23 19:35:26 +01:00
7da5eb917a feat(fixtures): ajouter système de fixtures pour les données
- fixtures/data.sql: dump des données actuelles de la base
- fixtures/load.sh: script shell de chargement
- makefile: targets fixtures-dump, fixtures-load, fixtures-reset

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 13:34:07 +01:00
d65eb9ff0f WIP: corrections sérialisation API et script normalisation SQL
Backend:
- Fix Groups sur TypeMachine*Requirement: exposer typePiece/typeComposant/typeProduct
- Fix Groups sur Document, Piece, Product, Composant pour sérialisation
- Add addConstructeur/removeConstructeur sur Piece et Product

Scripts:
- Fix normalize-dump.py: gérer les schémas quotés ("public"."table")

Frontend (sous-module):
- Corrections formulaires et sérialisation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 12:34:00 +01:00
51 changed files with 5136 additions and 691 deletions

328
DEPLOY.md Normal file
View File

@@ -0,0 +1,328 @@
# Inventory - Guide de Déploiement & Release
## Architecture
```
inventory.malio-dev.fr/ → Frontend Nuxt (statique)
inventory.malio-dev.fr/api → Backend Symfony (PHP-FPM)
```
| Composant | Technologie | Emplacement serveur |
|-----------|-------------|---------------------|
| Backend | Symfony 8 + API Platform | `/var/www/Inventory/` |
| Frontend | Nuxt 4 (statique) | `/var/www/Inventory/Inventory_frontend/.output/public/` |
| Base de données | PostgreSQL 16 | `inventory` |
---
## Prérequis serveur
- **OS** : Ubuntu/Debian
- **PHP** : 8.4 avec extensions : pgsql, intl, zip, gd, mbstring, curl
- **Node.js** : 20+
- **Nginx**
- **PostgreSQL** : 16
- **Composer**
Vérifier :
```bash
php -v # PHP 8.4+
php -m | grep -E 'pgsql|intl|zip|gd|mbstring'
node -v # Node 20+
nginx -v
psql --version
composer --version
```
---
## Déploiement initial
### 1. Cloner le projet
```bash
cd /var/www
sudo git clone --recurse-submodules gitea@gitea.malio.fr:MALIO-DEV/Inventory.git Inventory
sudo chown -R malio:malio Inventory
cd Inventory
git checkout master
git submodule update --init --recursive
```
### 2. Créer la base de données
```bash
sudo -u postgres psql
CREATE DATABASE inventory OWNER ferme_user;
GRANT ALL PRIVILEGES ON DATABASE inventory TO ferme_user;
\q
```
Importer le dump :
```bash
# Copier le dump depuis le PC local
scp backup_v1.0.0_clean.sql malio@192.168.0.159:/tmp/
# Importer
psql -U ferme_user -h 127.0.0.1 -d inventory -f /tmp/backup_v1.0.0_clean.sql
```
### 3. Configurer le backend Symfony
```bash
cd /var/www/Inventory
# Installer les dépendances
composer install --no-dev --optimize-autoloader
# Créer .env.local
cat > .env.local << 'EOF'
APP_ENV=prod
APP_DEBUG=0
APP_SECRET=CHANGE_ME
DATABASE_URL="postgresql://ferme_user:fermerecette@127.0.0.1:5432/inventory?serverVersion=16"
CORS_ALLOW_ORIGIN='^https?://inventory\.malio-dev\.fr$'
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=inventoryjwt
EOF
# Générer APP_SECRET
sed -i "s/CHANGE_ME/$(openssl rand -hex 32)/" .env.local
# Générer les clés JWT
mkdir -p config/jwt
openssl genrsa -out config/jwt/private.pem -aes256 4096
# Passphrase : inventoryjwt
openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem
chmod 600 config/jwt/private.pem
# Permissions
sudo chown -R www-data:www-data var/
sudo chmod -R 775 var/
# Vider le cache
php bin/console cache:clear --env=prod
```
### 4. Configurer le frontend Nuxt
```bash
cd /var/www/Inventory/Inventory_frontend
# Permissions
sudo chown -R malio:malio .
# Installer les dépendances
npm install
# Créer .env
cat > .env << 'EOF'
NUXT_PUBLIC_API_BASE_URL=http://inventory.malio-dev.fr/api
EOF
# Générer le site statique
npx nuxi generate
```
### 5. Configurer Nginx
```bash
sudo nano /etc/nginx/sites-available/inventory
```
Contenu :
```nginx
server {
listen 80;
server_name inventory.malio-dev.fr;
# Gros fichiers (100MB max)
client_max_body_size 100M;
client_body_timeout 300s;
send_timeout 300s;
access_log /var/log/nginx/inventory-access.log;
error_log /var/log/nginx/inventory-error.log;
# Backend Symfony - /api
location /api {
root /var/www/Inventory/public;
try_files $uri /index.php$is_args$args;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/Inventory/public/index.php;
fastcgi_param DOCUMENT_ROOT /var/www/Inventory/public;
fastcgi_read_timeout 300s;
internal;
}
# Frontend statique
location / {
root /var/www/Inventory/Inventory_frontend/.output/public;
index index.html;
try_files $uri $uri/ /index.html;
}
}
```
Activer :
```bash
sudo ln -s /etc/nginx/sites-available/inventory /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
### 6. Vérifier
```bash
curl http://inventory.malio-dev.fr
curl http://inventory.malio-dev.fr/api
```
---
## Mises à jour
### Mettre à jour l'application
```bash
cd /var/www/Inventory
# Pull les changements
git pull
git submodule update --init --recursive
# Backend
composer install --no-dev --optimize-autoloader
php bin/console cache:clear --env=prod
sudo chown -R www-data:www-data var/
# Frontend
cd Inventory_frontend
npm install
npx nuxi generate
```
---
## Versioning & Releases
### Source de vérité
Le fichier `VERSION` à la racine contient le numéro de version (ex: `1.0.0`).
Cette version est synchronisée avec :
- Le footer de l'application
- `config/packages/api_platform.yaml`
### Créer une release
```bash
# Depuis le PC de dev
./scripts/release.sh patch # 1.0.0 → 1.0.1
./scripts/release.sh minor # 1.0.0 → 1.1.0
./scripts/release.sh major # 1.0.0 → 2.0.0
./scripts/release.sh 2.0.0 # Version exacte
```
Le script :
1. Vérifie/commit le submodule frontend
2. Met à jour `VERSION` et `api_platform.yaml`
3. Commit et tag les deux repos
4. Affiche les commandes pour push
### Pousser la release
```bash
# Frontend (submodule)
cd Inventory_frontend && git push && git push --tags && cd ..
# Backend
git push && git push --tags
```
### Créer la release sur Gitea
1. Aller sur le dépôt Gitea
2. **Releases** > **New Release**
3. Sélectionner le tag `vX.Y.Z`
4. Ajouter les notes de release
---
## Commandes utiles
```bash
# Logs Nginx
tail -f /var/log/nginx/inventory-error.log
# Logs Symfony
tail -f /var/www/Inventory/var/log/prod.log
# Vider le cache Symfony
php /var/www/Inventory/bin/console cache:clear --env=prod
# Rebuild frontend
cd /var/www/Inventory/Inventory_frontend && npx nuxi generate
# Status PHP-FPM
systemctl status php8.4-fpm
# Reload Nginx
sudo systemctl reload nginx
```
---
## Backup base de données
### Export
```bash
pg_dump -U ferme_user -h 127.0.0.1 -d inventory --no-owner --no-acl --inserts --column-inserts --clean --if-exists > backup_inventory_$(date +%Y%m%d).sql
```
### Import
```bash
psql -U ferme_user -h 127.0.0.1 -d inventory -f backup_inventory_YYYYMMDD.sql
```
---
## Troubleshooting
### Erreur 502 Bad Gateway
```bash
# Vérifier PHP-FPM
systemctl status php8.4-fpm
sudo systemctl restart php8.4-fpm
```
### Erreur 403 Forbidden
```bash
# Vérifier les permissions
sudo chown -R www-data:www-data /var/www/Inventory/var/
sudo chmod -R 775 /var/www/Inventory/var/
```
### Erreur API "No route found"
```bash
# Vider le cache
php /var/www/Inventory/bin/console cache:clear --env=prod
```
### Frontend ne se met pas à jour
```bash
# Rebuild
cd /var/www/Inventory/Inventory_frontend
rm -rf .output
npx nuxi generate
```

View File

@@ -44,6 +44,35 @@ make dev-nuxt
``` ```
Le front sera accessible sur http://localhost:3000 Le front sera accessible sur http://localhost:3000
## Compression automatique des PDFs
Les documents PDF uploadés sont automatiquement compressés sans perte de qualité grâce à **qpdf**.
### Prérequis
```bash
# Installation de qpdf (outil système)
sudo apt install qpdf
# Ou dans Docker
docker exec -it php-inventory-apache apt update && apt install -y qpdf
```
### Fonctionnement
- À chaque upload de PDF, le système compresse automatiquement le fichier
- Compression lossless (sans perte de qualité)
- Le PDF est compressé uniquement si la taille diminue
- Si qpdf n'est pas installé, le système fonctionne normalement sans compression
### Compresser les PDFs existants
Pour compresser tous les PDFs déjà en base :
```bash
# Voir ce qui serait compressé (dry-run)
php bin/console app:compress-pdf --dry-run
# Compresser tous les PDFs
php bin/console app:compress-pdf
```
## Commandes utiles ## Commandes utiles
Pour restart le container Pour restart le container
```bash ```bash

138
RELEASE.md Normal file
View File

@@ -0,0 +1,138 @@
# Guide de Release
## Versioning
Le projet utilise le [Semantic Versioning](https://semver.org/) (SemVer) : `MAJOR.MINOR.PATCH`
- **MAJOR** : Changements incompatibles avec les versions précédentes
- **MINOR** : Nouvelles fonctionnalités rétrocompatibles
- **PATCH** : Corrections de bugs rétrocompatibles
La version est centralisée dans le fichier `VERSION` à la racine du projet.
## Créer une release
### Prérequis
- Tous les changements doivent être commités
- Les tests doivent passer
- Être sur la branche à releaser (ex: `main`, `develop`)
### Utilisation du script
```bash
# Afficher l'aide et la version actuelle
./scripts/release.sh
# Bump patch : 1.0.0 → 1.0.1
./scripts/release.sh patch
# Bump minor : 1.0.0 → 1.1.0
./scripts/release.sh minor
# Bump major : 1.0.0 → 2.0.0
./scripts/release.sh major
# Version spécifique
./scripts/release.sh 2.0.0
```
Le script :
1. Met à jour le fichier `VERSION`
2. Met à jour `config/packages/api_platform.yaml`
3. Crée un commit `chore(release): vX.Y.Z`
4. Crée le tag `vX.Y.Z`
### Pousser la release
```bash
git push && git push --tags
```
### Créer la release sur Gitea
1. Aller sur le dépôt Gitea
2. **Releases** > **New Release**
3. Sélectionner le tag `vX.Y.Z`
4. Titre : `v1.0.0` (ou avec un nom descriptif)
5. Description : résumé des changements (voir section Notes de release)
## Notes de release
Template pour les notes de release :
```markdown
## Nouveautés
- Feature A
- Feature B
## Corrections
- Fix du bug X
- Fix du bug Y
## Changements
- Refactoring de Z
- Mise à jour des dépendances
```
## Fichiers impactés par le versioning
| Fichier | Usage |
|---------|-------|
| `VERSION` | Source unique de vérité |
| `config/packages/api_platform.yaml` | Version affichée dans l'API |
| `Inventory_frontend/nuxt.config.ts` | Lit VERSION au build |
| Footer de l'app | Affiche `v{{ appVersion }}` |
## Déploiement en production
### 1. Base de données
Dump de la base locale :
```bash
pg_dump -h localhost -p 5433 -U root -d inventory > backup_v1.0.0.sql
```
Import en production :
```bash
psql -h <PROD_HOST> -U <PROD_USER> -d inventory < backup_v1.0.0.sql
```
### 2. Variables d'environnement production
Créer un fichier `.env.local` en production avec :
```env
APP_ENV=prod
APP_SECRET=<générer avec: openssl rand -hex 32>
DATABASE_URL="postgresql://user:password@host:5432/inventory?serverVersion=16"
CORS_ALLOW_ORIGIN='^https://votre-domaine\.com$'
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=<votre-passphrase>
```
### 3. Build production
Backend :
```bash
composer install --no-dev --optimize-autoloader
php bin/console cache:clear --env=prod
php bin/console doctrine:migrations:migrate --no-interaction
```
Frontend :
```bash
cd Inventory_frontend
NUXT_PUBLIC_API_BASE_URL=https://api.votre-domaine.com yarn build
```
### 4. Checklist avant mise en prod
- [ ] Tests passent
- [ ] Migrations DB testées
- [ ] Variables d'environnement configurées
- [ ] Clés JWT générées
- [ ] CORS configuré
- [ ] SSL/HTTPS actif
- [ ] Backup de la DB prod existante (si upgrade)

1
VERSION Normal file
View File

@@ -0,0 +1 @@
1.2.0

View File

@@ -1,6 +1,6 @@
api_platform: api_platform:
title: Hello API Platform title: Hello API Platform
version: 1.0.0 version: 1.2.0
defaults: defaults:
stateless: false stateless: false
cache_headers: cache_headers:

View File

@@ -1,6 +1,9 @@
doctrine: doctrine:
dbal: dbal:
url: '%env(resolve:DATABASE_URL)%' url: '%env(resolve:DATABASE_URL)%'
mapping_types:
modelcategory: string
_text: string
# IMPORTANT: You MUST configure your server version, # IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file) # either here or in the DATABASE_URL env var (see .env file)

View File

@@ -21,3 +21,15 @@ services:
# add more service definitions when explicit configuration is needed # add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones # please note that last definitions always *replace* previous ones
App\EventSubscriber\ProductAuditSubscriber:
tags:
- { name: doctrine.event_subscriber }
App\EventSubscriber\PieceAuditSubscriber:
tags:
- { name: doctrine.event_subscriber }
App\EventSubscriber\ComposantAuditSubscriber:
tags:
- { name: doctrine.event_subscriber }

View File

@@ -33,6 +33,7 @@ RUN apt-get update && apt-get install -y \
wget \ wget \
git \ git \
unzip \ unzip \
qpdf \
&& docker-php-ext-install -j$(nproc) \ && docker-php-ext-install -j$(nproc) \
intl \ intl \
zip \ zip \

1092
fixtures/data.sql Normal file

File diff suppressed because one or more lines are too long

42
fixtures/load.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/bin/bash
# Load fixtures into the database
# Usage: ./fixtures/load.sh [--reset]
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
# Load environment variables
if [ -f "$PROJECT_DIR/.env" ]; then
export $(grep -v '^#' "$PROJECT_DIR/.env" | xargs)
fi
DB_USER="${POSTGRES_USER:-root}"
DB_NAME="${POSTGRES_DB:-inventory}"
CONTAINER="${DB_CONTAINER:-inventory-db-1}"
echo "Loading fixtures into $DB_NAME..."
# Check if --reset flag is passed
if [ "$1" == "--reset" ]; then
echo "Resetting database (truncating all tables)..."
docker exec -i "$CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" -c "
DO \$\$ DECLARE
r RECORD;
BEGIN
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename != 'doctrine_migration_versions') LOOP
EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' CASCADE';
END LOOP;
END \$\$;
"
fi
# Load fixtures with foreign key checks disabled
docker exec -i "$CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" <<EOF
SET session_replication_role = replica;
$(cat "$SCRIPT_DIR/data.sql")
SET session_replication_role = DEFAULT;
EOF
echo "Fixtures loaded successfully!"

View File

@@ -19,6 +19,8 @@ EXEC_PHP_ROOT = $(DOCKER) exec -t -u root $(PHP_CONTAINER)
EXEC_PHP_INTERACTIVE = $(DOCKER) exec -it -u $(APP_USER) $(PHP_CONTAINER) EXEC_PHP_INTERACTIVE = $(DOCKER) exec -it -u $(APP_USER) $(PHP_CONTAINER)
EXEC_PHP_INTERACTIVE_ROOT = $(DOCKER) exec -it -u root $(PHP_CONTAINER) EXEC_PHP_INTERACTIVE_ROOT = $(DOCKER) exec -it -u root $(PHP_CONTAINER)
FILES = FILES =
DATA_SQL ?= data.sql
DATA_SQL_NORM ?= data_norm.sql
#======================================================================================== #========================================================================================
@@ -58,7 +60,7 @@ build-nuxtJS:
$(EXEC_PHP) sh -lc "cd Inventory_frontend && npm install && npm run generate" $(EXEC_PHP) sh -lc "cd Inventory_frontend && npm install && npm run generate"
dev-nuxt: dev-nuxt:
$(EXEC_PHP) sh -c "cd Inventory_frontend && npm run dev" $(EXEC_PHP) sh -lc "cd Inventory_frontend && npm run dev"
delete_built_dir: delete_built_dir:
CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d CURRENT_UID=$(shell id -u) CURRENT_GID=$(shell id -g) $(DOCKER_COMPOSE) up -d
@@ -117,3 +119,31 @@ test:
wait: wait:
sleep 10 sleep 10
# Normalize pgAdmin data-only dump and import into DB
import-data:
python3 scripts/normalize-dump.py $(DATA_SQL) $(DATA_SQL_NORM) --lower
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -v ON_ERROR_STOP=1 -c "SET session_replication_role = replica;"
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -v ON_ERROR_STOP=1 < $(DATA_SQL_NORM)
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -v ON_ERROR_STOP=1 -c "SET session_replication_role = DEFAULT;"
# Fixtures management
fixtures-dump:
@echo "Dumping current database to fixtures/data.sql..."
$(DOCKER_COMPOSE) exec -T db pg_dump -U $(POSTGRES_USER) -d $(POSTGRES_DB) \
--data-only --inserts --no-owner --no-privileges \
--exclude-table=doctrine_migration_versions \
| grep -v "^pg_dump:" | grep -v "^\\\\restrict" > fixtures/data.sql
@echo "Fixtures saved to fixtures/data.sql"
fixtures-load:
@echo "Loading fixtures from fixtures/data.sql (FK checks disabled)..."
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -c "SET session_replication_role = replica;"
-$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) < fixtures/data.sql
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -c "SET session_replication_role = DEFAULT;"
@echo "Fixtures loaded!"
fixtures-reset:
@echo "Resetting database and loading fixtures..."
$(DOCKER_COMPOSE) exec -T db psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) -c "DO \$$\$$ DECLARE r RECORD; BEGIN FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename != 'doctrine_migration_versions') LOOP EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' CASCADE'; END LOOP; END \$$\$$;"
$(MAKE) fixtures-load

35
migratebdd.md Normal file
View File

@@ -0,0 +1,35 @@
# Migration DB (manuel)
Ce guide explique comment importer un dump SQL venant de pgAdmin dans la base Docker.
## 1) Export pgAdmin
Dans pgAdmin:
- Format: Plain
- Options: Use INSERT commands + Use column inserts
- Fichier: `data.sql`
## 2) Normaliser le dump
Convertit les colonnes camelCase en lowercase compact.
```bash
python3 scripts/normalize-dump.py data.sql data_norm.sql --lower
```
## 3) Importer dans la base Docker
Utilise `session_replication_role` pour eviter les erreurs de contraintes circulaires.
```bash
docker compose --env-file docker/.env.docker.local exec -T db psql -U root -d inventory -v ON_ERROR_STOP=1 -c "SET session_replication_role = replica;"
docker compose --env-file docker/.env.docker.local exec -T db psql -U root -d inventory -v ON_ERROR_STOP=1 < data_norm.sql
docker compose --env-file docker/.env.docker.local exec -T db psql -U root -d inventory -v ON_ERROR_STOP=1 -c "SET session_replication_role = DEFAULT;"
```
## 4) Verifier
```bash
docker compose --env-file docker/.env.docker.local exec -T db psql -U $POSTGRES_USER -d $POSTGRES_DB -c "\\dt"
```

View File

@@ -1,229 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260110202855 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE composants (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, reference VARCHAR(255) DEFAULT NULL, prix NUMERIC(10, 2) DEFAULT NULL, structure JSON DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, typeComposantId VARCHAR(36) DEFAULT NULL, productId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_F95A31995E237E06 ON composants (name)');
$this->addSql('CREATE INDEX IDX_F95A3199CC8A4CEE ON composants (typeComposantId)');
$this->addSql('CREATE INDEX IDX_F95A319936799605 ON composants (productId)');
$this->addSql('CREATE TABLE _ComposantConstructeurs (A VARCHAR(36) NOT NULL, B VARCHAR(36) NOT NULL, PRIMARY KEY (A, B))');
$this->addSql('CREATE INDEX IDX_60760125D3D99E8B ON _ComposantConstructeurs (A)');
$this->addSql('CREATE INDEX IDX_607601254AD0CF31 ON _ComposantConstructeurs (B)');
$this->addSql('CREATE TABLE constructeurs (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, email VARCHAR(255) DEFAULT NULL, phone VARCHAR(255) DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_CC8D6F55E237E06 ON constructeurs (name)');
$this->addSql('CREATE TABLE custom_field_values (id VARCHAR(36) NOT NULL, value VARCHAR(255) NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, customFieldId VARCHAR(36) NOT NULL, machineId VARCHAR(36) DEFAULT NULL, composantId VARCHAR(36) DEFAULT NULL, pieceId VARCHAR(36) DEFAULT NULL, productId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_6B64D7FF5C4A705F ON custom_field_values (customFieldId)');
$this->addSql('CREATE INDEX IDX_6B64D7FF633EC4FD ON custom_field_values (machineId)');
$this->addSql('CREATE INDEX IDX_6B64D7FF345EE564 ON custom_field_values (composantId)');
$this->addSql('CREATE INDEX IDX_6B64D7FF3C6A9D1 ON custom_field_values (pieceId)');
$this->addSql('CREATE INDEX IDX_6B64D7FF36799605 ON custom_field_values (productId)');
$this->addSql('CREATE TABLE custom_fields (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, type VARCHAR(50) NOT NULL, required BOOLEAN DEFAULT false NOT NULL, defaultValue VARCHAR(255) DEFAULT NULL, options JSON NOT NULL, orderIndex INT DEFAULT 0 NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, typeMachineId VARCHAR(36) DEFAULT NULL, typeComposantId VARCHAR(36) DEFAULT NULL, typePieceId VARCHAR(36) DEFAULT NULL, typeProductId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_4A48378C2F024C2 ON custom_fields (typeMachineId)');
$this->addSql('CREATE INDEX IDX_4A48378CCC8A4CEE ON custom_fields (typeComposantId)');
$this->addSql('CREATE INDEX IDX_4A48378C169F1CF6 ON custom_fields (typePieceId)');
$this->addSql('CREATE INDEX IDX_4A48378C57B7763A ON custom_fields (typeProductId)');
$this->addSql('CREATE TABLE documents (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, filename VARCHAR(255) NOT NULL, path VARCHAR(500) NOT NULL, mimeType VARCHAR(100) NOT NULL, size INT NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, machineId VARCHAR(36) DEFAULT NULL, composantId VARCHAR(36) DEFAULT NULL, pieceId VARCHAR(36) DEFAULT NULL, productId VARCHAR(36) DEFAULT NULL, siteId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_A2B07288633EC4FD ON documents (machineId)');
$this->addSql('CREATE INDEX IDX_A2B07288345EE564 ON documents (composantId)');
$this->addSql('CREATE INDEX IDX_A2B072883C6A9D1 ON documents (pieceId)');
$this->addSql('CREATE INDEX IDX_A2B0728836799605 ON documents (productId)');
$this->addSql('CREATE INDEX IDX_A2B072886973A4FD ON documents (siteId)');
$this->addSql('CREATE TABLE machine_component_links (id VARCHAR(36) NOT NULL, nameOverride VARCHAR(255) DEFAULT NULL, referenceOverride VARCHAR(255) DEFAULT NULL, prixOverride NUMERIC(10, 2) DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, machineId VARCHAR(36) NOT NULL, composantId VARCHAR(36) NOT NULL, parentLinkId VARCHAR(36) DEFAULT NULL, typeMachineComponentRequirementId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_528EFE19633EC4FD ON machine_component_links (machineId)');
$this->addSql('CREATE INDEX IDX_528EFE19345EE564 ON machine_component_links (composantId)');
$this->addSql('CREATE INDEX IDX_528EFE19EF6CF34B ON machine_component_links (parentLinkId)');
$this->addSql('CREATE INDEX IDX_528EFE19C44B383C ON machine_component_links (typeMachineComponentRequirementId)');
$this->addSql('CREATE TABLE machine_piece_links (id VARCHAR(36) NOT NULL, nameOverride VARCHAR(255) DEFAULT NULL, referenceOverride VARCHAR(255) DEFAULT NULL, prixOverride NUMERIC(10, 2) DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, machineId VARCHAR(36) NOT NULL, pieceId VARCHAR(36) NOT NULL, parentLinkId VARCHAR(36) DEFAULT NULL, typeMachinePieceRequirementId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_62941615633EC4FD ON machine_piece_links (machineId)');
$this->addSql('CREATE INDEX IDX_629416153C6A9D1 ON machine_piece_links (pieceId)');
$this->addSql('CREATE INDEX IDX_62941615EF6CF34B ON machine_piece_links (parentLinkId)');
$this->addSql('CREATE INDEX IDX_62941615F957D314 ON machine_piece_links (typeMachinePieceRequirementId)');
$this->addSql('CREATE TABLE machine_product_links (id VARCHAR(36) NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, machineId VARCHAR(36) NOT NULL, productId VARCHAR(36) NOT NULL, typeMachineProductRequirementId VARCHAR(36) DEFAULT NULL, parentLinkId VARCHAR(36) DEFAULT NULL, parentComponentLinkId VARCHAR(36) DEFAULT NULL, parentPieceLinkId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_8CC32259633EC4FD ON machine_product_links (machineId)');
$this->addSql('CREATE INDEX IDX_8CC3225936799605 ON machine_product_links (productId)');
$this->addSql('CREATE INDEX IDX_8CC32259B590B209 ON machine_product_links (typeMachineProductRequirementId)');
$this->addSql('CREATE INDEX IDX_8CC32259EF6CF34B ON machine_product_links (parentLinkId)');
$this->addSql('CREATE INDEX IDX_8CC32259A63AC5DC ON machine_product_links (parentComponentLinkId)');
$this->addSql('CREATE INDEX IDX_8CC32259937A1D7C ON machine_product_links (parentPieceLinkId)');
$this->addSql('CREATE TABLE machines (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, reference VARCHAR(255) DEFAULT NULL, prix NUMERIC(10, 2) DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, siteId VARCHAR(36) NOT NULL, typeMachineId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_F1CE8DED5E237E06 ON machines (name)');
$this->addSql('CREATE INDEX IDX_F1CE8DED6973A4FD ON machines (siteId)');
$this->addSql('CREATE INDEX IDX_F1CE8DED2F024C2 ON machines (typeMachineId)');
$this->addSql('CREATE TABLE _MachineConstructeurs (A VARCHAR(36) NOT NULL, B VARCHAR(36) NOT NULL, PRIMARY KEY (A, B))');
$this->addSql('CREATE INDEX IDX_E6A040CCD3D99E8B ON _MachineConstructeurs (A)');
$this->addSql('CREATE INDEX IDX_E6A040CC4AD0CF31 ON _MachineConstructeurs (B)');
$this->addSql('CREATE TABLE model_types (id VARCHAR(36) NOT NULL, name VARCHAR(120) NOT NULL, code VARCHAR(60) NOT NULL, category VARCHAR(255) NOT NULL, notes TEXT DEFAULT NULL, description TEXT DEFAULT NULL, componentSkeleton JSON DEFAULT NULL, pieceSkeleton JSON DEFAULT NULL, productSkeleton JSON DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_6773A9C777153098 ON model_types (code)');
$this->addSql('CREATE UNIQUE INDEX unique_category_name ON model_types (category, name)');
$this->addSql('CREATE TABLE pieces (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, reference VARCHAR(255) DEFAULT NULL, prix NUMERIC(10, 2) DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, typePieceId VARCHAR(36) DEFAULT NULL, productId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_B92D74725E237E06 ON pieces (name)');
$this->addSql('CREATE INDEX IDX_B92D7472169F1CF6 ON pieces (typePieceId)');
$this->addSql('CREATE INDEX IDX_B92D747236799605 ON pieces (productId)');
$this->addSql('CREATE TABLE _PieceConstructeurs (A VARCHAR(36) NOT NULL, B VARCHAR(36) NOT NULL, PRIMARY KEY (A, B))');
$this->addSql('CREATE INDEX IDX_E94732E5D3D99E8B ON _PieceConstructeurs (A)');
$this->addSql('CREATE INDEX IDX_E94732E54AD0CF31 ON _PieceConstructeurs (B)');
$this->addSql('CREATE TABLE products (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, reference VARCHAR(255) DEFAULT NULL, supplierPrice NUMERIC(10, 2) DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, typeProductId VARCHAR(36) DEFAULT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_B3BA5A5A5E237E06 ON products (name)');
$this->addSql('CREATE INDEX IDX_B3BA5A5A57B7763A ON products (typeProductId)');
$this->addSql('CREATE TABLE _ProductConstructeurs (A VARCHAR(36) NOT NULL, B VARCHAR(36) NOT NULL, PRIMARY KEY (A, B))');
$this->addSql('CREATE INDEX IDX_CF7403FCD3D99E8B ON _ProductConstructeurs (A)');
$this->addSql('CREATE INDEX IDX_CF7403FC4AD0CF31 ON _ProductConstructeurs (B)');
$this->addSql('CREATE TABLE profiles (id VARCHAR(36) NOT NULL, email VARCHAR(180) DEFAULT NULL, firstName VARCHAR(100) NOT NULL, lastName VARCHAR(100) NOT NULL, isActive BOOLEAN DEFAULT true NOT NULL, roles JSON DEFAULT \'["ROLE_USER"]\' NOT NULL, password VARCHAR(255) DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_email ON profiles (email)');
$this->addSql('CREATE TABLE sites (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, contactName VARCHAR(255) DEFAULT \'\' NOT NULL, contactPhone VARCHAR(20) DEFAULT \'\' NOT NULL, contactAddress VARCHAR(500) DEFAULT \'\' NOT NULL, contactPostalCode VARCHAR(10) DEFAULT \'\' NOT NULL, contactCity VARCHAR(100) DEFAULT \'\' NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE TABLE type_machine_component_requirements (id VARCHAR(36) NOT NULL, label VARCHAR(255) DEFAULT NULL, minCount INT DEFAULT 1 NOT NULL, maxCount INT DEFAULT NULL, allowNewModels BOOLEAN DEFAULT true NOT NULL, allow_new_models BOOLEAN DEFAULT true NOT NULL, orderIndex INT DEFAULT 0 NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, typeMachineId VARCHAR(36) NOT NULL, typeComposantId VARCHAR(36) NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_969587902F024C2 ON type_machine_component_requirements (typeMachineId)');
$this->addSql('CREATE INDEX IDX_96958790CC8A4CEE ON type_machine_component_requirements (typeComposantId)');
$this->addSql('CREATE TABLE type_machine_piece_requirements (id VARCHAR(36) NOT NULL, label VARCHAR(255) DEFAULT NULL, minCount INT DEFAULT 0 NOT NULL, maxCount INT DEFAULT NULL, required BOOLEAN DEFAULT false NOT NULL, allowNewModels BOOLEAN DEFAULT true NOT NULL, orderIndex INT DEFAULT 0 NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, typeMachineId VARCHAR(36) NOT NULL, typePieceId VARCHAR(36) NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_F609E59E2F024C2 ON type_machine_piece_requirements (typeMachineId)');
$this->addSql('CREATE INDEX IDX_F609E59E169F1CF6 ON type_machine_piece_requirements (typePieceId)');
$this->addSql('CREATE TABLE type_machine_product_requirements (id VARCHAR(36) NOT NULL, label VARCHAR(255) DEFAULT NULL, minCount INT DEFAULT 0 NOT NULL, maxCount INT DEFAULT NULL, required BOOLEAN DEFAULT false NOT NULL, allowNewModels BOOLEAN DEFAULT true NOT NULL, orderIndex INT DEFAULT 0 NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, typeMachineId VARCHAR(36) NOT NULL, typeProductId VARCHAR(36) NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE INDEX IDX_29A51F982F024C2 ON type_machine_product_requirements (typeMachineId)');
$this->addSql('CREATE INDEX IDX_29A51F9857B7763A ON type_machine_product_requirements (typeProductId)');
$this->addSql('CREATE TABLE type_machines (id VARCHAR(36) NOT NULL, name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, maintenanceFrequency VARCHAR(255) DEFAULT NULL, maintenance_frequency VARCHAR(255) DEFAULT NULL, criticalParts JSON DEFAULT NULL, machinePieces JSON DEFAULT NULL, machine_pieces JSON DEFAULT NULL, specifications JSON DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY (id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_3C31AA115E237E06 ON type_machines (name)');
$this->addSql('ALTER TABLE composants ADD CONSTRAINT FK_F95A3199CC8A4CEE FOREIGN KEY (typeComposantId) REFERENCES model_types (id) NOT DEFERRABLE');
$this->addSql('ALTER TABLE composants ADD CONSTRAINT FK_F95A319936799605 FOREIGN KEY (productId) REFERENCES products (id) ON DELETE SET NULL NOT DEFERRABLE');
$this->addSql('ALTER TABLE _ComposantConstructeurs ADD CONSTRAINT FK_60760125D3D99E8B FOREIGN KEY (A) REFERENCES composants (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _ComposantConstructeurs ADD CONSTRAINT FK_607601254AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT FK_6B64D7FF5C4A705F FOREIGN KEY (customFieldId) REFERENCES custom_fields (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT FK_6B64D7FF633EC4FD FOREIGN KEY (machineId) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT FK_6B64D7FF345EE564 FOREIGN KEY (composantId) REFERENCES composants (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT FK_6B64D7FF3C6A9D1 FOREIGN KEY (pieceId) REFERENCES pieces (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_field_values ADD CONSTRAINT FK_6B64D7FF36799605 FOREIGN KEY (productId) REFERENCES products (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_fields ADD CONSTRAINT FK_4A48378C2F024C2 FOREIGN KEY (typeMachineId) REFERENCES type_machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_fields ADD CONSTRAINT FK_4A48378CCC8A4CEE FOREIGN KEY (typeComposantId) REFERENCES model_types (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_fields ADD CONSTRAINT FK_4A48378C169F1CF6 FOREIGN KEY (typePieceId) REFERENCES model_types (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE custom_fields ADD CONSTRAINT FK_4A48378C57B7763A FOREIGN KEY (typeProductId) REFERENCES model_types (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B07288633EC4FD FOREIGN KEY (machineId) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B07288345EE564 FOREIGN KEY (composantId) REFERENCES composants (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B072883C6A9D1 FOREIGN KEY (pieceId) REFERENCES pieces (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B0728836799605 FOREIGN KEY (productId) REFERENCES products (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE documents ADD CONSTRAINT FK_A2B072886973A4FD FOREIGN KEY (siteId) REFERENCES sites (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_component_links ADD CONSTRAINT FK_528EFE19633EC4FD FOREIGN KEY (machineId) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_component_links ADD CONSTRAINT FK_528EFE19345EE564 FOREIGN KEY (composantId) REFERENCES composants (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_component_links ADD CONSTRAINT FK_528EFE19EF6CF34B FOREIGN KEY (parentLinkId) REFERENCES machine_component_links (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_component_links ADD CONSTRAINT FK_528EFE19C44B383C FOREIGN KEY (typeMachineComponentRequirementId) REFERENCES type_machine_component_requirements (id) ON DELETE SET NULL NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_piece_links ADD CONSTRAINT FK_62941615633EC4FD FOREIGN KEY (machineId) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_piece_links ADD CONSTRAINT FK_629416153C6A9D1 FOREIGN KEY (pieceId) REFERENCES pieces (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_piece_links ADD CONSTRAINT FK_62941615EF6CF34B FOREIGN KEY (parentLinkId) REFERENCES machine_component_links (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_piece_links ADD CONSTRAINT FK_62941615F957D314 FOREIGN KEY (typeMachinePieceRequirementId) REFERENCES type_machine_piece_requirements (id) ON DELETE SET NULL NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC32259633EC4FD FOREIGN KEY (machineId) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC3225936799605 FOREIGN KEY (productId) REFERENCES products (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC32259B590B209 FOREIGN KEY (typeMachineProductRequirementId) REFERENCES type_machine_product_requirements (id) ON DELETE SET NULL NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC32259EF6CF34B FOREIGN KEY (parentLinkId) REFERENCES machine_product_links (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC32259A63AC5DC FOREIGN KEY (parentComponentLinkId) REFERENCES machine_component_links (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machine_product_links ADD CONSTRAINT FK_8CC32259937A1D7C FOREIGN KEY (parentPieceLinkId) REFERENCES machine_piece_links (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machines ADD CONSTRAINT FK_F1CE8DED6973A4FD FOREIGN KEY (siteId) REFERENCES sites (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE machines ADD CONSTRAINT FK_F1CE8DED2F024C2 FOREIGN KEY (typeMachineId) REFERENCES type_machines (id) NOT DEFERRABLE');
$this->addSql('ALTER TABLE _MachineConstructeurs ADD CONSTRAINT FK_E6A040CCD3D99E8B FOREIGN KEY (A) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _MachineConstructeurs ADD CONSTRAINT FK_E6A040CC4AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE pieces ADD CONSTRAINT FK_B92D7472169F1CF6 FOREIGN KEY (typePieceId) REFERENCES model_types (id) NOT DEFERRABLE');
$this->addSql('ALTER TABLE pieces ADD CONSTRAINT FK_B92D747236799605 FOREIGN KEY (productId) REFERENCES products (id) ON DELETE SET NULL NOT DEFERRABLE');
$this->addSql('ALTER TABLE _PieceConstructeurs ADD CONSTRAINT FK_E94732E5D3D99E8B FOREIGN KEY (A) REFERENCES pieces (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _PieceConstructeurs ADD CONSTRAINT FK_E94732E54AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE products ADD CONSTRAINT FK_B3BA5A5A57B7763A FOREIGN KEY (typeProductId) REFERENCES model_types (id) NOT DEFERRABLE');
$this->addSql('ALTER TABLE _ProductConstructeurs ADD CONSTRAINT FK_CF7403FCD3D99E8B FOREIGN KEY (A) REFERENCES products (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _ProductConstructeurs ADD CONSTRAINT FK_CF7403FC4AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE type_machine_component_requirements ADD CONSTRAINT FK_969587902F024C2 FOREIGN KEY (typeMachineId) REFERENCES type_machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE type_machine_component_requirements ADD CONSTRAINT FK_96958790CC8A4CEE FOREIGN KEY (typeComposantId) REFERENCES model_types (id) NOT DEFERRABLE');
$this->addSql('ALTER TABLE type_machine_piece_requirements ADD CONSTRAINT FK_F609E59E2F024C2 FOREIGN KEY (typeMachineId) REFERENCES type_machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE type_machine_piece_requirements ADD CONSTRAINT FK_F609E59E169F1CF6 FOREIGN KEY (typePieceId) REFERENCES model_types (id) NOT DEFERRABLE');
$this->addSql('ALTER TABLE type_machine_product_requirements ADD CONSTRAINT FK_29A51F982F024C2 FOREIGN KEY (typeMachineId) REFERENCES type_machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE type_machine_product_requirements ADD CONSTRAINT FK_29A51F9857B7763A FOREIGN KEY (typeProductId) REFERENCES model_types (id) NOT DEFERRABLE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE composants DROP CONSTRAINT FK_F95A3199CC8A4CEE');
$this->addSql('ALTER TABLE composants DROP CONSTRAINT FK_F95A319936799605');
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT FK_60760125D3D99E8B');
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT FK_607601254AD0CF31');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT FK_6B64D7FF5C4A705F');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT FK_6B64D7FF633EC4FD');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT FK_6B64D7FF345EE564');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT FK_6B64D7FF3C6A9D1');
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT FK_6B64D7FF36799605');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT FK_4A48378C2F024C2');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT FK_4A48378CCC8A4CEE');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT FK_4A48378C169F1CF6');
$this->addSql('ALTER TABLE custom_fields DROP CONSTRAINT FK_4A48378C57B7763A');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT FK_A2B07288633EC4FD');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT FK_A2B07288345EE564');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT FK_A2B072883C6A9D1');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT FK_A2B0728836799605');
$this->addSql('ALTER TABLE documents DROP CONSTRAINT FK_A2B072886973A4FD');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT FK_528EFE19633EC4FD');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT FK_528EFE19345EE564');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT FK_528EFE19EF6CF34B');
$this->addSql('ALTER TABLE machine_component_links DROP CONSTRAINT FK_528EFE19C44B383C');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT FK_62941615633EC4FD');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT FK_629416153C6A9D1');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT FK_62941615EF6CF34B');
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT FK_62941615F957D314');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT FK_8CC32259633EC4FD');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT FK_8CC3225936799605');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT FK_8CC32259B590B209');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT FK_8CC32259EF6CF34B');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT FK_8CC32259A63AC5DC');
$this->addSql('ALTER TABLE machine_product_links DROP CONSTRAINT FK_8CC32259937A1D7C');
$this->addSql('ALTER TABLE machines DROP CONSTRAINT FK_F1CE8DED6973A4FD');
$this->addSql('ALTER TABLE machines DROP CONSTRAINT FK_F1CE8DED2F024C2');
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT FK_E6A040CCD3D99E8B');
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT FK_E6A040CC4AD0CF31');
$this->addSql('ALTER TABLE pieces DROP CONSTRAINT FK_B92D7472169F1CF6');
$this->addSql('ALTER TABLE pieces DROP CONSTRAINT FK_B92D747236799605');
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT FK_E94732E5D3D99E8B');
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT FK_E94732E54AD0CF31');
$this->addSql('ALTER TABLE products DROP CONSTRAINT FK_B3BA5A5A57B7763A');
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT FK_CF7403FCD3D99E8B');
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT FK_CF7403FC4AD0CF31');
$this->addSql('ALTER TABLE type_machine_component_requirements DROP CONSTRAINT FK_969587902F024C2');
$this->addSql('ALTER TABLE type_machine_component_requirements DROP CONSTRAINT FK_96958790CC8A4CEE');
$this->addSql('ALTER TABLE type_machine_piece_requirements DROP CONSTRAINT FK_F609E59E2F024C2');
$this->addSql('ALTER TABLE type_machine_piece_requirements DROP CONSTRAINT FK_F609E59E169F1CF6');
$this->addSql('ALTER TABLE type_machine_product_requirements DROP CONSTRAINT FK_29A51F982F024C2');
$this->addSql('ALTER TABLE type_machine_product_requirements DROP CONSTRAINT FK_29A51F9857B7763A');
$this->addSql('DROP TABLE composants');
$this->addSql('DROP TABLE _ComposantConstructeurs');
$this->addSql('DROP TABLE constructeurs');
$this->addSql('DROP TABLE custom_field_values');
$this->addSql('DROP TABLE custom_fields');
$this->addSql('DROP TABLE documents');
$this->addSql('DROP TABLE machine_component_links');
$this->addSql('DROP TABLE machine_piece_links');
$this->addSql('DROP TABLE machine_product_links');
$this->addSql('DROP TABLE machines');
$this->addSql('DROP TABLE _MachineConstructeurs');
$this->addSql('DROP TABLE model_types');
$this->addSql('DROP TABLE pieces');
$this->addSql('DROP TABLE _PieceConstructeurs');
$this->addSql('DROP TABLE products');
$this->addSql('DROP TABLE _ProductConstructeurs');
$this->addSql('DROP TABLE profiles');
$this->addSql('DROP TABLE sites');
$this->addSql('DROP TABLE type_machine_component_requirements');
$this->addSql('DROP TABLE type_machine_piece_requirements');
$this->addSql('DROP TABLE type_machine_product_requirements');
$this->addSql('DROP TABLE type_machines');
}
}

View File

@@ -0,0 +1,891 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260125143939 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f95a3199df92e79b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f95a3199df92e79b RENAME TO IDX_F95A3199CC8A4CEE';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f95a3199a3fdb2a7') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f95a3199a3fdb2a7 RENAME TO IDX_F95A319936799605';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE _composantconstructeurs DROP CONSTRAINT IF EXISTS "_ComposantConstructeurs_A_fkey"');
$this->addSql('ALTER TABLE _composantconstructeurs DROP CONSTRAINT IF EXISTS "_ComposantConstructeurs_B_fkey"');
$this->addSql('ALTER TABLE _composantconstructeurs ALTER A TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE _composantconstructeurs ALTER B TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE _composantconstructeurs ADD CONSTRAINT FK_60760125D3D99E8B FOREIGN KEY (A) REFERENCES composants (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _composantconstructeurs ADD CONSTRAINT FK_607601254AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _composantconstructeurs ADD PRIMARY KEY (A, B)');
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_5b97d813e8b7be43') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_5b97d813e8b7be43 RENAME TO IDX_60760125D3D99E8B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('_composantconstructeurs_b_index') IS NOT NULL THEN
EXECUTE 'ALTER INDEX _composantconstructeurs_b_index RENAME TO IDX_607601254AD0CF31';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ff6736d61') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ff6736d61 RENAME TO IDX_6B64D7FF5C4A705F';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7fff6bae05f') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7fff6bae05f RENAME TO IDX_6B64D7FF633EC4FD';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ffa1dac1c6') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ffa1dac1c6 RENAME TO IDX_6B64D7FF345EE564';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ff96428d73') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ff96428d73 RENAME TO IDX_6B64D7FF3C6A9D1';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ffa3fdb2a7') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ffa3fdb2a7 RENAME TO IDX_6B64D7FF36799605';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4a48378c158582c3') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4a48378c158582c3 RENAME TO IDX_4A48378C2F024C2';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4a48378cdf92e79b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4a48378cdf92e79b RENAME TO IDX_4A48378CCC8A4CEE';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4a48378c4ca601c8') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4a48378c4ca601c8 RENAME TO IDX_4A48378C169F1CF6';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4a48378c40c2d03b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4a48378c40c2d03b RENAME TO IDX_4A48378C57B7763A';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b07288f6bae05f') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b07288f6bae05f RENAME TO IDX_A2B07288633EC4FD';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b07288a1dac1c6') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b07288a1dac1c6 RENAME TO IDX_A2B07288345EE564';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b0728896428d73') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b0728896428d73 RENAME TO IDX_A2B072883C6A9D1';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b07288a3fdb2a7') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b07288a3fdb2a7 RENAME TO IDX_A2B0728836799605';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b07288fcf7805f') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b07288fcf7805f RENAME TO IDX_A2B072886973A4FD';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_528efe19f6bae05f') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_528efe19f6bae05f RENAME TO IDX_528EFE19633EC4FD';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_528efe19a1dac1c6') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_528efe19a1dac1c6 RENAME TO IDX_528EFE19345EE564';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_528efe197d44d2df') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_528efe197d44d2df RENAME TO IDX_528EFE19EF6CF34B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_528efe19bcced9e3') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_528efe19bcced9e3 RENAME TO IDX_528EFE19C44B383C';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_62941615f6bae05f') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_62941615f6bae05f RENAME TO IDX_62941615633EC4FD';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6294161596428d73') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6294161596428d73 RENAME TO IDX_629416153C6A9D1';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_629416157d44d2df') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_629416157d44d2df RENAME TO IDX_62941615EF6CF34B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6294161532c54aaf') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6294161532c54aaf RENAME TO IDX_62941615F957D314';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('machine_product_links_machineid_idx') IS NOT NULL THEN
EXECUTE 'ALTER INDEX machine_product_links_machineid_idx RENAME TO IDX_8CC32259633EC4FD';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('machine_product_links_productid_idx') IS NOT NULL THEN
EXECUTE 'ALTER INDEX machine_product_links_productid_idx RENAME TO IDX_8CC3225936799605';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc32259357fdbff') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc32259357fdbff RENAME TO IDX_8CC32259B590B209';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc322597d44d2df') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc322597d44d2df RENAME TO IDX_8CC32259EF6CF34B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc32259bcd7dad6') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc32259bcd7dad6 RENAME TO IDX_8CC32259A63AC5DC';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc3225987ceb33f') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc3225987ceb33f RENAME TO IDX_8CC32259937A1D7C';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f1ce8dedfcf7805f') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f1ce8dedfcf7805f RENAME TO IDX_F1CE8DED6973A4FD';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f1ce8ded158582c3') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f1ce8ded158582c3 RENAME TO IDX_F1CE8DED2F024C2';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE _machineconstructeurs DROP CONSTRAINT IF EXISTS "_MachineConstructeurs_B_fkey"');
$this->addSql('ALTER TABLE _machineconstructeurs DROP CONSTRAINT IF EXISTS "_MachineConstructeurs_A_fkey"');
$this->addSql('ALTER TABLE _machineconstructeurs ALTER A TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE _machineconstructeurs ALTER B TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE _machineconstructeurs ADD CONSTRAINT FK_E6A040CCD3D99E8B FOREIGN KEY (A) REFERENCES machines (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _machineconstructeurs ADD CONSTRAINT FK_E6A040CC4AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _machineconstructeurs ADD PRIMARY KEY (A, B)');
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4f225b32e8b7be43') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4f225b32e8b7be43 RENAME TO IDX_E6A040CCD3D99E8B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('_machineconstructeurs_b_index') IS NOT NULL THEN
EXECUTE 'ALTER INDEX _machineconstructeurs_b_index RENAME TO IDX_E6A040CC4AD0CF31';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE model_types DROP CONSTRAINT IF EXISTS "ModelType_category_name_key"');
$this->addSql('ALTER TABLE model_types DROP CONSTRAINT IF EXISTS "ModelType_code_key"');
$this->addSql('ALTER TABLE model_types ALTER id TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE model_types ALTER category TYPE VARCHAR(255)');
$this->addSql('ALTER TABLE model_types ALTER createdAt DROP DEFAULT');
$this->addSql('ALTER TABLE model_types ALTER componentSkeleton TYPE JSON');
$this->addSql('ALTER TABLE model_types ALTER pieceSkeleton TYPE JSON');
$this->addSql('ALTER TABLE model_types ALTER productSkeleton TYPE JSON');
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_b92d74724ca601c8') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_b92d74724ca601c8 RENAME TO IDX_B92D7472169F1CF6';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_b92d7472a3fdb2a7') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_b92d7472a3fdb2a7 RENAME TO IDX_B92D747236799605';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE _piececonstructeurs DROP CONSTRAINT IF EXISTS "_PieceConstructeurs_A_fkey"');
$this->addSql('ALTER TABLE _piececonstructeurs DROP CONSTRAINT IF EXISTS "_PieceConstructeurs_B_fkey"');
$this->addSql('ALTER TABLE _piececonstructeurs ALTER A TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE _piececonstructeurs ALTER B TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE _piececonstructeurs ADD CONSTRAINT FK_E94732E5D3D99E8B FOREIGN KEY (A) REFERENCES pieces (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _piececonstructeurs ADD CONSTRAINT FK_E94732E54AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _piececonstructeurs ADD PRIMARY KEY (A, B)');
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_77fc120e8b7be43') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_77fc120e8b7be43 RENAME TO IDX_E94732E5D3D99E8B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('_piececonstructeurs_b_index') IS NOT NULL THEN
EXECUTE 'ALTER INDEX _piececonstructeurs_b_index RENAME TO IDX_E94732E54AD0CF31';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_b3ba5a5a40c2d03b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_b3ba5a5a40c2d03b RENAME TO IDX_B3BA5A5A57B7763A';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE _productconstructeurs DROP CONSTRAINT IF EXISTS "_ProductConstructeurs_B_fkey"');
$this->addSql('ALTER TABLE _productconstructeurs DROP CONSTRAINT IF EXISTS "_ProductConstructeurs_A_fkey"');
$this->addSql('ALTER TABLE _productconstructeurs ALTER A TYPE VARCHAR(36)');
$this->addSql('ALTER TABLE _productconstructeurs ALTER B TYPE VARCHAR(36)');
// Clean orphaned relations before re-adding foreign keys.
$this->addSql('DELETE FROM _productconstructeurs WHERE A IS NULL OR B IS NULL');
$this->addSql('DELETE FROM _productconstructeurs pc WHERE NOT EXISTS (SELECT 1 FROM products p WHERE p.id = pc.A)');
$this->addSql('DELETE FROM _productconstructeurs pc WHERE NOT EXISTS (SELECT 1 FROM constructeurs c WHERE c.id = pc.B)');
$this->addSql('ALTER TABLE _productconstructeurs ADD CONSTRAINT FK_CF7403FCD3D99E8B FOREIGN KEY (A) REFERENCES products (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _productconstructeurs ADD CONSTRAINT FK_CF7403FC4AD0CF31 FOREIGN KEY (B) REFERENCES constructeurs (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE _productconstructeurs ADD PRIMARY KEY (A, B)');
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_66f61802e8b7be43') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_66f61802e8b7be43 RENAME TO IDX_CF7403FCD3D99E8B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('_productconstructeurs_b_index') IS NOT NULL THEN
EXECUTE 'ALTER INDEX _productconstructeurs_b_index RENAME TO IDX_CF7403FC4AD0CF31';
END IF;
END $$;
SQL
);
$this->addSql('DROP INDEX IF EXISTS uniq_profiles_email');
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_96958790158582c3') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_96958790158582c3 RENAME TO IDX_969587902F024C2';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_96958790df92e79b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_96958790df92e79b RENAME TO IDX_96958790CC8A4CEE';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f609e59e158582c3') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f609e59e158582c3 RENAME TO IDX_F609E59E2F024C2';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f609e59e4ca601c8') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f609e59e4ca601c8 RENAME TO IDX_F609E59E169F1CF6';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_29a51f98158582c3') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_29a51f98158582c3 RENAME TO IDX_29A51F982F024C2';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_29a51f9840c2d03b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_29a51f9840c2d03b RENAME TO IDX_29A51F9857B7763A';
END IF;
END $$;
SQL
);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT IF EXISTS FK_60760125D3D99E8B');
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT IF EXISTS FK_607601254AD0CF31');
$this->addSql('ALTER TABLE _ComposantConstructeurs DROP CONSTRAINT IF EXISTS _ComposantConstructeurs_pkey');
$this->addSql('ALTER TABLE _ComposantConstructeurs ALTER a TYPE TEXT');
$this->addSql('ALTER TABLE _ComposantConstructeurs ALTER b TYPE TEXT');
$this->addSql('ALTER TABLE _ComposantConstructeurs ADD CONSTRAINT "_ComposantConstructeurs_A_fkey" FOREIGN KEY (a) REFERENCES composants (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE _ComposantConstructeurs ADD CONSTRAINT "_ComposantConstructeurs_B_fkey" FOREIGN KEY (b) REFERENCES constructeurs (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_607601254ad0cf31') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_607601254ad0cf31 RENAME TO "_ComposantConstructeurs_B_index"';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_60760125d3d99e8b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_60760125d3d99e8b RENAME TO IDX_5B97D813E8B7BE43';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT IF EXISTS FK_E6A040CCD3D99E8B');
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT IF EXISTS FK_E6A040CC4AD0CF31');
$this->addSql('ALTER TABLE _MachineConstructeurs DROP CONSTRAINT IF EXISTS _MachineConstructeurs_pkey');
$this->addSql('ALTER TABLE _MachineConstructeurs ALTER a TYPE TEXT');
$this->addSql('ALTER TABLE _MachineConstructeurs ALTER b TYPE TEXT');
$this->addSql('ALTER TABLE _MachineConstructeurs ADD CONSTRAINT "_MachineConstructeurs_B_fkey" FOREIGN KEY (b) REFERENCES constructeurs (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE _MachineConstructeurs ADD CONSTRAINT "_MachineConstructeurs_A_fkey" FOREIGN KEY (a) REFERENCES machines (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_e6a040cc4ad0cf31') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_e6a040cc4ad0cf31 RENAME TO "_MachineConstructeurs_B_index"';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_e6a040ccd3d99e8b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_e6a040ccd3d99e8b RENAME TO IDX_4F225B32E8B7BE43';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT IF EXISTS FK_E94732E5D3D99E8B');
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT IF EXISTS FK_E94732E54AD0CF31');
$this->addSql('ALTER TABLE _PieceConstructeurs DROP CONSTRAINT IF EXISTS _PieceConstructeurs_pkey');
$this->addSql('ALTER TABLE _PieceConstructeurs ALTER a TYPE TEXT');
$this->addSql('ALTER TABLE _PieceConstructeurs ALTER b TYPE TEXT');
$this->addSql('ALTER TABLE _PieceConstructeurs ADD CONSTRAINT "_PieceConstructeurs_A_fkey" FOREIGN KEY (a) REFERENCES pieces (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE _PieceConstructeurs ADD CONSTRAINT "_PieceConstructeurs_B_fkey" FOREIGN KEY (b) REFERENCES constructeurs (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_e94732e54ad0cf31') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_e94732e54ad0cf31 RENAME TO "_PieceConstructeurs_B_index"';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_e94732e5d3d99e8b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_e94732e5d3d99e8b RENAME TO IDX_77FC120E8B7BE43';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT IF EXISTS FK_CF7403FCD3D99E8B');
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT IF EXISTS FK_CF7403FC4AD0CF31');
$this->addSql('ALTER TABLE _ProductConstructeurs DROP CONSTRAINT IF EXISTS _ProductConstructeurs_pkey');
$this->addSql('ALTER TABLE _ProductConstructeurs ALTER a TYPE TEXT');
$this->addSql('ALTER TABLE _ProductConstructeurs ALTER b TYPE TEXT');
$this->addSql('ALTER TABLE _ProductConstructeurs ADD CONSTRAINT "_ProductConstructeurs_B_fkey" FOREIGN KEY (b) REFERENCES products (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE _ProductConstructeurs ADD CONSTRAINT "_ProductConstructeurs_A_fkey" FOREIGN KEY (a) REFERENCES constructeurs (id) ON UPDATE CASCADE ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_cf7403fc4ad0cf31') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_cf7403fc4ad0cf31 RENAME TO "_ProductConstructeurs_B_index"';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_cf7403fcd3d99e8b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_cf7403fcd3d99e8b RENAME TO IDX_66F61802E8B7BE43';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f95a319936799605') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f95a319936799605 RENAME TO IDX_F95A3199A3FDB2A7';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f95a3199cc8a4cee') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f95a3199cc8a4cee RENAME TO IDX_F95A3199DF92E79B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ff345ee564') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ff345ee564 RENAME TO IDX_6B64D7FFA1DAC1C6';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ff5c4a705f') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ff5c4a705f RENAME TO IDX_6B64D7FF6736D61';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ff633ec4fd') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ff633ec4fd RENAME TO IDX_6B64D7FFF6BAE05F';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ff3c6a9d1') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ff3c6a9d1 RENAME TO IDX_6B64D7FF96428D73';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_6b64d7ff36799605') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_6b64d7ff36799605 RENAME TO IDX_6B64D7FFA3FDB2A7';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4a48378c57b7763a') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4a48378c57b7763a RENAME TO IDX_4A48378C40C2D03B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4a48378c2f024c2') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4a48378c2f024c2 RENAME TO IDX_4A48378C158582C3';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4a48378c169f1cf6') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4a48378c169f1cf6 RENAME TO IDX_4A48378C4CA601C8';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_4a48378ccc8a4cee') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_4a48378ccc8a4cee RENAME TO IDX_4A48378CDF92E79B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b07288345ee564') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b07288345ee564 RENAME TO IDX_A2B07288A1DAC1C6';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b07288633ec4fd') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b07288633ec4fd RENAME TO IDX_A2B07288F6BAE05F';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b072886973a4fd') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b072886973a4fd RENAME TO IDX_A2B07288FCF7805F';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b072883c6a9d1') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b072883c6a9d1 RENAME TO IDX_A2B0728896428D73';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_a2b0728836799605') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_a2b0728836799605 RENAME TO IDX_A2B07288A3FDB2A7';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_528efe19345ee564') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_528efe19345ee564 RENAME TO IDX_528EFE19A1DAC1C6';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_528efe19633ec4fd') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_528efe19633ec4fd RENAME TO IDX_528EFE19F6BAE05F';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_528efe19ef6cf34b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_528efe19ef6cf34b RENAME TO IDX_528EFE197D44D2DF';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_528efe19c44b383c') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_528efe19c44b383c RENAME TO IDX_528EFE19BCCED9E3';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_62941615ef6cf34b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_62941615ef6cf34b RENAME TO IDX_629416157D44D2DF';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_62941615633ec4fd') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_62941615633ec4fd RENAME TO IDX_62941615F6BAE05F';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_629416153c6a9d1') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_629416153c6a9d1 RENAME TO IDX_6294161596428D73';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_62941615f957d314') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_62941615f957d314 RENAME TO IDX_6294161532C54AAF';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc32259633ec4fd') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc32259633ec4fd RENAME TO "machine_product_links_machineId_idx"';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc3225936799605') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc3225936799605 RENAME TO "machine_product_links_productId_idx"';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc32259ef6cf34b') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc32259ef6cf34b RENAME TO IDX_8CC322597D44D2DF';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc32259b590b209') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc32259b590b209 RENAME TO IDX_8CC32259357FDBFF';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc32259a63ac5dc') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc32259a63ac5dc RENAME TO IDX_8CC32259BCD7DAD6';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_8cc32259937a1d7c') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_8cc32259937a1d7c RENAME TO IDX_8CC3225987CEB33F';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f1ce8ded2f024c2') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f1ce8ded2f024c2 RENAME TO IDX_F1CE8DED158582C3';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f1ce8ded6973a4fd') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f1ce8ded6973a4fd RENAME TO IDX_F1CE8DEDFCF7805F';
END IF;
END $$;
SQL
);
$this->addSql('ALTER TABLE model_types ALTER id TYPE TEXT');
$this->addSql('ALTER TABLE model_types ALTER category TYPE VARCHAR');
$this->addSql('ALTER TABLE model_types ALTER componentskeleton TYPE JSONB');
$this->addSql('ALTER TABLE model_types ALTER pieceskeleton TYPE JSONB');
$this->addSql('ALTER TABLE model_types ALTER productskeleton TYPE JSONB');
$this->addSql('ALTER TABLE model_types ALTER createdat SET DEFAULT CURRENT_TIMESTAMP');
$this->addSql('CREATE UNIQUE INDEX "ModelType_category_name_key" ON model_types (category, name)');
$this->addSql('CREATE UNIQUE INDEX "ModelType_code_key" ON model_types (code)');
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_b92d7472169f1cf6') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_b92d7472169f1cf6 RENAME TO IDX_B92D74724CA601C8';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_b92d747236799605') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_b92d747236799605 RENAME TO IDX_B92D7472A3FDB2A7';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_b3ba5a5a57b7763a') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_b3ba5a5a57b7763a RENAME TO IDX_B3BA5A5A40C2D03B';
END IF;
END $$;
SQL
);
$this->addSql('CREATE UNIQUE INDEX uniq_profiles_email ON profiles (email)');
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_969587902f024c2') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_969587902f024c2 RENAME TO IDX_96958790158582C3';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_96958790cc8a4cee') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_96958790cc8a4cee RENAME TO IDX_96958790DF92E79B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f609e59e169f1cf6') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f609e59e169f1cf6 RENAME TO IDX_F609E59E4CA601C8';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_f609e59e2f024c2') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_f609e59e2f024c2 RENAME TO IDX_F609E59E158582C3';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_29a51f9857b7763a') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_29a51f9857b7763a RENAME TO IDX_29A51F9840C2D03B';
END IF;
END $$;
SQL
);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF to_regclass('idx_29a51f982f024c2') IS NOT NULL THEN
EXECUTE 'ALTER INDEX idx_29a51f982f024c2 RENAME TO IDX_29A51F98158582C3';
END IF;
END $$;
SQL
);
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260125170000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add audit_logs table to store per-entity history entries.';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
CREATE TABLE audit_logs (
id VARCHAR(36) NOT NULL,
entityType VARCHAR(50) NOT NULL,
entityId VARCHAR(36) NOT NULL,
action VARCHAR(20) NOT NULL,
diff JSON DEFAULT NULL,
snapshot JSON DEFAULT NULL,
actorProfileId VARCHAR(36) DEFAULT NULL,
createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
PRIMARY KEY(id)
)
SQL);
$this->addSql('CREATE INDEX idx_audit_entity ON audit_logs (entityType, entityId)');
$this->addSql('CREATE INDEX idx_audit_created_at ON audit_logs (createdAt)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP TABLE audit_logs');
}
}

View File

@@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20261120120000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add email column to profiles when missing.';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE profiles ADD COLUMN IF NOT EXISTS email VARCHAR(180) DEFAULT NULL');
$this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS uniq_profiles_email ON profiles (email)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX IF EXISTS uniq_profiles_email');
$this->addSql('ALTER TABLE profiles DROP COLUMN IF EXISTS email');
}
}

View File

@@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20261120123000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Ensure profiles.email exists (camelCase schema).';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS email VARCHAR(180) DEFAULT NULL');
$this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS uniq_profiles_email ON public.profiles (email)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX IF EXISTS uniq_profiles_email');
$this->addSql('ALTER TABLE public.profiles DROP COLUMN IF EXISTS email');
}
}

View File

@@ -1,94 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20261120124000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Normalize profiles columns to camelCase (quoted identifiers).';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'firstname'
) THEN
ALTER TABLE public.profiles RENAME COLUMN firstname TO "firstName";
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'lastname'
) THEN
ALTER TABLE public.profiles RENAME COLUMN lastname TO "lastName";
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'isactive'
) THEN
ALTER TABLE public.profiles RENAME COLUMN isactive TO "isActive";
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'createdat'
) THEN
ALTER TABLE public.profiles RENAME COLUMN createdat TO "createdAt";
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'updatedat'
) THEN
ALTER TABLE public.profiles RENAME COLUMN updatedat TO "updatedAt";
END IF;
END $$;
SQL);
}
public function down(Schema $schema): void
{
$this->addSql(<<<'SQL'
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'firstName'
) THEN
ALTER TABLE public.profiles RENAME COLUMN "firstName" TO firstname;
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'lastName'
) THEN
ALTER TABLE public.profiles RENAME COLUMN "lastName" TO lastname;
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'isActive'
) THEN
ALTER TABLE public.profiles RENAME COLUMN "isActive" TO isactive;
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'createdAt'
) THEN
ALTER TABLE public.profiles RENAME COLUMN "createdAt" TO createdat;
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'updatedAt'
) THEN
ALTER TABLE public.profiles RENAME COLUMN "updatedAt" TO updatedat;
END IF;
END $$;
SQL);
}
}

View File

@@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20261120125000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add missing profile auth columns (email, roles, password).';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS email VARCHAR(180) DEFAULT NULL');
$this->addSql('ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS roles JSON DEFAULT \'["ROLE_USER"]\' NOT NULL');
$this->addSql('ALTER TABLE public.profiles ADD COLUMN IF NOT EXISTS password VARCHAR(255) DEFAULT NULL');
$this->addSql('CREATE UNIQUE INDEX IF NOT EXISTS uniq_profiles_email ON public.profiles (email)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX IF EXISTS uniq_profiles_email');
$this->addSql('ALTER TABLE public.profiles DROP COLUMN IF EXISTS password');
$this->addSql('ALTER TABLE public.profiles DROP COLUMN IF EXISTS roles');
$this->addSql('ALTER TABLE public.profiles DROP COLUMN IF EXISTS email');
}
}

View File

@@ -1,66 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20261120131000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Normalize public schema identifiers to lowercase (tables + columns).';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
DO $$
DECLARE
r RECORD;
BEGIN
-- Special-case legacy table name from Prisma.
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'ModelType'
) THEN
EXECUTE 'ALTER TABLE public."ModelType" RENAME TO model_types';
END IF;
-- Rename columns containing uppercase letters.
FOR r IN
SELECT table_name, column_name
FROM information_schema.columns
WHERE table_schema = 'public' AND column_name ~ '[A-Z]'
LOOP
EXECUTE format(
'ALTER TABLE public.%I RENAME COLUMN %I TO %I',
r.table_name,
r.column_name,
lower(r.column_name)
);
END LOOP;
-- Rename tables containing uppercase letters.
FOR r IN
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public' AND table_name ~ '[A-Z]'
LOOP
EXECUTE format(
'ALTER TABLE public.%I RENAME TO %I',
r.table_name,
lower(r.table_name)
);
END LOOP;
END $$;
SQL);
}
public function down(Schema $schema): void
{
// Irreversible: cannot restore original casing reliably.
}
}

View File

@@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20261120140000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Convert custom_fields.options from text[] to json.';
}
public function up(Schema $schema): void
{
$this->addSql("ALTER TABLE custom_fields ALTER COLUMN options TYPE JSON USING to_json(options)");
}
public function down(Schema $schema): void
{
$this->addSql("ALTER TABLE custom_fields ALTER COLUMN options TYPE TEXT[] USING ARRAY(SELECT json_array_elements_text(options))");
}
}

View File

@@ -0,0 +1,32 @@
DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN
SELECT table_schema, table_name, column_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND column_name <> lower(column_name)
ORDER BY table_name, column_name
LOOP
-- Skip if a lowercase version already exists to avoid collisions.
IF EXISTS (
SELECT 1
FROM information_schema.columns c
WHERE c.table_schema = r.table_schema
AND c.table_name = r.table_name
AND c.column_name = lower(r.column_name)
) THEN
RAISE NOTICE 'Skip %.%: % -> % (target exists)', r.table_name, r.column_name, r.column_name, lower(r.column_name);
ELSE
EXECUTE format(
'ALTER TABLE %I.%I RENAME COLUMN %I TO %I',
r.table_schema,
r.table_name,
r.column_name,
lower(r.column_name)
);
RAISE NOTICE 'Renamed %.%: % -> %', r.table_name, r.column_name, r.column_name, lower(r.column_name);
END IF;
END LOOP;
END $$;

View File

@@ -1,74 +1,171 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import re
import sys import sys
INSERT_RE = re.compile(
r"(?P<prefix>INSERT\s+INTO\s+[^;]*?\()(?P<cols>[^)]*)(?P<suffix>\)\s+VALUES)",
re.IGNORECASE | re.DOTALL,
)
TABLE_RE = re.compile(
r"(?P<before>INSERT\s+INTO\s+)(?P<table>(?:\"[^\"]+\"\.|[A-Za-z_][\w$]*\.)?\"[^\"]+\"|(?:\"[^\"]+\"\.|[A-Za-z_][\w$]*\.)?[A-Za-z_][\w$]*)",
re.IGNORECASE,
)
CREATE_DB_RE = re.compile(r"^CREATE\s+DATABASE\s+.+?;$", re.IGNORECASE | re.MULTILINE)
CONNECT_RE = re.compile(r"^\\connect\\s+.+?$", re.IGNORECASE | re.MULTILINE)
TABLE_NAME_MAP = { TABLE_NAME_MAP = {
"ModelType": "model_types", "ModelType": "model_types",
"TypeMachine": "type_machines",
"TypeMachineComponentRequirement": "type_machine_component_requirements",
"TypeMachinePieceRequirement": "type_machine_piece_requirements",
"TypeMachineProductRequirement": "type_machine_product_requirements",
"MachinePieceLink": "machine_piece_links",
"MachineComponentLink": "machine_component_links",
"MachineProductLink": "machine_product_links",
"Machine": "machines",
"Product": "products",
"Piece": "pieces",
"Composant": "composants",
"Profile": "profiles",
"CustomField": "custom_fields",
"CustomFieldValue": "custom_field_values",
"Document": "documents",
"Constructeur": "constructeurs",
"Site": "sites",
} }
def normalize_identifiers(sql: str) -> str: SKIP_TABLES = {
"_prisma_migrations",
}
def to_snake(name: str) -> str:
out = [] out = []
i = 0 length = len(name)
in_single = False for i, ch in enumerate(name):
length = len(sql) if ch.isupper():
prev = name[i - 1] if i > 0 else ""
while i < length: nxt = name[i + 1] if i + 1 < length else ""
ch = sql[i] if i > 0 and (prev.islower() or prev.isdigit() or (prev.isupper() and nxt.islower())):
out.append("_")
if in_single: out.append(ch.lower())
if ch == "'":
if i + 1 < length and sql[i + 1] == "'":
out.append("''")
i += 2
continue
in_single = False
out.append(ch)
i += 1
continue
if ch == "'":
in_single = True
out.append(ch)
i += 1
continue
if ch == '"':
i += 1
ident = []
while i < length:
if sql[i] == '"':
break
ident.append(sql[i])
i += 1
ident_str = ''.join(ident)
mapped = TABLE_NAME_MAP.get(ident_str)
if mapped is not None:
out.append(mapped)
else: else:
out.append(ident_str.lower())
if i < length and sql[i] == '"':
i += 1
continue
out.append(ch) out.append(ch)
i += 1 return "".join(out)
return ''.join(out)
def to_lower_compact(name: str) -> str:
return name.replace("_", "").lower()
def remap_table(ident: str, mode: str) -> str:
mapped = TABLE_NAME_MAP.get(ident)
if mapped is not None:
return mapped
if mode == "snake":
return to_snake(ident)
if mode == "lower":
return to_lower_compact(ident)
raise ValueError(f"Unsupported mode: {mode}")
def extract_table_ident(prefix: str) -> str | None:
match = TABLE_RE.search(prefix)
if not match:
return None
table = match.group("table")
# Handle quoted schema like "public"."TableName"
if '"."' in table:
parts = table.split('"."', 1)
ident = parts[1].strip('"')
elif "." in table:
_, ident = table.split(".", 1)
ident = ident.strip('"')
else:
ident = table.strip('"')
return ident
def normalize_table_name(prefix: str, mode: str) -> str:
def repl(match: re.Match[str]) -> str:
table = match.group("table")
schema = ""
ident = table
# Handle quoted schema like "public"."TableName"
if '"."' in table:
parts = table.split('"."', 1)
schema_name = parts[0].strip('"')
ident = parts[1].strip('"')
schema = f'"{schema_name}".'
elif "." in table:
schema_part, ident = table.split(".", 1)
schema_name = schema_part.strip('"')
schema = f'"{schema_name}".'
ident = ident.strip('"')
else:
ident = table.strip('"')
mapped = remap_table(ident, mode)
return f'{match.group("before")}{schema}"{mapped}"'
return TABLE_RE.sub(repl, prefix)
def remap_columns(cols: str, mode: str) -> str:
def repl(match: re.Match[str]) -> str:
name = match.group(1)
if mode == "snake":
if any(ch.isupper() for ch in name):
return f"\"{to_snake(name)}\""
return match.group(0)
if mode == "lower":
return f"\"{to_lower_compact(name)}\""
raise ValueError(f"Unsupported mode: {mode}")
return re.sub(r"\"([^\"]+)\"", repl, cols)
def normalize_dump(sql: str, mode: str) -> str:
sql = CREATE_DB_RE.sub("", sql)
sql = CONNECT_RE.sub("", sql)
def repl(match: re.Match[str]) -> str:
raw_prefix = match.group("prefix")
ident = extract_table_ident(raw_prefix)
if ident is not None:
mapped = remap_table(ident, mode)
if ident in SKIP_TABLES or mapped in SKIP_TABLES:
return ""
prefix = normalize_table_name(raw_prefix, mode)
cols = remap_columns(match.group("cols"), mode)
return f"{prefix}{cols}{match.group('suffix')}"
return INSERT_RE.sub(repl, sql)
def main() -> int: def main() -> int:
if len(sys.argv) != 3: if len(sys.argv) not in (3, 4):
print("Usage: scripts/normalize-dump.py <input.sql> <output.sql>", file=sys.stderr) print("Usage: scripts/normalize-dump.py <input.sql> <output.sql> [--snake|--lower]", file=sys.stderr)
return 1 return 1
src = sys.argv[1] src = sys.argv[1]
dst = sys.argv[2] dst = sys.argv[2]
mode = "lower"
if len(sys.argv) == 4:
if sys.argv[3] == "--snake":
mode = "snake"
elif sys.argv[3] == "--lower":
mode = "lower"
else:
print("Invalid mode. Use --snake or --lower.", file=sys.stderr)
return 1
with open(src, "r", encoding="utf-8") as f: with open(src, "r", encoding="utf-8") as f:
data = f.read() data = f.read()
normalized = normalize_identifiers(data) normalized = normalize_dump(data, mode)
with open(dst, "w", encoding="utf-8") as f: with open(dst, "w", encoding="utf-8") as f:
f.write(normalized) f.write(normalized)

195
scripts/release.sh Executable file
View File

@@ -0,0 +1,195 @@
#!/bin/bash
set -e
# Couleurs pour l'affichage
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Répertoire racine du projet
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
VERSION_FILE="$PROJECT_ROOT/VERSION"
API_PLATFORM_FILE="$PROJECT_ROOT/config/packages/api_platform.yaml"
FRONTEND_DIR="$PROJECT_ROOT/Inventory_frontend"
# Lire la version actuelle
current_version=$(cat "$VERSION_FILE" | tr -d '\n')
# Fonction pour afficher l'aide
show_help() {
echo -e "${BLUE}Usage:${NC} $0 [version|bump_type]"
echo ""
echo "Arguments:"
echo " version Version spécifique (ex: 1.2.3)"
echo " bump_type Type de bump: major, minor, patch"
echo ""
echo "Exemples:"
echo " $0 1.0.0 # Définit la version à 1.0.0"
echo " $0 patch # 1.0.0 -> 1.0.1"
echo " $0 minor # 1.0.0 -> 1.1.0"
echo " $0 major # 1.0.0 -> 2.0.0"
echo ""
echo -e "Version actuelle: ${GREEN}$current_version${NC}"
}
# Fonction pour bumper la version
bump_version() {
local version=$1
local bump_type=$2
IFS='.' read -r major minor patch <<< "$version"
case $bump_type in
major)
major=$((major + 1))
minor=0
patch=0
;;
minor)
minor=$((minor + 1))
patch=0
;;
patch)
patch=$((patch + 1))
;;
esac
echo "$major.$minor.$patch"
}
# Vérifier les arguments
if [ $# -eq 0 ]; then
show_help
exit 0
fi
arg=$1
# Déterminer la nouvelle version
case $arg in
major|minor|patch)
new_version=$(bump_version "$current_version" "$arg")
;;
-h|--help|help)
show_help
exit 0
;;
*)
# Vérifier le format de version (semver basique)
if [[ $arg =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
new_version=$arg
else
echo -e "${RED}Erreur:${NC} Format de version invalide: $arg"
echo "Utilisez le format X.Y.Z (ex: 1.2.3) ou major/minor/patch"
exit 1
fi
;;
esac
echo -e "${BLUE}Release v$new_version${NC}"
echo "================================"
echo -e "Version actuelle: ${YELLOW}$current_version${NC}"
echo -e "Nouvelle version: ${GREEN}$new_version${NC}"
echo ""
# Vérifier qu'on n'est pas sur une version identique
if [ "$current_version" = "$new_version" ]; then
echo -e "${YELLOW}La version est déjà $new_version${NC}"
exit 0
fi
# Demander confirmation
read -p "Continuer ? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Annulé."
exit 0
fi
cd "$PROJECT_ROOT"
# ===========================================
# ÉTAPE 1 : Gérer le submodule frontend
# ===========================================
echo ""
echo -e "${BLUE}[1/6]${NC} Vérification du submodule frontend..."
cd "$FRONTEND_DIR"
# Vérifier s'il y a des changements non commités dans le submodule
if ! git diff --quiet --exit-code || ! git diff --cached --quiet --exit-code; then
echo -e "${YELLOW}Changements détectés dans le submodule frontend${NC}"
git status --short
echo ""
read -p "Commiter ces changements dans le submodule ? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
git add -A
git commit -m "chore(release) : prepare v$new_version"
else
echo -e "${RED}Erreur:${NC} Veuillez d'abord commiter les changements du submodule."
exit 1
fi
fi
# ===========================================
# ÉTAPE 2 : Tag le submodule
# ===========================================
echo -e "${BLUE}[2/6]${NC} Création du tag v$new_version dans le submodule..."
# Vérifier si le tag existe déjà
if git rev-parse "v$new_version" >/dev/null 2>&1; then
echo -e "${YELLOW}Le tag v$new_version existe déjà dans le submodule${NC}"
else
git tag -a "v$new_version" -m "Release v$new_version"
echo -e "${GREEN}Tag v$new_version créé dans le submodule${NC}"
fi
# Retourner au projet principal
cd "$PROJECT_ROOT"
# ===========================================
# ÉTAPE 3 : Mettre à jour VERSION
# ===========================================
echo -e "${BLUE}[3/6]${NC} Mise à jour du fichier VERSION..."
echo "$new_version" > "$VERSION_FILE"
# ===========================================
# ÉTAPE 4 : Mettre à jour api_platform.yaml
# ===========================================
echo -e "${BLUE}[4/6]${NC} Mise à jour de api_platform.yaml..."
sed -i "s/version: .*/version: $new_version/" "$API_PLATFORM_FILE"
# ===========================================
# ÉTAPE 5 : Commit principal (avec mise à jour du submodule)
# ===========================================
echo -e "${BLUE}[5/6]${NC} Création du commit principal..."
git add "$VERSION_FILE" "$API_PLATFORM_FILE" "$FRONTEND_DIR"
git commit -m "chore(release) : v$new_version"
# ===========================================
# ÉTAPE 6 : Tag principal
# ===========================================
echo -e "${BLUE}[6/6]${NC} Création du tag v$new_version..."
git tag -a "v$new_version" -m "Release v$new_version"
echo ""
echo -e "${GREEN}✓ Release v$new_version préparée avec succès !${NC}"
echo ""
echo "================================"
echo -e "${YELLOW}Prochaines étapes :${NC}"
echo ""
echo "1. Pousser le submodule frontend :"
echo -e " ${BLUE}cd Inventory_frontend && git push && git push --tags && cd ..${NC}"
echo ""
echo "2. Pousser le projet principal :"
echo -e " ${BLUE}git push && git push --tags${NC}"
echo ""
echo "3. Créer les releases sur Gitea :"
echo " - Inventory_frontend : tag v$new_version"
echo " - Inventory (backend) : tag v$new_version"
echo ""
echo "================================"

View File

@@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Repository\DocumentRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:compress-pdf',
description: 'Compress all PDF documents stored in database without quality loss',
)]
class CompressPdfCommand extends Command
{
public function __construct(
private readonly DocumentRepository $documentRepository,
private readonly EntityManagerInterface $em,
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would be compressed without actually doing it')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$dryRun = $input->getOption('dry-run');
// Check if qpdf is installed
exec('which qpdf', $qpdfPath, $returnCode);
if (0 !== $returnCode) {
$io->error('qpdf is not installed. Run: sudo apt install qpdf');
return Command::FAILURE;
}
$documents = $this->documentRepository->findBy(['mimeType' => 'application/pdf']);
if (empty($documents)) {
$io->info('No PDF documents found.');
return Command::SUCCESS;
}
$io->title('PDF Compression');
$io->text(sprintf('Found %d PDF documents', count($documents)));
$totalSaved = 0;
$compressed = 0;
foreach ($documents as $document) {
$base64Data = $document->getPath();
// Remove data URI prefix if present
if (str_contains($base64Data, ',')) {
$base64Data = explode(',', $base64Data, 2)[1];
}
$pdfContent = base64_decode($base64Data, true);
if (false === $pdfContent) {
$io->warning(sprintf('Failed to decode document: %s', $document->getName()));
continue;
}
$originalSize = strlen($pdfContent);
if ($dryRun) {
$io->text(sprintf(
' [DRY-RUN] Would compress: %s (%s)',
$document->getName(),
$this->formatBytes($originalSize)
));
continue;
}
// Create temp files
$tempInput = tempnam(sys_get_temp_dir(), 'pdf_in_');
$tempOutput = tempnam(sys_get_temp_dir(), 'pdf_out_');
file_put_contents($tempInput, $pdfContent);
// Compress with qpdf (lossless)
$command = sprintf(
'qpdf --linearize --object-streams=generate %s %s 2>&1',
escapeshellarg($tempInput),
escapeshellarg($tempOutput)
);
exec($command, $cmdOutput, $returnCode);
if (0 !== $returnCode || !file_exists($tempOutput)) {
$io->warning(sprintf('Failed to compress: %s', $document->getName()));
@unlink($tempInput);
@unlink($tempOutput);
continue;
}
$compressedContent = file_get_contents($tempOutput);
$compressedSize = strlen($compressedContent);
// Only update if we actually saved space
if ($compressedSize < $originalSize) {
$saved = $originalSize - $compressedSize;
$totalSaved += $saved;
++$compressed;
// Rebuild base64 with data URI prefix
$newBase64 = 'data:application/pdf;base64,'.base64_encode($compressedContent);
$document->setPath($newBase64);
$document->setSize($compressedSize);
$io->text(sprintf(
' ✓ %s: %s → %s (-%s, -%.1f%%)',
$document->getName(),
$this->formatBytes($originalSize),
$this->formatBytes($compressedSize),
$this->formatBytes($saved),
($saved / $originalSize) * 100
));
} else {
$io->text(sprintf(
' - %s: Already optimal (%s)',
$document->getName(),
$this->formatBytes($originalSize)
));
}
@unlink($tempInput);
@unlink($tempOutput);
}
if (!$dryRun && $compressed > 0) {
$this->em->flush();
$io->success(sprintf(
'Compressed %d/%d PDFs. Total space saved: %s',
$compressed,
count($documents),
$this->formatBytes($totalSaved)
));
} elseif ($dryRun) {
$io->info('Dry run completed. No changes made.');
} else {
$io->info('No PDFs needed compression.');
}
return Command::SUCCESS;
}
private function formatBytes(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$i = 0;
while ($bytes >= 1024 && $i < count($units) - 1) {
$bytes /= 1024;
++$i;
}
return round($bytes, 2).' '.$units[$i];
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Repository\AuditLogRepository;
use App\Repository\ComposantRepository;
use App\Repository\ProfileRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class ComposantHistoryController
{
public function __construct(
private readonly ComposantRepository $components,
private readonly AuditLogRepository $auditLogs,
private readonly ProfileRepository $profiles,
) {
}
#[Route('/api/composants/{id}/history', name: 'api_composant_history', methods: ['GET'])]
public function __invoke(string $id): JsonResponse
{
$component = $this->components->find($id);
if (!$component) {
return new JsonResponse(
['message' => 'Composant introuvable.'],
Response::HTTP_NOT_FOUND,
);
}
$logs = $this->auditLogs->findEntityHistory('composant', $id, 200);
$actorIds = array_values(array_unique(array_filter(array_map(
static fn ($log) => $log->getActorProfileId(),
$logs,
))));
$actorMap = [];
if ($actorIds !== []) {
$profiles = $this->profiles->findBy(['id' => $actorIds]);
foreach ($profiles as $profile) {
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
if ($label === '') {
$label = $profile->getEmail() ?? $profile->getId();
}
$actorMap[$profile->getId()] = $label;
}
}
$items = array_map(
static function ($log) use ($actorMap) {
$actorId = $log->getActorProfileId();
return [
'id' => $log->getId(),
'action' => $log->getAction(),
'createdAt' => $log->getCreatedAt()->format(\DateTimeInterface::ATOM),
'actor' => $actorId
? [
'id' => $actorId,
'label' => $actorMap[$actorId] ?? $actorId,
]
: null,
'diff' => $log->getDiff(),
'snapshot' => $log->getSnapshot(),
];
},
$logs,
);
return new JsonResponse([
'items' => array_values($items),
'total' => count($items),
]);
}
}

View File

@@ -419,6 +419,16 @@ class MachineSkeletonController extends AbstractController
'name' => $typeMachine->getName(), 'name' => $typeMachine->getName(),
'category' => $typeMachine->getCategory(), 'category' => $typeMachine->getCategory(),
'description' => $typeMachine->getDescription(), 'description' => $typeMachine->getDescription(),
'customFields' => $this->normalizeCustomFields($typeMachine->getCustomFields()),
'componentRequirements' => $typeMachine->getComponentRequirements()
->map(fn (TypeMachineComponentRequirement $req) => $this->normalizeComponentRequirement($req))
->toArray(),
'pieceRequirements' => $typeMachine->getPieceRequirements()
->map(fn (TypeMachinePieceRequirement $req) => $this->normalizePieceRequirement($req))
->toArray(),
'productRequirements' => $typeMachine->getProductRequirements()
->map(fn (TypeMachineProductRequirement $req) => $this->normalizeProductRequirement($req))
->toArray(),
] : null, ] : null,
'constructeurs' => $this->normalizeConstructeurs($machine->getConstructeurs()), 'constructeurs' => $this->normalizeConstructeurs($machine->getConstructeurs()),
'documents' => null, 'documents' => null,
@@ -426,6 +436,27 @@ class MachineSkeletonController extends AbstractController
]; ];
} }
private function normalizeCustomFields(Collection $customFields): array
{
$items = [];
foreach ($customFields as $customField) {
if (!$customField instanceof CustomField) {
continue;
}
$items[] = [
'id' => $customField->getId(),
'name' => $customField->getName(),
'type' => $customField->getType(),
'required' => $customField->isRequired(),
'options' => $customField->getOptions(),
'defaultValue' => $customField->getDefaultValue(),
'orderIndex' => $customField->getOrderIndex(),
];
}
return $items;
}
private function normalizeComponentLinks(array $links): array private function normalizeComponentLinks(array $links): array
{ {
return array_map(function (MachineComponentLink $link): array { return array_map(function (MachineComponentLink $link): array {

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Repository\AuditLogRepository;
use App\Repository\PieceRepository;
use App\Repository\ProfileRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class PieceHistoryController
{
public function __construct(
private readonly PieceRepository $pieces,
private readonly AuditLogRepository $auditLogs,
private readonly ProfileRepository $profiles,
) {
}
#[Route('/api/pieces/{id}/history', name: 'api_piece_history', methods: ['GET'])]
public function __invoke(string $id): JsonResponse
{
$piece = $this->pieces->find($id);
if (!$piece) {
return new JsonResponse(
['message' => 'Pièce introuvable.'],
Response::HTTP_NOT_FOUND,
);
}
$logs = $this->auditLogs->findEntityHistory('piece', $id, 200);
$actorIds = array_values(array_unique(array_filter(array_map(
static fn ($log) => $log->getActorProfileId(),
$logs,
))));
$actorMap = [];
if ($actorIds !== []) {
$profiles = $this->profiles->findBy(['id' => $actorIds]);
foreach ($profiles as $profile) {
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
if ($label === '') {
$label = $profile->getEmail() ?? $profile->getId();
}
$actorMap[$profile->getId()] = $label;
}
}
$items = array_map(
static function ($log) use ($actorMap) {
$actorId = $log->getActorProfileId();
return [
'id' => $log->getId(),
'action' => $log->getAction(),
'createdAt' => $log->getCreatedAt()->format(\DateTimeInterface::ATOM),
'actor' => $actorId
? [
'id' => $actorId,
'label' => $actorMap[$actorId] ?? $actorId,
]
: null,
'diff' => $log->getDiff(),
'snapshot' => $log->getSnapshot(),
];
},
$logs,
);
return new JsonResponse([
'items' => array_values($items),
'total' => count($items),
]);
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Repository\AuditLogRepository;
use App\Repository\ProductRepository;
use App\Repository\ProfileRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class ProductHistoryController
{
public function __construct(
private readonly ProductRepository $products,
private readonly AuditLogRepository $auditLogs,
private readonly ProfileRepository $profiles,
) {
}
#[Route('/api/products/{id}/history', name: 'api_product_history', methods: ['GET'])]
public function __invoke(string $id): JsonResponse
{
$product = $this->products->find($id);
if (!$product) {
return new JsonResponse(
['message' => 'Produit introuvable.'],
Response::HTTP_NOT_FOUND,
);
}
$logs = $this->auditLogs->findEntityHistory('product', $id, 200);
$actorIds = array_values(array_unique(array_filter(array_map(
static fn ($log) => $log->getActorProfileId(),
$logs,
))));
$actorMap = [];
if ($actorIds !== []) {
$profiles = $this->profiles->findBy(['id' => $actorIds]);
foreach ($profiles as $profile) {
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
if ($label === '') {
$label = $profile->getEmail() ?? $profile->getId();
}
$actorMap[$profile->getId()] = $label;
}
}
$items = array_map(
static function ($log) use ($actorMap) {
$actorId = $log->getActorProfileId();
return [
'id' => $log->getId(),
'action' => $log->getAction(),
'createdAt' => $log->getCreatedAt()->format(\DateTimeInterface::ATOM),
'actor' => $actorId
? [
'id' => $actorId,
'label' => $actorMap[$actorId] ?? $actorId,
]
: null,
'diff' => $log->getDiff(),
'snapshot' => $log->getSnapshot(),
];
},
$logs,
);
return new JsonResponse([
'items' => array_values($items),
'total' => count($items),
]);
}
}

117
src/Entity/AuditLog.php Normal file
View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\AuditLogRepository;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: AuditLogRepository::class)]
#[ORM\Table(name: 'audit_logs')]
#[ORM\Index(name: 'idx_audit_entity', columns: ['entityType', 'entityId'])]
#[ORM\Index(name: 'idx_audit_created_at', columns: ['createdAt'])]
#[ORM\HasLifecycleCallbacks]
class AuditLog
{
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 50)]
private string $entityType;
#[ORM\Column(type: Types::STRING, length: 36)]
private string $entityId;
#[ORM\Column(type: Types::STRING, length: 20)]
private string $action;
#[ORM\Column(type: Types::JSON, nullable: true)]
private ?array $diff = null;
#[ORM\Column(type: Types::JSON, nullable: true)]
private ?array $snapshot = null;
#[ORM\Column(type: Types::STRING, length: 36, nullable: true)]
private ?string $actorProfileId = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
private DateTimeImmutable $createdAt;
public function __construct(
string $entityType,
string $entityId,
string $action,
?array $diff = null,
?array $snapshot = null,
?string $actorProfileId = null,
) {
$this->entityType = $entityType;
$this->entityId = $entityId;
$this->action = $action;
$this->diff = $diff;
$this->snapshot = $snapshot;
$this->actorProfileId = $actorProfileId;
}
#[ORM\PrePersist]
public function initializeAuditLog(): void
{
if (!isset($this->createdAt)) {
$this->createdAt = new DateTimeImmutable();
}
if ($this->id === null) {
$this->id = $this->generateCuid();
}
}
public function getId(): ?string
{
return $this->id;
}
public function getEntityType(): string
{
return $this->entityType;
}
public function getEntityId(): string
{
return $this->entityId;
}
public function getAction(): string
{
return $this->action;
}
public function getDiff(): ?array
{
return $this->diff;
}
public function getSnapshot(): ?array
{
return $this->snapshot;
}
public function getActorProfileId(): ?string
{
return $this->actorProfileId;
}
public function getCreatedAt(): DateTimeImmutable
{
return $this->createdAt;
}
private function generateCuid(): string
{
// Keep the same lightweight CUID-like strategy used across the project.
return 'cl'.substr(strtolower(base_convert(bin2hex(random_bytes(12)), 16, 36)), 0, 24);
}
}

View File

@@ -4,41 +4,59 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\ApiResource;
use App\Repository\ComposantRepository; use App\Repository\ComposantRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: ComposantRepository::class)] #[ORM\Entity(repositoryClass: ComposantRepository::class)]
#[ORM\Table(name: 'composants')] #[ORM\Table(name: 'composants')]
#[ORM\HasLifecycleCallbacks] #[ORM\HasLifecycleCallbacks]
#[ApiResource] #[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeComposant' => 'exact'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
#[ApiResource(
normalizationContext: ['groups' => ['composant:read']],
paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 500
)]
class Composant class Composant
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)] #[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['composant:read'])]
private ?string $id = null; private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255, unique: true)] #[ORM\Column(type: Types::STRING, length: 255, unique: true)]
#[Groups(['composant:read'])]
private string $name; private string $name;
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)] #[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
#[Groups(['composant:read'])]
private ?string $reference = null; private ?string $reference = null;
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)] #[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)]
#[Groups(['composant:read'])]
private ?string $prix = null; private ?string $prix = null;
#[ORM\Column(type: Types::JSON, nullable: true)] #[ORM\Column(type: Types::JSON, nullable: true)]
#[Groups(['composant:read'])]
private ?array $structure = null; private ?array $structure = null;
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'composants')] #[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'composants')]
#[ORM\JoinColumn(name: 'typeComposantId', referencedColumnName: 'id', nullable: true)] #[ORM\JoinColumn(name: 'typeComposantId', referencedColumnName: 'id', nullable: true)]
#[Groups(['composant:read'])]
private ?ModelType $typeComposant = null; private ?ModelType $typeComposant = null;
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'composants')] #[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'composants')]
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] #[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
#[Groups(['composant:read'])]
private ?Product $product = null; private ?Product $product = null;
/** /**
@@ -50,18 +68,21 @@ class Composant
joinColumns: [new ORM\JoinColumn(name: 'A', referencedColumnName: 'id', onDelete: 'CASCADE')], joinColumns: [new ORM\JoinColumn(name: 'A', referencedColumnName: 'id', onDelete: 'CASCADE')],
inverseJoinColumns: [new ORM\InverseJoinColumn(name: 'B', referencedColumnName: 'id', onDelete: 'CASCADE')] inverseJoinColumns: [new ORM\InverseJoinColumn(name: 'B', referencedColumnName: 'id', onDelete: 'CASCADE')]
)] )]
#[Groups(['composant:read'])]
private Collection $constructeurs; private Collection $constructeurs;
/** /**
* @var Collection<int, Document> * @var Collection<int, Document>
*/ */
#[ORM\OneToMany(mappedBy: 'composant', targetEntity: Document::class)] #[ORM\OneToMany(mappedBy: 'composant', targetEntity: Document::class)]
#[Groups(['composant:read'])]
private Collection $documents; private Collection $documents;
/** /**
* @var Collection<int, CustomFieldValue> * @var Collection<int, CustomFieldValue>
*/ */
#[ORM\OneToMany(mappedBy: 'composant', targetEntity: CustomFieldValue::class)] #[ORM\OneToMany(mappedBy: 'composant', targetEntity: CustomFieldValue::class)]
#[Groups(['composant:read'])]
private Collection $customFieldValues; private Collection $customFieldValues;
/** /**
@@ -71,10 +92,12 @@ class Composant
private Collection $machineLinks; private Collection $machineLinks;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
private \DateTimeImmutable $createdAt; #[Groups(['composant:read'])]
private DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
private \DateTimeImmutable $updatedAt; #[Groups(['composant:read'])]
private DateTimeImmutable $updatedAt;
public function __construct() public function __construct()
{ {
@@ -87,11 +110,11 @@ class Composant
#[ORM\PrePersist] #[ORM\PrePersist]
public function setCreatedAtValue(): void public function setCreatedAtValue(): void
{ {
$now = new \DateTimeImmutable(); $now = new DateTimeImmutable();
$this->createdAt = $now; $this->createdAt = $now;
$this->updatedAt = $now; $this->updatedAt = $now;
if ($this->id === null) { if (null === $this->id) {
$this->id = $this->generateCuid(); $this->id = $this->generateCuid();
} }
} }
@@ -99,12 +122,7 @@ class Composant
#[ORM\PreUpdate] #[ORM\PreUpdate]
public function setUpdatedAtValue(): void public function setUpdatedAtValue(): void
{ {
$this->updatedAt = new \DateTimeImmutable(); $this->updatedAt = new DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
} }
public function getId(): ?string public function getId(): ?string
@@ -199,6 +217,38 @@ class Composant
return $this->constructeurs; return $this->constructeurs;
} }
/**
* @param iterable<Constructeur> $constructeurs
*/
public function setConstructeurs(iterable $constructeurs): static
{
$this->constructeurs = new ArrayCollection();
foreach ($constructeurs as $constructeur) {
if ($constructeur instanceof Constructeur && !$this->constructeurs->contains($constructeur)) {
$this->constructeurs->add($constructeur);
}
}
return $this;
}
public function addConstructeur(Constructeur $constructeur): static
{
if (!$this->constructeurs->contains($constructeur)) {
$this->constructeurs->add($constructeur);
}
return $this;
}
public function removeConstructeur(Constructeur $constructeur): static
{
$this->constructeurs->removeElement($constructeur);
return $this;
}
/** /**
* @return Collection<int, Document> * @return Collection<int, Document>
*/ */
@@ -215,13 +265,18 @@ class Composant
return $this->customFieldValues; return $this->customFieldValues;
} }
public function getCreatedAt(): \DateTimeImmutable public function getCreatedAt(): DateTimeImmutable
{ {
return $this->createdAt; return $this->createdAt;
} }
public function getUpdatedAt(): \DateTimeImmutable public function getUpdatedAt(): DateTimeImmutable
{ {
return $this->updatedAt; return $this->updatedAt;
} }
private function generateCuid(): string
{
return 'cl'.bin2hex(random_bytes(12));
}
} }

View File

@@ -6,6 +6,7 @@ namespace App\Entity;
use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\ApiResource;
use App\Repository\ConstructeurRepository; use App\Repository\ConstructeurRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
@@ -14,7 +15,10 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ConstructeurRepository::class)] #[ORM\Entity(repositoryClass: ConstructeurRepository::class)]
#[ORM\Table(name: 'constructeurs')] #[ORM\Table(name: 'constructeurs')]
#[ORM\HasLifecycleCallbacks] #[ORM\HasLifecycleCallbacks]
#[ApiResource] #[ApiResource(
paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 500
)]
class Constructeur class Constructeur
{ {
#[ORM\Id] #[ORM\Id]
@@ -31,10 +35,10 @@ class Constructeur
private ?string $phone = null; private ?string $phone = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
private \DateTimeImmutable $createdAt; private DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
private \DateTimeImmutable $updatedAt; private DateTimeImmutable $updatedAt;
/** /**
* @var Collection<int, Machine> * @var Collection<int, Machine>
@@ -71,11 +75,11 @@ class Constructeur
#[ORM\PrePersist] #[ORM\PrePersist]
public function setCreatedAtValue(): void public function setCreatedAtValue(): void
{ {
$now = new \DateTimeImmutable(); $now = new DateTimeImmutable();
$this->createdAt = $now; $this->createdAt = $now;
$this->updatedAt = $now; $this->updatedAt = $now;
if ($this->id === null) { if (null === $this->id) {
$this->id = $this->generateCuid(); $this->id = $this->generateCuid();
} }
} }
@@ -83,12 +87,7 @@ class Constructeur
#[ORM\PreUpdate] #[ORM\PreUpdate]
public function setUpdatedAtValue(): void public function setUpdatedAtValue(): void
{ {
$this->updatedAt = new \DateTimeImmutable(); $this->updatedAt = new DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
} }
public function getId(): ?string public function getId(): ?string
@@ -139,13 +138,18 @@ class Constructeur
return $this; return $this;
} }
public function getCreatedAt(): \DateTimeImmutable public function getCreatedAt(): DateTimeImmutable
{ {
return $this->createdAt; return $this->createdAt;
} }
public function getUpdatedAt(): \DateTimeImmutable public function getUpdatedAt(): DateTimeImmutable
{ {
return $this->updatedAt; return $this->updatedAt;
} }
private function generateCuid(): string
{
return 'cl'.bin2hex(random_bytes(12));
}
} }

View File

@@ -179,6 +179,18 @@ class CustomField
return $this; return $this;
} }
public function getTypeMachine(): ?TypeMachine
{
return $this->typeMachine;
}
public function setTypeMachine(?TypeMachine $typeMachine): static
{
$this->typeMachine = $typeMachine;
return $this;
}
public function getCreatedAt(): \DateTimeImmutable public function getCreatedAt(): \DateTimeImmutable
{ {
return $this->createdAt; return $this->createdAt;

View File

@@ -8,6 +8,7 @@ use ApiPlatform\Metadata\ApiResource;
use App\Repository\DocumentRepository; use App\Repository\DocumentRepository;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: DocumentRepository::class)] #[ORM\Entity(repositoryClass: DocumentRepository::class)]
#[ORM\Table(name: 'documents')] #[ORM\Table(name: 'documents')]
@@ -17,21 +18,27 @@ class Document
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)] #[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
private ?string $id = null; private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255)] #[ORM\Column(type: Types::STRING, length: 255)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
private string $name; private string $name;
#[ORM\Column(type: Types::STRING, length: 255)] #[ORM\Column(type: Types::STRING, length: 255)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
private string $filename; private string $filename;
#[ORM\Column(type: Types::TEXT)] #[ORM\Column(type: Types::TEXT)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
private string $path; private string $path;
#[ORM\Column(type: Types::STRING, length: 100, name: 'mimeType')] #[ORM\Column(type: Types::STRING, length: 100, name: 'mimeType')]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
private string $mimeType; private string $mimeType;
#[ORM\Column(type: Types::INTEGER)] #[ORM\Column(type: Types::INTEGER)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])]
private int $size; private int $size;
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'documents')] #[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'documents')]

View File

@@ -9,6 +9,7 @@ use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\ApiResource;
use App\Enum\ModelCategory; use App\Enum\ModelCategory;
use App\Repository\ModelTypeRepository; use App\Repository\ModelTypeRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
@@ -19,8 +20,11 @@ use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Table(name: 'model_types')] #[ORM\Table(name: 'model_types')]
#[ORM\UniqueConstraint(name: 'unique_category_name', columns: ['category', 'name'])] #[ORM\UniqueConstraint(name: 'unique_category_name', columns: ['category', 'name'])]
#[ORM\HasLifecycleCallbacks] #[ORM\HasLifecycleCallbacks]
#[ApiFilter(SearchFilter::class, properties: ['category' => 'exact'])] #[ApiFilter(SearchFilter::class, properties: ['category' => 'exact', 'name' => 'ipartial'])]
#[ApiResource] #[ApiResource(
paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 500
)]
class ModelType class ModelType
{ {
#[ORM\Id] #[ORM\Id]
@@ -62,11 +66,11 @@ class ModelType
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
#[Groups(['model_type:read'])] #[Groups(['model_type:read'])]
private \DateTimeImmutable $createdAt; private DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
#[Groups(['model_type:read'])] #[Groups(['model_type:read'])]
private \DateTimeImmutable $updatedAt; private DateTimeImmutable $updatedAt;
private ?array $pendingStructure = null; private ?array $pendingStructure = null;
@@ -140,11 +144,11 @@ class ModelType
#[ORM\PrePersist] #[ORM\PrePersist]
public function setCreatedAtValue(): void public function setCreatedAtValue(): void
{ {
$now = new \DateTimeImmutable(); $now = new DateTimeImmutable();
$this->createdAt = $now; $this->createdAt = $now;
$this->updatedAt = $now; $this->updatedAt = $now;
if ($this->id === null) { if (null === $this->id) {
$this->id = $this->generateCuid(); $this->id = $this->generateCuid();
} }
} }
@@ -152,12 +156,7 @@ class ModelType
#[ORM\PreUpdate] #[ORM\PreUpdate]
public function setUpdatedAtValue(): void public function setUpdatedAtValue(): void
{ {
$this->updatedAt = new \DateTimeImmutable(); $this->updatedAt = new DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
} }
public function getId(): ?string public function getId(): ?string
@@ -205,7 +204,7 @@ class ModelType
{ {
$this->category = $category; $this->category = $category;
if ($this->pendingStructure !== null) { if (null !== $this->pendingStructure) {
$this->applyStructureForCategory($this->pendingStructure, $category); $this->applyStructureForCategory($this->pendingStructure, $category);
$this->pendingStructure = null; $this->pendingStructure = null;
} }
@@ -288,6 +287,7 @@ class ModelType
{ {
if (!isset($this->category)) { if (!isset($this->category)) {
$this->pendingStructure = $structure; $this->pendingStructure = $structure;
return $this; return $this;
} }
@@ -296,19 +296,36 @@ class ModelType
return $this; return $this;
} }
public function getCreatedAt(): DateTimeImmutable
{
return $this->createdAt;
}
public function getUpdatedAt(): DateTimeImmutable
{
return $this->updatedAt;
}
private function generateCuid(): string
{
return 'cl'.bin2hex(random_bytes(12));
}
private function applyStructureForCategory(?array $structure, ModelCategory $category): void private function applyStructureForCategory(?array $structure, ModelCategory $category): void
{ {
if ($category === ModelCategory::COMPONENT) { if (ModelCategory::COMPONENT === $category) {
$this->componentSkeleton = $structure; $this->componentSkeleton = $structure;
$this->pieceSkeleton = null; $this->pieceSkeleton = null;
$this->productSkeleton = null; $this->productSkeleton = null;
return; return;
} }
if ($category === ModelCategory::PIECE) { if (ModelCategory::PIECE === $category) {
$this->pieceSkeleton = $structure; $this->pieceSkeleton = $structure;
$this->componentSkeleton = null; $this->componentSkeleton = null;
$this->productSkeleton = null; $this->productSkeleton = null;
return; return;
} }
@@ -316,14 +333,4 @@ class ModelType
$this->componentSkeleton = null; $this->componentSkeleton = null;
$this->pieceSkeleton = null; $this->pieceSkeleton = null;
} }
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}
public function getUpdatedAt(): \DateTimeImmutable
{
return $this->updatedAt;
}
} }

View File

@@ -4,40 +4,61 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\ApiResource;
use App\Repository\PieceRepository; use App\Repository\PieceRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: PieceRepository::class)] #[ORM\Entity(repositoryClass: PieceRepository::class)]
#[ORM\Table(name: 'pieces')] #[ORM\Table(name: 'pieces')]
#[ORM\HasLifecycleCallbacks] #[ORM\HasLifecycleCallbacks]
#[ApiResource] #[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typePiece' => 'exact'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
#[ApiResource(
normalizationContext: ['groups' => ['piece:read']],
paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 500
)]
class Piece class Piece
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)] #[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['piece:read'])]
private ?string $id = null; private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255, unique: true)] #[ORM\Column(type: Types::STRING, length: 255, unique: true)]
#[Groups(['piece:read'])]
private string $name; private string $name;
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)] #[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
#[Groups(['piece:read'])]
private ?string $reference = null; private ?string $reference = null;
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)] #[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)]
#[Groups(['piece:read'])]
private ?string $prix = null; private ?string $prix = null;
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'pieces')] #[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'pieces')]
#[ORM\JoinColumn(name: 'typePieceId', referencedColumnName: 'id', nullable: true)] #[ORM\JoinColumn(name: 'typePieceId', referencedColumnName: 'id', nullable: true)]
#[Groups(['piece:read'])]
private ?ModelType $typePiece = null; private ?ModelType $typePiece = null;
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'pieces')] #[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'pieces')]
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] #[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
#[Groups(['piece:read'])]
private ?Product $product = null; private ?Product $product = null;
#[ORM\Column(type: Types::JSON, nullable: true, name: 'productIds')]
#[Groups(['piece:read'])]
private ?array $productIds = null;
/** /**
* @var Collection<int, Constructeur> * @var Collection<int, Constructeur>
*/ */
@@ -47,18 +68,21 @@ class Piece
joinColumns: [new ORM\JoinColumn(name: 'A', referencedColumnName: 'id', onDelete: 'CASCADE')], joinColumns: [new ORM\JoinColumn(name: 'A', referencedColumnName: 'id', onDelete: 'CASCADE')],
inverseJoinColumns: [new ORM\InverseJoinColumn(name: 'B', referencedColumnName: 'id', onDelete: 'CASCADE')] inverseJoinColumns: [new ORM\InverseJoinColumn(name: 'B', referencedColumnName: 'id', onDelete: 'CASCADE')]
)] )]
#[Groups(['piece:read'])]
private Collection $constructeurs; private Collection $constructeurs;
/** /**
* @var Collection<int, Document> * @var Collection<int, Document>
*/ */
#[ORM\OneToMany(mappedBy: 'piece', targetEntity: Document::class)] #[ORM\OneToMany(mappedBy: 'piece', targetEntity: Document::class)]
#[Groups(['piece:read'])]
private Collection $documents; private Collection $documents;
/** /**
* @var Collection<int, CustomFieldValue> * @var Collection<int, CustomFieldValue>
*/ */
#[ORM\OneToMany(mappedBy: 'piece', targetEntity: CustomFieldValue::class)] #[ORM\OneToMany(mappedBy: 'piece', targetEntity: CustomFieldValue::class)]
#[Groups(['piece:read'])]
private Collection $customFieldValues; private Collection $customFieldValues;
/** /**
@@ -68,10 +92,12 @@ class Piece
private Collection $machineLinks; private Collection $machineLinks;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
private \DateTimeImmutable $createdAt; #[Groups(['piece:read'])]
private DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
private \DateTimeImmutable $updatedAt; #[Groups(['piece:read'])]
private DateTimeImmutable $updatedAt;
public function __construct() public function __construct()
{ {
@@ -84,11 +110,11 @@ class Piece
#[ORM\PrePersist] #[ORM\PrePersist]
public function setCreatedAtValue(): void public function setCreatedAtValue(): void
{ {
$now = new \DateTimeImmutable(); $now = new DateTimeImmutable();
$this->createdAt = $now; $this->createdAt = $now;
$this->updatedAt = $now; $this->updatedAt = $now;
if ($this->id === null) { if (null === $this->id) {
$this->id = $this->generateCuid(); $this->id = $this->generateCuid();
} }
} }
@@ -96,12 +122,7 @@ class Piece
#[ORM\PreUpdate] #[ORM\PreUpdate]
public function setUpdatedAtValue(): void public function setUpdatedAtValue(): void
{ {
$this->updatedAt = new \DateTimeImmutable(); $this->updatedAt = new DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
} }
public function getId(): ?string public function getId(): ?string
@@ -173,6 +194,60 @@ class Piece
{ {
$this->product = $product; $this->product = $product;
if ($product && empty($this->productIds)) {
$productId = $product->getId();
$this->productIds = $productId ? [$productId] : null;
}
if (!$product && empty($this->productIds)) {
$this->productIds = null;
}
return $this;
}
/**
* @return string[]
*/
public function getProductIds(): array
{
if (!is_array($this->productIds)) {
return [];
}
return array_values(
array_filter(
array_map(
static fn ($value) => is_string($value) ? trim($value) : '',
$this->productIds,
),
static fn (string $value) => '' !== $value,
),
);
}
public function setProductIds(?array $productIds): static
{
if (!is_array($productIds)) {
$this->productIds = null;
return $this;
}
$normalized = array_values(
array_unique(
array_filter(
array_map(
static fn ($value) => is_string($value) ? trim($value) : '',
$productIds,
),
static fn (string $value) => '' !== $value,
),
),
);
$this->productIds = [] === $normalized ? null : $normalized;
return $this; return $this;
} }
@@ -184,6 +259,22 @@ class Piece
return $this->constructeurs; return $this->constructeurs;
} }
public function addConstructeur(Constructeur $constructeur): static
{
if (!$this->constructeurs->contains($constructeur)) {
$this->constructeurs->add($constructeur);
}
return $this;
}
public function removeConstructeur(Constructeur $constructeur): static
{
$this->constructeurs->removeElement($constructeur);
return $this;
}
/** /**
* @return Collection<int, Document> * @return Collection<int, Document>
*/ */
@@ -200,13 +291,18 @@ class Piece
return $this->customFieldValues; return $this->customFieldValues;
} }
public function getCreatedAt(): \DateTimeImmutable public function getCreatedAt(): DateTimeImmutable
{ {
return $this->createdAt; return $this->createdAt;
} }
public function getUpdatedAt(): \DateTimeImmutable public function getUpdatedAt(): DateTimeImmutable
{ {
return $this->updatedAt; return $this->updatedAt;
} }
private function generateCuid(): string
{
return 'cl'.bin2hex(random_bytes(12));
}
} }

View File

@@ -4,34 +4,50 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\ApiResource;
use App\Repository\ProductRepository; use App\Repository\ProductRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: ProductRepository::class)] #[ORM\Entity(repositoryClass: ProductRepository::class)]
#[ORM\Table(name: 'products')] #[ORM\Table(name: 'products')]
#[ORM\HasLifecycleCallbacks] #[ORM\HasLifecycleCallbacks]
#[ApiResource] #[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeProduct' => 'exact'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
#[ApiResource(
normalizationContext: ['groups' => ['product:read']],
paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 500
)]
class Product class Product
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)] #[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['product:read'])]
private ?string $id = null; private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255, unique: true)] #[ORM\Column(type: Types::STRING, length: 255, unique: true)]
#[Groups(['product:read'])]
private string $name; private string $name;
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)] #[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
#[Groups(['product:read'])]
private ?string $reference = null; private ?string $reference = null;
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true, name: 'supplierPrice')] #[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true, name: 'supplierPrice')]
#[Groups(['product:read'])]
private ?string $supplierPrice = null; private ?string $supplierPrice = null;
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'products')] #[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'products')]
#[ORM\JoinColumn(name: 'typeProductId', referencedColumnName: 'id', nullable: true)] #[ORM\JoinColumn(name: 'typeProductId', referencedColumnName: 'id', nullable: true)]
#[Groups(['product:read'])]
private ?ModelType $typeProduct = null; private ?ModelType $typeProduct = null;
/** /**
@@ -43,18 +59,21 @@ class Product
joinColumns: [new ORM\JoinColumn(name: 'A', referencedColumnName: 'id', onDelete: 'CASCADE')], joinColumns: [new ORM\JoinColumn(name: 'A', referencedColumnName: 'id', onDelete: 'CASCADE')],
inverseJoinColumns: [new ORM\InverseJoinColumn(name: 'B', referencedColumnName: 'id', onDelete: 'CASCADE')] inverseJoinColumns: [new ORM\InverseJoinColumn(name: 'B', referencedColumnName: 'id', onDelete: 'CASCADE')]
)] )]
#[Groups(['product:read'])]
private Collection $constructeurs; private Collection $constructeurs;
/** /**
* @var Collection<int, Document> * @var Collection<int, Document>
*/ */
#[ORM\OneToMany(mappedBy: 'product', targetEntity: Document::class)] #[ORM\OneToMany(mappedBy: 'product', targetEntity: Document::class)]
#[Groups(['product:read'])]
private Collection $documents; private Collection $documents;
/** /**
* @var Collection<int, CustomFieldValue> * @var Collection<int, CustomFieldValue>
*/ */
#[ORM\OneToMany(mappedBy: 'product', targetEntity: CustomFieldValue::class)] #[ORM\OneToMany(mappedBy: 'product', targetEntity: CustomFieldValue::class)]
#[Groups(['product:read'])]
private Collection $customFieldValues; private Collection $customFieldValues;
/** /**
@@ -76,10 +95,12 @@ class Product
private Collection $machineLinks; private Collection $machineLinks;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
private \DateTimeImmutable $createdAt; #[Groups(['product:read'])]
private DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
private \DateTimeImmutable $updatedAt; #[Groups(['product:read'])]
private DateTimeImmutable $updatedAt;
public function __construct() public function __construct()
{ {
@@ -94,11 +115,11 @@ class Product
#[ORM\PrePersist] #[ORM\PrePersist]
public function setCreatedAtValue(): void public function setCreatedAtValue(): void
{ {
$now = new \DateTimeImmutable(); $now = new DateTimeImmutable();
$this->createdAt = $now; $this->createdAt = $now;
$this->updatedAt = $now; $this->updatedAt = $now;
if ($this->id === null) { if (null === $this->id) {
$this->id = $this->generateCuid(); $this->id = $this->generateCuid();
} }
} }
@@ -106,12 +127,7 @@ class Product
#[ORM\PreUpdate] #[ORM\PreUpdate]
public function setUpdatedAtValue(): void public function setUpdatedAtValue(): void
{ {
$this->updatedAt = new \DateTimeImmutable(); $this->updatedAt = new DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
} }
public function getId(): ?string public function getId(): ?string
@@ -182,6 +198,22 @@ class Product
return $this->constructeurs; return $this->constructeurs;
} }
public function addConstructeur(Constructeur $constructeur): static
{
if (!$this->constructeurs->contains($constructeur)) {
$this->constructeurs->add($constructeur);
}
return $this;
}
public function removeConstructeur(Constructeur $constructeur): static
{
$this->constructeurs->removeElement($constructeur);
return $this;
}
/** /**
* @return Collection<int, Document> * @return Collection<int, Document>
*/ */
@@ -198,13 +230,18 @@ class Product
return $this->customFieldValues; return $this->customFieldValues;
} }
public function getCreatedAt(): \DateTimeImmutable public function getCreatedAt(): DateTimeImmutable
{ {
return $this->createdAt; return $this->createdAt;
} }
public function getUpdatedAt(): \DateTimeImmutable public function getUpdatedAt(): DateTimeImmutable
{ {
return $this->updatedAt; return $this->updatedAt;
} }
private function generateCuid(): string
{
return 'cl'.bin2hex(random_bytes(12));
}
} }

View File

@@ -11,6 +11,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Put;
use App\Repository\SiteRepository; use App\Repository\SiteRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
@@ -27,7 +28,9 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(), new Post(),
new Put(), new Put(),
new Delete(), new Delete(),
] ],
paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 500
)] )]
class Site class Site
{ {
@@ -55,10 +58,10 @@ class Site
private string $contactCity = ''; private string $contactCity = '';
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
private \DateTimeImmutable $createdAt; private DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
private \DateTimeImmutable $updatedAt; private DateTimeImmutable $updatedAt;
/** /**
* @var Collection<int, Machine> * @var Collection<int, Machine>
@@ -81,11 +84,11 @@ class Site
#[ORM\PrePersist] #[ORM\PrePersist]
public function setCreatedAtValue(): void public function setCreatedAtValue(): void
{ {
$this->createdAt = new \DateTimeImmutable(); $this->createdAt = new DateTimeImmutable();
$this->updatedAt = new \DateTimeImmutable(); $this->updatedAt = new DateTimeImmutable();
// Générer un ID CUID-compatible si nécessaire // Générer un ID CUID-compatible si nécessaire
if ($this->id === null) { if (null === $this->id) {
$this->id = $this->generateCuid(); $this->id = $this->generateCuid();
} }
} }
@@ -93,13 +96,7 @@ class Site
#[ORM\PreUpdate] #[ORM\PreUpdate]
public function setUpdatedAtValue(): void public function setUpdatedAtValue(): void
{ {
$this->updatedAt = new \DateTimeImmutable(); $this->updatedAt = new DateTimeImmutable();
}
private function generateCuid(): string
{
// Génération d'un ID compatible CUID (format: cl + 24 caractères)
return 'cl' . bin2hex(random_bytes(12));
} }
// Getters et Setters // Getters et Setters
@@ -188,12 +185,12 @@ class Site
return $this; return $this;
} }
public function getCreatedAt(): \DateTimeImmutable public function getCreatedAt(): DateTimeImmutable
{ {
return $this->createdAt; return $this->createdAt;
} }
public function getUpdatedAt(): \DateTimeImmutable public function getUpdatedAt(): DateTimeImmutable
{ {
return $this->updatedAt; return $this->updatedAt;
} }
@@ -257,4 +254,10 @@ class Site
return $this; return $this;
} }
private function generateCuid(): string
{
// Génération d'un ID compatible CUID (format: cl + 24 caractères)
return 'cl'.bin2hex(random_bytes(12));
}
} }

View File

@@ -32,7 +32,9 @@ use Symfony\Component\Validator\Constraints as Assert;
new Post(), new Post(),
new Put(), new Put(),
new Delete(), new Delete(),
] ],
paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 500
)] )]
class TypeMachine class TypeMachine
{ {
@@ -91,33 +93,34 @@ class TypeMachine
/** /**
* @var Collection<int, CustomField> * @var Collection<int, CustomField>
*/ */
#[ORM\OneToMany(targetEntity: CustomField::class, mappedBy: 'typeMachine')] #[ORM\OneToMany(targetEntity: CustomField::class, mappedBy: 'typeMachine', cascade: ['persist', 'remove'])]
#[ApiProperty(readableLink: true, writableLink: true)]
private Collection $customFields; private Collection $customFields;
/** /**
* @var Collection<int, TypeMachineComponentRequirement> * @var Collection<int, TypeMachineComponentRequirement>
*/ */
#[ORM\OneToMany(targetEntity: TypeMachineComponentRequirement::class, mappedBy: 'typeMachine', cascade: ['persist', 'remove'])] #[ORM\OneToMany(targetEntity: TypeMachineComponentRequirement::class, mappedBy: 'typeMachine', cascade: ['persist', 'remove'])]
#[ApiProperty(readableLink: true)] #[ApiProperty(readableLink: true, writableLink: true)]
private Collection $componentRequirements; private Collection $componentRequirements;
/** /**
* @var Collection<int, TypeMachinePieceRequirement> * @var Collection<int, TypeMachinePieceRequirement>
*/ */
#[ORM\OneToMany(targetEntity: TypeMachinePieceRequirement::class, mappedBy: 'typeMachine', cascade: ['persist', 'remove'])] #[ORM\OneToMany(targetEntity: TypeMachinePieceRequirement::class, mappedBy: 'typeMachine', cascade: ['persist', 'remove'])]
#[ApiProperty(readableLink: true)] #[ApiProperty(readableLink: true, writableLink: true)]
private Collection $pieceRequirements; private Collection $pieceRequirements;
/** /**
* @var Collection<int, TypeMachineProductRequirement> * @var Collection<int, TypeMachineProductRequirement>
*/ */
#[ORM\OneToMany(targetEntity: TypeMachineProductRequirement::class, mappedBy: 'typeMachine', cascade: ['persist', 'remove'])] #[ORM\OneToMany(targetEntity: TypeMachineProductRequirement::class, mappedBy: 'typeMachine', cascade: ['persist', 'remove'])]
#[ApiProperty(readableLink: true)] #[ApiProperty(readableLink: true, writableLink: true)]
private Collection $productRequirements; private Collection $productRequirements;
public function __construct() public function __construct()
{ {
$this->id = 'cl' . bin2hex(random_bytes(12)); $this->id = 'cl'.bin2hex(random_bytes(12));
$this->createdAt = new DateTimeImmutable(); $this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable(); $this->updatedAt = new DateTimeImmutable();
$this->machines = new ArrayCollection(); $this->machines = new ArrayCollection();

View File

@@ -55,6 +55,7 @@ class TypeMachineComponentRequirement
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'componentRequirements')] #[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'componentRequirements')]
#[ORM\JoinColumn(name: 'typeComposantId', referencedColumnName: 'id', nullable: false)] #[ORM\JoinColumn(name: 'typeComposantId', referencedColumnName: 'id', nullable: false)]
#[ApiProperty(readableLink: true)] #[ApiProperty(readableLink: true)]
#[Groups(['type_machine:read'])]
private ModelType $typeComposant; private ModelType $typeComposant;
/** /**

View File

@@ -55,6 +55,7 @@ class TypeMachinePieceRequirement
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'pieceRequirements')] #[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'pieceRequirements')]
#[ORM\JoinColumn(name: 'typePieceId', referencedColumnName: 'id', nullable: false)] #[ORM\JoinColumn(name: 'typePieceId', referencedColumnName: 'id', nullable: false)]
#[ApiProperty(readableLink: true)] #[ApiProperty(readableLink: true)]
#[Groups(['type_machine:read'])]
private ModelType $typePiece; private ModelType $typePiece;
/** /**

View File

@@ -55,6 +55,7 @@ class TypeMachineProductRequirement
#[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'productRequirements')] #[ORM\ManyToOne(targetEntity: ModelType::class, inversedBy: 'productRequirements')]
#[ORM\JoinColumn(name: 'typeProductId', referencedColumnName: 'id', nullable: false)] #[ORM\JoinColumn(name: 'typeProductId', referencedColumnName: 'id', nullable: false)]
#[ApiProperty(readableLink: true)] #[ApiProperty(readableLink: true)]
#[Groups(['type_machine:read'])]
private ModelType $typeProduct; private ModelType $typeProduct;
/** /**

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\EventListener;
use App\Entity\Document;
use App\Service\PdfCompressorService;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
use Doctrine\ORM\Events;
use Psr\Log\LoggerInterface;
#[AsEntityListener(event: Events::prePersist, method: 'prePersist', entity: Document::class)]
#[AsEntityListener(event: Events::preUpdate, method: 'preUpdate', entity: Document::class)]
class DocumentPdfCompressorListener
{
public function __construct(
private readonly PdfCompressorService $pdfCompressor,
private readonly ?LoggerInterface $logger = null,
) {}
public function prePersist(Document $document): void
{
$this->compressIfPdf($document);
}
public function preUpdate(Document $document): void
{
$this->compressIfPdf($document);
}
private function compressIfPdf(Document $document): void
{
if ('application/pdf' !== $document->getMimeType()) {
return;
}
$result = $this->pdfCompressor->compressBase64Pdf($document->getPath());
if (null === $result) {
return;
}
$document->setPath($result['path']);
$document->setSize($result['size']);
$this->logger?->info('PDF compressed', [
'document' => $document->getName(),
'originalSize' => $result['originalSize'],
'compressedSize' => $result['size'],
'saved' => $result['saved'],
]);
}
}

View File

@@ -0,0 +1,300 @@
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use App\Entity\AuditLog;
use App\Entity\Composant;
use App\Entity\ModelType;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\PersistentCollection;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
#[AsDoctrineListener(event: Events::onFlush)]
final class ComposantAuditSubscriber implements EventSubscriber
{
public function __construct(private readonly RequestStack $requestStack)
{
}
public function getSubscribedEvents(): array
{
return [
Events::onFlush,
];
}
public function onFlush(OnFlushEventArgs $args): void
{
$em = $args->getObjectManager();
if (!$em instanceof EntityManagerInterface) {
return;
}
$uow = $em->getUnitOfWork();
$actorProfileId = $this->resolveActorProfileId();
$pendingUpdates = [];
$pendingSnapshots = [];
$pendingComponents = [];
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if (!$entity instanceof Composant) {
continue;
}
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
$snapshot = $this->snapshotComposant($entity);
$this->persistAuditLog($em, new AuditLog('composant', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if (!$entity instanceof Composant) {
continue;
}
$componentId = (string) $entity->getId();
if ($componentId === '') {
continue;
}
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
if ($diff !== []) {
$pendingUpdates[$componentId] = $this->mergeDiffs($pendingUpdates[$componentId] ?? [], $diff);
$pendingSnapshots[$componentId] = $this->snapshotComposant($entity);
$pendingComponents[$componentId] = $entity;
}
}
foreach ($uow->getScheduledEntityDeletions() as $entity) {
if (!$entity instanceof Composant) {
continue;
}
$snapshot = $this->snapshotComposant($entity);
$this->persistAuditLog($em, new AuditLog('composant', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
}
foreach ($uow->getScheduledCollectionUpdates() as $collection) {
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingComponents);
}
foreach ($uow->getScheduledCollectionDeletions() as $collection) {
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingComponents);
}
foreach ($pendingUpdates as $componentId => $diff) {
if ($diff === []) {
continue;
}
$component = $pendingComponents[$componentId] ?? null;
if (!$component instanceof Composant) {
continue;
}
$snapshot = $pendingSnapshots[$componentId] ?? $this->snapshotComposant($component);
$this->persistAuditLog($em, new AuditLog('composant', $componentId, 'update', $diff, $snapshot, $actorProfileId));
}
}
/**
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
* @param array<string, array<string, mixed>> $pendingSnapshots
* @param array<string, Composant> $pendingComponents
*/
private function collectCollectionUpdate(
object $collection,
array &$pendingUpdates,
array &$pendingSnapshots,
array &$pendingComponents,
): void {
if (!$collection instanceof PersistentCollection) {
return;
}
$owner = $collection->getOwner();
if (!$owner instanceof Composant) {
return;
}
$componentId = (string) $owner->getId();
if ($componentId === '') {
return;
}
$mapping = $collection->getMapping();
$fieldName = $mapping['fieldName'] ?? null;
if ($fieldName !== 'constructeurs') {
return;
}
$before = $this->normalizeCollection($collection->getSnapshot());
$after = $this->normalizeCollection($collection->toArray());
if ($before === $after) {
return;
}
$diff = [
'constructeurIds' => [
'from' => $before,
'to' => $after,
],
];
$pendingUpdates[$componentId] = $this->mergeDiffs($pendingUpdates[$componentId] ?? [], $diff);
$pendingSnapshots[$componentId] = $this->snapshotComposant($owner);
$pendingComponents[$componentId] = $owner;
}
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
{
$uow = $em->getUnitOfWork();
$log->initializeAuditLog();
$em->persist($log);
$meta = $em->getClassMetadata(AuditLog::class);
$uow->computeChangeSet($meta, $log);
}
/**
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
* @return array<string, array{from:mixed, to:mixed}>
*/
private function buildDiffFromChangeSet(array $changeSet): array
{
$diff = [];
foreach ($changeSet as $field => [$oldValue, $newValue]) {
if ($field === 'updatedAt' || $field === 'createdAt') {
continue;
}
$normalizedOld = $this->normalizeValue($oldValue);
$normalizedNew = $this->normalizeValue($newValue);
if ($normalizedOld === $normalizedNew) {
continue;
}
$diff[$field] = [
'from' => $normalizedOld,
'to' => $normalizedNew,
];
}
return $diff;
}
private function snapshotComposant(Composant $component): array
{
return [
'id' => $component->getId(),
'name' => $component->getName(),
'reference' => $component->getReference(),
'prix' => $component->getPrix(),
'structure' => $component->getStructure(),
'typeComposant' => $this->normalizeValue($component->getTypeComposant()),
'product' => $this->normalizeValue($component->getProduct()),
'constructeurIds' => $this->normalizeCollection($component->getConstructeurs()),
];
}
/**
* @param iterable<mixed> $items
* @return list<string>
*/
private function normalizeCollection(iterable $items): array
{
$ids = [];
foreach ($items as $item) {
if (\is_object($item) && \method_exists($item, 'getId')) {
$id = $item->getId();
if ($id !== null && $id !== '') {
$ids[] = (string) $id;
}
}
}
sort($ids);
return array_values(array_unique($ids));
}
private function normalizeValue(mixed $value): mixed
{
if ($value === null || \is_scalar($value)) {
return $value;
}
if ($value instanceof \DateTimeInterface) {
return $value->format(\DateTimeInterface::ATOM);
}
if ($value instanceof ModelType) {
return [
'id' => $value->getId(),
'name' => $value->getName(),
'code' => $value->getCode(),
];
}
if ($value instanceof Product) {
return [
'id' => $value->getId(),
'name' => $value->getName(),
'reference' => $value->getReference(),
];
}
if ($value instanceof Collection) {
return $this->normalizeCollection($value);
}
if (\is_object($value) && \method_exists($value, 'getId')) {
return (string) $value->getId();
}
if (\is_array($value)) {
return $value;
}
return (string) $value;
}
/**
* @param array<string, array{from:mixed, to:mixed}> $base
* @param array<string, array{from:mixed, to:mixed}> $extra
* @return array<string, array{from:mixed, to:mixed}>
*/
private function mergeDiffs(array $base, array $extra): array
{
foreach ($extra as $field => $change) {
$base[$field] = $change;
}
return $base;
}
private function resolveActorProfileId(): ?string
{
$session = $this->requestStack->getSession();
if (!$session instanceof SessionInterface) {
return null;
}
$profileId = $session->get('profileId');
if (!$profileId) {
return null;
}
return (string) $profileId;
}
}

View File

@@ -0,0 +1,300 @@
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use App\Entity\AuditLog;
use App\Entity\ModelType;
use App\Entity\Piece;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\PersistentCollection;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
#[AsDoctrineListener(event: Events::onFlush)]
final class PieceAuditSubscriber implements EventSubscriber
{
public function __construct(private readonly RequestStack $requestStack)
{
}
public function getSubscribedEvents(): array
{
return [
Events::onFlush,
];
}
public function onFlush(OnFlushEventArgs $args): void
{
$em = $args->getObjectManager();
if (!$em instanceof EntityManagerInterface) {
return;
}
$uow = $em->getUnitOfWork();
$actorProfileId = $this->resolveActorProfileId();
$pendingUpdates = [];
$pendingSnapshots = [];
$pendingPieces = [];
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if (!$entity instanceof Piece) {
continue;
}
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
$snapshot = $this->snapshotPiece($entity);
$this->persistAuditLog($em, new AuditLog('piece', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if (!$entity instanceof Piece) {
continue;
}
$pieceId = (string) $entity->getId();
if ($pieceId === '') {
continue;
}
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
if ($diff !== []) {
$pendingUpdates[$pieceId] = $this->mergeDiffs($pendingUpdates[$pieceId] ?? [], $diff);
$pendingSnapshots[$pieceId] = $this->snapshotPiece($entity);
$pendingPieces[$pieceId] = $entity;
}
}
foreach ($uow->getScheduledEntityDeletions() as $entity) {
if (!$entity instanceof Piece) {
continue;
}
$snapshot = $this->snapshotPiece($entity);
$this->persistAuditLog($em, new AuditLog('piece', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
}
foreach ($uow->getScheduledCollectionUpdates() as $collection) {
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingPieces);
}
foreach ($uow->getScheduledCollectionDeletions() as $collection) {
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingPieces);
}
foreach ($pendingUpdates as $pieceId => $diff) {
if ($diff === []) {
continue;
}
$piece = $pendingPieces[$pieceId] ?? null;
if (!$piece instanceof Piece) {
continue;
}
$snapshot = $pendingSnapshots[$pieceId] ?? $this->snapshotPiece($piece);
$this->persistAuditLog($em, new AuditLog('piece', $pieceId, 'update', $diff, $snapshot, $actorProfileId));
}
}
/**
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
* @param array<string, array<string, mixed>> $pendingSnapshots
* @param array<string, Piece> $pendingPieces
*/
private function collectCollectionUpdate(
object $collection,
array &$pendingUpdates,
array &$pendingSnapshots,
array &$pendingPieces,
): void {
if (!$collection instanceof PersistentCollection) {
return;
}
$owner = $collection->getOwner();
if (!$owner instanceof Piece) {
return;
}
$pieceId = (string) $owner->getId();
if ($pieceId === '') {
return;
}
$mapping = $collection->getMapping();
$fieldName = $mapping['fieldName'] ?? null;
if ($fieldName !== 'constructeurs') {
return;
}
$before = $this->normalizeCollection($collection->getSnapshot());
$after = $this->normalizeCollection($collection->toArray());
if ($before === $after) {
return;
}
$diff = [
'constructeurIds' => [
'from' => $before,
'to' => $after,
],
];
$pendingUpdates[$pieceId] = $this->mergeDiffs($pendingUpdates[$pieceId] ?? [], $diff);
$pendingSnapshots[$pieceId] = $this->snapshotPiece($owner);
$pendingPieces[$pieceId] = $owner;
}
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
{
$uow = $em->getUnitOfWork();
$log->initializeAuditLog();
$em->persist($log);
$meta = $em->getClassMetadata(AuditLog::class);
$uow->computeChangeSet($meta, $log);
}
/**
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
* @return array<string, array{from:mixed, to:mixed}>
*/
private function buildDiffFromChangeSet(array $changeSet): array
{
$diff = [];
foreach ($changeSet as $field => [$oldValue, $newValue]) {
if ($field === 'updatedAt' || $field === 'createdAt') {
continue;
}
$normalizedOld = $this->normalizeValue($oldValue);
$normalizedNew = $this->normalizeValue($newValue);
if ($normalizedOld === $normalizedNew) {
continue;
}
$diff[$field] = [
'from' => $normalizedOld,
'to' => $normalizedNew,
];
}
return $diff;
}
private function snapshotPiece(Piece $piece): array
{
return [
'id' => $piece->getId(),
'name' => $piece->getName(),
'reference' => $piece->getReference(),
'prix' => $piece->getPrix(),
'typePiece' => $this->normalizeValue($piece->getTypePiece()),
'product' => $this->normalizeValue($piece->getProduct()),
'productIds' => $piece->getProductIds(),
'constructeurIds' => $this->normalizeCollection($piece->getConstructeurs()),
];
}
/**
* @param iterable<mixed> $items
* @return list<string>
*/
private function normalizeCollection(iterable $items): array
{
$ids = [];
foreach ($items as $item) {
if (\is_object($item) && \method_exists($item, 'getId')) {
$id = $item->getId();
if ($id !== null && $id !== '') {
$ids[] = (string) $id;
}
}
}
sort($ids);
return array_values(array_unique($ids));
}
private function normalizeValue(mixed $value): mixed
{
if ($value === null || \is_scalar($value)) {
return $value;
}
if ($value instanceof \DateTimeInterface) {
return $value->format(\DateTimeInterface::ATOM);
}
if ($value instanceof ModelType) {
return [
'id' => $value->getId(),
'name' => $value->getName(),
'code' => $value->getCode(),
];
}
if ($value instanceof Product) {
return [
'id' => $value->getId(),
'name' => $value->getName(),
'reference' => $value->getReference(),
];
}
if ($value instanceof Collection) {
return $this->normalizeCollection($value);
}
if (\is_object($value) && \method_exists($value, 'getId')) {
return (string) $value->getId();
}
if (\is_array($value)) {
return $value;
}
return (string) $value;
}
/**
* @param array<string, array{from:mixed, to:mixed}> $base
* @param array<string, array{from:mixed, to:mixed}> $extra
* @return array<string, array{from:mixed, to:mixed}>
*/
private function mergeDiffs(array $base, array $extra): array
{
foreach ($extra as $field => $change) {
$base[$field] = $change;
}
return $base;
}
private function resolveActorProfileId(): ?string
{
$session = $this->requestStack->getSession();
if (!$session instanceof SessionInterface) {
return null;
}
$profileId = $session->get('profileId');
if (!$profileId) {
return null;
}
return (string) $profileId;
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use App\Entity\Piece;
use App\Repository\ProductRepository;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
/**
* Keep the legacy single product relation in sync with the new productIds array.
*/
final class PieceProductSyncSubscriber implements EventSubscriber
{
public function __construct(private readonly ProductRepository $productRepository)
{
}
public function getSubscribedEvents(): array
{
return [
Events::prePersist,
Events::preUpdate,
];
}
public function prePersist(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
if (!$entity instanceof Piece) {
return;
}
$this->syncPrimaryProduct($entity);
}
public function preUpdate(PreUpdateEventArgs $args): void
{
$entity = $args->getObject();
if (!$entity instanceof Piece) {
return;
}
$this->syncPrimaryProduct($entity);
$em = $args->getObjectManager();
$meta = $em->getClassMetadata(Piece::class);
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $entity);
}
private function syncPrimaryProduct(Piece $piece): void
{
$productIds = $piece->getProductIds();
if ($productIds === []) {
// If no explicit list is provided, keep the legacy relation as-is.
return;
}
$primaryId = $productIds[0] ?? null;
if (!$primaryId) {
$piece->setProduct(null);
return;
}
$currentProductId = $piece->getProduct()?->getId();
if ($currentProductId === $primaryId) {
return;
}
$primaryProduct = $this->productRepository->find($primaryId);
$piece->setProduct($primaryProduct);
}
}

View File

@@ -0,0 +1,298 @@
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use App\Entity\AuditLog;
use App\Entity\ModelType;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\PersistentCollection;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/**
* Record a lightweight, per-product audit trail.
*
* This MVP focuses on Product updates and captures:
* - scalar field changes (from Doctrine change sets)
* - constructeur collection changes (from collection updates)
*/
#[AsDoctrineListener(event: Events::onFlush)]
final class ProductAuditSubscriber implements EventSubscriber
{
public function __construct(private readonly RequestStack $requestStack)
{
}
public function getSubscribedEvents(): array
{
return [
Events::onFlush,
];
}
public function onFlush(OnFlushEventArgs $args): void
{
$em = $args->getObjectManager();
if (!$em instanceof EntityManagerInterface) {
return;
}
$uow = $em->getUnitOfWork();
$actorProfileId = $this->resolveActorProfileId();
$pendingUpdates = [];
$pendingSnapshots = [];
$pendingProducts = [];
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if (!$entity instanceof Product) {
continue;
}
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
$snapshot = $this->snapshotProduct($entity);
$this->persistAuditLog($em, new AuditLog('product', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if (!$entity instanceof Product) {
continue;
}
$productId = (string) $entity->getId();
if ($productId === '') {
continue;
}
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
if ($diff !== []) {
$pendingUpdates[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff);
$pendingSnapshots[$productId] = $this->snapshotProduct($entity);
$pendingProducts[$productId] = $entity;
}
}
foreach ($uow->getScheduledEntityDeletions() as $entity) {
if (!$entity instanceof Product) {
continue;
}
$snapshot = $this->snapshotProduct($entity);
$this->persistAuditLog($em, new AuditLog('product', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
}
// Capture constructeur collection updates, which are not included in the change set.
foreach ($uow->getScheduledCollectionUpdates() as $collection) {
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingProducts);
}
foreach ($uow->getScheduledCollectionDeletions() as $collection) {
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingProducts);
}
foreach ($pendingUpdates as $productId => $diff) {
if ($diff === []) {
continue;
}
$product = $pendingProducts[$productId] ?? null;
if (!$product instanceof Product) {
continue;
}
$snapshot = $pendingSnapshots[$productId] ?? $this->snapshotProduct($product);
$this->persistAuditLog($em, new AuditLog('product', $productId, 'update', $diff, $snapshot, $actorProfileId));
}
}
/**
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
* @param array<string, array<string, mixed>> $pendingSnapshots
* @param array<string, Product> $pendingProducts
*/
private function collectCollectionUpdate(
object $collection,
array &$pendingUpdates,
array &$pendingSnapshots,
array &$pendingProducts,
): void {
if (!$collection instanceof PersistentCollection) {
return;
}
$owner = $collection->getOwner();
if (!$owner instanceof Product) {
return;
}
$productId = (string) $owner->getId();
if ($productId === '') {
return;
}
$mapping = $collection->getMapping();
$fieldName = $mapping['fieldName'] ?? null;
if ($fieldName !== 'constructeurs') {
return;
}
$before = $this->normalizeCollection($collection->getSnapshot());
$after = $this->normalizeCollection($collection->toArray());
if ($before === $after) {
return;
}
$diff = [
'constructeurIds' => [
'from' => $before,
'to' => $after,
],
];
$pendingUpdates[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff);
$pendingSnapshots[$productId] = $this->snapshotProduct($owner);
$pendingProducts[$productId] = $owner;
}
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
{
$uow = $em->getUnitOfWork();
// Ensure identifiers and timestamps are set even when persisting during onFlush.
$log->initializeAuditLog();
$em->persist($log);
$meta = $em->getClassMetadata(AuditLog::class);
$uow->computeChangeSet($meta, $log);
}
/**
* @param array<string, array{0:mixed, 1:mixed}> $changeSet
* @return array<string, array{from:mixed, to:mixed}>
*/
private function buildDiffFromChangeSet(array $changeSet): array
{
$diff = [];
foreach ($changeSet as $field => [$oldValue, $newValue]) {
// Skip noisy timestamps managed automatically.
if ($field === 'updatedAt' || $field === 'createdAt') {
continue;
}
$normalizedOld = $this->normalizeValue($oldValue);
$normalizedNew = $this->normalizeValue($newValue);
if ($normalizedOld === $normalizedNew) {
continue;
}
$diff[$field] = [
'from' => $normalizedOld,
'to' => $normalizedNew,
];
}
return $diff;
}
private function snapshotProduct(Product $product): array
{
return [
'id' => $product->getId(),
'name' => $product->getName(),
'reference' => $product->getReference(),
'supplierPrice' => $product->getSupplierPrice(),
'typeProduct' => $this->normalizeValue($product->getTypeProduct()),
'constructeurIds' => $this->normalizeCollection($product->getConstructeurs()),
];
}
/**
* @param array<string, array{from:mixed, to:mixed}> $base
* @param array<string, array{from:mixed, to:mixed}> $extra
* @return array<string, array{from:mixed, to:mixed}>
*/
private function mergeDiffs(array $base, array $extra): array
{
foreach ($extra as $field => $change) {
$base[$field] = $change;
}
return $base;
}
/**
* @param iterable<mixed> $items
* @return list<string>
*/
private function normalizeCollection(iterable $items): array
{
$ids = [];
foreach ($items as $item) {
if (\is_object($item) && \method_exists($item, 'getId')) {
$id = $item->getId();
if ($id !== null && $id !== '') {
$ids[] = (string) $id;
}
}
}
sort($ids);
return array_values(array_unique($ids));
}
private function normalizeValue(mixed $value): mixed
{
if ($value === null || \is_scalar($value)) {
return $value;
}
if ($value instanceof \DateTimeInterface) {
return $value->format(\DateTimeInterface::ATOM);
}
if ($value instanceof ModelType) {
return [
'id' => $value->getId(),
'name' => $value->getName(),
'code' => $value->getCode(),
];
}
if ($value instanceof Collection) {
return $this->normalizeCollection($value);
}
if (\is_object($value) && \method_exists($value, 'getId')) {
return (string) $value->getId();
}
if (\is_array($value)) {
return $value;
}
return (string) $value;
}
private function resolveActorProfileId(): ?string
{
$session = $this->requestStack->getSession();
if (!$session instanceof SessionInterface) {
return null;
}
$profileId = $session->get('profileId');
if (!$profileId) {
return null;
}
return (string) $profileId;
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
final class UniqueConstraintSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
KernelEvents::EXCEPTION => 'onKernelException',
];
}
public function onKernelException(ExceptionEvent $event): void
{
$exception = $this->findUniqueConstraintViolation($event->getThrowable());
if (!$exception) {
return;
}
$event->setResponse(new JsonResponse(
[
'success' => false,
'error' => 'nom duplique',
],
JsonResponse::HTTP_CONFLICT
));
}
private function findUniqueConstraintViolation(\Throwable $throwable): ?UniqueConstraintViolationException
{
for ($current = $throwable; $current !== null; $current = $current->getPrevious()) {
if ($current instanceof UniqueConstraintViolationException) {
return $current;
}
}
return null;
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\AuditLog;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<AuditLog>
*/
final class AuditLogRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, AuditLog::class);
}
/**
* @return list<AuditLog>
*/
public function findEntityHistory(string $entityType, string $entityId, int $limit = 100): array
{
return $this->createQueryBuilder('a')
->andWhere('a.entityType = :entityType')
->andWhere('a.entityId = :entityId')
->setParameter('entityType', $entityType)
->setParameter('entityId', $entityId)
->orderBy('a.createdAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Service;
class PdfCompressorService
{
public function compressBase64Pdf(string $base64Data): ?array
{
// Check if qpdf is available
exec('which qpdf', $qpdfPath, $returnCode);
if (0 !== $returnCode) {
return null;
}
// Remove data URI prefix if present
$originalBase64 = $base64Data;
if (str_contains($base64Data, ',')) {
$base64Data = explode(',', $base64Data, 2)[1];
}
$pdfContent = base64_decode($base64Data, true);
if (false === $pdfContent) {
return null;
}
$originalSize = strlen($pdfContent);
// Create temp files
$tempInput = tempnam(sys_get_temp_dir(), 'pdf_in_');
$tempOutput = tempnam(sys_get_temp_dir(), 'pdf_out_');
file_put_contents($tempInput, $pdfContent);
// Compress with qpdf (lossless)
$command = sprintf(
'qpdf --linearize --object-streams=generate %s %s 2>&1',
escapeshellarg($tempInput),
escapeshellarg($tempOutput)
);
exec($command, $cmdOutput, $returnCode);
if (0 !== $returnCode || !file_exists($tempOutput)) {
@unlink($tempInput);
@unlink($tempOutput);
return null;
}
$compressedContent = file_get_contents($tempOutput);
$compressedSize = strlen($compressedContent);
@unlink($tempInput);
@unlink($tempOutput);
// Only return compressed version if it's smaller
if ($compressedSize >= $originalSize) {
return null;
}
// Rebuild with data URI prefix
$newBase64 = 'data:application/pdf;base64,'.base64_encode($compressedContent);
return [
'path' => $newBase64,
'size' => $compressedSize,
'originalSize' => $originalSize,
'saved' => $originalSize - $compressedSize,
];
}
}