UNPKG

@synatic/noql

Version:

Convert SQL statements to mongo queries or aggregates

611 lines (591 loc) 17.3 kB
const $check = require('check-types'); const _allowableFunctions = require('../MongoFunctions'); const makeProjectionExpressionPartModule = require('./makeProjectionExpressionPart'); const set = require('lodash/set'); const {findSchema} = require('./schema-utils'); exports.makeCastPart = makeCastPart; /** * @typedef {import("json-schema").JSONSchema6TypeName} JSONSchema6TypeName * @typedef {import("json-schema").JSONSchema6} JSONSchema6 */ /** * Makes an mongo expression tree from the cast statement * * @param {import("../types").Expression} expr - the AST expression that is a cast * @param {import("../types").NoqlContext} context - The Noql context to use when generating the output * @returns {*} */ function makeCastPart(expr, context) { if (expr.type !== 'cast') { throw new Error(`Invalid type for cast:${expr.type}`); } const convertFunction = _allowableFunctions.functionByName('convert'); if (!convertFunction) { throw new Error('No conversion function found'); } const to = expr.target.dataType.toLowerCase(); /** @type {string} */ let from; if (expr.expr.column) { from = `$${expr.expr.table ? expr.expr.table + '.' : ''}${ expr.expr.column }`; } else if (expr.expr.value) { from = expr.expr.value; } else { from = makeProjectionExpressionPartModule.makeProjectionExpressionPart( expr.expr, context ); } if ( to === 'varchar' && context.schemas && Object.keys(context.schemas).length ) { const fromCol = from.replace(/\$/g, ''); const foundSchema = findSchema(fromCol, context); if (foundSchema) { const projection = buildStringifyProjection(foundSchema, fromCol); if (projection) { return projection; } // todo RK, what if you are casting a number column to a varchar, need to be able to check for that and not do a reduce } } return convertFunction.parse([from, to]); } /** * support for non object/array types * update the , between object props to not be there when empty */ /** * * @param {import("../types").JSONSchema6} schema * @param {string} from * @returns {*} */ function buildStringifyProjection(schema, from) { const schemaTypes = getTypes(schema.type); const typeMap = {}; for (const type of schemaTypes) { switch (type) { case 'object': typeMap[type] = buildConcatForObject(schema, from); break; case 'array': typeMap[type] = buildConcatForArray(schema, from); break; case 'boolean': case 'integer': case 'number': case 'string': case 'null': typeMap[type] = buildConcatForPrimitive(type, from); break; case 'any': throw new Error(`Unsupported JSON Schema type "${type}"`); default: throw new Error(`Unsupported JSON Schema type "${type}"`); } } const keys = Object.keys(typeMap); if (keys.length === 0) { return null; } if (keys.length === 1) { return typeMap[keys[0]]; } const conditions = convertTypeMapToConditions(typeMap, from, from, false); return conditions.reduce((previous, current, index) => { const path = buildPath(index); set(previous, path, current); return previous; }, makeCondition(from, 'missing', null, from, false)); } /** * @param {import("../types").JSONSchema6} schema * @param {string} from * @returns {*} */ function buildConcatForObject(schema, from) { const concatItems = []; concatItems.push('{'); let counter = 0; const length = Object.keys(schema.properties).length; // eslint-disable-next-line guard-for-in for (const propName in schema.properties) { counter++; const isLast = counter === length; const includeComma = !isLast; const propValue = schema.properties[propName]; if ($check.boolean(propValue)) { continue; } const fullPropName = `${from}.${propName}`; const typeMap = buildTypeMap(propValue, fullPropName, propName, from); const conditions = convertTypeMapToConditions( typeMap, fullPropName, propName, includeComma ); if (!conditions) { continue; } const reduced = conditions.reduce((previous, current, index) => { if (!previous) { return current; } const path = buildPath(index); set(previous, path, current); return previous; }); concatItems.push(reduced); } concatItems.push('}'); return { $concat: [ { $cond: [ { $eq: [{$type: `$${from}`}, 'missing'], }, null, {$concat: concatItems}, ], }, ], }; } /** * @param {JSONSchema6} propValue * @param {string} fullPropName * @param {string} propName * @param {string} from * @returns {*} */ function buildTypeMap(propValue, fullPropName, propName, from) { const propertyTypes = getTypes(propValue.type); const typeMap = {}; for (const type of propertyTypes) { switch (type) { case 'object': { const res = buildConcatForObject(propValue, fullPropName); if (res) { typeMap[type] = [`"${propName}":`, res]; } } break; case 'array': { const res = buildConcatForArray(propValue, fullPropName); if (res) { typeMap[type] = [`"${propName}":`, res]; } } break; case 'boolean': case 'integer': case 'number': case 'string': typeMap[type] = buildConcatForPrimitive(type, propName, from); break; case 'null': break; case 'any': throw new Error(`Unsupported JSON Schema type "${type}"`); default: throw new Error(`Unsupported JSON Schema type "${type}"`); } } return typeMap; } /** * * @param {object} typeMap * @param {string} fullPropName * @param {string} propName * @param {boolean} includeComma * @returns {*[]} */ function convertTypeMapToConditions( typeMap, fullPropName, propName, includeComma ) { const keys = Object.keys(typeMap); if (keys.length === 0) { return null; } const conditions = []; for (const key of keys) { const subPipeline = typeMap[key]; switch (key) { case 'object': conditions.push( makeCondition( fullPropName, 'object', subPipeline, propName, includeComma ) ); break; case 'array': conditions.push( makeCondition( fullPropName, 'array', subPipeline, propName, includeComma ) ); break; case 'boolean': conditions.push( makeCondition( fullPropName, 'bool', subPipeline, propName, includeComma ) ); break; case 'integer': case 'number': conditions.push( makeCondition( fullPropName, ['double', 'int', 'long', 'decimal'], subPipeline, propName, includeComma ) ); break; case 'string': conditions.push( makeCondition( fullPropName, 'string', subPipeline, propName, includeComma ) ); break; case 'null': break; // case 'any': // throw new Error( // `Unsupported JSON Schema type "${key}"` // ); default: throw new Error(`Unsupported JSON Schema type "${key}"`); } } return conditions; } /** * * @param {number}index * @returns {string} */ function buildPath(index) { const str = []; for (let i = 0; i <= index; i++) { str.push('$cond[2]'); } return str.join('.'); } /** * * @param {string} fullPropName * @param {string|string[]} mongoType * @param {*}subPipeline * @param {string} propName * @param {boolean}includeComma */ function makeCondition( fullPropName, mongoType, subPipeline, propName, includeComma = false ) { if (Array.isArray(subPipeline)) { subPipeline = {$concat: subPipeline}; } if (includeComma && $check.object(subPipeline) && subPipeline.$concat) { subPipeline.$concat.push(','); } if (!Array.isArray(mongoType)) { return { $cond: [ { $eq: [{$type: `$${fullPropName}`}, mongoType], }, subPipeline, `"${propName}":null${includeComma ? ',' : ''}`, ], }; } return { $cond: [ { $or: mongoType.map((type) => { return { $eq: [{$type: `$${fullPropName}`}, type], }; }), }, subPipeline, `"${propName}":null${includeComma ? ',' : ''}`, ], }; } /** * * @param {JSONSchema6TypeName} type * @param {string} propName * @param {string }[from] */ function buildConcatForPrimitive(type, propName, from) { if (!from) { return { $concat: [ type !== 'string' ? {$toString: `$${propName}`} : `$${propName}`, ], }; } const fullPath = `${from}.${propName}`; return { $concat: [ `"${propName}":${type === 'string' ? '"' : ''}`, type !== 'string' ? {$toString: `$${fullPath}`} : `$${fullPath}`, `${type === 'string' ? '"' : ''}`, ], }; } /** * * @param {JSONSchema6TypeName|JSONSchema6TypeName[]}type */ function getTypes(type) { if (!type) { type = ['any']; } /** @type {JSONSchema6TypeName[]}*/ let typeArray = Array.isArray(type) ? type : [type]; typeArray = typeArray.filter((t) => t !== 'null'); return typeArray; } /** * @param {import("../types").JSONSchema6} schema * @param {string} from * @returns {*} */ function buildConcatForArray(schema, from) { if (!schema.items) { throw new Error(`Schema.items is not set`); } if ($check.boolean(schema.items)) { throw new Error(`Schema.items is a boolean, not yet supported`); } if (Array.isArray(schema.items)) { throw new Error( `Schema.items was an array, not yet supported: ${JSON.stringify( schema.items, null, 4 )}` ); } const propertyTypes = getTypes(schema.items.type); const typeMap = {}; if (schema.items.properties) { if (propertyTypes.indexOf('object') < 0) { propertyTypes.push('object'); } const res = buildConcatForObject(schema.items, '$this'); if (res) { typeMap['object'] = res; } } if (schema.items.items) { if (propertyTypes.indexOf('array') < 0) { propertyTypes.push('array'); } const res = buildConcatForArray(schema.items, '$this'); if (res) { typeMap['array'] = res; } } for (const type of propertyTypes) { switch (type) { case 'object': // handled above break; case 'array': // todo above break; case 'boolean': case 'integer': case 'number': case 'string': typeMap[type] = makePrimitiveArrayProjection(type); break; case 'null': break; // case 'any': // throw new Error(`Unsupported JSON Schema type "${type}"`); default: throw new Error(`Unsupported JSON Schema type "${type}"`); } } const conditions = convertTypeMapToArrayConditions(typeMap, false); if (!conditions) { return null; } const reduced = conditions.reduce((previous, current, index) => { const path = buildPath(index); set(previous, path, current); return previous; }, makeArrayCondition('missing', '', false)); return { $concat: [ '[', { $reduce: { input: `$${from}`, initialValue: '', in: { $concat: [ '$$value', {$cond: [{$eq: ['$$value', '']}, '', ',']}, reduced, ], }, }, }, ']', ], }; } /** * * @param {JSONSchema6TypeName} type * @returns {*[]} */ function makePrimitiveArrayProjection(type) { return [ type === 'string' ? '"' : '', type === 'string' ? '$$this' : {$toString: '$$this'}, type === 'string' ? '"' : '', ]; } /** * * @param {object} typeMap * @param {boolean} includeComma * @returns {*[]} */ function convertTypeMapToArrayConditions(typeMap, includeComma) { const keys = Object.keys(typeMap); if (keys.length === 0) { return null; } const conditions = []; for (const key of keys) { const subPipeline = typeMap[key]; switch (key) { case 'object': conditions.push( makeArrayCondition('object', subPipeline, includeComma) ); break; case 'array': conditions.push( makeArrayCondition('array', subPipeline, includeComma) ); break; case 'boolean': conditions.push( makeArrayCondition('bool', subPipeline, includeComma) ); break; case 'integer': case 'number': conditions.push( makeArrayCondition( ['double', 'int', 'long', 'decimal'], subPipeline, includeComma ) ); break; case 'string': conditions.push( makeArrayCondition('string', subPipeline, includeComma) ); break; case 'null': break; // case 'any': // throw new Error( // `Unsupported JSON Schema type "${key}"` // ); default: throw new Error(`Unsupported JSON Schema type "${key}"`); } } return conditions; } /** * * @param {string|string[]} mongoType * @param {*}subPipeline * @param {boolean}includeComma */ function makeArrayCondition(mongoType, subPipeline, includeComma = false) { if (Array.isArray(subPipeline)) { subPipeline = {$concat: subPipeline}; } if (includeComma && $check.object(subPipeline) && subPipeline.$concat) { subPipeline.$concat.push(','); } if (!Array.isArray(mongoType)) { return { $cond: [ { $eq: [{$type: `$$this`}, mongoType], }, subPipeline, // `null${includeComma ? ',' : ''}`, '', ], }; } return { $cond: [ { $or: mongoType.map((type) => { return { $eq: [{$type: `$$this`}, type], }; }), }, subPipeline, // `null${includeComma ? ',' : ''}`, '', ], }; }