UNPKG

@fibery/ai-utils

Version:

Utilities for Fibery AI

508 lines 20.5 kB
import _ from 'lodash'; import { printFormulaExpression } from '@fibery/formulas'; import { getDataToRenderTemplate, renderToString } from './templates.js'; const getFieldName = (field) => { return `${field.title} [${field.name}]`; }; const getFormulaText = (options) => { const { type, field } = options; if (!field.isFormula) { return ''; } const formulaType = field.isLookup ? 'Formula (Lookup)' : 'Formula'; const formula = field.formula; const formulaText = printFormulaExpression({ expression: formula?.expression || {}, params: formula?.params || {}, implicitRootTypeObject: type, }); return `${formulaType}: \`${formulaText}\``; }; const processTypeObject = ({ typeObject, typeObjects, enums, }) => { const typeSchema = { color: _.toUpper(typeObject.color), fields: {}, description: !typeObject.description || _.isEmpty(typeObject.description) ? `no description` : typeObject.description, }; const typeName = typeObject.name; const externalTypes = {}; const fieldObjects = typeObject.fieldObjects; for (const fieldObject of fieldObjects) { if (fieldObject.isHidden) { continue; } if (typeObject.name === 'comments/comment' && fieldObject.name === 'comment/author') { continue; } const fieldTypeName = fieldObject.typeObject.isPrimitive ? fieldObject.typeObject.nameParts.name : fieldObject.typeObject.name; const processedField = { type: fieldTypeName }; if (fieldObject.isCollection) { processedField.type = `Collection(${fieldTypeName})`; } if (fieldObject.typeObject.name === `fibery/rank`) { processedField.type = `int`; } if (fieldObject.isReadOnly) { processedField.type += ` // read-only`; } if (fieldObject.isFormula) { processedField.type += `; ${getFormulaText({ type: typeObject, field: fieldObject })}`; } if (fieldObject.isTitle) { processedField.type += ` // UI Title`; } const fieldName = getFieldName(fieldObject); if (typeObject.isEnum && fieldObject.isTitle) { const { values } = enums[typeObject.name]; processedField.type += ` // available values: ${values.join(`, `)}`; } if (typeObject.nameParts.namespace === `workflow` && fieldObject.title === `Type`) { processedField.type += ` // available values: "Not started", "Started", "Finished"`; } if (fieldObject.typeObject.isEnum) { const { defaultValue, values } = enums[fieldObject.typeObject.name]; _.extend(processedField, { values, defaultValue }); } if (!fieldObject.typeObject.isPrimitive && !_.find(typeObjects, { name: fieldObject.typeObject.name })) { if (!externalTypes[fieldTypeName]) { externalTypes[fieldTypeName] = fieldObject.typeObject; } } if (fieldObject.isDependencyStart) { processedField.type += ' // Dependency Start'; } else if (fieldObject.isDependency) { processedField.type += ' // Dependency'; } typeSchema.fields[fieldName] = processedField; } return { typeSchema, typeName, externalTypes, enums }; }; const retrieveEnumValues = async ({ fetchEnums, typeObjects, }) => { const enumTypeObjects = _.uniqBy(typeObjects.flatMap((typeObject) => { return typeObject.fieldObjects .filter((fieldObject) => !fieldObject.isHidden && fieldObject.typeObject.isEnum) .map((fieldObject) => { return { enumTypeObject: fieldObject.typeObject, defaultValue: fieldObject.defaultValue }; }) .concat(typeObjects .filter((typeObject) => typeObject.isEnum) .map((t) => ({ enumTypeObject: t, defaultValue: null }))); }), (x) => x.enumTypeObject.id); if (enumTypeObjects.length === 0) { return {}; } const enumValues = await fetchEnums(enumTypeObjects.map((x) => x.enumTypeObject)); return Object.fromEntries(Object.entries(enumValues).map(([enumType, enumValues]) => { const enumTitles = _.sortBy(enumValues, `rank`).map((v) => v.name); const defaultValueId = enumTypeObjects.find((x) => x.enumTypeObject.name === enumType)?.defaultValue?.['fibery/id']; const defaultValueName = enumValues.find((v) => v.id === defaultValueId)?.name; return [ enumType, { defaultValue: defaultValueName, values: enumTitles, }, ]; })); }; const createTypesSchema = async ({ fetchEnums, typeObjects }) => { const typesSchema = {}; const external = {}; const enums = await retrieveEnumValues({ fetchEnums, typeObjects, }); for (const typeObject of typeObjects) { const { typeSchema, typeName, externalTypes } = processTypeObject({ typeObject, typeObjects, enums, }); typesSchema[typeName] = typeSchema; _.extend(external, externalTypes); } return { typesSchema, external, enums }; }; const createFromTypeObjectsWithExternals = async ({ fetchEnums, compact = false, typeObjects, }) => { const { typesSchema, external, enums } = await createTypesSchema({ fetchEnums, typeObjects, }); const nextLevelOfExternals = external; let allFetchedEnums = enums; if (!compact) { const externalTypeObjects = Object.values(nextLevelOfExternals); const missingExternalEnums = await retrieveEnumValues({ fetchEnums, typeObjects: externalTypeObjects.filter((x) => !Object.hasOwn(enums, x.name)), }); allFetchedEnums = { ...enums, ...missingExternalEnums }; for (const key of _.keys(external)) { if (typesSchema[key]) { continue; } const typeObject = external[key]; if (key === `fibery/field` || key === `fibery/type`) { continue; } const { typeSchema, externalTypes } = processTypeObject({ typeObject, typeObjects: externalTypeObjects, enums: allFetchedEnums, }); typesSchema[key] = typeSchema; _.extend(nextLevelOfExternals, externalTypes); } } return { typesSchema, enums: allFetchedEnums }; }; export const processSchema = async ({ fetchEnums, typeObjects, compact = false }) => { if (!fetchEnums) { throw new Error(`Please, pass fetchEnums function for dynamic schema exploration`); } const { typesSchema, enums } = await createFromTypeObjectsWithExternals({ fetchEnums, typeObjects, compact, }); return { typesSchema, enums }; }; const processSpaces = ({ schema, apps }) => { const spaces = {}; for (const app of apps) { spaces[app.name] = []; } for (const type of schema.typeObjects) { if (!type.isDomain && !_.includes([`fibery/file`, `comments/comment`], type.name)) { continue; } const spaceName = `${type.nameParts.namespace}`; if (!spaces[spaceName]) { spaces[spaceName] = []; } spaces[spaceName].push({ name: `${type.title}`, type }); } return { spaces }; }; const renderProcessedTypesToString = async ({ processedTypes, nonExistentTypes, compact = false, template = `workspace-schema`, }) => { const templateData = getDataToRenderTemplate({ processedTypes }); return await renderToString({ data: { ...templateData, compact, nonExistentTypes }, template, }); }; export const getSchemaString = async ({ fetchEnums, typeObjects, schema, nonExistentTypes = [], compact = false, }) => { const { typesSchema, enums } = await processSchema({ fetchEnums, typeObjects, compact, }); const schemaString = await renderProcessedTypesToString({ processedTypes: typesSchema, compact, nonExistentTypes, }); return { schemaString, typeObjects, schema, enums: Object.fromEntries(Object.entries(enums).map(([key, value]) => [key, value.values])) }; }; export const getWorkspaceStructureString = async ({ schema, apps }) => { const { spaces } = processSpaces({ schema, apps }); const data = _.keys(spaces).map((space) => ({ name: space, databases: spaces[space] })); const schemaString = await renderToString({ data: { spaces: data }, template: `databases-names` }); return { spaces, schemaString, schema }; }; export const getDomainTypes = ({ schema, apps }) => { const { spaces } = processSpaces({ schema, apps }); const typeObjects = []; for (const types of _.values(spaces)) { types.forEach((db) => typeObjects.push(db.type)); } return { schema, typeObjects }; }; export const getDomainTypesString = async ({ fetchEnums, schema, apps, detection, buildSchema = getSchemaString, }) => { const { typeObjects } = getDomainTypes({ schema, apps }); let detectedTypes = typeObjects; if (detection && !_.isEmpty(detection)) { detectedTypes = []; for (const db of detection) { const type = _.find(typeObjects, { name: db }); if (!type) { throw new Error(`${db} doesn't exist`); } detectedTypes.push(type); } } return await buildSchema({ fetchEnums, typeObjects: detectedTypes, schema }); }; export const getAllTypesString = async ({ fetchEnums, schema, detection, buildSchema = getSchemaString, }) => { // builds schema for given detection using ALL fibery types instead of only user-defined types let detectedTypes = schema.typeObjects; if (detection && !_.isEmpty(detection)) { detectedTypes = []; for (const db of detection) { const type = _.find(schema.typeObjects, { name: db }); if (!type) { throw new Error(`${db} doesn't exist`); } detectedTypes.push(type); } } return await buildSchema({ fetchEnums, typeObjects: detectedTypes, schema }); }; // // Independent of previous code block of code responsible for more Fibery API-like schema string // Consumes more tokens, but gives clearer understand of schema for Fibery agent // const getTypeObjectsWithRelations = ({ databases, includeRelatedDatabases, schema }) => { const typeObjects = databases.map((database) => schema.typeObjectsByName[database]).filter((to) => to); const result = []; const processedTypeNames = new Set(); const relatedTypes = new Map(); for (const typeObject of typeObjects) { if (!processedTypeNames.has(typeObject.name)) { result.push(typeObject); processedTypeNames.add(typeObject.name); } } if (!includeRelatedDatabases) { return result; } for (const typeObject of typeObjects) { for (const fieldObject of typeObject.fieldObjects) { if (fieldObject.isHidden) { continue; } if (fieldObject.typeObject.isPrimitive) { continue; } const relatedTypeName = fieldObject.typeObject.name; if (processedTypeNames.has(relatedTypeName)) { continue; } if (relatedTypeName === 'fibery/field' || relatedTypeName === 'fibery/type') { continue; } if (!relatedTypes.has(relatedTypeName)) { relatedTypes.set(relatedTypeName, fieldObject.typeObject); } } } for (const relatedType of relatedTypes.values()) { result.push(relatedType); } return result; }; const getSelectEnumCommand = ({ enumTypeObject }) => { const select = { id: `fibery/id`, name: `enum/name`, color: `enum/color`, icon: `enum/icon` }; if (enumTypeObject.name.indexOf(`workflow/`) === 0) { if (!select[`workflow/Final`]) { select.isFinal = `workflow/Final`; } if (!select[`workflow/Type`]) { select.type = `workflow/Type`; } } return { command: `fibery.entity/query`, args: { query: { 'q/from': enumTypeObject.name, 'q/select': select, 'q/where': ['q/and', ['=', ['q/null?', ['enum/name']], '$1'], ['!=', ['enum/name'], '$2']], 'q/limit': 'q/no-limit', }, params: { "$1": false, "$2": "null", } }, }; }; const fetchEnumValues = async ({ typeObjects, executeCommands, batchSize = 25, }) => { const enumTypeObjects = typeObjects.filter((t) => t.isEnum); if (enumTypeObjects.length === 0) { return {}; } const commands = enumTypeObjects.map((t) => getSelectEnumCommand({ enumTypeObject: t })); const commandsBatches = []; for (let i = 0; i < commands.length; i += batchSize) { commandsBatches.push(commands.slice(i, i + batchSize)); } const batchPromises = commandsBatches.map((batch) => executeCommands(batch)); const results = (await Promise.all(batchPromises)).flatMap((batch) => batch.map((r) => r.result)); return Object.fromEntries(enumTypeObjects.map((enumType, index) => [enumType.name, results[index] || []])); }; const allowedFieldMetaProperties = [ 'fibery/collection?', 'ui/title?', 'fibery/readonly?', 'ui/number-format', 'formula/formula?', 'formula/formula', 'fibery/dependency?', 'fibery/dependency-start?', 'fibery/default-value', ]; const processTypeObjectsToJson = ({ typeObjects, enumValues, }) => { const result = {}; for (const typeObject of typeObjects) { const processedType = { 'fibery/description': typeObject.description, 'fibery/fields': {}, }; if (typeObject.isEnum && enumValues[typeObject.name]) { const enumOptions = enumValues[typeObject.name]; processedType['fibery/values'] = enumOptions.map((option) => ({ 'fibery/id': option.id, 'enum/name': option.name, 'enum/color': option.color, 'enum/icon': option.icon, })); } for (const fieldObject of typeObject.fieldObjects) { if (fieldObject.isHidden) { continue; } const field = { 'fibery/type': fieldObject.typeObject.name, }; if (fieldObject.typeObject.isEnum && enumValues[fieldObject.typeObject.name]) { const fieldEnumValues = enumValues[fieldObject.typeObject.name]; field['fibery/values'] = fieldEnumValues.map((v) => ({ 'fibery/id': v.id, 'enum/name': v.name, })); const defaultValueId = fieldObject.defaultValue?.['fibery/id']; if (defaultValueId) { const defaultOption = fieldEnumValues.find((v) => v.id === defaultValueId); if (defaultOption) { field['fibery/default-value'] = { 'fibery/id': defaultOption.id, 'enum/name': defaultOption.name, }; } } } const meta = _.pick(fieldObject.rawMeta, allowedFieldMetaProperties); if (fieldObject.isFormula) { const formula = fieldObject.formula; meta['formula/formula'] = printFormulaExpression({ expression: formula?.expression || {}, params: formula?.params || {}, implicitRootTypeObject: typeObject, }); } if (Object.keys(meta).length > 0) { field['fibery/meta'] = meta; } processedType['fibery/fields'][fieldObject.name] = field; } result[typeObject.name] = processedType; } return result; }; const buildMetaComment = (fieldObject, typeObject, enumValues) => { const comments = []; const meta = _.pick(fieldObject.rawMeta, allowedFieldMetaProperties); if (meta['fibery/collection?']) { comments.push('collection'); } if (meta['ui/title?']) { comments.push('UI title'); } if (meta['fibery/readonly?']) { comments.push('read-only'); } if (meta['ui/number-format']) { comments.push(meta['ui/number-format']); } if (meta['fibery/dependency-start?']) { comments.push('dependency start'); } else if (meta['fibery/dependency?']) { comments.push('dependency'); } if (meta['fibery/default-value']) { const defaultValue = meta['fibery/default-value']; if (typeof defaultValue === 'object' && defaultValue !== null) { const idValue = defaultValue['fibery/id']; if (idValue) { // Try to find the enum name for this default value let displayValue = idValue; if (fieldObject.typeObject.isEnum && enumValues[fieldObject.typeObject.name]) { const enumOption = enumValues[fieldObject.typeObject.name].find((v) => v.id === idValue); if (enumOption) { displayValue = enumOption.name; } } comments.push(`default: ${displayValue}`); } } else { comments.push(`default: ${defaultValue}`); } } if (fieldObject.isFormula) { const formula = fieldObject.formula; const formulaText = printFormulaExpression({ expression: formula?.expression || {}, params: formula?.params || {}, implicitRootTypeObject: typeObject, }); comments.push(`formula: \`${formulaText}\``); } return comments.length > 0 ? ` # ${comments.join('; ')}` : ''; }; const processTypeObjectsToYAML = ({ typeObjects, enumValues, }) => { const typeLines = []; for (const typeObject of typeObjects) { const lines = []; // Type name and description const description = typeObject.description ? ` # ${typeObject.description}` : ''; lines.push(`${typeObject.name}:${description}`); lines.push(` fields:`); // Process fields for (const fieldObject of typeObject.fieldObjects) { if (fieldObject.isHidden) { continue; } if (typeObject.name === 'comments/comment' && fieldObject.name === 'comment/author') { continue; } const fieldType = fieldObject.typeObject.name; const metaComment = buildMetaComment(fieldObject, typeObject, enumValues); lines.push(` ${fieldObject.name}: ${fieldType}${metaComment}`); } // Add enum values if this type is an enum if (typeObject.isEnum && enumValues[typeObject.name]) { const enumOptions = enumValues[typeObject.name]; lines.push(` values:`); enumOptions.forEach((v) => { lines.push(` - id: ${v.id}`); lines.push(` name: ${v.name}`); lines.push(` color: ${v.color}`); lines.push(` icon: "${v.icon}"`); }); } typeLines.push(lines.join('\n')); } return typeLines.join('\n\n'); }; export const getSchemaJSON = async ({ databases, schema, executeCommands, }) => { const typeObjects = getTypeObjectsWithRelations({ databases, includeRelatedDatabases: true, schema }); const enumValues = await fetchEnumValues({ typeObjects, executeCommands }); return processTypeObjectsToJson({ typeObjects, enumValues }); }; export const getSchemaYAML = async ({ databases, includeRelatedDatabases = true, schema, executeCommands, }) => { const typeObjects = getTypeObjectsWithRelations({ databases, includeRelatedDatabases, schema }); const enumValues = await fetchEnumValues({ typeObjects, executeCommands }); return processTypeObjectsToYAML({ typeObjects, enumValues }); }; //# sourceMappingURL=schema.js.map