diff --git a/API.articleattributevalue.md b/API.articleattributevalue.md new file mode 100644 index 0000000..c70896b --- /dev/null +++ b/API.articleattributevalue.md @@ -0,0 +1,40 @@ +# articleattributevalue + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `articleattributevalue.listByArticleSku` + +*Source: `src/store/articleattributevalue/listByArticleSku.js`* + +Lists the attribute values assigned to an article by its SKU. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise>` + +## `articleattributevalue.setByArticleSku` + +*Source: `src/store/articleattributevalue/setByArticleSku.js`* + +Replaces all attribute values for an article by its SKU. Validates company ownership and enforces the `exclusive` flag on attributes. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `values` | `Array` | Attribute value IDs or items. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise>` diff --git a/API.articleproperty.md b/API.articleproperty.md new file mode 100644 index 0000000..684142b --- /dev/null +++ b/API.articleproperty.md @@ -0,0 +1,59 @@ +# articleproperty + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `articleproperty.delete` + +*Source: `src/store/articleproperty/delete.js`* + +Deletes a property from an article by SKU and propertyId. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `propertyId` | `number` | The property id. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise` + +## `articleproperty.listByArticleSku` + +*Source: `src/store/articleproperty/listByArticleSku.js`* + +Lists the properties of an article by its SKU. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise>` + +## `articleproperty.setByArticleSku` + +*Source: `src/store/articleproperty/setByArticleSku.js`* + +Replaces all properties for an article by its SKU. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `properties` | `Array<{propertyId: number, value: string, sort?: number}>` | The properties to set. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise>` diff --git a/API.attributename.md b/API.attributename.md new file mode 100644 index 0000000..949e5a6 --- /dev/null +++ b/API.attributename.md @@ -0,0 +1,100 @@ +# attributename + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `attributename.create` + +*Source: `src/store/attributename/create.js`* + +Creates an attribute name for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | The attribute name input. | +| `input.name` | `string` | The attribute name. | +| `[input.sort]` | `number` | Optional sort order. | +| `[input.exclusive=false]` | `boolean` | Whether values are mutually exclusive on an article. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>` + +## `attributename.delete` + +*Source: `src/store/attributename/delete.js`* + +Deletes an attribute name for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The attribute name id. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise` + +## `attributename.list` + +*Source: `src/store/attributename/list.js`* + +Lists the attribute names for the user's selected company, including values. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise}>>` + +## `attributename.order` + +*Source: `src/store/attributename/order.js`* + +Reorders the attribute names for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | Reorder input. | +| `input.attributenameIds` | `Array` | IDs in their new order. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise<{attributenameIds: Array}>` + +## `attributename.update` + +*Source: `src/store/attributename/update.js`* + +Updates an attribute name for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The attribute name id. | +| `input` | `object` | Update patch. | +| `[input.name]` | `string` | New name. | +| `[input.sort]` | `number` | New sort. | +| `[input.exclusive]` | `boolean` | New exclusive flag. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>` diff --git a/API.attributevalue.md b/API.attributevalue.md new file mode 100644 index 0000000..6fb0f37 --- /dev/null +++ b/API.attributevalue.md @@ -0,0 +1,101 @@ +# attributevalue + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `attributevalue.create` + +*Source: `src/store/attributevalue/create.js`* + +Creates an attribute value under a given attribute. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `attributeId` | `number` | The attribute id. | +| `input` | `object` | Input value data. | +| `input.value` | `string` | The value text. | +| `[input.sort]` | `number` | Optional sort order. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise<{id: number, attributeId: number, value: string, sort: number}>` + +## `attributevalue.delete` + +*Source: `src/store/attributevalue/delete.js`* + +Deletes an attribute value. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The attribute value id. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise` + +## `attributevalue.listByAttributeId` + +*Source: `src/store/attributevalue/listByAttributeId.js`* + +Lists the values for an attribute. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `attributeId` | `number` | The attribute id. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise>` + +## `attributevalue.order` + +*Source: `src/store/attributevalue/order.js`* + +Reorders attribute values under an attribute. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `attributeId` | `number` | The attribute id. | +| `input` | `object` | Reorder input. | +| `input.attributevalueIds` | `Array` | Value IDs in their new order. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise<{attributevalueIds: Array}>` + +## `attributevalue.update` + +*Source: `src/store/attributevalue/update.js`* + +Updates an attribute value. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The attribute value id. | +| `input` | `object` | Update patch. | +| `[input.value]` | `string` | New value text. | +| `[input.sort]` | `number` | New sort order. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise<{id: number, attributeId: number, value: string, sort: number}>` diff --git a/API.md b/API.md index cdaa531..4dc071b 100644 --- a/API.md +++ b/API.md @@ -217,11 +217,36 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - [numbers](API.numbers.md) - `list(googleAuthId: number, tx?: object): Promise>` - `update(googleAuthId: number, id: number, input: {prefix?: string, padding?: number, next?: number}, tx?: object): Promise<{id: number, type: string, prefix: string, padding: number, next: number}>` +- [articleattributevalue](API.articleattributevalue.md) + - `listByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise>` + - `setByArticleSku(googleAuthId: number, sku: string, values: Array, tx?: object): Promise>` +- [articleproperty](API.articleproperty.md) + - `delete(googleAuthId: number, sku: string, propertyId: number, tx?: object): Promise` + - `listByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise>` + - `setByArticleSku(googleAuthId: number, sku: string, properties: Array<{propertyId: number, value: string, sort?: number}>, tx?: object): Promise>` +- [attributename](API.attributename.md) + - `create(googleAuthId: number, input: {name: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>` + - `delete(googleAuthId: number, id: number, tx?: object): Promise` + - `list(googleAuthId: number, tx?: object): Promise}>>` + - `order(googleAuthId: number, input: {attributenameIds: Array}, tx?: object): Promise<{attributenameIds: Array}>` + - `update(googleAuthId: number, id: number, input: {name?: string, sort?: number, exclusive?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>` +- [attributevalue](API.attributevalue.md) + - `create(googleAuthId: number, attributeId: number, input: {value: string, sort?: number}, tx?: object): Promise<{id: number, attributeId: number, value: string, sort: number}>` + - `delete(googleAuthId: number, id: number, tx?: object): Promise` + - `listByAttributeId(googleAuthId: number, attributeId: number, tx?: object): Promise>` + - `order(googleAuthId: number, attributeId: number, input: {attributevalueIds: Array}, tx?: object): Promise<{attributevalueIds: Array}>` + - `update(googleAuthId: number, id: number, input: {value?: string, sort?: number}, tx?: object): Promise<{id: number, attributeId: number, value: string, sort: number}>` - [labeltemplate](API.labeltemplate.md) - `create(googleAuthId: number, input: {name: string, width: number, height: number, dpi: number, type: string, content: object, thumb?: Buffer|null}, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>` - `delete(googleAuthId: number, id: number, tx?: object): Promise` - `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}|undefined>` - `list(googleAuthId: number, tx?: object): Promise>` - `update(googleAuthId: number, id: number, input: {name?: string, width?: number, height?: number, dpi?: number, type?: string, content?: object, thumb?: Buffer|null}, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>` +- [propertyname](API.propertyname.md) + - `create(googleAuthId: number, input: {name: string, sort?: number}, tx?: object): Promise<{id: number, name: string, sort: number}>` + - `delete(googleAuthId: number, id: number, tx?: object): Promise` + - `list(googleAuthId: number, tx?: object): Promise>` + - `order(googleAuthId: number, input: {propertynameIds: Array}, tx?: object): Promise<{propertynameIds: Array}>` + - `update(googleAuthId: number, id: number, input: {name?: string, sort?: number}, tx?: object): Promise<{id: number, name: string, sort: number}>` - [runner.js](API.runner.js.md) - `runner(provider: import('../providers/provider.js').DatabaseProvider, tx?: object): ((sql: string, params?: object) => Promise) & { query: Function, dialect?: string }` diff --git a/API.propertyname.md b/API.propertyname.md new file mode 100644 index 0000000..a663c2b --- /dev/null +++ b/API.propertyname.md @@ -0,0 +1,98 @@ +# propertyname + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `propertyname.create` + +*Source: `src/store/propertyname/create.js`* + +Creates a property name for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | The property name input. | +| `input.name` | `string` | The property name. | +| `[input.sort]` | `number` | Optional sort order. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise<{id: number, name: string, sort: number}>` + +## `propertyname.delete` + +*Source: `src/store/propertyname/delete.js`* + +Deletes a property name for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The property name id. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise` + +## `propertyname.list` + +*Source: `src/store/propertyname/list.js`* + +Lists the property names for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise>` + +## `propertyname.order` + +*Source: `src/store/propertyname/order.js`* + +Reorders the property names for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | Reorder input. | +| `input.propertynameIds` | `Array` | IDs in their new order. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise<{propertynameIds: Array}>` + +## `propertyname.update` + +*Source: `src/store/propertyname/update.js`* + +Updates a property name for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The property name id. | +| `input` | `object` | Update patch. | +| `[input.name]` | `string` | New name. | +| `[input.sort]` | `number` | New sort. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise<{id: number, name: string, sort: number}>` diff --git a/src/index.js b/src/index.js index 50bd256..21afe36 100644 --- a/src/index.js +++ b/src/index.js @@ -147,6 +147,26 @@ import labeltemplateList from './store/labeltemplate/list.js'; import labeltemplateUpdate from './store/labeltemplate/update.js'; import labeltemplateDelete from './store/labeltemplate/delete.js'; import stockAdjust from './store/stock/adjust.js'; +import propertynameList from './store/propertyname/list.js'; +import propertynameCreate from './store/propertyname/create.js'; +import propertynameUpdate from './store/propertyname/update.js'; +import propertynameOrder from './store/propertyname/order.js'; +import propertynameDelete from './store/propertyname/delete.js'; +import articlepropertyListByArticleSku from './store/articleproperty/listByArticleSku.js'; +import articlepropertySetByArticleSku from './store/articleproperty/setByArticleSku.js'; +import articlepropertyDelete from './store/articleproperty/delete.js'; +import attributenameList from './store/attributename/list.js'; +import attributenameCreate from './store/attributename/create.js'; +import attributenameUpdate from './store/attributename/update.js'; +import attributenameOrder from './store/attributename/order.js'; +import attributenameDelete from './store/attributename/delete.js'; +import attributevalueListByAttributeId from './store/attributevalue/listByAttributeId.js'; +import attributevalueCreate from './store/attributevalue/create.js'; +import attributevalueUpdate from './store/attributevalue/update.js'; +import attributevalueOrder from './store/attributevalue/order.js'; +import attributevalueDelete from './store/attributevalue/delete.js'; +import articleattributevalueListByArticleSku from './store/articleattributevalue/listByArticleSku.js'; +import articleattributevalueSetByArticleSku from './store/articleattributevalue/setByArticleSku.js'; export default class LibDashStore { /** @@ -399,6 +419,41 @@ export default class LibDashStore { }; this.adjustment = this.stock; + this.propertyname = { + list: propertynameList(provider), + create: propertynameCreate(provider), + update: propertynameUpdate(provider), + order: propertynameOrder(provider), + delete: propertynameDelete(provider), + }; + + this.articleproperty = { + listByArticleSku: articlepropertyListByArticleSku(provider), + setByArticleSku: articlepropertySetByArticleSku(provider), + delete: articlepropertyDelete(provider), + }; + + this.attributename = { + list: attributenameList(provider), + create: attributenameCreate(provider), + update: attributenameUpdate(provider), + order: attributenameOrder(provider), + delete: attributenameDelete(provider), + }; + + this.attributevalue = { + listByAttributeId: attributevalueListByAttributeId(provider), + create: attributevalueCreate(provider), + update: attributevalueUpdate(provider), + order: attributevalueOrder(provider), + delete: attributevalueDelete(provider), + }; + + this.articleattributevalue = { + listByArticleSku: articleattributevalueListByArticleSku(provider), + setByArticleSku: articleattributevalueSetByArticleSku(provider), + }; + this.purge = storePurge(provider); this.initialize = storeInitialize(provider); diff --git a/src/providers/mssql/purgeSql.js b/src/providers/mssql/purgeSql.js index f882166..d4073b9 100644 --- a/src/providers/mssql/purgeSql.js +++ b/src/providers/mssql/purgeSql.js @@ -1,5 +1,10 @@ /** Drop order = reverse of schema create dependency order. */ export const TABLES = [ + 'articleattributevalue', + 'articleproperty', + 'attributevalue', + 'attributename', + 'propertyname', 'manifestattachment', 'labeltemplate', 'file', diff --git a/src/providers/mssql/queries/articleattributevalue.js b/src/providers/mssql/queries/articleattributevalue.js new file mode 100644 index 0000000..5c02cfc --- /dev/null +++ b/src/providers/mssql/queries/articleattributevalue.js @@ -0,0 +1,41 @@ +export const LIST_BY_ARTICLE_ID = ` +SELECT + aav.id, + aav.article_id AS articleId, + an.id AS attributeId, + an.name AS attributeName, + an.exclusive, + av.id AS attributeValueId, + av.value, + aav.sort +FROM articleattributevalue aav +INNER JOIN attributevalue av ON av.id = aav.attributevalue_id +INNER JOIN attributename an ON an.id = av.attribute_id +WHERE aav.article_id = @articleId +ORDER BY an.sort ASC, an.id ASC, aav.sort ASC, aav.id ASC +`; + +export const DELETE_BY_ARTICLE_ID = ` +DELETE FROM articleattributevalue +WHERE article_id = @articleId +`; + +export const INSERT_ROW = ` +INSERT INTO articleattributevalue (article_id, attributevalue_id, sort) +OUTPUT INSERTED.id, INSERTED.article_id AS articleId, INSERTED.attributevalue_id AS attributeValueId, INSERTED.sort +VALUES (@articleId, @attributeValueId, @sort) +`; + +export const GET_VALUE_DETAIL = ` +SELECT + av.id AS attributeValueId, + av.value, + av.sort, + an.id AS attributeId, + an.name AS attributeName, + an.exclusive, + an.company_id AS companyId +FROM attributevalue av +INNER JOIN attributename an ON an.id = av.attribute_id +WHERE av.id = @attributeValueId +`; diff --git a/src/providers/mssql/queries/articleproperty.js b/src/providers/mssql/queries/articleproperty.js new file mode 100644 index 0000000..cdcd456 --- /dev/null +++ b/src/providers/mssql/queries/articleproperty.js @@ -0,0 +1,29 @@ +export const LIST_BY_ARTICLE_ID = ` +SELECT + ap.id, + ap.article_id AS articleId, + ap.property_id AS propertyId, + pn.name AS name, + ap.value, + ap.sort +FROM articleproperty ap +INNER JOIN propertyname pn ON pn.id = ap.property_id +WHERE ap.article_id = @articleId +ORDER BY ap.sort ASC, ap.id ASC +`; + +export const DELETE_BY_ARTICLE_ID = ` +DELETE FROM articleproperty +WHERE article_id = @articleId +`; + +export const DELETE_BY_ARTICLE_AND_PROPERTY_ID = ` +DELETE FROM articleproperty +WHERE article_id = @articleId AND property_id = @propertyId +`; + +export const INSERT_ROW = ` +INSERT INTO articleproperty (article_id, property_id, value, sort) +OUTPUT INSERTED.id, INSERTED.article_id AS articleId, INSERTED.property_id AS propertyId, INSERTED.value, INSERTED.sort +VALUES (@articleId, @propertyId, @value, @sort) +`; diff --git a/src/providers/mssql/queries/attributename.js b/src/providers/mssql/queries/attributename.js new file mode 100644 index 0000000..3d5bc44 --- /dev/null +++ b/src/providers/mssql/queries/attributename.js @@ -0,0 +1,88 @@ +export const LIST_BY_COMPANY = ` +SELECT + an.id, + an.name, + an.sort, + an.exclusive, + ( + SELECT + av.id, + av.value, + av.sort + FROM attributevalue av + WHERE av.attribute_id = an.id + ORDER BY av.sort ASC, av.id ASC + FOR JSON PATH + ) AS valuesJson +FROM attributename an +WHERE an.company_id = @companyId +ORDER BY an.sort ASC, an.id ASC +`; + +export const LIST_IDS_BY_COMPANY = ` +SELECT id +FROM attributename +WHERE company_id = @companyId +ORDER BY sort ASC, id ASC +`; + +export const GET_BY_COMPANY_AND_ID = ` +SELECT + an.id, + an.company_id, + an.name, + an.sort, + an.exclusive, + ( + SELECT + av.id, + av.value, + av.sort + FROM attributevalue av + WHERE av.attribute_id = an.id + ORDER BY av.sort ASC, av.id ASC + FOR JSON PATH + ) AS valuesJson +FROM attributename an +WHERE an.id = @id AND an.company_id = @companyId +`; + +export const GET_BY_COMPANY_AND_NAME = ` +SELECT + id, + company_id, + name, + sort, + exclusive +FROM attributename +WHERE name = @name AND company_id = @companyId +`; + +export const GET_NEXT_SORT = ` +SELECT ISNULL(MAX(sort), -1) + 1 AS sort +FROM attributename +WHERE company_id = @companyId +`; + +export const CREATE = ` +INSERT INTO attributename (company_id, name, sort, exclusive) +OUTPUT INSERTED.id, INSERTED.name, INSERTED.sort, INSERTED.exclusive +VALUES (@companyId, @name, @sort, @exclusive) +`; + +export const UPDATE_BY_ID = ` +UPDATE attributename +SET name = @name, sort = @sort, exclusive = @exclusive +WHERE id = @id AND company_id = @companyId +`; + +export const UPDATE_SORT = ` +UPDATE attributename +SET sort = @sort +WHERE id = @id AND company_id = @companyId +`; + +export const DELETE_BY_COMPANY_AND_ID = ` +DELETE FROM attributename +WHERE id = @id AND company_id = @companyId +`; diff --git a/src/providers/mssql/queries/attributevalue.js b/src/providers/mssql/queries/attributevalue.js new file mode 100644 index 0000000..50089a8 --- /dev/null +++ b/src/providers/mssql/queries/attributevalue.js @@ -0,0 +1,70 @@ +export const LIST_BY_ATTRIBUTE_ID = ` +SELECT + av.id, + av.attribute_id AS attributeId, + av.value, + av.sort +FROM attributevalue av +INNER JOIN attributename an ON an.id = av.attribute_id +WHERE av.attribute_id = @attributeId AND an.company_id = @companyId +ORDER BY av.sort ASC, av.id ASC +`; + +export const LIST_IDS_BY_ATTRIBUTE_ID = ` +SELECT av.id +FROM attributevalue av +INNER JOIN attributename an ON an.id = av.attribute_id +WHERE av.attribute_id = @attributeId AND an.company_id = @companyId +ORDER BY av.sort ASC, av.id ASC +`; + +export const GET_BY_ID_AND_COMPANY = ` +SELECT + av.id, + av.attribute_id AS attributeId, + av.value, + av.sort, + an.company_id AS companyId +FROM attributevalue av +INNER JOIN attributename an ON an.id = av.attribute_id +WHERE av.id = @id AND an.company_id = @companyId +`; + +export const GET_BY_ATTRIBUTE_AND_VALUE = ` +SELECT + av.id, + av.attribute_id AS attributeId, + av.value, + av.sort +FROM attributevalue av +WHERE av.attribute_id = @attributeId AND av.value = @value +`; + +export const GET_NEXT_SORT = ` +SELECT ISNULL(MAX(sort), -1) + 1 AS sort +FROM attributevalue +WHERE attribute_id = @attributeId +`; + +export const CREATE = ` +INSERT INTO attributevalue (attribute_id, value, sort) +OUTPUT INSERTED.id, INSERTED.attribute_id AS attributeId, INSERTED.value, INSERTED.sort +VALUES (@attributeId, @value, @sort) +`; + +export const UPDATE_BY_ID = ` +UPDATE attributevalue +SET value = @value, sort = @sort +WHERE id = @id +`; + +export const UPDATE_SORT = ` +UPDATE attributevalue +SET sort = @sort +WHERE id = @id +`; + +export const DELETE_BY_ID = ` +DELETE FROM attributevalue +WHERE id = @id +`; diff --git a/src/providers/mssql/queries/propertyname.js b/src/providers/mssql/queries/propertyname.js new file mode 100644 index 0000000..4e63cdc --- /dev/null +++ b/src/providers/mssql/queries/propertyname.js @@ -0,0 +1,65 @@ +export const LIST_BY_COMPANY = ` +SELECT + id, + name, + sort +FROM propertyname +WHERE company_id = @companyId +ORDER BY sort ASC, id ASC +`; + +export const LIST_IDS_BY_COMPANY = ` +SELECT id +FROM propertyname +WHERE company_id = @companyId +ORDER BY sort ASC, id ASC +`; + +export const GET_BY_COMPANY_AND_ID = ` +SELECT + id, + company_id, + name, + sort +FROM propertyname +WHERE id = @id AND company_id = @companyId +`; + +export const GET_BY_COMPANY_AND_NAME = ` +SELECT + id, + company_id, + name, + sort +FROM propertyname +WHERE name = @name AND company_id = @companyId +`; + +export const GET_NEXT_SORT = ` +SELECT ISNULL(MAX(sort), -1) + 1 AS sort +FROM propertyname +WHERE company_id = @companyId +`; + +export const CREATE = ` +INSERT INTO propertyname (company_id, name, sort) +OUTPUT INSERTED.id, INSERTED.name, INSERTED.sort +VALUES (@companyId, @name, @sort) +`; + +export const UPDATE_BY_ID = ` +UPDATE propertyname +SET name = @name, sort = @sort +WHERE id = @id AND company_id = @companyId +`; + +export const UPDATE_SORT = ` +UPDATE propertyname +SET sort = @sort +WHERE id = @id AND company_id = @companyId +`; + +export const DELETE_BY_COMPANY_AND_ID = ` +DELETE FROM propertyname +WHERE id = @id AND company_id = @companyId +`; diff --git a/src/providers/mssql/schema.js b/src/providers/mssql/schema.js index 82a9c54..e5a7e49 100644 --- a/src/providers/mssql/schema.js +++ b/src/providers/mssql/schema.js @@ -894,6 +894,80 @@ END '); `; +export const INIT_PROPERTYNAME = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'propertyname') +BEGIN + CREATE TABLE propertyname ( + id INT IDENTITY(1,1) PRIMARY KEY, + company_id INT NOT NULL, + name NVARCHAR(255) NOT NULL, + sort INT NOT NULL CONSTRAINT DF_propertyname_sort DEFAULT 0, + CONSTRAINT FK_propertyname_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE, + CONSTRAINT UQ_propertyname_company_name UNIQUE (company_id, name) + ); +END +`; + +export const INIT_ARTICLEPROPERTY = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articleproperty') +BEGIN + CREATE TABLE articleproperty ( + id INT IDENTITY(1,1) PRIMARY KEY, + article_id INT NOT NULL, + property_id INT NOT NULL, + value NVARCHAR(MAX) NOT NULL, + sort INT NOT NULL CONSTRAINT DF_articleproperty_sort DEFAULT 0, + CONSTRAINT FK_articleproperty_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + CONSTRAINT FK_articleproperty_property FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE CASCADE, + CONSTRAINT UQ_articleproperty_article_property UNIQUE (article_id, property_id) + ); +END +`; + +export const INIT_ATTRIBUTENAME = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'attributename') +BEGIN + CREATE TABLE attributename ( + id INT IDENTITY(1,1) PRIMARY KEY, + company_id INT NOT NULL, + name NVARCHAR(255) NOT NULL, + sort INT NOT NULL CONSTRAINT DF_attributename_sort DEFAULT 0, + exclusive BIT NOT NULL CONSTRAINT DF_attributename_exclusive DEFAULT 0, + CONSTRAINT FK_attributename_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE, + CONSTRAINT UQ_attributename_company_name UNIQUE (company_id, name) + ); +END +`; + +export const INIT_ATTRIBUTEVALUE = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'attributevalue') +BEGIN + CREATE TABLE attributevalue ( + id INT IDENTITY(1,1) PRIMARY KEY, + attribute_id INT NOT NULL, + value NVARCHAR(255) NOT NULL, + sort INT NOT NULL CONSTRAINT DF_attributevalue_sort DEFAULT 0, + CONSTRAINT FK_attributevalue_attribute FOREIGN KEY (attribute_id) REFERENCES attributename(id) ON DELETE CASCADE, + CONSTRAINT UQ_attributevalue_attribute_value UNIQUE (attribute_id, value) + ); +END +`; + +export const INIT_ARTICLEATTRIBUTEVALUE = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articleattributevalue') +BEGIN + CREATE TABLE articleattributevalue ( + id INT IDENTITY(1,1) PRIMARY KEY, + article_id INT NOT NULL, + attributevalue_id INT NOT NULL, + sort INT NOT NULL CONSTRAINT DF_articleattributevalue_sort DEFAULT 0, + CONSTRAINT FK_articleattributevalue_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + CONSTRAINT FK_articleattributevalue_attributevalue FOREIGN KEY (attributevalue_id) REFERENCES attributevalue(id) ON DELETE CASCADE, + CONSTRAINT UQ_articleattributevalue_article_attributevalue UNIQUE (article_id, attributevalue_id) + ); +END +`; + /** DDL scripts in dependency order for schema init. */ export const SCHEMA_SCRIPTS = [ INIT_META, @@ -926,6 +1000,11 @@ export const SCHEMA_SCRIPTS = [ INIT_ARTICLENAME, INIT_PRICE, INIT_ARTICLE_SUPPLIER, + INIT_PROPERTYNAME, + INIT_ARTICLEPROPERTY, + INIT_ATTRIBUTENAME, + INIT_ATTRIBUTEVALUE, + INIT_ARTICLEATTRIBUTEVALUE, INIT_MANIFEST, INIT_MANIFESTARTICLE, INIT_WAREHOUSE, diff --git a/src/providers/sqlite3/queries/articleattributevalue.js b/src/providers/sqlite3/queries/articleattributevalue.js new file mode 100644 index 0000000..78d8af9 --- /dev/null +++ b/src/providers/sqlite3/queries/articleattributevalue.js @@ -0,0 +1,10 @@ +import * as source from '../../mssql/queries/articleattributevalue.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'articleattributevalue'); +export const { + LIST_BY_ARTICLE_ID, + DELETE_BY_ARTICLE_ID, + INSERT_ROW, + GET_VALUE_DETAIL, +} = queries; diff --git a/src/providers/sqlite3/queries/articleproperty.js b/src/providers/sqlite3/queries/articleproperty.js new file mode 100644 index 0000000..ad873f2 --- /dev/null +++ b/src/providers/sqlite3/queries/articleproperty.js @@ -0,0 +1,10 @@ +import * as source from '../../mssql/queries/articleproperty.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'articleproperty'); +export const { + LIST_BY_ARTICLE_ID, + DELETE_BY_ARTICLE_ID, + DELETE_BY_ARTICLE_AND_PROPERTY_ID, + INSERT_ROW, +} = queries; diff --git a/src/providers/sqlite3/queries/attributename.js b/src/providers/sqlite3/queries/attributename.js new file mode 100644 index 0000000..b077fcf --- /dev/null +++ b/src/providers/sqlite3/queries/attributename.js @@ -0,0 +1,61 @@ +import * as source from '../../mssql/queries/attributename.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'attributename'); +export const { + LIST_IDS_BY_COMPANY, + GET_BY_COMPANY_AND_NAME, + GET_NEXT_SORT, + CREATE, + UPDATE_BY_ID, + UPDATE_SORT, + DELETE_BY_COMPANY_AND_ID, +} = queries; + +export const LIST_BY_COMPANY = ` +SELECT + an.id, + an.name, + an.sort, + an.exclusive, + ( + SELECT COALESCE(json_group_array(json_object( + 'id', id, + 'value', value, + 'sort', sort + )), '[]') + FROM ( + SELECT av.id, av.value, av.sort + FROM attributevalue av + WHERE av.attribute_id = an.id + ORDER BY av.sort ASC, av.id ASC + ) + ) AS valuesJson +FROM attributename an +WHERE an.company_id = @companyId +ORDER BY an.sort ASC, an.id ASC +`; + +export const GET_BY_COMPANY_AND_ID = ` +SELECT + an.id, + an.company_id, + an.name, + an.sort, + an.exclusive, + ( + SELECT COALESCE(json_group_array(json_object( + 'id', id, + 'value', value, + 'sort', sort + )), '[]') + FROM ( + SELECT av.id, av.value, av.sort + FROM attributevalue av + WHERE av.attribute_id = an.id + ORDER BY av.sort ASC, av.id ASC + ) + ) AS valuesJson +FROM attributename an +WHERE an.id = @id AND an.company_id = @companyId +`; diff --git a/src/providers/sqlite3/queries/attributevalue.js b/src/providers/sqlite3/queries/attributevalue.js new file mode 100644 index 0000000..6651ddd --- /dev/null +++ b/src/providers/sqlite3/queries/attributevalue.js @@ -0,0 +1,15 @@ +import * as source from '../../mssql/queries/attributevalue.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'attributevalue'); +export const { + LIST_BY_ATTRIBUTE_ID, + LIST_IDS_BY_ATTRIBUTE_ID, + GET_BY_ID_AND_COMPANY, + GET_BY_ATTRIBUTE_AND_VALUE, + GET_NEXT_SORT, + CREATE, + UPDATE_BY_ID, + UPDATE_SORT, + DELETE_BY_ID, +} = queries; diff --git a/src/providers/sqlite3/queries/propertyname.js b/src/providers/sqlite3/queries/propertyname.js new file mode 100644 index 0000000..565b70f --- /dev/null +++ b/src/providers/sqlite3/queries/propertyname.js @@ -0,0 +1,15 @@ +import * as source from '../../mssql/queries/propertyname.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'propertyname'); +export const { + LIST_BY_COMPANY, + LIST_IDS_BY_COMPANY, + GET_BY_COMPANY_AND_ID, + GET_BY_COMPANY_AND_NAME, + GET_NEXT_SORT, + CREATE, + UPDATE_BY_ID, + UPDATE_SORT, + DELETE_BY_COMPANY_AND_ID, +} = queries; diff --git a/src/providers/sqlite3/schema.js b/src/providers/sqlite3/schema.js index 08807cf..ccf0079 100644 --- a/src/providers/sqlite3/schema.js +++ b/src/providers/sqlite3/schema.js @@ -621,6 +621,65 @@ BEGIN END; `; +export const INIT_PROPERTYNAME = ` +CREATE TABLE IF NOT EXISTS propertyname ( + id INTEGER PRIMARY KEY, + company_id INTEGER NOT NULL, + name TEXT NOT NULL, + sort INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE, + UNIQUE (company_id, name) +); +`; + +export const INIT_ARTICLEPROPERTY = ` +CREATE TABLE IF NOT EXISTS articleproperty ( + id INTEGER PRIMARY KEY, + article_id INTEGER NOT NULL, + property_id INTEGER NOT NULL, + value TEXT NOT NULL, + sort INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE CASCADE, + UNIQUE (article_id, property_id) +); +`; + +export const INIT_ATTRIBUTENAME = ` +CREATE TABLE IF NOT EXISTS attributename ( + id INTEGER PRIMARY KEY, + company_id INTEGER NOT NULL, + name TEXT NOT NULL, + sort INTEGER NOT NULL DEFAULT 0, + exclusive INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE, + UNIQUE (company_id, name) +); +`; + +export const INIT_ATTRIBUTEVALUE = ` +CREATE TABLE IF NOT EXISTS attributevalue ( + id INTEGER PRIMARY KEY, + attribute_id INTEGER NOT NULL, + value TEXT NOT NULL, + sort INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (attribute_id) REFERENCES attributename(id) ON DELETE CASCADE, + UNIQUE (attribute_id, value) +); +`; + +export const INIT_ARTICLEATTRIBUTEVALUE = ` +CREATE TABLE IF NOT EXISTS articleattributevalue ( + id INTEGER PRIMARY KEY, + article_id INTEGER NOT NULL, + attributevalue_id INTEGER NOT NULL, + sort INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, + FOREIGN KEY (attributevalue_id) REFERENCES attributevalue(id) ON DELETE CASCADE, + UNIQUE (article_id, attributevalue_id) +); +`; + /** DDL scripts in dependency order for SQLite schema initialization. */ export const SCHEMA_SCRIPTS = [ INIT_META, @@ -652,6 +711,11 @@ export const SCHEMA_SCRIPTS = [ INIT_ARTICLENAME, INIT_PRICE, INIT_ARTICLE_SUPPLIER, + INIT_PROPERTYNAME, + INIT_ARTICLEPROPERTY, + INIT_ATTRIBUTENAME, + INIT_ATTRIBUTEVALUE, + INIT_ARTICLEATTRIBUTEVALUE, INIT_MANIFEST, INIT_MANIFESTARTICLE, INIT_WAREHOUSE, diff --git a/src/store/articleattributevalue/listByArticleSku.js b/src/store/articleattributevalue/listByArticleSku.js new file mode 100644 index 0000000..c262e76 --- /dev/null +++ b/src/store/articleattributevalue/listByArticleSku.js @@ -0,0 +1,34 @@ +import { LIST_BY_ARTICLE_ID as MSSQL_QUERY } from '../../providers/mssql/queries/articleattributevalue.js'; +import { LIST_BY_ARTICLE_ID as SQLITE_QUERY } from '../../providers/sqlite3/queries/articleattributevalue.js'; +import resolveArticleBySku from '../article/resolveArticleBySku.js'; +import makeRunner from '../runner.js'; + +export default function listByArticleSku(provider) { + const resolve = resolveArticleBySku(provider); + const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY; + + /** + * Lists the attribute values assigned to an article by its SKU. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {object} [tx] - Optional transaction. + * @returns {Promise>} + */ + return async (googleAuthId, sku, tx) => { + const runner = makeRunner(provider, tx); + const { articleId } = await resolve(googleAuthId, sku, runner); + + const result = await runner.query(sql, { articleId }); + return result.rows.map((row) => ({ + id: row.id, + articleId: row.articleId, + attributeId: row.attributeId, + attributeName: row.attributeName, + exclusive: Boolean(row.exclusive), + attributeValueId: row.attributeValueId, + value: row.value, + sort: row.sort, + })); + }; +} diff --git a/src/store/articleattributevalue/setByArticleSku.js b/src/store/articleattributevalue/setByArticleSku.js new file mode 100644 index 0000000..db35987 --- /dev/null +++ b/src/store/articleattributevalue/setByArticleSku.js @@ -0,0 +1,115 @@ +import { + DELETE_BY_ARTICLE_ID as MSSQL_DELETE, + INSERT_ROW as MSSQL_INSERT, + GET_VALUE_DETAIL as MSSQL_GET_DETAIL, +} from '../../providers/mssql/queries/articleattributevalue.js'; +import { + DELETE_BY_ARTICLE_ID as SQLITE_DELETE, + INSERT_ROW as SQLITE_INSERT, + GET_VALUE_DETAIL as SQLITE_GET_DETAIL, +} from '../../providers/sqlite3/queries/articleattributevalue.js'; +import resolveArticleBySku from '../article/resolveArticleBySku.js'; +import makeRunner from '../runner.js'; + +export default function setByArticleSku(provider) { + const resolve = resolveArticleBySku(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE; + const insertSql = isSqlite ? SQLITE_INSERT : MSSQL_INSERT; + const getDetailSql = isSqlite ? SQLITE_GET_DETAIL : MSSQL_GET_DETAIL; + + /** + * Replaces all attribute values for an article by its SKU. + * Validates company ownership and enforces the `exclusive` flag on attributes. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {Array} values - Attribute value IDs or items. + * @param {object} [tx] - Optional transaction. + * @returns {Promise>} + */ + return async (googleAuthId, sku, values = [], tx) => { + if (!Array.isArray(values)) { + throw new Error('Invalid attribute values array'); + } + + const runSet = async (runner) => { + const { articleId, companyId } = await resolve(googleAuthId, sku, runner); + + const validated = []; + const seenValues = new Set(); + const exclusiveAttributesSeen = new Map(); // attributeId -> attributeName + + for (let i = 0; i < values.length; i++) { + const item = values[i]; + const valId = typeof item === 'object' && item !== null ? Number(item.attributeValueId) : Number(item); + if (!Number.isInteger(valId) || valId <= 0) { + throw new Error('Invalid attributeValueId'); + } + if (seenValues.has(valId)) { + throw new Error('Duplicate attributeValueId'); + } + seenValues.add(valId); + + const detailRes = await runner.query(getDetailSql, { attributeValueId: valId }); + const detail = detailRes.rows[0]; + if (!detail || detail.companyId !== companyId) { + throw new Error(`Attribute value not found: ${valId}`); + } + + const isExclusive = Boolean(detail.exclusive); + if (isExclusive) { + if (exclusiveAttributesSeen.has(detail.attributeId)) { + throw new Error(`Attribute "${detail.attributeName}" is exclusive and can only have one value`); + } + exclusiveAttributesSeen.set(detail.attributeId, detail.attributeName); + } + + const sort = typeof item === 'object' && item !== null && item.sort != null && Number.isInteger(item.sort) + ? item.sort + : i; + + validated.push({ + attributeId: detail.attributeId, + attributeName: detail.attributeName, + exclusive: isExclusive, + attributeValueId: valId, + value: detail.value, + sort, + }); + } + + await runner.query(deleteSql, { articleId }); + + const inserted = []; + for (const item of validated) { + const res = await runner.query(insertSql, { + articleId, + attributeValueId: item.attributeValueId, + sort: item.sort, + }); + const id = res.rows[0]?.id ?? res.insertId; + inserted.push({ + id, + articleId, + attributeId: item.attributeId, + attributeName: item.attributeName, + exclusive: item.exclusive, + attributeValueId: item.attributeValueId, + value: item.value, + sort: item.sort, + }); + } + + return inserted; + }; + + if (tx) { + return runSet(makeRunner(provider, tx)); + } + if (typeof provider?.transaction === 'function') { + return provider.transaction(runSet); + } + return runSet(makeRunner(provider)); + }; +} diff --git a/src/store/articleproperty/delete.js b/src/store/articleproperty/delete.js new file mode 100644 index 0000000..67385a7 --- /dev/null +++ b/src/store/articleproperty/delete.js @@ -0,0 +1,28 @@ +import { + DELETE_BY_ARTICLE_AND_PROPERTY_ID as MSSQL_DELETE, +} from '../../providers/mssql/queries/articleproperty.js'; +import { + DELETE_BY_ARTICLE_AND_PROPERTY_ID as SQLITE_DELETE, +} from '../../providers/sqlite3/queries/articleproperty.js'; +import resolveArticleBySku from '../article/resolveArticleBySku.js'; +import makeRunner from '../runner.js'; + +export default function deleteArticleProperty(provider) { + const resolve = resolveArticleBySku(provider); + const deleteSql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE; + + /** + * Deletes a property from an article by SKU and propertyId. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {number} propertyId - The property id. + * @param {object} [tx] - Optional transaction. + * @returns {Promise} + */ + return async (googleAuthId, sku, propertyId, tx) => { + const runner = makeRunner(provider, tx); + const { articleId } = await resolve(googleAuthId, sku, runner); + await runner.query(deleteSql, { articleId, propertyId }); + }; +} diff --git a/src/store/articleproperty/listByArticleSku.js b/src/store/articleproperty/listByArticleSku.js new file mode 100644 index 0000000..fbf91bf --- /dev/null +++ b/src/store/articleproperty/listByArticleSku.js @@ -0,0 +1,32 @@ +import { LIST_BY_ARTICLE_ID as MSSQL_QUERY } from '../../providers/mssql/queries/articleproperty.js'; +import { LIST_BY_ARTICLE_ID as SQLITE_QUERY } from '../../providers/sqlite3/queries/articleproperty.js'; +import resolveArticleBySku from '../article/resolveArticleBySku.js'; +import makeRunner from '../runner.js'; + +export default function listByArticleSku(provider) { + const resolve = resolveArticleBySku(provider); + const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY; + + /** + * Lists the properties of an article by its SKU. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {object} [tx] - Optional transaction. + * @returns {Promise>} + */ + return async (googleAuthId, sku, tx) => { + const runner = makeRunner(provider, tx); + const { articleId } = await resolve(googleAuthId, sku, runner); + + const result = await runner.query(sql, { articleId }); + return result.rows.map((row) => ({ + id: row.id, + articleId: row.articleId, + propertyId: row.propertyId, + name: row.name, + value: row.value, + sort: row.sort, + })); + }; +} diff --git a/src/store/articleproperty/setByArticleSku.js b/src/store/articleproperty/setByArticleSku.js new file mode 100644 index 0000000..cc18c41 --- /dev/null +++ b/src/store/articleproperty/setByArticleSku.js @@ -0,0 +1,103 @@ +import { + DELETE_BY_ARTICLE_ID as MSSQL_DELETE, + INSERT_ROW as MSSQL_INSERT, +} from '../../providers/mssql/queries/articleproperty.js'; +import { + DELETE_BY_ARTICLE_ID as SQLITE_DELETE, + INSERT_ROW as SQLITE_INSERT, +} from '../../providers/sqlite3/queries/articleproperty.js'; +import { + GET_BY_COMPANY_AND_ID as MSSQL_GET_PROP, +} from '../../providers/mssql/queries/propertyname.js'; +import { + GET_BY_COMPANY_AND_ID as SQLITE_GET_PROP, +} from '../../providers/sqlite3/queries/propertyname.js'; +import resolveArticleBySku from '../article/resolveArticleBySku.js'; +import makeRunner from '../runner.js'; + +export default function setByArticleSku(provider) { + const resolve = resolveArticleBySku(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE; + const insertSql = isSqlite ? SQLITE_INSERT : MSSQL_INSERT; + const getPropSql = isSqlite ? SQLITE_GET_PROP : MSSQL_GET_PROP; + + /** + * Replaces all properties for an article by its SKU. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {Array<{propertyId: number, value: string, sort?: number}>} properties - The properties to set. + * @param {object} [tx] - Optional transaction. + * @returns {Promise>} + */ + return async (googleAuthId, sku, properties = [], tx) => { + if (!Array.isArray(properties)) { + throw new Error('Invalid properties array'); + } + + const runSet = async (runner) => { + const { articleId, companyId } = await resolve(googleAuthId, sku, runner); + + const validated = []; + const seenProps = new Set(); + + for (let i = 0; i < properties.length; i++) { + const item = properties[i]; + const propertyId = Number(item.propertyId); + if (!Number.isInteger(propertyId) || propertyId <= 0) { + throw new Error('Invalid propertyId'); + } + if (seenProps.has(propertyId)) { + throw new Error('Duplicate propertyId'); + } + seenProps.add(propertyId); + + const propRes = await runner.query(getPropSql, { id: propertyId, companyId }); + if (!propRes.rows[0]) { + throw new Error(`Property not found: ${propertyId}`); + } + + const value = item.value != null ? String(item.value).trim() : ''; + const sort = item.sort != null && Number.isInteger(item.sort) ? item.sort : i; + validated.push({ + propertyId, + name: propRes.rows[0].name, + value, + sort, + }); + } + + await runner.query(deleteSql, { articleId }); + + const inserted = []; + for (const item of validated) { + const res = await runner.query(insertSql, { + articleId, + propertyId: item.propertyId, + value: item.value, + sort: item.sort, + }); + const id = res.rows[0]?.id ?? res.insertId; + inserted.push({ + id, + articleId, + propertyId: item.propertyId, + name: item.name, + value: item.value, + sort: item.sort, + }); + } + + return inserted; + }; + + if (tx) { + return runSet(makeRunner(provider, tx)); + } + if (typeof provider?.transaction === 'function') { + return provider.transaction(runSet); + } + return runSet(makeRunner(provider)); + }; +} diff --git a/src/store/attributename/create.js b/src/store/attributename/create.js new file mode 100644 index 0000000..859781c --- /dev/null +++ b/src/store/attributename/create.js @@ -0,0 +1,62 @@ +import { + CREATE as MSSQL_CREATE, + GET_NEXT_SORT as MSSQL_GET_NEXT_SORT, +} from '../../providers/mssql/queries/attributename.js'; +import { + CREATE as SQLITE_CREATE, + GET_NEXT_SORT as SQLITE_GET_NEXT_SORT, +} from '../../providers/sqlite3/queries/attributename.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function create(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT; + const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE; + + /** + * Creates an attribute name for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - The attribute name input. + * @param {string} input.name - The attribute name. + * @param {number} [input.sort] - Optional sort order. + * @param {boolean} [input.exclusive=false] - Whether values are mutually exclusive on an article. + * @param {object} [tx] - Optional transaction. + * @returns {Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>} + */ + return async (googleAuthId, input = {}, tx) => { + const name = input.name?.trim(); + if (!name) { + throw new Error('Name required'); + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, tx); + + let sort = input.sort; + if (sort == null || !Number.isInteger(sort)) { + const next = await runner.query(getSortSql, { companyId }); + sort = next.rows[0].sort; + } + + const exclusive = Boolean(input.exclusive) ? 1 : 0; + + const result = await runner.query(createSql, { + companyId, + name, + sort, + exclusive, + }); + + const id = result.rows[0]?.id ?? result.insertId; + return { + id, + name, + sort, + exclusive: Boolean(exclusive), + values: [], + }; + }; +} diff --git a/src/store/attributename/delete.js b/src/store/attributename/delete.js new file mode 100644 index 0000000..9278430 --- /dev/null +++ b/src/store/attributename/delete.js @@ -0,0 +1,37 @@ +import { + GET_BY_COMPANY_AND_ID as MSSQL_GET, + DELETE_BY_COMPANY_AND_ID as MSSQL_DELETE, +} from '../../providers/mssql/queries/attributename.js'; +import { + GET_BY_COMPANY_AND_ID as SQLITE_GET, + DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE, +} from '../../providers/sqlite3/queries/attributename.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function deleteAttributename(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const getSql = isSqlite ? SQLITE_GET : MSSQL_GET; + const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE; + + /** + * Deletes an attribute name for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The attribute name id. + * @param {object} [tx] - Optional transaction. + * @returns {Promise} + */ + return async (googleAuthId, id, tx) => { + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, tx); + + const existing = await runner.query(getSql, { id, companyId }); + if (!existing.rows[0]) { + throw new Error('Attribute name not found'); + } + + await runner.query(deleteSql, { id, companyId }); + }; +} diff --git a/src/store/attributename/list.js b/src/store/attributename/list.js new file mode 100644 index 0000000..32265f4 --- /dev/null +++ b/src/store/attributename/list.js @@ -0,0 +1,34 @@ +import { LIST_BY_COMPANY as MSSQL_LIST } from '../../providers/mssql/queries/attributename.js'; +import { LIST_BY_COMPANY as SQLITE_LIST } from '../../providers/sqlite3/queries/attributename.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +function parseValues(raw) { + if (!raw) return []; + const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw; + return Array.isArray(parsed) ? parsed : []; +} + +export default function list(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = provider?.dialect === 'sqlite3' ? SQLITE_LIST : MSSQL_LIST; + + /** + * Lists the attribute names for the user's selected company, including values. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} [tx] - Optional transaction. + * @returns {Promise}>>} + */ + return async (googleAuthId, tx) => { + const { companyId } = await access(googleAuthId, tx); + const result = await makeRunner(provider, tx).query(sql, { companyId }); + return result.rows.map((row) => ({ + id: row.id, + name: row.name, + sort: row.sort, + exclusive: Boolean(row.exclusive), + values: parseValues(row.valuesJson), + })); + }; +} diff --git a/src/store/attributename/order.js b/src/store/attributename/order.js new file mode 100644 index 0000000..276cd2b --- /dev/null +++ b/src/store/attributename/order.js @@ -0,0 +1,68 @@ +import { + LIST_IDS_BY_COMPANY as MSSQL_LIST_IDS, + UPDATE_SORT as MSSQL_UPDATE_SORT, +} from '../../providers/mssql/queries/attributename.js'; +import { + LIST_IDS_BY_COMPANY as SQLITE_LIST_IDS, + UPDATE_SORT as SQLITE_UPDATE_SORT, +} from '../../providers/sqlite3/queries/attributename.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function order(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const listIdsSql = isSqlite ? SQLITE_LIST_IDS : MSSQL_LIST_IDS; + const updateSortSql = isSqlite ? SQLITE_UPDATE_SORT : MSSQL_UPDATE_SORT; + + /** + * Reorders the attribute names for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - Reorder input. + * @param {Array} input.attributenameIds - IDs in their new order. + * @param {object} [tx] - Optional transaction. + * @returns {Promise<{attributenameIds: Array}>} + */ + return async (googleAuthId, { attributenameIds } = {}, tx) => { + if (!Array.isArray(attributenameIds)) { + throw new Error('Invalid attribute name order'); + } + + const runOrder = async (runner) => { + const { companyId } = await access(googleAuthId, runner); + const existingRows = await runner.query(listIdsSql, { companyId }); + const existing = new Set(existingRows.rows.map((r) => r.id)); + + if (attributenameIds.length !== existing.size) { + throw new Error('Invalid attribute name order'); + } + + const seen = new Set(); + for (const id of attributenameIds) { + if (!Number.isFinite(id) || !existing.has(id) || seen.has(id)) { + throw new Error('Invalid attribute name order'); + } + seen.add(id); + } + + for (let sort = 0; sort < attributenameIds.length; sort++) { + await runner.query(updateSortSql, { + id: attributenameIds[sort], + companyId, + sort, + }); + } + + return { attributenameIds }; + }; + + if (tx) { + return runOrder(makeRunner(provider, tx)); + } + if (typeof provider?.transaction === 'function') { + return provider.transaction(runOrder); + } + return runOrder(makeRunner(provider)); + }; +} diff --git a/src/store/attributename/update.js b/src/store/attributename/update.js new file mode 100644 index 0000000..aecb459 --- /dev/null +++ b/src/store/attributename/update.js @@ -0,0 +1,72 @@ +import { + GET_BY_COMPANY_AND_ID as MSSQL_GET, + UPDATE_BY_ID as MSSQL_UPDATE, +} from '../../providers/mssql/queries/attributename.js'; +import { + GET_BY_COMPANY_AND_ID as SQLITE_GET, + UPDATE_BY_ID as SQLITE_UPDATE, +} from '../../providers/sqlite3/queries/attributename.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +function parseValues(raw) { + if (!raw) return []; + const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw; + return Array.isArray(parsed) ? parsed : []; +} + +export default function update(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const getSql = isSqlite ? SQLITE_GET : MSSQL_GET; + const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE; + + /** + * Updates an attribute name for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The attribute name id. + * @param {object} input - Update patch. + * @param {string} [input.name] - New name. + * @param {number} [input.sort] - New sort. + * @param {boolean} [input.exclusive] - New exclusive flag. + * @param {object} [tx] - Optional transaction. + * @returns {Promise<{id: number, name: string, sort: number, exclusive: boolean, values: Array}>} + */ + return async (googleAuthId, id, input = {}, tx) => { + if (!id || !Number.isInteger(Number(id))) { + throw new Error('Invalid id'); + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, tx); + + const existing = await runner.query(getSql, { id, companyId }); + if (!existing.rows[0]) { + throw new Error('Attribute name not found'); + } + + const name = input.name !== undefined ? input.name?.trim() : existing.rows[0].name; + if (!name) { + throw new Error('Name required'); + } + const sort = input.sort !== undefined && Number.isInteger(input.sort) ? input.sort : existing.rows[0].sort; + const exclusive = input.exclusive !== undefined ? (Boolean(input.exclusive) ? 1 : 0) : existing.rows[0].exclusive; + + await runner.query(updateSql, { + id, + companyId, + name, + sort, + exclusive, + }); + + return { + id, + name, + sort, + exclusive: Boolean(exclusive), + values: parseValues(existing.rows[0].valuesJson), + }; + }; +} diff --git a/src/store/attributevalue/create.js b/src/store/attributevalue/create.js new file mode 100644 index 0000000..9a179b0 --- /dev/null +++ b/src/store/attributevalue/create.js @@ -0,0 +1,74 @@ +import { + CREATE as MSSQL_CREATE, + GET_NEXT_SORT as MSSQL_GET_NEXT_SORT, +} from '../../providers/mssql/queries/attributevalue.js'; +import { + CREATE as SQLITE_CREATE, + GET_NEXT_SORT as SQLITE_GET_NEXT_SORT, +} from '../../providers/sqlite3/queries/attributevalue.js'; +import { + GET_BY_COMPANY_AND_ID as MSSQL_GET_ATTR, +} from '../../providers/mssql/queries/attributename.js'; +import { + GET_BY_COMPANY_AND_ID as SQLITE_GET_ATTR, +} from '../../providers/sqlite3/queries/attributename.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function create(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const getAttrSql = isSqlite ? SQLITE_GET_ATTR : MSSQL_GET_ATTR; + const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT; + const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE; + + /** + * Creates an attribute value under a given attribute. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} attributeId - The attribute id. + * @param {object} input - Input value data. + * @param {string} input.value - The value text. + * @param {number} [input.sort] - Optional sort order. + * @param {object} [tx] - Optional transaction. + * @returns {Promise<{id: number, attributeId: number, value: string, sort: number}>} + */ + return async (googleAuthId, attributeId, input = {}, tx) => { + const attrId = Number(attributeId); + if (!Number.isInteger(attrId) || attrId <= 0) { + throw new Error('Invalid attributeId'); + } + const value = input.value?.trim(); + if (!value) { + throw new Error('Value required'); + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, tx); + + const attrRes = await runner.query(getAttrSql, { id: attrId, companyId }); + if (!attrRes.rows[0]) { + throw new Error('Attribute name not found'); + } + + let sort = input.sort; + if (sort == null || !Number.isInteger(sort)) { + const next = await runner.query(getSortSql, { attributeId: attrId }); + sort = next.rows[0].sort; + } + + const result = await runner.query(createSql, { + attributeId: attrId, + value, + sort, + }); + + const id = result.rows[0]?.id ?? result.insertId; + return { + id, + attributeId: attrId, + value, + sort, + }; + }; +} diff --git a/src/store/attributevalue/delete.js b/src/store/attributevalue/delete.js new file mode 100644 index 0000000..a3206e2 --- /dev/null +++ b/src/store/attributevalue/delete.js @@ -0,0 +1,42 @@ +import { + GET_BY_ID_AND_COMPANY as MSSQL_GET, + DELETE_BY_ID as MSSQL_DELETE, +} from '../../providers/mssql/queries/attributevalue.js'; +import { + GET_BY_ID_AND_COMPANY as SQLITE_GET, + DELETE_BY_ID as SQLITE_DELETE, +} from '../../providers/sqlite3/queries/attributevalue.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function deleteAttributeValue(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const getSql = isSqlite ? SQLITE_GET : MSSQL_GET; + const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE; + + /** + * Deletes an attribute value. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The attribute value id. + * @param {object} [tx] - Optional transaction. + * @returns {Promise} + */ + return async (googleAuthId, id, tx) => { + const valId = Number(id); + if (!Number.isInteger(valId) || valId <= 0) { + throw new Error('Invalid id'); + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, tx); + + const existing = await runner.query(getSql, { id: valId, companyId }); + if (!existing.rows[0]) { + throw new Error('Attribute value not found'); + } + + await runner.query(deleteSql, { id: valId }); + }; +} diff --git a/src/store/attributevalue/listByAttributeId.js b/src/store/attributevalue/listByAttributeId.js new file mode 100644 index 0000000..560c4d3 --- /dev/null +++ b/src/store/attributevalue/listByAttributeId.js @@ -0,0 +1,34 @@ +import { LIST_BY_ATTRIBUTE_ID as MSSQL_LIST } from '../../providers/mssql/queries/attributevalue.js'; +import { LIST_BY_ATTRIBUTE_ID as SQLITE_LIST } from '../../providers/sqlite3/queries/attributevalue.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function listByAttributeId(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = provider?.dialect === 'sqlite3' ? SQLITE_LIST : MSSQL_LIST; + + /** + * Lists the values for an attribute. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} attributeId - The attribute id. + * @param {object} [tx] - Optional transaction. + * @returns {Promise>} + */ + return async (googleAuthId, attributeId, tx) => { + if (!attributeId || !Number.isInteger(Number(attributeId))) { + throw new Error('Invalid attributeId'); + } + const { companyId } = await access(googleAuthId, tx); + const result = await makeRunner(provider, tx).query(sql, { + attributeId: Number(attributeId), + companyId, + }); + return result.rows.map((row) => ({ + id: row.id, + attributeId: row.attributeId, + value: row.value, + sort: row.sort, + })); + }; +} diff --git a/src/store/attributevalue/order.js b/src/store/attributevalue/order.js new file mode 100644 index 0000000..52bc7d1 --- /dev/null +++ b/src/store/attributevalue/order.js @@ -0,0 +1,72 @@ +import { + LIST_IDS_BY_ATTRIBUTE_ID as MSSQL_LIST_IDS, + UPDATE_SORT as MSSQL_UPDATE_SORT, +} from '../../providers/mssql/queries/attributevalue.js'; +import { + LIST_IDS_BY_ATTRIBUTE_ID as SQLITE_LIST_IDS, + UPDATE_SORT as SQLITE_UPDATE_SORT, +} from '../../providers/sqlite3/queries/attributevalue.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function order(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const listIdsSql = isSqlite ? SQLITE_LIST_IDS : MSSQL_LIST_IDS; + const updateSortSql = isSqlite ? SQLITE_UPDATE_SORT : MSSQL_UPDATE_SORT; + + /** + * Reorders attribute values under an attribute. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} attributeId - The attribute id. + * @param {object} input - Reorder input. + * @param {Array} input.attributevalueIds - Value IDs in their new order. + * @param {object} [tx] - Optional transaction. + * @returns {Promise<{attributevalueIds: Array}>} + */ + return async (googleAuthId, attributeId, { attributevalueIds } = {}, tx) => { + const attrId = Number(attributeId); + if (!Number.isInteger(attrId) || attrId <= 0) { + throw new Error('Invalid attributeId'); + } + if (!Array.isArray(attributevalueIds)) { + throw new Error('Invalid attribute value order'); + } + + const runOrder = async (runner) => { + const { companyId } = await access(googleAuthId, runner); + const existingRows = await runner.query(listIdsSql, { attributeId: attrId, companyId }); + const existing = new Set(existingRows.rows.map((r) => r.id)); + + if (attributevalueIds.length !== existing.size) { + throw new Error('Invalid attribute value order'); + } + + const seen = new Set(); + for (const id of attributevalueIds) { + if (!Number.isFinite(id) || !existing.has(id) || seen.has(id)) { + throw new Error('Invalid attribute value order'); + } + seen.add(id); + } + + for (let sort = 0; sort < attributevalueIds.length; sort++) { + await runner.query(updateSortSql, { + id: attributevalueIds[sort], + sort, + }); + } + + return { attributevalueIds }; + }; + + if (tx) { + return runOrder(makeRunner(provider, tx)); + } + if (typeof provider?.transaction === 'function') { + return provider.transaction(runOrder); + } + return runOrder(makeRunner(provider)); + }; +} diff --git a/src/store/attributevalue/update.js b/src/store/attributevalue/update.js new file mode 100644 index 0000000..34d93f2 --- /dev/null +++ b/src/store/attributevalue/update.js @@ -0,0 +1,62 @@ +import { + GET_BY_ID_AND_COMPANY as MSSQL_GET, + UPDATE_BY_ID as MSSQL_UPDATE, +} from '../../providers/mssql/queries/attributevalue.js'; +import { + GET_BY_ID_AND_COMPANY as SQLITE_GET, + UPDATE_BY_ID as SQLITE_UPDATE, +} from '../../providers/sqlite3/queries/attributevalue.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function update(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const getSql = isSqlite ? SQLITE_GET : MSSQL_GET; + const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE; + + /** + * Updates an attribute value. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The attribute value id. + * @param {object} input - Update patch. + * @param {string} [input.value] - New value text. + * @param {number} [input.sort] - New sort order. + * @param {object} [tx] - Optional transaction. + * @returns {Promise<{id: number, attributeId: number, value: string, sort: number}>} + */ + return async (googleAuthId, id, input = {}, tx) => { + const valId = Number(id); + if (!Number.isInteger(valId) || valId <= 0) { + throw new Error('Invalid id'); + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, tx); + + const existing = await runner.query(getSql, { id: valId, companyId }); + if (!existing.rows[0]) { + throw new Error('Attribute value not found'); + } + + const value = input.value !== undefined ? input.value?.trim() : existing.rows[0].value; + if (!value) { + throw new Error('Value required'); + } + const sort = input.sort !== undefined && Number.isInteger(input.sort) ? input.sort : existing.rows[0].sort; + + await runner.query(updateSql, { + id: valId, + value, + sort, + }); + + return { + id: valId, + attributeId: existing.rows[0].attributeId, + value, + sort, + }; + }; +} diff --git a/src/store/propertyname/create.js b/src/store/propertyname/create.js new file mode 100644 index 0000000..c33e551 --- /dev/null +++ b/src/store/propertyname/create.js @@ -0,0 +1,52 @@ +import { + CREATE as MSSQL_CREATE, + GET_NEXT_SORT as MSSQL_GET_NEXT_SORT, +} from '../../providers/mssql/queries/propertyname.js'; +import { + CREATE as SQLITE_CREATE, + GET_NEXT_SORT as SQLITE_GET_NEXT_SORT, +} from '../../providers/sqlite3/queries/propertyname.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function create(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT; + const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE; + + /** + * Creates a property name for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - The property name input. + * @param {string} input.name - The property name. + * @param {number} [input.sort] - Optional sort order. + * @param {object} [tx] - Optional transaction. + * @returns {Promise<{id: number, name: string, sort: number}>} + */ + return async (googleAuthId, input = {}, tx) => { + const name = input.name?.trim(); + if (!name) { + throw new Error('Name required'); + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, tx); + + let sort = input.sort; + if (sort == null || !Number.isInteger(sort)) { + const next = await runner.query(getSortSql, { companyId }); + sort = next.rows[0].sort; + } + + const result = await runner.query(createSql, { + companyId, + name, + sort, + }); + + const id = result.rows[0]?.id ?? result.insertId; + return { id, name, sort }; + }; +} diff --git a/src/store/propertyname/delete.js b/src/store/propertyname/delete.js new file mode 100644 index 0000000..f5417ba --- /dev/null +++ b/src/store/propertyname/delete.js @@ -0,0 +1,37 @@ +import { + GET_BY_COMPANY_AND_ID as MSSQL_GET, + DELETE_BY_COMPANY_AND_ID as MSSQL_DELETE, +} from '../../providers/mssql/queries/propertyname.js'; +import { + GET_BY_COMPANY_AND_ID as SQLITE_GET, + DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE, +} from '../../providers/sqlite3/queries/propertyname.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function deletePropertyname(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const getSql = isSqlite ? SQLITE_GET : MSSQL_GET; + const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE; + + /** + * Deletes a property name for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The property name id. + * @param {object} [tx] - Optional transaction. + * @returns {Promise} + */ + return async (googleAuthId, id, tx) => { + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, tx); + + const existing = await runner.query(getSql, { id, companyId }); + if (!existing.rows[0]) { + throw new Error('Property name not found'); + } + + await runner.query(deleteSql, { id, companyId }); + }; +} diff --git a/src/store/propertyname/list.js b/src/store/propertyname/list.js new file mode 100644 index 0000000..f09f95f --- /dev/null +++ b/src/store/propertyname/list.js @@ -0,0 +1,26 @@ +import { LIST_BY_COMPANY as MSSQL_LIST } from '../../providers/mssql/queries/propertyname.js'; +import { LIST_BY_COMPANY as SQLITE_LIST } from '../../providers/sqlite3/queries/propertyname.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function list(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = provider?.dialect === 'sqlite3' ? SQLITE_LIST : MSSQL_LIST; + + /** + * Lists the property names for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} [tx] - Optional transaction. + * @returns {Promise>} + */ + return async (googleAuthId, tx) => { + const { companyId } = await access(googleAuthId, tx); + const result = await makeRunner(provider, tx).query(sql, { companyId }); + return result.rows.map((row) => ({ + id: row.id, + name: row.name, + sort: row.sort, + })); + }; +} diff --git a/src/store/propertyname/order.js b/src/store/propertyname/order.js new file mode 100644 index 0000000..b244d40 --- /dev/null +++ b/src/store/propertyname/order.js @@ -0,0 +1,68 @@ +import { + LIST_IDS_BY_COMPANY as MSSQL_LIST_IDS, + UPDATE_SORT as MSSQL_UPDATE_SORT, +} from '../../providers/mssql/queries/propertyname.js'; +import { + LIST_IDS_BY_COMPANY as SQLITE_LIST_IDS, + UPDATE_SORT as SQLITE_UPDATE_SORT, +} from '../../providers/sqlite3/queries/propertyname.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function order(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const listIdsSql = isSqlite ? SQLITE_LIST_IDS : MSSQL_LIST_IDS; + const updateSortSql = isSqlite ? SQLITE_UPDATE_SORT : MSSQL_UPDATE_SORT; + + /** + * Reorders the property names for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - Reorder input. + * @param {Array} input.propertynameIds - IDs in their new order. + * @param {object} [tx] - Optional transaction. + * @returns {Promise<{propertynameIds: Array}>} + */ + return async (googleAuthId, { propertynameIds } = {}, tx) => { + if (!Array.isArray(propertynameIds)) { + throw new Error('Invalid property name order'); + } + + const runOrder = async (runner) => { + const { companyId } = await access(googleAuthId, runner); + const existingRows = await runner.query(listIdsSql, { companyId }); + const existing = new Set(existingRows.rows.map((r) => r.id)); + + if (propertynameIds.length !== existing.size) { + throw new Error('Invalid property name order'); + } + + const seen = new Set(); + for (const id of propertynameIds) { + if (!Number.isFinite(id) || !existing.has(id) || seen.has(id)) { + throw new Error('Invalid property name order'); + } + seen.add(id); + } + + for (let sort = 0; sort < propertynameIds.length; sort++) { + await runner.query(updateSortSql, { + id: propertynameIds[sort], + companyId, + sort, + }); + } + + return { propertynameIds }; + }; + + if (tx) { + return runOrder(makeRunner(provider, tx)); + } + if (typeof provider?.transaction === 'function') { + return provider.transaction(runOrder); + } + return runOrder(makeRunner(provider)); + }; +} diff --git a/src/store/propertyname/update.js b/src/store/propertyname/update.js new file mode 100644 index 0000000..5403c30 --- /dev/null +++ b/src/store/propertyname/update.js @@ -0,0 +1,57 @@ +import { + GET_BY_COMPANY_AND_ID as MSSQL_GET, + UPDATE_BY_ID as MSSQL_UPDATE, +} from '../../providers/mssql/queries/propertyname.js'; +import { + GET_BY_COMPANY_AND_ID as SQLITE_GET, + UPDATE_BY_ID as SQLITE_UPDATE, +} from '../../providers/sqlite3/queries/propertyname.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function update(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const getSql = isSqlite ? SQLITE_GET : MSSQL_GET; + const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE; + + /** + * Updates a property name for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The property name id. + * @param {object} input - Update patch. + * @param {string} [input.name] - New name. + * @param {number} [input.sort] - New sort. + * @param {object} [tx] - Optional transaction. + * @returns {Promise<{id: number, name: string, sort: number}>} + */ + return async (googleAuthId, id, input = {}, tx) => { + if (!id || !Number.isInteger(Number(id))) { + throw new Error('Invalid id'); + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, tx); + + const existing = await runner.query(getSql, { id, companyId }); + if (!existing.rows[0]) { + throw new Error('Property name not found'); + } + + const name = input.name !== undefined ? input.name?.trim() : existing.rows[0].name; + if (!name) { + throw new Error('Name required'); + } + const sort = input.sort !== undefined && Number.isInteger(input.sort) ? input.sort : existing.rows[0].sort; + + await runner.query(updateSql, { + id, + companyId, + name, + sort, + }); + + return { id, name, sort }; + }; +}