UNPKG

@nlighten/json-transform-core

Version:

Core types and utilities for handling JSON transformers

522 lines 27.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseTransformer = void 0; const json_schema_utils_1 = require("@nlighten/json-schema-utils"); const types_1 = require("./functions/types"); const functionsParser_1 = require("./functions/functionsParser"); const jsonpathFunctions_1 = require("./jsonpath/jsonpathFunctions"); const jsonpathJoin_1 = require("./jsonpath/jsonpathJoin"); const ParseContext_1 = __importDefault(require("./ParseContext")); const ALL_DIGITS = /^\d+$/; class TransformerParser { static copySubPathsOnWalk(sourcePath, targetPath, localPath, previousPaths, paths, context) { let simplePath = sourcePath.replace(/\[[^\]]+]/g, "[]"); const sourceIsArray = sourcePath.includes("[*]") || sourcePath.includes("[?("); if (context.hasPaths() && sourceIsArray) { const itemType = context.resolve(sourcePath) ?? context.resolve(simplePath); context.setPath(targetPath + localPath, { type: "array", items: itemType ? structuredClone(itemType) : context.resolve(simplePath.slice(0, -2)) ? structuredClone(context.resolve(simplePath.slice(0, -2))) : undefined, }); if (!itemType) { simplePath = simplePath.slice(0, -2); } } const baseSubPath = localPath + (sourceIsArray ? "[]" : ""); previousPaths .filter(x => x === sourcePath || x.startsWith(sourcePath + ".") || x.startsWith(sourcePath + "[") || x === simplePath || x.startsWith(simplePath + ".") || x.startsWith(simplePath + "[")) .forEach(prevPath => { const detectedBySimple = prevPath === simplePath || prevPath.startsWith(simplePath + ".") || prevPath.startsWith(simplePath + "["); const subPath = baseSubPath + prevPath.substring(detectedBySimple ? simplePath.length : sourcePath.length); // copy types if (context.hasPaths() && context.resolve(prevPath)) { context.setPath(targetPath + subPath, structuredClone(context.resolve(prevPath))); } if (!paths.includes(targetPath + subPath)) { paths.push(targetPath + subPath); } }); } static isCustomJsonPath(data, context) { return typeof data === "string" && (data.startsWith("$.") || data.startsWith("#") || data === "$"); } static rawWalkOnObject(targetPath, data, s, paths, context) { if (s) paths.push(targetPath + s); if (context.hasPaths() && typeof data !== "undefined") { context.setPath(targetPath + s, (0, json_schema_utils_1.inferJSONSchemaType)(data)); } // definition is Array if (Array.isArray(data)) { for (let i = 0; i < data.length; i++) { TransformerParser.rawWalkOnObject(targetPath, data[i], s + "[" + i + "]", paths, context); } const arrayType = context.getPath(targetPath + s); if (arrayType && data.length > 0) { arrayType.items = data.map((c, i) => structuredClone(context.resolve(targetPath + s + `[${i}]`))); } return; } if (data && typeof data === "object") { // definition is plain object (not OJF) for (const p in data) { if (jsonpathJoin_1.VALID_ID_REGEXP.test(p)) { TransformerParser.rawWalkOnObject(targetPath, data[p], s + "." + p, paths, context); } else { TransformerParser.rawWalkOnObject(targetPath, data[p], s + "[" + (ALL_DIGITS.test(p) ? p : JSON.stringify(p)) + "]", paths, context); } } } } static findNonNull(value, context) { if (!context.hasPaths()) return null; const items = context.resolve(value)?.items; if (!Array.isArray(items)) return null; return items.findIndex(x => x.type !== "null"); } /** * value is array of unknown size. * if value is array, take first (non null preferably), * otherwise, try to form a jsonpath to first non-null element */ static copyArrayItemTypeOnWalk(value, targetPath, localPath, previousPaths, paths, context) { const dataWithResultType = Array.isArray(value) ? (context.hasPaths() && value.find((x) => context.resolve(x)?.type !== "null")) ?? value.find(x => typeof x !== "undefined" && x !== null) ?? value[0] : previousPaths.includes(value + "[0]") ? value + `[${TransformerParser.findNonNull(value, context) ?? 0}]` : value + "[]"; TransformerParser.parse(dataWithResultType, targetPath, localPath, previousPaths, paths, context); } } /** * Traverse an object and set all paths encountered to targetPath + localPath * @param definition * @param targetPath extract output paths to this prefix * @param localPath current path prefix in traversal * @param previousPaths paths that are input to this transformation (but might not be part of the output) * @param paths result paths encountered to be in the output object of this transformation * @param context map of path to schema, should contain already existing paths to extract from (will be updated if exists) */ TransformerParser.parse = (definition, targetPath, localPath, previousPaths, paths, context) => { if (localPath.includes("*") || localPath.includes("..")) return; if (localPath) paths.push(targetPath + localPath); // definition is a reference inside $$map / $$transform functions if (definition === "##index") { if (context.hasPaths()) { context.setPath(targetPath + localPath, { type: "integer" }); } return; } // definition is string and custom JsonPath (e.g. $ / $. / # / ##) if (context.isReferencingKnownVariable(definition)) { // check if jsonpath is using a function (e.g. $.concat()) const jsonPathFunctionSchema = (0, jsonpathFunctions_1.matchJsonPathFunction)(definition); if (jsonPathFunctionSchema) { if (context.hasPaths()) { context.setPath(targetPath + localPath, structuredClone(jsonPathFunctionSchema)); if (jsonPathFunctionSchema.type === "array") { paths.push(targetPath + localPath + "[]"); context.setPath(targetPath + localPath + "[]", structuredClone(jsonPathFunctionSchema.items)); } } } else { // copy all the sub paths of that path object TransformerParser.copySubPathsOnWalk(definition, targetPath, localPath, previousPaths, paths, context); if (context.hasPaths() && !context.resolve(targetPath + localPath)) { context.setPath(targetPath + localPath, context.resolve(definition) ? structuredClone(context.resolve(definition)) : {}); } } return; } if (functionsParser_1.functionsParser.matchInline(definition, (funcName, func, value, args) => { TransformerParser.handleFunction("inline", funcName, func, value, args, targetPath, localPath, previousPaths, paths, context); })) { // inline function - handled return; } if (definition && typeof definition === "object" && !Array.isArray(definition)) { const objFunc = functionsParser_1.functionsParser.matchObject(definition, true); if (objFunc) { TransformerParser.handleFunction("object", objFunc.name, objFunc.func, objFunc.value, objFunc.args, targetPath, localPath, previousPaths, paths, context); } else { // definition is plain object for (const p in definition) { if (p === "*") { if (context.isReferencingKnownVariable(definition["*"])) { TransformerParser.copySubPathsOnWalk(definition["*"], targetPath, localPath, previousPaths, paths, context); } else if (Array.isArray(definition["*"])) { definition["*"].forEach(pc => context.isReferencingKnownVariable(pc) && TransformerParser.copySubPathsOnWalk(pc, targetPath, localPath, previousPaths, paths, context)); } continue; } const propPath = jsonpathJoin_1.VALID_ID_REGEXP.test(p) ? "." + p : "[" + (ALL_DIGITS.test(p) ? p : JSON.stringify(p)) + "]"; TransformerParser.parse(definition[p], targetPath, localPath + propPath, previousPaths, paths, context); if (context.hasPaths()) { if (!context.hasPath(targetPath + localPath)) { context.setPath(targetPath + localPath, { type: "object", additionalProperties: false }); } const objType = context.getPath(targetPath + localPath); const properties = objType?.properties ?? {}; const srcType = context.resolve(targetPath + localPath + propPath); if (objType && typeof srcType !== "undefined") { properties[p] = structuredClone(srcType); objType.properties = properties; } } } } } else if (Array.isArray(definition) && definition.length > 0) { if (context.hasPaths()) { context.setPath(targetPath + localPath, { type: "array" }); } for (let i = 0; i < definition.length; i++) { TransformerParser.parse(definition[i], targetPath, localPath + "[" + i + "]", previousPaths, paths, context); } if (context.hasPaths()) { // check if all paths are the same, then compact it const arrType = context.getPath(targetPath + localPath); const firstType = context.getPath(targetPath + localPath + "[0]"); const singleType = firstType && definition.every((x, i) => (0, json_schema_utils_1.areSimilar)(firstType, context.resolve(targetPath + localPath + "[" + i + "]"))); if (singleType) { const pref = targetPath + localPath; const firstIndex = paths.indexOf(targetPath + localPath + "[0]"); const lastIndex = paths.indexOf(targetPath + localPath + "[" + (definition.length - 1) + "]"); for (let i = firstIndex; i <= lastIndex; i++) { const x = paths[i]; if (context.resolve(x)) { context.setPath(pref + x.slice(pref.length).replace(/^\[\d+]/, "[]"), structuredClone(context.resolve(x))); } } Array.prototype.splice.apply(paths, [firstIndex, paths.length - firstIndex].concat(paths.slice(lastIndex).map(x => pref + x.slice(pref.length).replace(/^\[\d+]/, "[]")))); if (arrType) { arrType.items = structuredClone(context.resolve(targetPath + localPath + "[]")); } } else if (arrType) { arrType.items = definition.map((c, i) => structuredClone(context.resolve(targetPath + localPath + `[${i}]`))); } } } else { // definition is either unrecognized string (treated as string) or another json type if (context.hasPaths() && typeof definition !== "undefined") { context.setPath(targetPath + localPath, (0, json_schema_utils_1.inferJSONSchemaType)(definition)); } } }; /** * Handle resolving of function output type */ TransformerParser.handleFunction = (detectedAs, funcName, func, value, args, targetPath, localPath, previousPaths, paths, context) => { let unhandled = false; switch (funcName) { case types_1.EmbeddedTransformerFunction.at: case types_1.EmbeddedTransformerFunction.coalesce: case types_1.EmbeddedTransformerFunction.concat: case types_1.EmbeddedTransformerFunction.find: case types_1.EmbeddedTransformerFunction.flat: { TransformerParser.copyArrayItemTypeOnWalk(value, targetPath, localPath, previousPaths, paths, context); if (funcName === types_1.EmbeddedTransformerFunction.concat || funcName === types_1.EmbeddedTransformerFunction.flat) { // fix type if not array if (context.hasPaths() && context.resolve(targetPath + localPath)?.type !== "array") { context.setPath(targetPath + localPath, { type: "array", items: context.resolve(targetPath + localPath), }); } } break; } case types_1.EmbeddedTransformerFunction.distinct: case types_1.EmbeddedTransformerFunction.filter: case types_1.EmbeddedTransformerFunction.reverse: case types_1.EmbeddedTransformerFunction.slice: case types_1.EmbeddedTransformerFunction.sort: { // input is unknown size array (result might be smaller in size, but of same type) TransformerParser.copyArrayItemTypeOnWalk(value, targetPath, localPath + "[]", previousPaths, paths, context); if (!paths.includes(targetPath + localPath + "[]")) { paths.push(targetPath + localPath + "[]"); if (context.hasPaths()) { if (!context.resolve(targetPath + localPath + "[]")) { context.setPath(targetPath + localPath + "[]", structuredClone(context.resolve(targetPath + localPath + "[0]") ?? { type: "object", })); } } } if (context.hasPaths() && (!context.resolve(targetPath + localPath) || context.resolve(targetPath + localPath)?.type !== "array")) { context.setPath(targetPath + localPath, { type: "array", items: structuredClone(context.resolve(targetPath + localPath + "[]")), }); } break; } case types_1.EmbeddedTransformerFunction.if: { if (typeof args.then !== "undefined") { TransformerParser.parse(args.then, targetPath, localPath, previousPaths, paths, context); break; } // input is array of known size (object with result type is at index 1) const dataWithResultType = typeof value === "string" ? value + "[1]" : value?.[1]; TransformerParser.parse(dataWithResultType, targetPath, localPath, previousPaths, paths, context); break; } case types_1.EmbeddedTransformerFunction.reduce: { if (typeof args.identity !== "undefined") { TransformerParser.parse(args.identity, targetPath, localPath, previousPaths, paths, context); } break; } case types_1.EmbeddedTransformerFunction.jsonpatch: case types_1.EmbeddedTransformerFunction.value: { // input and output should have the same schema TransformerParser.parse(value, targetPath, localPath, previousPaths, paths, context); break; } case types_1.EmbeddedTransformerFunction.raw: { // input type is the result type TransformerParser.rawWalkOnObject(targetPath, value, localPath, paths, context); break; } case types_1.EmbeddedTransformerFunction.max: case types_1.EmbeddedTransformerFunction.min: { // if (localPath) paths.push(targetPath + localPath); const typ = args?.type?.toUpperCase(); switch (typ) { case "NUMBER": if (context.hasPaths()) { context.setPath(targetPath + localPath, { type: "number" }); } break; case "BOOLEAN": if (context.hasPaths()) { context.setPath(targetPath + localPath, { type: "boolean" }); } break; case "STRING": if (context.hasPaths()) { context.setPath(targetPath + localPath, { type: "string" }); } break; default: TransformerParser.copyArrayItemTypeOnWalk(value, targetPath, localPath, previousPaths, paths, context); return true; } break; } case types_1.EmbeddedTransformerFunction.merge: { //if (args.deep) { // collect all paths from all objects if (Array.isArray(value)) { value.forEach((src) => { TransformerParser.parse(src, targetPath, localPath, previousPaths, paths, context); }); } /*} else { // TODO: do shallow copy paths value.forEach((src: any) => { TransformerParser.parse(src, targetPath, localPath, previousPaths, paths, context); }); }*/ break; } case types_1.EmbeddedTransformerFunction.partition: { paths.push(targetPath + localPath + "[]"); TransformerParser.copyArrayItemTypeOnWalk(value, targetPath, localPath + "[][]", previousPaths, paths, context); if (context.hasPaths()) { if (!context.resolve(targetPath + localPath + "[]")) { context.setPath(targetPath + localPath + "[]", { type: "array", items: structuredClone(context.resolve(targetPath + localPath + "[][]")), }); } context.setPath(targetPath + localPath, { type: "array", items: structuredClone(context.resolve(targetPath + localPath + "[]")), }); } break; } case types_1.EmbeddedTransformerFunction.repeat: { TransformerParser.parse(value, targetPath, localPath + "[]", previousPaths, paths, context); if (context.hasPaths()) { context.setPath(targetPath + localPath, { type: "array", items: structuredClone(context.resolve(targetPath + localPath + "[]")), }); } break; } case types_1.EmbeddedTransformerFunction.lookup: { // input is array of known size (object with result type is at index 0) // * This is a special case because the transformer uses ##current/{alias} / ##index to reference the object / iteration element const itemsData = value; const dataWithCurrentType = Array.isArray(itemsData) ? itemsData[0] : previousPaths.includes(itemsData + "[0]") ? itemsData + "[0]" // this is not the best approach (other indices may have different fields) : itemsData + "[]"; const currentPaths = []; TransformerParser.parse(dataWithCurrentType, "##current", "", previousPaths, currentPaths, context); if (Array.isArray(args.using)) { if (args.to) { args.using.forEach((use) => { if (typeof use.as !== "string") return; // invalid const dataWithMatchType = Array.isArray(use.with) ? use.with[0] : previousPaths.includes(use.with + "[0]") ? use.with + "[0]" // this is not the best approach (other indices may have different fields) : use.with + "[]"; TransformerParser.parse(dataWithMatchType, "##" + use.as, "", previousPaths, currentPaths, context); }); const prevPaths = currentPaths.concat(previousPaths.filter(x => !x.startsWith("##current"))); TransformerParser.parse(args.to, targetPath, localPath + "[]", prevPaths, paths, context); } else { args.using.forEach((use) => { if (typeof use.as !== "string") return; // invalid const dataWithMatchType = Array.isArray(use.with) ? use.with[0] : previousPaths.includes(use.with + "[0]") ? use.with + "[0]" // this is not the best approach (other indices may have different fields) : use.with + "[]"; TransformerParser.parse(dataWithMatchType, targetPath, localPath + "[]", previousPaths, paths, context); }); const dataWithResultType = typeof value === "string" ? value + "[0]" : value?.[0]; TransformerParser.parse(dataWithResultType, targetPath, localPath + "[]", previousPaths, paths, context); } } if (context.hasPaths()) { context.setPath(targetPath + localPath, { type: "array", items: structuredClone(context.resolve(targetPath + localPath + "[]")), }); // remove all ##current and other aliases from paths context.removePaths(currentPaths, "##current"); } break; } case types_1.EmbeddedTransformerFunction.map: case types_1.EmbeddedTransformerFunction.transform: { // input is array of known size // * This is a special case because the transformer uses ##current / ##index to reference the object / iteration element const dataWithResultType = args.to ?? (typeof value === "string" ? value + "[1]" : value?.[1]); const itemsData = args.to ? value : typeof value === "string" ? value + "[0]" : value?.[0]; let targetSuffix = ""; const currentPaths = []; if (funcName === types_1.EmbeddedTransformerFunction.map) { // at any existing index of array at index 0 const dataWithCurrentType = Array.isArray(itemsData) ? itemsData[0] : previousPaths.includes(itemsData + "[0]") ? itemsData + "[0]" // this is not the best approach (other indices may have different fields) : itemsData + "[]"; TransformerParser.parse(dataWithCurrentType, "##current", "", previousPaths, currentPaths, context); targetSuffix = "[]"; } else if (funcName === types_1.EmbeddedTransformerFunction.transform) { // at index 0 TransformerParser.parse(itemsData, "##current", "", previousPaths, currentPaths, context); } const prevPaths = currentPaths.concat(previousPaths.filter(x => !x.startsWith("##current"))); TransformerParser.parse(dataWithResultType, targetPath, localPath + targetSuffix, prevPaths, paths, context); if (funcName === types_1.EmbeddedTransformerFunction.map) { if (context.hasPaths()) { context.setPath(targetPath + localPath, { type: "array", items: structuredClone(context.resolve(targetPath + localPath + "[]")), }); } } if (context.hasPaths()) { // remove all ##current from paths context.removePaths(currentPaths, "##current"); } break; } case types_1.EmbeddedTransformerFunction.switch: { if (typeof args.default !== "undefined") { TransformerParser.parse(args.default, targetPath, localPath, previousPaths, paths, context); break; } else { // try to get type from default, otherwise from the first case if (args.cases && typeof args.cases === "object") { const dataWithResultType = args.cases[Object.keys(args.cases)[0]]; if (dataWithResultType) { TransformerParser.parse(dataWithResultType, targetPath, localPath, previousPaths, paths, context); break; } } // can't detect type, put object if (context.hasPaths()) { context.setPath(targetPath + localPath, { type: "object" }); } } break; } default: { if (functionsParser_1.functionsParser.handleClientFunction?.call(null, detectedAs, funcName, func, value, args, targetPath, localPath, previousPaths, paths, context, TransformerParser.parse)) { break; } unhandled = true; } } if (unhandled && func.parsedOutputSchema) { if (func.outputSchema && context.hasPaths()) { context.setPath(targetPath + localPath, structuredClone(func.outputSchema)); } func.parsedOutputSchema.paths.forEach(p => { const key = (0, jsonpathJoin_1.jsonpathJoin)(targetPath + localPath, p.$path); paths.push(key); if (context.hasPaths()) { context.setPath(key, (0, json_schema_utils_1.cleanParsedSchemaProperty)(p)); } }); } return !unhandled; }; /** * Traverse an object and set all paths encountered to targetPath + localPath * @param definition * @param targetPath extract output paths to this prefix * @param previousPaths paths that are input to this transformation (but might not be part of the output) * @param paths result paths encountered to be in the output object of this transformation * @param typesMap map of path to schema, should contain already existing paths to extract from (will be updated if exists) * @param additionalContext additional context to resolve types from */ function parseTransformer(definition, targetPath, previousPaths, paths = [], typesMap, additionalContext) { const context = new ParseContext_1.default(typesMap, additionalContext, previousPaths); return TransformerParser.parse(definition, targetPath, "", previousPaths, paths, context); } exports.parseTransformer = parseTransformer; //# sourceMappingURL=parse.js.map