@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
JavaScript
;
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();
}