@openfga/syntax-transformer
Version:
Javascript implementation of ANTLR Grammar for the OpenFGA DSL and parser from and to the OpenFGA JSON Syntax
272 lines (271 loc) • 12.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.transformModuleFilesToModel = void 0;
const validate_dsl_1 = require("../../validator/validate-dsl");
const dsltojson_1 = require("../dsltojson");
const errors_1 = require("../../errors");
const line_numbers_1 = require("../../util/line-numbers");
const exceptions_1 = require("../../util/exceptions");
const transformModuleFilesToModel = (files, schemaVersion) => {
var _a, _b, _c, _d, _e;
const model = {
schema_version: schemaVersion,
type_definitions: [],
conditions: {},
};
const typeDefs = [];
const types = new Set();
const extendedTypeDefs = {};
const conditions = new Map();
const errors = [];
const moduleFiles = new Map(files.map((file) => [file.name, file.contents]));
for (const { name, contents } of files) {
try {
const lines = contents.split("\n");
const { authorizationModel, typeDefExtensions } = (0, dsltojson_1.transformModularDSLToJSONObject)(contents);
for (const typeDef of authorizationModel.type_definitions) {
// If this is an extension mark it to be merged later
if (typeDefExtensions.has(typeDef.type)) {
if (!extendedTypeDefs[name]) {
extendedTypeDefs[name] = [];
}
extendedTypeDefs[name].push(typeDef);
continue;
}
if (!typeDef.metadata) {
errors.push(new errors_1.ModuleTransformationSingleError({
msg: "file is not a module",
line: { start: 0, end: 0 },
column: { start: 0, end: 0 },
file: "nomodule.fga",
}));
continue;
}
typeDef.metadata.source_info = {
file: name,
};
types.add(typeDef.type);
typeDefs.push(typeDef);
}
if (authorizationModel.conditions) {
for (const [conditionName, condition] of Object.entries(authorizationModel.conditions)) {
// If we have already seen a condition with this name mark it as duplicate
if (conditions.has(conditionName)) {
const lineIndex = (0, line_numbers_1.getConditionLineNumber)(conditionName, lines);
errors.push((0, exceptions_1.constructTransformationError)({
message: `duplicate condition ${conditionName}`,
lines,
lineIndex,
metadata: {
symbol: conditionName,
file: name,
},
}));
continue;
}
condition.metadata.source_info = {
file: name,
};
conditions.set(conditionName, condition);
}
}
}
catch (error) {
if (error instanceof errors_1.DSLSyntaxError) {
for (const e of error.errors) {
e.file = name;
errors.push(e);
}
}
else if (error instanceof Error) {
errors.push(error);
}
}
}
for (const [filename, extended] of Object.entries(extendedTypeDefs)) {
const lines = (_a = moduleFiles.get(filename)) === null || _a === void 0 ? void 0 : _a.split("\n");
for (const typeDef of extended) {
if (!typeDef.relations) {
// TODO: Maybe should be an error case or at least a warning?
continue;
}
const originalIndex = typeDefs.findIndex((t) => t.type === typeDef.type);
const original = typeDefs[originalIndex];
if (!original) {
const lineIndex = (0, line_numbers_1.getTypeLineNumber)(typeDef.type, lines, 0, true);
errors.push((0, exceptions_1.constructTransformationError)({
message: `extended type ${typeDef.type} does not exist`,
lines,
lineIndex,
metadata: {
symbol: typeDef.type,
file: filename,
},
}));
continue;
}
const existingRelationNames = Object.keys(original.relations || {});
if (!existingRelationNames || !existingRelationNames.length) {
original.relations = typeDef.relations;
if (!original.metadata) {
original.metadata = {};
}
original.metadata.relations = typeDef.metadata.relations;
// Add the file metadata to any relations metadata that exists
for (const relationName of Object.keys(original.metadata.relations)) {
original.metadata.relations[relationName].source_info = {
file: filename,
};
}
typeDefs[originalIndex] = original;
continue;
}
for (const [name, relation] of Object.entries(typeDef.relations)) {
if (existingRelationNames.includes(name)) {
const lineIndex = (0, line_numbers_1.getRelationLineNumber)(name, lines);
errors.push((0, exceptions_1.constructTransformationError)({
message: `relation ${name} already exists on type ${typeDef.type}`,
lines,
lineIndex,
metadata: {
symbol: name,
file: filename,
},
}));
continue;
}
const relationsMeta = Object.entries(((_b = typeDef.metadata) === null || _b === void 0 ? void 0 : _b.relations) || {}).find(([n]) => n === name);
if (!relationsMeta) {
errors.push(new errors_1.ModuleTransformationSingleError({
msg: `unable to find relation metadata for ${name}`,
}));
continue;
}
const [, meta] = relationsMeta;
meta.source_info = {
file: filename,
};
original.relations[name] = relation;
original.metadata.relations[name] = meta;
}
typeDefs[originalIndex] = original;
}
}
model.type_definitions = typeDefs;
model.conditions = Object.fromEntries(conditions);
try {
(0, validate_dsl_1.validateJSON)(model);
}
catch (error) {
if (error instanceof errors_1.ModelValidationError) {
for (const e of error.errors) {
if (!e.file || !((_c = e.metadata) === null || _c === void 0 ? void 0 : _c.module) || !e.metadata.symbol) {
errors.push(e);
continue;
}
const lines = (_d = moduleFiles.get(e.file)) === null || _d === void 0 ? void 0 : _d.split("\n");
if (!lines) {
errors.push(e);
continue;
}
const lineIndex = resolveLineIndex(e, lines);
if (lineIndex === -1) {
errors.push(e);
continue;
}
const line = lines[lineIndex];
const wordIndex = resolveWordIndex(e, line);
e.line = { start: lineIndex, end: lineIndex };
e.column = { start: wordIndex, end: wordIndex + (((_e = e.metadata.symbol) === null || _e === void 0 ? void 0 : _e.length) || 0) };
errors.push(e);
}
}
}
if (errors.length) {
throw new errors_1.ModuleTransformationError(errors);
}
return model;
};
exports.transformModuleFilesToModel = transformModuleFilesToModel;
function resolveLineIndex(e, lines) {
const { metadata } = e;
let lineIndex;
if (!metadata || !lines) {
return -1;
}
switch (metadata === null || metadata === void 0 ? void 0 : metadata.errorType) {
case errors_1.ValidationError.ConditionNotUsed:
lineIndex = (0, line_numbers_1.getConditionLineNumber)(metadata.symbol, lines);
break;
case errors_1.ValidationError.ReservedTypeKeywords:
lineIndex = (0, line_numbers_1.getTypeLineNumber)(metadata.symbol, lines);
break;
case errors_1.ValidationError.InvalidName:
// handle type vs relation, this isn't ideal but no other way to check
if (e.message.startsWith("invalid-name error: relation")) {
lineIndex = (0, line_numbers_1.getRelationLineNumber)(metadata.symbol, lines);
}
else {
lineIndex = (0, line_numbers_1.getTypeLineNumber)(metadata.symbol, lines);
}
break;
case errors_1.ValidationError.ReservedRelationKeywords:
lineIndex = (0, line_numbers_1.getRelationLineNumber)(metadata.symbol, lines);
break;
case errors_1.ValidationError.InvalidRelationType:
let offendingTypeIndex = (0, line_numbers_1.getTypeLineNumber)(metadata.offendingType, lines);
if (offendingTypeIndex === -1) {
offendingTypeIndex = (0, line_numbers_1.getTypeLineNumber)(metadata.offendingType, lines, undefined, true);
}
lineIndex = (0, line_numbers_1.getRelationLineNumber)(metadata.relation, lines, offendingTypeIndex);
break;
case errors_1.ValidationError.InvalidRelationOnTupleset:
case errors_1.ValidationError.MissingDefinition:
case errors_1.ValidationError.InvalidType:
case errors_1.ValidationError.ConditionNotDefined:
case errors_1.ValidationError.TuplesetNotDirect:
case errors_1.ValidationError.TypeRestrictionCannotHaveWildcardAndRelation:
case errors_1.ValidationError.RelationNoEntrypoint:
case errors_1.ValidationError.DuplicatedError:
let typeIndex = (0, line_numbers_1.getTypeLineNumber)(metadata.type, lines);
if (typeIndex === -1) {
typeIndex = (0, line_numbers_1.getTypeLineNumber)(metadata.type, lines, undefined, true);
}
// If we don't have a relation then we just want the type index to be the line index
if (!metadata.relation) {
lineIndex = typeIndex;
break;
}
lineIndex = (0, line_numbers_1.getRelationLineNumber)(metadata.relation, lines, typeIndex);
break;
}
if (lineIndex === undefined) {
return -1;
}
return lineIndex;
}
function resolveWordIndex(e, line) {
const { metadata } = e;
if (!metadata) {
return -1;
}
const re = new RegExp("\\b" + metadata.symbol + "\\b");
let wordIdx;
switch (metadata.errorType) {
case errors_1.ValidationError.InvalidType:
// Split line at definition as InvalidType should mark the value, not the key
const splitLine = line.split(":");
wordIdx = splitLine[0].length + splitLine[1].search(re) + 1;
break;
case errors_1.ValidationError.TuplesetNotDirect:
const clauseStartsAt = line.indexOf("from") + "from".length;
wordIdx = clauseStartsAt + line.slice(clauseStartsAt).indexOf(metadata.symbol);
break;
default:
wordIdx = line === null || line === void 0 ? void 0 : line.search(re);
}
if (wordIdx == undefined || isNaN(wordIdx) || wordIdx === -1) {
wordIdx = 0;
}
return wordIdx;
}