/** * Gets the schema for a datasource which is targeting a JSON array, including * nested JSON arrays. The returned schema is a squashed, table-like schema * which is fully compatible with the rest of the platform. * @param tableSchema the full schema for the table this JSON field is in * @param datasource the datasource configuration */ export const getJSONArrayDatasourceSchema = (tableSchema, datasource) => { let jsonSchema = tableSchema let keysToSchema = [] // If we are already deep inside a JSON field then we need to account // for the keys that brought us here, so we can get the schema for the // depth we're actually at if (datasource.prefixKeys) { keysToSchema = datasource.prefixKeys.concat(["schema"]) } // We parse the label of the datasource to work out where we are inside // the structure. We can use this to know which part of the schema // is available underneath our current position. keysToSchema = keysToSchema.concat(datasource.label.split(".").slice(2)) // Follow the JSON key path until we reach the schema for the level // we are at for (let i = 0; i < keysToSchema.length; i++) { jsonSchema = jsonSchema?.[keysToSchema[i]] if (jsonSchema?.schema) { jsonSchema = jsonSchema.schema } } // We need to convert the JSON schema into a more typical looking table // schema so that it works with the rest of the platform return convertJSONSchemaToTableSchema(jsonSchema, { squashObjects: true, prefixKeys: keysToSchema, }) } /** * Converts a JSON field schema (or sub-schema of a nested field) into a schema * that looks like a typical table schema. * @param jsonSchema the JSON field schema or sub-schema * @param options */ export const convertJSONSchemaToTableSchema = (jsonSchema, options) => { if (!jsonSchema) { return null } // Add default options options = { squashObjects: false, prefixKeys: null, ...options } // Immediately strip the wrapper schema for objects, or wrap shallow values in // a fake "value" schema if (jsonSchema.schema) { jsonSchema = jsonSchema.schema } else { jsonSchema = { value: jsonSchema, } } // Extract all deep keys from the schema const keys = extractJSONSchemaKeys(jsonSchema, options.squashObjects) // Form a full schema from all the deep schema keys let schema = {} keys.forEach(({ key, type }) => { schema[key] = { type, name: key, prefixKeys: options.prefixKeys } }) return schema } /** * Recursively builds paths to all leaf fields in a JSON field schema structure, * stopping when leaf nodes or arrays are reached. * @param jsonSchema the JSON field schema or sub-schema * @param squashObjects whether to recurse into objects or not */ const extractJSONSchemaKeys = (jsonSchema, squashObjects = false) => { if (!jsonSchema || !Object.keys(jsonSchema).length) { return [] } // Iterate through every schema key let keys = [] Object.keys(jsonSchema).forEach(key => { const type = jsonSchema[key].type // If we encounter an object, then only go deeper if we want to squash // object paths if (type === "json" && squashObjects) { // Find all keys within this objects schema const childKeys = extractJSONSchemaKeys( jsonSchema[key].schema, squashObjects ) // Append child paths onto the current path to build the full path keys = keys.concat( childKeys.map(childKey => ({ key: `${key}.${childKey.key}`, type: childKey.type, })) ) } // Otherwise add this as a lead node. // We transform array types from "array" into "jsonarray" here to avoid // confusion with the existing "array" type that represents a multi-select. else { keys.push({ key, type: type === "array" ? "jsonarray" : type, }) } }) return keys }