@convo-lang/convo-lang
Version:
The language of AI
317 lines • 12.2 kB
JavaScript
import { convoDbDriverFunctions, ConvoNodePermissionType, isConvoNodeGroupCondition, isConvoNodePropertyCondition } from "./convo-db-types.js";
/**
* Normalizes a convo node path.
*
* Normalization rules:
* - leading and trailing whitespace is trimmed
* - the path must be absolute and start with `/`
* - duplicate slashes are collapsed to a single slash
* - trailing slashes are removed except for the root path `/`
* - `.` and `..` path segments are not allowed
* - empty strings are invalid
*
* Wildcard rules:
* - `none` no `*` characters are allowed
* - `end` validates query path globs and is retained for backwards compatibility
* - `any` validates query path globs
* - `*` matches zero or more characters within a single path segment
* - `**` matches zero or more complete path segments and must be its own path segment
* - `*` and `**` are the only supported wildcard tokens
*
* Returns the normalized path or `undefined` if the path is invalid.
*/
export const normalizeConvoNodePath = (path, wildcard) => {
path = path?.trim();
if (!path || path.includes(reservedPathSuffix)) {
return undefined;
}
if (path[0] !== "/") {
return undefined;
}
path = path.replace(/\/+/g, '/');
if (path !== "/" && path.endsWith('/')) {
path = path.replace(/\/+$/, '');
}
const parts = path.split('/');
for (let i = 1; i < parts.length; i++) {
const part = parts[i];
if (part === "." || part === "..") {
return undefined;
}
}
switch (wildcard) {
case 'none':
if (path.includes('*')) {
return undefined;
}
break;
case 'end':
case 'any':
if (!isValidConvoNodePathGlob(parts)) {
return undefined;
}
break;
}
return path;
};
export const validateConvoNodeCondition = (condition) => {
if (isConvoNodePropertyCondition(condition)) {
if ((condition.target === 'path' || condition.target === 'toPath' || condition.target === 'fromPath') &&
normalizeConvoNodePath(condition.target, 'any') !== condition.target) {
return `target path should be normalized`;
}
if ((condition.op === 'in' || condition.op === 'all-in' || condition.op === 'any-in' ||
condition.op === 'contains-all' || condition.op === 'contains-any') && !Array.isArray(condition.value)) {
return `condition value must be array for op(${condition.op})`;
}
}
else if (isConvoNodeGroupCondition(condition)) {
for (const c of condition.conditions) {
const error = validateConvoNodeCondition(c);
if (error) {
return error;
}
}
}
else {
return 'Unknown condition type';
}
return undefined;
};
const reservedPathSuffix = '.@db-';
const isValidConvoNodePathGlob = (parts) => {
for (let i = 1; i < parts.length; i++) {
const part = parts[i];
if (!part) {
continue;
}
if (!part.includes('*')) {
continue;
}
if (part === '**') {
continue;
}
if (part.includes('**')) {
return false;
}
}
return true;
};
export const validateConvoNodeQuery = (query) => {
if (query.permissionFrom !== undefined && (normalizeConvoNodePath(query.permissionFrom, 'none') !== query.permissionFrom ||
query.permissionFrom.includes(reservedPathSuffix))) {
return `Invalid query permissionFrom. permissionFrom:${query.permissionFrom}`;
}
for (let i = 0; i < query.steps.length; i++) {
const step = query.steps[i];
if (!step) {
return `Undefined step at index ${i}`;
}
if (step.path !== undefined && (normalizeConvoNodePath(step.path, 'any') !== step.path ||
step.path.includes(reservedPathSuffix))) {
return `Invalid step path. stepIndex: ${i}, path:${step.path}`;
}
if (step.permissionFrom !== undefined && (normalizeConvoNodePath(step.permissionFrom, 'none') !== step.permissionFrom ||
step.permissionFrom.includes(reservedPathSuffix))) {
return `Invalid step permissionFrom. stepIndex: ${i}, permissionFrom:${step.permissionFrom}`;
}
if (step.condition) {
const error = validateConvoNodeCondition(step.condition);
if (error) {
return error;
}
}
if (step.edge && (typeof step.edge === 'object')) {
const error = validateConvoNodeCondition(step.edge);
if (error) {
return error;
}
}
}
if (query.watch) {
const last = query.steps[query.steps.length - 1];
if (last) {
for (const e in last) {
switch (e) {
case 'path':
case 'condition':
break;
default:
return 'The last step of a watch query can only use the path and condition properties';
}
}
}
}
return undefined;
};
/**
* Calls a ConvoDbDriver function and ensures the specified function is an allowed function to be called
*/
export const callConvoDbDriverCmdAsync = (driver, fn, args) => {
if (!convoDbDriverFunctions.includes(fn)) {
const error = {
success: false,
error: 'Invalid ConvoDb driver function',
statusCode: 400,
};
return Promise.resolve(error);
}
return driver[fn](...args);
};
/**
* Sets the `permissionFrom` and `permissionRequired` based on `identityPath` and the action being
* taken by the step. The `permissionFrom` of the query will be updated if it is defined but does
* not match the `identityPath`
*/
export const applyIdentityToConvoDbQuery = (identityPath, query) => {
if (query.permissionFrom && query.permissionFrom !== identityPath) {
query.permissionFrom = identityPath;
}
for (const step of query.steps) {
step.permissionFrom = identityPath;
step.permissionRequired = ((step.permissionRequired ?? ConvoNodePermissionType.none) |
(step.call ? ConvoNodePermissionType.execute : ConvoNodePermissionType.read));
}
return query;
};
/**
* Sets the permissionFrom of the given object to `identityPath` If the given `permissionObject`
*/
export const applyIdentityToPermission = (identityPath, permissionObject) => {
permissionObject.permissionFrom = identityPath;
return permissionObject;
};
/**
* Applies the identityPath to all properties of all commands. Changes to the commands are made in-place,
* mutating the commands.
*/
export const applyIdentityToConvoDbCommands = (identityPath, cmds) => {
for (const cmd of cmds) {
const r = applyIdentityToConvoDbCommand(identityPath, cmd);
if (!r.success) {
return r;
}
}
return { success: true };
};
/**
* Applies the identityPath to all properties of the command. Changes to the command are made in-place,
* mutating the command.
*/
export const applyIdentityToConvoDbCommand = (identityPath, cmd) => {
for (const e in cmd) {
switch (e) {
case 'queryNodes':
if (cmd.queryNodes) {
applyIdentityToConvoDbQuery(identityPath, cmd.queryNodes.query);
}
break;
case 'getNodesByPath':
if (cmd.getNodesByPath) {
applyIdentityToPermission(identityPath, cmd.getNodesByPath);
}
break;
case 'getNodePermission':
if (cmd.getNodePermission) {
if (cmd.getNodePermission.fromPath !== identityPath && cmd.getNodePermission.toPath !== identityPath) {
return {
success: false,
error: 'getNodePermission fromPath or toPath must point to identityPath',
statusCode: 401,
};
}
}
break;
case 'checkNodePermission':
if (cmd.checkNodePermission) {
if (cmd.checkNodePermission.fromPath !== identityPath && cmd.checkNodePermission.toPath !== identityPath) {
return {
success: false,
error: 'checkNodePermissions fromPath or toPath must point to identityPath',
statusCode: 401,
};
}
}
break;
case 'insertNode':
if (cmd.insertNode) {
cmd.insertNode.options = applyIdentityToPermission(identityPath, cmd.insertNode.options ?? {});
}
break;
case 'updateNode':
if (cmd.updateNode) {
cmd.updateNode.options = applyIdentityToPermission(identityPath, cmd.updateNode.options ?? {});
}
break;
case 'deleteNode':
if (cmd.deleteNode) {
cmd.deleteNode.options = applyIdentityToPermission(identityPath, cmd.deleteNode.options ?? {});
}
break;
case 'queryEdges':
if (cmd.queryEdges) {
applyIdentityToPermission(identityPath, cmd.queryEdges.query);
}
break;
case 'getEdgeById':
if (cmd.getEdgeById) {
applyIdentityToPermission(identityPath, cmd.getEdgeById);
}
break;
case 'insertEdge':
if (cmd.insertEdge) {
cmd.insertEdge.options = applyIdentityToPermission(identityPath, cmd.insertEdge.options ?? {});
}
break;
case 'updateEdge':
if (cmd.updateEdge) {
cmd.updateEdge.options = applyIdentityToPermission(identityPath, cmd.updateEdge.options ?? {});
}
break;
case 'deleteEdge':
if (cmd.deleteEdge) {
cmd.deleteEdge.options = applyIdentityToPermission(identityPath, cmd.deleteEdge.options ?? {});
}
break;
case 'queryEmbeddings':
if (cmd.queryEmbeddings) {
applyIdentityToPermission(identityPath, cmd.queryEmbeddings.query);
}
break;
case 'getEmbeddingById':
if (cmd.getEmbeddingById) {
applyIdentityToPermission(identityPath, cmd.getEmbeddingById);
}
break;
case 'insertEmbedding':
if (cmd.insertEmbedding) {
cmd.insertEmbedding.options = applyIdentityToPermission(identityPath, cmd.insertEmbedding.options ?? {});
}
break;
case 'updateEmbedding':
if (cmd.updateEmbedding) {
cmd.updateEmbedding.options = applyIdentityToPermission(identityPath, cmd.updateEmbedding.options ?? {});
}
break;
case 'deleteEmbedding':
if (cmd.deleteEmbedding) {
cmd.deleteEmbedding.options = applyIdentityToPermission(identityPath, cmd.deleteEmbedding.options ?? {});
}
break;
case 'driverCmd':
return {
success: false,
error: 'Driver commands are not allowed to passed between permission boundaries',
statusCode: 401,
};
default:
return {
success: false,
error: `Unknown ConvoDbCommand property: ${e}`,
statusCode: 400
};
}
}
return { success: true };
};
//# sourceMappingURL=convo-db-lib.js.map