u
This commit is contained in:
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