Replace custom sqlite dbms with prisma (#239)

* WIP converted all sqlite models into prisma calls

* modify db setup and fix ApiKey model calls in admin.js

* renaming function params to be consistent

* converted adminEndpoints to utilize prisma orm

* converted chatEndpoints to utilize prisma orm

* converted inviteEndpoints to utilize prisma orm

* converted systemEndpoints to utilize prisma orm

* converted workspaceEndpoints to utilize prisma orm

* converting sql queries to prisma calls

* fixed default param bug for orderBy and limit

* fixed typo for workspace chats

* fixed order of deletion to account for sql relations

* fix invite CRUD and workspace management CRUD

* fixed CRUD for api keys

* created prisma setup scripts/docs for understanding how to use prisma

* prisma dependency change

* removing unneeded console.logs

* removing unneeded sql escape function

* linting and creating migration script

* migration from depreciated sqlite script update

* removing unneeded migrations in prisma folder

* create backup of old sqlite db and use transactions to ensure all operations complete successfully

* adding migrations to gitignore

* updated PRISMA.md docs for info on how to use sqlite migration script

* comment changes

* adding back migrations folder to repo

* Reviewing SQL and prisma integraiton on fresh repo

* update inline key replacement

* ensure migration script executes and maps foreign_keys regardless of db ordering

* run migration endpoint

* support new prisma backend

* bump version

* change migration call

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
Sean Hatfield 2023-09-28 14:00:03 -07:00 committed by GitHub
parent e4a5fe5971
commit a126b5f5aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 9535 additions and 1397 deletions

View File

@ -77,6 +77,10 @@ RUN cd /app/collector && \
. v-env/bin/activate && \
pip install --no-cache-dir -r requirements.txt
# Migrate and Run Prisma against known schema
RUN cd ./server && npx prisma generate --schema=./prisma/schema.prisma
RUN cd ./server && npx prisma migrate deploy --schema=./prisma/schema.prisma
# Setup the environment
ENV NODE_ENV=production
ENV PATH=/app/collector/v-env/bin:$PATH

View File

@ -22,7 +22,7 @@ export default function DocumentSettings({ workspace }) {
async function fetchKeys(refetchWorkspace = false) {
const localFiles = await System.localFiles();
const currentWorkspace = refetchWorkspace
? await Workspace.bySlug(slug)
? await Workspace.bySlug(slug ?? workspace.slug)
: workspace;
const originalDocs =
currentWorkspace.documents.map((doc) => doc.docpath) || [];

View File

@ -3,6 +3,22 @@ import Workspace from "../../../../models/workspace";
import paths from "../../../../utils/paths";
import { chatPrompt } from "../../../../utils/chat";
// Ensure that a type is correct before sending the body
// to the backend.
function castToType(key, value) {
const definitions = {
openAiTemp: {
cast: (value) => Number(value),
},
openAiHistory: {
cast: (value) => Number(value),
},
};
if (!definitions.hasOwnProperty(key)) return value;
return definitions[key].cast(value);
}
export default function WorkspaceSettings({ workspace }) {
const formEl = useRef(null);
const [saving, setSaving] = useState(false);
@ -34,7 +50,7 @@ export default function WorkspaceSettings({ workspace }) {
e.preventDefault();
const data = {};
const form = new FormData(formEl.current);
for (var [key, value] of form.entries()) data[key] = value;
for (var [key, value] of form.entries()) data[key] = castToType(key, value);
const { workspace: updatedWorkspace, message } = await Workspace.update(
workspace.slug,
data

View File

@ -1,6 +1,6 @@
{
"name": "anything-llm",
"version": "0.1.0",
"version": "0.2.0",
"description": "The best solution for turning private documents into a chat bot using off-the-shelf tools and commercially viable AI technologies.",
"main": "index.js",
"author": "Timothy Carambat (Mintplex Labs)",
@ -10,10 +10,14 @@
},
"scripts": {
"lint": "cd server && yarn lint && cd .. && cd frontend && yarn lint",
"setup": "cd server && yarn && cd ../frontend && yarn && cd .. && yarn setup:envs && echo \"Please run yarn dev:server and yarn dev:frontend in separate terminal tabs.\"",
"setup": "cd server && yarn && cd ../frontend && yarn && cd .. && yarn setup:envs && yarn prisma:setup && echo \"Please run yarn dev:server and yarn dev:frontend in separate terminal tabs.\"",
"setup:envs": "cp -n ./frontend/.env.example ./frontend/.env && cp -n ./server/.env.example ./server/.env.development && cp -n ./collector/.env.example ./collector/.env && cp -n ./docker/.env.example ./docker/.env && echo \"All ENV files copied!\n\"",
"dev:server": "cd server && yarn dev",
"dev:frontend": "cd frontend && yarn start",
"prisma:generate": "cd server && npx prisma generate",
"prisma:migrate": "cd server && npx prisma migrate dev --name init",
"prisma:seed": "cd server && npx prisma db seed",
"prisma:setup": "yarn prisma:generate && yarn prisma:migrate && yarn prisma:seed",
"prod:server": "cd server && yarn start",
"prod:frontend": "cd frontend && yarn build",
"generate:cloudformation": "node cloud-deployments/aws/cloudformation/generate.mjs",

1
server/.gitignore vendored
View File

@ -16,3 +16,4 @@ public/
# For legacy copies of repo
documents
vector-cache
yarn-error.log

View File

@ -1,4 +1,3 @@
const { escape } = require("sqlstring-sqlite");
const { ApiKey } = require("../models/apiKeys");
const { Document } = require("../models/documents");
const { Invite } = require("../models/invite");
@ -82,7 +81,7 @@ function adminEndpoints(app) {
return;
}
const { id } = request.params;
await User.delete(`id = ${id}`);
await User.delete({ id: Number(id) });
response.status(200).json({ success: true, error: null });
} catch (e) {
console.error(e);
@ -204,7 +203,7 @@ function adminEndpoints(app) {
const { workspaceId } = request.params;
const { userIds } = reqBody(request);
const { success, error } = await Workspace.updateUsers(
escape(workspaceId),
workspaceId,
userIds
);
response.status(200).json({ success, error });
@ -228,23 +227,23 @@ function adminEndpoints(app) {
const { id } = request.params;
const VectorDb = getVectorDbClass();
const workspace = Workspace.get(`id = ${escape(id)}`);
const workspace = await Workspace.get({ id: Number(id) });
if (!workspace) {
response.sendStatus(404).end();
return;
}
await Workspace.delete(`id = ${workspace.id}`);
await DocumentVectors.deleteForWorkspace(workspace.id);
await Document.delete(`workspaceId = ${Number(workspace.id)}`);
await WorkspaceChats.delete(`workspaceId = ${Number(workspace.id)}`);
await WorkspaceChats.delete({ workspaceId: Number(workspace.id) });
await DocumentVectors.deleteForWorkspace(Number(workspace.id));
await Document.delete({ workspaceId: Number(workspace.id) });
await Workspace.delete({ id: Number(workspace.id) });
try {
await VectorDb["delete-namespace"]({ namespace: workspace.slug });
} catch (e) {
console.error(e.message);
}
response.status(200).json({ success, error });
response.status(200).json({ success: true, error: null });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
@ -262,13 +261,18 @@ function adminEndpoints(app) {
response.sendStatus(401).end();
return;
}
const { offset = 0 } = reqBody(request);
const { offset = 0, limit = 20 } = reqBody(request);
const chats = await WorkspaceChats.whereWithData(
`id >= ${escape(offset)}`,
20
{ id: { gte: offset } },
limit
);
const hasPages = (await WorkspaceChats.count()) > 20;
response.status(200).json({ chats: chats.reverse(), hasPages });
const totalChats = await WorkspaceChats.count();
const hasPages = totalChats > offset + limit;
response
.status(200)
.json({ chats: chats.reverse(), hasPages, totalChats });
} catch (e) {
console.error(e);
response.sendStatus(500).end();
@ -288,7 +292,7 @@ function adminEndpoints(app) {
}
const { id } = request.params;
await WorkspaceChats.delete(`id = ${id}`);
await WorkspaceChats.delete({ id: Number(id) });
response.status(200).json({ success, error });
} catch (e) {
console.error(e);
@ -310,14 +314,14 @@ function adminEndpoints(app) {
const settings = {
users_can_delete_workspaces:
(await SystemSettings.get(`label = 'users_can_delete_workspaces'`))
(await SystemSettings.get({ label: "users_can_delete_workspaces" }))
?.value === "true",
limit_user_messages:
(await SystemSettings.get(`label = 'limit_user_messages'`))
(await SystemSettings.get({ label: "limit_user_messages" }))
?.value === "true",
message_limit:
Number(
(await SystemSettings.get(`label = 'message_limit'`))?.value
(await SystemSettings.get({ label: "message_limit" }))?.value
) || 10,
};
response.status(200).json({ settings });
@ -357,7 +361,7 @@ function adminEndpoints(app) {
return;
}
const apiKeys = await ApiKey.whereWithUser("id IS NOT NULL");
const apiKeys = await ApiKey.whereWithUser({});
return response.status(200).json({
apiKeys,
error: null,
@ -405,8 +409,7 @@ function adminEndpoints(app) {
response.sendStatus(401).end();
return;
}
await ApiKey.delete(`id = ${id}`);
await ApiKey.delete({ id: Number(id) });
return response.status(200).end();
} catch (e) {
console.error(e);

View File

@ -1,4 +1,3 @@
const { escape } = require("sqlstring-sqlite");
const { Invite } = require("../../../models/invite");
const { SystemSettings } = require("../../../models/systemSettings");
const { User } = require("../../../models/user");
@ -249,7 +248,7 @@ function apiAdminEndpoints(app) {
}
const { id } = request.params;
await User.delete(`id = ${id}`);
await User.delete({ id });
response.status(200).json({ success: true, error: null });
} catch (e) {
console.error(e);
@ -457,7 +456,7 @@ function apiAdminEndpoints(app) {
const { workspaceId } = request.params;
const { userIds } = reqBody(request);
const { success, error } = await Workspace.updateUsers(
escape(workspaceId),
workspaceId,
userIds
);
response.status(200).json({ success, error });
@ -517,7 +516,7 @@ function apiAdminEndpoints(app) {
const { offset = 0 } = reqBody(request);
const chats = await WorkspaceChats.whereWithData(
`id >= ${escape(offset)}`,
{ id: { gte: offset } },
20
);
const hasPages = (await WorkspaceChats.count()) > 20;
@ -566,14 +565,14 @@ function apiAdminEndpoints(app) {
const settings = {
users_can_delete_workspaces:
(await SystemSettings.get(`label = 'users_can_delete_workspaces'`))
(await SystemSettings.get({ label: "users_can_delete_workspaces" }))
?.value === "true",
limit_user_messages:
(await SystemSettings.get(`label = 'limit_user_messages'`))?.value ===
"true",
(await SystemSettings.get({ label: "limit_user_messages" }))
?.value === "true",
message_limit:
Number(
(await SystemSettings.get(`label = 'message_limit'`))?.value
(await SystemSettings.get({ label: "message_limit" }))?.value
) || 10,
};
response.status(200).json({ settings });

View File

@ -1,4 +1,3 @@
const { escape } = require("sqlstring-sqlite");
const { Document } = require("../../../models/documents");
const { Telemetry } = require("../../../models/telemetry");
const { DocumentVectors } = require("../../../models/vectors");
@ -154,7 +153,7 @@ function apiWorkspaceEndpoints(app) {
*/
try {
const { slug } = request.params;
const workspace = await Workspace.get(`slug = ${escape(slug)}`);
const workspace = await Workspace.get({ slug });
response.status(200).json({ workspace });
} catch (e) {
console.log(e.message, e);
@ -185,17 +184,17 @@ function apiWorkspaceEndpoints(app) {
try {
const { slug = "" } = request.params;
const VectorDb = getVectorDbClass();
const workspace = await Workspace.get(`slug = ${escape(slug)}`);
const workspace = await Workspace.get({ slug });
if (!workspace) {
response.sendStatus(400).end();
return;
}
await Workspace.delete(`id = ${Number(workspace.id)}`);
await DocumentVectors.deleteForWorkspace(workspace.id);
await Document.delete(`workspaceId = ${Number(workspace.id)}`);
await WorkspaceChats.delete(`workspaceId = ${Number(workspace.id)}`);
await WorkspaceChats.delete({ workspaceId: Number(workspace.id) });
await DocumentVectors.deleteForWorkspace(Number(workspace.id));
await Document.delete({ workspaceId: Number(workspace.id) });
await Workspace.delete({ id: Number(workspace.id) });
try {
await VectorDb["delete-namespace"]({ namespace: slug });
} catch (e) {
@ -270,7 +269,7 @@ function apiWorkspaceEndpoints(app) {
try {
const { slug = null } = request.params;
const data = reqBody(request);
const currWorkspace = await Workspace.get(`slug = ${escape(slug)}`);
const currWorkspace = await Workspace.get({ slug });
if (!currWorkspace) {
response.sendStatus(400).end();
@ -334,7 +333,7 @@ function apiWorkspaceEndpoints(app) {
*/
try {
const { slug } = request.params;
const workspace = await Workspace.get(`slug = ${escape(slug)}`);
const workspace = await Workspace.get({ slug });
if (!workspace) {
response.sendStatus(400).end();
@ -409,7 +408,7 @@ function apiWorkspaceEndpoints(app) {
try {
const { slug = null } = request.params;
const { adds = [], deletes = [] } = reqBody(request);
const currWorkspace = await Workspace.get(`slug = ${escape(slug)}`);
const currWorkspace = await Workspace.get({ slug });
if (!currWorkspace) {
response.sendStatus(400).end();

View File

@ -6,7 +6,6 @@ const { validatedRequest } = require("../utils/middleware/validatedRequest");
const { WorkspaceChats } = require("../models/workspaceChats");
const { SystemSettings } = require("../models/systemSettings");
const { Telemetry } = require("../models/telemetry");
const { escape } = require("sqlstring-sqlite");
function chatEndpoints(app) {
if (!app) return;
@ -19,9 +18,10 @@ function chatEndpoints(app) {
const user = await userFromSession(request, response);
const { slug } = request.params;
const { message, mode = "query" } = reqBody(request);
const workspace = multiUserMode(response)
? await Workspace.getWithUser(user, `slug = ${escape(slug)}`)
: await Workspace.get(`slug = ${escape(slug)}`);
? await Workspace.getWithUser(user, { slug })
: await Workspace.get({ slug });
if (!workspace) {
response.sendStatus(400).end();
@ -29,18 +29,25 @@ function chatEndpoints(app) {
}
if (multiUserMode(response) && user.role !== "admin") {
const limitMessages =
(await SystemSettings.get(`label = 'limit_user_messages'`))
?.value === "true";
const limitMessagesSetting = await SystemSettings.get({
label: "limit_user_messages",
});
const limitMessages = limitMessagesSetting?.value === "true";
if (limitMessages) {
const systemLimit = Number(
(await SystemSettings.get(`label = 'message_limit'`))?.value
);
const messageLimitSetting = await SystemSettings.get({
label: "message_limit",
});
const systemLimit = Number(messageLimitSetting?.value);
if (!!systemLimit) {
const currentChatCount = await WorkspaceChats.count(
`user_id = ${user.id} AND createdAt > datetime(CURRENT_TIMESTAMP, '-1 days')`
);
const currentChatCount = await WorkspaceChats.count({
user_id: user.id,
createdAt: {
gte: new Date(new Date() - 24 * 60 * 60 * 1000),
},
});
if (currentChatCount >= systemLimit) {
response.status(500).json({
id: uuidv4(),

View File

@ -1,4 +1,3 @@
const { escape } = require("sqlstring-sqlite");
const { Invite } = require("../models/invite");
const { User } = require("../models/user");
const { reqBody } = require("../utils/http");
@ -9,7 +8,7 @@ function inviteEndpoints(app) {
app.get("/invite/:code", async (request, response) => {
try {
const { code } = request.params;
const invite = await Invite.get(`code = ${escape(code)}`);
const invite = await Invite.get({ code });
if (!invite) {
response.status(200).json({ invite: null, error: "Invite not found." });
return;
@ -35,7 +34,7 @@ function inviteEndpoints(app) {
try {
const { code } = request.params;
const userParams = reqBody(request);
const invite = await Invite.get(`code = ${escape(code)}`);
const invite = await Invite.get({ code });
if (!invite || invite.status !== "pending") {
response
.status(200)

View File

@ -1,7 +1,6 @@
process.env.NODE_ENV === "development"
? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` })
: require("dotenv").config();
const { validateTablePragmas } = require("../utils/database");
const { viewLocalFiles } = require("../utils/files");
const { exportData, unpackAndOverwriteImport } = require("../utils/files/data");
const {
@ -38,7 +37,6 @@ const {
const { Telemetry } = require("../models/telemetry");
const { WelcomeMessages } = require("../models/welcomeMessages");
const { ApiKey } = require("../models/apiKeys");
const { escape } = require("sqlstring-sqlite");
function systemEndpoints(app) {
if (!app) return;
@ -48,7 +46,10 @@ function systemEndpoints(app) {
});
app.get("/migrate", async (_, response) => {
await validateTablePragmas(true);
const execSync = require("child_process").execSync;
execSync("npx prisma migrate deploy --schema=./prisma/schema.prisma", {
stdio: "inherit",
});
response.sendStatus(200);
});
@ -97,7 +98,7 @@ function systemEndpoints(app) {
try {
if (await SystemSettings.isMultiUserMode()) {
const { username, password } = reqBody(request);
const existingUser = await User.get(`username = ${escape(username)}`);
const existingUser = await User.get({ username });
if (!existingUser) {
response.status(200).json({
@ -524,7 +525,7 @@ function systemEndpoints(app) {
return response.sendStatus(401).end();
}
const apiKey = await ApiKey.get("id IS NOT NULL");
const apiKey = await ApiKey.get({});
return response.status(200).json({
apiKey,
error: null,

View File

@ -13,7 +13,6 @@ const {
const { validatedRequest } = require("../utils/middleware/validatedRequest");
const { SystemSettings } = require("../models/systemSettings");
const { Telemetry } = require("../models/telemetry");
const { escape } = require("sqlstring-sqlite");
const { handleUploads } = setupMulter();
function workspaceEndpoints(app) {
@ -45,8 +44,8 @@ function workspaceEndpoints(app) {
const { slug = null } = request.params;
const data = reqBody(request);
const currWorkspace = multiUserMode(response)
? await Workspace.getWithUser(user, `slug = ${escape(slug)}`)
: await Workspace.get(`slug = ${escape(slug)}`);
? await Workspace.getWithUser(user, { slug })
: await Workspace.get({ slug });
if (!currWorkspace) {
response.sendStatus(400).end();
@ -106,8 +105,8 @@ function workspaceEndpoints(app) {
const { slug = null } = request.params;
const { adds = [], deletes = [] } = reqBody(request);
const currWorkspace = multiUserMode(response)
? await Workspace.getWithUser(user, `slug = ${escape(slug)}`)
: await Workspace.get(`slug = ${escape(slug)}`);
? await Workspace.getWithUser(user, { slug })
: await Workspace.get({ slug });
if (!currWorkspace) {
response.sendStatus(400).end();
@ -116,9 +115,7 @@ function workspaceEndpoints(app) {
await Document.removeDocuments(currWorkspace, deletes);
await Document.addDocuments(currWorkspace, adds);
const updatedWorkspace = await Workspace.get(
`id = ${currWorkspace.id}`
);
const updatedWorkspace = await Workspace.get({ id: currWorkspace.id });
response.status(200).json({ workspace: updatedWorkspace });
} catch (e) {
console.log(e.message, e);
@ -136,8 +133,8 @@ function workspaceEndpoints(app) {
const user = await userFromSession(request, response);
const VectorDb = getVectorDbClass();
const workspace = multiUserMode(response)
? await Workspace.getWithUser(user, `slug = ${escape(slug)}`)
: await Workspace.get(`slug = ${escape(slug)}`);
? await Workspace.getWithUser(user, { slug })
: await Workspace.get({ slug });
if (!workspace) {
response.sendStatus(400).end();
@ -146,7 +143,7 @@ function workspaceEndpoints(app) {
if (multiUserMode(response) && user.role !== "admin") {
const canDelete =
(await SystemSettings.get(`label = 'users_can_delete_workspaces'`))
(await SystemSettings.get({ label: "users_can_delete_workspaces" }))
?.value === "true";
if (!canDelete) {
response.sendStatus(500).end();
@ -154,10 +151,11 @@ function workspaceEndpoints(app) {
}
}
await Workspace.delete(`id = ${Number(workspace.id)}`);
await WorkspaceChats.delete({ workspaceId: Number(workspace.id) });
await DocumentVectors.deleteForWorkspace(workspace.id);
await Document.delete(`workspaceId = ${Number(workspace.id)}`);
await WorkspaceChats.delete(`workspaceId = ${Number(workspace.id)}`);
await Document.delete({ workspaceId: Number(workspace.id) });
await Workspace.delete({ id: Number(workspace.id) });
try {
await VectorDb["delete-namespace"]({ namespace: slug });
} catch (e) {
@ -190,8 +188,8 @@ function workspaceEndpoints(app) {
const { slug } = request.params;
const user = await userFromSession(request, response);
const workspace = multiUserMode(response)
? await Workspace.getWithUser(user, `slug = ${escape(slug)}`)
: await Workspace.get(`slug = ${escape(slug)}`);
? await Workspace.getWithUser(user, { slug })
: await Workspace.get({ slug });
response.status(200).json({ workspace });
} catch (e) {
@ -208,8 +206,8 @@ function workspaceEndpoints(app) {
const { slug } = request.params;
const user = await userFromSession(request, response);
const workspace = multiUserMode(response)
? await Workspace.getWithUser(user, `slug = ${escape(slug)}`)
: await Workspace.get(`slug = ${escape(slug)}`);
? await Workspace.getWithUser(user, { slug })
: await Workspace.get({ slug });
if (!workspace) {
response.sendStatus(400).end();

View File

@ -12,12 +12,12 @@ const { systemEndpoints } = require("./endpoints/system");
const { workspaceEndpoints } = require("./endpoints/workspaces");
const { chatEndpoints } = require("./endpoints/chat");
const { getVectorDbClass } = require("./utils/helpers");
const { validateTablePragmas, setupTelemetry } = require("./utils/database");
const { adminEndpoints } = require("./endpoints/admin");
const { inviteEndpoints } = require("./endpoints/invite");
const { utilEndpoints } = require("./endpoints/utils");
const { Telemetry } = require("./models/telemetry");
const { developerEndpoints } = require("./endpoints/api");
const setupTelemetry = require("./utils/telemetry");
const app = express();
const apiRouter = express.Router();
const FILE_LIMIT = "3GB";
@ -90,7 +90,6 @@ app.all("*", function (_, response) {
app
.listen(process.env.SERVER_PORT || 3001, async () => {
await validateTablePragmas();
await setupTelemetry();
console.log(
`Example app listening on port ${process.env.SERVER_PORT || 3001}`

View File

@ -1,122 +1,83 @@
const { Telemetry } = require("./telemetry");
const prisma = require("../utils/prisma");
const ApiKey = {
tablename: "api_keys",
writable: [],
colsInit: `
id INTEGER PRIMARY KEY AUTOINCREMENT,
secret TEXT UNIQUE,
createdBy INTEGER DEFAULT NULL,
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP
`,
migrateTable: async function () {
const { checkForMigrations } = require("../utils/database");
console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for ApiKey migrations`);
const db = await this.db(false);
await checkForMigrations(this, db);
},
migrations: function () {
return [];
},
makeSecret: () => {
const uuidAPIKey = require("uuid-apikey");
return uuidAPIKey.create().apiKey;
},
db: async function (tracing = true) {
const sqlite3 = require("sqlite3").verbose();
const { open } = require("sqlite");
const db = await open({
filename: `${
!!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "storage/"
}anythingllm.db`,
driver: sqlite3.Database,
});
await db.exec(
`PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
);
if (tracing) db.on("trace", (sql) => console.log(sql));
return db;
},
create: async function (createdByUserId = null) {
const db = await this.db();
const { id, success, message } = await db
.run(`INSERT INTO ${this.tablename} (secret, createdBy) VALUES(?, ?)`, [
this.makeSecret(),
createdByUserId,
])
.then((res) => {
return { id: res.lastID, success: true, message: null };
})
.catch((error) => {
return { id: null, success: false, message: error.message };
try {
const apiKey = await prisma.api_keys.create({
data: {
secret: this.makeSecret(),
createdBy: createdByUserId,
},
});
if (!success) {
db.close();
console.error("FAILED TO CREATE API KEY.", message);
return { apiKey: null, error: message };
}
const apiKey = await db.get(
`SELECT * FROM ${this.tablename} WHERE id = ${id} `
);
db.close();
await Telemetry.sendTelemetry("api_key_created");
return { apiKey, error: null };
} catch (error) {
console.error("FAILED TO CREATE API KEY.", error.message);
return { apiKey: null, error: error.message };
}
},
get: async function (clause = "") {
const db = await this.db();
const result = await db
.get(
`SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : clause}`
)
.then((res) => res || null);
if (!result) return null;
db.close();
return { ...result };
},
count: async function (clause = null) {
const db = await this.db();
const { count } = await db.get(
`SELECT COUNT(*) as count FROM ${this.tablename} ${
clause ? `WHERE ${clause}` : ""
} `
);
db.close();
get: async function (clause = {}) {
try {
const apiKey = await prisma.api_keys.findFirst({ where: clause });
return apiKey;
} catch (error) {
console.error("FAILED TO GET API KEY.", error.message);
return null;
}
},
count: async function (clause = {}) {
try {
const count = await prisma.api_keys.count({ where: clause });
return count;
} catch (error) {
console.error("FAILED TO COUNT API KEYS.", error.message);
return 0;
}
},
delete: async function (clause = "") {
const db = await this.db();
await db.get(
`DELETE FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""}`
);
db.close();
delete: async function (clause = {}) {
try {
await prisma.api_keys.deleteMany({ where: clause });
return true;
} catch (error) {
console.error("FAILED TO DELETE API KEY.", error.message);
return false;
}
},
where: async function (clause = "", limit = null) {
const db = await this.db();
const results = await db.all(
`SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${
!!limit ? `LIMIT ${limit}` : ""
}`
);
db.close();
return results;
where: async function (clause = {}, limit) {
try {
const apiKeys = await prisma.api_keys.findMany({
where: clause,
take: limit,
});
return apiKeys;
} catch (error) {
console.error("FAILED TO GET API KEYS.", error.message);
return [];
}
},
whereWithUser: async function (clause = "", limit = null) {
whereWithUser: async function (clause = {}, limit) {
try {
const { User } = require("./user");
const apiKeys = await this.where(clause, limit);
for (const apiKey of apiKeys) {
if (!apiKey.createdBy) continue;
const user = await User.get(`id = ${apiKey.createdBy}`);
const user = await User.get({ id: apiKey.createdBy });
if (!user) continue;
apiKey.createdBy = {
@ -127,6 +88,10 @@ const ApiKey = {
}
return apiKeys;
} catch (error) {
console.error("FAILED TO GET API KEYS WITH USER.", error.message);
return [];
}
},
};

View File

@ -1,76 +1,42 @@
const { fileData } = require("../utils/files");
const { v4: uuidv4 } = require("uuid");
const { getVectorDbClass } = require("../utils/helpers");
const { checkForMigrations } = require("../utils/database");
const prisma = require("../utils/prisma");
const { Telemetry } = require("./telemetry");
const Document = {
tablename: "workspace_documents",
colsInit: `
id INTEGER PRIMARY KEY AUTOINCREMENT,
docId TEXT NOT NULL UNIQUE,
filename TEXT NOT NULL,
docpath TEXT NOT NULL,
workspaceId INTEGER NOT NULL,
metadata TEXT NULL,
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP
`,
migrateTable: async function () {
console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for Document migrations`);
const db = await this.db(false);
await checkForMigrations(this, db);
},
migrations: function () {
return [];
},
db: async function (tracing = true) {
const sqlite3 = require("sqlite3").verbose();
const { open } = require("sqlite");
const db = await open({
filename: `${
!!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "storage/"
}anythingllm.db`,
driver: sqlite3.Database,
});
await db.exec(
`PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
);
if (tracing) db.on("trace", (sql) => console.log(sql));
return db;
},
forWorkspace: async function (workspaceId = null) {
if (!workspaceId) return [];
return await this.where(`workspaceId = ${workspaceId}`);
return await prisma.workspace_documents.findMany({
where: { workspaceId },
});
},
delete: async function (clause = "") {
const db = await this.db();
await db.get(`DELETE FROM ${this.tablename} WHERE ${clause}`);
db.close();
return true;
},
where: async function (clause = "", limit = null) {
const db = await this.db();
const results = await db.all(
`SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${
!!limit ? `LIMIT ${limit}` : ""
}`
);
db.close();
return results;
delete: async function (clause = {}) {
try {
await prisma.workspace_documents.deleteMany({ where: clause });
return true;
} catch (error) {
console.error(error.message);
return false;
}
},
firstWhere: async function (clause = "") {
const results = await this.where(clause);
return results.length > 0 ? results[0] : null;
firstWhere: async function (clause = {}) {
try {
const document = await prisma.workspace_documents.findFirst({
where: clause,
});
return document || null;
} catch (error) {
console.error(error.message);
return null;
}
},
addDocuments: async function (workspace, additions = []) {
const VectorDb = getVectorDbClass();
if (additions.length === 0) return;
const insertParams = [];
for (const path of additions) {
const data = await fileData(path);
@ -82,7 +48,7 @@ const Document = {
docId,
filename: path.split("/")[1],
docpath: path,
workspaceId: Number(workspace.id),
workspaceId: workspace.id,
metadata: JSON.stringify(metadata),
};
const vectorized = await VectorDb.addDocumentToNamespace(
@ -95,72 +61,44 @@ const Document = {
continue;
}
insertParams.push([
docId,
newDoc.filename,
newDoc.docpath,
newDoc.workspaceId,
newDoc.metadata,
]);
}
const db = await this.db();
const stmt = await db.prepare(
`INSERT INTO ${this.tablename} (docId, filename, docpath, workspaceId, metadata) VALUES (?,?,?,?,?)`
);
await db.exec("BEGIN TRANSACTION");
try {
for (const params of insertParams) {
await stmt.run(params);
await prisma.workspace_documents.create({ data: newDoc });
} catch (error) {
console.error(error.message);
}
await db.exec("COMMIT");
} catch {
await db.exec("ROLLBACK");
}
stmt.finalize();
db.close();
await Telemetry.sendTelemetry("documents_embedded_in_workspace", {
LLMSelection: process.env.LLM_PROVIDER || "openai",
VectorDbSelection: process.env.VECTOR_DB || "pinecone",
});
return;
},
removeDocuments: async function (workspace, removals = []) {
const VectorDb = getVectorDbClass();
const deleteParams = [];
if (removals.length === 0) return;
for (const path of removals) {
const document = await this.firstWhere(
`docPath = '${path}' AND workspaceId = ${workspace.id}`
);
const document = await this.firstWhere({
docpath: path,
workspaceId: workspace.id,
});
if (!document) continue;
await VectorDb.deleteDocumentFromNamespace(
workspace.slug,
document.docId
);
deleteParams.push([path, workspace.id]);
}
const db = await this.db();
const stmt = await db.prepare(
`DELETE FROM ${this.tablename} WHERE docpath = ? AND workspaceId = ?`
);
await db.exec("BEGIN TRANSACTION");
try {
for (const params of deleteParams) {
await stmt.run(params);
await prisma.workspace_documents.delete({
where: { id: document.id, workspaceId: workspace.id },
});
} catch (error) {
console.error(error.message);
}
await db.exec("COMMIT");
} catch {
await db.exec("ROLLBACK");
}
stmt.finalize();
db.close();
await Telemetry.sendTelemetry("documents_removed_in_workspace", {
LLMSelection: process.env.LLM_PROVIDER || "openai",
VectorDbSelection: process.env.VECTOR_DB || "pinecone",

View File

@ -1,192 +1,122 @@
const { escape } = require("sqlstring-sqlite");
const prisma = require("../utils/prisma");
const Invite = {
tablename: "invites",
writable: [],
colsInit: `
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
status TEXT NOT NULL DEFAULT "pending",
claimedBy INTEGER DEFAULT NULL,
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
createdBy INTEGER NOT NULL,
lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP
`,
migrateTable: async function () {
const { checkForMigrations } = require("../utils/database");
console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for Invites migrations`);
const db = await this.db(false);
await checkForMigrations(this, db);
},
migrations: function () {
return [];
},
makeCode: () => {
const uuidAPIKey = require("uuid-apikey");
return uuidAPIKey.create().apiKey;
},
db: async function (tracing = true) {
const sqlite3 = require("sqlite3").verbose();
const { open } = require("sqlite");
const db = await open({
filename: `${
!!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "storage/"
}anythingllm.db`,
driver: sqlite3.Database,
});
await db.exec(
`PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
);
if (tracing) db.on("trace", (sql) => console.log(sql));
return db;
},
create: async function (createdByUserId = 0) {
const db = await this.db();
const { id, success, message } = await db
.run(`INSERT INTO ${this.tablename} (code, createdBy) VALUES(?, ?)`, [
this.makeCode(),
createdByUserId,
])
.then((res) => {
return { id: res.lastID, success: true, message: null };
})
.catch((error) => {
return { id: null, success: false, message: error.message };
try {
const invite = await prisma.invites.create({
data: {
code: this.makeCode(),
createdBy: createdByUserId,
},
});
if (!success) {
db.close();
console.error("FAILED TO CREATE USER.", message);
return { invite: null, error: message };
}
const invite = await db.get(
`SELECT * FROM ${this.tablename} WHERE id = ${id} `
);
db.close();
return { invite, error: null };
} catch (error) {
console.error("FAILED TO CREATE INVITE.", error.message);
return { invite: null, error: error.message };
}
},
deactivate: async function (inviteId = null) {
const invite = await this.get(`id = ${escape(inviteId)}`);
if (!invite) return { success: false, error: "Invite does not exist." };
if (invite.status !== "pending")
return { success: false, error: "Invite is not in pending status." };
const db = await this.db();
const { success, message } = await db
.run(`UPDATE ${this.tablename} SET status=? WHERE id=?`, [
"disabled",
inviteId,
])
.then(() => {
return { success: true, message: null };
})
.catch((error) => {
return { success: false, message: error.message };
try {
const invite = await prisma.invites.update({
where: { id: Number(inviteId) },
data: { status: "disabled" },
});
db.close();
if (!success) {
console.error(message);
return { success: false, error: message };
}
return { success: true, error: null };
} catch (error) {
console.error(error.message);
return { success: false, error: error.message };
}
},
markClaimed: async function (inviteId = null, user) {
const invite = await this.get(`id = ${escape(inviteId)}`);
if (!invite) return { success: false, error: "Invite does not exist." };
if (invite.status !== "pending")
return { success: false, error: "Invite is not in pending status." };
const db = await this.db();
const { success, message } = await db
.run(`UPDATE ${this.tablename} SET status=?,claimedBy=? WHERE id=?`, [
"claimed",
user.id,
inviteId,
])
.then(() => {
return { success: true, message: null };
})
.catch((error) => {
return { success: false, message: error.message };
try {
const invite = await prisma.invites.update({
where: { id: Number(inviteId) },
data: { status: "claimed", claimedBy: user.id },
});
db.close();
if (!success) {
console.error(message);
return { success: false, error: message };
}
return { success: true, error: null };
} catch (error) {
console.error(error.message);
return { success: false, error: error.message };
}
},
get: async function (clause = "") {
const db = await this.db();
const result = await db
.get(
`SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : clause}`
)
.then((res) => res || null);
if (!result) return null;
db.close();
return { ...result };
},
count: async function (clause = null) {
const db = await this.db();
const { count } = await db.get(
`SELECT COUNT(*) as count FROM ${this.tablename} ${
clause ? `WHERE ${clause}` : ""
} `
);
db.close();
get: async function (clause = {}) {
try {
const invite = await prisma.invites.findFirst({ where: clause });
return invite || null;
} catch (error) {
console.error(error.message);
return null;
}
},
count: async function (clause = {}) {
try {
const count = await prisma.invites.count({ where: clause });
return count;
} catch (error) {
console.error(error.message);
return 0;
}
},
delete: async function (clause = "") {
const db = await this.db();
await db.get(`DELETE FROM ${this.tablename} WHERE ${clause}`);
db.close();
delete: async function (clause = {}) {
try {
await prisma.invites.deleteMany({ where: clause });
return true;
} catch (error) {
console.error(error.message);
return false;
}
},
where: async function (clause = "", limit = null) {
const db = await this.db();
const results = await db.all(
`SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${
!!limit ? `LIMIT ${limit}` : ""
}`
);
db.close();
return results;
where: async function (clause = {}, limit) {
try {
const invites = await prisma.invites.findMany({
where: clause,
take: limit || undefined,
});
return invites;
} catch (error) {
console.error(error.message);
return [];
}
},
whereWithUsers: async function (clause = "", limit = null) {
whereWithUsers: async function (clause = {}, limit) {
const { User } = require("./user");
const results = await this.where(clause, limit);
for (const invite of results) {
if (!!invite.claimedBy) {
const acceptedUser = await User.get(`id = ${invite.claimedBy}`);
try {
const invites = await this.where(clause, limit);
for (const invite of invites) {
if (invite.claimedBy) {
const acceptedUser = await User.get({ id: invite.claimedBy });
invite.claimedBy = {
id: acceptedUser?.id,
username: acceptedUser?.username,
};
}
if (!!invite.createdBy) {
const createdUser = await User.get(`id = ${invite.createdBy}`);
if (invite.createdBy) {
const createdUser = await User.get({ id: invite.createdBy });
invite.createdBy = {
id: createdUser?.id,
username: createdUser?.username,
};
}
}
return results;
return invites;
} catch (error) {
console.error(error.message);
return [];
}
},
};
module.exports.Invite = Invite;
module.exports = { Invite };

View File

@ -2,6 +2,8 @@ process.env.NODE_ENV === "development"
? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` })
: require("dotenv").config();
const prisma = require("../utils/prisma");
const SystemSettings = {
supportedFields: [
"multi_user_mode",
@ -11,44 +13,6 @@ const SystemSettings = {
"logo_filename",
"telemetry_id",
],
privateField: [],
tablename: "system_settings",
colsInit: `
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT UNIQUE NOT NULL,
value TEXT,
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP
`,
migrateTable: async function () {
const { checkForMigrations } = require("../utils/database");
console.log(
`\x1b[34m[MIGRATING]\x1b[0m Checking for System Setting migrations`
);
const db = await this.db(false);
await checkForMigrations(this, db);
},
migrations: function () {
return [];
},
db: async function (tracing = true) {
const sqlite3 = require("sqlite3").verbose();
const { open } = require("sqlite");
const db = await open({
filename: `${
!!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "storage/"
}anythingllm.db`,
driver: sqlite3.Database,
});
await db.exec(
`PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
);
if (tracing) db.on("trace", (sql) => console.log(sql));
return db;
},
currentSettings: async function () {
const llmProvider = process.env.LLM_PROVIDER || "openai";
const vectorDB = process.env.VECTOR_DB || "pinecone";
@ -102,89 +66,83 @@ const SystemSettings = {
: {}),
};
},
get: async function (clause = "") {
const db = await this.db();
const result = await db
.get(`SELECT * FROM ${this.tablename} WHERE ${clause}`)
.then((res) => res || null);
if (!result) return null;
db.close();
return result;
get: async function (clause = {}) {
try {
const setting = await prisma.system_settings.findFirst({ where: clause });
return setting || null;
} catch (error) {
console.error(error.message);
return null;
}
},
where: async function (clause = null, limit = null) {
const db = await this.db();
const results = await db.all(
`SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${
!!limit ? `LIMIT ${limit}` : ""
}`
);
db.close();
return results;
where: async function (clause = {}, limit) {
try {
const settings = await prisma.system_settings.findMany({
where: clause,
take: limit || undefined,
});
return settings;
} catch (error) {
console.error(error.message);
return [];
}
},
updateSettings: async function (updates = {}) {
const validConfigKeys = Object.keys(updates).filter((key) =>
this.supportedFields.includes(key)
);
for (const key of validConfigKeys) {
const existingRecord = await this.get(`label = '${key}'`);
if (!existingRecord) {
const db = await this.db();
const value = updates[key] === null ? null : String(updates[key]);
const { success, message } = await db
.run(`INSERT INTO ${this.tablename} (label, value) VALUES (?, ?)`, [
key,
value,
])
.then((res) => {
return { id: res.lastID, success: true, message: null };
})
.catch((error) => {
return { id: null, success: false, message: error.message };
try {
const updatePromises = Object.keys(updates)
.filter((key) => this.supportedFields.includes(key))
.map((key) => {
return prisma.system_settings.upsert({
where: { label: key },
update: {
value: updates[key] === null ? null : String(updates[key]),
},
create: {
label: key,
value: updates[key] === null ? null : String(updates[key]),
},
});
db.close();
if (!success) {
console.error("FAILED TO ADD SYSTEM CONFIG OPTION", message);
return { success: false, error: message };
}
} else {
const db = await this.db();
const value = updates[key] === null ? null : String(updates[key]);
const { success, message } = await db
.run(`UPDATE ${this.tablename} SET label=?,value=? WHERE id = ?`, [
key,
value,
existingRecord.id,
])
.then(() => {
return { success: true, message: null };
})
.catch((error) => {
return { success: false, message: error.message };
});
db.close();
if (!success) {
console.error("FAILED TO UPDATE SYSTEM CONFIG OPTION", message);
return { success: false, error: message };
}
}
}
await Promise.all(updatePromises);
return { success: true, error: null };
} catch (error) {
console.error("FAILED TO UPDATE SYSTEM SETTINGS", error.message);
return { success: false, error: error.message };
}
},
isMultiUserMode: async function () {
return (await this.get(`label = 'multi_user_mode'`))?.value === "true";
try {
const setting = await this.get({ label: "multi_user_mode" });
return setting?.value === "true";
} catch (error) {
console.error(error.message);
return false;
}
},
currentLogoFilename: async function () {
const result = await this.get(`label = 'logo_filename'`);
return result ? result.value : null;
try {
const setting = await this.get({ label: "logo_filename" });
return setting?.value || null;
} catch (error) {
console.error(error.message);
return null;
}
},
canDeleteWorkspaces: async function () {
return (
(await this.get(`label = 'users_can_delete_workspaces'`))?.value ===
"true"
);
try {
const setting = await this.get({ label: "users_can_delete_workspaces" });
return setting?.value === "true";
} catch (error) {
console.error(error.message);
return false;
}
},
};

View File

@ -6,26 +6,28 @@ const Telemetry = {
pubkey: "phc_9qu7QLpV8L84P3vFmEiZxL020t2EqIubP7HHHxrSsqS",
stubDevelopmentEvents: true, // [DO NOT TOUCH] Core team only.
label: "telemetry_id",
id: async function () {
const result = await SystemSettings.get(`label = '${this.label}'`);
if (!!result?.value) return result.value;
return result?.value;
const result = await SystemSettings.get({ label: this.label });
return result?.value || null;
},
connect: async function () {
const client = this.client();
const distinctId = await this.findOrCreateId();
return { client, distinctId };
},
isDev: function () {
if (process.env.NODE_ENV === "development")
return this.stubDevelopmentEvents;
return false;
return process.env.NODE_ENV === "development" && this.stubDevelopmentEvents;
},
client: function () {
if (process.env.DISABLE_TELEMETRY === "true" || this.isDev()) return null;
const { PostHog } = require("posthog-node");
return new PostHog(this.pubkey);
},
sendTelemetry: async function (event, properties = {}) {
try {
const { client, distinctId } = await this.connect();
@ -43,22 +45,25 @@ const Telemetry = {
return;
}
},
flush: async function () {
const { client } = this.client();
const client = this.client();
if (!client) return;
await client.shutdownAsync();
return;
},
setUid: async function () {
const newId = v4();
await SystemSettings.updateSettings({ [this.label]: newId });
return newId;
},
findOrCreateId: async function () {
const currentId = await this.id();
if (!!currentId) return currentId;
const newId = await this.setUid();
return newId;
let currentId = await this.id();
if (currentId) return currentId;
currentId = await this.setUid();
return currentId;
},
};

View File

@ -1,171 +1,78 @@
const { escape } = require("sqlstring-sqlite");
const prisma = require("../utils/prisma");
const bcrypt = require("bcrypt");
const User = {
tablename: "users",
writable: [],
colsInit: `
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE,
password TEXT NOT NULL,
role TEXT NOT NULL DEFAULT "default",
suspended INTEGER NOT NULL DEFAULT 0,
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP
`,
migrateTable: async function () {
const { checkForMigrations } = require("../utils/database");
console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for User migrations`);
const db = await this.db(false);
await checkForMigrations(this, db);
create: async function ({ username, password, role = "default" }) {
try {
const hashedPassword = bcrypt.hashSync(password, 10);
const user = await prisma.users.create({
data: {
username,
password: hashedPassword,
role,
},
migrations: function () {
return [];
},
db: async function (tracing = true) {
const sqlite3 = require("sqlite3").verbose();
const { open } = require("sqlite");
const db = await open({
filename: `${
!!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "storage/"
}anythingllm.db`,
driver: sqlite3.Database,
});
await db.exec(
`PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
);
if (tracing) db.on("trace", (sql) => console.log(sql));
return db;
},
create: async function ({ username, password, role = null }) {
const bcrypt = require("bcrypt");
const db = await this.db();
const { id, success, message } = await db
.run(
`INSERT INTO ${this.tablename} (username, password, role) VALUES(?, ?, ?)`,
[username, bcrypt.hashSync(password, 10), role ?? "default"]
)
.then((res) => {
return { id: res.lastID, success: true, message: null };
})
.catch((error) => {
return { id: null, success: false, message: error.message };
});
if (!success) {
db.close();
console.error("FAILED TO CREATE USER.", message);
return { user: null, error: message };
}
const user = await db.get(
`SELECT * FROM ${this.tablename} WHERE id = ${id} `
);
db.close();
return { user, error: null };
} catch (error) {
console.error("FAILED TO CREATE USER.", error.message);
return { user: null, error: error.message };
}
},
update: async function (userId, updates = {}) {
const user = await this.get(`id = ${escape(userId)}`);
if (!user) return { success: false, error: "User does not exist." };
const { username, password, role, suspended = 0 } = updates;
const toUpdate = { suspended };
if (user.username !== username && username?.length > 0) {
const usedUsername = !!(await this.get(`username = ${escape(username)}`));
if (usedUsername)
return { success: false, error: `${username} is already in use.` };
toUpdate.username = username;
}
if (!!password) {
const bcrypt = require("bcrypt");
toUpdate.password = bcrypt.hashSync(password, 10);
}
if (user.role !== role && ["admin", "default"].includes(role)) {
// If was existing admin and that has been changed
// make sure at least one admin exists
if (user.role === "admin") {
const validAdminCount = (await this.count(`role = 'admin'`)) > 1;
if (!validAdminCount)
return {
success: false,
error: `There would be no admins if this action was completed. There must be at least one admin.`,
};
}
toUpdate.role = role;
}
if (Object.keys(toUpdate).length !== 0) {
const values = Object.values(toUpdate);
const template = `UPDATE ${this.tablename} SET ${Object.keys(
toUpdate
).map((key) => {
return `${key}=?`;
})} WHERE id = ?`;
const db = await this.db();
const { success, message } = await db
.run(template, [...values, userId])
.then(() => {
return { success: true, message: null };
})
.catch((error) => {
return { success: false, message: error.message };
try {
const updatedUser = await prisma.users.update({
where: { id: parseInt(userId) },
data: updates,
});
db.close();
if (!success) {
console.error(message);
return { success: false, error: message };
}
}
return { success: true, error: null };
} catch (error) {
console.error(error.message);
return { success: false, error: error.message };
}
},
get: async function (clause = "") {
const db = await this.db();
const result = await db
.get(
`SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : clause}`
)
.then((res) => res || null);
if (!result) return null;
db.close();
return { ...result };
},
count: async function (clause = null) {
const db = await this.db();
const { count } = await db.get(
`SELECT COUNT(*) as count FROM ${this.tablename} ${
clause ? `WHERE ${clause}` : ""
} `
);
db.close();
get: async function (clause = {}) {
try {
const user = await prisma.users.findFirst({ where: clause });
return user ? { ...user } : null;
} catch (error) {
console.error(error.message);
return null;
}
},
count: async function (clause = {}) {
try {
const count = await prisma.users.count({ where: clause });
return count;
} catch (error) {
console.error(error.message);
return 0;
}
},
delete: async function (clause = "") {
const db = await this.db();
await db.get(`DELETE FROM ${this.tablename} WHERE ${clause}`);
db.close();
delete: async function (clause = {}) {
try {
await prisma.users.delete({ where: clause });
return true;
} catch (error) {
console.error(error.message);
return false;
}
},
where: async function (clause = "", limit = null) {
const db = await this.db();
const results = await db.all(
`SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${
!!limit ? `LIMIT ${limit}` : ""
}`
);
db.close();
return results;
where: async function (clause = {}, limit = null) {
try {
const users = await prisma.users.findMany({
where: clause,
...(limit !== null ? { take: limit } : {}),
});
return users;
} catch (error) {
console.error(error.message);
return [];
}
},
};

View File

@ -1,103 +1,68 @@
const { checkForMigrations } = require("../utils/database");
const prisma = require("../utils/prisma");
const { Document } = require("./documents");
// TODO: Do we want to store entire vectorized chunks in here
// so that we can easily spin up temp-namespace clones for threading
const DocumentVectors = {
tablename: "document_vectors",
colsInit: `
id INTEGER PRIMARY KEY AUTOINCREMENT,
docId TEXT NOT NULL,
vectorId TEXT NOT NULL,
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP
`,
migrateTable: async function () {
console.log(
`\x1b[34m[MIGRATING]\x1b[0m Checking for DocumentVector migrations`
);
const db = await this.db(false);
await checkForMigrations(this, db);
},
migrations: function () {
return [];
},
db: async function (tracing = true) {
const sqlite3 = require("sqlite3").verbose();
const { open } = require("sqlite");
const db = await open({
filename: `${
!!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "storage/"
}anythingllm.db`,
driver: sqlite3.Database,
});
await db.exec(
`PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
);
if (tracing) db.on("trace", (sql) => console.log(sql));
return db;
},
bulkInsert: async function (vectorRecords = []) {
if (vectorRecords.length === 0) return;
const db = await this.db();
// Build a single query string with multiple placeholders for the INSERT operation
const placeholders = vectorRecords.map(() => "(?, ?)").join(", ");
const stmt = await db.prepare(
`INSERT INTO ${this.tablename} (docId, vectorId) VALUES ${placeholders}`
);
// Flatten the vectorRecords array to match the order of placeholders
const values = vectorRecords.reduce(
(arr, record) => arr.concat([record.docId, record.vectorId]),
[]
);
await db.exec("BEGIN TRANSACTION");
try {
await stmt.run(values);
await db.exec("COMMIT");
} catch {
await db.exec("ROLLBACK");
}
stmt.finalize();
db.close();
return { documentsInserted: vectorRecords.length };
const inserts = [];
vectorRecords.forEach((record) => {
inserts.push(
prisma.document_vectors.create({
data: {
docId: record.docId,
vectorId: record.vectorId,
},
})
);
});
await prisma.$transaction(inserts);
return { documentsInserted: inserts.length };
} catch (error) {
console.error("Bulk insert failed", error);
return { documentsInserted: 0 };
}
},
deleteForWorkspace: async function (workspaceId) {
const documents = await Document.forWorkspace(workspaceId);
const docIds = [...new Set(documents.map((doc) => doc.docId))];
const ids = (
await this.where(`docId IN (${docIds.map((id) => `'${id}'`).join(",")})`)
).map((doc) => doc.id);
await this.deleteIds(ids);
return true;
},
where: async function (clause = "", limit = null) {
const db = await this.db();
const results = await db.all(
`SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${
!!limit ? `LIMIT ${limit}` : ""
}`
);
db.close();
return results;
},
deleteIds: async function (ids = []) {
const db = await this.db();
await db.get(
`DELETE FROM ${this.tablename} WHERE id IN (${ids.join(", ")}) `
);
db.close();
try {
await prisma.document_vectors.deleteMany({
where: { docId: { in: docIds } },
});
return true;
} catch (error) {
console.error("Delete for workspace failed", error);
return false;
}
},
where: async function (clause = {}, limit) {
try {
const results = await prisma.document_vectors.findMany({
where: clause,
take: limit || undefined,
});
return results;
} catch (error) {
console.error("Where query failed", error);
return [];
}
},
deleteIds: async function (ids = []) {
try {
await prisma.document_vectors.deleteMany({
where: { id: { in: ids } },
});
return true;
} catch (error) {
console.error("Delete IDs failed", error);
return false;
}
},
};

View File

@ -1,88 +1,61 @@
const prisma = require("../utils/prisma");
const WelcomeMessages = {
tablename: "welcome_messages",
colsInit: `
id INTEGER PRIMARY KEY AUTOINCREMENT,
user TEXT NOT NULL,
response TEXT NOT NULL,
orderIndex INTEGER,
createdAt TEXT DEFAULT CURRENT_TIMESTAMP
`,
migrateTable: async function () {
const { checkForMigrations } = require("../utils/database");
console.log(
`\x1b[34m[MIGRATING]\x1b[0m Checking for Welcome Messages migrations`
);
const db = await this.db(false);
await checkForMigrations(this, db);
db.close();
},
migrations: function () {
return [];
},
db: async function (tracing = true) {
const sqlite3 = require("sqlite3").verbose();
const { open } = require("sqlite");
const db = await open({
filename: `${
!!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "storage/"
}anythingllm.db`,
driver: sqlite3.Database,
get: async function (clause = {}) {
try {
const message = await prisma.welcome_messages.findFirst({
where: clause,
});
await db.exec(
`PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
);
if (tracing) {
db.on("trace", (sql) => console.log(sql));
return message || null;
} catch (error) {
console.error(error.message);
return null;
}
return db;
},
get: async function (clause = "") {
const db = await this.db();
const result = await db
.get(`SELECT * FROM ${this.tablename} WHERE ${clause}`)
.then((res) => res || null);
db.close();
return result;
},
where: async function (clause = null, limit = null) {
const db = await this.db();
const results = await db.all(
`SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${
!!limit ? `LIMIT ${limit}` : ""
}`
);
db.close();
return results;
where: async function (clause = {}, limit) {
try {
const messages = await prisma.welcome_messages.findMany({
where: clause,
take: limit || undefined,
});
return messages;
} catch (error) {
console.error(error.message);
return [];
}
},
saveAll: async function (messages) {
const db = await this.db();
await db.run(`DELETE FROM ${this.tablename}`);
try {
await prisma.welcome_messages.deleteMany({}); // Delete all existing messages
// Create new messages
for (const [index, message] of messages.entries()) {
await db.run(
`INSERT INTO ${this.tablename} (user, response, orderIndex) VALUES (?, ?, ?)`,
[message.user, message.response, index]
);
await prisma.welcome_messages.create({
data: {
user: message.user,
response: message.response,
orderIndex: index,
},
});
}
} catch (error) {
console.error("Failed to save all messages", error.message);
}
db.close();
},
getMessages: async function () {
const db = await this.db();
const results = await db.all(
`SELECT user, response FROM ${this.tablename} ORDER BY orderIndex ASC`
);
db.close();
return results;
try {
const messages = await prisma.welcome_messages.findMany({
orderBy: { orderIndex: "asc" },
select: { user: true, response: true },
});
return messages;
} catch (error) {
console.error("Failed to get all messages", error.message);
return [];
}
},
};

View File

@ -1,11 +1,9 @@
const prisma = require("../utils/prisma");
const slugify = require("slugify");
const { Document } = require("./documents");
const { checkForMigrations } = require("../utils/database");
const { WorkspaceUser } = require("./workspaceUsers");
const { escape } = require("sqlstring-sqlite");
const Workspace = {
tablename: "workspaces",
writable: [
// Used for generic updates so we can validate keys in request body
"name",
@ -16,223 +14,180 @@ const Workspace = {
"lastUpdatedAt",
"openAiPrompt",
],
colsInit: `
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
vectorTag TEXT DEFAULT NULL,
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
openAiTemp REAL DEFAULT NULL,
openAiHistory INTEGER DEFAULT 20,
lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
openAiPrompt TEXT DEFAULT NULL
`,
migrateTable: async function () {
console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for Workspace migrations`);
const db = await this.db(false);
await checkForMigrations(this, db);
},
migrations: function () {
return [
{
colName: "openAiTemp",
execCmd: `ALTER TABLE ${this.tablename} ADD COLUMN openAiTemp REAL DEFAULT NULL`,
doif: false,
},
{
colName: "openAiPrompt",
execCmd: `ALTER TABLE ${this.tablename} ADD COLUMN openAiPrompt TEXT DEFAULT NULL`,
doif: false,
},
{
colName: "id",
execCmd: `CREATE TRIGGER IF NOT EXISTS Trg_LastUpdated AFTER UPDATE ON ${this.tablename}
FOR EACH ROW
BEGIN
UPDATE ${this.tablename} SET lastUpdatedAt = CURRENT_TIMESTAMP WHERE id = old.id;
END`,
doif: true,
},
{
colName: "openAiHistory",
execCmd: `ALTER TABLE ${this.tablename} ADD COLUMN openAiHistory INTEGER DEFAULT 20`,
doif: false,
},
];
},
db: async function (tracing = true) {
const sqlite3 = require("sqlite3").verbose();
const { open } = require("sqlite");
const db = await open({
filename: `${
!!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "storage/"
}anythingllm.db`,
driver: sqlite3.Database,
});
await db.exec(
`PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
);
if (tracing) db.on("trace", (sql) => console.log(sql));
return db;
},
new: async function (name = null, creatorId = null) {
if (!name) return { result: null, message: "name cannot be null" };
var slug = slugify(name, { lower: true });
const existingBySlug = await this.get(`slug = ${escape(slug)}`);
const existingBySlug = await this.get({ slug });
if (existingBySlug !== null) {
const slugSeed = Math.floor(10000000 + Math.random() * 90000000);
slug = slugify(`${name}-${slugSeed}`, { lower: true });
}
const db = await this.db();
const { id, success, message } = await db
.run(`INSERT INTO ${this.tablename} (name, slug) VALUES (?, ?)`, [
name,
slug,
])
.then((res) => {
return { id: res.lastID, success: true, message: null };
})
.catch((error) => {
return { id: null, success: false, message: error.message };
try {
const workspace = await prisma.workspaces.create({
data: { name, slug },
});
if (!success) {
db.close();
return { workspace: null, message };
}
const workspace = await db.get(
`SELECT * FROM ${this.tablename} WHERE id = ${id}`
);
db.close();
// If created with a user then we need to create the relationship as well.
// If creating with an admin User it wont change anything because admins can
// view all workspaces anyway.
if (!!creatorId) await WorkspaceUser.create(creatorId, workspace.id);
return { workspace, message: null };
} catch (error) {
console.error(error.message);
return { workspace: null, message: error.message };
}
},
update: async function (id = null, data = {}) {
if (!id) throw new Error("No workspace id provided for update");
const validKeys = Object.keys(data).filter((key) =>
this.writable.includes(key)
);
const values = Object.values(data);
if (validKeys.length === 0 || validKeys.length !== values.length)
if (validKeys.length === 0)
return { workspace: { id }, message: "No valid fields to update!" };
const template = `UPDATE ${this.tablename} SET ${validKeys.map((key) => {
return `${key}=?`;
})} WHERE id = ?`;
const db = await this.db();
const { success, message } = await db
.run(template, [...values, id])
.then(() => {
return { success: true, message: null };
})
.catch((error) => {
return { success: false, message: error.message };
try {
const workspace = await prisma.workspaces.update({
where: { id },
data,
});
db.close();
if (!success) {
return { workspace: null, message };
return { workspace, message: null };
} catch (error) {
console.error(error.message);
return { workspace: null, message: error.message };
}
const updatedWorkspace = await this.get(`id = ${id}`);
return { workspace: updatedWorkspace, message: null };
},
getWithUser: async function (user = null, clause = "") {
getWithUser: async function (user = null, clause = {}) {
if (user.role === "admin") return this.get(clause);
const db = await this.db();
const result = await db
.get(
`SELECT * FROM ${this.tablename} as workspace
LEFT JOIN workspace_users as ws_users
ON ws_users.workspace_id = workspace.id
WHERE ws_users.user_id = ${user?.id} AND ${clause}`
)
.then((res) => res || null);
if (!result) return null;
db.close();
const workspace = { ...result, id: result.workspace_id };
const documents = await Document.forWorkspace(workspace.id);
return { ...workspace, documents };
try {
const workspace = await prisma.workspaces.findFirst({
where: {
...clause,
workspace_users: {
some: {
user_id: user?.id,
},
get: async function (clause = "") {
const db = await this.db();
const result = await db
.get(`SELECT * FROM ${this.tablename} WHERE ${clause}`)
.then((res) => res || null);
if (!result) return null;
db.close();
const documents = await Document.forWorkspace(result.id);
return { ...result, documents };
},
delete: async function (clause = "") {
const db = await this.db();
await db.get(`DELETE FROM ${this.tablename} WHERE ${clause}`);
db.close();
},
include: {
workspace_users: true,
documents: true,
},
});
if (!workspace) return null;
return {
...workspace,
documents: await Document.forWorkspace(workspace.id),
};
} catch (error) {
console.error(error.message);
return null;
}
},
get: async function (clause = {}) {
try {
const workspace = await prisma.workspaces.findFirst({
where: clause,
include: {
documents: true,
},
});
return workspace || null;
} catch (error) {
console.error(error.message);
return null;
}
},
delete: async function (clause = {}) {
try {
await prisma.workspaces.delete({
where: clause,
});
return true;
} catch (error) {
console.error(error.message);
return false;
}
},
where: async function (clause = "", limit = null, orderBy = null) {
const db = await this.db();
const results = await db.all(
`SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${
!!limit ? `LIMIT ${limit}` : ""
} ${!!orderBy ? orderBy : ""}`
);
db.close();
where: async function (clause = {}, limit = null, orderBy = null) {
try {
const results = await prisma.workspaces.findMany({
where: clause,
...(limit !== null ? { take: limit } : {}),
...(orderBy !== null ? { orderBy } : {}),
});
return results;
} catch (error) {
console.error(error.message);
return [];
}
},
whereWithUser: async function (
user,
clause = null,
clause = {},
limit = null,
orderBy = null
) {
if (user.role === "admin") return await this.where(clause, limit);
const db = await this.db();
const results = await db.all(
`SELECT * FROM ${this.tablename} as workspace
LEFT JOIN workspace_users as ws_users
ON ws_users.workspace_id = workspace.id
WHERE ws_users.user_id = ${user.id} ${clause ? `AND ${clause}` : ""} ${
!!limit ? `LIMIT ${limit}` : ""
} ${!!orderBy ? orderBy : ""}`
);
db.close();
const workspaces = results.map((ws) => {
return { ...ws, id: ws.workspace_id };
});
if (user.role === "admin") return await this.where(clause, limit, orderBy);
return workspaces;
try {
const workspaces = await prisma.workspaces.findMany({
where: {
...clause,
workspace_users: {
some: {
user_id: user.id,
},
whereWithUsers: async function (clause = "", limit = null, orderBy = null) {
},
},
...(limit !== null ? { take: limit } : {}),
...(orderBy !== null ? { orderBy } : {}),
});
return workspaces;
} catch (error) {
console.error(error.message);
return [];
}
},
whereWithUsers: async function (clause = {}, limit = null, orderBy = null) {
try {
const workspaces = await this.where(clause, limit, orderBy);
for (const workspace of workspaces) {
const userIds = (
await WorkspaceUser.where(`workspace_id = ${workspace.id}`)
await WorkspaceUser.where({ workspace_id: Number(workspace.id) })
).map((rel) => rel.user_id);
workspace.userIds = userIds;
}
return workspaces;
} catch (error) {
console.error(error.message);
return [];
}
},
updateUsers: async function (workspaceId, userIds = []) {
await WorkspaceUser.delete(`workspace_id = ${workspaceId}`);
try {
await WorkspaceUser.delete({ workspace_id: Number(workspaceId) });
await WorkspaceUser.createManyUsers(userIds, workspaceId);
return { success: true, error: null };
} catch (error) {
console.error(error.message);
return { success: false, error: error.message };
}
},
};

View File

@ -1,170 +1,160 @@
const { checkForMigrations } = require("../utils/database");
const prisma = require("../utils/prisma");
const WorkspaceChats = {
tablename: "workspace_chats",
colsInit: `
id INTEGER PRIMARY KEY AUTOINCREMENT,
workspaceId INTEGER NOT NULL,
prompt TEXT NOT NULL,
response TEXT NOT NULL,
include BOOL DEFAULT true,
user_id INTEGER DEFAULT NULL,
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
`,
migrateTable: async function () {
console.log(
`\x1b[34m[MIGRATING]\x1b[0m Checking for WorkspaceChats migrations`
);
const db = await this.db(false);
await checkForMigrations(this, db);
},
migrations: function () {
return [
{
colName: "user_id",
execCmd: `ALTER TABLE ${this.tablename} ADD COLUMN user_id INTEGER DEFAULT NULL`,
doif: false,
},
];
},
db: async function (tracing = true) {
const sqlite3 = require("sqlite3").verbose();
const { open } = require("sqlite");
const db = await open({
filename: `${
!!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "storage/"
}anythingllm.db`,
driver: sqlite3.Database,
});
await db.exec(
`PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
);
if (tracing) db.on("trace", (sql) => console.log(sql));
return db;
},
new: async function ({ workspaceId, prompt, response = {}, user = null }) {
const db = await this.db();
const { id, success, message } = await db
.run(
`INSERT INTO ${this.tablename} (workspaceId, prompt, response, user_id) VALUES (?, ?, ?, ?)`,
[workspaceId, prompt, JSON.stringify(response), user?.id || null]
)
.then((res) => {
return { id: res.lastID, success: true, message: null };
})
.catch((error) => {
return { id: null, success: false, message: error.message };
});
if (!success) {
db.close();
return { chat: null, message };
}
const chat = await db.get(
`SELECT * FROM ${this.tablename} WHERE id = ${id}`
);
db.close();
return { chat, message: null };
try {
const chat = await prisma.workspace_chats.create({
data: {
workspaceId,
prompt,
response: JSON.stringify(response),
user_id: user?.id || null,
},
});
return { chat, message: null };
} catch (error) {
console.error(error.message);
return { chat: null, message: error.message };
}
},
forWorkspaceByUser: async function (
workspaceId = null,
userId = null,
limit = null
) {
if (!workspaceId || !userId) return [];
return await this.where(
`workspaceId = ${workspaceId} AND include = true AND user_id = ${userId}`,
limit,
"ORDER BY id ASC"
);
try {
const chats = await prisma.workspace_chats.findMany({
where: {
workspaceId,
user_id: userId,
},
...(limit !== null ? { take: limit } : {}),
orderBy: {
id: "asc",
},
});
return chats;
} catch (error) {
console.error(error.message);
return [];
}
},
forWorkspace: async function (workspaceId = null, limit = null) {
if (!workspaceId) return [];
return await this.where(
`workspaceId = ${workspaceId} AND include = true`,
limit,
"ORDER BY id ASC"
);
try {
const chats = await prisma.workspace_chats.findMany({
where: {
workspaceId,
},
...(limit !== null ? { take: limit } : {}),
orderBy: {
id: "asc",
},
});
return chats;
} catch (error) {
console.error(error.message);
return [];
}
},
markHistoryInvalid: async function (workspaceId = null, user = null) {
if (!workspaceId) return;
const db = await this.db();
await db.run(
`UPDATE ${this.tablename} SET include = false WHERE workspaceId = ? ${
user ? `AND user_id = ${user.id}` : ""
}`,
[workspaceId]
);
db.close();
try {
await prisma.workspace_chats.updateMany({
where: {
workspaceId,
user_id: user?.id,
},
data: {
include: false,
},
});
return;
} catch (error) {
console.error(error.message);
}
},
get: async function (clause = "", limit = null, order = null) {
const db = await this.db();
const result = await db
.get(
`SELECT * FROM ${this.tablename} WHERE ${clause} ${
!!order ? order : ""
} ${!!limit ? `LIMIT ${limit}` : ""}`
)
.then((res) => res || null);
db.close();
if (!result) return null;
return result;
get: async function (clause = {}, limit = null, orderBy = null) {
try {
const chat = await prisma.workspace_chats.findFirst({
where: clause,
...(limit !== null ? { take: limit } : {}),
...(orderBy !== null ? { orderBy } : {}),
});
return chat || null;
} catch (error) {
console.error(error.message);
return null;
}
},
delete: async function (clause = "") {
const db = await this.db();
await db.get(`DELETE FROM ${this.tablename} WHERE ${clause}`);
db.close();
delete: async function (clause = {}) {
try {
await prisma.workspace_chats.deleteMany({
where: clause,
});
return true;
} catch (error) {
console.error(error.message);
return false;
}
},
where: async function (clause = "", limit = null, order = null) {
const db = await this.db();
const results = await db.all(
`SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${
!!order ? order : ""
} ${!!limit ? `LIMIT ${limit}` : ""}`
);
db.close();
return results;
where: async function (clause = {}, limit = null, orderBy = null) {
try {
const chats = await prisma.workspace_chats.findMany({
where: clause,
...(limit !== null ? { take: limit } : {}),
...(orderBy !== null ? { orderBy } : {}),
});
return chats;
} catch (error) {
console.error(error.message);
return [];
}
},
count: async function (clause = null) {
const db = await this.db();
const { count } = await db.get(
`SELECT COUNT(*) as count FROM ${this.tablename} ${
clause ? `WHERE ${clause}` : ""
} `
);
db.close();
count: async function (clause = {}) {
try {
const count = await prisma.workspace_chats.count({
where: clause,
});
return count;
} catch (error) {
console.error(error.message);
return 0;
}
},
whereWithData: async function (clause = "", limit = null, order = null) {
whereWithData: async function (clause = {}, limit = null, orderBy = null) {
const { Workspace } = require("./workspace");
const { User } = require("./user");
const results = await this.where(clause, limit, order);
try {
const results = await this.where(clause, limit, orderBy);
for (const res of results) {
const workspace = await Workspace.get(`id = ${res.workspaceId}`);
const workspace = await Workspace.get({ id: res.workspaceId });
res.workspace = workspace
? { name: workspace.name, slug: workspace.slug }
: { name: "deleted workspace", slug: null };
const user = await User.get(`id = ${res.user_id}`);
const user = await User.get({ id: res.user_id });
res.user = user
? { username: user.username }
: { username: "deleted user" };
}
return results;
} catch (error) {
console.error(error.message);
return [];
}
},
};

View File

@ -1,142 +1,95 @@
const prisma = require("../utils/prisma");
const WorkspaceUser = {
tablename: "workspace_users",
colsInit: `
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
workspace_id INTEGER NOT NULL,
createdAt TEXT DEFAULT CURRENT_TIMESTAMP,
lastUpdatedAt TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (workspace_id) REFERENCES workspaces (id) ON DELETE CASCADE
`,
migrateTable: async function () {
const { checkForMigrations } = require("../utils/database");
console.log(
`\x1b[34m[MIGRATING]\x1b[0m Checking for Workspace User migrations`
);
const db = await this.db(false);
await checkForMigrations(this, db);
},
migrations: function () {
return [];
},
db: async function (tracing = true) {
const sqlite3 = require("sqlite3").verbose();
const { open } = require("sqlite");
const db = await open({
filename: `${
!!process.env.STORAGE_DIR ? `${process.env.STORAGE_DIR}/` : "storage/"
}anythingllm.db`,
driver: sqlite3.Database,
});
await db.exec(
`PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})`
);
if (tracing) db.on("trace", (sql) => console.log(sql));
return db;
},
createMany: async function (userId, workspaceIds = []) {
if (workspaceIds.length === 0) return;
const db = await this.db();
const stmt = await db.prepare(
`INSERT INTO ${this.tablename} (user_id, workspace_id) VALUES (?,?)`
);
await db.exec("BEGIN TRANSACTION");
try {
for (const workspaceId of workspaceIds) {
await stmt.run([userId, workspaceId]);
await prisma.$transaction(
workspaceIds.map((workspaceId) =>
prisma.workspace_users.create({
data: { user_id: userId, workspace_id: workspaceId },
})
)
);
} catch (error) {
console.error(error.message);
}
await db.exec("COMMIT");
} catch {
await db.exec("ROLLBACK");
}
stmt.finalize();
db.close();
return;
},
createManyUsers: async function (userIds = [], workspaceId) {
if (userIds.length === 0) return;
const db = await this.db();
const stmt = await db.prepare(
`INSERT INTO ${this.tablename} (user_id, workspace_id) VALUES (?,?)`
);
await db.exec("BEGIN TRANSACTION");
try {
for (const userId of userIds) {
await stmt.run([userId, workspaceId]);
await prisma.$transaction(
userIds.map((userId) =>
prisma.workspace_users.create({
data: {
user_id: Number(userId),
workspace_id: Number(workspaceId),
},
})
)
);
} catch (error) {
console.error(error.message);
}
await db.exec("COMMIT");
} catch {
await db.exec("ROLLBACK");
}
stmt.finalize();
db.close();
return;
},
create: async function (userId = 0, workspaceId = 0) {
const db = await this.db();
const { success, message } = await db
.run(
`INSERT INTO ${this.tablename} (user_id, workspace_id) VALUES (?, ?)`,
[userId, workspaceId]
)
.then((res) => {
return { id: res.lastID, success: true, message: null };
})
.catch((error) => {
return { id: null, success: false, message: error.message };
});
if (!success) {
db.close();
console.error("FAILED TO CREATE WORKSPACE_USER RELATIONSHIP.", message);
create: async function (userId = 0, workspaceId = 0) {
try {
await prisma.workspace_users.create({
data: { user_id: Number(userId), workspace_id: Number(workspaceId) },
});
return true;
} catch (error) {
console.error(
"FAILED TO CREATE WORKSPACE_USER RELATIONSHIP.",
error.message
);
return false;
}
return true;
},
get: async function (clause = "") {
const db = await this.db();
const result = await db
.get(`SELECT * FROM ${this.tablename} WHERE ${clause}`)
.then((res) => res || null);
if (!result) return null;
db.close();
return result;
get: async function (clause = {}) {
try {
const result = await prisma.workspace_users.findFirst({ where: clause });
return result || null;
} catch (error) {
console.error(error.message);
return null;
}
},
where: async function (clause = null, limit = null) {
const db = await this.db();
const results = await db.all(
`SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${
!!limit ? `LIMIT ${limit}` : ""
}`
);
db.close();
where: async function (clause = {}, limit = null) {
try {
const results = await prisma.workspace_users.findMany({
where: clause,
...(limit !== null ? { take: limit } : {}),
});
return results;
} catch (error) {
console.error(error.message);
return [];
}
},
count: async function (clause = null) {
const db = await this.db();
const { count } = await db.get(
`SELECT COUNT(*) as count FROM ${this.tablename} ${
clause ? `WHERE ${clause}` : ""
}`
);
db.close();
count: async function (clause = {}) {
try {
const count = await prisma.workspace_users.count({ where: clause });
return count;
} catch (error) {
console.error(error.message);
return 0;
}
},
delete: async function (clause = null) {
const db = await this.db();
await db.get(`DELETE FROM ${this.tablename} WHERE ${clause}`);
delete: async function (clause = {}) {
try {
await prisma.workspace_users.deleteMany({ where: clause });
} catch (error) {
console.error(error.message);
}
return;
},
};

7905
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "anything-llm-server",
"version": "0.0.1-beta",
"version": "0.2.0",
"description": "Server endpoints to process or create content for chatting",
"main": "index.js",
"author": "Timothy Carambat (Mintplex Labs)",
@ -13,12 +13,17 @@
"dev": "NODE_ENV=development nodemon --ignore documents --ignore vector-cache --ignore storage --ignore swagger --trace-warnings index.js",
"start": "NODE_ENV=production node index.js",
"lint": "yarn prettier --write ./endpoints ./models ./utils index.js",
"swagger": "node ./swagger/init.js"
"swagger": "node ./swagger/init.js",
"sqlite:migrate": "cd ./utils/prisma && node migrateFromSqlite.js"
},
"prisma": {
"seed": "node prisma/seed.js"
},
"dependencies": {
"@azure/openai": "^1.0.0-beta.3",
"@googleapis/youtube": "^9.0.0",
"@pinecone-database/pinecone": "^0.1.6",
"@prisma/client": "5.3.0",
"@qdrant/js-client-rest": "^1.4.0",
"archiver": "^5.3.1",
"bcrypt": "^5.1.0",
@ -38,11 +43,11 @@
"openai": "^3.2.1",
"pinecone-client": "^1.1.0",
"posthog-node": "^3.1.1",
"prisma": "^5.3.1",
"serve-index": "^1.9.1",
"slugify": "^1.6.6",
"sqlite": "^4.2.1",
"sqlite3": "^5.1.6",
"sqlstring-sqlite": "^0.1.1",
"swagger-autogen": "^2.23.5",
"swagger-ui-express": "^5.0.0",
"uuid": "^9.0.0",

View File

@ -0,0 +1,125 @@
-- CreateTable
CREATE TABLE "api_keys" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"secret" TEXT,
"createdBy" INTEGER,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "workspace_documents" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"docId" TEXT NOT NULL,
"filename" TEXT NOT NULL,
"docpath" TEXT NOT NULL,
"workspaceId" INTEGER NOT NULL,
"metadata" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "workspace_documents_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "invites" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"code" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"claimedBy" INTEGER,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdBy" INTEGER NOT NULL,
"lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "system_settings" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"label" TEXT NOT NULL,
"value" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "users" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"username" TEXT,
"password" TEXT NOT NULL,
"role" TEXT NOT NULL DEFAULT 'default',
"suspended" INTEGER NOT NULL DEFAULT 0,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "document_vectors" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"docId" TEXT NOT NULL,
"vectorId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "welcome_messages" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"user" TEXT NOT NULL,
"response" TEXT NOT NULL,
"orderIndex" INTEGER,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "workspaces" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"vectorTag" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"openAiTemp" REAL,
"openAiHistory" INTEGER NOT NULL DEFAULT 20,
"lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"openAiPrompt" TEXT
);
-- CreateTable
CREATE TABLE "workspace_chats" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"workspaceId" INTEGER NOT NULL,
"prompt" TEXT NOT NULL,
"response" TEXT NOT NULL,
"include" BOOLEAN NOT NULL DEFAULT true,
"user_id" INTEGER,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "workspace_chats_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "workspace_users" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"user_id" INTEGER NOT NULL,
"workspace_id" INTEGER NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "workspace_users_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "workspace_users_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "api_keys_secret_key" ON "api_keys"("secret");
-- CreateIndex
CREATE UNIQUE INDEX "workspace_documents_docId_key" ON "workspace_documents"("docId");
-- CreateIndex
CREATE UNIQUE INDEX "invites_code_key" ON "invites"("code");
-- CreateIndex
CREATE UNIQUE INDEX "system_settings_label_key" ON "system_settings"("label");
-- CreateIndex
CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
-- CreateIndex
CREATE UNIQUE INDEX "workspaces_slug_key" ON "workspaces"("slug");

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "sqlite"

118
server/prisma/schema.prisma Normal file
View File

@ -0,0 +1,118 @@
generator client {
provider = "prisma-client-js"
}
// Uncomment the following lines and comment out the SQLite datasource block above to use PostgreSQL
// Make sure to set the correct DATABASE_URL in your .env file
// After swapping run `yarn prisma:setup` from the root directory to migrate the database
//
// datasource db {
// provider = "postgresql"
// url = env("DATABASE_URL")
// }
datasource db {
provider = "sqlite"
url = "file:../storage/anythingllm.db"
}
model api_keys {
id Int @id @default(autoincrement())
secret String? @unique
createdBy Int?
createdAt DateTime @default(now())
lastUpdatedAt DateTime @default(now())
}
model workspace_documents {
id Int @id @default(autoincrement())
docId String @unique
filename String
docpath String
workspaceId Int
metadata String?
createdAt DateTime @default(now())
lastUpdatedAt DateTime @default(now())
workspace workspaces @relation(fields: [workspaceId], references: [id])
}
model invites {
id Int @id @default(autoincrement())
code String @unique
status String @default("pending")
claimedBy Int?
createdAt DateTime @default(now())
createdBy Int
lastUpdatedAt DateTime @default(now())
}
model system_settings {
id Int @id @default(autoincrement())
label String @unique
value String?
createdAt DateTime @default(now())
lastUpdatedAt DateTime @default(now())
}
model users {
id Int @id @default(autoincrement())
username String? @unique
password String
role String @default("default")
suspended Int @default(0)
createdAt DateTime @default(now())
lastUpdatedAt DateTime @default(now())
workspace_chats workspace_chats[]
workspace_users workspace_users[]
}
model document_vectors {
id Int @id @default(autoincrement())
docId String
vectorId String
createdAt DateTime @default(now())
lastUpdatedAt DateTime @default(now())
}
model welcome_messages {
id Int @id @default(autoincrement())
user String
response String
orderIndex Int?
createdAt DateTime @default(now())
}
model workspaces {
id Int @id @default(autoincrement())
name String
slug String @unique
vectorTag String?
createdAt DateTime @default(now())
openAiTemp Float?
openAiHistory Int @default(20)
lastUpdatedAt DateTime @default(now())
openAiPrompt String?
workspace_users workspace_users[]
documents workspace_documents[]
}
model workspace_chats {
id Int @id @default(autoincrement())
workspaceId Int
prompt String
response String
include Boolean @default(true)
user_id Int?
createdAt DateTime @default(now())
lastUpdatedAt DateTime @default(now())
users users? @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
}
model workspace_users {
id Int @id @default(autoincrement())
user_id Int
workspace_id Int
createdAt DateTime @default(now())
lastUpdatedAt DateTime @default(now())
workspaces workspaces @relation(fields: [workspace_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
}

33
server/prisma/seed.js Normal file
View File

@ -0,0 +1,33 @@
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function main() {
const settings = [
{ label: 'multi_user_mode', value: 'false' },
{ label: 'users_can_delete_workspaces', value: 'false' },
{ label: 'limit_user_messages', value: 'false' },
{ label: 'message_limit', value: '25' },
];
for (let setting of settings) {
const existing = await prisma.system_settings.findUnique({
where: { label: setting.label },
});
// Only create the setting if it doesn't already exist
if (!existing) {
await prisma.system_settings.create({
data: setting,
});
}
}
}
main()
.catch(e => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@ -37,7 +37,7 @@ async function userFromSession(request, response = null) {
return null;
}
const user = await User.get(`id = ${valid.id}`);
const user = await User.get({ id: valid.id });
return user;
}

View File

@ -1,4 +1,3 @@
const { escape } = require("sqlstring-sqlite");
const { ApiKey } = require("../../models/apiKeys");
const { SystemSettings } = require("../../models/systemSettings");
@ -15,7 +14,7 @@ async function validApiKey(request, response, next) {
return;
}
if (!(await ApiKey.get(`secret = ${escape(bearerKey)}`))) {
if (!(await ApiKey.get({ secret: bearerKey }))) {
response.status(403).json({
error: "No valid api key found.",
});

View File

@ -66,7 +66,7 @@ async function validateMultiUserRequest(request, response, next) {
return;
}
const user = await User.get(`id = ${valid.id}`);
const user = await User.get({ id: valid.id });
if (!user) {
response.status(403).json({
error: "Invalid auth for user.",

View File

@ -0,0 +1,47 @@
# Prisma Setup and Usage Guide
This guide will help you set up and use Prisma for the project. Prisma is a powerful ORM for Node.js and TypeScript, helping developers build faster and make fewer errors. Follow the guide to understand how to use Prisma and the scripts available in the project to manage the Prisma setup.
## Setting Up Prisma
To get started with setting up Prisma, you should run the setup script from the project root directory:
```sh
yarn setup
```
This script will install the necessary node modules in both the server and frontend directories, set up the environment files, and set up Prisma (generate client, run migrations, and seed the database).
## Prisma Scripts
In the project root's `package.json`, there are several scripts set up to help you manage Prisma:
- **prisma:generate**: Generates the Prisma client.
- **prisma:migrate**: Runs the migrations to ensure the database is in sync with the schema.
- **prisma:seed**: Seeds the database with initial data.
- **prisma:setup**: A convenience script that runs `prisma:generate`, `prisma:migrate`, and `prisma:seed` in sequence.
- **sqlite:migrate**: (To be run from the `server` directory) This script is for users transitioning from the old SQLite custom ORM setup to Prisma and will migrate all exisiting data over to Prisma. If you're a new user, your setup will already use Prisma.
To run any of these scripts, use `yarn` followed by the script name from the project root directory. For example:
```sh
yarn prisma:setup
```
## Manual Prisma Commands
While the scripts should cover most of your needs, you may sometimes want to run Prisma commands manually. Here are some commands you might find useful, along with their descriptions:
- `npx prisma introspect`: Introspects the database to update the Prisma schema by reading the schema of the existing database.
- `npx prisma generate`: Generates the Prisma client.
- `npx prisma migrate dev --name init`: Ensures the database is in sync with the schema, naming the migration 'init'.
- `npx prisma migrate reset`: Resets the database, deleting all data and recreating the schema.
These commands should be run from the `server` directory, where the Prisma schema is located.
## Notes
- Always make sure to run scripts from the root level to avoid path issues.
- Before running migrations, ensure that the Prisma schema is correctly defined to prevent data loss or corruption.
- If you are adding a new feature or making changes that require a change in the database schema, create a new migration rather than editing existing migrations.
- For users transitioning from the old SQLite ORM, navigate to the `server` directory and run the `sqlite:migrate` script to smoothly transition to Prisma. If you're setting up the project fresh, this step is unnecessary as the setup will already be using Prisma.

View File

@ -0,0 +1,12 @@
const { PrismaClient } = require("@prisma/client");
// npx prisma introspect
// npx prisma generate
// npx prisma migrate dev --name init -> ensures that db is in sync with schema
// npx prisma migrate reset -> resets the db
const prisma = new PrismaClient({
log: ["query", "info", "warn"],
});
module.exports = prisma;

View File

@ -0,0 +1,269 @@
const { PrismaClient } = require("@prisma/client");
const execSync = require("child_process").execSync;
const fs = require("fs");
const path = require("path");
require("dotenv").config();
const DATABASE_PATH = process.env.DB_URL || "../../storage/anythingllm.db";
const BACKUP_PATH = path.join(
path.dirname(DATABASE_PATH),
"anythingllm_backup.db"
);
// Backup the database before migrating data
function backupDatabase() {
try {
fs.copyFileSync(DATABASE_PATH, BACKUP_PATH);
console.log("Database backup created successfully.");
} catch (error) {
console.error("Failed to create database backup:", error);
}
}
backupDatabase();
const prisma = new PrismaClient();
// Reset the prisma database and prepare it for migration of data from sqlite
function resetAndMigrateDatabase() {
try {
console.log("Resetting and migrating the database...");
execSync("cd ../.. && npx prisma migrate reset --skip-seed --force", {
stdio: "inherit",
});
execSync("cd ../.. && npx prisma migrate dev --name init", {
stdio: "inherit",
});
console.log("Database reset and initial migration completed successfully");
} catch (error) {
console.error("Failed to reset and migrate the database:", error);
}
}
resetAndMigrateDatabase();
// Migrate data from sqlite to prisma
async function migrateData() {
try {
console.log("Starting data migration...");
var legacyMap = {
users: {
count: 0,
},
workspaces: {
count: 0,
},
};
// Step 1: Migrate system_settings table
await migrateTable("system_settings", (row) => {
return prisma.system_settings.create({
data: {
label: row.label,
value: row.value,
createdAt: new Date(row.createdAt),
lastUpdatedAt: new Date(row.lastUpdatedAt),
},
});
});
// Step 2: Migrate users table
await migrateTable("users", (row) => {
legacyMap.users[`user_${row.id}`] = legacyMap.users.count + 1;
legacyMap.users.count++;
return prisma.users.create({
data: {
username: row.username,
password: row.password,
role: row.role,
suspended: row.suspended,
createdAt: new Date(row.createdAt),
lastUpdatedAt: new Date(row.lastUpdatedAt),
},
});
});
// Step 3: Migrate workspaces table
await migrateTable("workspaces", (row) => {
legacyMap.workspaces[`workspace_${row.id}`] =
legacyMap.workspaces.count + 1;
legacyMap.workspaces.count++;
return prisma.workspaces.create({
data: {
name: row.name,
slug: row.slug,
vectorTag: row.vectorTag,
openAiTemp: Number(row.openAiTemp) || 0.7,
openAiHistory: Number(row.openAiHistory) || 20,
openAiPrompt: row.openAiPrompt,
createdAt: new Date(row.createdAt),
lastUpdatedAt: new Date(row.lastUpdatedAt),
},
});
});
// Step 4: Migrate api_keys table
await migrateTable("api_keys", async (row) => {
const legacyUserId = row.createdBy
? legacyMap.users?.[`user_${row.createdBy}`]
: null;
return prisma.api_keys.create({
data: {
secret: row.secret,
...(legacyUserId
? { createdBy: Number(legacyUserId) }
: { createdBy: Number(row.createdBy) }),
createdAt: new Date(row.createdAt),
lastUpdatedAt: new Date(row.lastUpdatedAt),
},
});
});
// Step 5: Migrate invites table
await migrateTable("invites", async (row) => {
const legacyCreatedUserId = row.createdBy
? legacyMap.users?.[`user_${row.createdBy}`]
: null;
const legacyClaimedUserId = row.claimedBy
? legacyMap.users?.[`user_${row.claimedBy}`]
: null;
return prisma.invites.create({
data: {
code: row.code,
status: row.status,
...(legacyClaimedUserId
? { claimedBy: Number(legacyClaimedUserId) }
: { claimedBy: Number(row.claimedBy) }),
...(legacyCreatedUserId
? { createdBy: Number(legacyCreatedUserId) }
: { createdBy: Number(row.createdBy) }),
createdAt: new Date(row.createdAt),
lastUpdatedAt: new Date(row.lastUpdatedAt),
},
});
});
// Step 6: Migrate workspace_documents table
await migrateTable("workspace_documents", async (row) => {
const legacyWorkspaceId = row.workspaceId
? legacyMap.workspaces?.[`workspace_${row.workspaceId}`]
: null;
return prisma.workspace_documents.create({
data: {
docId: row.docId,
filename: row.filename,
docpath: row.docpath,
...(legacyWorkspaceId
? { workspaceId: Number(legacyWorkspaceId) }
: {}),
metadata: row.metadata,
createdAt: new Date(row.createdAt),
lastUpdatedAt: new Date(row.lastUpdatedAt),
},
});
});
// Step 7: Migrate document_vectors table
await migrateTable("document_vectors", (row) => {
return prisma.document_vectors.create({
data: {
docId: row.docId,
vectorId: row.vectorId,
createdAt: new Date(row.createdAt),
lastUpdatedAt: new Date(row.lastUpdatedAt),
},
});
});
// Step 8: Migrate welcome_messages table
await migrateTable("welcome_messages", (row) => {
return prisma.welcome_messages.create({
data: {
user: row.user,
response: row.response,
orderIndex: row.orderIndex,
createdAt: new Date(row.createdAt),
},
});
});
// Step 9: Migrate workspace_users table
await migrateTable("workspace_users", async (row) => {
const legacyUserId = row.user_id
? legacyMap.users?.[`user_${row.user_id}`]
: null;
const legacyWorkspaceId = row.workspace_id
? legacyMap.workspaces?.[`workspace_${row.workspace_id}`]
: null;
if (!legacyUserId || !legacyWorkspaceId) return;
return prisma.workspace_users.create({
data: {
user_id: Number(legacyUserId),
workspace_id: Number(legacyWorkspaceId),
createdAt: new Date(row.createdAt),
lastUpdatedAt: new Date(row.lastUpdatedAt),
},
});
});
// Step 10: Migrate workspace_chats table
await migrateTable("workspace_chats", async (row) => {
const legacyUserId = row.user_id
? legacyMap.users?.[`user_${row.user_id}`]
: null;
const legacyWorkspaceId = row.workspaceId
? legacyMap.workspaces?.[`workspace_${row.workspaceId}`]
: null;
return prisma.workspace_chats.create({
data: {
workspaceId: Number(legacyWorkspaceId),
prompt: row.prompt,
response: row.response,
include: row.include === 1,
...(legacyUserId ? { user_id: Number(legacyUserId) } : {}),
createdAt: new Date(row.createdAt),
lastUpdatedAt: new Date(row.lastUpdatedAt),
},
});
});
console.log("Data migration completed successfully");
} catch (error) {
console.error("Data migration failed:", error);
} finally {
await prisma.$disconnect();
}
}
async function migrateTable(tableName, migrateRowFunc) {
const sqlite3 = require("sqlite3").verbose();
const { open } = require("sqlite");
const db = await open({
filename: BACKUP_PATH,
driver: sqlite3.Database,
});
const upserts = [];
const rows = await db.all(`SELECT * FROM ${tableName}`);
try {
for (const row of rows) {
await migrateRowFunc(row);
upserts.push(row);
}
} catch (e) {
console.error(e);
console.log({ tableName, upserts });
} finally {
await db.close();
}
return;
}
migrateData();

View File

@ -0,0 +1,33 @@
const { getGitVersion } = require("../../endpoints/utils");
const { Telemetry } = require("../../models/telemetry");
// Telemetry is anonymized and your data is never read. This can be disabled by setting
// DISABLE_TELEMETRY=true in the `.env` of however you setup. Telemetry helps us determine use
// of how AnythingLLM is used and how to improve this product!
// You can see all Telemetry events by ctrl+f `Telemetry.sendEvent` calls to verify this claim.
async function setupTelemetry() {
if (process.env.DISABLE_TELEMETRY === "true") {
console.log(
`\x1b[31m[TELEMETRY DISABLED]\x1b[0m Telemetry is marked as disabled - no events will send. Telemetry helps Mintplex Labs Inc improve AnythingLLM.`
);
return true;
}
if (Telemetry.isDev()) {
console.log(
`\x1b[33m[TELEMETRY STUBBED]\x1b[0m Anonymous Telemetry stubbed in development.`
);
return;
}
console.log(
`\x1b[32m[TELEMETRY ENABLED]\x1b[0m Anonymous Telemetry enabled. Telemetry helps Mintplex Labs Inc improve AnythingLLM.`
);
await Telemetry.findOrCreateId();
await Telemetry.sendTelemetry("server_boot", {
commit: getGitVersion(),
});
return;
}
module.exports = setupTelemetry;

View File

@ -225,7 +225,7 @@ const Chroma = {
name: namespace,
});
const knownDocuments = await DocumentVectors.where(`docId = '${docId}'`);
const knownDocuments = await DocumentVectors.where({ docId });
if (knownDocuments.length === 0) return;
const vectorIds = knownDocuments.map((doc) => doc.vectorId);

View File

@ -118,10 +118,11 @@ const LanceDb = {
const { DocumentVectors } = require("../../../models/vectors");
const table = await client.openTable(namespace);
const vectorIds = (await DocumentVectors.where(`docId = '${docId}'`)).map(
const vectorIds = (await DocumentVectors.where({ docId })).map(
(record) => record.vectorId
);
if (vectorIds.length === 0) return;
await table.delete(`id IN (${vectorIds.map((v) => `'${v}'`).join(",")})`);
return true;
},

View File

@ -182,7 +182,7 @@ const Pinecone = {
const { pineconeIndex } = await this.connect();
if (!(await this.namespaceExists(pineconeIndex, namespace))) return;
const knownDocuments = await DocumentVectors.where(`docId = '${docId}'`);
const knownDocuments = await DocumentVectors.where({ docId });
if (knownDocuments.length === 0) return;
const vectorIds = knownDocuments.map((doc) => doc.vectorId);

View File

@ -246,7 +246,7 @@ const QDrant = {
const { client } = await this.connect();
if (!(await this.namespaceExists(client, namespace))) return;
const knownDocuments = await DocumentVectors.where(`docId = '${docId}'`);
const knownDocuments = await DocumentVectors.where({ docId });
if (knownDocuments.length === 0) return;
const vectorIds = knownDocuments.map((doc) => doc.vectorId);

View File

@ -316,7 +316,7 @@ const Weaviate = {
const { client } = await this.connect();
if (!(await this.namespaceExists(client, namespace))) return;
const knownDocuments = await DocumentVectors.where(`docId = '${docId}'`);
const knownDocuments = await DocumentVectors.where({ docId });
if (knownDocuments.length === 0) return;
for (const doc of knownDocuments) {

View File

@ -173,6 +173,23 @@
dependencies:
cross-fetch "^3.1.5"
"@prisma/client@5.3.0":
version "5.3.0"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.3.0.tgz#47f07e5639993cffcf1c740a144495410562f279"
integrity sha512-cduYBlwj6oBfAUx2OI5i7t3NlpVeOtkN7pAqv0cw0B6gs4y8cY1mr8ZYywja0NUCOCqEWDkcZWBTVBwm6mnRIw==
dependencies:
"@prisma/engines-version" "5.3.0-36.e90b936d84779543cbe0e494bc8b9d7337fad8e4"
"@prisma/engines-version@5.3.0-36.e90b936d84779543cbe0e494bc8b9d7337fad8e4":
version "5.3.0-36.e90b936d84779543cbe0e494bc8b9d7337fad8e4"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.3.0-36.e90b936d84779543cbe0e494bc8b9d7337fad8e4.tgz#46ee2884e04cdba1163461ef856cec882d31c836"
integrity sha512-uftIog5FQ/OUR8Vb9TzpNBJ6L+zJnBgmd1A0uPJUzuvGMU32UmeyobpdXVzST5UprKryTdWOYXQFVyiQ2OU4Nw==
"@prisma/engines@5.3.1":
version "5.3.1"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.3.1.tgz#53cc72a5ed176dc27d22305fe5569c64cc78b381"
integrity sha512-6QkILNyfeeN67BNEPEtkgh3Xo2tm6D7V+UhrkBbRHqKw9CTaz/vvTP/ROwYSP/3JT2MtIutZm/EnhxUiuOPVDA==
"@qdrant/js-client-rest@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@qdrant/js-client-rest/-/js-client-rest-1.4.0.tgz#efd341a9a30b241e7e11f773b581b3102db1adc6"
@ -2081,6 +2098,13 @@ prettier@^2.4.1:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
prisma@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.3.1.tgz#a0932c1c1a5ed4ff449d064b193d9c7e94e8bf77"
integrity sha512-Wp2msQIlMPHe+5k5Od6xnsI/WNG7UJGgFUJgqv/ygc7kOECZapcSz/iU4NIEzISs3H1W9sFLjAPbg/gOqqtB7A==
dependencies:
"@prisma/engines" "5.3.1"
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@ -2385,11 +2409,6 @@ sqlite@^4.2.1:
resolved "https://registry.yarnpkg.com/sqlite/-/sqlite-4.2.1.tgz#d4eedfd1ad702f79110792375f4241a90c75c828"
integrity sha512-Tll0Ndvnwkuv5Hn6WIbh26rZiYQORuH1t5m/or9LUpSmDmmyFG89G9fKrSeugMPxwmEIXoVxqTun4LbizTs4uw==
sqlstring-sqlite@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/sqlstring-sqlite/-/sqlstring-sqlite-0.1.1.tgz#c8c61810663f2e59a6b0d737b70a8752bda3a078"
integrity sha512-9CAYUJ0lEUPYJrswqiqdINNSfq3jqWo/bFJ7tufdoNeSK0Fy+d1kFTxjqO9PIqza0Kri+ZtYMfPVf1aZaFOvrQ==
ssri@^8.0.0, ssri@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af"