diff --git a/API.article.md b/API.article.md index 9322122..efae001 100644 --- a/API.article.md +++ b/API.article.md @@ -95,6 +95,11 @@ Creates a new article under a given parent category, allocating a SKU and a defa | `input.googleAuthId` | `number` | The id of the Google auth user. | | `input.parentId` | `number` | The id of the parent category. | | `input.name` | `string` | The article name. | +| `[input.description]` | `string` | The article description. | +| `[input.desc]` | `string` | Alias for description. | +| `[input.seo]` | `object|string` | The SEO configuration or JSON string. | +| `[input.seoTitle]` | `string` | The SEO title. | +| `[input.seoDescription]` | `string` | The SEO meta description. | | `[input.track_stock=true]` | `boolean|number` | Whether inventory stock is tracked. | | `[tx]` | `object` | Optional transaction to run within. | @@ -184,7 +189,7 @@ Returns the full detail of an article by its SKU, including unit, brand, categor **Returns** -`Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>` — The full article detail. +`Promise<{article: {id: number, sku: string, name: string, slug: string, description: string|null, seo: string|null, seoTitle: string|null, seoDescription: string|null, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>` — The full article detail. **Throws** @@ -664,6 +669,11 @@ Updates an article's name and/or unit (SI and amount), recording history. | `sku` | `string` | The article SKU. | | `patch` | `object` | The update patch. | | `[patch.name]` | `string` | The new article name. | +| `[patch.description]` | `string` | The article description. | +| `[patch.desc]` | `string` | Alias for description. | +| `[patch.seo]` | `object|string` | The SEO configuration or JSON string. | +| `[patch.seoTitle]` | `string` | The SEO title. | +| `[patch.seoDescription]` | `string` | The SEO meta description. | | `[patch.unitSi]` | `string` | The new unit SI code. | | `[patch.unitamount]` | `number` | The new unit amount (must be > 0). | | `[patch.track_stock]` | `boolean|number` | Whether inventory stock is tracked. | diff --git a/API.md b/API.md index 11b018b..5b7f338 100644 --- a/API.md +++ b/API.md @@ -88,10 +88,10 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `addChild(googleAuthId: number, parentSku: string, options: {attach?: boolean, attributeValueId?: number, value?: string, name?: string, unitamount?: number, weight?: number, track_stock?: boolean|number}, tx?: object): Promise` - `addImage(googleAuthId: number, sku: string, image: Buffer, tx?: object): Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>` - `attachChild(googleAuthId: number, parentSku: string, options: {sku: string, childSku?: string, attributeValueId?: number, value?: string, reparent?: boolean, unitamount?: number, weight?: number, track_stock?: boolean|number}, tx?: object): Promise<{article: object, children: Array}>` - - `createUnderParent(input: {googleAuthId: number, parentId: number, name: string, track_stock?: boolean|number}, tx?: object): Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>` + - `createUnderParent(input: {googleAuthId: number, parentId: number, name: string, description?: string, desc?: string, seo?: object|string, seoTitle?: string, seoDescription?: string, track_stock?: boolean|number}, tx?: object): Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>` - `delete(googleAuthId: number, sku: string, optionsOrTx?: object|*, maybeTx?: object): Promise` - `deleteImage(googleAuthId: number, sku: string, fullHash: string, tx?: object): Promise` - - `getBySku(googleAuthId: number, sku: string, tx?: object): Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>` + - `getBySku(googleAuthId: number, sku: string, tx?: object): Promise<{article: {id: number, sku: string, name: string, slug: string, description: string|null, seo: string|null, seoTitle: string|null, seoDescription: string|null, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>` - `getHistoryBySku(googleAuthId: number, sku: string, opts?: {limit?: number}, tx?: object): Promise>` - `importRows(input: {googleAuthId: number, rows: Array}, tx?: object): Promise<{imported: Array, skipped: Array, errors: Array}>` - `listChildren(googleAuthId: number, parentSku: string, tx?: object): Promise>` @@ -108,7 +108,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `setTrackStockBySku(googleAuthId: number, sku: string, trackStock: boolean|number, tx?: object): Promise` - `setWeightBySku(googleAuthId: number, sku: string, weight: number, tx?: object): Promise` - `toggleParent(googleAuthId: number, sku: string, options: {isParent: boolean, attributeNameId?: number, parentAttributeNameId?: number}, tx?: object): Promise` - - `updateBySku(googleAuthId: number, sku: string, patch: {name?: string, unitSi?: string, unitamount?: number, track_stock?: boolean|number}, tx?: object): Promise` + - `updateBySku(googleAuthId: number, sku: string, patch: {name?: string, description?: string, desc?: string, seo?: object|string, seoTitle?: string, seoDescription?: string, unitSi?: string, unitamount?: number, track_stock?: boolean|number}, tx?: object): Promise` - [lang](API.lang.md) - `create(iso639: string, tx?: object): Promise` - `delete(id: number): Promise` diff --git a/src/providers/mssql/queries/article.js b/src/providers/mssql/queries/article.js index 4a36289..9863ff0 100644 --- a/src/providers/mssql/queries/article.js +++ b/src/providers/mssql/queries/article.js @@ -432,6 +432,8 @@ SELECT a.sku, at.name, at.slug, + at.[desc] AS description, + at.seo AS seo, a.unitamount, a.weight, a.track_stock, diff --git a/src/providers/mssql/queries/articleDetail.js b/src/providers/mssql/queries/articleDetail.js index f5c9f38..eb53da9 100644 --- a/src/providers/mssql/queries/articleDetail.js +++ b/src/providers/mssql/queries/articleDetail.js @@ -3,6 +3,8 @@ export const ARTICLE_DETAIL_COLUMNS = ` a.sku, at.name, at.slug, + at.[desc] AS description, + at.seo, a.unitamount, a.weight, a.track_stock, diff --git a/src/providers/mssql/queries/articletext.js b/src/providers/mssql/queries/articletext.js index b7ea523..0a00bea 100644 --- a/src/providers/mssql/queries/articletext.js +++ b/src/providers/mssql/queries/articletext.js @@ -1,11 +1,11 @@ export const CREATE = ` -INSERT INTO articletext (article_id, language_id, name, slug) -VALUES (@articleId, @languageId, @name, @slug) +INSERT INTO articletext (article_id, language_id, name, slug, [desc], seo) +VALUES (@articleId, @languageId, @name, @slug, @desc, @seo) `; export const UPDATE_BY_ARTICLE_AND_LANGUAGE = ` UPDATE articletext -SET name = @name, slug = @slug +SET name = @name, slug = @slug, [desc] = @desc, seo = @seo WHERE article_id = @articleId AND language_id = @languageId `; diff --git a/src/providers/sqlite3/queries/article.js b/src/providers/sqlite3/queries/article.js index 8dc96cd..9336095 100644 --- a/src/providers/sqlite3/queries/article.js +++ b/src/providers/sqlite3/queries/article.js @@ -122,6 +122,7 @@ WITH auth AS ( SELECT auth.*, json_object( 'id', a.id, 'sku', a.sku, 'name', at.name, 'slug', at.slug, + 'description', at."desc", 'seo', at.seo, 'unitamount', a.unitamount, 'weight', a.weight, 'track_stock', a.track_stock, 'unitSi', u.si, 'unitBasesi', u.basesi, diff --git a/src/store/article/addChild.js b/src/store/article/addChild.js index 2f72d6f..966efe0 100644 --- a/src/store/article/addChild.js +++ b/src/store/article/addChild.js @@ -237,6 +237,8 @@ export default function addChild(provider) { languageId, name: childName, slug: childSlug, + desc: null, + seo: null, }); // History on child diff --git a/src/store/article/createUnderParent.js b/src/store/article/createUnderParent.js index ab9116b..7f4e560 100644 --- a/src/store/article/createUnderParent.js +++ b/src/store/article/createUnderParent.js @@ -33,6 +33,11 @@ export default function createUnderParent(provider) { * @param {number} input.googleAuthId - The id of the Google auth user. * @param {number} input.parentId - The id of the parent category. * @param {string} input.name - The article name. + * @param {string} [input.description] - The article description. + * @param {string} [input.desc] - Alias for description. + * @param {object|string} [input.seo] - The SEO configuration or JSON string. + * @param {string} [input.seoTitle] - The SEO title. + * @param {string} [input.seoDescription] - The SEO meta description. * @param {boolean|number} [input.track_stock=true] - Whether inventory stock is tracked. * @param {object} [tx] - Optional transaction to run within. * @returns {Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>} The created article. @@ -41,7 +46,7 @@ export default function createUnderParent(provider) { * const article = await store.article.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Cola' }); * // => { id, parentId, name, sku, track_stock } */ - return async ({ googleAuthId, parentId, name, track_stock, trackStock, sku: inputSku }, tx) => { + return async ({ googleAuthId, parentId, name, description, desc, shortDescription, seo, seoTitle, seoDescription, track_stock, trackStock, sku: inputSku }, tx) => { const rawTrack = track_stock ?? trackStock; const finalTrackStock = rawTrack == null ? 1 : (rawTrack ? 1 : 0); @@ -80,11 +85,31 @@ export default function createUnderParent(provider) { articleId: id, }); + const rawDesc = description !== undefined ? description : desc; + const finalDesc = rawDesc != null && rawDesc !== '' ? String(rawDesc) : null; + let finalSeo = null; + if (shortDescription !== undefined) { + finalSeo = shortDescription != null && shortDescription !== '' ? String(shortDescription) : null; + } else if (typeof seo === 'string') { + finalSeo = seo !== '' ? seo : null; + } else if (seoDescription && !seoTitle) { + finalSeo = String(seoDescription); + } else if (seo != null) { + finalSeo = typeof seo === 'string' ? seo : JSON.stringify(seo); + } else if (seoTitle || seoDescription) { + finalSeo = JSON.stringify({ + title: seoTitle != null && seoTitle !== '' ? String(seoTitle) : '', + description: seoDescription != null && seoDescription !== '' ? String(seoDescription) : '', + }); + } + await runner.query(createArticleTextSql, { articleId: id, languageId, name, slug, + desc: finalDesc, + seo: finalSeo, }); await runner.query(insertHistorySql, { diff --git a/src/store/article/getBySku.js b/src/store/article/getBySku.js index 9511624..3901560 100644 --- a/src/store/article/getBySku.js +++ b/src/store/article/getBySku.js @@ -18,7 +18,7 @@ export default function getBySku(provider) { * @param {number} googleAuthId - The id of the Google auth user. * @param {string} sku - The article SKU. * @param {object} [tx] - Optional transaction. - * @returns {Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>} The full article detail. + * @returns {Promise<{article: {id: number, sku: string, name: string, slug: string, description: string|null, seo: string|null, seoTitle: string|null, seoDescription: string|null, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>} The full article detail. * @throws {Error} If the article is not found or the user is not authorized. * * @example @@ -48,12 +48,33 @@ export default function getBySku(provider) { const a = typeof row.articleJson === 'string' ? JSON.parse(row.articleJson) : row.articleJson; + let seoTitle = null; + let seoDescription = null; + if (a.seo) { + try { + const parsed = typeof a.seo === 'string' ? JSON.parse(a.seo) : a.seo; + if (parsed && typeof parsed === 'object') { + seoTitle = parsed.title ?? parsed.seoTitle ?? null; + seoDescription = parsed.description ?? parsed.seoDescription ?? null; + } else { + seoDescription = String(a.seo); + } + } catch { + seoDescription = String(a.seo); + } + } + return { article: { id: a.id, sku: a.sku, name: a.name, slug: a.slug, + description: a.description ?? null, + shortDescription: a.seo ?? null, + seo: a.seo ?? null, + seoTitle, + seoDescription: seoDescription ?? (a.seo ? String(a.seo) : null), unitamount: a.unitamount, weight: a.weight, track_stock: Boolean(a.track_stock ?? 1), diff --git a/src/store/article/importRows.js b/src/store/article/importRows.js index ed7c4c6..a560f34 100644 --- a/src/store/article/importRows.js +++ b/src/store/article/importRows.js @@ -206,6 +206,8 @@ export default function importRows(provider) { languageId, name, slug, + desc: row.description ?? row.desc ?? null, + seo: null, }); await runner.query(insertHistorySql, { diff --git a/src/store/article/updateBySku.js b/src/store/article/updateBySku.js index d928a7b..6b36b09 100644 --- a/src/store/article/updateBySku.js +++ b/src/store/article/updateBySku.js @@ -40,6 +40,11 @@ export default function updateBySku(provider) { * @param {string} sku - The article SKU. * @param {object} patch - The update patch. * @param {string} [patch.name] - The new article name. + * @param {string} [patch.description] - The article description. + * @param {string} [patch.desc] - Alias for description. + * @param {object|string} [patch.seo] - The SEO configuration or JSON string. + * @param {string} [patch.seoTitle] - The SEO title. + * @param {string} [patch.seoDescription] - The SEO meta description. * @param {string} [patch.unitSi] - The new unit SI code. * @param {number} [patch.unitamount] - The new unit amount (must be > 0). * @param {boolean|number} [patch.track_stock] - Whether inventory stock is tracked. @@ -58,8 +63,15 @@ export default function updateBySku(provider) { const hasTrackStock = Object.prototype.hasOwnProperty.call(patch, 'track_stock') || Object.prototype.hasOwnProperty.call(patch, 'trackStock'); + const hasDescription = + Object.prototype.hasOwnProperty.call(patch, 'description') || + Object.prototype.hasOwnProperty.call(patch, 'desc'); + const hasShortDescription = Object.prototype.hasOwnProperty.call(patch, 'shortDescription'); + const hasSeo = Object.prototype.hasOwnProperty.call(patch, 'seo'); + const hasSeoTitle = Object.prototype.hasOwnProperty.call(patch, 'seoTitle'); + const hasSeoDescription = Object.prototype.hasOwnProperty.call(patch, 'seoDescription'); - if (!hasName && !hasUnitamount && !hasUnitSi && !hasTrackStock) { + if (!hasName && !hasUnitamount && !hasUnitSi && !hasTrackStock && !hasDescription && !hasShortDescription && !hasSeo && !hasSeoTitle && !hasSeoDescription) { throw new Error('Name required'); } @@ -143,19 +155,67 @@ export default function updateBySku(provider) { ); } } + if (hasDescription) { + const oldDesc = current.description ?? current.desc ?? ''; + const rawDesc = patch.description !== undefined ? patch.description : patch.desc; + const newDesc = rawDesc != null ? String(rawDesc) : ''; + if (oldDesc !== newDesc) { + historyLines.push('Beschreibung geändert'); + } + } + + const needsTextUpdate = hasName || hasDescription || hasSeo || hasSeoTitle || hasSeoDescription; + if (needsTextUpdate) { + const finalName = hasName ? patch.name.trim() : (current.name || ''); + const slug = hasName + ? await resolveSlug(runner, { + companyId, + languageId, + name: patch.name, + excludeArticleId: articleId, + }) + : (current.slug || ''); + + let finalDesc = current.description ?? current.desc ?? null; + if (hasDescription) { + const rawDesc = patch.description !== undefined ? patch.description : patch.desc; + finalDesc = rawDesc != null && rawDesc !== '' ? String(rawDesc) : null; + } + + let finalSeo = current.seo ?? null; + if (hasShortDescription) { + finalSeo = patch.shortDescription != null && patch.shortDescription !== '' ? String(patch.shortDescription) : null; + } else if (hasSeo && typeof patch.seo === 'string') { + finalSeo = patch.seo !== '' ? patch.seo : null; + } else if (hasSeoDescription && !hasSeoTitle) { + finalSeo = patch.seoDescription != null && patch.seoDescription !== '' ? String(patch.seoDescription) : null; + } else if (hasSeo || hasSeoTitle || hasSeoDescription) { + let seoObj = {}; + if (current.seo) { + try { + const parsed = typeof current.seo === 'string' ? JSON.parse(current.seo) : current.seo; + if (parsed && typeof parsed === 'object') seoObj = parsed; + } catch {} + } + if (hasSeo && patch.seo && typeof patch.seo === 'object') { + seoObj = { ...seoObj, ...patch.seo }; + } + if (hasSeoTitle) { + seoObj.title = patch.seoTitle != null && patch.seoTitle !== '' ? String(patch.seoTitle) : ''; + } + if (hasSeoDescription) { + seoObj.description = patch.seoDescription != null && patch.seoDescription !== '' ? String(patch.seoDescription) : ''; + } + finalSeo = Object.keys(seoObj).length > 0 ? JSON.stringify(seoObj) : null; + } - if (hasName) { - const slug = await resolveSlug(runner, { - companyId, - languageId, - name: patch.name, - excludeArticleId: articleId, - }); const update = await runner.query(updateTextSql, { articleId, languageId, - name: patch.name.trim(), + name: finalName, slug, + desc: finalDesc, + seo: finalSeo, }); if (!update.rowsAffected) {