@fibery/ai-utils
Version:
Utilities for Fibery AI
508 lines • 20.5 kB
JavaScript
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