UNPKG

@openfga/syntax-transformer

Version:

Javascript implementation of ANTLR Grammar for the OpenFGA DSL and parser from and to the OpenFGA JSON Syntax

310 lines (309 loc) 16.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.transformJSONStringToDSL = exports.transformJSONToDSL = void 0; exports.getModulesFromJSON = getModulesFromJSON; const errors_1 = require("../errors"); class DirectAssignmentValidator { constructor() { this.occured = 0; this.isFirstPosition = (userset) => { var _a, _b, _c; // Throw error if direct assignment is present, and not the first element. if (userset.this) { return true; } if ((_a = userset.difference) === null || _a === void 0 ? void 0 : _a.base) { if (userset.difference.base.this) { return true; } else { return this.isFirstPosition(userset.difference.base); } } else if ((_b = userset.intersection) === null || _b === void 0 ? void 0 : _b.child.length) { if (userset.intersection.child[0].this) { return true; } else { return this.isFirstPosition(userset.intersection.child[0]); } } else if ((_c = userset.union) === null || _c === void 0 ? void 0 : _c.child.length) { if (userset.union.child[0].this) { return true; } else { return this.isFirstPosition(userset.union.child[0]); } } return false; }; } } function parseTypeRestriction(restriction) { const typeName = restriction.type; const relation = restriction.relation; const wildcard = restriction.wildcard; const condition = restriction.condition; let typeRestriction = typeName; if (wildcard) { typeRestriction = `${typeRestriction}:*`; } if (relation) { typeRestriction = `${typeRestriction}#${relation}`; } if (condition) { typeRestriction = `${typeRestriction} with ${condition}`; } return typeRestriction; } function parseTypeRestrictions(restrictions) { const parsedTypeRestrictions = []; for (let index = 0; index < (restrictions === null || restrictions === void 0 ? void 0 : restrictions.length); index++) { parsedTypeRestrictions.push(parseTypeRestriction(restrictions[index])); } return parsedTypeRestrictions; } function parseThis(typeRestrictions) { const parsedTypeRestrictions = parseTypeRestrictions(typeRestrictions); return `[${parsedTypeRestrictions.join(", ")}]`; } function parseTupleToUserset(relationDefinition) { var _a, _b, _c, _d; const computedUserset = (_b = (_a = relationDefinition === null || relationDefinition === void 0 ? void 0 : relationDefinition.tupleToUserset) === null || _a === void 0 ? void 0 : _a.computedUserset) === null || _b === void 0 ? void 0 : _b.relation; const tupleset = (_d = (_c = relationDefinition === null || relationDefinition === void 0 ? void 0 : relationDefinition.tupleToUserset) === null || _c === void 0 ? void 0 : _c.tupleset) === null || _d === void 0 ? void 0 : _d.relation; return `${computedUserset} from ${tupleset}`; } function parseComputedUserset(relationDefinition) { return relationDefinition.computedUserset.relation; } function parseDifference(typeName, relationName, relationDefinition, typeRestrictions, validator) { const base = parseSubRelation(typeName, relationName, relationDefinition.difference.base, typeRestrictions, validator); const difference = parseSubRelation(typeName, relationName, relationDefinition.difference.subtract, typeRestrictions, validator); return `${base} but not ${difference}`; } function parseUnion(typeName, relationName, relationDefinition, typeRestrictions, validator) { var _a; const parsedString = []; const children = prioritizeDirectAssignment((_a = relationDefinition === null || relationDefinition === void 0 ? void 0 : relationDefinition.union) === null || _a === void 0 ? void 0 : _a.child); for (const child of children || []) { parsedString.push(parseSubRelation(typeName, relationName, child, typeRestrictions, validator)); } return parsedString.join(" or "); } function parseIntersection(typeName, relationName, relationDefinition, typeRestrictions, validator) { var _a; const parsedString = []; const children = prioritizeDirectAssignment((_a = relationDefinition === null || relationDefinition === void 0 ? void 0 : relationDefinition.intersection) === null || _a === void 0 ? void 0 : _a.child); for (const child of children || []) { parsedString.push(parseSubRelation(typeName, relationName, child, typeRestrictions, validator)); } return parsedString.join(" and "); } function parseSubRelation(typeName, relationName, relationDefinition, typeRestrictions, validator) { if (relationDefinition.this) { // Make sure we have no more than 1 reference for direct assignment in a given relation validator.occured++; return parseThis(typeRestrictions); } if (relationDefinition.computedUserset) { return parseComputedUserset(relationDefinition); } if (relationDefinition.tupleToUserset) { return parseTupleToUserset(relationDefinition); } if (relationDefinition.union) { return `(${parseUnion(typeName, relationName, relationDefinition, typeRestrictions, validator)})`; } if (relationDefinition.intersection) { return `(${parseIntersection(typeName, relationName, relationDefinition, typeRestrictions, validator)})`; } if (relationDefinition.difference) { return `(${parseDifference(typeName, relationName, relationDefinition, typeRestrictions, validator)})`; } throw new errors_1.UnsupportedDSLNestingError(typeName, relationName); } function parseRelation(typeName, relationName, relationDefinition = {}, relationMetadata = {}, includeSourceInformation = false) { const validator = new DirectAssignmentValidator(); let parsedRelationString = ` define ${relationName}: `; const typeRestrictions = relationMetadata.directly_related_user_types || []; if (relationDefinition.difference != null) { parsedRelationString += parseDifference(typeName, relationName, relationDefinition, typeRestrictions, validator); } else if (relationDefinition.union != null) { parsedRelationString += parseUnion(typeName, relationName, relationDefinition, typeRestrictions, validator); } else if (relationDefinition.intersection != null) { parsedRelationString += parseIntersection(typeName, relationName, relationDefinition, typeRestrictions, validator); } else { parsedRelationString += parseSubRelation(typeName, relationName, relationDefinition, typeRestrictions, validator); } parsedRelationString += constructSourceComment(relationMetadata, " extended by:", includeSourceInformation); // Check if we have either no direct assignment, or we had exactly 1 direct assignment in the first position if (!validator.occured || (validator.occured === 1 && validator.isFirstPosition(relationDefinition))) { return parsedRelationString; } throw new Error(`the '${relationName}' relation definition under the '${typeName}' type is not supported by the OpenFGA DSL syntax yet`); } const prioritizeDirectAssignment = (usersets) => { if (usersets === null || usersets === void 0 ? void 0 : usersets.length) { const thisPosition = usersets.findIndex((userset) => userset.this); if (thisPosition > 0) { usersets.unshift(...usersets.splice(thisPosition, 1)); } } return usersets; }; const parseType = (typeDef, isModularModel, includeSourceInformation = false) => { var _a, _b; const typeName = typeDef.type; const sourceString = constructSourceComment(typeDef.metadata, "", includeSourceInformation); let parsedTypeString = `\ntype ${typeName}${sourceString}`; const relations = typeDef.relations || {}; const metadata = typeDef.metadata; if ((_a = Object.keys(relations)) === null || _a === void 0 ? void 0 : _a.length) { parsedTypeString += "\n relations"; const sortedRelations = Object.entries(relations).sort(([aName], [bName]) => { var _a, _b; if (!isModularModel) { return 0; } const aMetadata = ((_a = metadata === null || metadata === void 0 ? void 0 : metadata.relations) === null || _a === void 0 ? void 0 : _a[aName]) || {}; const bMetadata = ((_b = metadata === null || metadata === void 0 ? void 0 : metadata.relations) === null || _b === void 0 ? void 0 : _b[bName]) || {}; return sortByModule(aName, bName, aMetadata, bMetadata); }); for (const [name, definition] of sortedRelations) { const parsedRelationString = parseRelation(typeName, name, definition, (_b = metadata === null || metadata === void 0 ? void 0 : metadata.relations) === null || _b === void 0 ? void 0 : _b[name], includeSourceInformation); parsedTypeString += `\n${parsedRelationString}`; } } return parsedTypeString; }; const parseConditionParams = (parameterMap) => { const parametersStringArray = []; Object.keys(parameterMap) .sort() .forEach((parameterName) => { var _a; const parameterType = parameterMap[parameterName]; let parameterTypeString = parameterType.type_name.replace("TYPE_NAME_", "").toLowerCase(); if (parameterTypeString === "list" || parameterTypeString === "map") { const genericTypeString = (_a = parameterType.generic_types) === null || _a === void 0 ? void 0 : _a[0].type_name.replace("TYPE_NAME_", "").toLowerCase(); parameterTypeString = `${parameterTypeString}<${genericTypeString}>`; } parametersStringArray.push(`${parameterName}: ${parameterTypeString}`); }); return parametersStringArray.join(", "); }; const parseCondition = (conditionName, conditionDef, includeSourceInformation = false) => { if (conditionName != conditionDef.name) { throw new errors_1.ConditionNameDoesntMatchError(conditionName, conditionDef.name); } const paramsString = parseConditionParams(conditionDef.parameters || {}); const sourceString = constructSourceComment(conditionDef.metadata, "", includeSourceInformation); return `condition ${conditionName}(${paramsString}) {\n ${conditionDef.expression}\n}${sourceString}\n`; }; const parseConditions = (model, isModularModel, includeSourceInformation = false) => { const conditionsMap = model.conditions || {}; if (!Object.keys(conditionsMap).length) { return ""; } let parsedConditionsString = ""; Object.entries(conditionsMap) .sort(([aName, aCondition], [bName, bCondition]) => { if (!isModularModel) { return aName.localeCompare(bName); } return sortByModule(aName, bName, aCondition.metadata, bCondition.metadata); }) .forEach(([conditionName, condition]) => { const parsedConditionString = parseCondition(conditionName, condition, includeSourceInformation); parsedConditionsString += `\n${parsedConditionString}`; }); return parsedConditionsString; }; const constructSourceComment = (metadata, leadingString = "", includeSourceInformation = false) => { var _a; return (metadata === null || metadata === void 0 ? void 0 : metadata.module) && includeSourceInformation ? ` #${leadingString} module: ${metadata.module}, file: ${(_a = metadata.source_info) === null || _a === void 0 ? void 0 : _a.file}` : ""; }; const transformJSONToDSL = (model, options) => { var _a, _b; const schemaVersion = (model === null || model === void 0 ? void 0 : model.schema_version) || "1.1"; const isModularModel = (_a = model.type_definitions) === null || _a === void 0 ? void 0 : _a.some((typeDef) => { var _a; return (_a = typeDef.metadata) === null || _a === void 0 ? void 0 : _a.module; }); const typeDefinitions = (_b = (isModularModel ? model === null || model === void 0 ? void 0 : model.type_definitions.sort((a, b) => sortByModule(a.type, b.type, a.metadata, b.metadata)) : model === null || model === void 0 ? void 0 : model.type_definitions)) === null || _b === void 0 ? void 0 : _b.map((typeDef) => parseType(typeDef, isModularModel, options === null || options === void 0 ? void 0 : options.includeSourceInformation)); const parsedConditionsString = parseConditions(model, isModularModel, options === null || options === void 0 ? void 0 : options.includeSourceInformation); return `model schema ${schemaVersion} ${typeDefinitions ? `${typeDefinitions.join("\n")}\n` : ""}${parsedConditionsString}`; }; exports.transformJSONToDSL = transformJSONToDSL; const transformJSONStringToDSL = (modelString, options) => { const model = JSON.parse(modelString); return (0, exports.transformJSONToDSL)(model, options); }; exports.transformJSONStringToDSL = transformJSONStringToDSL; function sortByModule(aName, bName, aMeta, bMeta) { var _a, _b; // If we have no module information for both, sort by name if (!(aMeta === null || aMeta === void 0 ? void 0 : aMeta.module) && !(bMeta === null || bMeta === void 0 ? void 0 : bMeta.module)) { return aName.localeCompare(bName); } // If there is no module then it belongs to the same file as the type so sort it at the top if ((aMeta === null || aMeta === void 0 ? void 0 : aMeta.module) == undefined) { return -1; } if ((bMeta === null || bMeta === void 0 ? void 0 : bMeta.module) === undefined) { return 1; } // First we sort by module name if (aMeta.module !== bMeta.module) { return aMeta.module.localeCompare(bMeta.module); } // If the module name is the same then sort by file name if (((_a = aMeta.source_info) === null || _a === void 0 ? void 0 : _a.file) !== ((_b = bMeta.source_info) === null || _b === void 0 ? void 0 : _b.file)) { return aMeta.source_info.file.localeCompare(bMeta.source_info.file); } // If the module name and file name are the same then sort based on name return aName.localeCompare(bName); } /* This function gets the modules from the JSON model and * returns them as an alphabetically sorted array */ function getModulesFromJSON(model) { var _a, _b, _c; const schemaVersion = (model === null || model === void 0 ? void 0 : model.schema_version) || "1.1"; if (schemaVersion !== "1.2") { throw new errors_1.UnsupportedModularModules(schemaVersion); } const isModularModel = (_a = model.type_definitions) === null || _a === void 0 ? void 0 : _a.some((typeDef) => { var _a; return (_a = typeDef.metadata) === null || _a === void 0 ? void 0 : _a.module; }); if (!isModularModel) { return []; } const modulesMap = {}; (_b = model.type_definitions) === null || _b === void 0 ? void 0 : _b.forEach((typeDef) => { var _a, _b, _c, _d; const key = (_a = typeDef.metadata) === null || _a === void 0 ? void 0 : _a.module; if (key) { modulesMap[key] = true; } for (const relationMeta in (_b = typeDef === null || typeDef === void 0 ? void 0 : typeDef.metadata) === null || _b === void 0 ? void 0 : _b.relations) { const key = (_d = (_c = typeDef === null || typeDef === void 0 ? void 0 : typeDef.metadata) === null || _c === void 0 ? void 0 : _c.relations[relationMeta]) === null || _d === void 0 ? void 0 : _d.module; if (key) { modulesMap[key] = true; } } }); const conditions = model.conditions || {}; for (const conditionMeta in conditions) { const key = (_c = conditions[conditionMeta].metadata) === null || _c === void 0 ? void 0 : _c.module; if (key) { modulesMap[key] = true; } } return Object.keys(modulesMap).sort(); }