UNPKG

@convo-lang/convo-lang

Version:
317 lines 12.2 kB
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