datasourceMetadataUtils.ts•8.01 kB
import { z } from 'zod';
import { GraphQLResponse } from '../../sdks/tableau/apis/metadataApi.js';
import { MetadataResponse } from '../../sdks/tableau/apis/vizqlDataServiceApi.js';
export const fieldSchema = z
.object({
name: z.string(),
columnClass: z.string(),
dataType: z.string().nullable(),
defaultAggregation: z.string().nullable(),
description: z.string().nullable(),
descriptionInherited: z
.array(z.object({ attribute: z.string(), value: z.string().nullable() }).nullable())
.nullable(),
dataCategory: z.string().nullable(),
role: z.string().nullable(),
defaultFormat: z.string().nullable(),
semanticRole: z.string().nullable(),
aggregation: z.string().nullable(),
aggregationParam: z.string().nullable(),
formula: z.string().nullable(),
isAutoGenerated: z.boolean().nullable(),
hasUserReference: z.boolean().nullable(),
binSize: z.number().nullable(),
})
.partial();
export const parameterSchema = z
.object({
name: z.string(),
parameterType: z.string(),
dataType: z.string().nullable(),
value: z.union([z.number(), z.string(), z.boolean(), z.null()]),
members: z.array(z.union([z.number(), z.string(), z.boolean(), z.null()])),
min: z.number().nullable(),
max: z.number().nullable(),
step: z.number().nullable(),
minDate: z.string().nullable(),
maxDate: z.string().nullable(),
periodValue: z.number().nullable(),
periodType: z.string().nullable(),
})
.partial();
export const fieldsResultSchema = z.object({
fields: z.array(fieldSchema),
parameters: z.array(parameterSchema),
});
type Parameter = z.infer<typeof parameterSchema>;
type Field = z.infer<typeof fieldSchema>;
export type FieldsResult = z.infer<typeof fieldsResultSchema>;
export function simplifyReadMetadataResult(readMetadataResult: MetadataResponse): FieldsResult {
const simplifiedResponse: FieldsResult = {
fields: [],
parameters: [],
};
if (!readMetadataResult.data) {
return simplifiedResponse;
}
// This is a simplified response that attempts to reduce tokens by
// only including essential fields and renaming properties.
for (const field of readMetadataResult.data) {
const toPush: Field = {
name: field.fieldCaption,
dataType: field.dataType,
columnClass: field.columnClass,
};
if (field.defaultAggregation) {
toPush.defaultAggregation = field.defaultAggregation;
}
if (field.formula) {
toPush.formula = field.formula;
}
simplifiedResponse.fields.push(toPush);
}
// Populate parameters from readMetadata results.
if (readMetadataResult.extraData?.parameters) {
for (const parameter of readMetadataResult.extraData.parameters) {
const toPush: Parameter = {
name: parameter.parameterCaption,
parameterType: parameter.parameterType,
dataType: parameter.dataType,
value: parameter.value,
};
if (parameter.parameterType === 'LIST' && parameter.members) {
toPush.members = parameter.members;
} else if (parameter.parameterType === 'QUANTITATIVE_DATE') {
toPush.minDate = parameter.minDate;
toPush.maxDate = parameter.maxDate;
toPush.periodValue = parameter.periodValue;
toPush.periodType = parameter.periodType;
} else if (parameter.parameterType === 'QUANTITATIVE_RANGE') {
toPush.min = parameter.min;
toPush.max = parameter.max;
toPush.step = parameter.step;
}
simplifiedResponse.parameters.push(toPush);
}
}
return simplifiedResponse;
}
export function combineFields(
readMetadataResult: MetadataResponse,
listFieldsResult: GraphQLResponse,
): FieldsResult {
// Create a response object that combines field data from
// readMetadata (VizQL Data Service API) and listFields (GraphQL Metadata API) results
// to optimize for LLM accuracy and reduce tokens in response.
const combinedFields: FieldsResult = {
fields: [],
parameters: [],
};
if (!readMetadataResult.data) {
if (listFieldsResult.data.publishedDatasources[0]?.fields.length) {
// fallback to returning listFields results if we don't have any fields from readMetadata.
for (const field of listFieldsResult.data.publishedDatasources[0].fields) {
const toPush: Field = { name: field.name };
if (field.dataType) {
toPush.dataType = field.dataType;
}
if (field.aggregation) {
toPush.defaultAggregation = field.aggregation;
}
populateFieldWithAdditionalProperties(field, toPush);
combinedFields.fields.push(toPush);
}
}
return combinedFields;
}
// Only include fields from readMetadata results in our response object since only those can be used in queries.
for (const field of readMetadataResult.data) {
// only keeping essential field properties.
const toPush: Field = {
name: field.fieldCaption,
dataType: field.dataType,
columnClass: field.columnClass,
};
if (field.defaultAggregation) {
toPush.defaultAggregation = field.defaultAggregation;
}
if (field.formula) {
toPush.formula = field.formula;
}
combinedFields.fields.push(toPush);
}
// Populate parameters from readMetadata results.
if (readMetadataResult.extraData?.parameters) {
for (const parameter of readMetadataResult.extraData.parameters) {
const toPush: Parameter = {
name: parameter.parameterCaption,
parameterType: parameter.parameterType,
dataType: parameter.dataType,
value: parameter.value,
};
if (parameter.parameterType === 'LIST' && parameter.members) {
toPush.members = parameter.members;
} else if (parameter.parameterType === 'QUANTITATIVE_DATE') {
toPush.minDate = parameter.minDate;
toPush.maxDate = parameter.maxDate;
toPush.periodValue = parameter.periodValue;
toPush.periodType = parameter.periodType;
} else if (parameter.parameterType === 'QUANTITATIVE_RANGE') {
toPush.min = parameter.min;
toPush.max = parameter.max;
toPush.step = parameter.step;
}
combinedFields.parameters.push(toPush);
}
}
if (!listFieldsResult.data.publishedDatasources[0]?.fields.length) {
return combinedFields;
}
// Of the fields in our response object, populate them with additional properties we get from listFields results.
for (const field of combinedFields.fields) {
const matchingListField = listFieldsResult.data.publishedDatasources[0].fields.find(
(f) => f.name === field.name,
);
if (matchingListField) {
populateFieldWithAdditionalProperties(matchingListField, field);
}
}
return combinedFields;
}
function populateFieldWithAdditionalProperties(sourceField: Field, targetField: Field): void {
if (sourceField.description) {
targetField.description = sourceField.description;
}
if (sourceField.descriptionInherited?.length) {
targetField.descriptionInherited = sourceField.descriptionInherited;
}
if (sourceField.dataCategory) {
targetField.dataCategory = sourceField.dataCategory;
}
if (sourceField.role) {
targetField.role = sourceField.role;
}
if (sourceField.defaultFormat) {
targetField.defaultFormat = sourceField.defaultFormat;
}
if (sourceField.formula) {
targetField.formula = sourceField.formula;
// Possibly null or undefined, only populate if true or false.
if (sourceField.isAutoGenerated != undefined) {
targetField.isAutoGenerated = sourceField.isAutoGenerated;
}
// Possibly null or undefined, only populate if true or false.
if (sourceField.hasUserReference != undefined) {
targetField.hasUserReference = sourceField.hasUserReference;
}
}
// Possibly null or undefined, only populate if defined.
if (sourceField.binSize != undefined) {
targetField.binSize = sourceField.binSize;
}
}