From ec060f5c623c166c3334d3ae24b58d925f5de1cb Mon Sep 17 00:00:00 2001 From: seb Date: Sun, 4 Oct 2026 21:45:49 +0200 Subject: [PATCH] u --- docs/api/routes/article/_sku/parent.md | 8 ++-- scripts/migrate-parent-rename.mssql.sql | 56 ++++++++++++++++++++----- src/api/routes/article.js | 16 +++++-- 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/docs/api/routes/article/_sku/parent.md b/docs/api/routes/article/_sku/parent.md index 4e69497..71f7bdb 100644 --- a/docs/api/routes/article/_sku/parent.md +++ b/docs/api/routes/article/_sku/parent.md @@ -54,11 +54,13 @@ Updated article | Status | Description | |--------|-------------| -| 400 | Invalid input, `attributeNameId` missing while `isParent` is true, article still -has children, article has stock/suppliers/a Stückliste/options, or the attribute is -already used by an ancestor parent article. +| 400 | Invalid input — `attributeNameId` missing while `isParent` is true, article still +has children (disabling), or the attribute is already used by an ancestor parent. | | 404 | Article or attribute name not found | +| 409 | Article state conflicts with becoming a variant parent — it still has stock, +assigned suppliers, a Stückliste, or options. Clear these first. + | --- diff --git a/scripts/migrate-parent-rename.mssql.sql b/scripts/migrate-parent-rename.mssql.sql index 9974b80..830c0c4 100644 --- a/scripts/migrate-parent-rename.mssql.sql +++ b/scripts/migrate-parent-rename.mssql.sql @@ -3,56 +3,90 @@ -- One-off manual migration (no migration code is kept in the repo). -- sqlcmd -S -d -E -i scripts/migrate-parent-rename.mssql.sql -- --- Wrapped in a transaction and guarded so it is safe to re-run. --- Drop the existing index explicitly: SQL Server will not let a filtered --- unique index be carried over to a renamed column. +-- Guarded and safe to re-run. Safe to retry after a partial run. +-- +-- NOTE ON BATCHING: sp_rename is deferred and its effect is not visible to +-- subsequent statements in the SAME batch. Each rename therefore runs in its +-- own batch (GO), and constraint/index recreation happens after a GO so the +-- new column names resolve. SET XACT_ABORT ON; +GO + BEGIN TRANSACTION; +GO -- Drop the old filtered unique index (references fathersort). IF EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'UQ_article_fatherarticle_fathersort') DROP INDEX UQ_article_fatherarticle_fathersort ON article; +GO -- Drop the default constraints bound to the old column names. IF EXISTS (SELECT 1 FROM sys.default_constraints WHERE name = 'DF_article_is_father') ALTER TABLE article DROP CONSTRAINT DF_article_is_father; IF EXISTS (SELECT 1 FROM sys.default_constraints WHERE name = 'DF_article_fathersort') ALTER TABLE article DROP CONSTRAINT DF_article_fathersort; +GO -- Drop foreign keys bound to the old columns. IF EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_article_fatherarticle') ALTER TABLE article DROP CONSTRAINT FK_article_fatherarticle; IF EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_article_fatherattributename') ALTER TABLE article DROP CONSTRAINT FK_article_fatherattributename; +GO --- Rename the columns (sp_rename keeps indexes/FKs tracking the column). +-- Rename the columns (each sp_rename must be its own batch). IF COL_LENGTH('article', 'fatherarticle_id') IS NOT NULL AND COL_LENGTH('article', 'parentarticle_id') IS NULL EXEC sp_rename 'article.fatherarticle_id', 'parentarticle_id', 'COLUMN'; +GO + IF COL_LENGTH('article', 'fatherattributename_id') IS NOT NULL AND COL_LENGTH('article', 'parentattributename_id') IS NULL EXEC sp_rename 'article.fatherattributename_id', 'parentattributename_id', 'COLUMN'; +GO + IF COL_LENGTH('article', 'fathersort') IS NOT NULL AND COL_LENGTH('article', 'parentsort') IS NULL EXEC sp_rename 'article.fathersort', 'parentsort', 'COLUMN'; +GO + IF COL_LENGTH('article', 'is_father') IS NOT NULL AND COL_LENGTH('article', 'is_parent') IS NULL EXEC sp_rename 'article.is_father', 'is_parent', 'COLUMN'; +GO --- Recreate the constraints under their new names. +COMMIT TRANSACTION; +GO + +-- Recreate the constraints under their new names (after the rename batches). IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_article_parentarticle') ALTER TABLE article ADD CONSTRAINT FK_article_parentarticle FOREIGN KEY (parentarticle_id) REFERENCES article(id) ON DELETE NO ACTION; +GO + IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_article_parentattributename') ALTER TABLE article ADD CONSTRAINT FK_article_parentattributename FOREIGN KEY (parentattributename_id) REFERENCES attributename(id) ON DELETE NO ACTION; +GO -ALTER TABLE article ADD CONSTRAINT DF_article_is_parent DEFAULT (0) FOR is_parent; -ALTER TABLE article ADD CONSTRAINT DF_article_parentsort DEFAULT (0) FOR parentsort; +IF NOT EXISTS (SELECT 1 FROM sys.default_constraints WHERE name = 'DF_article_is_parent') + ALTER TABLE article ADD CONSTRAINT DF_article_is_parent DEFAULT (0) FOR is_parent; +GO -CREATE UNIQUE INDEX UQ_article_parentarticle_parentsort - ON article (parentarticle_id, parentsort) - WHERE parentarticle_id IS NOT NULL; +IF NOT EXISTS (SELECT 1 FROM sys.default_constraints WHERE name = 'DF_article_parentsort') + ALTER TABLE article ADD CONSTRAINT DF_article_parentsort DEFAULT (0) FOR parentsort; +GO -COMMIT TRANSACTION; \ No newline at end of file +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'UQ_article_parentarticle_parentsort') + CREATE UNIQUE INDEX UQ_article_parentarticle_parentsort + ON article (parentarticle_id, parentsort) + WHERE parentarticle_id IS NOT NULL; +GO + +-- Verify +SELECT 'COL:' + c.name AS result +FROM sys.columns c +WHERE c.object_id = OBJECT_ID('article') + AND (c.name LIKE '%father%' OR c.name LIKE '%parent%'); +GO \ No newline at end of file diff --git a/src/api/routes/article.js b/src/api/routes/article.js index 7adea0d..91120ae 100644 --- a/src/api/routes/article.js +++ b/src/api/routes/article.js @@ -81,6 +81,13 @@ function articleErrorStatus(error) { if (error.message === 'Attribute name not found') return 404; if (error.message === 'attributeNameId is required to become a parent article') return 400; if (error.message?.includes('already used by an ancestor parent')) return 400; + // Variant-parent preconditions: a parent article may not carry stock, + // suppliers, a Stückliste or options. These are user-correctable 409s, + // not server faults — without these the route returns a bare 500. + if (error.message === 'An article with existing stock cannot become a parent article') return 409; + if (error.message === 'An article with assigned suppliers cannot become a parent article') return 409; + if (error.message === 'A Stückliste article cannot become a parent') return 409; + if (error.message === 'An article with options cannot become a parent') return 409; if (error.message?.includes('cannot have attribute values for its variant attribute')) return 400; if (error.message?.includes('A parent article cannot have') || error.message?.includes('parent article cannot have')) return 400; if (error.message === 'Cannot disable parent mode on an article that still has children') return 400; @@ -1855,9 +1862,12 @@ export function register(app, basePath = '') { * $ref: '#/components/schemas/Article' * 400: * description: | - * Invalid input, `attributeNameId` missing while `isParent` is true, article still - * has children, article has stock/suppliers/a Stückliste/options, or the attribute is - * already used by an ancestor parent article. + * Invalid input — `attributeNameId` missing while `isParent` is true, article still + * has children (disabling), or the attribute is already used by an ancestor parent. + * 409: + * description: | + * Article state conflicts with becoming a variant parent — it still has stock, + * assigned suppliers, a Stückliste, or options. Clear these first. * 404: * description: Article or attribute name not found */