2024-02-19 10:13:03 +01:00
|
|
|
import { utils } from "@budibase/shared-core"
|
|
|
|
|
2021-12-07 22:19:14 +01:00
|
|
|
/**
|
|
|
|
* 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) => {
|
2021-12-06 12:41:17 +01:00
|
|
|
if (!jsonSchema) {
|
|
|
|
return null
|
|
|
|
}
|
2021-12-07 22:19:14 +01:00
|
|
|
|
|
|
|
// 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
|
2021-12-06 12:41:17 +01:00
|
|
|
if (jsonSchema.schema) {
|
|
|
|
jsonSchema = jsonSchema.schema
|
2021-12-06 19:12:27 +01:00
|
|
|
} else {
|
|
|
|
jsonSchema = {
|
|
|
|
value: jsonSchema,
|
|
|
|
}
|
2021-12-06 12:41:17 +01:00
|
|
|
}
|
2021-12-07 22:19:14 +01:00
|
|
|
|
|
|
|
// Extract all deep keys from the schema
|
|
|
|
const keys = extractJSONSchemaKeys(jsonSchema, options.squashObjects)
|
|
|
|
|
|
|
|
// Form a full schema from all the deep schema keys
|
2021-12-06 12:41:17 +01:00
|
|
|
let schema = {}
|
|
|
|
keys.forEach(({ key, type }) => {
|
2021-12-07 22:19:14 +01:00
|
|
|
schema[key] = { type, name: key, prefixKeys: options.prefixKeys }
|
2021-12-06 12:41:17 +01:00
|
|
|
})
|
|
|
|
return schema
|
|
|
|
}
|
|
|
|
|
2021-12-07 22:19:14 +01:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2021-12-06 12:41:17 +01:00
|
|
|
const extractJSONSchemaKeys = (jsonSchema, squashObjects = false) => {
|
|
|
|
if (!jsonSchema || !Object.keys(jsonSchema).length) {
|
|
|
|
return []
|
|
|
|
}
|
2021-12-07 22:19:14 +01:00
|
|
|
|
|
|
|
// Iterate through every schema key
|
2021-12-06 12:41:17 +01:00
|
|
|
let keys = []
|
|
|
|
Object.keys(jsonSchema).forEach(key => {
|
|
|
|
const type = jsonSchema[key].type
|
2021-12-07 22:19:14 +01:00
|
|
|
|
|
|
|
// If we encounter an object, then only go deeper if we want to squash
|
|
|
|
// object paths
|
2021-12-06 12:41:17 +01:00
|
|
|
if (type === "json" && squashObjects) {
|
2021-12-07 22:19:14 +01:00
|
|
|
// Find all keys within this objects schema
|
2021-12-06 20:56:05 +01:00
|
|
|
const childKeys = extractJSONSchemaKeys(
|
|
|
|
jsonSchema[key].schema,
|
|
|
|
squashObjects
|
|
|
|
)
|
2021-12-07 22:19:14 +01:00
|
|
|
|
|
|
|
// Append child paths onto the current path to build the full path
|
2021-12-06 12:41:17 +01:00
|
|
|
keys = keys.concat(
|
|
|
|
childKeys.map(childKey => ({
|
|
|
|
key: `${key}.${childKey.key}`,
|
|
|
|
type: childKey.type,
|
|
|
|
}))
|
|
|
|
)
|
2021-12-07 22:19:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
})
|
2021-12-06 12:41:17 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
return keys
|
|
|
|
}
|
2024-02-19 10:13:03 +01:00
|
|
|
|
|
|
|
export const generateQueryArraySchemas = (schema, nestedSchemaFields) => {
|
|
|
|
for (let key in schema) {
|
|
|
|
if (
|
|
|
|
schema[key]?.type === "json" &&
|
|
|
|
schema[key]?.subtype === "array" &&
|
|
|
|
utils.hasSchema(nestedSchemaFields[key])
|
|
|
|
) {
|
|
|
|
schema[key] = {
|
|
|
|
schema: {
|
|
|
|
schema: Object.entries(nestedSchemaFields[key] || {}).reduce(
|
|
|
|
(acc, [nestedKey, fieldSchema]) => {
|
|
|
|
acc[nestedKey] = {
|
|
|
|
name: nestedKey,
|
|
|
|
type: fieldSchema.type,
|
|
|
|
subtype: fieldSchema.subtype,
|
|
|
|
}
|
|
|
|
return acc
|
|
|
|
},
|
|
|
|
{}
|
|
|
|
),
|
|
|
|
type: "json",
|
|
|
|
},
|
|
|
|
type: "json",
|
|
|
|
subtype: "array",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return schema
|
|
|
|
}
|