Skip to content

Commit f8885a4

Browse files
Append writable fields to dev API new workspace endpoint (#2843)
* add writible fields to dev api new workspace endpoint * lint * implement validations for workspace model * update swagger comments * simplify validations for workspace on frontend and API * cleanup validations --------- Co-authored-by: Timothy Carambat <[email protected]>
1 parent dd7c467 commit f8885a4

File tree

3 files changed

+160
-29
lines changed

3 files changed

+160
-29
lines changed

server/endpoints/api/workspace/index.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,19 @@ function apiWorkspaceEndpoints(app) {
2323
#swagger.tags = ['Workspaces']
2424
#swagger.description = 'Create a new workspace'
2525
#swagger.requestBody = {
26-
description: 'JSON object containing new display name of workspace.',
26+
description: 'JSON object containing workspace configuration.',
2727
required: true,
2828
content: {
2929
"application/json": {
3030
example: {
3131
name: "My New Workspace",
32+
similarityThreshold: 0.7,
33+
openAiTemp: 0.7,
34+
openAiHistory: 20,
35+
openAiPrompt: "Custom prompt for responses",
36+
queryRefusalResponse: "Custom refusal message",
37+
chatMode: "chat",
38+
topN: 4
3239
}
3340
}
3441
}
@@ -62,8 +69,18 @@ function apiWorkspaceEndpoints(app) {
6269
}
6370
*/
6471
try {
65-
const { name = null } = reqBody(request);
66-
const { workspace, message } = await Workspace.new(name);
72+
const { name = null, ...additionalFields } = reqBody(request);
73+
const { workspace, message } = await Workspace.new(
74+
name,
75+
null,
76+
additionalFields
77+
);
78+
79+
if (!workspace) {
80+
response.status(400).json({ workspace: null, message });
81+
return;
82+
}
83+
6784
await Telemetry.sendTelemetry("workspace_created", {
6885
multiUserMode: multiUserMode(response),
6986
LLMSelection: process.env.LLM_PROVIDER || "openai",

server/models/workspace.js

Lines changed: 128 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,21 @@ const { ROLES } = require("../utils/middleware/multiUserProtected");
66
const { v4: uuidv4 } = require("uuid");
77
const { User } = require("./user");
88

9+
function isNullOrNaN(value) {
10+
if (value === null) return true;
11+
return isNaN(value);
12+
}
13+
914
const Workspace = {
1015
defaultPrompt:
1116
"Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed.",
17+
18+
// Used for generic updates so we can validate keys in request body
19+
// commented fields are not writable, but are available on the db object
1220
writable: [
13-
// Used for generic updates so we can validate keys in request body
1421
"name",
15-
"slug",
16-
"vectorTag",
22+
// "slug",
23+
// "vectorTag",
1724
"openAiTemp",
1825
"openAiHistory",
1926
"lastUpdatedAt",
@@ -23,11 +30,77 @@ const Workspace = {
2330
"chatModel",
2431
"topN",
2532
"chatMode",
26-
"pfpFilename",
33+
// "pfpFilename",
2734
"agentProvider",
2835
"agentModel",
2936
"queryRefusalResponse",
3037
],
38+
39+
validations: {
40+
name: (value) => {
41+
// If the name is not provided or is not a string then we will use a default name.
42+
// as the name field is not nullable in the db schema or has a default value.
43+
if (!value || typeof value !== "string") return "My Workspace";
44+
return String(value).slice(0, 255);
45+
},
46+
openAiTemp: (value) => {
47+
if (value === null || value === undefined) return null;
48+
const temp = parseFloat(value);
49+
if (isNullOrNaN(temp) || temp < 0) return null;
50+
return temp;
51+
},
52+
openAiHistory: (value) => {
53+
if (value === null || value === undefined) return 20;
54+
const history = parseInt(value);
55+
if (isNullOrNaN(history)) return 20;
56+
if (history < 0) return 0;
57+
return history;
58+
},
59+
similarityThreshold: (value) => {
60+
if (value === null || value === undefined) return 0.25;
61+
const threshold = parseFloat(value);
62+
if (isNullOrNaN(threshold)) return 0.25;
63+
if (threshold < 0) return 0.0;
64+
if (threshold > 1) return 1.0;
65+
return threshold;
66+
},
67+
topN: (value) => {
68+
if (value === null || value === undefined) return 4;
69+
const n = parseInt(value);
70+
if (isNullOrNaN(n)) return 4;
71+
if (n < 1) return 1;
72+
return n;
73+
},
74+
chatMode: (value) => {
75+
if (!value || !["chat", "query"].includes(value)) return "chat";
76+
return value;
77+
},
78+
chatProvider: (value) => {
79+
if (!value || typeof value !== "string" || value === "none") return null;
80+
return String(value);
81+
},
82+
chatModel: (value) => {
83+
if (!value || typeof value !== "string") return null;
84+
return String(value);
85+
},
86+
agentProvider: (value) => {
87+
if (!value || typeof value !== "string" || value === "none") return null;
88+
return String(value);
89+
},
90+
agentModel: (value) => {
91+
if (!value || typeof value !== "string") return null;
92+
return String(value);
93+
},
94+
queryRefusalResponse: (value) => {
95+
if (!value || typeof value !== "string") return null;
96+
return String(value);
97+
},
98+
openAiPrompt: (value) => {
99+
if (!value || typeof value !== "string") return null;
100+
return String(value);
101+
},
102+
},
103+
31104
/**
32105
* The default Slugify module requires some additional mapping to prevent downstream issues
33106
* with some vector db providers and instead of building a normalization method for every provider
@@ -53,8 +126,34 @@ const Workspace = {
53126
return slugifyModule(...args);
54127
},
55128

56-
new: async function (name = null, creatorId = null) {
57-
if (!name) return { result: null, message: "name cannot be null" };
129+
/**
130+
* Validate the fields for a workspace update.
131+
* @param {Object} updates - The updates to validate - should be writable fields
132+
* @returns {Object} The validated updates. Only valid fields are returned.
133+
*/
134+
validateFields: function (updates = {}) {
135+
const validatedFields = {};
136+
for (const [key, value] of Object.entries(updates)) {
137+
if (!this.writable.includes(key)) continue;
138+
if (this.validations[key]) {
139+
validatedFields[key] = this.validations[key](value);
140+
} else {
141+
// If there is no validation for the field then we will just pass it through.
142+
validatedFields[key] = value;
143+
}
144+
}
145+
return validatedFields;
146+
},
147+
148+
/**
149+
* Create a new workspace.
150+
* @param {string} name - The name of the workspace.
151+
* @param {number} creatorId - The ID of the user creating the workspace.
152+
* @param {Object} additionalFields - Additional fields to apply to the workspace - will be validated.
153+
* @returns {Promise<{workspace: Object | null, message: string | null}>} A promise that resolves to an object containing the created workspace and an error message if applicable.
154+
*/
155+
new: async function (name = null, creatorId = null, additionalFields = {}) {
156+
if (!name) return { workspace: null, message: "name cannot be null" };
58157
var slug = this.slugify(name, { lower: true });
59158
slug = slug || uuidv4();
60159

@@ -66,7 +165,11 @@ const Workspace = {
66165

67166
try {
68167
const workspace = await prisma.workspaces.create({
69-
data: { name, slug },
168+
data: {
169+
name: this.validations.name(name),
170+
...this.validateFields(additionalFields),
171+
slug,
172+
},
70173
});
71174

72175
// If created with a user then we need to create the relationship as well.
@@ -80,35 +183,36 @@ const Workspace = {
80183
}
81184
},
82185

186+
/**
187+
* Update the settings for a workspace. Applies validations to the updates provided.
188+
* @param {number} id - The ID of the workspace to update.
189+
* @param {Object} updates - The data to update.
190+
* @returns {Promise<{workspace: Object | null, message: string | null}>} A promise that resolves to an object containing the updated workspace and an error message if applicable.
191+
*/
83192
update: async function (id = null, updates = {}) {
84193
if (!id) throw new Error("No workspace id provided for update");
85194

86-
const validFields = Object.keys(updates).filter((key) =>
87-
this.writable.includes(key)
88-
);
89-
90-
Object.entries(updates).forEach(([key]) => {
91-
if (validFields.includes(key)) return;
92-
delete updates[key];
93-
});
94-
95-
if (Object.keys(updates).length === 0)
195+
const validatedUpdates = this.validateFields(updates);
196+
if (Object.keys(validatedUpdates).length === 0)
96197
return { workspace: { id }, message: "No valid fields to update!" };
97198

98199
// If the user unset the chatProvider we will need
99200
// to then clear the chatModel as well to prevent confusion during
100201
// LLM loading.
101-
if (updates?.chatProvider === "default") {
102-
updates.chatProvider = null;
103-
updates.chatModel = null;
202+
if (validatedUpdates?.chatProvider === "default") {
203+
validatedUpdates.chatProvider = null;
204+
validatedUpdates.chatModel = null;
104205
}
105206

106-
return this._update(id, updates);
207+
return this._update(id, validatedUpdates);
107208
},
108209

109-
// Explicit update of settings + key validations.
110-
// Only use this method when directly setting a key value
111-
// that takes no user input for the keys being modified.
210+
/**
211+
* Direct update of workspace settings without any validation.
212+
* @param {number} id - The ID of the workspace to update.
213+
* @param {Object} data - The data to update.
214+
* @returns {Promise<{workspace: Object | null, message: string | null}>} A promise that resolves to an object containing the updated workspace and an error message if applicable.
215+
*/
112216
_update: async function (id = null, data = {}) {
113217
if (!id) throw new Error("No workspace id provided for update");
114218

server/swagger/openapi.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1451,6 +1451,9 @@
14511451
}
14521452
}
14531453
},
1454+
"400": {
1455+
"description": "Bad Request"
1456+
},
14541457
"403": {
14551458
"description": "Forbidden",
14561459
"content": {
@@ -1471,12 +1474,19 @@
14711474
}
14721475
},
14731476
"requestBody": {
1474-
"description": "JSON object containing new display name of workspace.",
1477+
"description": "JSON object containing workspace configuration.",
14751478
"required": true,
14761479
"content": {
14771480
"application/json": {
14781481
"example": {
1479-
"name": "My New Workspace"
1482+
"name": "My New Workspace",
1483+
"similarityThreshold": 0.7,
1484+
"openAiTemp": 0.7,
1485+
"openAiHistory": 20,
1486+
"openAiPrompt": "Custom prompt for responses",
1487+
"queryRefusalResponse": "Custom refusal message",
1488+
"chatMode": "chat",
1489+
"topN": 4
14801490
}
14811491
}
14821492
}

0 commit comments

Comments
 (0)