From 787e411547b6c1b04ae826ac9a2cff63c9ca2e72 Mon Sep 17 00:00:00 2001 From: seb Date: Sat, 1 Aug 2026 06:59:27 +0200 Subject: [PATCH] Add company-scoped brand and company settings API. brand now carries a NOT NULL company_id (unique per (company_id, name)) with brand.list/upsert/delete backed by resolveCompanyAccess. Adds companysetting key-value store (list/get/set/delete, frontend flag). --- README.md | 39 + generate-schema-graph.js | 1 + schema-graph.dot | 101 +- schema-graph.svg | 2338 +++++++++++----------- scripts/test-api.js | 178 ++ src/index.js | 20 + src/schema.js | 21 +- src/sql/brand.js | 36 + src/sql/companysetting.js | 47 + src/sql/purge.js | 1 + src/store/brand/delete.js | 31 + src/store/brand/list.js | 17 + src/store/brand/upsert.js | 51 + src/store/companysetting/delete.js | 24 + src/store/companysetting/get.js | 31 + src/store/companysetting/list.js | 21 + src/store/companysetting/set.js | 36 + src/store/companysetting/shapeSetting.js | 9 + 18 files changed, 1810 insertions(+), 1192 deletions(-) create mode 100644 src/sql/brand.js create mode 100644 src/sql/companysetting.js create mode 100644 src/store/brand/delete.js create mode 100644 src/store/brand/list.js create mode 100644 src/store/brand/upsert.js create mode 100644 src/store/companysetting/delete.js create mode 100644 src/store/companysetting/get.js create mode 100644 src/store/companysetting/list.js create mode 100644 src/store/companysetting/set.js create mode 100644 src/store/companysetting/shapeSetting.js diff --git a/README.md b/README.md index 0a44535..9f64d07 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,39 @@ Deletes the unit for `(companyId, si)`. Blocked by FK if any article still refer --- +## `companysetting` + +Company-scoped key/value settings. Auth via `resolveCompanyAccess`. + +| Method | Signature | Returns | +| --- | --- | --- | +| `list` | `(googleAuthId, companyId, { frontendOnly? })` | `{ key, value, frontend }[]` | +| `get` | `(googleAuthId, companyId, key, { frontendOnly? })` | `{ key, value, frontend } \| undefined` | +| `set` | `(googleAuthId, companyId, { key, value, frontend? })` | `{ key, value, frontend }` | +| `delete` | `(googleAuthId, companyId, key)` | `void` | + +- `value` is always a string (`NVARCHAR(MAX)`). +- `frontend: true` → readable by frontend routes (`frontendOnly: true`). Default on `set` is `false` (backend-only). +- With `frontendOnly: true`, backend-only keys are omitted / `get` returns `undefined` (no leak via error type beyond missing). + +Errors: `Company not found`, `Not authorized`, `Invalid setting`, `Setting not found`. + +--- + +## `brand` + +Company-scoped brands. Auth via `resolveCompanyAccess`. + +| Method | Signature | Returns | +| --- | --- | --- | +| `list` | `(googleAuthId, companyId)` | `{ id, name, gsprpinfo }[]` | +| `upsert` | `(googleAuthId, companyId, { name, gsprpinfo? })` | `{ id, name, gsprpinfo }` | +| `delete` | `(googleAuthId, companyId, id)` | `void` | + +`brand.company_id` **NOT NULL**, unique `(company_id, name)`. `upsert` matches by `(companyId, name)`. `delete` blocked by FK if any article references it (`Brand in use`). Errors: `Company not found`, `Not authorized`, `Invalid brand`, `Brand not found`, `Brand in use`. + +--- + ## `category` | Method | Signature | Returns | @@ -272,6 +305,12 @@ res.set('Content-Type', thumb.contentType).send(thumb.buffer); | `GET /api/unit?companyId=` | `unit.list(googleAuthId, companyId)` | | `PUT /api/unit` | `unit.upsert(googleAuthId, companyId, { si, basesi, basesifactor, symbol, name })` | | `DELETE /api/unit` | `unit.delete(googleAuthId, companyId, si)` | +| `GET /api/company/:id/settings` | `companysetting.list(googleAuthId, companyId, { frontendOnly: true })` | +| `PUT /api/company/:id/settings` | `companysetting.set(googleAuthId, companyId, { key, value, frontend? })` | +| `DELETE /api/company/:id/settings/:key` | `companysetting.delete(googleAuthId, companyId, key)` | +| `GET /api/brand?companyId=` | `brand.list(googleAuthId, companyId)` | +| `PUT /api/brand` | `brand.upsert(googleAuthId, companyId, { name, gsprpinfo? })` | +| `DELETE /api/brand/:id` | `brand.delete(googleAuthId, companyId, id)` | | `POST /api/article` | `article.createUnderParent({ googleAuthId, parentId, name })` | | `GET /api/article/:sku` | `article.getBySku(googleAuthId, sku)` | | `PATCH /api/article/:sku` | `article.updateBySku(googleAuthId, sku, { name?, unitamount?, unitSi? })` | diff --git a/generate-schema-graph.js b/generate-schema-graph.js index 7cd76be..d9edbbc 100644 --- a/generate-schema-graph.js +++ b/generate-schema-graph.js @@ -271,6 +271,7 @@ const CLUSTERS = [ 'companyuser', 'syncpos', 'company', + 'companysetting', 'customergroup', 'entity', 'numbers', diff --git a/schema-graph.dot b/schema-graph.dot index 28f3a7b..f341dee 100644 --- a/schema-graph.dot +++ b/schema-graph.dot @@ -12,44 +12,45 @@ digraph schema { numbers [label=<
numbers
idINT PK
company_idINT FK
typeNVARCHAR(100)
prefixNVARCHAR(50)
paddingINT
nextINT
>]; entity [label=<
entity
idINT PK
customergroup_idINT FK
firstnameNVARCHAR(255)
lastnameNVARCHAR(255)
>]; syncpos [label=<
syncpos
idINT PK
company_idINT FK
paircodeNVARCHAR(50)
token?NVARCHAR(255)
>]; + company [label=<
company
idINT PK
default_language_id?INT FK
root_category_id?INT FK
poscustomergroup_idINT FK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
nameNVARCHAR(255)
>]; meta [label=<
meta
keyNVARCHAR(255)
valueNVARCHAR(MAX)
>]; - company [label=<
company
idINT PK
default_language_id?INT FK
root_category_id?INT FK
poscustomergroup_idINT FK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
nameNVARCHAR(255)
>]; - companyuser [label=<
companyuser
idINT PK
company_idINT FK
google_auth_idINT FK
>]; customergroup [label=<
customergroup
idINT PK
company_id?INT FK
nameNVARCHAR(255)
>]; + companysetting [label=<
companysetting
idINT PK
company_idINT FK
keyNVARCHAR(255)
valueNVARCHAR(MAX)
frontendBIT
>]; + companyuser [label=<
companyuser
idINT PK
company_idINT FK
google_auth_idINT FK
>]; google_auth [label=<
google_auth
idINT PK
selected_company?INT FK
emailNVARCHAR(320)
created_atDATETIME2
>]; } subgraph cluster_i18n { label="Localization"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; - categorytext [label=<
categorytext
idINT PK
language_idINT FK
category_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; + language [label=<
language
idINT PK
iso639NVARCHAR(10)
>]; articletext [label=<
articletext
idINT PK
language_idINT FK
article_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; unittext [label=<
unittext
idINT PK
language_idINT FK
unit_idINT FK
symbolNVARCHAR(50)
nameNVARCHAR(255)
>]; - language [label=<
language
idINT PK
iso639NVARCHAR(10)
>]; + categorytext [label=<
categorytext
idINT PK
language_idINT FK
category_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; } subgraph cluster_catalog { label="Catalog"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; articlehistory [label=<
articlehistory
idINT PK
article_idINT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; - articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; - categoryarticle [label=<
categoryarticle
idINT PK
article_idINT FK
category_idINT FK
>]; - articlelist [label=<
articlelist
idINT PK
>]; - article [label=<
article
idINT PK
unit_idINT FK
articlelist_id?INT FK
fatherarticle_id?INT FK
optionslist_id?INT FK
brand_id?INT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; category [label=<
category
idINT PK
parent_id?INT FK
sortINT
>]; - articlelistarticle [label=<
articlelistarticle
idINT PK
articlelist_idINT FK
article_idINT FK
countINT
sortINT
>]; + articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; + article [label=<
article
idINT PK
unit_idINT FK
fatherarticle_id?INT FK
brand_id?INT FK
articlelist_id?INT FK
optionslist_id?INT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; + brand [label=<
brand
idINT PK
company_idINT FK
nameNVARCHAR(255)
gsprpinfo?NVARCHAR(MAX)
>]; + categoryarticle [label=<
categoryarticle
idINT PK
category_idINT FK
article_idINT FK
>]; + articlelist [label=<
articlelist
idINT PK
>]; + articlelistarticle [label=<
articlelistarticle
idINT PK
article_idINT FK
articlelist_idINT FK
countINT
sortINT
>]; optionslist [label=<
optionslist
idINT PK
>]; - brand [label=<
brand
idINT PK
nameNVARCHAR(255)
gsprpinfo?NVARCHAR(MAX)
>]; optionslistarticle [label=<
optionslistarticle
idINT PK
article_idINT FK
optionslist_idINT FK
countINT
sortINT
mandatedBIT
>]; } subgraph cluster_media { label="Media"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; - brandpic [label=<
brandpic
idINT PK
brand_idINT FK
pic_idINT FK
sortINT
>]; - articlepic [label=<
articlepic
idINT PK
article_idINT FK
pic_idINT FK
sortINT
>]; + categorypic [label=<
categorypic
idINT PK
pic_idINT FK
category_idINT FK
sortINT
>]; pic [label=<
pic
idINT PK
wINT
hINT
sizeBIGINT
hashNVARCHAR(64)
thumbwINT
thumbhINT
thumbhashNVARCHAR(64)
sourceNVARCHAR(MAX)
fullbinaryVARBINARY(MAX)
thumbbinaryVARBINARY(MAX)
>]; - categorypic [label=<
categorypic
idINT PK
category_idINT FK
pic_idINT FK
sortINT
>]; + articlepic [label=<
articlepic
idINT PK
pic_idINT FK
article_idINT FK
sortINT
>]; + brandpic [label=<
brandpic
idINT PK
pic_idINT FK
brand_idINT FK
sortINT
>]; } subgraph cluster_commerce { label="Pricing & Documents"; labeljust="l"; fontsize=15; @@ -69,7 +70,7 @@ digraph schema { __legend [label=<
Legend
PKprimary key
FKforeign key
?nullable column
—FOREIGN KEY constraint
- -naming convention only, no constraint
>]; - google_auth:selected_company:sw -> company:in5:n [color="#c53030", fontcolor="#c53030", tooltip="FK_google_auth_selected_company"]; + google_auth:selected_company:sw -> company:in7:n [color="#c53030", fontcolor="#c53030", tooltip="FK_google_auth_selected_company"]; category:parent_id:e -> category:id:e [color="#2f855a", fontcolor="#2f855a", tooltip="FK_category_parent"]; company:root_category_id:w -> category:in4:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_company_root_category"]; company:default_language_id:w -> language:in3:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_company_default_language"]; @@ -80,39 +81,41 @@ digraph schema { company:poscustomergroup_id:se -> customergroup:in1:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_company_poscustomergroup"]; entity:customergroup_id:e -> customergroup:in0:n [color="#276749", fontcolor="#276749", tooltip="FK_entity_customergroup"]; unit:company_id:e -> company:in0:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_unit_company"]; - unittext:unit_id:se -> unit:in0:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_unittext_unit"]; - unittext:language_id:se -> language:in2:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_unittext_language"]; - article:unit_id:w -> unit:in1:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_article_unit"]; - article:brand_id:se -> brand:in0:n [color="#22543d", fontcolor="#22543d", tooltip="FK_article_brand"]; - article:articlelist_id:sw -> articlelist:in0:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_article_articlelist"]; - article:optionslist_id:se -> optionslist:in0:n [color="#702459", fontcolor="#702459", tooltip="FK_article_optionslist"]; - article:fatherarticle_id:e -> article:id:e [color="#744210", fontcolor="#744210", tooltip="FK_article_fatherarticle"]; - articlehistory:article_id:e -> article:in1:n [color="#285e61", fontcolor="#285e61", tooltip="FK_articlehistory_article"]; - articleean:article_id:se -> article:in2:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_articleean_article"]; - articlepic:article_id:w -> article:in7:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_articlepic_article"]; - articlepic:pic_id:se -> pic:in1:n [color="#38a169", fontcolor="#38a169", tooltip="FK_articlepic_pic"]; - brandpic:brand_id:sw -> brand:in1:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_brandpic_brand"]; - brandpic:pic_id:se -> pic:in0:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_brandpic_pic"]; - articlelistarticle:articlelist_id:sw -> articlelist:in1:n [color="#c53030", fontcolor="#c53030", tooltip="FK_articlelistarticle_articlelist"]; - articlelistarticle:article_id:sw -> article:in5:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_articlelistarticle_article"]; - optionslistarticle:optionslist_id:sw -> optionslist:in1:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_optionslistarticle_optionslist"]; - optionslistarticle:article_id:sw -> article:in6:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_optionslistarticle_article"]; - categoryarticle:category_id:se -> category:in1:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_categoryarticle_category"]; - categoryarticle:article_id:se -> article:in3:n [color="#c05621", fontcolor="#c05621", tooltip="FK_categoryarticle_article"]; - categorypic:category_id:sw -> category:in3:n [color="#319795", fontcolor="#319795", tooltip="FK_categorypic_category"]; - categorypic:pic_id:sw -> pic:in2:n [color="#97266d", fontcolor="#97266d", tooltip="FK_categorypic_pic"]; - categorytext:category_id:e -> category:in0:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_categorytext_category"]; - categorytext:language_id:se -> language:in0:n [color="#276749", fontcolor="#276749", tooltip="FK_categorytext_language"]; - articletext:article_id:e -> article:in0:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_articletext_article"]; - articletext:language_id:se -> language:in1:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_articletext_language"]; - price:tax_id:sw -> tax:in3:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_price_tax"]; - price:article_id:w -> article:in8:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_price_article"]; - price:customergroup_id:sw -> customergroup:in2:n [color="#22543d", fontcolor="#22543d", tooltip="FK_price_customergroup"]; - manifestarticle:manifest_id:sw -> manifest:in0:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_manifestarticle_manifest"]; - manifestarticle:article_id:w -> article:in9:n [color="#702459", fontcolor="#702459", tooltip="FK_manifestarticle_article"]; - manifestarticle:tax_id:sw -> tax:in4:n [color="#744210", fontcolor="#744210", tooltip="FK_manifestarticle_tax"]; - numbers:company_id:se -> company:in1:n [color="#285e61", fontcolor="#285e61", tooltip="FK_numbers_company"]; - companyuser:google_auth_id:se -> google_auth:in0:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_companyuser_google_auth"]; - companyuser:company_id:sw -> company:in3:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_companyuser_company"]; - syncpos:company_id:se -> company:in2:n [color="#38a169", fontcolor="#38a169", tooltip="FK_syncpos_company"]; + unittext:unit_id:e -> unit:in0:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_unittext_unit"]; + unittext:language_id:sw -> language:in1:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_unittext_language"]; + brand:company_id:e -> company:in1:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_brand_company"]; + article:unit_id:w -> unit:in1:n [color="#22543d", fontcolor="#22543d", tooltip="FK_article_unit"]; + article:brand_id:se -> brand:in1:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_article_brand"]; + article:articlelist_id:se -> articlelist:in0:n [color="#702459", fontcolor="#702459", tooltip="FK_article_articlelist"]; + article:optionslist_id:se -> optionslist:in0:n [color="#744210", fontcolor="#744210", tooltip="FK_article_optionslist"]; + article:fatherarticle_id:e -> article:id:e [color="#285e61", fontcolor="#285e61", tooltip="FK_article_fatherarticle"]; + articlehistory:article_id:se -> article:in2:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_articlehistory_article"]; + articleean:article_id:se -> article:in3:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_articleean_article"]; + articlepic:article_id:e -> article:in1:n [color="#38a169", fontcolor="#38a169", tooltip="FK_articlepic_article"]; + articlepic:pic_id:sw -> pic:in1:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_articlepic_pic"]; + brandpic:brand_id:e -> brand:in0:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_brandpic_brand"]; + brandpic:pic_id:sw -> pic:in2:n [color="#c53030", fontcolor="#c53030", tooltip="FK_brandpic_pic"]; + articlelistarticle:articlelist_id:sw -> articlelist:in1:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_articlelistarticle_articlelist"]; + articlelistarticle:article_id:sw -> article:in6:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_articlelistarticle_article"]; + optionslistarticle:optionslist_id:sw -> optionslist:in1:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_optionslistarticle_optionslist"]; + optionslistarticle:article_id:w -> article:in7:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_optionslistarticle_article"]; + categoryarticle:category_id:sw -> category:in3:n [color="#c05621", fontcolor="#c05621", tooltip="FK_categoryarticle_category"]; + categoryarticle:article_id:sw -> article:in5:n [color="#319795", fontcolor="#319795", tooltip="FK_categoryarticle_article"]; + categorypic:category_id:se -> category:in1:n [color="#97266d", fontcolor="#97266d", tooltip="FK_categorypic_category"]; + categorypic:pic_id:se -> pic:in0:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_categorypic_pic"]; + categorytext:category_id:e -> category:in0:n [color="#276749", fontcolor="#276749", tooltip="FK_categorytext_category"]; + categorytext:language_id:sw -> language:in2:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_categorytext_language"]; + articletext:article_id:e -> article:in0:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_articletext_article"]; + articletext:language_id:se -> language:in0:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_articletext_language"]; + price:tax_id:sw -> tax:in3:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_price_tax"]; + price:article_id:w -> article:in8:n [color="#22543d", fontcolor="#22543d", tooltip="FK_price_article"]; + price:customergroup_id:sw -> customergroup:in2:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_price_customergroup"]; + manifestarticle:manifest_id:sw -> manifest:in0:n [color="#702459", fontcolor="#702459", tooltip="FK_manifestarticle_manifest"]; + manifestarticle:article_id:w -> article:in9:n [color="#744210", fontcolor="#744210", tooltip="FK_manifestarticle_article"]; + manifestarticle:tax_id:sw -> tax:in4:n [color="#285e61", fontcolor="#285e61", tooltip="FK_manifestarticle_tax"]; + numbers:company_id:se -> company:in2:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_numbers_company"]; + companyuser:google_auth_id:se -> google_auth:in0:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_companyuser_google_auth"]; + companyuser:company_id:sw -> company:in6:n [color="#38a169", fontcolor="#38a169", tooltip="FK_companyuser_company"]; + syncpos:company_id:se -> company:in3:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_syncpos_company"]; + companysetting:company_id:sw -> company:in5:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_companysetting_company"]; } \ No newline at end of file diff --git a/schema-graph.svg b/schema-graph.svg index 42e7d88..d7e6295 100644 --- a/schema-graph.svg +++ b/schema-graph.svg @@ -4,349 +4,336 @@ - + schema - + cluster_identity - -Identity & Company + +Identity & Company cluster_i18n - -Localization + +Localization cluster_catalog - -Catalog + +Catalog cluster_media - -Media + +Media cluster_commerce - -Pricing & Documents + +Pricing & Documents cluster_units - -Units + +Units numbers - - -numbers - - -id -INT -PK - - -company_id -INT -FK - - -type -NVARCHAR(100) - - -prefix -NVARCHAR(50) - - -padding -INT - - -next -INT + + +numbers + + +id +INT +PK + + +company_id +INT +FK + + +type +NVARCHAR(100) + + +prefix +NVARCHAR(50) + + +padding +INT + + +next +INT - + company - - - - - - - - -company - - -id -INT -PK - - -default_language_id -? -INT -FK - - -root_category_id -? -INT -FK - - -poscustomergroup_id -INT -FK - - -taxrate -? -INT -FK - - -reducedtaxrate -? -INT -FK - - -taxfreerate -? -INT -FK - - -name -NVARCHAR(255) + + + + + + + + + + +company + + +id +INT +PK + + +default_language_id +? +INT +FK + + +root_category_id +? +INT +FK + + +poscustomergroup_id +INT +FK + + +taxrate +? +INT +FK + + +reducedtaxrate +? +INT +FK + + +taxfreerate +? +INT +FK + + +name +NVARCHAR(255) - + numbers:se->company:n - - - + + + entity - - -entity - - -id -INT -PK - - -customergroup_id -INT -FK - - -firstname -NVARCHAR(255) - - -lastname -NVARCHAR(255) + + +entity + + +id +INT +PK + + +customergroup_id +INT +FK + + +firstname +NVARCHAR(255) + + +lastname +NVARCHAR(255) - + customergroup - - - - - -customergroup - - -id -INT -PK - - -company_id -? -INT -FK - - -name -NVARCHAR(255) + + + + + +customergroup + + +id +INT +PK + + +company_id +? +INT +FK + + +name +NVARCHAR(255) entity:e->customergroup:n - - + + syncpos - - -syncpos - - -id -INT -PK - - -company_id -INT -FK - - -paircode -NVARCHAR(50) - - -token? -NVARCHAR(255) + + +syncpos + + +id +INT +PK + + +company_id +INT +FK + + +paircode +NVARCHAR(50) + + +token? +NVARCHAR(255) - + syncpos:se->company:n - - - + + + - - -meta - - -meta - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) - company:se->customergroup:n - - + + - + language - - - - - - -language - - -id -INT -PK - - -iso639 -NVARCHAR(10) + + + + + + +language + + +id +INT +PK + + +iso639 +NVARCHAR(10) company:w->language:n - - + + - + category - - - - - - - -category - - -id -INT -PK - - -parent_id -? -INT -FK - - -sort -INT + + + + + + + +category + + +id +INT +PK + + +parent_id +? +INT +FK + + +sort +INT company:w->category:n - - + + - + tax - - - - - - - -tax - - -id -INT -PK - - -rate -DECIMAL(18,4) + + + + + + + +tax + + +id +INT +PK + + +rate +DECIMAL(18,4) company:e->tax:n - - + + @@ -354,8 +341,8 @@ company:e->tax:n - - + + @@ -363,84 +350,137 @@ company:e->tax:n - - + + - - -companyuser - - -companyuser - - -id -INT -PK - - -company_id -INT -FK - - -google_auth_id -INT -FK - - - -companyuser:sw->company:n - - - - - - - - -google_auth - - - -google_auth - - -id -INT -PK - - -selected_company -? -INT -FK - - -email -NVARCHAR(320) - - -created_at -DATETIME2 - - - -companyuser:se->google_auth:n - - - - - + + +meta + + +meta + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) customergroup:nw->company:n - - + + + + + + + +companysetting + + +companysetting + + +id +INT +PK + + +company_id +INT +FK + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) + + +frontend +BIT + + + +companysetting:sw->company:n + + + + + + + + +companyuser + + +companyuser + + +id +INT +PK + + +company_id +INT +FK + + +google_auth_id +INT +FK + + + +companyuser:sw->company:n + + + + + + + + +google_auth + + + +google_auth + + +id +INT +PK + + +selected_company +? +INT +FK + + +email +NVARCHAR(320) + + +created_at +DATETIME2 + + + +companyuser:se->google_auth:n + + + @@ -448,477 +488,312 @@ google_auth:sw->company:n - - - - - - - -categorytext - - -categorytext - - -id -INT -PK - - -language_id -INT -FK - - -category_id -INT -FK - - -name -NVARCHAR(255) - - -slug -NVARCHAR(255) - - -seo? -NVARCHAR(MAX) - - -desc? -NVARCHAR(MAX) - - - -categorytext:se->language:n - - - - - - - - -categorytext:e->category:n - - - + + - + articletext - - -articletext - - -id -INT -PK - - -language_id -INT -FK - - -article_id -INT -FK - - -name -NVARCHAR(255) - - -slug -NVARCHAR(255) - - -seo? -NVARCHAR(MAX) - - -desc? -NVARCHAR(MAX) + + +articletext + + +id +INT +PK + + +language_id +INT +FK + + +article_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) - + articletext:se->language:n - - - + + + article - - - - - - - - - - - - -article - - -id -INT -PK - - -unit_id -INT -FK - - -articlelist_id -? -INT -FK - - -fatherarticle_id -? -INT -FK - - -optionslist_id -? -INT -FK - - -brand_id -? -INT -FK - - -sku? -NVARCHAR(100) - - -unitamount -FLOAT - - -weight -FLOAT - - -is_father -BIT - - -fathersort -INT - - -created_at -DATETIME2 - - -updated_at -DATETIME2 + + + + + + + + + + + + +article + + +id +INT +PK + + +unit_id +INT +FK + + +fatherarticle_id +? +INT +FK + + +brand_id +? +INT +FK + + +articlelist_id +? +INT +FK + + +optionslist_id +? +INT +FK + + +sku? +NVARCHAR(100) + + +unitamount +FLOAT + + +weight +FLOAT + + +is_father +BIT + + +fathersort +INT + + +created_at +DATETIME2 + + +updated_at +DATETIME2 - + articletext:e->article:n - - - + + + - + unittext - - -unittext - - -id -INT -PK - - -language_id -INT -FK - - -unit_id -INT -FK - - -symbol -NVARCHAR(50) - - -name -NVARCHAR(255) + + +unittext + + +id +INT +PK + + +language_id +INT +FK + + +unit_id +INT +FK + + +symbol +NVARCHAR(50) + + +name +NVARCHAR(255) -unittext:se->language:n +unittext:sw->language:n - - + + - + unit - - - - -unit - - -id -INT -PK - - -company_id -INT -FK - - -si -NVARCHAR(50) - - -basesi -NVARCHAR(50) - - -basesifactor -FLOAT + + + + +unit + + +id +INT +PK + + +company_id +INT +FK + + +si +NVARCHAR(50) + + +basesi +NVARCHAR(50) + + +basesifactor +FLOAT -unittext:se->unit:n +unittext:e->unit:n - - + + + + + + + +categorytext + + +categorytext + + +id +INT +PK + + +language_id +INT +FK + + +category_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) + + + +categorytext:sw->language:n + + + + + + + + +categorytext:e->category:n + + + - + articlehistory - - -articlehistory - - -id -INT -PK - - -article_id -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) + + +articlehistory + + +id +INT +PK + + +article_id +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) - -articlehistory:e->article:n - - - - - - - - -articleean - - -articleean - - -id -INT -PK - - -article_id -INT -FK - - -ean -NVARCHAR(50) - - -articleean:se->article:n - - - - - - - - -categoryarticle - - -categoryarticle - - -id -INT -PK - - -article_id -INT -FK - - -category_id -INT -FK - - - -categoryarticle:se->article:n - - - - - - - - -categoryarticle:se->category:n - - - - - - - - -articlelist - - - - -articlelist - - -id -INT -PK - - - -article:sw->articlelist:n - - - - - - - - -article:e->article:e - - - - - - - - -optionslist - - - - -optionslist - - -id -INT -PK - - - -article:se->optionslist:n - - - - - - - - -brand - - - - -brand - - -id -INT -PK - - -name -NVARCHAR(255) - - -gsprpinfo? -NVARCHAR(MAX) - - - -article:se->brand:n - - - - - - - - -article:w->unit:n - - - +articlehistory:se->article:n + + + @@ -926,457 +801,636 @@ category:e->category:e - - + + + + + + + +articleean + + +articleean + + +id +INT +PK + + +article_id +INT +FK + + +ean +NVARCHAR(50) + + + +articleean:se->article:n + + + + + + + + +article:e->article:e + + + + + + + + +brand + + + + +brand + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +gsprpinfo? +NVARCHAR(MAX) + + + +article:se->brand:n + + + + + + + + +articlelist + + + + +articlelist + + +id +INT +PK + + + +article:se->articlelist:n + + + + + + + + +optionslist + + + + +optionslist + + +id +INT +PK + + + +article:se->optionslist:n + + + + + + + + +article:w->unit:n + + + + + + + + +brand:e->company:n + + + + + + + + +categoryarticle + + +categoryarticle + + +id +INT +PK + + +category_id +INT +FK + + +article_id +INT +FK + + + +categoryarticle:sw->category:n + + + + + + + + +categoryarticle:sw->article:n + + + - + articlelistarticle - - -articlelistarticle - - -id -INT -PK - - -articlelist_id -INT -FK - - -article_id -INT -FK - - -count -INT - - -sort -INT + + +articlelistarticle + + +id +INT +PK + + +article_id +INT +FK + + +articlelist_id +INT +FK + + +count +INT + + +sort +INT - - -articlelistarticle:sw->articlelist:n - - - + + +articlelistarticle:sw->article:n + + + - + -articlelistarticle:sw->article:n - - - +articlelistarticle:sw->articlelist:n + + + - + optionslistarticle - - -optionslistarticle - - -id -INT -PK - - -article_id -INT -FK - - -optionslist_id -INT -FK - - -count -INT - - -sort -INT - - -mandated -BIT + + +optionslistarticle + + +id +INT +PK + + +article_id +INT +FK + + +optionslist_id +INT +FK + + +count +INT + + +sort +INT + + +mandated +BIT - -optionslistarticle:sw->article:n - - - + +optionslistarticle:w->article:n + + + - + optionslistarticle:sw->optionslist:n - - - + + + - - -brandpic - - -brandpic - - -id -INT -PK - - -brand_id -INT -FK - - -pic_id -INT -FK - - -sort -INT + + +categorypic + + +categorypic + + +id +INT +PK + + +pic_id +INT +FK + + +category_id +INT +FK + + +sort +INT - - -brandpic:sw->brand:n - - - + + +categorypic:se->category:n + + + pic - - - - - -pic - - -id -INT -PK - - -w -INT - - -h -INT - - -size -BIGINT - - -hash -NVARCHAR(64) - - -thumbw -INT - - -thumbh -INT - - -thumbhash -NVARCHAR(64) - - -source -NVARCHAR(MAX) - - -fullbinary -VARBINARY(MAX) - - -thumbbinary -VARBINARY(MAX) + + + + + +pic + + +id +INT +PK + + +w +INT + + +h +INT + + +size +BIGINT + + +hash +NVARCHAR(64) + + +thumbw +INT + + +thumbh +INT + + +thumbhash +NVARCHAR(64) + + +source +NVARCHAR(MAX) + + +fullbinary +VARBINARY(MAX) + + +thumbbinary +VARBINARY(MAX) - - -brandpic:se->pic:n - - - + + +categorypic:se->pic:n + + + - + articlepic - - -articlepic - - -id -INT -PK - - -article_id -INT -FK - - -pic_id -INT -FK - - -sort -INT + + +articlepic + + +id +INT +PK + + +pic_id +INT +FK + + +article_id +INT +FK + + +sort +INT - -articlepic:w->article:n - - - + +articlepic:e->article:n + + + - -articlepic:se->pic:n - - - + +articlepic:sw->pic:n + + + - - -categorypic - - -categorypic - - -id -INT -PK - - -category_id -INT -FK - - -pic_id -INT -FK - - -sort -INT + + +brandpic + + +brandpic + + +id +INT +PK + + +pic_id +INT +FK + + +brand_id +INT +FK + + +sort +INT - - -categorypic:sw->category:n - - - + + +brandpic:e->brand:n + + + - - -categorypic:sw->pic:n - - - + + +brandpic:sw->pic:n + + + - + price - - -price - - -id -INT -PK - - -article_id -INT -FK - - -customergroup_id -INT -FK - - -tax_id -INT -FK - - -minmultiplier -DECIMAL(18,4) - - -net -FLOAT + + +price + + +id +INT +PK + + +article_id +INT +FK + + +customergroup_id +INT +FK + + +tax_id +INT +FK + + +minmultiplier +DECIMAL(18,4) + + +net +FLOAT - + price:sw->customergroup:n - - - + + + - + price:w->article:n - - - + + + - + price:sw->tax:n - - - + + + - + manifest - - - -manifest - - -id -INT -PK - - -number -NVARCHAR(100) - - -type -NVARCHAR(100) - - -date -DATETIME2 + + + +manifest + + +id +INT +PK + + +number +NVARCHAR(100) + + +type +NVARCHAR(100) + + +date +DATETIME2 - + manifestarticle - - -manifestarticle - - -id -INT -PK - - -article_id -? -INT -FK - - -tax_id -INT -FK - - -manifest_id -INT -FK - - -amount -FLOAT - - -net -FLOAT - - -transactedamount -FLOAT - - -comment? -NVARCHAR(MAX) - - -rebate -FLOAT + + +manifestarticle + + +id +INT +PK + + +article_id +? +INT +FK + + +tax_id +INT +FK + + +manifest_id +INT +FK + + +amount +FLOAT + + +net +FLOAT + + +transactedamount +FLOAT + + +comment? +NVARCHAR(MAX) + + +rebate +FLOAT - + manifestarticle:w->article:n - - - + + + - + manifestarticle:sw->tax:n - - - + + + - + manifestarticle:sw->manifest:n - - - + + + @@ -1384,38 +1438,38 @@ unit:e->company:n - - + + - + __legend - - - -Legend - -PK - -primary key - -FK - -foreign key - -? - -nullable column - -— - -FOREIGN KEY constraint - -- - - -naming convention only, no constraint + + + +Legend + +PK + +primary key + +FK + +foreign key + +? + +nullable column + +— + +FOREIGN KEY constraint + +- - + +naming convention only, no constraint diff --git a/scripts/test-api.js b/scripts/test-api.js index bb58dc7..b066691 100644 --- a/scripts/test-api.js +++ b/scripts/test-api.js @@ -113,6 +113,51 @@ const MENU = { }, ], }, + companysetting: { + label: 'companysetting', + items: [ + { + value: 'companysetting.list', + label: 'list', + hint: 'googleAuthId, companyId, frontendOnly? → rows', + }, + { + value: 'companysetting.get', + label: 'get', + hint: 'googleAuthId, companyId, key → row', + }, + { + value: 'companysetting.set', + label: 'set', + hint: 'googleAuthId, companyId, JSON setting → row', + }, + { + value: 'companysetting.delete', + label: 'delete', + hint: 'googleAuthId, companyId, key', + }, + ], + }, + brand: { + label: 'brand', + items: [ + { + value: 'brand.list', + label: 'list', + hint: 'googleAuthId, companyId → rows', + }, + { + value: 'brand.upsert', + label: 'upsert', + hint: 'googleAuthId, companyId, JSON brand → row', + }, + { + value: 'brand.delete', + label: 'delete', + hint: 'googleAuthId, companyId, id', + }, + ], + }, category: { label: 'category', items: [ @@ -418,6 +463,139 @@ async function runMethod(client, action) { await client.unit.delete(Number(googleAuthId), Number(companyId), si); return ['ok', true]; } + case 'companysetting.list': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const frontendOnly = await askText('frontendOnly (true/false)', { + placeholder: 'false', + }); + if (frontendOnly == null) return; + return [ + 'result[]', + await client.companysetting.list(Number(googleAuthId), Number(companyId), { + frontendOnly: frontendOnly.trim().toLowerCase() === 'true', + }), + ]; + } + case 'companysetting.get': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const key = await askText('key', { + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (key == null) return; + const frontendOnly = await askText('frontendOnly (true/false)', { + placeholder: 'false', + }); + if (frontendOnly == null) return; + return [ + 'result', + await client.companysetting.get( + Number(googleAuthId), + Number(companyId), + key, + { frontendOnly: frontendOnly.trim().toLowerCase() === 'true' }, + ), + ]; + } + case 'companysetting.set': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const settingJson = await askText('setting JSON', { + placeholder: '{"key":"theme","value":"dark","frontend":true}', + validate: (v) => { + if (!v?.trim()) return 'Required'; + try { + const parsed = JSON.parse(v); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return 'Must be a JSON object'; + } + } catch { + return 'Invalid JSON'; + } + }, + }); + if (settingJson == null) return; + return [ + 'result', + await client.companysetting.set( + Number(googleAuthId), + Number(companyId), + JSON.parse(settingJson), + ), + ]; + } + case 'companysetting.delete': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const key = await askText('key', { + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (key == null) return; + await client.companysetting.delete( + Number(googleAuthId), + Number(companyId), + key, + ); + return ['ok', true]; + } + case 'brand.list': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + return [ + 'result[]', + await client.brand.list(Number(googleAuthId), Number(companyId)), + ]; + } + case 'brand.upsert': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const brandJson = await askText('brand JSON', { + placeholder: '{"name":"Nike","gsprpinfo":"..."}', + validate: (v) => { + if (!v?.trim()) return 'Required'; + try { + const parsed = JSON.parse(v); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return 'Must be a JSON object'; + } + } catch { + return 'Invalid JSON'; + } + }, + }); + if (brandJson == null) return; + return [ + 'result', + await client.brand.upsert( + Number(googleAuthId), + Number(companyId), + JSON.parse(brandJson), + ), + ]; + } + case 'brand.delete': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const id = await askInt('brand id'); + if (id == null) return; + await client.brand.delete(Number(googleAuthId), Number(companyId), id); + return ['ok', true]; + } case 'lang.create': { const iso639 = await askText('ISO 639', { placeholder: 'de', diff --git a/src/index.js b/src/index.js index 2aa2ce5..8aeb0b5 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,13 @@ import articleSetPricesBySku from './store/article/setPricesBySku.js'; import unitList from './store/unit/list.js'; import unitUpsert from './store/unit/upsert.js'; import unitDelete from './store/unit/delete.js'; +import companysettingList from './store/companysetting/list.js'; +import companysettingGet from './store/companysetting/get.js'; +import companysettingSet from './store/companysetting/set.js'; +import companysettingDelete from './store/companysetting/delete.js'; +import brandList from './store/brand/list.js'; +import brandUpsert from './store/brand/upsert.js'; +import brandDelete from './store/brand/delete.js'; import langCreate from './store/lang/create.js'; import langDelete from './store/lang/delete.js'; import langList from './store/lang/list.js'; @@ -95,6 +102,19 @@ export default class LibDashStore { delete: unitDelete(pool), }; + this.companysetting = { + list: companysettingList(pool), + get: companysettingGet(pool), + set: companysettingSet(pool), + delete: companysettingDelete(pool), + }; + + this.brand = { + list: brandList(pool), + upsert: brandUpsert(pool), + delete: brandDelete(pool), + }; + this.google_auth = { add: googleAuthAdd(pool), getByEmail: googleAuthGetByEmail(pool), diff --git a/src/schema.js b/src/schema.js index 5f4c960..75aaa0c 100644 --- a/src/schema.js +++ b/src/schema.js @@ -212,8 +212,11 @@ IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'brand') BEGIN CREATE TABLE brand ( id INT IDENTITY(1,1) PRIMARY KEY, + company_id INT NOT NULL, name NVARCHAR(255) NOT NULL, - gsprpinfo NVARCHAR(MAX) NULL + gsprpinfo NVARCHAR(MAX) NULL, + CONSTRAINT UQ_brand_company_name UNIQUE (company_id, name), + CONSTRAINT FK_brand_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION ); END `; @@ -550,6 +553,21 @@ BEGIN END `; +export const INIT_COMPANYSETTING = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'companysetting') +BEGIN + CREATE TABLE companysetting ( + id INT IDENTITY(1,1) PRIMARY KEY, + company_id INT NOT NULL, + [key] NVARCHAR(255) NOT NULL, + value NVARCHAR(MAX) NOT NULL, + frontend BIT NOT NULL CONSTRAINT DF_companysetting_frontend DEFAULT 0, + CONSTRAINT UQ_companysetting_company_key UNIQUE (company_id, [key]), + CONSTRAINT FK_companysetting_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE + ); +END +`; + /** DDL scripts in dependency order for schema init. */ export const SCHEMA_SCRIPTS = [ INIT_META, @@ -585,4 +603,5 @@ export const SCHEMA_SCRIPTS = [ INIT_GET_NEXT_NUMBER_WITH_PREFIX_AND_PADDING, INIT_COMPANYUSER, INIT_SYNCPOS, + INIT_COMPANYSETTING, ]; diff --git a/src/sql/brand.js b/src/sql/brand.js new file mode 100644 index 0000000..bbe4e8c --- /dev/null +++ b/src/sql/brand.js @@ -0,0 +1,36 @@ +export const LIST_BY_COMPANY = ` +SELECT + id, + name, + gsprpinfo +FROM brand +WHERE company_id = @companyId +ORDER BY name +`; + +export const GET_BY_COMPANY_AND_NAME = ` +SELECT + id, + name, + gsprpinfo +FROM brand +WHERE company_id = @companyId + AND name = @name +`; + +export const CREATE = ` +INSERT INTO brand (company_id, name, gsprpinfo) +OUTPUT INSERTED.id +VALUES (@companyId, @name, @gsprpinfo) +`; + +export const UPDATE_BY_ID = ` +UPDATE brand +SET name = @name, gsprpinfo = @gsprpinfo +WHERE id = @id AND company_id = @companyId +`; + +export const DELETE_BY_COMPANY_AND_ID = ` +DELETE FROM brand +WHERE id = @id AND company_id = @companyId +`; diff --git a/src/sql/companysetting.js b/src/sql/companysetting.js new file mode 100644 index 0000000..85d189c --- /dev/null +++ b/src/sql/companysetting.js @@ -0,0 +1,47 @@ +export const LIST_BY_COMPANY = ` +SELECT + [key], + value, + CAST(frontend AS INT) AS frontend +FROM companysetting +WHERE company_id = @companyId +ORDER BY [key] +`; + +export const LIST_BY_COMPANY_FRONTEND = ` +SELECT + [key], + value, + CAST(frontend AS INT) AS frontend +FROM companysetting +WHERE company_id = @companyId + AND frontend = 1 +ORDER BY [key] +`; + +export const GET_BY_COMPANY_AND_KEY = ` +SELECT + [key], + value, + CAST(frontend AS INT) AS frontend +FROM companysetting +WHERE company_id = @companyId + AND [key] = @key +`; + +export const UPSERT = ` +MERGE companysetting AS target +USING (SELECT @companyId AS company_id, @key AS [key]) AS src + ON target.company_id = src.company_id AND target.[key] = src.[key] +WHEN MATCHED THEN + UPDATE SET value = @value, frontend = @frontend +WHEN NOT MATCHED THEN + INSERT (company_id, [key], value, frontend) + VALUES (@companyId, @key, @value, @frontend); +`; + +export const DELETE_BY_COMPANY_AND_KEY = ` +DELETE FROM companysetting +WHERE company_id = @companyId + AND [key] = @key +`; diff --git a/src/sql/purge.js b/src/sql/purge.js index c2e6527..7c24353 100644 --- a/src/sql/purge.js +++ b/src/sql/purge.js @@ -2,6 +2,7 @@ export const TABLES = [ 'companyuser', 'syncpos', + 'companysetting', 'numbers', 'manifestarticle', 'manifest', diff --git a/src/store/brand/delete.js b/src/store/brand/delete.js new file mode 100644 index 0000000..a3bcc06 --- /dev/null +++ b/src/store/brand/delete.js @@ -0,0 +1,31 @@ +import { DELETE_BY_COMPANY_AND_ID } from '../../sql/brand.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +export default function del(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, id) => { + if (!Number.isFinite(id)) { + throw new Error('Brand not found'); + } + + await access(googleAuthId, companyId); + + try { + const result = await pool + .request() + .input('id', id) + .input('companyId', companyId) + .query(DELETE_BY_COMPANY_AND_ID); + + if (!result.rowsAffected[0]) { + throw new Error('Brand not found'); + } + } catch (err) { + if (err.number === 547) { + throw new Error('Brand in use'); + } + throw err; + } + }; +} diff --git a/src/store/brand/list.js b/src/store/brand/list.js new file mode 100644 index 0000000..97c7d08 --- /dev/null +++ b/src/store/brand/list.js @@ -0,0 +1,17 @@ +import { LIST_BY_COMPANY } from '../../sql/brand.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +export default function list(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId) => { + await access(googleAuthId, companyId); + + const result = await pool + .request() + .input('companyId', companyId) + .query(LIST_BY_COMPANY); + + return result.recordset; + }; +} diff --git a/src/store/brand/upsert.js b/src/store/brand/upsert.js new file mode 100644 index 0000000..609dccd --- /dev/null +++ b/src/store/brand/upsert.js @@ -0,0 +1,51 @@ +import { + CREATE, + GET_BY_COMPANY_AND_NAME, + UPDATE_BY_ID, +} from '../../sql/brand.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +export default function upsert(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, input = {}) => { + const name = input.name?.trim(); + if (!name) { + throw new Error('Invalid brand'); + } + const gsprpinfo = input.gsprpinfo ?? null; + + await access(googleAuthId, companyId); + + const existing = await pool + .request() + .input('companyId', companyId) + .input('name', name) + .query(GET_BY_COMPANY_AND_NAME); + + if (existing.recordset[0]) { + const { id } = existing.recordset[0]; + await pool + .request() + .input('id', id) + .input('companyId', companyId) + .input('name', name) + .input('gsprpinfo', gsprpinfo) + .query(UPDATE_BY_ID); + return { id, name, gsprpinfo }; + } + + const created = await pool + .request() + .input('companyId', companyId) + .input('name', name) + .input('gsprpinfo', gsprpinfo) + .query(CREATE); + + return { + id: created.recordset[0].id, + name, + gsprpinfo, + }; + }; +} diff --git a/src/store/companysetting/delete.js b/src/store/companysetting/delete.js new file mode 100644 index 0000000..29b83ea --- /dev/null +++ b/src/store/companysetting/delete.js @@ -0,0 +1,24 @@ +import { DELETE_BY_COMPANY_AND_KEY } from '../../sql/companysetting.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +export default function del(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, key) => { + if (!key?.trim()) { + throw new Error('Setting not found'); + } + + await access(googleAuthId, companyId); + + const result = await pool + .request() + .input('companyId', companyId) + .input('key', key.trim()) + .query(DELETE_BY_COMPANY_AND_KEY); + + if (!result.rowsAffected[0]) { + throw new Error('Setting not found'); + } + }; +} diff --git a/src/store/companysetting/get.js b/src/store/companysetting/get.js new file mode 100644 index 0000000..fa5562c --- /dev/null +++ b/src/store/companysetting/get.js @@ -0,0 +1,31 @@ +import { GET_BY_COMPANY_AND_KEY } from '../../sql/companysetting.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import shapeSetting from './shapeSetting.js'; + +export default function get(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, key, { frontendOnly = false } = {}) => { + if (!key?.trim()) { + return undefined; + } + + await access(googleAuthId, companyId); + + const result = await pool + .request() + .input('companyId', companyId) + .input('key', key.trim()) + .query(GET_BY_COMPANY_AND_KEY); + + const row = result.recordset[0]; + if (!row) { + return undefined; + } + if (frontendOnly && !row.frontend) { + return undefined; + } + + return shapeSetting(row); + }; +} diff --git a/src/store/companysetting/list.js b/src/store/companysetting/list.js new file mode 100644 index 0000000..d25354b --- /dev/null +++ b/src/store/companysetting/list.js @@ -0,0 +1,21 @@ +import { + LIST_BY_COMPANY, + LIST_BY_COMPANY_FRONTEND, +} from '../../sql/companysetting.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import shapeSetting from './shapeSetting.js'; + +export default function list(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, { frontendOnly = false } = {}) => { + await access(googleAuthId, companyId); + + const result = await pool + .request() + .input('companyId', companyId) + .query(frontendOnly ? LIST_BY_COMPANY_FRONTEND : LIST_BY_COMPANY); + + return result.recordset.map(shapeSetting); + }; +} diff --git a/src/store/companysetting/set.js b/src/store/companysetting/set.js new file mode 100644 index 0000000..945ad42 --- /dev/null +++ b/src/store/companysetting/set.js @@ -0,0 +1,36 @@ +import { GET_BY_COMPANY_AND_KEY, UPSERT } from '../../sql/companysetting.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import shapeSetting from './shapeSetting.js'; + +export default function set(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, input = {}) => { + const key = input.key?.trim(); + if (!key) { + throw new Error('Invalid setting'); + } + if (typeof input.value !== 'string') { + throw new Error('Invalid setting'); + } + + const frontend = Boolean(input.frontend); + await access(googleAuthId, companyId); + + await pool + .request() + .input('companyId', companyId) + .input('key', key) + .input('value', input.value) + .input('frontend', frontend) + .query(UPSERT); + + const result = await pool + .request() + .input('companyId', companyId) + .input('key', key) + .query(GET_BY_COMPANY_AND_KEY); + + return shapeSetting(result.recordset[0]); + }; +} diff --git a/src/store/companysetting/shapeSetting.js b/src/store/companysetting/shapeSetting.js new file mode 100644 index 0000000..0e77fd0 --- /dev/null +++ b/src/store/companysetting/shapeSetting.js @@ -0,0 +1,9 @@ +function shapeSetting(row) { + return { + key: row.key, + value: row.value, + frontend: Boolean(row.frontend), + }; +} + +export default shapeSetting;