diff --git a/API.md b/API.md index 6d4c752..066d5ee 100644 --- a/API.md +++ b/API.md @@ -281,6 +281,21 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `delete(googleAuthId: number, categoryId: number, propertyId: number, tx?: object): Promise` - `listByCategoryId(googleAuthId: number, categoryId: number, tx?: object): Promise>` - `setByCategoryId(googleAuthId: number, categoryId: number, properties: Array<{propertyId: number, value: string, sort?: number}>, tx?: object): Promise>` +- [modelconversation](API.modelconversation.md) + - `create(googleAuthId: number, input: {provider: string, model: string}, tx?: object): Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>` + - `delete(googleAuthId: number, conversationId: number, tx?: object): Promise` + - `get(googleAuthId: number, conversationId: number, tx?: object): Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>` + - `list(googleAuthId: number, tx?: object): Promise>` + - `setState(googleAuthId: number, conversationId: number, state: string, tx?: object): Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>` +- [modelmessage](API.modelmessage.md) + - `add(googleAuthId: number, conversationId: number, input: {message: string, role?: 'user'|'assistant'|'system'|'tool'|'reasoning'}, tx?: object): Promise<{id: number, conversation_id: number, created_at: string, role: string, message: string}>` + - `list(googleAuthId: number, conversationId: number, tx?: object): Promise>` +- [modelvfs](API.modelvfs.md) + - `delete(googleAuthId: number, name: string, tx?: object): Promise<{deleted: boolean}>` + - `list(googleAuthId: number, tx?: object): Promise>` + - `read(googleAuthId: number, name: string, tx?: object): Promise<{id: number, company_id: number, name: string, mime: string, content: Buffer, meta: any, created_at: string, updated_at: string}>` + - `reset(googleAuthId: number, tx?: object): Promise<{deletedCount: number}>` + - `write(googleAuthId: number, input: {name: string, mime?: string, content: Buffer|string, meta?: object|string}, tx?: object): Promise` - [optionslist](API.optionslist.md) - `getOptionsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise>` - `setOptionsByArticleSku(googleAuthId: number, sku: string, options?: Array<{articleId?: number, sku?: string, count?: number, sort?: number, mandated?: boolean}>, tx?: object): Promise>` diff --git a/API.modelconversation.md b/API.modelconversation.md new file mode 100644 index 0000000..4000396 --- /dev/null +++ b/API.modelconversation.md @@ -0,0 +1,123 @@ +# modelconversation + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `modelconversation.create` + +*Source: `src/store/modelconversation/create.js`* + +Creates a new (idle) model conversation for the user in the selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | Conversation input. | +| `input.provider` | `string` | LLM provider name. | +| `input.model` | `string` | Model identifier. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>` + +**Throws** + +`Error` — 'Provider required' or 'Model required'. + +**Example** + +```js +const c = await store.modelconversation.create(42, { provider: 'openrouter', model: 'openai/gpt-5' }); +``` + +## `modelconversation.delete` + +*Source: `src/store/modelconversation/delete.js`* + +Deletes a model conversation by ID for the user in the selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `conversationId` | `number` | The conversation id. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — 'Conversation not found'. + +## `modelconversation.get` + +*Source: `src/store/modelconversation/get.js`* + +Gets a single model conversation by ID for the user in the selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `conversationId` | `number` | The conversation id. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>` + +**Throws** + +`Error` — 'Conversation not found'. + +## `modelconversation.list` + +*Source: `src/store/modelconversation/list.js`* + +Lists the user's own model conversations in the selected company, newest activity first. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise>` + +**Example** + +```js +const conversations = await store.modelconversation.list(42); +``` + +## `modelconversation.setState` + +*Source: `src/store/modelconversation/setState.js`* + +Sets the state of a model conversation ('idle' | 'busy'). + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `conversationId` | `number` | The conversation id. | +| `state` | `string` | The new state ('idle' | 'busy'). | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>` + +**Throws** + +`Error` — 'Invalid state' or 'Conversation not found'. diff --git a/API.modelmessage.md b/API.modelmessage.md new file mode 100644 index 0000000..7e15e94 --- /dev/null +++ b/API.modelmessage.md @@ -0,0 +1,56 @@ +# modelmessage + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `modelmessage.add` + +*Source: `src/store/modelmessage/add.js`* + +Appends a message to one of the user's own conversations. `user` messages are refused unless the conversation state is `idle`; other roles ('assistant', 'system', 'tool', 'reasoning') touch `updated_at` unconditionally. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `conversationId` | `number` | The conversation id. | +| `input` | `object` | Message input. | +| `input.message` | `string` | Message text. | +| `[input.role='user']` | `'user'|'assistant'|'system'|'tool'|'reasoning'` | Message role. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, conversation_id: number, created_at: string, role: string, message: string}>` + +**Throws** + +`Error` — 'Conversation not found', 'Conversation is not idle', 'Message required', 'Invalid role'. + +## `modelmessage.list` + +*Source: `src/store/modelmessage/list.js`* + +Lists messages of one of the user's own conversations, oldest first. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `conversationId` | `number` | The conversation id. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise>` + +**Throws** + +`Error` — 'Conversation not found' if missing or owned by another user. + +**Example** + +```js +const messages = await store.modelmessage.list(42, 7); +``` diff --git a/API.modelvfs.md b/API.modelvfs.md new file mode 100644 index 0000000..e47c03a --- /dev/null +++ b/API.modelvfs.md @@ -0,0 +1,99 @@ +# modelvfs + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `modelvfs.delete` + +*Source: `src/store/modelvfs/delete.js`* + +Deletes a virtual file by name for the selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `name` | `string` | File name. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise<{deleted: boolean}>` + +## `modelvfs.list` + +*Source: `src/store/modelvfs/list.js`* + +Lists virtual files for the selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise>` + +## `modelvfs.read` + +*Source: `src/store/modelvfs/read.js`* + +Reads a virtual file by name for the selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `name` | `string` | File name. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise<{id: number, company_id: number, name: string, mime: string, content: Buffer, meta: any, created_at: string, updated_at: string}>` + +**Throws** + +`Error` — 'File not found' + +## `modelvfs.reset` + +*Source: `src/store/modelvfs/reset.js`* + +Deletes all virtual files for the selected company (reset VFS). + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise<{deletedCount: number}>` + +## `modelvfs.write` + +*Source: `src/store/modelvfs/write.js`* + +Writes (inserts or overwrites) a virtual file for the selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | | +| `input.name` | `string` | File path/name (e.g. 'candidate.avif', 'report.json'). | +| `[input.mime='application/octet-stream']` | `string` | Mime type. | +| `input.content` | `Buffer|string` | File content buffer or string. | +| `[input.meta]` | `object|string` | Optional metadata object or JSON string. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise` — The stored file record metadata. diff --git a/generate-schema-graph.js b/generate-schema-graph.js index 8ff21d6..258f645 100644 --- a/generate-schema-graph.js +++ b/generate-schema-graph.js @@ -379,6 +379,11 @@ const CLUSTERS = [ }, { id: 'units', label: 'Units', tables: ['unit'] }, { id: 'pos', label: 'POS', tables: ['posterminal', 'posterminalcategory', 'printer', 'labeltemplate'] }, + { + id: 'ai', + label: 'AI & Models', + tables: ['modelconversation', 'modelmessage', 'modelvfs'], + }, { id: 'attributes', label: 'Properties & Attributes', diff --git a/schema-graph.dot b/schema-graph.dot index 039e181..12eac50 100644 --- a/schema-graph.dot +++ b/schema-graph.dot @@ -11,15 +11,15 @@ digraph schema { fontcolor="#e2e8f0"; margin=14; meta [label=<
meta
keyNVARCHAR(255)
valueNVARCHAR(MAX)
>]; customergroup [label=<
customergroup
idINT PK
company_id?INT FK
nameNVARCHAR(255)
sortINT
updated_atDATETIME2
>]; + company [label=<
company
idINT PK
root_category_id?INT FK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
default_language_id?INT FK
poscustomergroup_idINT FK
nameNVARCHAR(255)
>]; numbers [label=<
numbers
idINT PK
company_idINT FK
typeNVARCHAR(100)
prefixNVARCHAR(50)
paddingINT
nextINT
>]; 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
>]; entity [label=<
entity
idINT PK
customergroup_idINT FK
first_name?NVARCHAR(255)
nameNVARCHAR(255)
number?NVARCHAR(100)
created_atDATETIME2
updated_atDATETIME2
>]; - companyuser [label=<
companyuser
idINT PK
company_idINT FK
google_auth_idINT FK
>]; + 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
>]; 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
>]; - company [label=<
company
idINT PK
poscustomergroup_idINT FK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
root_category_id?INT FK
default_language_id?INT FK
nameNVARCHAR(255)
>]; 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 { @@ -35,45 +35,45 @@ digraph schema { label="Catalog"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; + articletombstone [label=<
articletombstone
idINT PK
company_idINT FK
article_id?INT
sku?NVARCHAR(100)
name?NVARCHAR(255)
taxrate?DECIMAL(18,4)
net?FLOAT
deleted_atDATETIME2
>]; + brand [label=<
brand
idINT PK
company_idINT FK
nameNVARCHAR(255)
gsprpinfo?NVARCHAR(MAX)
>]; categoryarticle [label=<
categoryarticle
idINT PK
category_idINT FK
article_idINT FK
>]; category [label=<
category
idINT PK
parent_id?INT FK
sortINT
updated_atDATETIME2
>]; - articlehistory [label=<
articlehistory
idINT PK
companyuser_id?INT FK
article_idINT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; - article [label=<
article
idINT PK
company_idINT FK
fatherattributename_id?INT FK
fatherarticle_id?INT FK
articlelist_id?INT FK
optionslist_id?INT FK
brand_id?INT FK
unit_idINT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
track_stockBIT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; + articlehistory [label=<
articlehistory
idINT PK
article_idINT FK
companyuser_id?INT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; - articlesupplier [label=<
articlesupplier
idINT PK
entity_idINT FK
price_id?INT FK
article_idINT FK
sku?NVARCHAR(100)
stock?FLOAT
puFLOAT
sortINT
>]; + article [label=<
article
idINT PK
fatherattributename_id?INT FK
brand_id?INT FK
fatherarticle_id?INT FK
articlelist_id?INT FK
optionslist_id?INT FK
unit_idINT FK
company_idINT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
track_stockBIT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; + articlesupplier [label=<
articlesupplier
idINT PK
article_idINT FK
price_id?INT FK
entity_idINT FK
sku?NVARCHAR(100)
stock?FLOAT
puFLOAT
sortINT
>]; articlelist [label=<
articlelist
idINT PK
>]; articlelistarticle [label=<
articlelistarticle
idINT PK
article_idINT FK
articlelist_idINT FK
countINT
sortINT
>]; optionslist [label=<
optionslist
idINT PK
>]; optionslistarticle [label=<
optionslistarticle
idINT PK
article_idINT FK
optionslist_idINT FK
countINT
sortINT
mandatedBIT
>]; - brand [label=<
brand
idINT PK
company_idINT FK
nameNVARCHAR(255)
gsprpinfo?NVARCHAR(MAX)
>]; - articletombstone [label=<
articletombstone
idINT PK
company_idINT FK
article_id?INT
sku?NVARCHAR(100)
name?NVARCHAR(255)
taxrate?DECIMAL(18,4)
net?FLOAT
deleted_atDATETIME2
>]; } subgraph cluster_media { label="Media"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; pic [label=<
pic
idINT PK
wINT
hINT
sizeBIGINT
hashNVARCHAR(64)
thumbwINT
thumbhINT
thumbhashNVARCHAR(64)
sourceNVARCHAR(MAX)
fullbinaryVARBINARY(MAX)
thumbbinaryVARBINARY(MAX)
>]; - categorypic [label=<
categorypic
idINT PK
pic_idINT FK
category_idINT FK
sortINT
>]; - articlepic [label=<
articlepic
idINT PK
pic_idINT FK
article_idINT FK
sortINT
>]; - brandpic [label=<
brandpic
idINT PK
pic_idINT FK
brand_idINT FK
sortINT
>]; + brandpic [label=<
brandpic
idINT PK
brand_idINT FK
pic_idINT FK
sortINT
>]; + categorypic [label=<
categorypic
idINT PK
category_idINT FK
pic_idINT FK
sortINT
>]; + articlepic [label=<
articlepic
idINT PK
article_idINT FK
pic_idINT FK
sortINT
>]; file [label=<
file
idINT PK
hashNVARCHAR(64)
mimetypeNVARCHAR(255)
nameNVARCHAR(255)
sizeBIGINT
createdDATETIME2
modifiedDATETIME2
contentVARBINARY(MAX)
thumbavif?VARBINARY(MAX)
thumbx?INT
thumby?INT
meta?NVARCHAR(MAX)
>]; } subgraph cluster_manifests { label="Documents"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; - manifestarticle [label=<
manifestarticle
idINT PK
manifest_idINT FK
sku?NVARCHAR(100)
name?NVARCHAR(255)
puFLOAT
amountFLOAT
netFLOAT
comment?NVARCHAR(MAX)
rebateFLOAT
taxrateFLOAT
>]; - ledger [label=<
ledger
idINT PK
manifestarticle_idINT FK
warehouse_id?INT FK
transaction_typeNVARCHAR(100)
amountFLOAT
transaction_textNVARCHAR(MAX)
hash_versionINT
prev_hash?NVARCHAR(128)
created_atDATETIME2
>]; - manifest [label=<
manifest
idINT PK
entity_id?INT FK
company_idINT FK
numberNVARCHAR(100)
typeNVARCHAR(100)
external_number?NVARCHAR(100)
dateDATETIME2
created_atDATETIME2
>]; - manifestattachment [label=<
manifestattachment
idINT PK
manifest_idINT FK
file_idINT FK
sortINT
>]; warehouse [label=<
warehouse
idINT PK
company_idINT FK
nameNVARCHAR(255)
sortINT
>]; + manifestarticle [label=<
manifestarticle
idINT PK
manifest_idINT FK
sku?NVARCHAR(100)
name?NVARCHAR(255)
puFLOAT
amountFLOAT
netFLOAT
comment?NVARCHAR(MAX)
rebateFLOAT
taxrateFLOAT
>]; + manifest [label=<
manifest
idINT PK
company_idINT FK
entity_id?INT FK
numberNVARCHAR(100)
typeNVARCHAR(100)
external_number?NVARCHAR(100)
dateDATETIME2
created_atDATETIME2
>]; + ledger [label=<
ledger
idINT PK
warehouse_id?INT FK
manifestarticle_idINT FK
transaction_typeNVARCHAR(100)
amountFLOAT
transaction_textNVARCHAR(MAX)
hash_versionINT
prev_hash?NVARCHAR(128)
created_atDATETIME2
>]; + manifestattachment [label=<
manifestattachment
idINT PK
file_idINT FK
manifest_idINT FK
sortINT
>]; } 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
tax_idINT FK
article_idINT FK
minmultiplierDECIMAL(18,4)
netFLOAT
>]; + price [label=<
price
idINT PK
article_idINT FK
tax_idINT FK
customergroup_idINT FK
minmultiplierDECIMAL(18,4)
netFLOAT
>]; } subgraph cluster_units { label="Units"; labeljust="l"; fontsize=15; @@ -85,18 +85,26 @@ digraph schema { label="POS"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; - posterminalcategory [label=<
posterminalcategory
idINT PK
category_idINT FK
posterminal_idINT FK
>]; - posterminal [label=<
posterminal
idINT PK
company_idINT FK
nameNVARCHAR(255)
token?NVARCHAR(255)
pairingpinNVARCHAR(50)
paired_at?DATETIME2
clientserial?NVARCHAR(255)
maxknowntombstoneid?INT
>]; printer [label=<
printer
idINT PK
company_idINT FK
addressNVARCHAR(255)
typeNVARCHAR(50)
model?NVARCHAR(255)
serial?NVARCHAR(255)
sortINT
desc?NVARCHAR(MAX)
>]; labeltemplate [label=<
labeltemplate
idINT PK
company_idINT FK
nameNVARCHAR(255)
widthINT
heightINT
dpiINT
typeNVARCHAR(50)
contentNVARCHAR(MAX)
thumb?VARBINARY(MAX)
>]; + posterminal [label=<
posterminal
idINT PK
company_idINT FK
nameNVARCHAR(255)
token?NVARCHAR(255)
pairingpinNVARCHAR(50)
paired_at?DATETIME2
clientserial?NVARCHAR(255)
maxknowntombstoneid?INT
>]; + posterminalcategory [label=<
posterminalcategory
idINT PK
category_idINT FK
posterminal_idINT FK
>]; + } + subgraph cluster_ai { + label="AI & Models"; labeljust="l"; fontsize=15; + style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; + fontcolor="#e2e8f0"; margin=14; + modelmessage [label=<
modelmessage
idINT PK
conversation_idINT FK
created_atDATETIME2
roleNVARCHAR(20)
messageNVARCHAR(MAX)
>]; + modelconversation [label=<
modelconversation
idINT PK
companyuser_idINT FK
providerNVARCHAR(100)
modelNVARCHAR(255)
created_atDATETIME2
updated_atDATETIME2
stateNVARCHAR(20)
>]; + modelvfs [label=<
modelvfs
idINT PK
company_idINT FK
nameNVARCHAR(255)
mimeNVARCHAR(100)
contentVARBINARY(MAX)
metaNVARCHAR(MAX)
created_atDATETIME2
updated_atDATETIME2
>]; } subgraph cluster_attributes { label="Properties & Attributes"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; attributevalue [label=<
attributevalue
idINT PK
attribute_idINT FK
valueNVARCHAR(255)
sortINT
>]; - attributename [label=<
attributename
idINT PK
company_idINT FK
nameNVARCHAR(255)
sortINT
exclusiveBIT
>]; propertyname [label=<
propertyname
idINT PK
company_idINT FK
nameNVARCHAR(255)
sortINT
exclusiveBIT
>]; + attributename [label=<
attributename
idINT PK
company_idINT FK
nameNVARCHAR(255)
sortINT
exclusiveBIT
>]; categoryproperty [label=<
categoryproperty
idINT PK
property_idINT FK
category_idINT FK
valueNVARCHAR(MAX)
sortINT
>]; categoryattributevalue [label=<
categoryattributevalue
idINT PK
attributevalue_idINT FK
category_idINT FK
sortINT
>]; articleproperty [label=<
articleproperty
idINT PK
property_idINT FK
article_idINT FK
valueNVARCHAR(MAX)
sortINT
>]; @@ -105,85 +113,88 @@ digraph schema { __legend [label=<
Legend
PKprimary key
FKforeign key
?nullable column
—FOREIGN KEY constraint
- -naming convention only, no constraint
>]; - google_auth:selected_company:se -> company:in4:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_google_auth_selected_company", weight=3]; + google_auth:selected_company:w -> company:in16: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:e -> category:in0:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_company_root_category", weight=3]; - company:default_language_id:e -> language:in0:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_company_default_language", constraint=false, weight=1]; + company:root_category_id:e -> category:in7:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_company_root_category", weight=3]; + company:default_language_id:w -> language:in3:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_company_default_language", constraint=false, weight=1]; company:taxrate:e -> tax:in3:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_company_taxrate", constraint=false, weight=1]; company:reducedtaxrate:e -> tax:in1: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:w -> company:in0:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_customergroup_company", weight=3]; - company:poscustomergroup_id:w -> customergroup:in1:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_company_poscustomergroup", constraint=false, weight=1]; - entity:customergroup_id:w -> customergroup:in0:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_entity_customergroup", weight=3]; + customergroup:company_id:sw -> company:in7:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_customergroup_company", weight=3]; + company:poscustomergroup_id:nw -> customergroup:in2: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]; entityaddress:entity_id:ne -> entity:in0:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_entityaddress_entity", weight=3]; - entitycontact:entity_id:se -> entity:in1:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_entitycontact_entity", weight=3]; - entityhistory:entity_id:sw -> entity:in2:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_entityhistory_entity", weight=3]; + entitycontact:entity_id:sw -> entity:in1:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_entitycontact_entity", weight=3]; + entityhistory:entity_id:sw -> entity:in3:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_entityhistory_entity", weight=3]; entityhistory:companyuser_id:se -> companyuser:in0:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_entityhistory_companyuser", constraint=false, weight=1]; - unit:company_id:e -> company:in13:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_unit_company", weight=3]; - unittext:unit_id:sw -> unit:in1:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_unittext_unit", weight=3]; - unittext:language_id:w -> language:in3:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_unittext_language", constraint=false, weight=1]; - brand:company_id:w -> company:in11:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_brand_company", weight=3]; - article:company_id:w -> company:in10:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_article_company", weight=3]; - article:unit_id:e -> unit:in0:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_article_unit", constraint=false, weight=1]; - article:brand_id:e -> brand:in1:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_article_brand", constraint=false, weight=1]; + unit:company_id:w -> company:in5:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_unit_company", weight=3]; + unittext:unit_id:w -> unit:in1:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_unittext_unit", weight=3]; + unittext:language_id:w -> language:in2:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_unittext_language", constraint=false, weight=1]; + brand:company_id:e -> company:in3:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_brand_company", weight=3]; + article:company_id:e -> company:in4:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_article_company", weight=3]; + article:unit_id:w -> unit:in0:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_article_unit", constraint=false, weight=1]; + article:brand_id:e -> brand:in0:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_article_brand", constraint=false, weight=1]; article:articlelist_id:e -> articlelist:in0:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_article_articlelist", constraint=false, weight=1]; article:optionslist_id:e -> optionslist:in0:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_article_optionslist", constraint=false, weight=1]; article:fatherarticle_id:e -> article:id:e [color="#c79bff", fontcolor="#c79bff", tooltip="FK_article_fatherarticle", constraint=false]; article:fatherattributename_id:sw -> attributename:in2:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_article_fatherattributename", constraint=false, weight=1]; article:fatherattributename_id:sw -> attributename:in2:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_article_fatherattributename", constraint=false, weight=1]; - articlehistory:article_id:se -> article:in5:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_articlehistory_article", weight=3]; - articlehistory:companyuser_id:e -> companyuser:in1:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_articlehistory_companyuser", constraint=false, weight=1]; - articletombstone:company_id:e -> company:in12:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_articletombstone_company", weight=3]; - articleean:article_id:sw -> article:in7:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_articleean_article", weight=3]; - articlepic:article_id:se -> article:in3:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_articlepic_article", weight=3]; - articlepic:pic_id:w -> pic:in2:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_articlepic_pic", constraint=false, weight=1]; - brandpic:brand_id:e -> brand:in0:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_brandpic_brand", weight=3]; - brandpic:pic_id:w -> pic:in0:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_brandpic_pic", constraint=false, weight=1]; - articlelistarticle:articlelist_id:se -> articlelist:in1:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_articlelistarticle_articlelist", weight=3]; - articlelistarticle:article_id:e -> article:in10:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_articlelistarticle_article", constraint=false, weight=1]; - optionslistarticle:optionslist_id:sw -> optionslist:in1:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_optionslistarticle_optionslist", weight=3]; - optionslistarticle:article_id:e -> article:in9:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_optionslistarticle_article", constraint=false, weight=1]; - categoryarticle:category_id:se -> category:in4:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_categoryarticle_category", weight=3]; - categoryarticle:article_id:se -> article:in4:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_categoryarticle_article", constraint=false, weight=1]; - categorypic:category_id:sw -> category:in3:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_categorypic_category", weight=3]; - categorypic:pic_id:w -> pic:in1:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_categorypic_pic", constraint=false, weight=1]; - categorytext:category_id:e -> category:in6:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_categorytext_category", weight=3]; - categorytext:language_id:w -> language:in2:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_categorytext_language", constraint=false, weight=1]; - articletext:article_id:w -> article:in11:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_articletext_article", weight=3]; + articlehistory:article_id:se -> article:in3:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_articlehistory_article", weight=3]; + articlehistory:companyuser_id:w -> companyuser:in2:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_articlehistory_companyuser", constraint=false, weight=1]; + articletombstone:company_id:e -> company:in2:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_articletombstone_company", weight=3]; + articleean:article_id:se -> article:in6:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_articleean_article", weight=3]; + articlepic:article_id:se -> article:in4:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_articlepic_article", weight=3]; + articlepic:pic_id:w -> pic:in1:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_articlepic_pic", constraint=false, weight=1]; + brandpic:brand_id:w -> brand:in1:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_brandpic_brand", weight=3]; + brandpic:pic_id:e -> pic:in0:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_brandpic_pic", constraint=false, weight=1]; + articlelistarticle:articlelist_id:sw -> articlelist:in1:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_articlelistarticle_articlelist", weight=3]; + articlelistarticle:article_id:sw -> article:in8:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_articlelistarticle_article", constraint=false, weight=1]; + optionslistarticle:optionslist_id:se -> optionslist:in1:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_optionslistarticle_optionslist", weight=3]; + optionslistarticle:article_id:e -> article:in11:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_optionslistarticle_article", constraint=false, weight=1]; + categoryarticle:category_id:nw -> category:in4:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_categoryarticle_category", weight=3]; + categoryarticle:article_id:w -> article:in2:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_categoryarticle_article", constraint=false, weight=1]; + categorypic:category_id:w -> category:in2:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_categorypic_category", weight=3]; + categorypic:pic_id:w -> pic:in2:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_categorypic_pic", constraint=false, weight=1]; + categorytext:category_id:w -> category:in5:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_categorytext_category", weight=3]; + categorytext:language_id:e -> language:in0:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_categorytext_language", constraint=false, weight=1]; + articletext:article_id:w -> article:in9:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_articletext_article", weight=3]; articletext:language_id:e -> language:in1:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_articletext_language", constraint=false, weight=1]; price:tax_id:w -> tax:in0:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_price_tax", constraint=false, weight=1]; - price:article_id:e -> article:in2:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_price_article", weight=3]; - price:customergroup_id:e -> customergroup:in2:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_price_customergroup", constraint=false, weight=1]; - articlesupplier:article_id:sw -> article:in8:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_articlesupplier_article", weight=3]; - articlesupplier:entity_id:w -> entity:in4:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_articlesupplier_entity", constraint=false, weight=1]; - articlesupplier:price_id:w -> price:in0:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_articlesupplier_price", constraint=false, weight=1]; - manifest:company_id:se -> company:in6:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_manifest_company", weight=3]; - manifest:entity_id:w -> entity:in3:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_manifest_entity", constraint=false, weight=1]; + price:article_id:e -> article:in7:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_price_article", weight=3]; + price:customergroup_id:e -> customergroup:in0:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_price_customergroup", constraint=false, weight=1]; + articlesupplier:article_id:sw -> article:in10:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_articlesupplier_article", weight=3]; + articlesupplier:entity_id:e -> entity:in2:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_articlesupplier_entity", constraint=false, weight=1]; + articlesupplier:price_id:e -> price:in0:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_articlesupplier_price", constraint=false, weight=1]; + manifest:company_id:w -> company:in17:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_manifest_company", weight=3]; + manifest:entity_id:e -> entity:in4:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_manifest_entity", constraint=false, weight=1]; manifestarticle:manifest_id:sw -> manifest:in0:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_manifestarticle_manifest", weight=3]; - warehouse:company_id:sw -> company:in7:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_warehouse_company", weight=3]; + warehouse:company_id:w -> company:in15:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_warehouse_company", weight=3]; ledger:manifestarticle_id:sw -> manifestarticle:in0:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_ledger_manifestarticle", weight=3]; - ledger:warehouse_id:e -> warehouse:in0:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_ledger_warehouse", constraint=false, weight=1]; - numbers:company_id:e -> company:in1:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_numbers_company", weight=3]; + ledger:warehouse_id:w -> warehouse:in0:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_ledger_warehouse", constraint=false, weight=1]; + numbers:company_id:se -> company:in8:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_numbers_company", weight=3]; companyuser:google_auth_id:e -> google_auth:in0:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_companyuser_google_auth", constraint=false, weight=1]; - companyuser:company_id:w -> company:in3:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_companyuser_company", weight=3]; - syncpos:company_id:se -> company:in2:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_syncpos_company", weight=3]; - companysetting:company_id:nw -> company:in5:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_companysetting_company", weight=3]; - posterminal:company_id:w -> company:in14:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_posterminal_company", weight=3]; - posterminalcategory:posterminal_id:ne -> posterminal:in0:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_posterminalcategory_posterminal", weight=3]; - posterminalcategory:category_id:w -> category:in7:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_posterminalcategory_category", constraint=false, weight=1]; - printer:company_id:e -> company:in15:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_printer_company", weight=3]; - labeltemplate:company_id:w -> company:in16:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_labeltemplate_company", weight=3]; - manifestattachment:manifest_id:sw -> manifest:in1:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_manifestattachment_manifest", weight=3]; - manifestattachment:file_id:e -> file:in0:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_manifestattachment_file", constraint=false, weight=1]; - propertyname:company_id:sw -> company:in9:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_propertyname_company", weight=3]; - articleproperty:article_id:w -> article:in0:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_articleproperty_article", weight=3]; - articleproperty:property_id:w -> propertyname:in0:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_articleproperty_property", constraint=false, weight=1]; - attributename:company_id:w -> company:in8:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_attributename_company", weight=3]; - attributevalue:attribute_id:se -> attributename:in0:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_attributevalue_attribute", weight=3]; - articleattributevalue:article_id:e -> article:in1:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_articleattributevalue_article", weight=3]; - articleattributevalue:attributevalue_id:w -> attributevalue:in1:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_articleattributevalue_attributevalue", constraint=false, weight=1]; - categoryproperty:category_id:e -> category:in1:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_categoryproperty_category", weight=3]; - categoryproperty:property_id:w -> propertyname:in1:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_categoryproperty_property", constraint=false, weight=1]; - categoryattributevalue:category_id:e -> category:in2:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_categoryattributevalue_category", weight=3]; - categoryattributevalue:attributevalue_id:w -> attributevalue:in0:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_categoryattributevalue_attributevalue", constraint=false, weight=1]; + companyuser:company_id:sw -> company:in10:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_companyuser_company", weight=3]; + modelconversation:companyuser_id:w -> companyuser:in1:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_modelconversation_companyuser", weight=3]; + modelvfs:company_id:w -> company:in11:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_modelvfs_company", weight=3]; + modelmessage:conversation_id:se -> modelconversation:in0:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_modelmessage_conversation", weight=3]; + syncpos:company_id:e -> company:in13:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_syncpos_company", weight=3]; + companysetting:company_id:w -> company:in14:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_companysetting_company", weight=3]; + posterminal:company_id:se -> company:in12:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_posterminal_company", weight=3]; + posterminalcategory:posterminal_id:se -> posterminal:in0:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_posterminalcategory_posterminal", weight=3]; + posterminalcategory:category_id:w -> category:in6:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_posterminalcategory_category", constraint=false, weight=1]; + printer:company_id:se -> company:in6:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_printer_company", weight=3]; + labeltemplate:company_id:se -> company:in9:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_labeltemplate_company", weight=3]; + manifestattachment:manifest_id:sw -> manifest:in1:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_manifestattachment_manifest", weight=3]; + manifestattachment:file_id:w -> file:in0:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_manifestattachment_file", constraint=false, weight=1]; + propertyname:company_id:e -> company:in0:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_propertyname_company", weight=3]; + articleproperty:article_id:w -> article:in0:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_articleproperty_article", weight=3]; + articleproperty:property_id:w -> propertyname:in1:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_articleproperty_property", constraint=false, weight=1]; + attributename:company_id:e -> company:in1:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_attributename_company", weight=3]; + attributevalue:attribute_id:se -> attributename:in0:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_attributevalue_attribute", weight=3]; + articleattributevalue:article_id:w -> article:in1:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_articleattributevalue_article", weight=3]; + articleattributevalue:attributevalue_id:w -> attributevalue:in0:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_articleattributevalue_attributevalue", constraint=false, weight=1]; + categoryproperty:category_id:w -> category:in0:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_categoryproperty_category", weight=3]; + categoryproperty:property_id:w -> propertyname:in0:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_categoryproperty_property", constraint=false, weight=1]; + categoryattributevalue:category_id:w -> category:in1:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_categoryattributevalue_category", weight=3]; + categoryattributevalue:attributevalue_id:w -> attributevalue:in1:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_categoryattributevalue_attributevalue", constraint=false, weight=1]; } \ No newline at end of file diff --git a/schema-graph.svg b/schema-graph.svg index bbb5246..7e44c4d 100644 --- a/schema-graph.svg +++ b/schema-graph.svg @@ -4,687 +4,294 @@ - - + + schema - + cluster_identity - -Identity & Company + +Identity & Company cluster_i18n - -Localization + +Localization cluster_catalog - -Catalog + +Catalog cluster_media - -Media + +Media cluster_manifests - -Documents + +Documents cluster_commerce - -Pricing + +Pricing cluster_units - -Units + +Units cluster_pos - -POS + +POS +cluster_ai + +AI & Models + + cluster_attributes - -Properties & Attributes + +Properties & Attributes 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 - - -updated_at -DATETIME2 + + + + + +customergroup + + +id +INT +PK + + +company_id +? +INT +FK + + +name +NVARCHAR(255) + + +sort +INT + + +updated_at +DATETIME2 - + company - - - - - - - - - - - - - - - - - - - -company - - -id -INT -PK - - -poscustomergroup_id -INT -FK - - -taxrate -? -INT -FK - - -reducedtaxrate -? -INT -FK - - -taxfreerate -? -INT -FK - - -root_category_id -? -INT -FK - - -default_language_id -? -INT -FK - - -name -NVARCHAR(255) + + + + + + + + + + + + + + + + + + + + +company + + +id +INT +PK + + +root_category_id +? +INT +FK + + +taxrate +? +INT +FK + + +reducedtaxrate +? +INT +FK + + +taxfreerate +? +INT +FK + + +default_language_id +? +INT +FK + + +poscustomergroup_id +INT +FK + + +name +NVARCHAR(255) -customergroup:w->company:n +customergroup:sw->company:n - - - - - - - -numbers - - -numbers - - -id -INT -PK - - -company_id -INT -FK - - -type -NVARCHAR(100) - - -prefix -NVARCHAR(50) - - -padding -INT - - -next -INT - - - -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 - - - -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 - - - -entityaddress:ne->entity:n - - - - - - - - -entity:w->customergroup:n - - - - - - - - -companyuser - - - - -companyuser - - -id -INT -PK - - -company_id -INT -FK - - -google_auth_id -INT -FK - - - -companyuser:w->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:e->google_auth:n - - - - - - - - -entitycontact - - -entitycontact - - -id -INT -PK - - -entity_id -INT -FK - - -contacttype -NVARCHAR(50) - - -contact -NVARCHAR(255) - - -sort -INT - - - -entitycontact:se->entity:n - - - - - - - - -syncpos - - -syncpos - - -id -INT -PK - - -company_id -INT -FK - - -paircode -NVARCHAR(50) - - -token? -NVARCHAR(255) - - - -syncpos:se->company:n - - - - - - - - -entityhistory - - -entityhistory - - -id -INT -PK - - -entity_id -INT -FK - - -companyuser_id -? -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) - - - -entityhistory:sw->entity:n - - - - - - - - -entityhistory:se->companyuser:n - - - - - - - - -companysetting - - -companysetting - - -id -INT -PK - - -company_id -INT -FK - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) - - -frontend -BIT - - -writeonly -BIT - - - -companysetting:nw->company:n - - - + + -company:w->customergroup:n +company:nw->customergroup:n - - + + language - - - - - - -language - - -id -INT -PK - - -iso639 -NVARCHAR(10) + + + + + + +language + + +id +INT +PK + + +iso639 +NVARCHAR(10) -company:e->language:n +company:w->language:n - - + + - + category - - - - - - - - - - -category - - -id -INT -PK - - -parent_id -? -INT -FK - - -sort -INT - - -updated_at -DATETIME2 + + + + + + + + + + +category + + +id +INT +PK + + +parent_id +? +INT +FK + + +sort +INT + + +updated_at +DATETIME2 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 - - + + @@ -692,8 +299,8 @@ company:e->tax:n - - + + @@ -701,344 +308,830 @@ company:e->tax:n - - + + + + + + + +numbers + + +numbers + + +id +INT +PK + + +company_id +INT +FK + + +type +NVARCHAR(100) + + +prefix +NVARCHAR(50) + + +padding +INT + + +next +INT + + + +numbers: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) + + +sort +INT + + + +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 + + + +entityaddress:ne->entity:n + + + + + + + + +entity:nw->customergroup:n + + + + + + + + +companyuser + + + + + +companyuser + + +id +INT +PK + + +company_id +INT +FK + + +google_auth_id +INT +FK + + + +companyuser:sw->company:n + + + + + + + + +google_auth + + + +google_auth + + +id +INT +PK + + +selected_company +? +INT +FK + + +email +NVARCHAR(320) + + +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:sw->entity:n + + + + + + + + +syncpos + + +syncpos + + +id +INT +PK + + +company_id +INT +FK + + +paircode +NVARCHAR(50) + + +token? +NVARCHAR(255) + + + +syncpos:e->company:n + + + + + + + + +entityhistory + + +entityhistory + + +id +INT +PK + + +entity_id +INT +FK + + +companyuser_id +? +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) + + + +entityhistory:sw->entity:n + + + + + + + + +entityhistory:se->companyuser:n + + + + + + + + +companysetting + + +companysetting + + +id +INT +PK + + +company_id +INT +FK + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) + + +frontend +BIT + + +writeonly +BIT + + + +companysetting:w->company:n + + + -google_auth:se->company:n +google_auth:w->company:n - - + + categorytext - - -categorytext - - -id -INT -PK - - -category_id -INT -FK - - -language_id -INT -FK - - -name -NVARCHAR(255) - - -slug -NVARCHAR(255) - - -seo? -NVARCHAR(MAX) - - -desc? -NVARCHAR(MAX) + + +categorytext + + +id +INT +PK + + +category_id +INT +FK + + +language_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) -categorytext:w->language:n +categorytext:e->language:n - - + + -categorytext:e->category:n +categorytext:w->category:n - - + + articletext - - -articletext - - -id -INT -PK - - -article_id -INT -FK - - -language_id -INT -FK - - -name -NVARCHAR(255) - - -slug -NVARCHAR(255) - - -seo? -NVARCHAR(MAX) - - -desc? -NVARCHAR(MAX) + + +articletext + + +id +INT +PK + + +article_id +INT +FK + + +language_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) articletext:e->language:n - - + + - + article - - - - - - - - - - - - - - -article - - -id -INT -PK - - -company_id -INT -FK - - -fatherattributename_id -? -INT -FK - - -fatherarticle_id -? -INT -FK - - -articlelist_id -? -INT -FK - - -optionslist_id -? -INT -FK - - -brand_id -? -INT -FK - - -unit_id -INT -FK - - -sku? -NVARCHAR(100) - - -unitamount -FLOAT - - -weight -FLOAT - - -track_stock -BIT - - -is_father -BIT - - -fathersort -INT - - -created_at -DATETIME2 - - -updated_at -DATETIME2 + + + + + + + + + + + + + + +article + + +id +INT +PK + + +fatherattributename_id +? +INT +FK + + +brand_id +? +INT +FK + + +fatherarticle_id +? +INT +FK + + +articlelist_id +? +INT +FK + + +optionslist_id +? +INT +FK + + +unit_id +INT +FK + + +company_id +INT +FK + + +sku? +NVARCHAR(100) + + +unitamount +FLOAT + + +weight +FLOAT + + +track_stock +BIT + + +is_father +BIT + + +fathersort +INT + + +created_at +DATETIME2 + + +updated_at +DATETIME2 articletext:w->article:n - - + + unittext - - -unittext - - -id -INT -PK - - -language_id -INT -FK - - -unit_id -INT -FK - - -symbol -NVARCHAR(50) - - -name -NVARCHAR(255) + + +unittext + + +id +INT +PK + + +language_id +INT +FK + + +unit_id +INT +FK + + +symbol +NVARCHAR(50) + + +name +NVARCHAR(255) unittext:w->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 +unittext:w->unit:n - - + + + + + + + +articletombstone + + +articletombstone + + +id +INT +PK + + +company_id +INT +FK + + +article_id? +INT + + +sku? +NVARCHAR(100) + + +name? +NVARCHAR(255) + + +taxrate? +DECIMAL(18,4) + + +net? +FLOAT + + +deleted_at +DATETIME2 + + + +articletombstone:e->company:n + + + + + + + + +brand + + + + +brand + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +gsprpinfo? +NVARCHAR(MAX) + + + +brand:e->company:n + + + - + 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 +categoryarticle:nw->category:n - - + + -categoryarticle:se->article:n +categoryarticle:w->article:n - - + + @@ -1046,48 +1139,48 @@ 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 + + +article_id +INT +FK + + +companyuser_id +? +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) -articlehistory:e->companyuser:n +articlehistory:w->companyuser:n - - + + @@ -1095,17 +1188,56 @@ articlehistory:se->article:n - - + + + + + + + +articleean + + +articleean + + +id +INT +PK + + +article_id +INT +FK + + +ean +NVARCHAR(50) + + + +articleean:se->article:n + + + -article:w->company:n +article:e->company:n - - + + + + + + + +article:e->brand:n + + + @@ -1113,140 +1245,104 @@ article:e->article:e - - + + - + articlelist - - - - -articlelist - - -id -INT -PK + + + + +articlelist + + +id +INT +PK article:e->articlelist:n - - + + - + optionslist - - - - -optionslist - - -id -INT -PK + + + + +optionslist + + +id +INT +PK article:e->optionslist:n - - - - - - - -brand - - - - -brand - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -gsprpinfo? -NVARCHAR(MAX) - - - -article:e->brand:n - - - + + -article:e->unit:n +article:w->unit:n - - + + - + attributename - - - - - -attributename - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -sort -INT - - -exclusive -BIT + + + + + +attributename + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +sort +INT + + +exclusive +BIT article:sw->attributename:n - - + + @@ -1254,91 +1350,61 @@ article:sw->attributename:n - - - - - - - -articleean - - -articleean - - -id -INT -PK - - -article_id -INT -FK - - -ean -NVARCHAR(50) - - - -articleean:sw->article: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 - - -sort -INT + + +articlesupplier + + +id +INT +PK + + +article_id +INT +FK + + +price_id +? +INT +FK + + +entity_id +INT +FK + + +sku? +NVARCHAR(100) + + +stock? +FLOAT + + +pu +FLOAT + + +sort +INT -articlesupplier:w->entity:n +articlesupplier:e->entity:n - - + + @@ -1346,302 +1412,287 @@ articlesupplier:sw->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 + + + +price + + +id +INT +PK + + +article_id +INT +FK + + +tax_id +INT +FK + + +customergroup_id +INT +FK + + +minmultiplier +DECIMAL(18,4) + + +net +FLOAT -articlesupplier:w->price:n +articlesupplier:e->price: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:e->article:n +articlelistarticle:sw->article:n - - + + -articlelistarticle:se->articlelist:n +articlelistarticle:sw->articlelist:n - - + + - + optionslistarticle - - -optionslistarticle - - -id -INT -PK - - -article_id -INT -FK - - -optionslist_id -INT -FK - - -count -INT - - -sort -INT - - -mandated -BIT + + +optionslistarticle + + +id +INT +PK + + +article_id +INT +FK + + +optionslist_id +INT +FK + + +count +INT + + +sort +INT + + +mandated +BIT optionslistarticle:e->article:n - - + + -optionslistarticle:sw->optionslist:n +optionslistarticle:se->optionslist:n - - - - - - - -brand:w->company:n - - - - - - - - -articletombstone - - -articletombstone - - -id -INT -PK - - -company_id -INT -FK - - -article_id? -INT - - -sku? -NVARCHAR(100) - - -name? -NVARCHAR(255) - - -taxrate? -DECIMAL(18,4) - - -net? -FLOAT - - -deleted_at -DATETIME2 - - - -articletombstone:e->company: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 + + +brandpic + + +id +INT +PK + + +brand_id +INT +FK + + +pic_id +INT +FK + + +sort +INT + + + +brandpic:w->brand:n + + + + + + + + +brandpic:e->pic:n + + + + + - + categorypic - - -categorypic - - -id -INT -PK - - -pic_id -INT -FK - - -category_id -INT -FK - - -sort -INT + + +categorypic + + +id +INT +PK + + +category_id +INT +FK + + +pic_id +INT +FK + + +sort +INT -categorypic:sw->category:n +categorypic:w->category:n - - + + @@ -1649,43 +1700,43 @@ categorypic:w->pic:n - - + + - + articlepic - - -articlepic - - -id -INT -PK - - -pic_id -INT -FK - - -article_id -INT -FK - - -sort -INT + + +articlepic + + +id +INT +PK + + +article_id +INT +FK + + +pic_id +INT +FK + + +sort +INT articlepic:se->article:n - - + + @@ -1693,375 +1744,331 @@ articlepic:w->pic:n - - - - - - - -brandpic - - -brandpic - - -id -INT -PK - - -pic_id -INT -FK - - -brand_id -INT -FK - - -sort -INT - - - -brandpic:e->brand:n - - - - - - - - -brandpic:w->pic:n - - - + + file - - - -file - - -id -INT -PK - - -hash -NVARCHAR(64) - - -mimetype -NVARCHAR(255) - - -name -NVARCHAR(255) - - -size -BIGINT - - -created -DATETIME2 - - -modified -DATETIME2 - - -content -VARBINARY(MAX) - - -thumbavif? -VARBINARY(MAX) - - -thumbx? -INT - - -thumby? -INT - - -meta? -NVARCHAR(MAX) + + + +file + + +id +INT +PK + + +hash +NVARCHAR(64) + + +mimetype +NVARCHAR(255) + + +name +NVARCHAR(255) + + +size +BIGINT + + +created +DATETIME2 + + +modified +DATETIME2 + + +content +VARBINARY(MAX) + + +thumbavif? +VARBINARY(MAX) + + +thumbx? +INT + + +thumby? +INT + + +meta? +NVARCHAR(MAX) + + + +warehouse + + + +warehouse + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +sort +INT + + + +warehouse:w->company: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 -FLOAT + + + +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 +FLOAT 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 - - -created_at -DATETIME2 + + + + +manifest + + +id +INT +PK + + +company_id +INT +FK + + +entity_id +? +INT +FK + + +number +NVARCHAR(100) + + +type +NVARCHAR(100) + + +external_number? +NVARCHAR(100) + + +date +DATETIME2 + + +created_at +DATETIME2 manifestarticle:sw->manifest:n - - - - - - - -ledger - - -ledger - - -id -INT -PK - - -manifestarticle_id -INT -FK - - -warehouse_id -? -INT -FK - - -transaction_type -NVARCHAR(100) - - -amount -FLOAT - - -transaction_text -NVARCHAR(MAX) - - -hash_version -INT - - -prev_hash? -NVARCHAR(128) - - -created_at -DATETIME2 - - - -ledger:sw->manifestarticle:n - - - - - - - - -warehouse - - - -warehouse - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -sort -INT - - - -ledger:e->warehouse:n - - - - - - - - -manifest:w->entity:n - - - + + -manifest:se->company:n +manifest:w->company:n - - + + + + + + + +manifest:e->entity:n + + + + + + + + +ledger + + +ledger + + +id +INT +PK + + +warehouse_id +? +INT +FK + + +manifestarticle_id +INT +FK + + +transaction_type +NVARCHAR(100) + + +amount +FLOAT + + +transaction_text +NVARCHAR(MAX) + + +hash_version +INT + + +prev_hash? +NVARCHAR(128) + + +created_at +DATETIME2 + + + +ledger:w->warehouse:n + + + + + + + + +ledger:sw->manifestarticle:n + + + - + manifestattachment - - -manifestattachment - - -id -INT -PK - - -manifest_id -INT -FK - - -file_id -INT -FK - - -sort -INT + + +manifestattachment + + +id +INT +PK + + +file_id +INT +FK + + +manifest_id +INT +FK + + +sort +INT - -manifestattachment:e->file:n - - - + +manifestattachment:w->file:n + + + - + manifestattachment:sw->manifest:n - - - - - - - - -warehouse:sw->company:n - - - + + + @@ -2069,8 +2076,8 @@ price:e->customergroup:n - - + + @@ -2078,8 +2085,8 @@ price:e->article:n - - + + @@ -2087,511 +2094,646 @@ price:w->tax:n - - + + -unit:e->company:n +unit:w->company:n - - - - - - - -posterminalcategory - - -posterminalcategory - - -id -INT -PK - - -category_id -INT -FK - - -posterminal_id -INT -FK - - - -posterminalcategory:w->category:n - - - - - - - - -posterminal - - - -posterminal - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -token? -NVARCHAR(255) - - -pairingpin -NVARCHAR(50) - - -paired_at? -DATETIME2 - - -clientserial? -NVARCHAR(255) - - -maxknowntombstoneid? -INT - - - -posterminalcategory:ne->posterminal:n - - - - - - - - -posterminal:w->company:n - - - + + - + printer - - -printer - - -id -INT -PK - - -company_id -INT -FK - - -address -NVARCHAR(255) - - -type -NVARCHAR(50) - - -model? -NVARCHAR(255) - - -serial? -NVARCHAR(255) - - -sort -INT - - -desc? -NVARCHAR(MAX) + + +printer + + +id +INT +PK + + +company_id +INT +FK + + +address +NVARCHAR(255) + + +type +NVARCHAR(50) + + +model? +NVARCHAR(255) + + +serial? +NVARCHAR(255) + + +sort +INT + + +desc? +NVARCHAR(MAX) - -printer:e->company:n - - - + +printer:se->company:n + + + - + labeltemplate - - -labeltemplate - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -width -INT - - -height -INT - - -dpi -INT - - -type -NVARCHAR(50) - - -content -NVARCHAR(MAX) - - -thumb? -VARBINARY(MAX) + + +labeltemplate + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +width +INT + + +height +INT + + +dpi +INT + + +type +NVARCHAR(50) + + +content +NVARCHAR(MAX) + + +thumb? +VARBINARY(MAX) + +labeltemplate:se->company:n + + + + + + + + +posterminal + + + +posterminal + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +token? +NVARCHAR(255) + + +pairingpin +NVARCHAR(50) + + +paired_at? +DATETIME2 + + +clientserial? +NVARCHAR(255) + + +maxknowntombstoneid? +INT + + + +posterminal:se->company:n + + + + + + + + +posterminalcategory + + +posterminalcategory + + +id +INT +PK + + +category_id +INT +FK + + +posterminal_id +INT +FK + + + +posterminalcategory:w->category:n + + + + + + + -labeltemplate:w->company:n - - - +posterminalcategory:se->posterminal:n + + + + + + + + +modelmessage + + +modelmessage + + +id +INT +PK + + +conversation_id +INT +FK + + +created_at +DATETIME2 + + +role +NVARCHAR(20) + + +message +NVARCHAR(MAX) + + + +modelconversation + + + +modelconversation + + +id +INT +PK + + +companyuser_id +INT +FK + + +provider +NVARCHAR(100) + + +model +NVARCHAR(255) + + +created_at +DATETIME2 + + +updated_at +DATETIME2 + + +state +NVARCHAR(20) + + + +modelmessage:se->modelconversation:n + + + + + + + + +modelconversation:w->companyuser:n + + + + + + + + +modelvfs + + +modelvfs + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +mime +NVARCHAR(100) + + +content +VARBINARY(MAX) + + +meta +NVARCHAR(MAX) + + +created_at +DATETIME2 + + +updated_at +DATETIME2 + + + +modelvfs:w->company:n + + + - + attributevalue - - - - -attributevalue - - -id -INT -PK - - -attribute_id -INT -FK - - -value -NVARCHAR(255) - - -sort -INT + + + + +attributevalue + + +id +INT +PK + + +attribute_id +INT +FK + + +value +NVARCHAR(255) + + +sort +INT - + attributevalue:se->attributename:n - - - - - - - - -attributename:w->company:n - - - + + + - + propertyname - - - - -propertyname - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -sort -INT - - -exclusive -BIT + + + + +propertyname + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +sort +INT + + +exclusive +BIT - -propertyname:sw->company:n - - - + +propertyname:e->company:n + + + + + + + + +attributename:e->company:n + + + - + categoryproperty - - -categoryproperty - - -id -INT -PK - - -property_id -INT -FK - - -category_id -INT -FK - - -value -NVARCHAR(MAX) - - -sort -INT + + +categoryproperty + + +id +INT +PK + + +property_id +INT +FK + + +category_id +INT +FK + + +value +NVARCHAR(MAX) + + +sort +INT - -categoryproperty:e->category:n - - - + +categoryproperty:w->category:n + + + - + categoryproperty:w->propertyname:n - - - + + + - + categoryattributevalue - - -categoryattributevalue - - -id -INT -PK - - -attributevalue_id -INT -FK - - -category_id -INT -FK - - -sort -INT + + +categoryattributevalue + + +id +INT +PK + + +attributevalue_id +INT +FK + + +category_id +INT +FK + + +sort +INT - -categoryattributevalue:e->category:n - - - + +categoryattributevalue:w->category:n + + + - + categoryattributevalue:w->attributevalue:n - - - + + + - + articleproperty - - -articleproperty - - -id -INT -PK - - -property_id -INT -FK - - -article_id -INT -FK - - -value -NVARCHAR(MAX) - - -sort -INT + + +articleproperty + + +id +INT +PK + + +property_id +INT +FK + + +article_id +INT +FK + + +value +NVARCHAR(MAX) + + +sort +INT - + articleproperty:w->article:n - - - + + + - + articleproperty:w->propertyname:n - - - + + + - + articleattributevalue - - -articleattributevalue - - -id -INT -PK - - -attributevalue_id -INT -FK - - -article_id -INT -FK - - -sort -INT + + +articleattributevalue + + +id +INT +PK + + +attributevalue_id +INT +FK + + +article_id +INT +FK + + +sort +INT - -articleattributevalue:e->article:n - - - + +articleattributevalue:w->article:n + + + - + articleattributevalue:w->attributevalue: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-modelconversation.js b/scripts/test-modelconversation.js new file mode 100644 index 0000000..a7751a0 --- /dev/null +++ b/scripts/test-modelconversation.js @@ -0,0 +1,87 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import LibDashStore from '../src/index.js'; +import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js'; + +async function run() { + console.log('--- Testing store.modelconversation / store.modelmessage ---'); + + const tempDir = mkdtempSync(join(tmpdir(), 'test-modelconv-')); + const provider = new BetterSqlite3Provider({ + filename: join(tempDir, 'test.db'), + storageDir: join(tempDir, 'storage'), + }); + await provider.initialize(); + const store = await new LibDashStore(provider); + + try { + const userA = await store.google_auth.add('mc-a@example.com'); + const company = await store.company.create({ + name: 'MC Corp', iso639: 'de', taxrate: 19, reducedtaxrate: 7, googleAuthId: userA, + }); + await store.google_auth.setSelectedCompany(userA, company.id); + + const conv = await store.modelconversation.create(userA, { + provider: 'openrouter', model: 'test/model', + }); + assert.equal(conv.state, 'idle'); + assert.equal(conv.provider, 'openrouter'); + await assert.rejects(store.modelconversation.create(userA, { model: 'x' }), /Provider required/); + await assert.rejects(store.modelconversation.create(userA, { provider: 'x' }), /Model required/); + + const list = await store.modelconversation.list(userA); + assert.equal(list.length, 1); + + assert.deepEqual(await store.modelmessage.list(userA, conv.id), []); + + const m1 = await store.modelmessage.add(userA, conv.id, { message: 'hello' }); + assert.equal(m1.role, 'user'); + assert.equal(m1.conversation_id, conv.id); + await assert.rejects(store.modelmessage.add(userA, conv.id, { message: ' ' }), /Message required/); + await assert.rejects(store.modelmessage.add(userA, 9999, { message: 'x' }), /Conversation not found/); + + // Non-idle conversation refuses user messages but allows assistant messages + const busyConv = await store.modelconversation.setState(userA, conv.id, 'busy'); + assert.equal(busyConv.state, 'busy'); + const fetchedBusy = await store.modelconversation.get(userA, conv.id); + assert.equal(fetchedBusy.state, 'busy'); + + await assert.rejects( + store.modelmessage.add(userA, conv.id, { message: 'again' }), + /Conversation is not idle/, + ); + + // Assistant message can be added while busy + const mAssistant = await store.modelmessage.add(userA, conv.id, { message: 'echo: hello', role: 'assistant' }); + assert.equal(mAssistant.role, 'assistant'); + assert.equal(mAssistant.message, 'echo: hello'); + + // Back to idle + await store.modelconversation.setState(userA, conv.id, 'idle'); + await store.modelmessage.add(userA, conv.id, { message: 'second' }); + + const msgs = await store.modelmessage.list(userA, conv.id); + assert.deepEqual(msgs.map((m) => m.message), ['hello', 'echo: hello', 'second']); + assert.deepEqual(msgs.map((m) => m.role), ['user', 'assistant', 'user']); + + // Other user cannot see or write + const userB = await store.google_auth.add('mc-b@example.com'); + await store.companyuser.add(company.id, userB); + await store.google_auth.setSelectedCompany(userB, company.id); + assert.equal((await store.modelconversation.list(userB)).length, 0); + await assert.rejects(store.modelmessage.list(userB, conv.id), /Conversation not found/); + await assert.rejects(store.modelmessage.add(userB, conv.id, { message: 'x' }), /Conversation not found/); + + console.log('PASS'); + } finally { + await provider.close?.(); + rmSync(tempDir, { recursive: true, force: true }); + } +} + +run().catch((err) => { + console.error('FAIL', err); + process.exit(1); +}); diff --git a/src/index.js b/src/index.js index 40bcd3f..c182da6 100644 --- a/src/index.js +++ b/src/index.js @@ -156,6 +156,19 @@ import printerList from './store/printer/list.js'; import printerUpdate from './store/printer/update.js'; import printerDelete from './store/printer/delete.js'; import printerOrder from './store/printer/order.js'; +import modelconversationCreate from './store/modelconversation/create.js'; +import modelconversationGet from './store/modelconversation/get.js'; +import modelconversationList from './store/modelconversation/list.js'; +import modelconversationSetState from './store/modelconversation/setState.js'; +import modelconversationDelete from './store/modelconversation/delete.js'; +import modelmessageList from './store/modelmessage/list.js'; +import modelmessageAdd from './store/modelmessage/add.js'; +import modelvfsWrite from './store/modelvfs/write.js'; +import modelvfsRead from './store/modelvfs/read.js'; +import modelvfsList from './store/modelvfs/list.js'; +import modelvfsDelete from './store/modelvfs/delete.js'; +import modelvfsReset from './store/modelvfs/reset.js'; + import labeltemplateCreate from './store/labeltemplate/create.js'; import labeltemplateGetById from './store/labeltemplate/getById.js'; import labeltemplateList from './store/labeltemplate/list.js'; @@ -449,6 +462,27 @@ export default class LibDashStore { order: printerOrder(provider), }; + this.modelconversation = { + create: modelconversationCreate(provider), + get: modelconversationGet(provider), + list: modelconversationList(provider), + setState: modelconversationSetState(provider), + delete: modelconversationDelete(provider), + }; + + + this.modelvfs = { + write: modelvfsWrite(provider), + read: modelvfsRead(provider), + list: modelvfsList(provider), + delete: modelvfsDelete(provider), + reset: modelvfsReset(provider), + }; + this.modelmessage = { + list: modelmessageList(provider), + add: modelmessageAdd(provider), + }; + this.labeltemplate = { create: labeltemplateCreate(provider), getById: labeltemplateGetById(provider), diff --git a/src/providers/mssql/purgeSql.js b/src/providers/mssql/purgeSql.js index d4073b9..d7b34ed 100644 --- a/src/providers/mssql/purgeSql.js +++ b/src/providers/mssql/purgeSql.js @@ -8,6 +8,8 @@ export const TABLES = [ 'manifestattachment', 'labeltemplate', 'file', + 'modelmessage', + 'modelconversation', 'companyuser', 'syncpos', 'companysetting', diff --git a/src/providers/mssql/queries/modelconversation.js b/src/providers/mssql/queries/modelconversation.js new file mode 100644 index 0000000..3930cde --- /dev/null +++ b/src/providers/mssql/queries/modelconversation.js @@ -0,0 +1,79 @@ +export const MODELCONVERSATION_COLUMNS = ` + id, + companyuser_id, + provider, + model, + created_at, + updated_at, + state +`; + +export const LIST_BY_COMPANYUSER = ` +SELECT +${MODELCONVERSATION_COLUMNS} +FROM modelconversation +WHERE companyuser_id = @companyuserId +ORDER BY updated_at DESC, id DESC +`; + +export const GET_BY_COMPANYUSER_AND_ID = ` +SELECT +${MODELCONVERSATION_COLUMNS} +FROM modelconversation +WHERE id = @id AND companyuser_id = @companyuserId +`; + +export const CREATE = ` +INSERT INTO modelconversation ( + companyuser_id, + provider, + model +) +OUTPUT + INSERTED.id, + INSERTED.companyuser_id, + INSERTED.provider, + INSERTED.model, + INSERTED.created_at, + INSERTED.updated_at, + INSERTED.state +VALUES ( + @companyuserId, + @provider, + @model +) +`; + +/** Atomic idle guard + touch: affects 0 rows unless the conversation is idle. */ +export const TOUCH_IF_IDLE = ` +UPDATE modelconversation +SET updated_at = SYSUTCDATETIME() +WHERE id = @id AND companyuser_id = @companyuserId AND state = 'idle' +`; + +/** Unconditional touch (used when appending assistant/system messages or updating activity). */ +export const TOUCH = ` +UPDATE modelconversation +SET updated_at = SYSUTCDATETIME() +WHERE id = @id AND companyuser_id = @companyuserId +`; + +/** Updates conversation state ('idle' | 'busy') and touches updated_at. */ +export const SET_STATE = ` +UPDATE modelconversation +SET state = @state, updated_at = SYSUTCDATETIME() +OUTPUT + INSERTED.id, + INSERTED.companyuser_id, + INSERTED.provider, + INSERTED.model, + INSERTED.created_at, + INSERTED.updated_at, + INSERTED.state +WHERE id = @id AND companyuser_id = @companyuserId +`; + +export const DELETE_BY_COMPANYUSER_AND_ID = ` +DELETE FROM modelconversation +WHERE id = @id AND companyuser_id = @companyuserId +`; diff --git a/src/providers/mssql/queries/modelmessage.js b/src/providers/mssql/queries/modelmessage.js new file mode 100644 index 0000000..2681cdc --- /dev/null +++ b/src/providers/mssql/queries/modelmessage.js @@ -0,0 +1,34 @@ +export const MODELMESSAGE_COLUMNS = ` + id, + conversation_id, + created_at, + role, + message +`; + +export const LIST_BY_CONVERSATION = ` +SELECT +${MODELMESSAGE_COLUMNS} +FROM modelmessage +WHERE conversation_id = @conversationId +ORDER BY id ASC +`; + +export const CREATE = ` +INSERT INTO modelmessage ( + conversation_id, + role, + message +) +OUTPUT + INSERTED.id, + INSERTED.conversation_id, + INSERTED.created_at, + INSERTED.role, + INSERTED.message +VALUES ( + @conversationId, + @role, + @message +) +`; diff --git a/src/providers/mssql/queries/modelvfs.js b/src/providers/mssql/queries/modelvfs.js new file mode 100644 index 0000000..c243a14 --- /dev/null +++ b/src/providers/mssql/queries/modelvfs.js @@ -0,0 +1,63 @@ +export const MODELVFS_COLUMNS = ` + id, + company_id, + name, + mime, + meta, + created_at, + updated_at +`; + +export const GET_BY_COMPANY_AND_NAME = ` +SELECT +${MODELVFS_COLUMNS}, +content +FROM modelvfs +WHERE company_id = @companyId AND name = @name +`; + +export const GET_META_BY_COMPANY_AND_NAME = ` +SELECT +${MODELVFS_COLUMNS} +FROM modelvfs +WHERE company_id = @companyId AND name = @name +`; + +export const LIST_BY_COMPANY = ` +SELECT +${MODELVFS_COLUMNS} +FROM modelvfs +WHERE company_id = @companyId +ORDER BY name ASC +`; + +export const UPSERT = ` +MERGE modelvfs AS target +USING (SELECT @companyId AS company_id, @name AS name) AS src + ON target.company_id = src.company_id AND target.name = src.name +WHEN MATCHED THEN + UPDATE SET mime = @mime, content = @content, meta = @meta, updated_at = SYSUTCDATETIME() +WHEN NOT MATCHED THEN + INSERT (company_id, name, mime, content, meta) + VALUES (@companyId, @name, @mime, @content, @meta) +OUTPUT + INSERTED.id, + INSERTED.company_id, + INSERTED.name, + INSERTED.mime, + INSERTED.meta, + INSERTED.created_at, + INSERTED.updated_at; +`; + +export const DELETE_BY_COMPANY_AND_NAME = ` +DELETE FROM modelvfs +OUTPUT DELETED.id +WHERE company_id = @companyId AND name = @name +`; + +export const DELETE_ALL_BY_COMPANY = ` +DELETE FROM modelvfs +OUTPUT DELETED.id +WHERE company_id = @companyId +`; diff --git a/src/providers/mssql/schema.js b/src/providers/mssql/schema.js index 2ee7f0f..a6ebc73 100644 --- a/src/providers/mssql/schema.js +++ b/src/providers/mssql/schema.js @@ -771,6 +771,60 @@ BEGIN END `; +export const INIT_MODELCONVERSATION = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'modelconversation') +BEGIN + CREATE TABLE modelconversation ( + id INT IDENTITY(1,1) PRIMARY KEY, + companyuser_id INT NOT NULL, + provider NVARCHAR(100) NOT NULL, + model NVARCHAR(255) NOT NULL, + created_at DATETIME2 NOT NULL CONSTRAINT DF_modelconversation_created_at DEFAULT SYSUTCDATETIME(), + updated_at DATETIME2 NOT NULL CONSTRAINT DF_modelconversation_updated_at DEFAULT SYSUTCDATETIME(), + state NVARCHAR(20) NOT NULL CONSTRAINT DF_modelconversation_state DEFAULT 'idle', + CONSTRAINT FK_modelconversation_companyuser FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE CASCADE, + CONSTRAINT CK_modelconversation_state CHECK (state IN ('idle', 'busy')) + ); + CREATE INDEX IX_modelconversation_companyuser ON modelconversation (companyuser_id, updated_at); +END +`; + + +export const INIT_MODELVFS = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'modelvfs') +BEGIN + CREATE TABLE modelvfs ( + id INT IDENTITY(1,1) PRIMARY KEY, + company_id INT NOT NULL, + name NVARCHAR(255) NOT NULL, + mime NVARCHAR(100) NOT NULL, + content VARBINARY(MAX) NOT NULL, + meta NVARCHAR(MAX), + created_at DATETIME2 NOT NULL CONSTRAINT DF_modelvfs_created_at DEFAULT SYSUTCDATETIME(), + updated_at DATETIME2 NOT NULL CONSTRAINT DF_modelvfs_updated_at DEFAULT SYSUTCDATETIME(), + CONSTRAINT FK_modelvfs_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE, + CONSTRAINT UQ_modelvfs_company_name UNIQUE (company_id, name) + ); + CREATE INDEX IX_modelvfs_company_name ON modelvfs (company_id, name); +END +`; + +export const INIT_MODELMESSAGE = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'modelmessage') +BEGIN + CREATE TABLE modelmessage ( + id INT IDENTITY(1,1) PRIMARY KEY, + conversation_id INT NOT NULL, + created_at DATETIME2 NOT NULL CONSTRAINT DF_modelmessage_created_at DEFAULT SYSUTCDATETIME(), + role NVARCHAR(20) NOT NULL, + message NVARCHAR(MAX) NOT NULL, + CONSTRAINT FK_modelmessage_conversation FOREIGN KEY (conversation_id) REFERENCES modelconversation(id) ON DELETE CASCADE, + CONSTRAINT CK_modelmessage_role CHECK (role IN ('system', 'user', 'assistant', 'tool', 'reasoning')) + ); + CREATE INDEX IX_modelmessage_conversation ON modelmessage (conversation_id, id); +END +`; + export const INIT_SYNCPOS = ` IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'syncpos') BEGIN @@ -1087,6 +1141,9 @@ export const SCHEMA_SCRIPTS = [ INIT_GET_NEXT_NUMBER_WITH_PREFIX_AND_PADDING, INIT_PEEK_NEXT_NUMBER_WITH_PREFIX_AND_PADDING, INIT_COMPANYUSER, + INIT_MODELCONVERSATION, + INIT_MODELMESSAGE, + INIT_MODELVFS, INIT_ARTICLEHISTORY, INIT_ARTICLETOMBSTONE, INIT_ENTITYHISTORY, diff --git a/src/providers/sqlite3/index.js b/src/providers/sqlite3/index.js index 3f1e1c9..22b9770 100644 --- a/src/providers/sqlite3/index.js +++ b/src/providers/sqlite3/index.js @@ -180,6 +180,31 @@ export class BetterSqlite3Executor { await safeDeleteByHash(join(this.storageDir, 'thumbavif'), hash); } + + async writeVfsContent(id, buffer) { + if (!this.storageDir || !id) return; + await safeWrite(join(this.storageDir, 'vfs', String(id)), buffer); + } + + async readVfsContent(id) { + if (!this.storageDir || !id) return null; + try { + return await fsPromises.readFile(join(this.storageDir, 'vfs', String(id))); + } catch (err) { + if (err.code === 'ENOENT') return null; + throw err; + } + } + + async deleteVfsContent(id) { + if (!this.storageDir || !id) return; + try { + await fsPromises.unlink(join(this.storageDir, 'vfs', String(id))); + } catch (err) { + if (err.code !== 'ENOENT') throw err; + } + } + async deletePic(hash) { if (!this.storageDir || !hash) return; await safeDeleteByHash(join(this.storageDir, 'fullbinary'), hash); @@ -188,7 +213,7 @@ export class BetterSqlite3Executor { async purgeFiles() { if (!this.storageDir) return; - for (const sub of ['content', 'thumbavif', 'fullbinary', 'thumbbinary']) { + for (const sub of ['content', 'thumbavif', 'fullbinary', 'thumbbinary', 'vfs']) { const dir = join(this.storageDir, sub); try { const files = await fsPromises.readdir(dir); @@ -276,7 +301,7 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor { super(db, storageDir); if (this.storageDir) { - for (const sub of ['content', 'thumbavif', 'fullbinary', 'thumbbinary']) { + for (const sub of ['content', 'thumbavif', 'fullbinary', 'thumbbinary', 'vfs']) { mkdirSync(join(this.storageDir, sub), { recursive: true }); } } @@ -316,6 +341,9 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor { runner.readPicFull = executor.readPicFull.bind(executor); runner.writePicThumb = executor.writePicThumb.bind(executor); runner.readPicThumb = executor.readPicThumb.bind(executor); + runner.writeVfsContent = executor.writeVfsContent.bind(executor); + runner.readVfsContent = executor.readVfsContent.bind(executor); + runner.deleteVfsContent = executor.deleteVfsContent.bind(executor); runner.deleteFile = executor.deleteFile.bind(executor); runner.deletePic = executor.deletePic.bind(executor); runner.purgeFiles = executor.purgeFiles.bind(executor); diff --git a/src/providers/sqlite3/queries/modelconversation.js b/src/providers/sqlite3/queries/modelconversation.js new file mode 100644 index 0000000..1aa173c --- /dev/null +++ b/src/providers/sqlite3/queries/modelconversation.js @@ -0,0 +1,14 @@ +import * as source from '../../mssql/queries/modelconversation.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'modelconversation'); +export const { + CREATE, + DELETE_BY_COMPANYUSER_AND_ID, + GET_BY_COMPANYUSER_AND_ID, + LIST_BY_COMPANYUSER, + MODELCONVERSATION_COLUMNS, + SET_STATE, + TOUCH, + TOUCH_IF_IDLE, +} = queries; diff --git a/src/providers/sqlite3/queries/modelmessage.js b/src/providers/sqlite3/queries/modelmessage.js new file mode 100644 index 0000000..1d6b4a6 --- /dev/null +++ b/src/providers/sqlite3/queries/modelmessage.js @@ -0,0 +1,5 @@ +import * as source from '../../mssql/queries/modelmessage.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'modelmessage'); +export const { CREATE, LIST_BY_CONVERSATION } = queries; diff --git a/src/providers/sqlite3/queries/modelvfs.js b/src/providers/sqlite3/queries/modelvfs.js new file mode 100644 index 0000000..7c068de --- /dev/null +++ b/src/providers/sqlite3/queries/modelvfs.js @@ -0,0 +1,38 @@ +import * as source from '../../mssql/queries/modelvfs.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'modelvfs'); +export const { + MODELVFS_COLUMNS, + GET_META_BY_COMPANY_AND_NAME, + LIST_BY_COMPANY, +} = queries; + +export const GET_BY_COMPANY_AND_NAME = ` +SELECT +${MODELVFS_COLUMNS} +FROM modelvfs +WHERE company_id = @companyId AND name = @name +`; + +export const UPSERT = ` +INSERT INTO modelvfs (company_id, name, mime, meta) +VALUES (@companyId, @name, @mime, @meta) +ON CONFLICT (company_id, name) DO UPDATE SET + mime = excluded.mime, + meta = excluded.meta, + updated_at = (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +RETURNING id, company_id, name, mime, meta, created_at, updated_at +`; + +export const DELETE_BY_COMPANY_AND_NAME = ` +DELETE FROM modelvfs +WHERE company_id = @companyId AND name = @name +RETURNING id +`; + +export const DELETE_ALL_BY_COMPANY = ` +DELETE FROM modelvfs +WHERE company_id = @companyId +RETURNING id +`; diff --git a/src/providers/sqlite3/schema.js b/src/providers/sqlite3/schema.js index d9c6413..e0a8270 100644 --- a/src/providers/sqlite3/schema.js +++ b/src/providers/sqlite3/schema.js @@ -483,6 +483,48 @@ CREATE TABLE IF NOT EXISTS companyuser ( ); `; +export const INIT_MODELCONVERSATION = ` +CREATE TABLE IF NOT EXISTS modelconversation ( + id INTEGER PRIMARY KEY, + companyuser_id INTEGER NOT NULL, + provider TEXT NOT NULL, + model TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (${UTC_NOW}), + updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}), + state TEXT NOT NULL DEFAULT 'idle' CHECK (state IN ('idle', 'busy')), + FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS IX_modelconversation_companyuser ON modelconversation (companyuser_id, updated_at); +`; + + +export const INIT_MODELVFS = ` +CREATE TABLE IF NOT EXISTS modelvfs ( + id INTEGER PRIMARY KEY, + company_id INTEGER NOT NULL, + name TEXT NOT NULL, + mime TEXT NOT NULL, + meta TEXT, + created_at TEXT NOT NULL DEFAULT (${UTC_NOW}), + updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}), + FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE, + UNIQUE (company_id, name) +); +CREATE INDEX IF NOT EXISTS IX_modelvfs_company_name ON modelvfs (company_id, name); +`; + +export const INIT_MODELMESSAGE = ` +CREATE TABLE IF NOT EXISTS modelmessage ( + id INTEGER PRIMARY KEY, + conversation_id INTEGER NOT NULL, + created_at TEXT NOT NULL DEFAULT (${UTC_NOW}), + role TEXT NOT NULL CHECK (role IN ('system', 'user', 'assistant', 'tool', 'reasoning')), + message TEXT NOT NULL, + FOREIGN KEY (conversation_id) REFERENCES modelconversation(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS IX_modelmessage_conversation ON modelmessage (conversation_id, id); +`; + export const INIT_ARTICLEHISTORY = ` CREATE TABLE IF NOT EXISTS articlehistory ( id INTEGER PRIMARY KEY, @@ -770,6 +812,9 @@ export const SCHEMA_SCRIPTS = [ INIT_LEDGER, INIT_NUMBERS, INIT_COMPANYUSER, + INIT_MODELCONVERSATION, + INIT_MODELMESSAGE, + INIT_MODELVFS, INIT_ARTICLEHISTORY, INIT_ARTICLETOMBSTONE, INIT_ENTITYHISTORY, diff --git a/src/store/article/createUnderParent.js b/src/store/article/createUnderParent.js index eb7f3e8..ab9116b 100644 --- a/src/store/article/createUnderParent.js +++ b/src/store/article/createUnderParent.js @@ -41,7 +41,7 @@ export default function createUnderParent(provider) { * const article = await store.article.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Cola' }); * // => { id, parentId, name, sku, track_stock } */ - return async ({ googleAuthId, parentId, name, track_stock, trackStock }, tx) => { + return async ({ googleAuthId, parentId, name, track_stock, trackStock, sku: inputSku }, tx) => { const rawTrack = track_stock ?? trackStock; const finalTrackStock = rawTrack == null ? 1 : (rawTrack ? 1 : 0); @@ -53,7 +53,7 @@ export default function createUnderParent(provider) { ); const unitId = await ensureUnit(runner, languageId, companyId); - const sku = await allocateSku(companyId, runner); + const sku = (typeof inputSku === 'string' && inputSku.trim()) ? inputSku.trim() : await allocateSku(companyId, runner); const slug = await resolveSlug(runner, { companyId, languageId, name }); let article; diff --git a/src/store/modelconversation/create.js b/src/store/modelconversation/create.js new file mode 100644 index 0000000..f146b08 --- /dev/null +++ b/src/store/modelconversation/create.js @@ -0,0 +1,42 @@ +import { CREATE as MSSQL_CREATE } from '../../providers/mssql/queries/modelconversation.js'; +import { CREATE as SQLITE_CREATE } from '../../providers/sqlite3/queries/modelconversation.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; +import shape from './shape.js'; + +export default function create(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = provider?.dialect === 'sqlite3' ? SQLITE_CREATE : MSSQL_CREATE; + + /** + * Creates a new (idle) model conversation for the user in the selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - Conversation input. + * @param {string} input.provider - LLM provider name. + * @param {string} input.model - Model identifier. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>} + * @throws {Error} 'Provider required' or 'Model required'. + * + * @example + * const c = await store.modelconversation.create(42, { provider: 'openrouter', model: 'openai/gpt-5' }); + */ + return async (googleAuthId, input = {}, tx) => { + const providerName = + typeof input.provider === 'string' ? input.provider.trim() : ''; + if (!providerName) throw new Error('Provider required'); + const model = typeof input.model === 'string' ? input.model.trim() : ''; + if (!model) throw new Error('Model required'); + + const runner = makeRunner(provider, tx); + const { companyuserId } = await access(googleAuthId, runner); + + const result = await runner.query(sql, { + companyuserId, + provider: providerName, + model, + }); + return shape(result.rows[0]); + }; +} diff --git a/src/store/modelconversation/delete.js b/src/store/modelconversation/delete.js new file mode 100644 index 0000000..c019d90 --- /dev/null +++ b/src/store/modelconversation/delete.js @@ -0,0 +1,34 @@ +import { DELETE_BY_COMPANYUSER_AND_ID as MSSQL_DELETE } from '../../providers/mssql/queries/modelconversation.js'; +import { DELETE_BY_COMPANYUSER_AND_ID as SQLITE_DELETE } from '../../providers/sqlite3/queries/modelconversation.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function del(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = + provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE; + + /** + * Deletes a model conversation by ID for the user in the selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} conversationId - The conversation id. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise} + * @throws {Error} 'Conversation not found'. + */ + return async (googleAuthId, conversationId, tx) => { + const id = Number(conversationId); + if (!Number.isInteger(id) || id <= 0) { + throw new Error('Conversation not found'); + } + + const runner = makeRunner(provider, tx); + const { companyuserId } = await access(googleAuthId, runner); + + const result = await runner.query(sql, { id, companyuserId }); + if (!result.rowsAffected) { + throw new Error('Conversation not found'); + } + }; +} diff --git a/src/store/modelconversation/get.js b/src/store/modelconversation/get.js new file mode 100644 index 0000000..d2b34b8 --- /dev/null +++ b/src/store/modelconversation/get.js @@ -0,0 +1,33 @@ +import { GET_BY_COMPANYUSER_AND_ID as MSSQL_GET } from '../../providers/mssql/queries/modelconversation.js'; +import { GET_BY_COMPANYUSER_AND_ID as SQLITE_GET } from '../../providers/sqlite3/queries/modelconversation.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; +import shape from './shape.js'; + +export default function get(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = provider?.dialect === 'sqlite3' ? SQLITE_GET : MSSQL_GET; + + /** + * Gets a single model conversation by ID for the user in the selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} conversationId - The conversation id. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>} + * @throws {Error} 'Conversation not found'. + */ + return async (googleAuthId, conversationId, tx) => { + const id = Number(conversationId); + if (!Number.isInteger(id) || id <= 0) { + throw new Error('Conversation not found'); + } + + const runner = makeRunner(provider, tx); + const { companyuserId } = await access(googleAuthId, runner); + + const result = await runner.query(sql, { id, companyuserId }); + if (!result.rows[0]) throw new Error('Conversation not found'); + return shape(result.rows[0]); + }; +} diff --git a/src/store/modelconversation/list.js b/src/store/modelconversation/list.js new file mode 100644 index 0000000..7d83123 --- /dev/null +++ b/src/store/modelconversation/list.js @@ -0,0 +1,27 @@ +import { LIST_BY_COMPANYUSER as MSSQL_LIST } from '../../providers/mssql/queries/modelconversation.js'; +import { LIST_BY_COMPANYUSER as SQLITE_LIST } from '../../providers/sqlite3/queries/modelconversation.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; +import shape from './shape.js'; + +export default function list(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = provider?.dialect === 'sqlite3' ? SQLITE_LIST : MSSQL_LIST; + + /** + * Lists the user's own model conversations in the selected company, newest activity first. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise>} + * + * @example + * const conversations = await store.modelconversation.list(42); + */ + return async (googleAuthId, tx) => { + const runner = makeRunner(provider, tx); + const { companyuserId } = await access(googleAuthId, runner); + const result = await runner.query(sql, { companyuserId }); + return result.rows.map(shape); + }; +} diff --git a/src/store/modelconversation/setState.js b/src/store/modelconversation/setState.js new file mode 100644 index 0000000..26e7d33 --- /dev/null +++ b/src/store/modelconversation/setState.js @@ -0,0 +1,37 @@ +import { SET_STATE as MSSQL_SET_STATE } from '../../providers/mssql/queries/modelconversation.js'; +import { SET_STATE as SQLITE_SET_STATE } from '../../providers/sqlite3/queries/modelconversation.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; +import shape from './shape.js'; + +export default function setState(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = provider?.dialect === 'sqlite3' ? SQLITE_SET_STATE : MSSQL_SET_STATE; + + /** + * Sets the state of a model conversation ('idle' | 'busy'). + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} conversationId - The conversation id. + * @param {string} state - The new state ('idle' | 'busy'). + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>} + * @throws {Error} 'Invalid state' or 'Conversation not found'. + */ + return async (googleAuthId, conversationId, state, tx) => { + const id = Number(conversationId); + if (!Number.isInteger(id) || id <= 0) { + throw new Error('Conversation not found'); + } + if (state !== 'idle' && state !== 'busy') { + throw new Error("Invalid state: must be 'idle' or 'busy'"); + } + + const runner = makeRunner(provider, tx); + const { companyuserId } = await access(googleAuthId, runner); + + const result = await runner.query(sql, { id, companyuserId, state }); + if (!result.rows[0]) throw new Error('Conversation not found'); + return shape(result.rows[0]); + }; +} diff --git a/src/store/modelconversation/shape.js b/src/store/modelconversation/shape.js new file mode 100644 index 0000000..94b1888 --- /dev/null +++ b/src/store/modelconversation/shape.js @@ -0,0 +1,11 @@ +export default function shape(row) { + return { + id: row.id, + companyuser_id: row.companyuser_id ?? row.companyuserId, + provider: row.provider, + model: row.model, + created_at: row.created_at ?? row.createdAt, + updated_at: row.updated_at ?? row.updatedAt, + state: row.state, + }; +} diff --git a/src/store/modelmessage/add.js b/src/store/modelmessage/add.js new file mode 100644 index 0000000..670cc79 --- /dev/null +++ b/src/store/modelmessage/add.js @@ -0,0 +1,85 @@ +import { + GET_BY_COMPANYUSER_AND_ID as MSSQL_GET_CONV, + TOUCH as MSSQL_TOUCH_UNCONDITIONAL, + TOUCH_IF_IDLE as MSSQL_TOUCH, +} from '../../providers/mssql/queries/modelconversation.js'; +import { CREATE as MSSQL_CREATE } from '../../providers/mssql/queries/modelmessage.js'; +import { + GET_BY_COMPANYUSER_AND_ID as SQLITE_GET_CONV, + TOUCH as SQLITE_TOUCH_UNCONDITIONAL, + TOUCH_IF_IDLE as SQLITE_TOUCH, +} from '../../providers/sqlite3/queries/modelconversation.js'; +import { CREATE as SQLITE_CREATE } from '../../providers/sqlite3/queries/modelmessage.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; +import shape from './shape.js'; + +export default function add(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const getConvSql = isSqlite ? SQLITE_GET_CONV : MSSQL_GET_CONV; + const touchSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; + const touchUnconditionalSql = isSqlite ? SQLITE_TOUCH_UNCONDITIONAL : MSSQL_TOUCH_UNCONDITIONAL; + const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE; + + /** + * Appends a message to one of the user's own conversations. + * `user` messages are refused unless the conversation state is `idle`; + * other roles ('assistant', 'system', 'tool', 'reasoning') touch `updated_at` unconditionally. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} conversationId - The conversation id. + * @param {object} input - Message input. + * @param {string} input.message - Message text. + * @param {'user'|'assistant'|'system'|'tool'|'reasoning'} [input.role='user'] - Message role. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{id: number, conversation_id: number, created_at: string, role: string, message: string}>} + * @throws {Error} 'Conversation not found', 'Conversation is not idle', 'Message required', 'Invalid role'. + */ + return async (googleAuthId, conversationId, input = {}, tx) => { + const id = Number(conversationId); + if (!Number.isInteger(id) || id <= 0) { + throw new Error('Conversation not found'); + } + const role = input.role || 'user'; + if (!['user', 'assistant', 'system', 'tool', 'reasoning'].includes(role)) { + throw new Error('Invalid role'); + } + const message = input.message != null ? String(input.message) : ''; + + if (!message.trim()) { + throw new Error('Message required'); + } + + const run = async (runner) => { + const { companyuserId } = await access(googleAuthId, runner); + + if (role === 'user') { + const touched = await runner.query(touchSql, { id, companyuserId }); + if (!touched.rowsAffected) { + const conv = await runner.query(getConvSql, { id, companyuserId }); + if (!conv.rows[0]) throw new Error('Conversation not found'); + throw new Error('Conversation is not idle'); + } + } else { + const touched = await runner.query(touchUnconditionalSql, { id, companyuserId }); + if (!touched.rowsAffected) { + throw new Error('Conversation not found'); + } + } + + const result = await runner.query(createSql, { + conversationId: id, + role, + message, + }); + return shape(result.rows[0]); + }; + + if (tx) return run(makeRunner(provider, tx)); + if (typeof provider?.transaction === 'function') { + return provider.transaction(run); + } + return run(makeRunner(provider)); + }; +} diff --git a/src/store/modelmessage/list.js b/src/store/modelmessage/list.js new file mode 100644 index 0000000..ec26900 --- /dev/null +++ b/src/store/modelmessage/list.js @@ -0,0 +1,42 @@ +import { GET_BY_COMPANYUSER_AND_ID as MSSQL_GET_CONV } from '../../providers/mssql/queries/modelconversation.js'; +import { LIST_BY_CONVERSATION as MSSQL_LIST } from '../../providers/mssql/queries/modelmessage.js'; +import { GET_BY_COMPANYUSER_AND_ID as SQLITE_GET_CONV } from '../../providers/sqlite3/queries/modelconversation.js'; +import { LIST_BY_CONVERSATION as SQLITE_LIST } from '../../providers/sqlite3/queries/modelmessage.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; +import shape from './shape.js'; + +export default function list(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const getConvSql = isSqlite ? SQLITE_GET_CONV : MSSQL_GET_CONV; + const listSql = isSqlite ? SQLITE_LIST : MSSQL_LIST; + + /** + * Lists messages of one of the user's own conversations, oldest first. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} conversationId - The conversation id. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise>} + * @throws {Error} 'Conversation not found' if missing or owned by another user. + * + * @example + * const messages = await store.modelmessage.list(42, 7); + */ + return async (googleAuthId, conversationId, tx) => { + const id = Number(conversationId); + if (!Number.isInteger(id) || id <= 0) { + throw new Error('Conversation not found'); + } + + const runner = makeRunner(provider, tx); + const { companyuserId } = await access(googleAuthId, runner); + + const conv = await runner.query(getConvSql, { id, companyuserId }); + if (!conv.rows[0]) throw new Error('Conversation not found'); + + const result = await runner.query(listSql, { conversationId: id }); + return result.rows.map(shape); + }; +} diff --git a/src/store/modelmessage/shape.js b/src/store/modelmessage/shape.js new file mode 100644 index 0000000..0b7bd5c --- /dev/null +++ b/src/store/modelmessage/shape.js @@ -0,0 +1,9 @@ +export default function shape(row) { + return { + id: row.id, + conversation_id: row.conversation_id ?? row.conversationId, + created_at: row.created_at ?? row.createdAt, + role: row.role, + message: row.message, + }; +} diff --git a/src/store/modelvfs/delete.js b/src/store/modelvfs/delete.js new file mode 100644 index 0000000..2d3839b --- /dev/null +++ b/src/store/modelvfs/delete.js @@ -0,0 +1,37 @@ +import { DELETE_BY_COMPANY_AND_NAME as MSSQL_DELETE } from '../../providers/mssql/queries/modelvfs.js'; +import { DELETE_BY_COMPANY_AND_NAME as SQLITE_DELETE } from '../../providers/sqlite3/queries/modelvfs.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function deleteFile(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE; + + /** + * Deletes a virtual file by name for the selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} name - File name. + * @param {object} [tx] - Optional transaction. + * @returns {Promise<{deleted: boolean}>} + */ + return async (googleAuthId, name, tx) => { + const filename = typeof name === 'string' ? name.trim() : ''; + if (!filename) throw new Error('File name is required'); + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const result = await runner.query(sql, { companyId, name: filename }); + const rows = result.rows || []; + + if (provider?.dialect === 'sqlite3') { + for (const row of rows) { + if (runner.deleteVfsContent) await runner.deleteVfsContent(row.id); + else if (provider.deleteVfsContent) await provider.deleteVfsContent(row.id); + } + } + + return { deleted: rows.length > 0 }; + }; +} diff --git a/src/store/modelvfs/list.js b/src/store/modelvfs/list.js new file mode 100644 index 0000000..717c423 --- /dev/null +++ b/src/store/modelvfs/list.js @@ -0,0 +1,25 @@ +import { LIST_BY_COMPANY as MSSQL_LIST } from '../../providers/mssql/queries/modelvfs.js'; +import { LIST_BY_COMPANY as SQLITE_LIST } from '../../providers/sqlite3/queries/modelvfs.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; +import shape from './shape.js'; + +export default function list(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = provider?.dialect === 'sqlite3' ? SQLITE_LIST : MSSQL_LIST; + + /** + * Lists virtual files for the selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} [tx] - Optional transaction. + * @returns {Promise>} + */ + return async (googleAuthId, tx) => { + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const result = await runner.query(sql, { companyId }); + return result.rows.map(shape); + }; +} diff --git a/src/store/modelvfs/read.js b/src/store/modelvfs/read.js new file mode 100644 index 0000000..0da2272 --- /dev/null +++ b/src/store/modelvfs/read.js @@ -0,0 +1,49 @@ +import { GET_BY_COMPANY_AND_NAME as MSSQL_GET } from '../../providers/mssql/queries/modelvfs.js'; +import { GET_BY_COMPANY_AND_NAME as SQLITE_GET } from '../../providers/sqlite3/queries/modelvfs.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; +import shape from './shape.js'; + +export default function read(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = provider?.dialect === 'sqlite3' ? SQLITE_GET : MSSQL_GET; + + /** + * Reads a virtual file by name for the selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} name - File name. + * @param {object} [tx] - Optional transaction. + * @returns {Promise<{id: number, company_id: number, name: string, mime: string, content: Buffer, meta: any, created_at: string, updated_at: string}>} + * @throws {Error} 'File not found' + */ + return async (googleAuthId, name, tx) => { + const filename = typeof name === 'string' ? name.trim() : ''; + if (!filename) throw new Error('File name is required'); + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const result = await runner.query(sql, { companyId, name: filename }); + const row = result.rows[0]; + if (!row) { + throw new Error('File not found'); + } + + let content; + if (provider?.dialect === 'sqlite3') { + content = runner.readVfsContent + ? await runner.readVfsContent(row.id) + : await provider.readVfsContent?.(row.id); + if (!content) content = Buffer.alloc(0); + } else { + content = row.content; + } + + return { + ...shape(row), + content, + size: content?.length || 0, + }; + }; +} diff --git a/src/store/modelvfs/reset.js b/src/store/modelvfs/reset.js new file mode 100644 index 0000000..d834171 --- /dev/null +++ b/src/store/modelvfs/reset.js @@ -0,0 +1,33 @@ +import { DELETE_ALL_BY_COMPANY as MSSQL_RESET } from '../../providers/mssql/queries/modelvfs.js'; +import { DELETE_ALL_BY_COMPANY as SQLITE_RESET } from '../../providers/sqlite3/queries/modelvfs.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function reset(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = provider?.dialect === 'sqlite3' ? SQLITE_RESET : MSSQL_RESET; + + /** + * Deletes all virtual files for the selected company (reset VFS). + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} [tx] - Optional transaction. + * @returns {Promise<{deletedCount: number}>} + */ + return async (googleAuthId, tx) => { + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const result = await runner.query(sql, { companyId }); + const rows = result.rows || []; + + if (provider?.dialect === 'sqlite3') { + for (const row of rows) { + if (runner.deleteVfsContent) await runner.deleteVfsContent(row.id); + else if (provider.deleteVfsContent) await provider.deleteVfsContent(row.id); + } + } + + return { deletedCount: rows.length }; + }; +} diff --git a/src/store/modelvfs/shape.js b/src/store/modelvfs/shape.js new file mode 100644 index 0000000..8f96dd0 --- /dev/null +++ b/src/store/modelvfs/shape.js @@ -0,0 +1,21 @@ +export default function shape(row) { + if (!row) return null; + let parsedMeta = null; + if (row.meta) { + try { + parsedMeta = typeof row.meta === 'string' ? JSON.parse(row.meta) : row.meta; + } catch { + parsedMeta = row.meta; + } + } + return { + id: row.id, + company_id: row.company_id, + name: row.name, + mime: row.mime, + meta: parsedMeta, + created_at: row.created_at, + updated_at: row.updated_at, + ...(row.content !== undefined ? { content: row.content } : {}), + }; +} diff --git a/src/store/modelvfs/write.js b/src/store/modelvfs/write.js new file mode 100644 index 0000000..a034ce9 --- /dev/null +++ b/src/store/modelvfs/write.js @@ -0,0 +1,61 @@ +import { UPSERT as MSSQL_UPSERT } from '../../providers/mssql/queries/modelvfs.js'; +import { UPSERT as SQLITE_UPSERT } from '../../providers/sqlite3/queries/modelvfs.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; +import shape from './shape.js'; + +export default function write(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = provider?.dialect === 'sqlite3' ? SQLITE_UPSERT : MSSQL_UPSERT; + + /** + * Writes (inserts or overwrites) a virtual file for the selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input + * @param {string} input.name - File path/name (e.g. 'candidate.avif', 'report.json'). + * @param {string} [input.mime='application/octet-stream'] - Mime type. + * @param {Buffer|string} input.content - File content buffer or string. + * @param {object|string} [input.meta] - Optional metadata object or JSON string. + * @param {object} [tx] - Optional transaction. + * @returns {Promise} The stored file record metadata. + */ + return async (googleAuthId, input = {}, tx) => { + const name = typeof input.name === 'string' ? input.name.trim() : ''; + if (!name) throw new Error('File name is required'); + const mime = typeof input.mime === 'string' && input.mime.trim() ? input.mime.trim() : 'application/octet-stream'; + const contentBuffer = Buffer.isBuffer(input.content) + ? input.content + : Buffer.from(input.content != null ? String(input.content) : ''); + const metaStr = input.meta != null + ? (typeof input.meta === 'object' ? JSON.stringify(input.meta) : String(input.meta)) + : null; + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const params = { + companyId, + name, + mime, + meta: metaStr, + ...(provider?.dialect !== 'sqlite3' ? { content: contentBuffer } : {}), + }; + + const result = await runner.query(sql, params); + const row = result.rows[0]; + + if (provider?.dialect === 'sqlite3' && row?.id) { + if (runner.writeVfsContent) { + await runner.writeVfsContent(row.id, contentBuffer); + } else if (provider.writeVfsContent) { + await provider.writeVfsContent(row.id, contentBuffer); + } + } + + return { + ...shape(row), + size: contentBuffer.length, + }; + }; +} diff --git a/src/store/runner.js b/src/store/runner.js index 275125b..a85dd6a 100644 --- a/src/store/runner.js +++ b/src/store/runner.js @@ -71,5 +71,8 @@ function createCallableRunner(base) { run.readPicThumb = base.readPicThumb?.bind(base); run.deleteFile = base.deleteFile?.bind(base); run.deletePic = base.deletePic?.bind(base); + run.writeVfsContent = base.writeVfsContent?.bind(base); + run.readVfsContent = base.readVfsContent?.bind(base); + run.deleteVfsContent = base.deleteVfsContent?.bind(base); return run; }