@cerbos/orm-prisma
Version:
Prisma adapter for Cerbos query plans
629 lines • 24.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PlanKind = void 0;
exports.queryPlanToPrisma = queryPlanToPrisma;
const core_1 = require("@cerbos/core");
Object.defineProperty(exports, "PlanKind", { enumerable: true, get: function () { return core_1.PlanKind; } });
function isNamedOperand(operand) {
return "name" in operand && typeof operand.name === "string";
}
function isValueOperand(operand) {
return "value" in operand && operand.value !== undefined;
}
function isOperatorOperand(operand) {
return ("operator" in operand &&
typeof operand.operator === "string" &&
"operands" in operand &&
Array.isArray(operand.operands));
}
function assertDefined(value, message) {
if (value === undefined) {
throw new Error(message);
}
return value;
}
function getLeafField(path) {
const fieldName = path[path.length - 1];
if (!fieldName) {
throw new Error("Field path cannot be empty");
}
return fieldName;
}
function getFilterEntry(filter) {
const entry = Object.entries(filter)[0];
if (!entry) {
throw new Error("Filter must contain at least one entry");
}
return entry;
}
function isResolvedFieldReference(operand) {
return "path" in operand;
}
function isResolvedValue(operand) {
return "value" in operand;
}
/**
* Converts a Cerbos query plan to a Prisma filter.
*/
function queryPlanToPrisma({ queryPlan, mapper = {}, }) {
switch (queryPlan.kind) {
case core_1.PlanKind.ALWAYS_ALLOWED:
return { kind: core_1.PlanKind.ALWAYS_ALLOWED };
case core_1.PlanKind.ALWAYS_DENIED:
return { kind: core_1.PlanKind.ALWAYS_DENIED };
case core_1.PlanKind.CONDITIONAL:
return {
kind: core_1.PlanKind.CONDITIONAL,
filters: buildPrismaFilterFromCerbosExpression(queryPlan.condition, mapper),
};
default:
throw Error(`Invalid query plan.`);
}
}
/**
* Resolves a field reference considering relations and nested fields.
*/
function resolveFieldReference(reference, mapper) {
const parts = reference.split(".");
const config = typeof mapper === "function" ? mapper(reference) : mapper[reference];
let matchedPrefix = "";
let matchedConfig;
// If no direct match, look for partial matches
if (!config) {
for (let i = parts.length - 1; i >= 0; i--) {
const prefix = parts.slice(0, i + 1).join(".");
const prefixConfig = typeof mapper === "function" ? mapper(prefix) : mapper[prefix];
if (prefixConfig) {
matchedPrefix = prefix;
matchedConfig = prefixConfig;
break;
}
}
}
const activeConfig = config ?? matchedConfig;
// Handle relation mapping
if (activeConfig?.relation) {
const { name, type, fields } = activeConfig.relation;
const matchedParts = matchedPrefix ? matchedPrefix.split(".") : [];
const remainingParts = matchedPrefix
? parts.slice(matchedParts.length)
: parts.slice(1);
let field;
const relations = [
{
name,
type,
field: activeConfig.relation.field,
nestedMapper: fields,
},
];
// Process nested relations
if (fields && remainingParts.length > 0) {
let currentMapper = fields;
let currentParts = remainingParts;
while (currentParts.length > 0) {
if (!currentMapper) {
break;
}
const currentPart = currentParts[0];
if (!currentPart) {
break;
}
const nextConfig = currentMapper[currentPart];
if (nextConfig?.relation) {
relations.push({
name: nextConfig.relation.name,
type: nextConfig.relation.type,
field: nextConfig.relation.field,
nestedMapper: nextConfig.relation.fields,
});
currentMapper = nextConfig.relation.fields || {};
currentParts = currentParts.slice(1);
}
else {
const lastPart = currentParts[currentParts.length - 1];
if (!lastPart) {
break;
}
field = nextConfig?.field || lastPart;
break;
}
}
}
return { path: field ? [field] : remainingParts, relations };
}
// Simple field mapping
return { path: [activeConfig?.field || reference] };
}
/**
* Determines the appropriate Prisma operator based on relation type.
*/
function getPrismaRelationOperator(relation) {
return relation.type === "one" ? "is" : "some";
}
/**
* Builds a nested relation filter for Prisma queries.
*/
function buildNestedRelationFilter(relations, fieldFilter) {
if (relations.length === 0)
return fieldFilter;
let currentFilter = fieldFilter;
// Build nested structure from inside out
for (let i = relations.length - 1; i >= 0; i--) {
const relation = relations[i];
if (!relation) {
throw new Error("Relation mapping is missing");
}
const relationOperator = getPrismaRelationOperator(relation);
// Handle special case for the deepest relation
if (relation.field && i === relations.length - 1) {
const [key, filterValue] = getFilterEntry(currentFilter);
if (key === "NOT") {
currentFilter = { NOT: { [relation.field]: filterValue } };
}
else {
currentFilter = { [relation.field]: filterValue };
}
}
currentFilter = { [relation.name]: { [relationOperator]: currentFilter } };
}
return currentFilter;
}
/**
* Resolves a PlanExpressionOperand into a ResolvedOperand.
*/
function resolveOperand(operand, mapper) {
if (isNamedOperand(operand)) {
return resolveFieldReference(operand.name, mapper);
}
else if (isValueOperand(operand)) {
return { value: operand.value };
}
else if (isOperatorOperand(operand)) {
const nestedResult = buildPrismaFilterFromCerbosExpression(operand, mapper);
return { value: nestedResult };
}
throw new Error("Operand must have name, value, or be an expression");
}
/**
* Creates a scoped mapper for collection operations
*/
function createScopedMapper(collectionPath, variableName, fullMapper) {
return (key) => {
// If the key starts with the variable name, it's accessing the collection item
if (key.startsWith(variableName + ".")) {
const strippedKey = key.replace(variableName + ".", "");
const parts = strippedKey.split(".");
// Get the collection's relation config
const collectionConfig = typeof fullMapper === "function"
? fullMapper(collectionPath)
: fullMapper[collectionPath];
if (collectionConfig?.relation?.fields) {
// For nested paths, traverse the fields configuration
const baseConfig = collectionConfig.relation.fields;
if (!baseConfig) {
return { field: strippedKey };
}
let currentConfig = baseConfig;
let field = parts[0] || strippedKey;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
const nextPart = parts[i + 1];
if (!part || !nextPart) {
break;
}
const nextConfig = currentConfig[part];
if (nextConfig?.relation?.fields) {
currentConfig = nextConfig.relation.fields;
field = nextPart;
}
else {
break;
}
}
if (!field) {
field = strippedKey;
}
// Return the field config if it exists, otherwise create a default one
return currentConfig[field] || { field };
}
return { field: strippedKey };
}
// For keys not referencing the collection item, use the full mapper
if (typeof fullMapper === "function") {
return fullMapper(key);
}
return fullMapper[key] || { field: key };
};
}
/**
* Builds a Prisma filter from a Cerbos expression.
*/
function buildPrismaFilterFromCerbosExpression(expression, mapper) {
// Validate expression structure
if (!isOperatorOperand(expression)) {
throw new Error("Invalid Cerbos expression structure");
}
const { operator, operands } = expression;
// Process different operator types
switch (operator) {
case "and":
return {
AND: operands.map((operand) => buildPrismaFilterFromCerbosExpression(operand, mapper)),
};
case "or":
return {
OR: operands.map((operand) => buildPrismaFilterFromCerbosExpression(operand, mapper)),
};
case "not": {
const operand = operands[0];
if (!operand) {
throw new Error("not operator requires an operand");
}
return {
NOT: buildPrismaFilterFromCerbosExpression(operand, mapper),
};
}
case "eq":
case "ne":
case "lt":
case "le":
case "gt":
case "ge": {
return handleRelationalOperator(operator, operands, mapper);
}
case "in": {
return handleInOperator(operands, mapper);
}
case "contains":
case "startsWith":
case "endsWith": {
return handleStringOperator(operator, operands, mapper);
}
case "isSet": {
return handleIsSetOperator(operands, mapper);
}
case "hasIntersection": {
return handleHasIntersectionOperator(operands, mapper);
}
case "lambda": {
return handleLambdaOperator(operands);
}
case "exists":
case "exists_one":
case "all":
case "except":
case "filter": {
return handleCollectionOperator(operator, operands, mapper);
}
case "map": {
return handleMapOperator(operands, mapper);
}
default:
throw new Error(`Unsupported operator: ${operator}`);
}
}
/**
* Helper function to process relational operators (eq, ne, lt, etc.)
*/
function handleRelationalOperator(operator, operands, mapper) {
const prismaOperator = {
eq: "equals",
ne: "not",
lt: "lt",
le: "lte",
gt: "gt",
ge: "gte",
}[operator];
if (!prismaOperator) {
throw new Error(`Unsupported operator: ${operator}`);
}
const leftOperand = operands.find((o) => isNamedOperand(o) || isOperatorOperand(o));
if (!leftOperand)
throw new Error("No valid left operand found");
const rightOperand = operands.find((o) => o !== leftOperand);
if (!rightOperand)
throw new Error("No valid right operand found");
const left = resolveOperand(leftOperand, mapper);
const right = resolveOperand(rightOperand, mapper);
if (isResolvedFieldReference(left)) {
const { path, relations } = left;
if (!isResolvedValue(right)) {
throw new Error("Right operand must be a value");
}
const filterValue = { [prismaOperator]: right.value };
const fieldName = getLeafField(path);
const fieldFilter = { [fieldName]: filterValue };
if (relations && relations.length > 0) {
return buildNestedRelationFilter(relations, fieldFilter);
}
return fieldFilter;
}
if (!isResolvedValue(right)) {
throw new Error("Right operand must be a value");
}
return { [prismaOperator]: right.value };
}
/**
* Helper function to handle "in" operator
*/
function handleInOperator(operands, mapper) {
const nameOperand = operands.find(isNamedOperand);
if (!nameOperand)
throw new Error("Name operand is undefined");
const valueOperand = operands.find(isValueOperand);
if (!valueOperand)
throw new Error("Value operand is undefined");
const resolved = resolveOperand(nameOperand, mapper);
if (!isResolvedFieldReference(resolved)) {
throw new Error("Name operand must resolve to a field reference");
}
const { path, relations } = resolved;
const resolvedValue = resolveOperand(valueOperand, mapper);
if (!isResolvedValue(resolvedValue)) {
throw new Error("Value operand must resolve to a value");
}
const { value } = resolvedValue;
const values = Array.isArray(value) ? value : [value];
const fieldName = getLeafField(path);
if (relations && relations.length > 0) {
const fieldFilter = values.length === 1
? { [fieldName]: values[0] }
: { [fieldName]: { in: values } };
return buildNestedRelationFilter(relations, fieldFilter);
}
return values.length === 1
? { [fieldName]: values[0] }
: { [fieldName]: { in: values } };
}
/**
* Helper function to handle string operators (contains, startsWith, endsWith)
*/
function handleStringOperator(operator, operands, mapper) {
const nameOperand = operands.find(isNamedOperand);
if (!nameOperand)
throw new Error("Name operand is undefined");
const resolved = resolveOperand(nameOperand, mapper);
if (!isResolvedFieldReference(resolved)) {
throw new Error("Name operand must resolve to a field reference");
}
const { path, relations } = resolved;
const valueOperand = operands.find(isValueOperand);
if (!valueOperand)
throw new Error("Value operand is undefined");
const resolvedValue = resolveOperand(valueOperand, mapper);
if (!isResolvedValue(resolvedValue)) {
throw new Error("Value operand must resolve to a value");
}
const { value } = resolvedValue;
if (typeof value !== "string") {
throw new Error(`${operator} operator requires string value`);
}
const fieldName = getLeafField(path);
const fieldFilter = { [fieldName]: { [operator]: value } };
if (relations && relations.length > 0) {
return buildNestedRelationFilter(relations, fieldFilter);
}
return fieldFilter;
}
/**
* Helper function to handle "isSet" operator
*/
function handleIsSetOperator(operands, mapper) {
const nameOperand = operands.find(isNamedOperand);
if (!nameOperand)
throw new Error("Name operand is undefined");
const resolved = resolveOperand(nameOperand, mapper);
if (!isResolvedFieldReference(resolved)) {
throw new Error("Name operand must resolve to a field reference");
}
const { path, relations } = resolved;
const valueOperand = operands.find(isValueOperand);
if (!valueOperand)
throw new Error("Value operand is undefined");
const resolvedValue = resolveOperand(valueOperand, mapper);
if (!isResolvedValue(resolvedValue)) {
throw new Error("Value operand must resolve to a value");
}
const fieldName = getLeafField(path);
const fieldFilter = {
[fieldName]: resolvedValue.value ? { not: null } : { equals: null },
};
if (relations && relations.length > 0) {
return buildNestedRelationFilter(relations, fieldFilter);
}
return fieldFilter;
}
/**
* Helper function to handle "hasIntersection" operator
*/
function handleHasIntersectionOperator(operands, mapper) {
if (operands.length !== 2) {
throw new Error("hasIntersection requires exactly two operands");
}
const leftOperand = assertDefined(operands[0], "hasIntersection requires a left operand");
const rightOperand = assertDefined(operands[1], "hasIntersection requires a right operand");
// Check if left operand is a map operation
if (isOperatorOperand(leftOperand) && leftOperand.operator === "map") {
if (!isValueOperand(rightOperand)) {
throw new Error("Second operand of hasIntersection must be a value");
}
const collection = assertDefined(leftOperand.operands[0], "Map expression must include a collection reference");
const lambda = assertDefined(leftOperand.operands[1], "Map expression must include a lambda expression");
if (!isNamedOperand(collection)) {
throw new Error("First operand of map must be a collection reference");
}
// Get variable name from lambda
if (!isOperatorOperand(lambda)) {
throw new Error("Lambda expression must have operands");
}
const variable = assertDefined(lambda.operands[1], "Lambda variable must have a name");
if (!isNamedOperand(variable)) {
throw new Error("Lambda variable must have a name");
}
// Create scoped mapper for the collection
const scopedMapper = createScopedMapper(collection.name, variable.name, mapper);
const { relations } = resolveFieldReference(collection.name, mapper);
if (!relations || relations.length === 0) {
throw new Error("Map operation requires relations");
}
const projection = assertDefined(lambda.operands[0], "Invalid map lambda expression structure");
if (!isNamedOperand(projection)) {
throw new Error("Invalid map lambda expression structure");
}
// Use scoped mapper for resolving the projection
const resolved = resolveFieldReference(projection.name, scopedMapper);
const fieldName = getLeafField(resolved.path);
return buildNestedRelationFilter(relations, {
[fieldName]: { in: rightOperand.value },
});
}
// Handle regular field reference
if (!isNamedOperand(leftOperand)) {
throw new Error("First operand of hasIntersection must be a field reference or map expression");
}
if (!isValueOperand(rightOperand)) {
throw new Error("Second operand of hasIntersection must be a value");
}
const { path, relations } = resolveFieldReference(leftOperand.name, mapper);
if (!Array.isArray(rightOperand.value)) {
throw new Error("hasIntersection requires an array value");
}
if (relations && relations.length > 0) {
const fieldName = getLeafField(path);
const fieldFilter = {
[fieldName]: { in: rightOperand.value },
};
return buildNestedRelationFilter(relations, fieldFilter);
}
const fieldName = getLeafField(path);
return { [fieldName]: { some: rightOperand.value } };
}
/**
* Helper function to handle "lambda" operator
*/
function handleLambdaOperator(operands) {
const condition = assertDefined(operands[0], "Lambda requires a condition operand");
const variable = assertDefined(operands[1], "Lambda requires a variable operand");
if (!isNamedOperand(variable)) {
throw new Error("Lambda variable must have a name");
}
return buildPrismaFilterFromCerbosExpression(condition, (key) => ({
field: key.replace(`${variable.name}.`, ""),
}));
}
/**
* Helper function to handle collection operators (exists, all, except, filter)
*/
function handleCollectionOperator(operator, operands, mapper) {
if (operands.length !== 2) {
throw new Error(`${operator} requires exactly two operands`);
}
const collection = assertDefined(operands[0], `${operator} requires a collection operand`);
const lambda = assertDefined(operands[1], `${operator} requires a lambda operand`);
if (!isNamedOperand(collection)) {
throw new Error(`First operand of ${operator} must be a collection reference`);
}
if (!isOperatorOperand(lambda)) {
throw new Error(`Second operand of ${operator} must be a lambda expression`);
}
// Get variable name from lambda
const variable = assertDefined(lambda.operands[1], "Lambda variable must have a name");
if (!isNamedOperand(variable)) {
throw new Error("Lambda variable must have a name");
}
// Create scoped mapper for the collection
const scopedMapper = createScopedMapper(collection.name, variable.name, mapper);
const { relations } = resolveFieldReference(collection.name, mapper);
if (!relations || relations.length === 0) {
throw new Error(`${operator} operator requires a relation mapping`);
}
const lambdaConditionOperand = assertDefined(lambda.operands[0], "Lambda expression must provide a condition");
const lambdaCondition = buildPrismaFilterFromCerbosExpression(lambdaConditionOperand, // Use the condition part of the lambda
scopedMapper);
const relation = assertDefined(relations[0], `${operator} operator requires a relation mapping`);
let filterValue = lambdaCondition;
// If the lambda condition already has a relation structure, merge it
if (lambdaCondition["AND"] || lambdaCondition["OR"]) {
filterValue = lambdaCondition;
}
else {
const lambdaKeys = Object.keys(lambdaCondition);
const defaultKey = lambdaKeys[0];
if (!defaultKey) {
throw new Error("Lambda condition must have at least one field");
}
const lambdaFieldValue = lambdaCondition[defaultKey];
if (lambdaFieldValue === undefined) {
throw new Error("Lambda condition field value cannot be undefined");
}
const filterField = relation.field || defaultKey;
filterValue = {
[filterField]: lambdaFieldValue,
};
}
switch (operator) {
case "exists":
case "filter":
return { [relation.name]: { some: filterValue } };
case "except":
return { [relation.name]: { some: { NOT: filterValue } } };
case "exists_one":
return {
[relation.name]: {
some: filterValue,
},
AND: [
{
[relation.name]: {
every: {
OR: [filterValue, { NOT: filterValue }],
},
},
},
],
};
case "all":
return { [relation.name]: { every: filterValue } };
default:
throw new Error(`Unexpected operator: ${operator}`);
}
}
/**
* Helper function to handle "map" operator
*/
function handleMapOperator(operands, mapper) {
if (operands.length !== 2) {
throw new Error("map requires exactly two operands");
}
const collection = assertDefined(operands[0], "map requires a collection operand");
const lambda = assertDefined(operands[1], "map requires a lambda operand");
if (!isNamedOperand(collection)) {
throw new Error("First operand of map must be a collection reference");
}
if (!isOperatorOperand(lambda) || lambda.operator !== "lambda") {
throw new Error("Second operand of map must be a lambda expression");
}
// Get variable name from lambda
const projection = assertDefined(lambda.operands[0], "Map lambda expression must provide a projection");
const variable = assertDefined(lambda.operands[1], "Map lambda expression must provide a variable");
if (!isNamedOperand(projection) || !isNamedOperand(variable)) {
throw new Error("Invalid map lambda expression structure");
}
// Create scoped mapper for the collection
const scopedMapper = createScopedMapper(collection.name, variable.name, mapper);
const { relations } = resolveFieldReference(collection.name, mapper);
if (!relations || relations.length === 0) {
throw new Error("map operator requires a relation mapping");
}
// Use scoped mapper for resolving the projection
const resolved = resolveFieldReference(projection.name, scopedMapper);
const fieldName = getLeafField(resolved.path);
const lastRelation = assertDefined(relations[relations.length - 1], "Relation mapping must contain at least one relation");
return buildNestedRelationFilter(relations, {
[getPrismaRelationOperator(lastRelation)]: {
select: { [fieldName]: true },
},
});
}
//# sourceMappingURL=index.js.map