diff --git a/API.manifestattachment.md b/API.manifestattachment.md new file mode 100644 index 0000000..4dfe7ac --- /dev/null +++ b/API.manifestattachment.md @@ -0,0 +1,124 @@ +# manifestattachment + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `manifestattachment.add` + +*Source: `src/store/manifestattachment/add.js`* + +Attaches a file to an existing manifest. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `manifestId` | `number` | The id of the manifest. | +| `input` | `object` | The file data. | +| `input.mimetype` | `string` | The MIME type of the file (e.g. `'application/pdf'`). | +| `input.name` | `string` | The file name. | +| `input.content` | `Buffer` | The file content. | + +**Returns** + +`Promise` — The created attachment record. + +**Throws** + +`Error` — If the manifest is not found or file input is invalid. + +**Example** + +```js +const attachment = await store.manifestattachment.add(42, 1, { + mimetype: 'application/pdf', + name: 'invoice.pdf', + content: fileBuffer, +}); +// => { id, manifestId, fileId, mimetype, name } +``` + +## `manifestattachment.getById` + +*Source: `src/store/manifestattachment/getById.js`* + +Gets an attachment for a manifest by its attachment ID or file ID, including full file content. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `manifestId` | `number` | The manifest ID. | +| `id` | `number` | The attachment ID or file ID. | + +**Returns** + +`Promise` — The attachment record with binary content. + +**Throws** + +`Error` — If the manifest or attachment is not found, or user is not authorized. + +**Example** + +```js +const attachment = await store.manifestattachment.getById(42, 1, 5); +// => { id, manifestId, fileId, mimetype, name, content } +``` + +## `manifestattachment.listByManifestId` + +*Source: `src/store/manifestattachment/listByManifestId.js`* + +Lists all attachments for a given manifest, without loading their full binary content. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `manifestId` | `number` | The manifest ID. | + +**Returns** + +`Promise>` — A list of attachment metadata objects. + +**Throws** + +`Error` — If the manifest is not found or the user is not authorized. + +**Example** + +```js +const attachments = await store.manifestattachment.listByManifestId(42, 1); +// => [{ id, manifestId, fileId, mimetype, name, size }] +``` + +## `manifestattachment.remove` + +*Source: `src/store/manifestattachment/remove.js`* + +Removes a file attachment from a manifest and deletes the underlying file. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `manifestId` | `number` | The manifest ID. | +| `id` | `number` | The attachment ID or file ID to remove. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If the manifest or attachment is not found, or user is not authorized. + +**Example** + +```js +await store.manifestattachment.remove(42, 1, 5); +``` diff --git a/API.md b/API.md index 88ee324..31ee2f0 100644 --- a/API.md +++ b/API.md @@ -7,23 +7,139 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met ## Table of Contents - [companyuser](API.companyuser.md) + - `add(companyId: number, googleAuthId: number): Promise` + - `delete(id: number): Promise` + - `getByEmail(email: string): Promise>` - [company](API.company.md) + - `create(input: object): Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number, posCustomerGroupId: number}>` + - `delete(companyId: number): Promise` + - `getById(id: number, tx?: object): Promise` + - `getByName(name: string, tx?: object): Promise` + - `list(googleAuthId: number): Promise>` + - `update(id: number, input: object): Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number}>` - [category](API.category.md) + - `addImage(googleAuthId: number, categoryId: number, image: Buffer): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>` + - `createUnderParent(input: object): Promise<{id: number, parentId: number, sort: number, name: string, slug: string}>` + - `delete(input: object): Promise` + - `deleteImage(googleAuthId: number, categoryId: number): Promise<{id: number, thumbW: null, thumbH: null, thumbHash: null}>` + - `ensureByPath(input: object, tx?: object): Promise` + - `getCategoryArticles(googleAuthId: number, categoryId: number): Promise>` + - `getTreeForCompany(googleAuthId: number): Promise<{languageId: number, tree: Array, articles: Array}>` + - `move(input: object): Promise<{id: number, parentId: number, sort: number}>` + - `order(input: object): Promise<{categoryIds: Array}>` + - `rename(input: object): Promise<{id: number, name: string, slug: string}>` - [article](API.article.md) + - `addImage(googleAuthId: number, sku: string, image: Buffer): Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>` + - `createUnderParent(input: object): Promise<{id: number, parentId: number, name: string, sku: string}>` + - `delete(googleAuthId: number, sku: string): Promise` + - `deleteImage(googleAuthId: number, sku: string, fullHash: string): Promise` + - `getBySku(googleAuthId: number, sku: string): Promise<{article: object}>` + - `getHistoryBySku(googleAuthId: number, sku: string): Promise>` + - `importRows(input: object): Promise<{imported: Array, skipped: Array, errors: Array}>` + - `listExport(googleAuthId: number, categoryId?: number|null): Promise>` + - `listSearchIndex(googleAuthId: number): Promise, categoryIds: Array}>>` + - `orderImages(googleAuthId: number, sku: string, input: object): Promise<{fullHashes: Array}>` + - `setBrandIdBySku(googleAuthId: number, sku: string, brandId: number|null): Promise` + - `setCategoryIdsBySku(googleAuthId: number, sku: string, categoryIds: Array): Promise` + - `setEansBySku(googleAuthId: number, sku: string, eans: Array): Promise` + - `setFatherBySku(googleAuthId: number, sku: string, fatherSku: string|null): Promise` + - `setPricesBySku(googleAuthId: number, sku: string, prices: Array<{price: number, tax: number, minmultiplier?: number}>): Promise` + - `setWeightBySku(googleAuthId: number, sku: string, weight: number): Promise` + - `updateBySku(googleAuthId: number, sku: string, patch: object): Promise` - [lang](API.lang.md) + - `create(iso639: string, tx?: object): Promise` + - `delete(id: number): Promise` + - `ensure(iso639: string, tx?: object): Promise` + - `getByIso639(iso639: string, tx?: object): Promise<{id: number, iso639: string}>` + - `list(): Promise>` - [tax](API.tax.md) + - `create(rate: number, tx?: object): Promise` + - `ensure(rate: number, tx?: object): Promise` + - `getByRate(rate: number, tx?: object): Promise<{id: number, rate: number}>` + - `list(): Promise>` - [unit](API.unit.md) + - `delete(googleAuthId: number, si: string): Promise` + - `list(googleAuthId: number): Promise>` + - `upsert(googleAuthId: number, input: object): Promise<{id: number, si: string, basesi: string, basesifactor: number, symbol: string, name: string}>` - [companysetting](API.companysetting.md) + - `get(googleAuthId: number, key: string, options?: object): Promise<{key: string, value: string, frontend: boolean, writeonly: boolean}|undefined>` + - `list(googleAuthId: number, options?: object): Promise>` + - `set(googleAuthId: number, input: object): Promise<{key: string, value: string, frontend: boolean, writeonly: boolean}>` - [brand](API.brand.md) + - `delete(googleAuthId: number, id: number): Promise` + - `list(googleAuthId: number): Promise>` + - `upsert(googleAuthId: number, input: object): Promise<{id: number, name: string, gsprpinfo: string|null}>` - [customergroup](API.customergroup.md) + - `create(googleAuthId: number, input: object): Promise<{id: number, name: string, sort: number}>` + - `delete(googleAuthId: number, id: number): Promise` + - `list(googleAuthId: number): Promise>` + - `order(googleAuthId: number, input: object): Promise<{customergroupIds: Array}>` + - `update(googleAuthId: number, id: number, input: object): Promise<{id: number, name: string, sort: number}>` - [entity](API.entity.md) + - `create(googleAuthId: number, input: object): Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>` + - `delete(googleAuthId: number, id: number): Promise` + - `getById(googleAuthId: number, id: number): Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>` + - `isNumberFree(googleAuthId: number, number: string, options?: object): Promise` + - `list(googleAuthId: number, options?: object): Promise>` + - `peekNextNumber(googleAuthId: number): Promise` + - `update(googleAuthId: number, id: number, patch: object): Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>` - [entityaddress](API.entityaddress.md) + - `create(googleAuthId: number, entityId: number, input: object): Promise` + - `delete(googleAuthId: number, entityId: number, id: number): Promise` + - `list(googleAuthId: number, entityId: number): Promise>` + - `update(googleAuthId: number, entityId: number, id: number, patch: object): Promise` - [entitycontact](API.entitycontact.md) + - `create(googleAuthId: number, entityId: number, input: object): Promise<{id: number, entityId: number, contactType: string, contact: string, sort: number}>` + - `delete(googleAuthId: number, entityId: number, id: number): Promise` + - `list(googleAuthId: number, entityId: number): Promise>` + - `update(googleAuthId: number, entityId: number, id: number, patch: object): Promise<{id: number, entityId: number, contactType: string, contact: string, sort: number}>` - [supplier](API.supplier.md) + - `create(googleAuthId: number, input: object): Promise<{id: number, name: string, createdAt: string, updatedAt: string}>` + - `delete(googleAuthId: number, id: number): Promise` + - `list(googleAuthId: number): Promise>` + - `update(googleAuthId: number, id: number, input: object): Promise<{id: number, name: string, createdAt: string, updatedAt: string}>` - [delivery](API.delivery.md) + - `create(googleAuthId: number, body: object): Promise<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string, lines: Array}>` + - `delete(googleAuthId: number, id: number): Promise` + - `getById(googleAuthId: number, id: number): Promise<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string, lines: Array}>` + - `list(googleAuthId: number): Promise>` + - `update(googleAuthId: number, id: number, patch: object): Promise<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string, lines: Array}>` - [quote](API.quote.md) + - `create(googleAuthId: number, body: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array}>` + - `delete(googleAuthId: number, id: number): Promise` + - `getById(googleAuthId: number, id: number): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array}>` + - `list(googleAuthId: number): Promise>` + - `update(googleAuthId: number, id: number, patch: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array}>` - [order](API.order.md) + - `create(googleAuthId: number, body: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array}>` + - `delete(googleAuthId: number, id: number): Promise` + - `getById(googleAuthId: number, id: number): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array}>` + - `list(googleAuthId: number): Promise>` + - `update(googleAuthId: number, id: number, patch: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array}>` +- [manifestattachment](API.manifestattachment.md) + - `add(googleAuthId: number, manifestId: number, input: object): Promise` + - `getById(googleAuthId: number, manifestId: number, id: number): Promise` + - `listByManifestId(googleAuthId: number, manifestId: number): Promise>` + - `remove(googleAuthId: number, manifestId: number, id: number): Promise` - [posterminal](API.posterminal.md) + - `delete(googleAuthId: number, id: number): Promise` + - `list(googleAuthId: number): Promise>` + - `setOff(googleAuthId: number, posterminalId: number, categoryId: number): Promise<{id: number, categoryIds: Array}>` + - `setOffAll(googleAuthId: number, posterminalId: number): Promise<{id: number, categoryIds: Array}>` + - `setOn(googleAuthId: number, posterminalId: number, categoryId: number): Promise<{id: number, categoryIds: Array}>` + - `setOnAll(googleAuthId: number, posterminalId: number): Promise<{id: number, categoryIds: Array}>` + - `upsert(googleAuthId: number, input: object): Promise<{id: number, name: string, token: string, pairingpin: string}>` - [google_auth](API.google_auth.md) + - `add(email: string): Promise` + - `getByEmail(email: string): Promise` + - `getById(googleAuthId: number): Promise` + - `listAllowedEmails(): Promise>` + - `removeAllowedEmail(email: string): Promise` + - `setProfile(googleAuthId: number, input: object): Promise` + - `setSelectedCompany(googleAuthId: number, companyId: number): Promise` - [pic](API.pic.md) + - `getImageByHash(googleAuthId: number, fullHash: string): Promise<{buffer: Buffer, w: number, h: number, size: number, contentType: string}>` + - `getThumbByHash(googleAuthId: number, fullHash: string): Promise<{buffer: Buffer, w: number, h: number, contentType: string}>` - [numbers](API.numbers.md) + - `list(googleAuthId: number): Promise>` + - `update(googleAuthId: number, id: number, input: object): Promise<{id: number, type: string, prefix: string, padding: number, next: number}>` diff --git a/generate-api-doc.js b/generate-api-doc.js index 4c8f9e2..2ce10c7 100644 --- a/generate-api-doc.js +++ b/generate-api-doc.js @@ -26,6 +26,7 @@ const MODULE_ORDER = [ 'delivery', 'quote', 'order', + 'manifestattachment', 'posterminal', 'google_auth', 'pic', @@ -51,6 +52,7 @@ const INTERNAL_FILES = new Set([ 'resolveEntityAccess.js', 'resolvePicAccess.js', 'resolveArticleBySku.js', + 'resolveManifestAccess.js', 'queryBySku.js', 'queryCategoryArticles.js', 'getCategoryPosterminalIds.js', @@ -193,6 +195,10 @@ function buildModules() { }); } + for (const entries of modules.values()) { + entries.sort((a, b) => a.name.localeCompare(b.name)); + } + return modules; } @@ -249,6 +255,21 @@ function renderSection(moduleName, entries) { return lines.join('\n'); } +function formatMethodSignature(entry) { + const topLevelParams = (entry.params || []).filter((p) => !p.name.includes('.')); + const paramsStr = topLevelParams + .map((p) => { + const isOptional = p.name.startsWith('['); + const cleanName = p.name.replace(/^\[|\]$/g, '').split('=')[0].trim(); + const typeStr = p.type ? `: ${p.type}` : ''; + return `${cleanName}${isOptional ? '?' : ''}${typeStr}`; + }) + .join(', '); + + const returnType = entry.returns?.type ? `: ${entry.returns.type}` : ': void'; + return `(${paramsStr})${returnType}`; +} + function render() { const modules = buildModules(); @@ -282,6 +303,11 @@ function render() { for (const { moduleName } of written) { lines.push(`- [${moduleName}](API.${moduleName}.md)`); + const entries = modules.get(moduleName) || []; + for (const entry of entries) { + const sig = formatMethodSignature(entry); + lines.push(` - \`${entry.name}${sig}\``); + } } lines.push(''); diff --git a/generate-schema-graph.js b/generate-schema-graph.js index 0b4d53d..0dac646 100644 --- a/generate-schema-graph.js +++ b/generate-schema-graph.js @@ -32,7 +32,7 @@ function parseSchema(source) { const trimmed = line.trim().replace(/,$/, ''); if (!trimmed || trimmed.startsWith('CONSTRAINT')) { const fkMatch = trimmed.match( - /CONSTRAINT\s+(\w+)\s+FOREIGN KEY\s*\(([^)]+)\)\s+REFERENCES\s+(\w+)\(([^)]+)\)(?:\s+ON DELETE\s+(.+))?/i + /CONSTRAINT\s+(\w+)\s+FOREIGN KEY\s*\(([^)]+)\)\s+REFERENCES\s+\[?(\w+)\]?\(([^)]+)\)(?:\s+ON DELETE\s+(.+))?/i ); if (fkMatch) { const constraintName = fkMatch[1]; @@ -105,7 +105,7 @@ function parseSchema(source) { } const alterFkRegex = - /ALTER TABLE\s+(\w+)\s+ADD CONSTRAINT\s+(\w+)\s+FOREIGN KEY\s*\(([^)]+)\)\s+REFERENCES\s+(\w+)\(([^)]+)\)(?:\s+ON DELETE\s+([\w\s]+?))?\s*;/gi; + /ALTER TABLE\s+(\w+)\s+ADD CONSTRAINT\s+(\w+)\s+FOREIGN KEY\s*\(([^)]+)\)\s+REFERENCES\s+\[?(\w+)\]?\(([^)]+)\)(?:\s+ON DELETE\s+([\w\s]+?))?\s*;/gi; let alterMatch; while ((alterMatch = alterFkRegex.exec(sqlBlock)) !== null) { @@ -361,12 +361,12 @@ const CLUSTERS = [ { id: 'media', label: 'Media', - tables: ['pic', 'articlepic', 'categorypic', 'brandpic'], + tables: ['pic', 'articlepic', 'categorypic', 'brandpic', 'file'], }, { id: 'manifests', label: 'Documents', - tables: ['manifest', 'manifestarticle', 'ledger'], + tables: ['manifest', 'manifestarticle', 'manifestattachment', 'ledger'], }, { id: 'commerce', diff --git a/schema-graph.dot b/schema-graph.dot index 124978b..449a64b 100644 --- a/schema-graph.dot +++ b/schema-graph.dot @@ -16,7 +16,7 @@ digraph schema { 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)
sortINT
>]; companyuser [label=<
companyuser
idINT PK
company_idINT FK
google_auth_idINT FK
>]; entitycontact [label=<
entitycontact
idINT PK
entity_idINT FK
contacttypeNVARCHAR(50)
contactNVARCHAR(255)
sortINT
>]; - company [label=<
company
idINT PK
poscustomergroup_idINT FK
default_language_id?INT FK
root_category_id?INT FK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
nameNVARCHAR(255)
>]; + company [label=<
company
idINT PK
poscustomergroup_idINT FK
default_language_id?INT FK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
root_category_id?INT FK
nameNVARCHAR(255)
>]; syncpos [label=<
syncpos
idINT PK
company_idINT FK
paircodeNVARCHAR(50)
token?NVARCHAR(255)
>]; entityhistory [label=<
entityhistory
idINT PK
entity_idINT FK
companyuser_id?INT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; companysetting [label=<
companysetting
idINT PK
company_idINT FK
keyNVARCHAR(255)
valueNVARCHAR(MAX)
frontendBIT
writeonlyBIT
>]; @@ -35,17 +35,17 @@ digraph schema { label="Catalog"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; - optionslist [label=<
optionslist
idINT PK
>]; categoryarticle [label=<
categoryarticle
idINT PK
category_idINT FK
article_idINT FK
>]; - optionslistarticle [label=<
optionslistarticle
idINT PK
optionslist_idINT FK
article_idINT FK
countINT
sortINT
mandatedBIT
>]; - category [label=<
category
idINT PK
parent_id?INT FK
sortINT
>]; articlehistory [label=<
articlehistory
idINT PK
companyuser_id?INT FK
article_idINT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; - article [label=<
article
idINT PK
unit_idINT FK
optionslist_id?INT FK
fatherarticle_id?INT FK
articlelist_id?INT FK
brand_id?INT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; - articlelist [label=<
articlelist
idINT PK
>]; + category [label=<
category
idINT PK
parent_id?INT FK
sortINT
>]; + article [label=<
article
idINT PK
unit_idINT FK
fatherarticle_id?INT FK
articlelist_id?INT FK
brand_id?INT FK
optionslist_id?INT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; + articlelist [label=<
articlelist
idINT PK
>]; articlelistarticle [label=<
articlelistarticle
idINT PK
article_idINT FK
articlelist_idINT FK
countINT
sortINT
>]; + articlesupplier [label=<
articlesupplier
idINT PK
entity_idINT FK
price_id?INT FK
article_idINT FK
sku?NVARCHAR(100)
stock?FLOAT
puFLOAT
>]; brand [label=<
brand
idINT PK
company_idINT FK
nameNVARCHAR(255)
gsprpinfo?NVARCHAR(MAX)
>]; - articlesupplier [label=<
articlesupplier
idINT PK
entity_idINT FK
article_idINT FK
price_id?INT FK
sku?NVARCHAR(100)
stock?FLOAT
puFLOAT
>]; + optionslist [label=<
optionslist
idINT PK
>]; + optionslistarticle [label=<
optionslistarticle
idINT PK
article_idINT FK
optionslist_idINT FK
countINT
sortINT
mandatedBIT
>]; } subgraph cluster_media { label="Media"; labeljust="l"; fontsize=15; @@ -60,16 +60,17 @@ digraph schema { label="Documents"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; - ledger [label=<
ledger
idINT PK
manifestarticle_idINT FK
transaction_typeNVARCHAR(100)
amountFLOAT
transaction_textNVARCHAR(MAX)
hash_versionINT
prev_hash?NVARCHAR(128)
>]; - manifest [label=<
manifest
idINT PK
entity_id?INT FK
company_idINT FK
numberNVARCHAR(100)
typeNVARCHAR(100)
external_number?NVARCHAR(100)
dateDATETIME2
>]; manifestarticle [label=<
manifestarticle
idINT PK
manifest_idINT FK
sku?NVARCHAR(100)
name?NVARCHAR(255)
puFLOAT
amountFLOAT
netFLOAT
comment?NVARCHAR(MAX)
rebateFLOAT
taxrateDECIMAL(18,4)
>]; + ledger [label=<
ledger
idINT PK
manifestarticle_idINT FK
transaction_typeNVARCHAR(100)
amountFLOAT
transaction_textNVARCHAR(MAX)
hash_versionINT
prev_hash?NVARCHAR(128)
>]; + manifest [label=<
manifest
idINT PK
entity_id?INT FK
company_idINT FK
numberNVARCHAR(100)
typeNVARCHAR(100)
external_number?NVARCHAR(100)
dateDATETIME2
>]; + manifestattachment [label=<
manifestattachment
idINT PK
manifest_idINT FK
file_idINT FK
>]; } subgraph cluster_commerce { label="Pricing"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; tax [label=<
tax
idINT PK
rateDECIMAL(18,4)
>]; - price [label=<
price
idINT PK
customergroup_idINT FK
article_idINT FK
tax_idINT FK
minmultiplierDECIMAL(18,4)
netFLOAT
>]; + price [label=<
price
idINT PK
customergroup_idINT FK
tax_idINT FK
article_idINT FK
minmultiplierDECIMAL(18,4)
netFLOAT
>]; } subgraph cluster_units { label="Units"; labeljust="l"; fontsize=15; @@ -89,14 +90,14 @@ digraph schema { google_auth:selected_company:w -> company:in7:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_google_auth_selected_company", weight=3]; category:parent_id:e -> category:id:e [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_category_parent", constraint=false]; - company:root_category_id:w -> category:in1:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_company_root_category", weight=3]; + company:root_category_id:e -> category:in0:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_company_root_category", weight=3]; company:default_language_id:se -> language:in0:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_company_default_language", constraint=false, weight=1]; company:taxrate:e -> tax:in1:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_company_taxrate", constraint=false, weight=1]; company:reducedtaxrate:e -> tax:in0:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_company_reducedtaxrate", constraint=false, weight=1]; company:taxfreerate:e -> tax:in2:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_company_taxfreerate", constraint=false, weight=1]; - customergroup:company_id:e -> company:in2:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_customergroup_company", weight=3]; - company:poscustomergroup_id:w -> customergroup:in0:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_company_poscustomergroup", constraint=false, weight=1]; - entity:customergroup_id:nw -> customergroup:in1:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_entity_customergroup", weight=3]; + customergroup:company_id:w -> company:in2:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_customergroup_company", weight=3]; + company:poscustomergroup_id:w -> customergroup:in2:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_company_poscustomergroup", constraint=false, weight=1]; + entity:customergroup_id:nw -> customergroup:in0:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_entity_customergroup", weight=3]; entityaddress:entity_id:se -> entity:in1:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_entityaddress_entity", weight=3]; entitycontact:entity_id:nw -> entity:in2:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_entitycontact_entity", weight=3]; entityhistory:entity_id:w -> entity:in3:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_entityhistory_entity", weight=3]; @@ -107,44 +108,46 @@ digraph schema { brand:company_id:e -> company:in9:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_brand_company", weight=3]; article:unit_id:w -> unit:in1:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_article_unit", constraint=false, weight=1]; article:brand_id:ne -> brand:in0:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_article_brand", constraint=false, weight=1]; - article:articlelist_id:sw -> articlelist:in0:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_article_articlelist", weight=3]; - article:optionslist_id:w -> optionslist:in1:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_article_optionslist", constraint=false, weight=1]; + article:articlelist_id:nw -> articlelist:in1:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_article_articlelist", weight=3]; + article:optionslist_id:e -> optionslist:in0:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_article_optionslist", constraint=false, weight=1]; article:fatherarticle_id:e -> article:id:e [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_article_fatherarticle", constraint=false]; articlehistory:article_id:ne -> article:in3:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_articlehistory_article", weight=3]; articlehistory:companyuser_id:w -> companyuser:in1:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_articlehistory_companyuser", constraint=false, weight=1]; - articleean:article_id:sw -> article:in6:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_articleean_article", weight=3]; + articleean:article_id:sw -> article:in5:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_articleean_article", weight=3]; articlepic:article_id:e -> article:in9:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_articlepic_article", weight=3]; articlepic:pic_id:w -> pic:in2:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_articlepic_pic", constraint=false, weight=1]; brandpic:brand_id:e -> brand:in1:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_brandpic_brand", weight=3]; brandpic:pic_id:nw -> pic:in1:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_brandpic_pic", constraint=false, weight=1]; - articlelistarticle:articlelist_id:sw -> articlelist:in1:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_articlelistarticle_articlelist", weight=3]; - articlelistarticle:article_id:w -> article:in8:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_articlelistarticle_article", constraint=false, weight=1]; - optionslistarticle:optionslist_id:sw -> optionslist:in0:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_optionslistarticle_optionslist", weight=3]; - optionslistarticle:article_id:se -> article:in2:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_optionslistarticle_article", constraint=false, weight=1]; - categoryarticle:category_id:se -> category:in3:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_categoryarticle_category", weight=3]; + articlelistarticle:articlelist_id:sw -> articlelist:in0:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_articlelistarticle_articlelist", weight=3]; + articlelistarticle:article_id:w -> article:in6:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_articlelistarticle_article", constraint=false, weight=1]; + optionslistarticle:optionslist_id:se -> optionslist:in1:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_optionslistarticle_optionslist", weight=3]; + optionslistarticle:article_id:w -> article:in8:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_optionslistarticle_article", constraint=false, weight=1]; + categoryarticle:category_id:se -> category:in2:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_categoryarticle_category", weight=3]; categoryarticle:article_id:ne -> article:in0:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_categoryarticle_article", constraint=false, weight=1]; categorypic:category_id:e -> category:in5:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_categorypic_category", weight=3]; categorypic:pic_id:nw -> pic:in0:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_categorypic_pic", constraint=false, weight=1]; - categorytext:category_id:se -> category:in2:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_categorytext_category", weight=3]; + categorytext:category_id:e -> category:in1:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_categorytext_category", weight=3]; categorytext:language_id:nw -> language:in3:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_categorytext_language", constraint=false, weight=1]; articletext:article_id:w -> article:in1:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_articletext_article", weight=3]; articletext:language_id:w -> language:in2:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_articletext_language", constraint=false, weight=1]; price:tax_id:w -> tax:in3:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_price_tax", constraint=false, weight=1]; - price:article_id:e -> article:in7:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_price_article", weight=3]; - price:customergroup_id:e -> customergroup:in2:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_price_customergroup", constraint=false, weight=1]; - articlesupplier:article_id:sw -> article:in5:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_articlesupplier_article", weight=3]; + price:article_id:sw -> article:in2:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_price_article", weight=3]; + price:customergroup_id:e -> customergroup:in1:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_price_customergroup", constraint=false, weight=1]; + articlesupplier:article_id:se -> article:in7:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_articlesupplier_article", weight=3]; articlesupplier:entity_id:w -> entity:in4:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_articlesupplier_entity", constraint=false, weight=1]; - articlesupplier:price_id:e -> price:in0:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_articlesupplier_price", constraint=false, weight=1]; + articlesupplier:price_id:w -> price:in0:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_articlesupplier_price", constraint=false, weight=1]; manifest:company_id:e -> company:in1:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_manifest_company", weight=3]; - manifest:entity_id:ne -> entity:in0:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_manifest_entity", constraint=false, weight=1]; - manifestarticle:manifest_id:sw -> manifest:in0:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_manifestarticle_manifest", weight=3]; - ledger:manifestarticle_id:se -> manifestarticle:in0:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_ledger_manifestarticle", weight=3]; - numbers:company_id:w -> company:in3:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_numbers_company", weight=3]; + manifest:entity_id:e -> entity:in0:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_manifest_entity", constraint=false, weight=1]; + manifestarticle:manifest_id:ne -> manifest:in0:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_manifestarticle_manifest", weight=3]; + ledger:manifestarticle_id:sw -> manifestarticle:in0:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_ledger_manifestarticle", weight=3]; + numbers:company_id:e -> company:in3:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_numbers_company", weight=3]; companyuser:google_auth_id:e -> google_auth:in0:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_companyuser_google_auth", constraint=false, weight=1]; companyuser:company_id:se -> company:in4:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_companyuser_company", weight=3]; syncpos:company_id:nw -> company:in5:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_syncpos_company", weight=3]; companysetting:company_id:sw -> company:in6:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_companysetting_company", weight=3]; posterminal:company_id:e -> company:in0:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_posterminal_company", weight=3]; posterminalcategory:posterminal_id:sw -> posterminal:in0:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_posterminalcategory_posterminal", weight=3]; - posterminalcategory:category_id:e -> category:in0:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_posterminalcategory_category", constraint=false, weight=1]; + posterminalcategory:category_id:e -> category:in3:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_posterminalcategory_category", constraint=false, weight=1]; + manifestattachment:manifest_id:se -> manifest:in1:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_manifestattachment_manifest", weight=3]; + manifestattachment:file_id:w -> file:in0:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_manifestattachment_file", constraint=false, weight=1]; } \ No newline at end of file diff --git a/schema-graph.svg b/schema-graph.svg index 29e6489..93b9314 100644 --- a/schema-graph.svg +++ b/schema-graph.svg @@ -4,45 +4,45 @@ - - + + schema - + cluster_identity - -Identity & Company + +Identity & Company cluster_i18n - -Localization + +Localization cluster_catalog - -Catalog + +Catalog cluster_media - -Media + +Media cluster_manifests - + Documents cluster_commerce - -Pricing + +Pricing cluster_units - -Units + +Units cluster_pos @@ -52,388 +52,388 @@ meta - - -meta - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) + + +meta + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) customergroup - - - - - -customergroup - - -id -INT -PK - - -company_id -? -INT -FK - - -name -NVARCHAR(255) - - -sort -INT + + + + + +customergroup + + +id +INT +PK + + +company_id +? +INT +FK + + +name +NVARCHAR(255) + + +sort +INT company - - - - - - - - - - - - -company - - -id -INT -PK - - -poscustomergroup_id -INT -FK - - -default_language_id -? -INT -FK - - -root_category_id -? -INT -FK - - -taxrate -? -INT -FK - - -reducedtaxrate -? -INT -FK - - -taxfreerate -? -INT -FK - - -name -NVARCHAR(255) + + + + + + + + + + + + +company + + +id +INT +PK + + +poscustomergroup_id +INT +FK + + +default_language_id +? +INT +FK + + +taxrate +? +INT +FK + + +reducedtaxrate +? +INT +FK + + +taxfreerate +? +INT +FK + + +root_category_id +? +INT +FK + + +name +NVARCHAR(255) -customergroup:e->company:n +customergroup:w->company:n - - + + entity - - - - - - - -entity - - -id -INT -PK - - -customergroup_id -INT -FK - - -first_name? -NVARCHAR(255) - - -name -NVARCHAR(255) - - -number? -NVARCHAR(100) - - -created_at -DATETIME2 - - -updated_at -DATETIME2 + + + + + + + +entity + + +id +INT +PK + + +customergroup_id +INT +FK + + +first_name? +NVARCHAR(255) + + +name +NVARCHAR(255) + + +number? +NVARCHAR(100) + + +created_at +DATETIME2 + + +updated_at +DATETIME2 entity:nw->customergroup: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:w->company:n +numbers:e->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) - - -sort -INT + + +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) + + +sort +INT entityaddress:se->entity:n - - + + companyuser - - - - -companyuser - - -id -INT -PK - - -company_id -INT -FK - - -google_auth_id -INT -FK + + + + +companyuser + + +id +INT +PK + + +company_id +INT +FK + + +google_auth_id +INT +FK companyuser:se->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 + + + +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:e->google_auth:n - - + + entitycontact - - -entitycontact - - -id -INT -PK - - -entity_id -INT -FK - - -contacttype -NVARCHAR(50) - - -contact -NVARCHAR(255) - - -sort -INT + + +entitycontact + + +id +INT +PK + + +entity_id +INT +FK + + +contacttype +NVARCHAR(50) + + +contact +NVARCHAR(255) + + +sort +INT entitycontact:nw->entity:n - - + + @@ -441,103 +441,103 @@ company:w->customergroup:n - - + + language - - - - - - -language - - -id -INT -PK - - -iso639 -NVARCHAR(10) + + + + + + +language + + +id +INT +PK + + +iso639 +NVARCHAR(10) company:se->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 +company:e->category:n - - + + - + tax - - - - - - -tax - - -id -INT -PK - - -rate -DECIMAL(18,4) + + + + + + +tax + + +id +INT +PK + + +rate +DECIMAL(18,4) company:e->tax:n - - + + @@ -545,8 +545,8 @@ company:e->tax:n - - + + @@ -554,82 +554,82 @@ company:e->tax: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:nw->company:n - - + + entityhistory - - -entityhistory - - -id -INT -PK - - -entity_id -INT -FK - - -companyuser_id -? -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) + + +entityhistory + + +id +INT +PK + + +entity_id +INT +FK + + +companyuser_id +? +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) entityhistory:w->entity:n - - + + @@ -637,50 +637,50 @@ entityhistory:sw->companyuser:n - - + + companysetting - - -companysetting - - -id -INT -PK - - -company_id -INT -FK - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) - - -frontend -BIT - - -writeonly -BIT + + +companysetting + + +id +INT +PK + + +company_id +INT +FK + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) + + +frontend +BIT + + +writeonly +BIT companysetting:sw->company:n - - + + @@ -688,323 +688,309 @@ google_auth:w->company:n - - + + unittext - - -unittext - - -id -INT -PK - - -unit_id -INT -FK - - -language_id -INT -FK - - -symbol -NVARCHAR(50) - - -name -NVARCHAR(255) + + +unittext + + +id +INT +PK + + +unit_id +INT +FK + + +language_id +INT +FK + + +symbol +NVARCHAR(50) + + +name +NVARCHAR(255) unittext:nw->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:sw->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:nw->language:n - - + + -categorytext:se->category: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:w->language:n - - + + - + article - - - - - - - - - - - - -article - - -id -INT -PK - - -unit_id -INT -FK - - -optionslist_id -? -INT -FK - - -fatherarticle_id -? -INT -FK - - -articlelist_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 + + +articlelist_id +? +INT +FK + + +brand_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:w->article:n - - + + - - -optionslist - - - - -optionslist - - -id -INT -PK - - + categoryarticle - - -categoryarticle - - -id -INT -PK - - -category_id -INT -FK - - -article_id -INT -FK + + +categoryarticle + + +id +INT +PK + + +category_id +INT +FK + + +article_id +INT +FK categoryarticle:se->category:n - - + + @@ -1012,109 +998,48 @@ categoryarticle:ne->article:n - - - - - - - -optionslistarticle - - -optionslistarticle - - -id -INT -PK - - -optionslist_id -INT -FK - - -article_id -INT -FK - - -count -INT - - -sort -INT - - -mandated -BIT - - - -optionslistarticle:sw->optionslist:n - - - - - - - - -optionslistarticle:se->article:n - - - - - - - - -category:e->category:e - - - + + - + articlehistory - - -articlehistory - - -id -INT -PK - - -companyuser_id -? -INT -FK - - -article_id -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) + + +articlehistory + + +id +INT +PK + + +companyuser_id +? +INT +FK + + +article_id +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) articlehistory:w->companyuser:n - - + + @@ -1122,17 +1047,17 @@ articlehistory:ne->article:n - - + + - - -article:w->optionslist:n - - - + + +category:e->category:e + + + @@ -1140,67 +1065,90 @@ article:e->article:e - - + + - + articlelist - - - - -articlelist - - -id -INT -PK + + + + +articlelist + + +id +INT +PK -article:sw->articlelist:n +article:nw->articlelist: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:ne->brand:n - - + + + + + + + +optionslist + + + + +optionslist + + +id +INT +PK + + + +article:e->optionslist:n + + + @@ -1208,77 +1156,77 @@ article:w->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 - - + + - + articlelistarticle - - -articlelistarticle - - -id -INT -PK - - -article_id -INT -FK - - -articlelist_id -INT -FK - - -count -INT - - -sort -INT + + +articlelistarticle + + +id +INT +PK + + +article_id +INT +FK + + +articlelist_id +INT +FK + + +count +INT + + +sort +INT articlelistarticle:w->article:n - - + + @@ -1286,8 +1234,111 @@ articlelistarticle:sw->articlelist:n - - + + + + + + + +articlesupplier + + +articlesupplier + + +id +INT +PK + + +entity_id +INT +FK + + +price_id +? +INT +FK + + +article_id +INT +FK + + +sku? +NVARCHAR(100) + + +stock? +FLOAT + + +pu +FLOAT + + + +articlesupplier:w->entity:n + + + + + + + + +articlesupplier:se->article:n + + + + + + + + +price + + + +price + + +id +INT +PK + + +customergroup_id +INT +FK + + +tax_id +INT +FK + + +article_id +INT +FK + + +minmultiplier +DECIMAL(18,4) + + +net +FLOAT + + + +articlesupplier:w->price:n + + + @@ -1295,245 +1346,194 @@ brand:e->company:n - - + + - + -articlesupplier - - -articlesupplier - - -id -INT -PK - - -entity_id -INT -FK - - -article_id -INT -FK - - -price_id -? -INT -FK - - -sku? -NVARCHAR(100) - - -stock? -FLOAT - - -pu -FLOAT +optionslistarticle + + +optionslistarticle + + +id +INT +PK + + +article_id +INT +FK + + +optionslist_id +INT +FK + + +count +INT + + +sort +INT + + +mandated +BIT - - -articlesupplier:w->entity:n - - - + + +optionslistarticle:w->article:n + + + - - -articlesupplier:sw->article:n - - - - - - - - -price - - - -price - - -id -INT -PK - - -customergroup_id -INT -FK - - -article_id -INT -FK - - -tax_id -INT -FK - - -minmultiplier -DECIMAL(18,4) - - -net -FLOAT - - - -articlesupplier:e->price:n - - - + + +optionslistarticle:se->optionslist: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:e->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) categorypic:nw->pic: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:e->brand:n - - + + @@ -1541,43 +1541,43 @@ brandpic:nw->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:e->article:n - - + + @@ -1585,153 +1585,163 @@ articlepic:w->pic:n - - + + + + + + + +manifestarticle + + + +manifestarticle + + +id +INT +PK + + +manifest_id +INT +FK + + +sku? +NVARCHAR(100) + + +name? +NVARCHAR(255) + + +pu +FLOAT + + +amount +FLOAT + + +net +FLOAT + + +comment? +NVARCHAR(MAX) + + +rebate +FLOAT + + +taxrate +DECIMAL(18,4) + + + +manifest + + + + +manifest + + +id +INT +PK + + +entity_id +? +INT +FK + + +company_id +INT +FK + + +number +NVARCHAR(100) + + +type +NVARCHAR(100) + + +external_number? +NVARCHAR(100) + + +date +DATETIME2 + + + +manifestarticle:ne->manifest:n + + + - + ledger - - -ledger - - -id -INT -PK - - -manifestarticle_id -INT -FK - - -transaction_type -NVARCHAR(100) - - -amount -FLOAT - - -transaction_text -NVARCHAR(MAX) - - -hash_version -INT - - -prev_hash? -NVARCHAR(128) - - - -manifestarticle - - - -manifestarticle - - -id -INT -PK - - -manifest_id -INT -FK - - -sku? -NVARCHAR(100) - - -name? -NVARCHAR(255) - - -pu -FLOAT - - -amount -FLOAT - - -net -FLOAT - - -comment? -NVARCHAR(MAX) - - -rebate -FLOAT - - -taxrate -DECIMAL(18,4) + + +ledger + + +id +INT +PK + + +manifestarticle_id +INT +FK + + +transaction_type +NVARCHAR(100) + + +amount +FLOAT + + +transaction_text +NVARCHAR(MAX) + + +hash_version +INT + + +prev_hash? +NVARCHAR(128) -ledger:se->manifestarticle:n +ledger:sw->manifestarticle:n - - + + - - -manifest - - - -manifest - - -id -INT -PK - - -entity_id -? -INT -FK - - -company_id -INT -FK - - -number -NVARCHAR(100) - - -type -NVARCHAR(100) - - -external_number? -NVARCHAR(100) - - -date -DATETIME2 - -manifest:ne->entity:n +manifest:e->entity:n - - + + @@ -1739,17 +1749,53 @@ manifest:e->company:n - - + + - - -manifestarticle:sw->manifest:n - - - + + +manifestattachment + + +manifestattachment + + +id +INT +PK + + +manifest_id +INT +FK + + +file_id +INT +FK + + + +manifestattachment:se->manifest:n + + + + + + + + +file +file + + + +manifestattachment:w->file:n + + + @@ -1757,17 +1803,17 @@ price:e->customergroup:n - - + + -price:e->article:n +price:sw->article:n - - + + @@ -1775,8 +1821,8 @@ price:w->tax:n - - + + @@ -1784,13 +1830,13 @@ unit:w->company:n - - + + - + posterminal @@ -1823,13 +1869,13 @@ posterminal:e->company:n - - + + - + posterminalcategory @@ -1854,8 +1900,8 @@ posterminalcategory:e->category:n - - + + @@ -1869,32 +1915,32 @@ - + __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/src/index.js b/src/index.js index 3755b31..05ebc6f 100644 --- a/src/index.js +++ b/src/index.js @@ -110,6 +110,10 @@ import numbersList from './store/numbers/list.js'; import numbersUpdate from './store/numbers/update.js'; import storePurge from './store/purge.js'; import storeInitialize from './store/initialize.js'; +import manifestattachmentAdd from './store/manifestattachment/add.js'; +import manifestattachmentRemove from './store/manifestattachment/remove.js'; +import manifestattachmentListByManifestId from './store/manifestattachment/listByManifestId.js'; +import manifestattachmentGetById from './store/manifestattachment/getById.js'; export default class LibDashStore { constructor(pool) { @@ -288,6 +292,13 @@ export default class LibDashStore { update: numbersUpdate(pool), }; + this.manifestattachment = { + add: manifestattachmentAdd(pool), + remove: manifestattachmentRemove(pool), + listByManifestId: manifestattachmentListByManifestId(pool), + getById: manifestattachmentGetById(pool), + }; + this.purge = storePurge(pool); this.initialize = storeInitialize(pool); diff --git a/src/schema.js b/src/schema.js index 43f09aa..6344212 100644 --- a/src/schema.js +++ b/src/schema.js @@ -765,6 +765,52 @@ BEGIN END `; +export const INIT_FILE = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'file') +BEGIN + CREATE TABLE [file] ( + id INT IDENTITY(1,1) PRIMARY KEY, + mimetype NVARCHAR(255) NOT NULL, + name NVARCHAR(255) NOT NULL, + content VARBINARY(MAX) NOT NULL + ); +END +`; + +export const INIT_MANIFESTATTACHMENT = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'manifestattachment') +BEGIN + CREATE TABLE manifestattachment ( + id INT IDENTITY(1,1) PRIMARY KEY, + manifest_id INT NOT NULL, + file_id INT NOT NULL, + CONSTRAINT FK_manifestattachment_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE, + CONSTRAINT FK_manifestattachment_file FOREIGN KEY (file_id) REFERENCES [file](id) ON DELETE CASCADE + ); +END +`; + +export const INIT_MANIFESTATTACHMENT_TRIGGER = ` +IF OBJECT_ID(N'dbo.TR_manifestattachment_delete_file', N'TR') IS NOT NULL + DROP TRIGGER dbo.TR_manifestattachment_delete_file; + +EXEC(' +CREATE TRIGGER dbo.TR_manifestattachment_delete_file +ON manifestattachment +AFTER DELETE +AS +BEGIN + SET NOCOUNT ON; + DELETE f + FROM [file] f + INNER JOIN deleted d ON f.id = d.file_id + WHERE NOT EXISTS ( + SELECT 1 FROM manifestattachment ma WHERE ma.file_id = f.id + ); +END +'); +`; + /** DDL scripts in dependency order for schema init. */ export const SCHEMA_SCRIPTS = [ INIT_META, @@ -810,4 +856,8 @@ export const SCHEMA_SCRIPTS = [ INIT_COMPANYSETTING, INIT_POSTERMINAL, INIT_POSTERMINALCATEGORY, + INIT_FILE, + INIT_MANIFESTATTACHMENT, + INIT_MANIFESTATTACHMENT_TRIGGER, ]; + diff --git a/src/sql/manifestattachment.js b/src/sql/manifestattachment.js new file mode 100644 index 0000000..12ea537 --- /dev/null +++ b/src/sql/manifestattachment.js @@ -0,0 +1,62 @@ +export const CHECK_MANIFEST_ACCESS = ` +SELECT id, company_id AS companyId +FROM manifest +WHERE id = @manifestId AND company_id = @companyId +`; + +export const CREATE_FILE = ` +INSERT INTO [file] (mimetype, name, content) +OUTPUT INSERTED.id +VALUES (@mimetype, @name, @content) +`; + +export const CREATE_ATTACHMENT = ` +INSERT INTO manifestattachment (manifest_id, file_id) +OUTPUT INSERTED.id +VALUES (@manifestId, @fileId) +`; + +export const LIST_BY_MANIFEST = ` +SELECT + ma.id, + ma.manifest_id AS manifestId, + ma.file_id AS fileId, + f.mimetype, + f.name, + DATALENGTH(f.content) AS size +FROM manifestattachment ma +INNER JOIN [file] f ON f.id = ma.file_id +WHERE ma.manifest_id = @manifestId +ORDER BY ma.id ASC +`; + +export const GET_BY_ID = ` +SELECT + ma.id, + ma.manifest_id AS manifestId, + ma.file_id AS fileId, + f.mimetype, + f.name, + f.content +FROM manifestattachment ma +INNER JOIN [file] f ON f.id = ma.file_id +WHERE ma.manifest_id = @manifestId + AND (ma.id = @id OR ma.file_id = @id) +`; + +export const FIND_ATTACHMENT = ` +SELECT ma.id, ma.file_id AS fileId +FROM manifestattachment ma +WHERE ma.manifest_id = @manifestId + AND (ma.id = @id OR ma.file_id = @id) +`; + +export const DELETE_ATTACHMENT = ` +DELETE FROM manifestattachment +WHERE id = @id +`; + +export const DELETE_FILE = ` +DELETE FROM [file] +WHERE id = @fileId +`; diff --git a/src/store/manifestattachment/add.js b/src/store/manifestattachment/add.js new file mode 100644 index 0000000..f43db10 --- /dev/null +++ b/src/store/manifestattachment/add.js @@ -0,0 +1,99 @@ +import { CREATE_FILE, CREATE_ATTACHMENT } from '../../sql/manifestattachment.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import resolveManifestAccess from './resolveManifestAccess.js'; +import request from '../request.js'; + +export default function add(pool) { + const access = resolveSelectedCompanyAccess(pool); + const manifestAccess = resolveManifestAccess(pool); + + /** + * Attaches a file to an existing manifest. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} manifestId - The id of the manifest. + * @param {object} input - The file data. + * @param {string} input.mimetype - The MIME type of the file (e.g. `'application/pdf'`). + * @param {string} input.name - The file name. + * @param {Buffer} input.content - The file content. + * @returns {Promise} The created attachment record. + * @throws {Error} If the manifest is not found or file input is invalid. + * + * @example + * const attachment = await store.manifestattachment.add(42, 1, { + * mimetype: 'application/pdf', + * name: 'invoice.pdf', + * content: fileBuffer, + * }); + * // => { id, manifestId, fileId, mimetype, name } + */ + return async (googleAuthId, manifestId, input, ...rest) => { + let rawMimetype, rawName, rawContent; + if (typeof input === 'object' && input !== null && !Buffer.isBuffer(input)) { + rawMimetype = input.mimetype; + rawName = input.name; + rawContent = input.content; + } else { + rawMimetype = input; + rawName = rest[0]; + rawContent = rest[1]; + } + + const mimetype = typeof rawMimetype === 'string' ? rawMimetype.trim() : null; + if (!mimetype) { + throw new Error('Mimetype required'); + } + + const name = typeof rawName === 'string' ? rawName.trim() : null; + if (!name) { + throw new Error('File name required'); + } + + if (rawContent == null) { + throw new Error('Content required'); + } + const content = Buffer.isBuffer(rawContent) + ? rawContent + : Buffer.from(rawContent); + + const { companyId } = await access(googleAuthId); + await manifestAccess(companyId, manifestId); + + const tx = pool.transaction(); + await tx.begin(); + + try { + const fileRes = await request(pool, tx) + .input('mimetype', mimetype) + .input('name', name) + .input('content', content) + .query(CREATE_FILE); + + const fileId = fileRes.recordset[0].id; + + const attachmentRes = await request(pool, tx) + .input('manifestId', manifestId) + .input('fileId', fileId) + .query(CREATE_ATTACHMENT); + + const attachmentId = attachmentRes.recordset[0].id; + + await tx.commit(); + + return { + id: attachmentId, + manifestId, + fileId, + mimetype, + name, + }; + } catch (err) { + try { + await tx.rollback(); + } catch { + // ignore + } + throw err; + } + }; +} diff --git a/src/store/manifestattachment/getById.js b/src/store/manifestattachment/getById.js new file mode 100644 index 0000000..a020fc6 --- /dev/null +++ b/src/store/manifestattachment/getById.js @@ -0,0 +1,50 @@ +import { GET_BY_ID } from '../../sql/manifestattachment.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import resolveManifestAccess from './resolveManifestAccess.js'; +import request from '../request.js'; + +export default function getById(pool) { + const access = resolveSelectedCompanyAccess(pool); + const manifestAccess = resolveManifestAccess(pool); + + /** + * Gets an attachment for a manifest by its attachment ID or file ID, including full file content. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} manifestId - The manifest ID. + * @param {number} id - The attachment ID or file ID. + * @returns {Promise} The attachment record with binary content. + * @throws {Error} If the manifest or attachment is not found, or user is not authorized. + * + * @example + * const attachment = await store.manifestattachment.getById(42, 1, 5); + * // => { id, manifestId, fileId, mimetype, name, content } + */ + return async (googleAuthId, manifestId, id) => { + if (!Number.isFinite(id)) { + throw new Error('Attachment not found'); + } + + const { companyId } = await access(googleAuthId); + await manifestAccess(companyId, manifestId); + + const res = await request(pool) + .input('manifestId', manifestId) + .input('id', id) + .query(GET_BY_ID); + + const row = res.recordset[0]; + if (!row) { + throw new Error('Attachment not found'); + } + + return { + id: row.id, + manifestId: row.manifestId, + fileId: row.fileId, + mimetype: row.mimetype, + name: row.name, + content: row.content, + }; + }; +} diff --git a/src/store/manifestattachment/listByManifestId.js b/src/store/manifestattachment/listByManifestId.js new file mode 100644 index 0000000..a23404d --- /dev/null +++ b/src/store/manifestattachment/listByManifestId.js @@ -0,0 +1,43 @@ +import { LIST_BY_MANIFEST } from '../../sql/manifestattachment.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import resolveManifestAccess from './resolveManifestAccess.js'; +import request from '../request.js'; + +function shape(row) { + return { + id: row.id, + manifestId: row.manifestId, + fileId: row.fileId, + mimetype: row.mimetype, + name: row.name, + size: row.size, + }; +} + +export default function listByManifestId(pool) { + const access = resolveSelectedCompanyAccess(pool); + const manifestAccess = resolveManifestAccess(pool); + + /** + * Lists all attachments for a given manifest, without loading their full binary content. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} manifestId - The manifest ID. + * @returns {Promise>} A list of attachment metadata objects. + * @throws {Error} If the manifest is not found or the user is not authorized. + * + * @example + * const attachments = await store.manifestattachment.listByManifestId(42, 1); + * // => [{ id, manifestId, fileId, mimetype, name, size }] + */ + return async (googleAuthId, manifestId) => { + const { companyId } = await access(googleAuthId); + await manifestAccess(companyId, manifestId); + + const res = await request(pool) + .input('manifestId', manifestId) + .query(LIST_BY_MANIFEST); + + return res.recordset.map(shape); + }; +} diff --git a/src/store/manifestattachment/remove.js b/src/store/manifestattachment/remove.js new file mode 100644 index 0000000..1a1c46c --- /dev/null +++ b/src/store/manifestattachment/remove.js @@ -0,0 +1,44 @@ +import { FIND_ATTACHMENT, DELETE_FILE } from '../../sql/manifestattachment.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import resolveManifestAccess from './resolveManifestAccess.js'; +import request from '../request.js'; + +export default function remove(pool) { + const access = resolveSelectedCompanyAccess(pool); + const manifestAccess = resolveManifestAccess(pool); + + /** + * Removes a file attachment from a manifest and deletes the underlying file. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} manifestId - The manifest ID. + * @param {number} id - The attachment ID or file ID to remove. + * @returns {Promise} + * @throws {Error} If the manifest or attachment is not found, or user is not authorized. + * + * @example + * await store.manifestattachment.remove(42, 1, 5); + */ + return async (googleAuthId, manifestId, id) => { + if (!Number.isFinite(id)) { + throw new Error('Attachment not found'); + } + + const { companyId } = await access(googleAuthId); + await manifestAccess(companyId, manifestId); + + const findRes = await request(pool) + .input('manifestId', manifestId) + .input('id', id) + .query(FIND_ATTACHMENT); + + const row = findRes.recordset[0]; + if (!row) { + throw new Error('Attachment not found'); + } + + await request(pool) + .input('fileId', row.fileId) + .query(DELETE_FILE); + }; +} diff --git a/src/store/manifestattachment/resolveManifestAccess.js b/src/store/manifestattachment/resolveManifestAccess.js new file mode 100644 index 0000000..88f57cc --- /dev/null +++ b/src/store/manifestattachment/resolveManifestAccess.js @@ -0,0 +1,22 @@ +import { CHECK_MANIFEST_ACCESS } from '../../sql/manifestattachment.js'; +import request from '../request.js'; + +/** Ensure manifest belongs to company. Throws Manifest not found. */ +export default function resolveManifestAccess(pool) { + return async (companyId, manifestId, tx) => { + if (!Number.isFinite(manifestId)) { + throw new Error('Manifest not found'); + } + + const result = await request(pool, tx) + .input('manifestId', manifestId) + .input('companyId', companyId) + .query(CHECK_MANIFEST_ACCESS); + + const row = result.recordset[0]; + if (!row) { + throw new Error('Manifest not found'); + } + return row; + }; +}