UNPKG

zenstack

Version:

FullStack enhancement for Prisma ORM: seamless integration from database to UI

424 lines 18.3 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ZModelLinker = void 0; const ast_1 = require("@zenstackhq/language/ast"); const sdk_1 = require("@zenstackhq/sdk"); const langium_1 = require("langium"); const ts_pattern_1 = require("ts-pattern"); const vscode_jsonrpc_1 = require("vscode-jsonrpc"); const ast_utils_1 = require("../utils/ast-utils"); const utils_1 = require("./utils"); const utils_2 = require("./validator/utils"); /** * Langium linker implementation which links references and resolves expression types */ class ZModelLinker extends langium_1.DefaultLinker { constructor(services) { super(services); this.descriptions = services.workspace.AstNodeDescriptionProvider; } //#region Reference linking link(document_1) { return __awaiter(this, arguments, void 0, function* (document, cancelToken = vscode_jsonrpc_1.CancellationToken.None) { var _a, _b; if (((_a = document.parseResult.lexerErrors) === null || _a === void 0 ? void 0 : _a.length) > 0 || ((_b = document.parseResult.parserErrors) === null || _b === void 0 ? void 0 : _b.length) > 0) { return; } for (const node of (0, langium_1.streamContents)(document.parseResult.value)) { yield (0, langium_1.interruptAndCheck)(cancelToken); this.resolve(node, document); } document.state = langium_1.DocumentState.Linked; }); } linkReference(container, property, document, extraScopes) { if (this.resolveFromScopeProviders(container, property, document, extraScopes)) { return; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const reference = container[property]; this.doLink({ reference, container, property }, document); } //#endregion //#region Expression type resolving resolveFromScopeProviders(node, property, document, providers) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const reference = node[property]; for (const provider of providers) { const target = provider(reference.$refText); if (target) { reference._ref = target; reference._nodeDescription = this.descriptions.createDescription(target, target.name, document); // Add the reference to the document's array of references document.references.push(reference); return target; } } return null; } resolve(node, document, extraScopes = []) { switch (node.$type) { case ast_1.StringLiteral: case ast_1.NumberLiteral: case ast_1.BooleanLiteral: this.resolveLiteral(node); break; case ast_1.InvocationExpr: this.resolveInvocation(node, document, extraScopes); break; case ast_1.ArrayExpr: this.resolveArray(node, document, extraScopes); break; case ast_1.ReferenceExpr: this.resolveReference(node, document, extraScopes); break; case ast_1.MemberAccessExpr: this.resolveMemberAccess(node, document, extraScopes); break; case ast_1.UnaryExpr: this.resolveUnary(node, document, extraScopes); break; case ast_1.BinaryExpr: this.resolveBinary(node, document, extraScopes); break; case ast_1.ObjectExpr: this.resolveObject(node, document, extraScopes); break; case ast_1.ThisExpr: this.resolveThis(node, document, extraScopes); break; case ast_1.NullExpr: this.resolveNull(node, document, extraScopes); break; case ast_1.AttributeArg: this.resolveAttributeArg(node, document, extraScopes); break; case ast_1.DataModel: this.resolveDataModel(node, document, extraScopes); break; case ast_1.DataModelField: this.resolveDataModelField(node, document, extraScopes); break; default: this.resolveDefault(node, document, extraScopes); break; } } resolveBinary(node, document, extraScopes) { switch (node.operator) { // TODO: support arithmetics? // case '+': // case '-': // case '*': // case '/': // this.resolve(node.left, document, extraScopes); // this.resolve(node.right, document, extraScopes); // this.resolveToBuiltinTypeOrDecl(node, 'Int'); // break; case '>': case '>=': case '<': case '<=': case '==': case '!=': case '&&': case '||': case 'in': this.resolve(node.left, document, extraScopes); this.resolve(node.right, document, extraScopes); this.resolveToBuiltinTypeOrDecl(node, 'Boolean'); break; case '?': case '!': case '^': this.resolveCollectionPredicate(node, document, extraScopes); break; default: throw Error(`Unsupported binary operator: ${node.operator}`); } } resolveUnary(node, document, extraScopes) { this.resolve(node.operand, document, extraScopes); switch (node.operator) { case '!': this.resolveToBuiltinTypeOrDecl(node, 'Boolean'); break; default: throw Error(`Unsupported unary operator: ${node.operator}`); } } resolveObject(node, document, extraScopes) { node.fields.forEach((field) => this.resolve(field.value, document, extraScopes)); this.resolveToBuiltinTypeOrDecl(node, 'Object'); } resolveReference(node, document, extraScopes) { this.resolveDefault(node, document, extraScopes); if (node.target.ref) { // resolve type if (node.target.ref.$type === ast_1.EnumField) { this.resolveToBuiltinTypeOrDecl(node, node.target.ref.$container); } else { this.resolveToDeclaredType(node, node.target.ref.type); } } } resolveArray(node, document, extraScopes) { node.items.forEach((item) => this.resolve(item, document, extraScopes)); if (node.items.length > 0) { const itemType = node.items[0].$resolvedType; if (itemType === null || itemType === void 0 ? void 0 : itemType.decl) { this.resolveToBuiltinTypeOrDecl(node, itemType.decl, true); } } else { this.resolveToBuiltinTypeOrDecl(node, 'Any', true); } } resolveInvocation(node, document, extraScopes) { this.linkReference(node, 'function', document, extraScopes); node.args.forEach((arg) => this.resolve(arg, document, extraScopes)); if (node.function.ref) { // eslint-disable-next-line @typescript-eslint/ban-types const funcDecl = node.function.ref; if ((0, sdk_1.isAuthInvocation)(node)) { // auth() function is resolved against all loaded and reachable documents // get all data models from loaded and reachable documents const allDecls = (0, ast_utils_1.getAllLoadedAndReachableDataModelsAndTypeDefs)(this.langiumDocuments(), (0, langium_1.getContainerOfType)(node, ast_1.isDataModel)); const authDecl = (0, sdk_1.getAuthDecl)(allDecls); if (authDecl) { node.$resolvedType = { decl: authDecl, nullable: true }; } } else if ((0, sdk_1.isFutureExpr)(node)) { // future() function is resolved to current model node.$resolvedType = { decl: (0, ast_utils_1.getContainingDataModel)(node) }; } else { this.resolveToDeclaredType(node, funcDecl.returnType); } } } resolveLiteral(node) { const type = (0, ts_pattern_1.match)(node) .when(ast_1.isStringLiteral, () => 'String') .when(ast_1.isBooleanLiteral, () => 'Boolean') .when(ast_1.isNumberLiteral, () => 'Int') .exhaustive(); if (type) { this.resolveToBuiltinTypeOrDecl(node, type); } } resolveMemberAccess(node, document, extraScopes) { this.resolveDefault(node, document, extraScopes); const operandResolved = node.operand.$resolvedType; if (operandResolved && !operandResolved.array && (0, utils_1.isMemberContainer)(operandResolved.decl)) { // member access is resolved only in the context of the operand type if (node.member.ref) { this.resolveToDeclaredType(node, node.member.ref.type); if (node.$resolvedType && (0, sdk_1.isAuthInvocation)(node.operand)) { // member access on auth() function is nullable // because user may not have provided all fields node.$resolvedType.nullable = true; } } } } resolveCollectionPredicate(node, document, extraScopes) { this.resolveDefault(node, document, extraScopes); const resolvedType = node.left.$resolvedType; if (resolvedType && (0, utils_1.isMemberContainer)(resolvedType.decl) && resolvedType.array) { this.resolveToBuiltinTypeOrDecl(node, 'Boolean'); } else { // error is reported in validation pass } } resolveThis(node, _document, extraScopes) { // resolve from scopes first for (const scope of extraScopes) { const r = scope('this'); if ((0, ast_1.isDataModel)(r)) { this.resolveToBuiltinTypeOrDecl(node, r); return; } } let decl = node.$container; while (decl && !(0, ast_1.isDataModel)(decl)) { decl = decl.$container; } if (decl) { this.resolveToBuiltinTypeOrDecl(node, decl); } } resolveNull(node, _document, _extraScopes) { // TODO: how to really resolve null? this.resolveToBuiltinTypeOrDecl(node, 'Null'); } resolveAttributeArg(node, document, extraScopes) { var _a, _b, _c; const attrParam = this.findAttrParamForArg(node); const attrAppliedOn = node.$container.$container; if ((attrParam === null || attrParam === void 0 ? void 0 : attrParam.type.type) === 'TransitiveFieldReference' && (0, ast_1.isDataModelField)(attrAppliedOn)) { // "TransitiveFieldReference" is resolved in the context of the containing model of the field // where the attribute is applied // // E.g.: // // model A { // myId @id String // } // // model B { // id @id String // a A @relation(fields: [id], references: [myId]) // } // // In model B, the attribute argument "myId" is resolved to the field "myId" in model A const transitiveDataModel = (_a = attrAppliedOn.type.reference) === null || _a === void 0 ? void 0 : _a.ref; if (transitiveDataModel) { // resolve references in the context of the transitive data model const scopeProvider = (name) => (0, sdk_1.getModelFieldsWithBases)(transitiveDataModel).find((f) => f.name === name); if ((0, ast_1.isArrayExpr)(node.value)) { node.value.items.forEach((item) => { if ((0, ast_1.isReferenceExpr)(item)) { const resolved = this.resolveFromScopeProviders(item, 'target', document, [scopeProvider]); if (resolved) { this.resolveToDeclaredType(item, resolved.type); } else { // mark unresolvable this.unresolvableRefExpr(item); } } }); if ((_c = (_b = node.value.items[0]) === null || _b === void 0 ? void 0 : _b.$resolvedType) === null || _c === void 0 ? void 0 : _c.decl) { this.resolveToBuiltinTypeOrDecl(node.value, node.value.items[0].$resolvedType.decl, true); } } else if ((0, ast_1.isReferenceExpr)(node.value)) { const resolved = this.resolveFromScopeProviders(node.value, 'target', document, [scopeProvider]); if (resolved) { this.resolveToDeclaredType(node.value, resolved.type); } else { // mark unresolvable this.unresolvableRefExpr(node.value); } } } } else { this.resolve(node.value, document, extraScopes); } node.$resolvedType = node.value.$resolvedType; } unresolvableRefExpr(item) { const ref = item.target; ref._ref = this.createLinkingError({ reference: ref, container: item, property: 'target', }); } findAttrParamForArg(arg) { var _a; const attr = arg.$container.decl.ref; if (!attr) { return undefined; } if (arg.name) { return (_a = attr.params) === null || _a === void 0 ? void 0 : _a.find((p) => p.name === arg.name); } else { const index = arg.$container.args.findIndex((a) => a === arg); return attr.params[index]; } } resolveDataModel(node, document, extraScopes) { return this.resolveDefault(node, document, extraScopes); } resolveDataModelField(node, document, extraScopes) { // Field declaration may contain enum references, and enum fields are pushed to the global // scope, so if there're enums with fields with the same name, an arbitrary one will be // used as resolution target. The correct behavior is to resolve to the enum that's used // as the declaration type of the field: // // enum FirstEnum { // E1 // E2 // } var _a; // enum SecondEnum { // E1 // E3 // E4 // } // model M { // id Int @id // first SecondEnum @default(E1) <- should resolve to SecondEnum // second FirstEnum @default(E1) <- should resolve to FirstEnum // } // // make sure type is resolved first this.resolve(node.type, document, extraScopes); let scopes = extraScopes; // if the field has enum declaration type, resolve the rest with that enum's fields on top of the scopes if (((_a = node.type.reference) === null || _a === void 0 ? void 0 : _a.ref) && (0, ast_1.isEnum)(node.type.reference.ref)) { const contextEnum = node.type.reference.ref; const enumScope = (name) => contextEnum.fields.find((f) => f.name === name); scopes = [enumScope, ...scopes]; } this.resolveDefault(node, document, scopes); } resolveDefault(node, document, extraScopes) { for (const [property, value] of Object.entries(node)) { if (!property.startsWith('$')) { if ((0, langium_1.isReference)(value)) { this.linkReference(node, property, document, extraScopes); } } } for (const child of (0, langium_1.streamContents)(node)) { this.resolve(child, document, extraScopes); } } //#endregion //#region Utils resolveToDeclaredType(node, type) { let nullable = false; if ((0, ast_1.isDataModelFieldType)(type) || (0, ast_1.isTypeDefField)(type)) { nullable = type.optional; // referencing a field of 'Unsupported' type if (type.unsupported) { node.$resolvedType = { decl: 'Unsupported', array: type.array, nullable }; return; } } if (type.type) { const mappedType = (0, utils_2.mapBuiltinTypeToExpressionType)(type.type); node.$resolvedType = { decl: mappedType, array: type.array, nullable: nullable }; } else if (type.reference) { node.$resolvedType = { decl: type.reference.ref, array: type.array, nullable: nullable, }; } } resolveToBuiltinTypeOrDecl(node, type, array = false, nullable = false) { node.$resolvedType = { decl: type, array, nullable }; } } exports.ZModelLinker = ZModelLinker; //# sourceMappingURL=zmodel-linker.js.map