UNPKG

@loopback/cli

Version:
215 lines (186 loc) 6.8 kB
// Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved. // Node module: @loopback/cli // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT 'use strict'; const {tsquery} = require('@phenomnomnominal/tsquery'); const {syntaxKindName} = tsquery; const debug = require('./debug')('ast-query'); /** * Parse the file using the possible formats specified in the arrays * rootNodesFindID and childNodesFindID * @param {string} fileContent with a model.ts class */ exports.getIdFromModel = function (fileContent) { const ast = tsquery.ast(fileContent); for (const queryName in MODEL_ID_QUERIES) { debug('Trying %s', queryName); const {query, getModelPropertyDeclaration} = MODEL_ID_QUERIES[queryName]; const idFieldAssignments = tsquery(ast, query); for (const node of idFieldAssignments) { const fieldName = node.name.escapedText; /* istanbul ignore if */ if (debug.enabled) { debug( ' trying prop metadata field "%s" with value `%s`', fieldName, getNodeSource(node), ); } if (!isPrimaryKeyFlag(node.initializer)) continue; const propDeclarationNode = getModelPropertyDeclaration(node); const modelPropertyName = propDeclarationNode.name.escapedText; /* istanbul ignore if */ if (debug.enabled) { debug( 'Found primary key `%s` with id flag set to `%s`', modelPropertyName, getNodeSource(node), ); } return modelPropertyName; } } // no primary key was found return null; function getNodeSource(node) { return fileContent.slice(node.pos, node.end).trim(); } }; const MODEL_ID_QUERIES = { 'default format generated by lb4 model': { // @property({id: true|1}) // id: number query: // Find all class properties decorated with `@property()` 'ClassDeclaration>PropertyDeclaration>Decorator:has([name="id"])>' + // Find object-literal argument passed to `@property` decorator 'CallExpression>ObjectLiteralExpression>' + // Find all assignments to `id` property (metadata field) 'PropertyAssignment:has([name="id"])', getModelPropertyDeclaration(node) { return node.parent.parent.parent.parent; }, }, 'model JSON definition inside the @model decorator': { // @model({properties: {id: {type:number, id:true|1}}}) query: // Find all classes decorated with `@model()` 'ClassDeclaration>Decorator:has([name="model"])>' + // Find object-literal argument passed to `@model` decorator 'CallExpression>ObjectLiteralExpression>' + // Find {properties:{...}} initializer 'PropertyAssignment:has([name="properties"])>ObjectLiteralExpression>' + // Find all model properties, e.g. {name: {required: true}} 'PropertyAssignment>ObjectLiteralExpression>' + // Find all assignments to `id` property (metadata field) 'PropertyAssignment:has([name="id"])', getModelPropertyDeclaration(node) { return node.parent.parent; }, }, 'model JSON definition inside a static model property "definition"': { // static definition = {properties: {id: {type:number, id:true|1}}} query: // Find all classes with static property `definition` // TODO: check for "static" modifier 'ClassDeclaration>PropertyDeclaration:has([name="definition"])>' + // Find object-literal argument used to initialize `definition` 'ObjectLiteralExpression>' + // Find {properties:{...}} initializer 'PropertyAssignment:has([name="properties"])>ObjectLiteralExpression>' + // Find all model properties, e.g. {name: {required: true}} 'PropertyAssignment>ObjectLiteralExpression>' + // Find all assignments to `id` property (metadata field) 'PropertyAssignment:has([name="id"])', getModelPropertyDeclaration(node) { return node.parent.parent; }, }, }; function isPrimaryKeyFlag(idInitializer) { const kindName = syntaxKindName(idInitializer.kind); /* istanbul ignore if */ if (debug.enabled) { debug( 'Checking primary key flag initializer, kind: %s node:', kindName, require('util').inspect( {...idInitializer, parent: '[removed for brevity]'}, {depth: null}, ), ); } // {id: true} if (kindName === 'TrueKeyword') return true; // {id: number} if (kindName === 'NumericLiteral') { const ix = +idInitializer.text; // the value must be a non-zero number, e.g. {id: 1} return ix !== 0 && !isNaN(ix); } return false; } exports.getDataSourceConfig = function (fileContent) { const ast = tsquery.ast(fileContent); // Find a top-level declaration of `config` variable const configVarDecl = tsquery( ast, // It must be a top-level declaration, not inside a function 'SourceFile>VariableStatement>VariableDeclarationList>' + // The declaration should be for a variable called exactly `config` 'VariableDeclaration:has([name="config"])', ); if (!configVarDecl || configVarDecl.length < 1) { debug('No top-level declaration of variable "config" was not found.'); return undefined; } const initializer = configVarDecl[0].initializer; const initializerKindName = syntaxKindName(initializer.kind); if (initializerKindName !== 'ObjectLiteralExpression') { debug( 'Variable "config" is declared with an unsupported initializer kind %s.', initializerKindName, ); return undefined; } const config = Object.create(null); for (const prop of initializer.properties) { const propKind = syntaxKindName(prop.kind); if (propKind !== 'PropertyAssignment') { debug('Skipping unknown property kind %s', propKind); continue; } const propNameKind = syntaxKindName(prop.name.kind); if (propNameKind !== 'Identifier') { debug('Skipping unknown property name kind %s', propNameKind); continue; } const propName = prop.name.escapedText; const propInitKind = syntaxKindName(prop.initializer.kind); let propValue; switch (propInitKind) { case 'StringLiteral': propValue = prop.initializer.text; break; case 'NumericLiteral': propValue = +prop.initializer.text; break; case 'TrueKeyword': propValue = true; break; case 'FalseKeyword': propValue = false; break; default: debug( 'Skipping unsupported property initializer kind %s', propInitKind, ); continue; } debug('Adding config entry %s: %j', propName, propValue); config[propName] = propValue; } return config; };