@loopback/cli
Version:
Yeoman generator for LoopBack 4
215 lines (186 loc) • 6.8 kB
JavaScript
// 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
;
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;
};