UNPKG

@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
"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; }