From 3b77a1c9460e343d2202351b589504e2fc38dc19 Mon Sep 17 00:00:00 2001 From: seb Date: Thu, 6 Aug 2026 05:43:50 +0200 Subject: [PATCH] Add entity address/contact tables and CRUD for entity resources --- README.md | 89 + generate-schema-graph.js | 2 + schema-graph.dot | 108 +- schema-graph.svg | 2706 +++++++++-------- scripts/test-api.js | 393 +++ src/index.js | 46 + src/schema.js | 34 + src/sql/customergroup.js | 29 +- src/sql/entity.js | 73 +- src/sql/entityaddress.js | 93 + src/sql/entitycontact.js | 47 + src/sql/purge.js | 2 + src/store/customergroup/create.js | 24 + src/store/customergroup/delete.js | 31 + src/store/customergroup/list.js | 20 + .../resolveCustomerGroupAccess.js | 22 + src/store/customergroup/update.js | 32 + src/store/entity/create.js | 69 + src/store/entity/delete.js | 24 + src/store/entity/getById.js | 37 + src/store/entity/list.js | 43 + src/store/entity/resolveEntityAccess.js | 22 + src/store/entity/update.js | 110 + src/store/entityaddress/create.js | 56 + src/store/entityaddress/delete.js | 27 + src/store/entityaddress/list.js | 35 + src/store/entityaddress/update.js | 104 + src/store/entitycontact/create.js | 39 + src/store/entitycontact/delete.js | 27 + src/store/entitycontact/list.js | 29 + src/store/entitycontact/update.js | 67 + src/store/supplier/create.js | 1 + 32 files changed, 3078 insertions(+), 1363 deletions(-) create mode 100644 src/sql/entityaddress.js create mode 100644 src/sql/entitycontact.js create mode 100644 src/store/customergroup/create.js create mode 100644 src/store/customergroup/delete.js create mode 100644 src/store/customergroup/list.js create mode 100644 src/store/customergroup/resolveCustomerGroupAccess.js create mode 100644 src/store/customergroup/update.js create mode 100644 src/store/entity/create.js create mode 100644 src/store/entity/delete.js create mode 100644 src/store/entity/getById.js create mode 100644 src/store/entity/list.js create mode 100644 src/store/entity/resolveEntityAccess.js create mode 100644 src/store/entity/update.js create mode 100644 src/store/entityaddress/create.js create mode 100644 src/store/entityaddress/delete.js create mode 100644 src/store/entityaddress/list.js create mode 100644 src/store/entityaddress/update.js create mode 100644 src/store/entitycontact/create.js create mode 100644 src/store/entitycontact/delete.js create mode 100644 src/store/entitycontact/list.js create mode 100644 src/store/entitycontact/update.js diff --git a/README.md b/README.md index e1c4799..cae33ff 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,78 @@ Company-scoped brands. Auth via `resolveCompanyAccess`. --- +## `customergroup` + +Company-scoped customer groups. Auth via `resolveCompanyAccess`. + +| Method | Signature | Returns | +| --- | --- | --- | +| `list` | `(googleAuthId, companyId)` | `{ id, name }[]` | +| `create` | `(googleAuthId, companyId, { name })` | `{ id, name }` | +| `update` | `(googleAuthId, companyId, id, { name })` | `{ id, name }` | +| `delete` | `(googleAuthId, companyId, id)` | `void` | + +Empty name → `Name required`. Deleting the POS group (or any group still referenced with `NO ACTION`) → `Customer group in use`. Cascades entities when the group is deleted. Errors: `Company not found`, `Not authorized`, `Name required`, `Customer group not found`, `Customer group in use`. + +--- + +## `entity` + +Entities belong to a `customergroup` of the company. Auth via `resolveCompanyAccess`. + +| Method | Signature | Returns | +| --- | --- | --- | +| `list` | `(googleAuthId, companyId, { customergroupId? })` | `Entity[]` | +| `getById` | `(googleAuthId, companyId, id)` | `Entity` | +| `create` | `(googleAuthId, companyId, { customergroupId, name, firstName? })` | `Entity` | +| `update` | `(googleAuthId, companyId, id, { name?, firstName? })` | `Entity` | +| `delete` | `(googleAuthId, companyId, id)` | `void` | + +```ts +type Entity = { + id: number, + customergroupId: number, + firstName: string | null, + name: string, + createdAt: Date, + updatedAt: Date, +} +``` + +`create` / `update` write `entityhistory` (`Erstellt`, or name/firstName change lines). `customergroupId` must belong to the company. Errors: `Company not found`, `Not authorized`, `Name required`, `Customer group not found`, `Entity not found`. + +--- + +## `entityaddress` + +Addresses for an entity. Auth via company + entity ownership. + +| Method | Signature | Returns | +| --- | --- | --- | +| `list` | `(googleAuthId, companyId, entityId)` | `EntityAddress[]` | +| `create` | `(googleAuthId, companyId, entityId, { addressType, firstName?, name?, street?, streetNumber?, zip?, city?, county? })` | `EntityAddress` | +| `update` | `(googleAuthId, companyId, entityId, id, patch)` | `EntityAddress` | +| `delete` | `(googleAuthId, companyId, entityId, id)` | `void` | + +`addressType` required. Errors: `Company not found`, `Not authorized`, `Entity not found`, `Address type required`, `Address not found`. + +--- + +## `entitycontact` + +Contacts for an entity (`contactType` e.g. `email`, `phone`). Auth via company + entity ownership. + +| Method | Signature | Returns | +| --- | --- | --- | +| `list` | `(googleAuthId, companyId, entityId)` | `{ id, entityId, contactType, contact }[]` | +| `create` | `(googleAuthId, companyId, entityId, { contactType, contact })` | row | +| `update` | `(googleAuthId, companyId, entityId, id, { contactType?, contact? })` | row | +| `delete` | `(googleAuthId, companyId, entityId, id)` | `void` | + +Errors: `Company not found`, `Not authorized`, `Entity not found`, `Contact required`, `Contact not found`. + +--- + ## `supplier` Suppliers are `entity` rows in the company `customergroup` named `Lieferanten` (created implicitly on first supplier). Auth via `resolveCompanyAccess`. @@ -519,6 +591,23 @@ res.set('Content-Type', thumb.contentType).send(thumb.buffer); | `GET /api/brand` | `brand.list(googleAuthId, companyId)` | | `PUT /api/brand` | `brand.upsert(googleAuthId, companyId, { name, gsprpinfo? })` | | `DELETE /api/brand/:id` | `brand.delete(googleAuthId, companyId, id)` | +| `GET /api/customergroup` | `customergroup.list(googleAuthId, companyId)` | +| `POST /api/customergroup` | `customergroup.create(googleAuthId, companyId, { name })` | +| `PATCH /api/customergroup/:id` | `customergroup.update(googleAuthId, companyId, id, { name })` | +| `DELETE /api/customergroup/:id` | `customergroup.delete(googleAuthId, companyId, id)` | +| `GET /api/entity` | `entity.list(googleAuthId, companyId, { customergroupId? })` | +| `GET /api/entity/:id` | `entity.getById(googleAuthId, companyId, id)` | +| `POST /api/entity` | `entity.create(googleAuthId, companyId, { customergroupId, name, firstName? })` | +| `PATCH /api/entity/:id` | `entity.update(googleAuthId, companyId, id, { name?, firstName? })` | +| `DELETE /api/entity/:id` | `entity.delete(googleAuthId, companyId, id)` | +| `GET /api/entity/:entityId/address` | `entityaddress.list(googleAuthId, companyId, entityId)` | +| `POST /api/entity/:entityId/address` | `entityaddress.create(googleAuthId, companyId, entityId, { addressType, … })` | +| `PATCH /api/entity/:entityId/address/:id` | `entityaddress.update(googleAuthId, companyId, entityId, id, patch)` | +| `DELETE /api/entity/:entityId/address/:id` | `entityaddress.delete(googleAuthId, companyId, entityId, id)` | +| `GET /api/entity/:entityId/contact` | `entitycontact.list(googleAuthId, companyId, entityId)` | +| `POST /api/entity/:entityId/contact` | `entitycontact.create(googleAuthId, companyId, entityId, { contactType, contact })` | +| `PATCH /api/entity/:entityId/contact/:id` | `entitycontact.update(googleAuthId, companyId, entityId, id, patch)` | +| `DELETE /api/entity/:entityId/contact/:id` | `entitycontact.delete(googleAuthId, companyId, entityId, id)` | | `GET /api/supplier` | `supplier.list(googleAuthId, companyId)` | | `POST /api/supplier` | `supplier.create(googleAuthId, companyId, { name })` | | `PATCH /api/supplier/:id` | `supplier.update(googleAuthId, companyId, id, { name })` | diff --git a/generate-schema-graph.js b/generate-schema-graph.js index ff6b20f..eb43386 100644 --- a/generate-schema-graph.js +++ b/generate-schema-graph.js @@ -275,6 +275,8 @@ const CLUSTERS = [ 'customergroup', 'entity', 'entityhistory', + 'entityaddress', + 'entitycontact', 'numbers', ], }, diff --git a/schema-graph.dot b/schema-graph.dot index 5797eb8..d8b2770 100644 --- a/schema-graph.dot +++ b/schema-graph.dot @@ -10,14 +10,16 @@ digraph schema { style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; customergroup [label=<
customergroup
idINT PK
company_id?INT FK
nameNVARCHAR(255)
>]; + entityaddress [label=<
entityaddress
idINT PK
entity_idINT FK
first_name?NVARCHAR(255)
name?NVARCHAR(255)
street?NVARCHAR(255)
streetnumber?NVARCHAR(50)
zip?NVARCHAR(50)
city?NVARCHAR(255)
county?NVARCHAR(255)
addresstypeNVARCHAR(50)
>]; numbers [label=<
numbers
idINT PK
company_idINT FK
typeNVARCHAR(100)
prefixNVARCHAR(50)
paddingINT
nextINT
>]; + entity [label=<
entity
idINT PK
customergroup_idINT FK
first_name?NVARCHAR(255)
nameNVARCHAR(255)
created_atDATETIME2
updated_atDATETIME2
>]; 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)
>]; - entity [label=<
entity
idINT PK
customergroup_idINT FK
first_name?NVARCHAR(255)
nameNVARCHAR(255)
created_atDATETIME2
updated_atDATETIME2
>]; syncpos [label=<
syncpos
idINT PK
company_idINT FK
paircodeNVARCHAR(50)
token?NVARCHAR(255)
>]; + entitycontact [label=<
entitycontact
idINT PK
entity_idINT FK
contacttypeNVARCHAR(50)
contactNVARCHAR(255)
>]; meta [label=<
meta
keyNVARCHAR(255)
valueNVARCHAR(MAX)
>]; + companysetting [label=<
companysetting
idINT PK
company_idINT FK
keyNVARCHAR(255)
valueNVARCHAR(MAX)
frontendBIT
>]; entityhistory [label=<
entityhistory
idINT PK
entity_idINT FK
companyuser_id?INT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; companyuser [label=<
companyuser
idINT PK
company_idINT FK
google_auth_idINT FK
>]; - companysetting [label=<
companysetting
idINT PK
company_idINT FK
keyNVARCHAR(255)
valueNVARCHAR(MAX)
frontendBIT
>]; google_auth [label=<
google_auth
idINT PK
selected_company?INT FK
emailNVARCHAR(320)
picture?NVARCHAR(2048)
picturebinary?VARBINARY(MAX)
sub?NVARCHAR(255)
created_atDATETIME2
>]; } subgraph cluster_i18n { @@ -58,9 +60,9 @@ digraph schema { style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; tax [label=<
tax
idINT PK
rateDECIMAL(18,4)
>]; - manifest [label=<
manifest
idINT PK
company_idINT FK
entity_id?INT FK
numberNVARCHAR(100)
typeNVARCHAR(100)
dateDATETIME2
>]; - manifestarticle [label=<
manifestarticle
idINT PK
article_id?INT FK
tax_idINT FK
manifest_idINT FK
amountFLOAT
netFLOAT
transactedamountFLOAT
comment?NVARCHAR(MAX)
rebateFLOAT
>]; price [label=<
price
idINT PK
article_idINT FK
customergroup_idINT FK
tax_idINT FK
minmultiplierDECIMAL(18,4)
netFLOAT
>]; + manifest [label=<
manifest
idINT PK
entity_id?INT FK
company_idINT FK
numberNVARCHAR(100)
typeNVARCHAR(100)
dateDATETIME2
>]; + manifestarticle [label=<
manifestarticle
idINT PK
article_id?INT FK
tax_idINT FK
manifest_idINT FK
amountFLOAT
netFLOAT
transactedamountFLOAT
comment?NVARCHAR(MAX)
rebateFLOAT
>]; } subgraph cluster_units { label="Units"; labeljust="l"; fontsize=15; @@ -81,52 +83,54 @@ digraph schema { company:reducedtaxrate:e -> tax:in0:n [color="#c05621", fontcolor="#c05621", tooltip="FK_company_reducedtaxrate"]; company:taxfreerate:e -> tax:in1:n [color="#319795", fontcolor="#319795", tooltip="FK_company_taxfreerate"]; customergroup:company_id:se -> company:in3:n [color="#97266d", fontcolor="#97266d", tooltip="FK_customergroup_company"]; - company:poscustomergroup_id:nw -> customergroup:in0:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_company_poscustomergroup"]; - entity:customergroup_id:sw -> customergroup:in1:n [color="#276749", fontcolor="#276749", tooltip="FK_entity_customergroup"]; - entityhistory:entity_id:sw -> entity:in0:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_entityhistory_entity"]; - entityhistory:companyuser_id:se -> companyuser:in1:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_entityhistory_companyuser"]; - unit:company_id:e -> company:in0:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_unit_company"]; - unittext:unit_id:se -> unit:in0:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_unittext_unit"]; - unittext:language_id:se -> language:in0:n [color="#22543d", fontcolor="#22543d", tooltip="FK_unittext_language"]; - brand:company_id:e -> company:in1:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_brand_company"]; - article:unit_id:sw -> unit:in1:n [color="#702459", fontcolor="#702459", tooltip="FK_article_unit"]; - article:brand_id:e -> brand:in0:n [color="#744210", fontcolor="#744210", tooltip="FK_article_brand"]; - article:articlelist_id:sw -> articlelist:in1:n [color="#285e61", fontcolor="#285e61", tooltip="FK_article_articlelist"]; - article:optionslist_id:se -> optionslist:in0:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_article_optionslist"]; - article:fatherarticle_id:e -> article:id:e [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_article_fatherarticle"]; - articlehistory:article_id:w -> article:in5:n [color="#38a169", fontcolor="#38a169", tooltip="FK_articlehistory_article"]; - articlehistory:companyuser_id:e -> companyuser:in0:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_articlehistory_companyuser"]; - articleean:article_id:sw -> article:in3:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_articleean_article"]; - articlepic:article_id:w -> article:in7:n [color="#c53030", fontcolor="#c53030", tooltip="FK_articlepic_article"]; - articlepic:pic_id:se -> pic:in1:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_articlepic_pic"]; - brandpic:brand_id:w -> brand:in1:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_brandpic_brand"]; - brandpic:pic_id:se -> pic:in0:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_brandpic_pic"]; - articlelistarticle:articlelist_id:se -> articlelist:in0:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_articlelistarticle_articlelist"]; - articlelistarticle:article_id:se -> article:in1:n [color="#c05621", fontcolor="#c05621", tooltip="FK_articlelistarticle_article"]; - optionslistarticle:optionslist_id:sw -> optionslist:in1:n [color="#319795", fontcolor="#319795", tooltip="FK_optionslistarticle_optionslist"]; - optionslistarticle:article_id:sw -> article:in4:n [color="#97266d", fontcolor="#97266d", tooltip="FK_optionslistarticle_article"]; - categoryarticle:category_id:sw -> category:in2:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_categoryarticle_category"]; - categoryarticle:article_id:w -> article:in6:n [color="#276749", fontcolor="#276749", tooltip="FK_categoryarticle_article"]; - categorypic:category_id:sw -> category:in4:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_categorypic_category"]; - categorypic:pic_id:sw -> pic:in2:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_categorypic_pic"]; - categorytext:category_id:e -> category:in0:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_categorytext_category"]; - categorytext:language_id:sw -> language:in1:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_categorytext_language"]; - articletext:article_id:e -> article:in0:n [color="#22543d", fontcolor="#22543d", tooltip="FK_articletext_article"]; - articletext:language_id:sw -> language:in2:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_articletext_language"]; - price:tax_id:sw -> tax:in4:n [color="#702459", fontcolor="#702459", tooltip="FK_price_tax"]; - price:article_id:w -> article:in9:n [color="#744210", fontcolor="#744210", tooltip="FK_price_article"]; - price:customergroup_id:w -> customergroup:in2:n [color="#285e61", fontcolor="#285e61", tooltip="FK_price_customergroup"]; - manifest:company_id:sw -> company:in9:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_manifest_company"]; - manifest:entity_id:w -> entity:in1:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_manifest_entity"]; - manifestarticle:manifest_id:sw -> manifest:in0:n [color="#38a169", fontcolor="#38a169", tooltip="FK_manifestarticle_manifest"]; - manifestarticle:article_id:w -> article:in8:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_manifestarticle_article"]; - manifestarticle:tax_id:sw -> tax:in3:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_manifestarticle_tax"]; - numbers:company_id:se -> company:in4:n [color="#c53030", fontcolor="#c53030", tooltip="FK_numbers_company"]; - companyuser:google_auth_id:se -> google_auth:in0:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_companyuser_google_auth"]; - companyuser:company_id:sw -> company:in6:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_companyuser_company"]; - syncpos:company_id:sw -> company:in5:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_syncpos_company"]; - companysetting:company_id:sw -> company:in7:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_companysetting_company"]; - posterminal:company_id:e -> company:in2:n [color="#c05621", fontcolor="#c05621", tooltip="FK_posterminal_company"]; - posterminalcategory:posterminal_id:se -> posterminal:in0:n [color="#319795", fontcolor="#319795", tooltip="FK_posterminalcategory_posterminal"]; - posterminalcategory:category_id:sw -> category:in3:n [color="#97266d", fontcolor="#97266d", tooltip="FK_posterminalcategory_category"]; + company:poscustomergroup_id:nw -> customergroup:in1:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_company_poscustomergroup"]; + entity:customergroup_id:sw -> customergroup:in0:n [color="#276749", fontcolor="#276749", tooltip="FK_entity_customergroup"]; + entityaddress:entity_id:se -> entity:in0:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_entityaddress_entity"]; + entitycontact:entity_id:sw -> entity:in1:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_entitycontact_entity"]; + entityhistory:entity_id:sw -> entity:in2:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_entityhistory_entity"]; + entityhistory:companyuser_id:se -> companyuser:in1:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_entityhistory_companyuser"]; + unit:company_id:e -> company:in0:n [color="#22543d", fontcolor="#22543d", tooltip="FK_unit_company"]; + unittext:unit_id:se -> unit:in0:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_unittext_unit"]; + unittext:language_id:se -> language:in0:n [color="#702459", fontcolor="#702459", tooltip="FK_unittext_language"]; + brand:company_id:e -> company:in1:n [color="#744210", fontcolor="#744210", tooltip="FK_brand_company"]; + article:unit_id:sw -> unit:in1:n [color="#285e61", fontcolor="#285e61", tooltip="FK_article_unit"]; + article:brand_id:e -> brand:in0:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_article_brand"]; + article:articlelist_id:sw -> articlelist:in1:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_article_articlelist"]; + article:optionslist_id:se -> optionslist:in0:n [color="#38a169", fontcolor="#38a169", tooltip="FK_article_optionslist"]; + article:fatherarticle_id:e -> article:id:e [color="#3182ce", fontcolor="#3182ce", tooltip="FK_article_fatherarticle"]; + articlehistory:article_id:w -> article:in5:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_articlehistory_article"]; + articlehistory:companyuser_id:e -> companyuser:in0:n [color="#c53030", fontcolor="#c53030", tooltip="FK_articlehistory_companyuser"]; + articleean:article_id:sw -> article:in3:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_articleean_article"]; + articlepic:article_id:w -> article:in7:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_articlepic_article"]; + articlepic:pic_id:se -> pic:in1:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_articlepic_pic"]; + brandpic:brand_id:w -> brand:in1:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_brandpic_brand"]; + brandpic:pic_id:se -> pic:in0:n [color="#c05621", fontcolor="#c05621", tooltip="FK_brandpic_pic"]; + articlelistarticle:articlelist_id:se -> articlelist:in0:n [color="#319795", fontcolor="#319795", tooltip="FK_articlelistarticle_articlelist"]; + articlelistarticle:article_id:se -> article:in1:n [color="#97266d", fontcolor="#97266d", tooltip="FK_articlelistarticle_article"]; + optionslistarticle:optionslist_id:sw -> optionslist:in1:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_optionslistarticle_optionslist"]; + optionslistarticle:article_id:sw -> article:in4:n [color="#276749", fontcolor="#276749", tooltip="FK_optionslistarticle_article"]; + categoryarticle:category_id:sw -> category:in2:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_categoryarticle_category"]; + categoryarticle:article_id:w -> article:in6:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_categoryarticle_article"]; + categorypic:category_id:sw -> category:in4:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_categorypic_category"]; + categorypic:pic_id:sw -> pic:in2:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_categorypic_pic"]; + categorytext:category_id:e -> category:in0:n [color="#22543d", fontcolor="#22543d", tooltip="FK_categorytext_category"]; + categorytext:language_id:sw -> language:in1:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_categorytext_language"]; + articletext:article_id:e -> article:in0:n [color="#702459", fontcolor="#702459", tooltip="FK_articletext_article"]; + articletext:language_id:sw -> language:in2:n [color="#744210", fontcolor="#744210", tooltip="FK_articletext_language"]; + price:tax_id:sw -> tax:in3:n [color="#285e61", fontcolor="#285e61", tooltip="FK_price_tax"]; + price:article_id:w -> article:in8:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_price_article"]; + price:customergroup_id:w -> customergroup:in2:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_price_customergroup"]; + manifest:company_id:sw -> company:in9:n [color="#38a169", fontcolor="#38a169", tooltip="FK_manifest_company"]; + manifest:entity_id:w -> entity:in3:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_manifest_entity"]; + manifestarticle:manifest_id:sw -> manifest:in0:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_manifestarticle_manifest"]; + manifestarticle:article_id:w -> article:in9:n [color="#c53030", fontcolor="#c53030", tooltip="FK_manifestarticle_article"]; + manifestarticle:tax_id:sw -> tax:in4:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_manifestarticle_tax"]; + numbers:company_id:se -> company:in4:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_numbers_company"]; + companyuser:google_auth_id:se -> google_auth:in0:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_companyuser_google_auth"]; + companyuser:company_id:sw -> company:in7:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_companyuser_company"]; + syncpos:company_id:sw -> company:in5:n [color="#c05621", fontcolor="#c05621", tooltip="FK_syncpos_company"]; + companysetting:company_id:sw -> company:in6:n [color="#319795", fontcolor="#319795", tooltip="FK_companysetting_company"]; + posterminal:company_id:e -> company:in2:n [color="#97266d", fontcolor="#97266d", tooltip="FK_posterminal_company"]; + posterminalcategory:posterminal_id:se -> posterminal:in0:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_posterminalcategory_posterminal"]; + posterminalcategory:category_id:sw -> category:in3:n [color="#276749", fontcolor="#276749", tooltip="FK_posterminalcategory_category"]; } \ No newline at end of file diff --git a/schema-graph.svg b/schema-graph.svg index 2360bcd..f7e0d52 100644 --- a/schema-graph.svg +++ b/schema-graph.svg @@ -4,175 +4,279 @@ - - + + 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 customergroup - - - - - -customergroup - - -id -INT -PK - - -company_id -? -INT -FK - - -name -NVARCHAR(255) + + + + + +customergroup + + +id +INT +PK + + +company_id +? +INT +FK + + +name +NVARCHAR(255) - + 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) customergroup:se->company:n - - + + + + + + + +entityaddress + + +entityaddress + + +id +INT +PK + + +entity_id +INT +FK + + +first_name? +NVARCHAR(255) + + +name? +NVARCHAR(255) + + +street? +NVARCHAR(255) + + +streetnumber? +NVARCHAR(50) + + +zip? +NVARCHAR(50) + + +city? +NVARCHAR(255) + + +county? +NVARCHAR(255) + + +addresstype +NVARCHAR(50) + + + +entity + + + + + + +entity + + +id +INT +PK + + +customergroup_id +INT +FK + + +first_name? +NVARCHAR(255) + + +name +NVARCHAR(255) + + +created_at +DATETIME2 + + +updated_at +DATETIME2 + + + +entityaddress:se->entity:n + + + - + 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 - + numbers:se->company:n - - - + + + + + + + + +entity:sw->customergroup:n + + + @@ -180,104 +284,104 @@ company:nw->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 - - + + @@ -285,8 +389,8 @@ company:e->tax:n - - + + @@ -294,269 +398,259 @@ company:e->tax:n - - - - - - - -entity - - - - -entity - - -id -INT -PK - - -customergroup_id -INT -FK - - -first_name? -NVARCHAR(255) - - -name -NVARCHAR(255) - - -created_at -DATETIME2 - - -updated_at -DATETIME2 - - - -entity:sw->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:sw->company:n - - - + + + + + + + + +entitycontact + + +entitycontact + + +id +INT +PK + + +entity_id +INT +FK + + +contacttype +NVARCHAR(50) + + +contact +NVARCHAR(255) + + + +entitycontact:sw->entity:n + + + - -meta - - -meta - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) - - - -entityhistory - - -entityhistory - - -id -INT -PK - - -entity_id -INT -FK - - -companyuser_id -? -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) - - - -entityhistory:sw->entity:n - - - - - - - -companyuser - - - - -companyuser - - -id -INT -PK - - -company_id -INT -FK - - -google_auth_id -INT -FK - - - -entityhistory:se->companyuser:n - - - - - - - - -companyuser:sw->company:n - - - - - - - - -google_auth - - - -google_auth - - -id -INT -PK - - -selected_company -? -INT -FK - - -email -NVARCHAR(320) - - -picture? -NVARCHAR(2048) - - -picturebinary? -VARBINARY(MAX) - - -sub? -NVARCHAR(255) - - -created_at -DATETIME2 - - - -companyuser:se->google_auth:n - - - - - +meta + + +meta + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) companysetting - - -companysetting - - -id -INT -PK - - -company_id -INT -FK - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) - - -frontend -BIT + + +companysetting + + +id +INT +PK + + +company_id +INT +FK + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) + + +frontend +BIT - + companysetting:sw->company:n - - - + + + + + + + + +entityhistory + + +entityhistory + + +id +INT +PK + + +entity_id +INT +FK + + +companyuser_id +? +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) + + + +entityhistory:sw->entity:n + + + + + + + + +companyuser + + + + +companyuser + + +id +INT +PK + + +company_id +INT +FK + + +google_auth_id +INT +FK + + + +entityhistory:se->companyuser:n + + + + + + + + +companyuser:sw->company:n + + + + + + + + +google_auth + + + +google_auth + + +id +INT +PK + + +selected_company +? +INT +FK + + +email +NVARCHAR(320) + + +picture? +NVARCHAR(2048) + + +picturebinary? +VARBINARY(MAX) + + +sub? +NVARCHAR(255) + + +created_at +DATETIME2 + + + +companyuser:se->google_auth:n + + + @@ -564,566 +658,566 @@ google_auth:w->company: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 - - - + + + - + 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 - - - + + + - + 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 + + +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 - - - + + + - + 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:sw->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 + + +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 - + articletext:e->article:n - - - + + + - + articlelistarticle - - -articlelistarticle - - -id -INT -PK - - -articlelist_id -INT -FK - - -article_id -INT -FK - - -count -INT - - -sort -INT + + +articlelistarticle + + +id +INT +PK + + +articlelist_id +INT +FK + + +article_id +INT +FK + + +count +INT + + +sort +INT - + articlelist - - - - -articlelist - - -id -INT -PK + + + + +articlelist + + +id +INT +PK - + articlelistarticle:se->articlelist:n - - - + + + - + articlelistarticle:se->article:n - - - + + + - + article:sw->articlelist:n - - - + + + - + article:e->article:e - - - + + + - + optionslist - - - - -optionslist - - -id -INT -PK + + + + +optionslist + + +id +INT +PK - + article:se->optionslist:n - - - + + + - + brand - - - - -brand - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -gsprpinfo? -NVARCHAR(MAX) + + + + +brand + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +gsprpinfo? +NVARCHAR(MAX) - + article:e->brand:n - - - + + + - + article:sw->unit:n - - - + + + - + articleean - - -articleean - - -id -INT -PK - - -article_id -INT -FK - - -ean -NVARCHAR(50) + + +articleean + + +id +INT +PK + + +article_id +INT +FK + + +ean +NVARCHAR(50) - + articleean:sw->article: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:sw->optionslist:n - - - + + + - + articlehistory - - -articlehistory - - -id -INT -PK - - -article_id -INT -FK - - -companyuser_id -? -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) + + +articlehistory + + +id +INT +PK + + +article_id +INT +FK + + +companyuser_id +? +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) - + articlehistory:e->companyuser:n - - - + + + - + articlehistory:w->article:n - - - + + + - + brand:e->company:n - - - + + + @@ -1131,544 +1225,544 @@ category:e->category:e - - + + - + categoryarticle - - -categoryarticle - - -id -INT -PK - - -article_id -INT -FK - - -category_id -INT -FK + + +categoryarticle + + +id +INT +PK + + +article_id +INT +FK + + +category_id +INT +FK - + categoryarticle:w->article:n - - - + + + - + categoryarticle:sw->category:n - - - + + + - + brandpic - - -brandpic - - -id -INT -PK - - -brand_id -INT -FK - - -pic_id -INT -FK - - -sort -INT + + +brandpic + + +id +INT +PK + + +brand_id +INT +FK + + +pic_id +INT +FK + + +sort +INT - + brandpic:w->brand: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 - - - + + + - + articlepic - - -articlepic - - -id -INT -PK - - -article_id -INT -FK - - -pic_id -INT -FK - - -sort -INT + + +articlepic + + +id +INT +PK + + +article_id +INT +FK + + +pic_id +INT +FK + + +sort +INT - + articlepic:w->article:n - - - + + + - + articlepic:se->pic:n - - - + + + - + categorypic - - -categorypic - - -id -INT -PK - - -category_id -INT -FK - - -pic_id -INT -FK - - -sort -INT + + +categorypic + + +id +INT +PK + + +category_id +INT +FK + + +pic_id +INT +FK + + +sort +INT - + categorypic:sw->category:n - - - + + + - + categorypic:sw->pic:n - - - - - - - - -manifest - - - -manifest - - -id -INT -PK - - -company_id -INT -FK - - -entity_id -? -INT -FK - - -number -NVARCHAR(100) - - -type -NVARCHAR(100) - - -date -DATETIME2 - - - -manifest:sw->company:n - - - - - - - - -manifest:w->entity:n - - - - - - - - -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:w->article:n - - - - - - - - -manifestarticle:sw->tax:n - - - - - - - - -manifestarticle:sw->manifest: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:w->customergroup:n - - - + + + - + price:w->article:n - - - + + + - + price:sw->tax:n - - - + + + + + + + + +manifest + + + +manifest + + +id +INT +PK + + +entity_id +? +INT +FK + + +company_id +INT +FK + + +number +NVARCHAR(100) + + +type +NVARCHAR(100) + + +date +DATETIME2 + + + +manifest:w->entity:n + + + + + + + + +manifest:sw->company:n + + + + + + + + +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:w->article:n + + + + + + + + +manifestarticle:sw->tax:n + + + + + + + + +manifestarticle:sw->manifest:n + + + - + unit:e->company:n - - - + + + - + posterminalcategory - - -posterminalcategory - - -id -INT -PK - - -category_id -INT -FK - - -posterminal_id -INT -FK + + +posterminalcategory + + +id +INT +PK + + +category_id +INT +FK + + +posterminal_id +INT +FK - + posterminalcategory:sw->category:n - - - + + + - + posterminal - - - -posterminal - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -token? -NVARCHAR(255) - - -pairingpin -NVARCHAR(50) + + + +posterminal + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +token? +NVARCHAR(255) + + +pairingpin +NVARCHAR(50) - + posterminalcategory:se->posterminal:n - - - + + + - + posterminal: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 053200b..b231e09 100644 --- a/scripts/test-api.js +++ b/scripts/test-api.js @@ -158,6 +158,43 @@ const MENU = { }, ], }, + customergroup: { + label: 'customergroup', + items: [ + { value: 'customergroup.list', label: 'list', hint: 'googleAuthId, companyId → rows' }, + { value: 'customergroup.create', label: 'create', hint: 'googleAuthId, companyId, name → row' }, + { value: 'customergroup.update', label: 'update', hint: 'googleAuthId, companyId, id, name → row' }, + { value: 'customergroup.delete', label: 'delete', hint: 'googleAuthId, companyId, id' }, + ], + }, + entity: { + label: 'entity', + items: [ + { value: 'entity.list', label: 'list', hint: 'googleAuthId, companyId, customergroupId? → rows' }, + { value: 'entity.getById', label: 'getById', hint: 'googleAuthId, companyId, id → row' }, + { value: 'entity.create', label: 'create', hint: 'googleAuthId, companyId, JSON → row' }, + { value: 'entity.update', label: 'update', hint: 'googleAuthId, companyId, id, JSON → row' }, + { value: 'entity.delete', label: 'delete', hint: 'googleAuthId, companyId, id' }, + ], + }, + entityaddress: { + label: 'entityaddress', + items: [ + { value: 'entityaddress.list', label: 'list', hint: 'googleAuthId, companyId, entityId → rows' }, + { value: 'entityaddress.create', label: 'create', hint: 'googleAuthId, companyId, entityId, JSON → row' }, + { value: 'entityaddress.update', label: 'update', hint: 'googleAuthId, companyId, entityId, id, JSON → row' }, + { value: 'entityaddress.delete', label: 'delete', hint: 'googleAuthId, companyId, entityId, id' }, + ], + }, + entitycontact: { + label: 'entitycontact', + items: [ + { value: 'entitycontact.list', label: 'list', hint: 'googleAuthId, companyId, entityId → rows' }, + { value: 'entitycontact.create', label: 'create', hint: 'googleAuthId, companyId, entityId, JSON → row' }, + { value: 'entitycontact.update', label: 'update', hint: 'googleAuthId, companyId, entityId, id, JSON → row' }, + { value: 'entitycontact.delete', label: 'delete', hint: 'googleAuthId, companyId, entityId, id' }, + ], + }, supplier: { label: 'supplier', items: [ @@ -681,6 +718,362 @@ async function runMethod(client, action) { await client.brand.delete(Number(googleAuthId), Number(companyId), id); return ['ok', true]; } + case 'customergroup.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.customergroup.list(Number(googleAuthId), Number(companyId)), + ]; + } + case 'customergroup.create': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const name = await askText('name', { + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (name == null) return; + return [ + 'result', + await client.customergroup.create(Number(googleAuthId), Number(companyId), { + name, + }), + ]; + } + case 'customergroup.update': { + 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('customergroup id'); + if (id == null) return; + const name = await askText('name', { + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (name == null) return; + return [ + 'result', + await client.customergroup.update( + Number(googleAuthId), + Number(companyId), + Number(id), + { name }, + ), + ]; + } + case 'customergroup.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('customergroup id'); + if (id == null) return; + await client.customergroup.delete( + Number(googleAuthId), + Number(companyId), + Number(id), + ); + return ['ok', true]; + } + case 'entity.list': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const groupRaw = await askText('customergroup id (empty = all)', { + placeholder: '', + validate: (v) => + v?.trim() && !/^\d+$/.test(v.trim()) ? 'Integer or empty' : undefined, + }); + if (groupRaw == null) return; + const customergroupId = groupRaw.trim() + ? Number(groupRaw.trim()) + : undefined; + return [ + 'result[]', + await client.entity.list(Number(googleAuthId), Number(companyId), { + customergroupId, + }), + ]; + } + case 'entity.getById': { + 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('entity id'); + if (id == null) return; + return [ + 'result', + await client.entity.getById( + Number(googleAuthId), + Number(companyId), + Number(id), + ), + ]; + } + case 'entity.create': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const bodyJson = await askText('entity JSON', { + placeholder: '{"customergroupId":1,"name":"Acme","firstName":null}', + validate: (v) => { + if (!v?.trim()) return 'Required'; + try { + JSON.parse(v); + } catch { + return 'Invalid JSON'; + } + }, + }); + if (bodyJson == null) return; + return [ + 'result', + await client.entity.create( + Number(googleAuthId), + Number(companyId), + JSON.parse(bodyJson), + ), + ]; + } + case 'entity.update': { + 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('entity id'); + if (id == null) return; + const patchJson = await askText('patch JSON', { + placeholder: '{"name":"Acme","firstName":"Ada"}', + validate: (v) => { + if (!v?.trim()) return 'Required'; + try { + JSON.parse(v); + } catch { + return 'Invalid JSON'; + } + }, + }); + if (patchJson == null) return; + return [ + 'result', + await client.entity.update( + Number(googleAuthId), + Number(companyId), + Number(id), + JSON.parse(patchJson), + ), + ]; + } + case 'entity.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('entity id'); + if (id == null) return; + await client.entity.delete( + Number(googleAuthId), + Number(companyId), + Number(id), + ); + return ['ok', true]; + } + case 'entityaddress.list': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const entityId = await askInt('entity id'); + if (entityId == null) return; + return [ + 'result[]', + await client.entityaddress.list( + Number(googleAuthId), + Number(companyId), + Number(entityId), + ), + ]; + } + case 'entityaddress.create': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const entityId = await askInt('entity id'); + if (entityId == null) return; + const bodyJson = await askText('address JSON', { + placeholder: + '{"addressType":"billing","street":"Main","streetNumber":"1","zip":"1010","city":"Wien"}', + validate: (v) => { + if (!v?.trim()) return 'Required'; + try { + JSON.parse(v); + } catch { + return 'Invalid JSON'; + } + }, + }); + if (bodyJson == null) return; + return [ + 'result', + await client.entityaddress.create( + Number(googleAuthId), + Number(companyId), + Number(entityId), + JSON.parse(bodyJson), + ), + ]; + } + case 'entityaddress.update': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const entityId = await askInt('entity id'); + if (entityId == null) return; + const id = await askInt('address id'); + if (id == null) return; + const patchJson = await askText('patch JSON', { + placeholder: '{"city":"Graz"}', + validate: (v) => { + if (!v?.trim()) return 'Required'; + try { + JSON.parse(v); + } catch { + return 'Invalid JSON'; + } + }, + }); + if (patchJson == null) return; + return [ + 'result', + await client.entityaddress.update( + Number(googleAuthId), + Number(companyId), + Number(entityId), + Number(id), + JSON.parse(patchJson), + ), + ]; + } + case 'entityaddress.delete': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const entityId = await askInt('entity id'); + if (entityId == null) return; + const id = await askInt('address id'); + if (id == null) return; + await client.entityaddress.delete( + Number(googleAuthId), + Number(companyId), + Number(entityId), + Number(id), + ); + return ['ok', true]; + } + case 'entitycontact.list': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const entityId = await askInt('entity id'); + if (entityId == null) return; + return [ + 'result[]', + await client.entitycontact.list( + Number(googleAuthId), + Number(companyId), + Number(entityId), + ), + ]; + } + case 'entitycontact.create': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const entityId = await askInt('entity id'); + if (entityId == null) return; + const bodyJson = await askText('contact JSON', { + placeholder: '{"contactType":"email","contact":"a@b.c"}', + validate: (v) => { + if (!v?.trim()) return 'Required'; + try { + JSON.parse(v); + } catch { + return 'Invalid JSON'; + } + }, + }); + if (bodyJson == null) return; + return [ + 'result', + await client.entitycontact.create( + Number(googleAuthId), + Number(companyId), + Number(entityId), + JSON.parse(bodyJson), + ), + ]; + } + case 'entitycontact.update': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const entityId = await askInt('entity id'); + if (entityId == null) return; + const id = await askInt('contact id'); + if (id == null) return; + const patchJson = await askText('patch JSON', { + placeholder: '{"contact":"b@c.d"}', + validate: (v) => { + if (!v?.trim()) return 'Required'; + try { + JSON.parse(v); + } catch { + return 'Invalid JSON'; + } + }, + }); + if (patchJson == null) return; + return [ + 'result', + await client.entitycontact.update( + Number(googleAuthId), + Number(companyId), + Number(entityId), + Number(id), + JSON.parse(patchJson), + ), + ]; + } + case 'entitycontact.delete': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const entityId = await askInt('entity id'); + if (entityId == null) return; + const id = await askInt('contact id'); + if (id == null) return; + await client.entitycontact.delete( + Number(googleAuthId), + Number(companyId), + Number(entityId), + Number(id), + ); + return ['ok', true]; + } case 'supplier.list': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; diff --git a/src/index.js b/src/index.js index 17a2a95..3d17d3e 100644 --- a/src/index.js +++ b/src/index.js @@ -39,6 +39,23 @@ 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 customergroupList from './store/customergroup/list.js'; +import customergroupCreate from './store/customergroup/create.js'; +import customergroupUpdate from './store/customergroup/update.js'; +import customergroupDelete from './store/customergroup/delete.js'; +import entityList from './store/entity/list.js'; +import entityGetById from './store/entity/getById.js'; +import entityCreate from './store/entity/create.js'; +import entityUpdate from './store/entity/update.js'; +import entityDelete from './store/entity/delete.js'; +import entityaddressList from './store/entityaddress/list.js'; +import entityaddressCreate from './store/entityaddress/create.js'; +import entityaddressUpdate from './store/entityaddress/update.js'; +import entityaddressDelete from './store/entityaddress/delete.js'; +import entitycontactList from './store/entitycontact/list.js'; +import entitycontactCreate from './store/entitycontact/create.js'; +import entitycontactUpdate from './store/entitycontact/update.js'; +import entitycontactDelete from './store/entitycontact/delete.js'; import supplierList from './store/supplier/list.js'; import supplierCreate from './store/supplier/create.js'; import supplierUpdate from './store/supplier/update.js'; @@ -158,6 +175,35 @@ export default class LibDashStore { delete: brandDelete(pool), }; + this.customergroup = { + list: customergroupList(pool), + create: customergroupCreate(pool), + update: customergroupUpdate(pool), + delete: customergroupDelete(pool), + }; + + this.entity = { + list: entityList(pool), + getById: entityGetById(pool), + create: entityCreate(pool), + update: entityUpdate(pool), + delete: entityDelete(pool), + }; + + this.entityaddress = { + list: entityaddressList(pool), + create: entityaddressCreate(pool), + update: entityaddressUpdate(pool), + delete: entityaddressDelete(pool), + }; + + this.entitycontact = { + list: entitycontactList(pool), + create: entitycontactCreate(pool), + update: entitycontactUpdate(pool), + delete: entitycontactDelete(pool), + }; + this.supplier = { list: supplierList(pool), create: supplierCreate(pool), diff --git a/src/schema.js b/src/schema.js index bbf1e00..3b15d41 100644 --- a/src/schema.js +++ b/src/schema.js @@ -164,6 +164,38 @@ BEGIN END `; +export const INIT_ENTITYADDRESS = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'entityaddress') +BEGIN + CREATE TABLE entityaddress ( + id INT IDENTITY(1,1) PRIMARY KEY, + entity_id INT NOT NULL, + first_name NVARCHAR(255) NULL, + name NVARCHAR(255) NULL, + street NVARCHAR(255) NULL, + streetnumber NVARCHAR(50) NULL, + zip NVARCHAR(50) NULL, + city NVARCHAR(255) NULL, + county NVARCHAR(255) NULL, + addresstype NVARCHAR(50) NOT NULL, + CONSTRAINT FK_entityaddress_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE CASCADE + ); +END +`; + +export const INIT_ENTITYCONTACT = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'entitycontact') +BEGIN + CREATE TABLE entitycontact ( + id INT IDENTITY(1,1) PRIMARY KEY, + entity_id INT NOT NULL, + contacttype NVARCHAR(50) NOT NULL, + contact NVARCHAR(255) NOT NULL, + CONSTRAINT FK_entitycontact_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE CASCADE + ); +END +`; + export const INIT_ENTITYHISTORY = ` IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'entityhistory') BEGIN @@ -636,6 +668,8 @@ export const SCHEMA_SCRIPTS = [ INIT_CUSTOMERGROUP, INIT_COMPANY_POS_CUSTOMERGROUP_FK, INIT_ENTITY, + INIT_ENTITYADDRESS, + INIT_ENTITYCONTACT, INIT_UNIT, INIT_UNITTEXT, INIT_OPTIONSLIST, diff --git a/src/sql/customergroup.js b/src/sql/customergroup.js index e5cf5e0..88fa153 100644 --- a/src/sql/customergroup.js +++ b/src/sql/customergroup.js @@ -1,6 +1,6 @@ export const CREATE = ` INSERT INTO customergroup (name, company_id) -OUTPUT INSERTED.id +OUTPUT INSERTED.id, INSERTED.name VALUES (@name, @companyId) `; @@ -17,7 +17,32 @@ WHERE id = @id `; export const GET_BY_COMPANY_AND_NAME = ` -SELECT id +SELECT id, name FROM customergroup WHERE company_id = @companyId AND name = @name `; + +export const LIST_BY_COMPANY = ` +SELECT id, name +FROM customergroup +WHERE company_id = @companyId +ORDER BY name, id +`; + +export const GET_BY_COMPANY_AND_ID = ` +SELECT id, name +FROM customergroup +WHERE id = @id AND company_id = @companyId +`; + +export const UPDATE_NAME = ` +UPDATE customergroup +SET name = @name +OUTPUT INSERTED.id, INSERTED.name +WHERE id = @id AND company_id = @companyId +`; + +export const DELETE_BY_COMPANY_AND_ID = ` +DELETE FROM customergroup +WHERE id = @id AND company_id = @companyId +`; diff --git a/src/sql/entity.js b/src/sql/entity.js index 9fc2b2a..fe0053c 100644 --- a/src/sql/entity.js +++ b/src/sql/entity.js @@ -1,5 +1,42 @@ export const SUPPLIER_GROUP_NAME = 'Lieferanten'; +export const ENTITY_COLUMNS = ` + e.id, + e.customergroup_id AS customergroupId, + e.first_name AS firstName, + e.name, + e.created_at AS createdAt, + e.updated_at AS updatedAt +`; + +export const LIST_BY_COMPANY = ` +SELECT +${ENTITY_COLUMNS} +FROM entity e +INNER JOIN customergroup cg ON cg.id = e.customergroup_id +WHERE cg.company_id = @companyId +ORDER BY e.name, e.id +`; + +export const LIST_BY_COMPANY_AND_GROUP = ` +SELECT +${ENTITY_COLUMNS} +FROM entity e +INNER JOIN customergroup cg ON cg.id = e.customergroup_id +WHERE cg.company_id = @companyId + AND e.customergroup_id = @customergroupId +ORDER BY e.name, e.id +`; + +export const GET_BY_COMPANY_AND_ID = ` +SELECT +${ENTITY_COLUMNS} +FROM entity e +INNER JOIN customergroup cg ON cg.id = e.customergroup_id +WHERE e.id = @id + AND cg.company_id = @companyId +`; + export const LIST_SUPPLIERS_BY_COMPANY = ` SELECT e.id, @@ -27,9 +64,31 @@ WHERE e.id = @id `; export const CREATE = ` -INSERT INTO entity (customergroup_id, name) -OUTPUT INSERTED.id, INSERTED.name, INSERTED.created_at AS createdAt, INSERTED.updated_at AS updatedAt -VALUES (@customergroupId, @name) +INSERT INTO entity (customergroup_id, first_name, name) +OUTPUT + INSERTED.id, + INSERTED.customergroup_id AS customergroupId, + INSERTED.first_name AS firstName, + INSERTED.name, + INSERTED.created_at AS createdAt, + INSERTED.updated_at AS updatedAt +VALUES (@customergroupId, @firstName, @name) +`; + +export const UPDATE = ` +UPDATE entity +SET + first_name = @firstName, + name = @name, + updated_at = SYSUTCDATETIME() +OUTPUT + INSERTED.id, + INSERTED.customergroup_id AS customergroupId, + INSERTED.first_name AS firstName, + INSERTED.name, + INSERTED.created_at AS createdAt, + INSERTED.updated_at AS updatedAt +WHERE id = @id `; export const UPDATE_NAME = ` @@ -40,6 +99,14 @@ WHERE id = @id AND customergroup_id = @customergroupId `; +export const DELETE_BY_COMPANY_AND_ID = ` +DELETE e +FROM entity e +INNER JOIN customergroup cg ON cg.id = e.customergroup_id +WHERE e.id = @id + AND cg.company_id = @companyId +`; + export const DELETE_BY_ID = ` DELETE e FROM entity e diff --git a/src/sql/entityaddress.js b/src/sql/entityaddress.js new file mode 100644 index 0000000..47eebf7 --- /dev/null +++ b/src/sql/entityaddress.js @@ -0,0 +1,93 @@ +export const ADDRESS_COLUMNS = ` + id, + entity_id AS entityId, + first_name AS firstName, + name, + street, + streetnumber AS streetNumber, + zip, + city, + county, + addresstype AS addressType +`; + +export const LIST_BY_ENTITY = ` +SELECT +${ADDRESS_COLUMNS} +FROM entityaddress +WHERE entity_id = @entityId +ORDER BY id +`; + +export const GET_BY_ENTITY_AND_ID = ` +SELECT +${ADDRESS_COLUMNS} +FROM entityaddress +WHERE id = @id AND entity_id = @entityId +`; + +export const CREATE = ` +INSERT INTO entityaddress ( + entity_id, + first_name, + name, + street, + streetnumber, + zip, + city, + county, + addresstype +) +OUTPUT + INSERTED.id, + INSERTED.entity_id AS entityId, + INSERTED.first_name AS firstName, + INSERTED.name, + INSERTED.street, + INSERTED.streetnumber AS streetNumber, + INSERTED.zip, + INSERTED.city, + INSERTED.county, + INSERTED.addresstype AS addressType +VALUES ( + @entityId, + @firstName, + @name, + @street, + @streetNumber, + @zip, + @city, + @county, + @addressType +) +`; + +export const UPDATE = ` +UPDATE entityaddress +SET + first_name = @firstName, + name = @name, + street = @street, + streetnumber = @streetNumber, + zip = @zip, + city = @city, + county = @county, + addresstype = @addressType +OUTPUT + INSERTED.id, + INSERTED.entity_id AS entityId, + INSERTED.first_name AS firstName, + INSERTED.name, + INSERTED.street, + INSERTED.streetnumber AS streetNumber, + INSERTED.zip, + INSERTED.city, + INSERTED.county, + INSERTED.addresstype AS addressType +WHERE id = @id AND entity_id = @entityId +`; + +export const DELETE_BY_ENTITY_AND_ID = ` +DELETE FROM entityaddress +WHERE id = @id AND entity_id = @entityId +`; diff --git a/src/sql/entitycontact.js b/src/sql/entitycontact.js new file mode 100644 index 0000000..2e7ef54 --- /dev/null +++ b/src/sql/entitycontact.js @@ -0,0 +1,47 @@ +export const CONTACT_COLUMNS = ` + id, + entity_id AS entityId, + contacttype AS contactType, + contact +`; + +export const LIST_BY_ENTITY = ` +SELECT +${CONTACT_COLUMNS} +FROM entitycontact +WHERE entity_id = @entityId +ORDER BY id +`; + +export const GET_BY_ENTITY_AND_ID = ` +SELECT +${CONTACT_COLUMNS} +FROM entitycontact +WHERE id = @id AND entity_id = @entityId +`; + +export const CREATE = ` +INSERT INTO entitycontact (entity_id, contacttype, contact) +OUTPUT + INSERTED.id, + INSERTED.entity_id AS entityId, + INSERTED.contacttype AS contactType, + INSERTED.contact +VALUES (@entityId, @contactType, @contact) +`; + +export const UPDATE = ` +UPDATE entitycontact +SET contacttype = @contactType, contact = @contact +OUTPUT + INSERTED.id, + INSERTED.entity_id AS entityId, + INSERTED.contacttype AS contactType, + INSERTED.contact +WHERE id = @id AND entity_id = @entityId +`; + +export const DELETE_BY_ENTITY_AND_ID = ` +DELETE FROM entitycontact +WHERE id = @id AND entity_id = @entityId +`; diff --git a/src/sql/purge.js b/src/sql/purge.js index 38f2159..e83e53d 100644 --- a/src/sql/purge.js +++ b/src/sql/purge.js @@ -20,6 +20,8 @@ export const TABLES = [ 'articleean', 'articlehistory', 'entityhistory', + 'entityaddress', + 'entitycontact', 'article', 'brand', 'articlelist', diff --git a/src/store/customergroup/create.js b/src/store/customergroup/create.js new file mode 100644 index 0000000..63e5ee2 --- /dev/null +++ b/src/store/customergroup/create.js @@ -0,0 +1,24 @@ +import { CREATE } from '../../sql/customergroup.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +export default function create(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, { name } = {}) => { + const cleaned = name?.trim(); + if (!cleaned) { + throw new Error('Name required'); + } + + await access(googleAuthId, companyId); + + const result = await pool + .request() + .input('companyId', companyId) + .input('name', cleaned) + .query(CREATE); + + const row = result.recordset[0]; + return { id: row.id, name: row.name }; + }; +} diff --git a/src/store/customergroup/delete.js b/src/store/customergroup/delete.js new file mode 100644 index 0000000..782d11a --- /dev/null +++ b/src/store/customergroup/delete.js @@ -0,0 +1,31 @@ +import { DELETE_BY_COMPANY_AND_ID } from '../../sql/customergroup.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('Customer group 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('Customer group not found'); + } + } catch (err) { + if (err.number === 547) { + throw new Error('Customer group in use'); + } + throw err; + } + }; +} diff --git a/src/store/customergroup/list.js b/src/store/customergroup/list.js new file mode 100644 index 0000000..c3b67a4 --- /dev/null +++ b/src/store/customergroup/list.js @@ -0,0 +1,20 @@ +import { LIST_BY_COMPANY } from '../../sql/customergroup.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.map((row) => ({ + id: row.id, + name: row.name, + })); + }; +} diff --git a/src/store/customergroup/resolveCustomerGroupAccess.js b/src/store/customergroup/resolveCustomerGroupAccess.js new file mode 100644 index 0000000..5caf6dd --- /dev/null +++ b/src/store/customergroup/resolveCustomerGroupAccess.js @@ -0,0 +1,22 @@ +import { GET_BY_COMPANY_AND_ID } from '../../sql/customergroup.js'; +import request from '../request.js'; + +/** Ensure customergroup belongs to company. Throws Customer group not found. */ +export default function resolveCustomerGroupAccess(pool) { + return async (companyId, customergroupId, tx) => { + if (!Number.isFinite(customergroupId)) { + throw new Error('Customer group not found'); + } + + const result = await request(pool, tx) + .input('id', customergroupId) + .input('companyId', companyId) + .query(GET_BY_COMPANY_AND_ID); + + const row = result.recordset[0]; + if (!row) { + throw new Error('Customer group not found'); + } + return row; + }; +} diff --git a/src/store/customergroup/update.js b/src/store/customergroup/update.js new file mode 100644 index 0000000..f88fe7e --- /dev/null +++ b/src/store/customergroup/update.js @@ -0,0 +1,32 @@ +import { UPDATE_NAME } from '../../sql/customergroup.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +export default function update(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, id, { name } = {}) => { + if (!Number.isFinite(id)) { + throw new Error('Customer group not found'); + } + + const cleaned = name?.trim(); + if (!cleaned) { + throw new Error('Name required'); + } + + await access(googleAuthId, companyId); + + const result = await pool + .request() + .input('id', id) + .input('companyId', companyId) + .input('name', cleaned) + .query(UPDATE_NAME); + + const row = result.recordset[0]; + if (!row) { + throw new Error('Customer group not found'); + } + return { id: row.id, name: row.name }; + }; +} diff --git a/src/store/entity/create.js b/src/store/entity/create.js new file mode 100644 index 0000000..2b57ca0 --- /dev/null +++ b/src/store/entity/create.js @@ -0,0 +1,69 @@ +import { CREATE } from '../../sql/entity.js'; +import { INSERT as INSERT_HISTORY } from '../../sql/entityhistory.js'; +import request from '../request.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveCustomerGroupAccess from '../customergroup/resolveCustomerGroupAccess.js'; + +function shape(row) { + return { + id: row.id, + customergroupId: row.customergroupId, + firstName: row.firstName ?? null, + name: row.name, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} + +export default function create(pool) { + const access = resolveCompanyAccess(pool); + const groupAccess = resolveCustomerGroupAccess(pool); + + return async ( + googleAuthId, + companyId, + { customergroupId, name, firstName = null } = {}, + ) => { + const cleaned = name?.trim(); + if (!cleaned) { + throw new Error('Name required'); + } + + const first = + firstName == null || firstName === '' + ? null + : String(firstName).trim() || null; + + const { companyuserId } = await access(googleAuthId, companyId); + await groupAccess(companyId, customergroupId); + + const tx = pool.transaction(); + await tx.begin(); + + try { + const created = await request(pool, tx) + .input('customergroupId', customergroupId) + .input('firstName', first) + .input('name', cleaned) + .query(CREATE); + + const row = created.recordset[0]; + + await request(pool, tx) + .input('entityId', row.id) + .input('companyuserId', companyuserId) + .input('text', 'Erstellt') + .query(INSERT_HISTORY); + + await tx.commit(); + return shape(row); + } catch (err) { + try { + await tx.rollback(); + } catch { + // ignore + } + throw err; + } + }; +} diff --git a/src/store/entity/delete.js b/src/store/entity/delete.js new file mode 100644 index 0000000..025f6d6 --- /dev/null +++ b/src/store/entity/delete.js @@ -0,0 +1,24 @@ +import { DELETE_BY_COMPANY_AND_ID } from '../../sql/entity.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('Entity not found'); + } + + await access(googleAuthId, companyId); + + const result = await pool + .request() + .input('id', id) + .input('companyId', companyId) + .query(DELETE_BY_COMPANY_AND_ID); + + if (!result.rowsAffected[0]) { + throw new Error('Entity not found'); + } + }; +} diff --git a/src/store/entity/getById.js b/src/store/entity/getById.js new file mode 100644 index 0000000..c75a864 --- /dev/null +++ b/src/store/entity/getById.js @@ -0,0 +1,37 @@ +import { GET_BY_COMPANY_AND_ID } from '../../sql/entity.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +function shape(row) { + return { + id: row.id, + customergroupId: row.customergroupId, + firstName: row.firstName ?? null, + name: row.name, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} + +export default function getById(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, id) => { + if (!Number.isFinite(id)) { + throw new Error('Entity not found'); + } + + await access(googleAuthId, companyId); + + const result = await pool + .request() + .input('id', id) + .input('companyId', companyId) + .query(GET_BY_COMPANY_AND_ID); + + const row = result.recordset[0]; + if (!row) { + throw new Error('Entity not found'); + } + return shape(row); + }; +} diff --git a/src/store/entity/list.js b/src/store/entity/list.js new file mode 100644 index 0000000..2aa3703 --- /dev/null +++ b/src/store/entity/list.js @@ -0,0 +1,43 @@ +import { + LIST_BY_COMPANY, + LIST_BY_COMPANY_AND_GROUP, +} from '../../sql/entity.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveCustomerGroupAccess from '../customergroup/resolveCustomerGroupAccess.js'; + +function shape(row) { + return { + id: row.id, + customergroupId: row.customergroupId, + firstName: row.firstName ?? null, + name: row.name, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} + +export default function list(pool) { + const access = resolveCompanyAccess(pool); + const groupAccess = resolveCustomerGroupAccess(pool); + + return async (googleAuthId, companyId, { customergroupId } = {}) => { + await access(googleAuthId, companyId); + + if (customergroupId != null) { + await groupAccess(companyId, customergroupId); + const result = await pool + .request() + .input('companyId', companyId) + .input('customergroupId', customergroupId) + .query(LIST_BY_COMPANY_AND_GROUP); + return result.recordset.map(shape); + } + + const result = await pool + .request() + .input('companyId', companyId) + .query(LIST_BY_COMPANY); + + return result.recordset.map(shape); + }; +} diff --git a/src/store/entity/resolveEntityAccess.js b/src/store/entity/resolveEntityAccess.js new file mode 100644 index 0000000..458e443 --- /dev/null +++ b/src/store/entity/resolveEntityAccess.js @@ -0,0 +1,22 @@ +import { GET_BY_COMPANY_AND_ID } from '../../sql/entity.js'; +import request from '../request.js'; + +/** Ensure entity belongs to company. Throws Entity not found. */ +export default function resolveEntityAccess(pool) { + return async (companyId, entityId, tx) => { + if (!Number.isFinite(entityId)) { + throw new Error('Entity not found'); + } + + const result = await request(pool, tx) + .input('id', entityId) + .input('companyId', companyId) + .query(GET_BY_COMPANY_AND_ID); + + const row = result.recordset[0]; + if (!row) { + throw new Error('Entity not found'); + } + return row; + }; +} diff --git a/src/store/entity/update.js b/src/store/entity/update.js new file mode 100644 index 0000000..8b9b893 --- /dev/null +++ b/src/store/entity/update.js @@ -0,0 +1,110 @@ +import { GET_BY_COMPANY_AND_ID, UPDATE } from '../../sql/entity.js'; +import { INSERT as INSERT_HISTORY } from '../../sql/entityhistory.js'; +import request from '../request.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +function shape(row) { + return { + id: row.id, + customergroupId: row.customergroupId, + firstName: row.firstName ?? null, + name: row.name, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} + +export default function update(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, id, patch = {}) => { + if (!Number.isFinite(id)) { + throw new Error('Entity not found'); + } + + const hasName = Object.prototype.hasOwnProperty.call(patch, 'name'); + const hasFirstName = Object.prototype.hasOwnProperty.call(patch, 'firstName'); + if (!hasName && !hasFirstName) { + throw new Error('Name required'); + } + + const { companyuserId } = await access(googleAuthId, companyId); + + const existing = await pool + .request() + .input('id', id) + .input('companyId', companyId) + .query(GET_BY_COMPANY_AND_ID); + + const current = existing.recordset[0]; + if (!current) { + throw new Error('Entity not found'); + } + + let name = current.name; + if (hasName) { + name = patch.name?.trim(); + if (!name) { + throw new Error('Name required'); + } + } + + let firstName = current.firstName ?? null; + if (hasFirstName) { + firstName = + patch.firstName == null || patch.firstName === '' + ? null + : String(patch.firstName).trim() || null; + } + + const historyLines = []; + const oldFirst = current.firstName ?? ''; + const newFirst = firstName ?? ''; + if (oldFirst !== newFirst) { + historyLines.push( + `Vorname geändert von "${oldFirst}" auf "${newFirst}"`, + ); + } + if (current.name !== name) { + historyLines.push( + `Name geändert von "${current.name}" auf "${name}"`, + ); + } + + if (!historyLines.length) { + return shape(current); + } + + const tx = pool.transaction(); + await tx.begin(); + + try { + const updated = await request(pool, tx) + .input('id', id) + .input('firstName', firstName) + .input('name', name) + .query(UPDATE); + + const row = updated.recordset[0]; + if (!row) { + throw new Error('Entity not found'); + } + + await request(pool, tx) + .input('entityId', id) + .input('companyuserId', companyuserId) + .input('text', historyLines.join('\n')) + .query(INSERT_HISTORY); + + await tx.commit(); + return shape(row); + } catch (err) { + try { + await tx.rollback(); + } catch { + // ignore + } + throw err; + } + }; +} diff --git a/src/store/entityaddress/create.js b/src/store/entityaddress/create.js new file mode 100644 index 0000000..d8bf78d --- /dev/null +++ b/src/store/entityaddress/create.js @@ -0,0 +1,56 @@ +import { CREATE } from '../../sql/entityaddress.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveEntityAccess from '../entity/resolveEntityAccess.js'; + +function shape(row) { + return { + id: row.id, + entityId: row.entityId, + firstName: row.firstName ?? null, + name: row.name ?? null, + street: row.street ?? null, + streetNumber: row.streetNumber ?? null, + zip: row.zip ?? null, + city: row.city ?? null, + county: row.county ?? null, + addressType: row.addressType, + }; +} + +function optionalString(value) { + if (value == null || value === '') return null; + const s = String(value).trim(); + return s || null; +} + +export default function create(pool) { + const access = resolveCompanyAccess(pool); + const entityAccess = resolveEntityAccess(pool); + + return async (googleAuthId, companyId, entityId, input = {}) => { + const addressType = input.addressType?.trim?.() + ? input.addressType.trim() + : null; + if (!addressType) { + throw new Error('Address type required'); + } + + await access(googleAuthId, companyId); + await entityAccess(companyId, entityId); + + const result = await pool + .request() + .input('entityId', entityId) + .input('firstName', optionalString(input.firstName)) + .input('name', optionalString(input.name)) + .input('street', optionalString(input.street)) + .input('streetNumber', optionalString(input.streetNumber)) + .input('zip', optionalString(input.zip)) + .input('city', optionalString(input.city)) + .input('county', optionalString(input.county)) + .input('addressType', addressType) + .query(CREATE); + + return shape(result.recordset[0]); + }; +} diff --git a/src/store/entityaddress/delete.js b/src/store/entityaddress/delete.js new file mode 100644 index 0000000..34038eb --- /dev/null +++ b/src/store/entityaddress/delete.js @@ -0,0 +1,27 @@ +import { DELETE_BY_ENTITY_AND_ID } from '../../sql/entityaddress.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveEntityAccess from '../entity/resolveEntityAccess.js'; + +export default function del(pool) { + const access = resolveCompanyAccess(pool); + const entityAccess = resolveEntityAccess(pool); + + return async (googleAuthId, companyId, entityId, id) => { + if (!Number.isFinite(id)) { + throw new Error('Address not found'); + } + + await access(googleAuthId, companyId); + await entityAccess(companyId, entityId); + + const result = await pool + .request() + .input('id', id) + .input('entityId', entityId) + .query(DELETE_BY_ENTITY_AND_ID); + + if (!result.rowsAffected[0]) { + throw new Error('Address not found'); + } + }; +} diff --git a/src/store/entityaddress/list.js b/src/store/entityaddress/list.js new file mode 100644 index 0000000..4723a40 --- /dev/null +++ b/src/store/entityaddress/list.js @@ -0,0 +1,35 @@ +import { LIST_BY_ENTITY } from '../../sql/entityaddress.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveEntityAccess from '../entity/resolveEntityAccess.js'; + +function shape(row) { + return { + id: row.id, + entityId: row.entityId, + firstName: row.firstName ?? null, + name: row.name ?? null, + street: row.street ?? null, + streetNumber: row.streetNumber ?? null, + zip: row.zip ?? null, + city: row.city ?? null, + county: row.county ?? null, + addressType: row.addressType, + }; +} + +export default function list(pool) { + const access = resolveCompanyAccess(pool); + const entityAccess = resolveEntityAccess(pool); + + return async (googleAuthId, companyId, entityId) => { + await access(googleAuthId, companyId); + await entityAccess(companyId, entityId); + + const result = await pool + .request() + .input('entityId', entityId) + .query(LIST_BY_ENTITY); + + return result.recordset.map(shape); + }; +} diff --git a/src/store/entityaddress/update.js b/src/store/entityaddress/update.js new file mode 100644 index 0000000..abb9d06 --- /dev/null +++ b/src/store/entityaddress/update.js @@ -0,0 +1,104 @@ +import { GET_BY_ENTITY_AND_ID, UPDATE } from '../../sql/entityaddress.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveEntityAccess from '../entity/resolveEntityAccess.js'; + +function shape(row) { + return { + id: row.id, + entityId: row.entityId, + firstName: row.firstName ?? null, + name: row.name ?? null, + street: row.street ?? null, + streetNumber: row.streetNumber ?? null, + zip: row.zip ?? null, + city: row.city ?? null, + county: row.county ?? null, + addressType: row.addressType, + }; +} + +function optionalString(value) { + if (value == null || value === '') return null; + const s = String(value).trim(); + return s || null; +} + +export default function update(pool) { + const access = resolveCompanyAccess(pool); + const entityAccess = resolveEntityAccess(pool); + + return async (googleAuthId, companyId, entityId, id, patch = {}) => { + if (!Number.isFinite(id)) { + throw new Error('Address not found'); + } + + await access(googleAuthId, companyId); + await entityAccess(companyId, entityId); + + const existing = await pool + .request() + .input('id', id) + .input('entityId', entityId) + .query(GET_BY_ENTITY_AND_ID); + + const current = existing.recordset[0]; + if (!current) { + throw new Error('Address not found'); + } + + const firstName = Object.prototype.hasOwnProperty.call(patch, 'firstName') + ? optionalString(patch.firstName) + : current.firstName ?? null; + const name = Object.prototype.hasOwnProperty.call(patch, 'name') + ? optionalString(patch.name) + : current.name ?? null; + const street = Object.prototype.hasOwnProperty.call(patch, 'street') + ? optionalString(patch.street) + : current.street ?? null; + const streetNumber = Object.prototype.hasOwnProperty.call( + patch, + 'streetNumber', + ) + ? optionalString(patch.streetNumber) + : current.streetNumber ?? null; + const zip = Object.prototype.hasOwnProperty.call(patch, 'zip') + ? optionalString(patch.zip) + : current.zip ?? null; + const city = Object.prototype.hasOwnProperty.call(patch, 'city') + ? optionalString(patch.city) + : current.city ?? null; + const county = Object.prototype.hasOwnProperty.call(patch, 'county') + ? optionalString(patch.county) + : current.county ?? null; + + let addressType = current.addressType; + if (Object.prototype.hasOwnProperty.call(patch, 'addressType')) { + addressType = patch.addressType?.trim?.() + ? patch.addressType.trim() + : null; + if (!addressType) { + throw new Error('Address type required'); + } + } + + const result = await pool + .request() + .input('id', id) + .input('entityId', entityId) + .input('firstName', firstName) + .input('name', name) + .input('street', street) + .input('streetNumber', streetNumber) + .input('zip', zip) + .input('city', city) + .input('county', county) + .input('addressType', addressType) + .query(UPDATE); + + const row = result.recordset[0]; + if (!row) { + throw new Error('Address not found'); + } + return shape(row); + }; +} diff --git a/src/store/entitycontact/create.js b/src/store/entitycontact/create.js new file mode 100644 index 0000000..e7221b5 --- /dev/null +++ b/src/store/entitycontact/create.js @@ -0,0 +1,39 @@ +import { CREATE } from '../../sql/entitycontact.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveEntityAccess from '../entity/resolveEntityAccess.js'; + +function shape(row) { + return { + id: row.id, + entityId: row.entityId, + contactType: row.contactType, + contact: row.contact, + }; +} + +export default function create(pool) { + const access = resolveCompanyAccess(pool); + const entityAccess = resolveEntityAccess(pool); + + return async (googleAuthId, companyId, entityId, input = {}) => { + const contactType = input.contactType?.trim?.() + ? input.contactType.trim() + : null; + const contact = input.contact?.trim?.() ? input.contact.trim() : null; + if (!contactType || !contact) { + throw new Error('Contact required'); + } + + await access(googleAuthId, companyId); + await entityAccess(companyId, entityId); + + const result = await pool + .request() + .input('entityId', entityId) + .input('contactType', contactType) + .input('contact', contact) + .query(CREATE); + + return shape(result.recordset[0]); + }; +} diff --git a/src/store/entitycontact/delete.js b/src/store/entitycontact/delete.js new file mode 100644 index 0000000..62def26 --- /dev/null +++ b/src/store/entitycontact/delete.js @@ -0,0 +1,27 @@ +import { DELETE_BY_ENTITY_AND_ID } from '../../sql/entitycontact.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveEntityAccess from '../entity/resolveEntityAccess.js'; + +export default function del(pool) { + const access = resolveCompanyAccess(pool); + const entityAccess = resolveEntityAccess(pool); + + return async (googleAuthId, companyId, entityId, id) => { + if (!Number.isFinite(id)) { + throw new Error('Contact not found'); + } + + await access(googleAuthId, companyId); + await entityAccess(companyId, entityId); + + const result = await pool + .request() + .input('id', id) + .input('entityId', entityId) + .query(DELETE_BY_ENTITY_AND_ID); + + if (!result.rowsAffected[0]) { + throw new Error('Contact not found'); + } + }; +} diff --git a/src/store/entitycontact/list.js b/src/store/entitycontact/list.js new file mode 100644 index 0000000..ca919aa --- /dev/null +++ b/src/store/entitycontact/list.js @@ -0,0 +1,29 @@ +import { LIST_BY_ENTITY } from '../../sql/entitycontact.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveEntityAccess from '../entity/resolveEntityAccess.js'; + +function shape(row) { + return { + id: row.id, + entityId: row.entityId, + contactType: row.contactType, + contact: row.contact, + }; +} + +export default function list(pool) { + const access = resolveCompanyAccess(pool); + const entityAccess = resolveEntityAccess(pool); + + return async (googleAuthId, companyId, entityId) => { + await access(googleAuthId, companyId); + await entityAccess(companyId, entityId); + + const result = await pool + .request() + .input('entityId', entityId) + .query(LIST_BY_ENTITY); + + return result.recordset.map(shape); + }; +} diff --git a/src/store/entitycontact/update.js b/src/store/entitycontact/update.js new file mode 100644 index 0000000..7f47474 --- /dev/null +++ b/src/store/entitycontact/update.js @@ -0,0 +1,67 @@ +import { GET_BY_ENTITY_AND_ID, UPDATE } from '../../sql/entitycontact.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveEntityAccess from '../entity/resolveEntityAccess.js'; + +function shape(row) { + return { + id: row.id, + entityId: row.entityId, + contactType: row.contactType, + contact: row.contact, + }; +} + +export default function update(pool) { + const access = resolveCompanyAccess(pool); + const entityAccess = resolveEntityAccess(pool); + + return async (googleAuthId, companyId, entityId, id, patch = {}) => { + if (!Number.isFinite(id)) { + throw new Error('Contact not found'); + } + + await access(googleAuthId, companyId); + await entityAccess(companyId, entityId); + + const existing = await pool + .request() + .input('id', id) + .input('entityId', entityId) + .query(GET_BY_ENTITY_AND_ID); + + const current = existing.recordset[0]; + if (!current) { + throw new Error('Contact not found'); + } + + let contactType = current.contactType; + if (Object.prototype.hasOwnProperty.call(patch, 'contactType')) { + contactType = patch.contactType?.trim?.() + ? patch.contactType.trim() + : null; + } + + let contact = current.contact; + if (Object.prototype.hasOwnProperty.call(patch, 'contact')) { + contact = patch.contact?.trim?.() ? patch.contact.trim() : null; + } + + if (!contactType || !contact) { + throw new Error('Contact required'); + } + + const result = await pool + .request() + .input('id', id) + .input('entityId', entityId) + .input('contactType', contactType) + .input('contact', contact) + .query(UPDATE); + + const row = result.recordset[0]; + if (!row) { + throw new Error('Contact not found'); + } + return shape(row); + }; +} diff --git a/src/store/supplier/create.js b/src/store/supplier/create.js index 4787bef..959f2bf 100644 --- a/src/store/supplier/create.js +++ b/src/store/supplier/create.js @@ -23,6 +23,7 @@ export default function create(pool) { const created = await request(pool, tx) .input('customergroupId', customergroupId) + .input('firstName', null) .input('name', cleaned) .query(CREATE);