u
This commit is contained in:
15
API.md
15
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<void>`
|
||||
- `listByCategoryId(googleAuthId: number, categoryId: number, tx?: object): Promise<Array<{id: number, categoryId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>`
|
||||
- `setByCategoryId(googleAuthId: number, categoryId: number, properties: Array<{propertyId: number, value: string, sort?: number}>, tx?: object): Promise<Array<{id: number, categoryId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>`
|
||||
- [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<void>`
|
||||
- `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<Array<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>>`
|
||||
- `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<Array<{id: number, conversation_id: number, created_at: string, role: string, message: string}>>`
|
||||
- [modelvfs](API.modelvfs.md)
|
||||
- `delete(googleAuthId: number, name: string, tx?: object): Promise<{deleted: boolean}>`
|
||||
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, company_id: number, name: string, mime: string, meta: any, created_at: string, updated_at: string}>>`
|
||||
- `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<object>`
|
||||
- [optionslist](API.optionslist.md)
|
||||
- `getOptionsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number, mandated: boolean}>>`
|
||||
- `setOptionsByArticleSku(googleAuthId: number, sku: string, options?: Array<{articleId?: number, sku?: string, count?: number, sort?: number, mandated?: boolean}>, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number, mandated: boolean}>>`
|
||||
|
||||
123
API.modelconversation.md
Normal file
123
API.modelconversation.md
Normal file
@@ -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<void>`
|
||||
|
||||
**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<Array<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>>`
|
||||
|
||||
**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'.
|
||||
56
API.modelmessage.md
Normal file
56
API.modelmessage.md
Normal file
@@ -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<Array<{id: number, conversation_id: number, created_at: string, role: string, message: string}>>`
|
||||
|
||||
**Throws**
|
||||
|
||||
`Error` — 'Conversation not found' if missing or owned by another user.
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
const messages = await store.modelmessage.list(42, 7);
|
||||
```
|
||||
99
API.modelvfs.md
Normal file
99
API.modelvfs.md
Normal file
@@ -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<Array<{id: number, company_id: number, name: string, mime: string, meta: any, created_at: string, updated_at: string}>>`
|
||||
|
||||
## `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<object>` — The stored file record metadata.
|
||||
@@ -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',
|
||||
|
||||
177
schema-graph.dot
177
schema-graph.dot
File diff suppressed because one or more lines are too long
4334
schema-graph.svg
4334
schema-graph.svg
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 272 KiB After Width: | Height: | Size: 289 KiB |
87
scripts/test-modelconversation.js
Normal file
87
scripts/test-modelconversation.js
Normal file
@@ -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);
|
||||
});
|
||||
34
src/index.js
34
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),
|
||||
|
||||
@@ -8,6 +8,8 @@ export const TABLES = [
|
||||
'manifestattachment',
|
||||
'labeltemplate',
|
||||
'file',
|
||||
'modelmessage',
|
||||
'modelconversation',
|
||||
'companyuser',
|
||||
'syncpos',
|
||||
'companysetting',
|
||||
|
||||
79
src/providers/mssql/queries/modelconversation.js
Normal file
79
src/providers/mssql/queries/modelconversation.js
Normal file
@@ -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
|
||||
`;
|
||||
34
src/providers/mssql/queries/modelmessage.js
Normal file
34
src/providers/mssql/queries/modelmessage.js
Normal file
@@ -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
|
||||
)
|
||||
`;
|
||||
63
src/providers/mssql/queries/modelvfs.js
Normal file
63
src/providers/mssql/queries/modelvfs.js
Normal file
@@ -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
|
||||
`;
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
14
src/providers/sqlite3/queries/modelconversation.js
Normal file
14
src/providers/sqlite3/queries/modelconversation.js
Normal file
@@ -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;
|
||||
5
src/providers/sqlite3/queries/modelmessage.js
Normal file
5
src/providers/sqlite3/queries/modelmessage.js
Normal file
@@ -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;
|
||||
38
src/providers/sqlite3/queries/modelvfs.js
Normal file
38
src/providers/sqlite3/queries/modelvfs.js
Normal file
@@ -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
|
||||
`;
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
42
src/store/modelconversation/create.js
Normal file
42
src/store/modelconversation/create.js
Normal file
@@ -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]);
|
||||
};
|
||||
}
|
||||
34
src/store/modelconversation/delete.js
Normal file
34
src/store/modelconversation/delete.js
Normal file
@@ -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<void>}
|
||||
* @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');
|
||||
}
|
||||
};
|
||||
}
|
||||
33
src/store/modelconversation/get.js
Normal file
33
src/store/modelconversation/get.js
Normal file
@@ -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]);
|
||||
};
|
||||
}
|
||||
27
src/store/modelconversation/list.js
Normal file
27
src/store/modelconversation/list.js
Normal file
@@ -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<Array<{id: number, companyuser_id: number, provider: string, model: string, created_at: string, updated_at: string, state: string}>>}
|
||||
*
|
||||
* @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);
|
||||
};
|
||||
}
|
||||
37
src/store/modelconversation/setState.js
Normal file
37
src/store/modelconversation/setState.js
Normal file
@@ -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]);
|
||||
};
|
||||
}
|
||||
11
src/store/modelconversation/shape.js
Normal file
11
src/store/modelconversation/shape.js
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
85
src/store/modelmessage/add.js
Normal file
85
src/store/modelmessage/add.js
Normal file
@@ -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));
|
||||
};
|
||||
}
|
||||
42
src/store/modelmessage/list.js
Normal file
42
src/store/modelmessage/list.js
Normal file
@@ -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<Array<{id: number, conversation_id: number, created_at: string, role: string, message: string}>>}
|
||||
* @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);
|
||||
};
|
||||
}
|
||||
9
src/store/modelmessage/shape.js
Normal file
9
src/store/modelmessage/shape.js
Normal file
@@ -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,
|
||||
};
|
||||
}
|
||||
37
src/store/modelvfs/delete.js
Normal file
37
src/store/modelvfs/delete.js
Normal file
@@ -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 };
|
||||
};
|
||||
}
|
||||
25
src/store/modelvfs/list.js
Normal file
25
src/store/modelvfs/list.js
Normal file
@@ -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<Array<{id: number, company_id: number, name: string, mime: string, meta: any, created_at: string, updated_at: string}>>}
|
||||
*/
|
||||
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);
|
||||
};
|
||||
}
|
||||
49
src/store/modelvfs/read.js
Normal file
49
src/store/modelvfs/read.js
Normal file
@@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
33
src/store/modelvfs/reset.js
Normal file
33
src/store/modelvfs/reset.js
Normal file
@@ -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 };
|
||||
};
|
||||
}
|
||||
21
src/store/modelvfs/shape.js
Normal file
21
src/store/modelvfs/shape.js
Normal file
@@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
61
src/store/modelvfs/write.js
Normal file
61
src/store/modelvfs/write.js
Normal file
@@ -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<object>} 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,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user