mirror of https://github.com/buster-so/buster.git
additional lint fixes
This commit is contained in:
parent
7e7aeb3480
commit
5d532a2957
|
@ -25,7 +25,8 @@ export type GenerateAssetMessagesInput = z.infer<typeof GenerateAssetMessagesInp
|
||||||
interface AssetDetails {
|
interface AssetDetails {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
content?: any;
|
//TODO: Dallin let's make a type for this. It should not just be a jsonb object.
|
||||||
|
content?: unknown;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -94,155 +94,6 @@ export async function getAllRawLlmMessagesForChat(chatId: string) {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Efficiently update the responseMessages JSONB field for a specific message
|
|
||||||
* Optimized for frequent streaming updates - replaces entire JSONB content
|
|
||||||
* @param messageId - The ID of the message to update
|
|
||||||
* @param responseMessages - The new response messages content (will completely replace existing)
|
|
||||||
* @returns Success status
|
|
||||||
*/
|
|
||||||
export async function updateMessageResponseMessages(
|
|
||||||
messageId: string,
|
|
||||||
responseMessages: any
|
|
||||||
): Promise<{ success: boolean }> {
|
|
||||||
try {
|
|
||||||
// First verify the message exists and is not deleted
|
|
||||||
const existingMessage = await db
|
|
||||||
.select({ id: messages.id })
|
|
||||||
.from(messages)
|
|
||||||
.where(and(eq(messages.id, messageId), isNull(messages.deletedAt)))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingMessage.length === 0) {
|
|
||||||
throw new Error(`Message not found or has been deleted: ${messageId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(messages)
|
|
||||||
.set({
|
|
||||||
responseMessages,
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
.where(and(eq(messages.id, messageId), isNull(messages.deletedAt)));
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to update message responseMessages:', error);
|
|
||||||
// Re-throw our specific validation errors
|
|
||||||
if (error instanceof Error && error.message.includes('Message not found')) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to update response messages for message ${messageId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Efficiently update the reasoning JSONB field for a specific message
|
|
||||||
* Optimized for frequent streaming updates - replaces entire JSONB content
|
|
||||||
* Note: reasoning field has NOT NULL constraint, so null values are not allowed
|
|
||||||
* @param messageId - The ID of the message to update
|
|
||||||
* @param reasoning - The new reasoning content (will completely replace existing)
|
|
||||||
* @returns Success status
|
|
||||||
*/
|
|
||||||
export async function updateMessageReasoning(
|
|
||||||
messageId: string,
|
|
||||||
reasoning: any
|
|
||||||
): Promise<{ success: boolean }> {
|
|
||||||
try {
|
|
||||||
// First verify the message exists and is not deleted
|
|
||||||
const existingMessage = await db
|
|
||||||
.select({ id: messages.id })
|
|
||||||
.from(messages)
|
|
||||||
.where(and(eq(messages.id, messageId), isNull(messages.deletedAt)))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingMessage.length === 0) {
|
|
||||||
throw new Error(`Message not found or has been deleted: ${messageId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate reasoning is not null (database constraint)
|
|
||||||
if (reasoning === null || reasoning === undefined) {
|
|
||||||
throw new Error('Reasoning cannot be null - database constraint violation');
|
|
||||||
}
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(messages)
|
|
||||||
.set({
|
|
||||||
reasoning,
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
.where(and(eq(messages.id, messageId), isNull(messages.deletedAt)));
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to update message reasoning:', error);
|
|
||||||
// Re-throw our specific validation errors
|
|
||||||
if (
|
|
||||||
error instanceof Error &&
|
|
||||||
(error.message.includes('Message not found') ||
|
|
||||||
error.message.includes('Reasoning cannot be null'))
|
|
||||||
) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to update reasoning for message ${messageId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Efficiently update both responseMessages and reasoning JSONB fields in a single query
|
|
||||||
* Most efficient option when both fields need updating during streaming
|
|
||||||
* Note: reasoning field has NOT NULL constraint, so null values are not allowed
|
|
||||||
* @param messageId - The ID of the message to update
|
|
||||||
* @param responseMessages - The new response messages content
|
|
||||||
* @param reasoning - The new reasoning content (cannot be null)
|
|
||||||
* @returns Success status
|
|
||||||
*/
|
|
||||||
export async function updateMessageStreamingFields(
|
|
||||||
messageId: string,
|
|
||||||
responseMessages: any,
|
|
||||||
reasoning: any
|
|
||||||
): Promise<{ success: boolean }> {
|
|
||||||
try {
|
|
||||||
// First verify the message exists and is not deleted
|
|
||||||
const existingMessage = await db
|
|
||||||
.select({ id: messages.id })
|
|
||||||
.from(messages)
|
|
||||||
.where(and(eq(messages.id, messageId), isNull(messages.deletedAt)))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingMessage.length === 0) {
|
|
||||||
throw new Error(`Message not found or has been deleted: ${messageId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate reasoning is not null (database constraint)
|
|
||||||
if (reasoning === null || reasoning === undefined) {
|
|
||||||
throw new Error('Reasoning cannot be null - database constraint violation');
|
|
||||||
}
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(messages)
|
|
||||||
.set({
|
|
||||||
responseMessages,
|
|
||||||
reasoning,
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
.where(and(eq(messages.id, messageId), isNull(messages.deletedAt)));
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to update message streaming fields:', error);
|
|
||||||
// Re-throw our specific validation errors
|
|
||||||
if (
|
|
||||||
error instanceof Error &&
|
|
||||||
(error.message.includes('Message not found') ||
|
|
||||||
error.message.includes('Reasoning cannot be null'))
|
|
||||||
) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to update streaming fields for message ${messageId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flexibly update message fields - only updates fields that are provided
|
* Flexibly update message fields - only updates fields that are provided
|
||||||
* Allows updating responseMessages, reasoning, and/or rawLlmMessages in a single query
|
* Allows updating responseMessages, reasoning, and/or rawLlmMessages in a single query
|
||||||
|
@ -254,9 +105,10 @@ export async function updateMessageStreamingFields(
|
||||||
export async function updateMessageFields(
|
export async function updateMessageFields(
|
||||||
messageId: string,
|
messageId: string,
|
||||||
fields: {
|
fields: {
|
||||||
responseMessages?: any;
|
//TODO: Dallin let's make a type for this. It should not just be a jsonb object.
|
||||||
reasoning?: any;
|
responseMessages?: unknown;
|
||||||
rawLlmMessages?: any;
|
reasoning?: unknown;
|
||||||
|
rawLlmMessages?: unknown;
|
||||||
finalReasoningMessage?: string;
|
finalReasoningMessage?: string;
|
||||||
}
|
}
|
||||||
): Promise<{ success: boolean }> {
|
): Promise<{ success: boolean }> {
|
||||||
|
@ -267,7 +119,13 @@ export async function updateMessageFields(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build update object with only provided fields
|
// Build update object with only provided fields
|
||||||
const updateData: any = {
|
const updateData: {
|
||||||
|
updatedAt: string;
|
||||||
|
responseMessages?: unknown;
|
||||||
|
reasoning?: unknown;
|
||||||
|
rawLlmMessages?: unknown;
|
||||||
|
finalReasoningMessage?: string;
|
||||||
|
} = {
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -335,18 +193,16 @@ export async function updateMessage(
|
||||||
throw new Error('Reasoning cannot be null - database constraint violation');
|
throw new Error('Reasoning cannot be null - database constraint violation');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove undefined fields and build update object
|
const updateData = {
|
||||||
const updateData: any = {
|
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(fields).filter(
|
||||||
|
([key, value]) =>
|
||||||
|
value !== undefined && key !== 'id' && key !== 'createdAt' && key !== 'deletedAt'
|
||||||
|
)
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only add fields that are actually provided (not undefined)
|
|
||||||
for (const [key, value] of Object.entries(fields)) {
|
|
||||||
if (value !== undefined && key !== 'id' && key !== 'createdAt' && key !== 'deletedAt') {
|
|
||||||
updateData[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If updatedAt was explicitly provided, use that instead
|
// If updatedAt was explicitly provided, use that instead
|
||||||
if ('updatedAt' in fields && fields.updatedAt !== undefined) {
|
if ('updatedAt' in fields && fields.updatedAt !== undefined) {
|
||||||
updateData.updatedAt = fields.updatedAt;
|
updateData.updatedAt = fields.updatedAt;
|
||||||
|
|
|
@ -9,13 +9,13 @@ export const BarAndLineAxisSchema = z
|
||||||
// the column ids to use for the category axis. If multiple column ids are provided, they will be grouped together. THE LLM SHOULD NEVER SET MULTIPLE CATEGORY COLUMNS. ONLY THE USER CAN SET THIS.
|
// the column ids to use for the category axis. If multiple column ids are provided, they will be grouped together. THE LLM SHOULD NEVER SET MULTIPLE CATEGORY COLUMNS. ONLY THE USER CAN SET THIS.
|
||||||
category: z.array(z.string()).default([]),
|
category: z.array(z.string()).default([]),
|
||||||
// if null the y axis will automatically be used, the y axis will be used for the tooltip.
|
// if null the y axis will automatically be used, the y axis will be used for the tooltip.
|
||||||
tooltip: z.nullable(z.array(z.string())).default(null).optional()
|
tooltip: z.nullable(z.array(z.string())).default(null).optional(),
|
||||||
})
|
})
|
||||||
.default({
|
.default({
|
||||||
x: [],
|
x: [],
|
||||||
y: [],
|
y: [],
|
||||||
category: [],
|
category: [],
|
||||||
tooltip: null
|
tooltip: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ScatterAxisSchema = z
|
export const ScatterAxisSchema = z
|
||||||
|
@ -29,14 +29,14 @@ export const ScatterAxisSchema = z
|
||||||
// the column id to use for the size range of the dots. ONLY one column id should be provided.
|
// the column id to use for the size range of the dots. ONLY one column id should be provided.
|
||||||
size: z.tuple([z.string()]).or(z.array(z.string()).length(0)).default([]),
|
size: z.tuple([z.string()]).or(z.array(z.string()).length(0)).default([]),
|
||||||
// if null the y axis will automatically be used, the y axis will be used for the tooltip.
|
// if null the y axis will automatically be used, the y axis will be used for the tooltip.
|
||||||
tooltip: z.nullable(z.array(z.string())).default(null)
|
tooltip: z.nullable(z.array(z.string())).default(null),
|
||||||
})
|
})
|
||||||
.default({
|
.default({
|
||||||
x: [],
|
x: [],
|
||||||
y: [],
|
y: [],
|
||||||
size: [],
|
size: [],
|
||||||
category: [],
|
category: [],
|
||||||
tooltip: null
|
tooltip: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ComboChartAxisSchema = z
|
export const ComboChartAxisSchema = z
|
||||||
|
@ -50,14 +50,14 @@ export const ComboChartAxisSchema = z
|
||||||
// the column ids to use for the category axis. If multiple column ids are provided, they will be grouped together. THE LLM SHOULD NEVER SET MULTIPLE CATEGORY COLUMNS. ONLY THE USER CAN SET THIS.
|
// the column ids to use for the category axis. If multiple column ids are provided, they will be grouped together. THE LLM SHOULD NEVER SET MULTIPLE CATEGORY COLUMNS. ONLY THE USER CAN SET THIS.
|
||||||
category: z.array(z.string()).default([]),
|
category: z.array(z.string()).default([]),
|
||||||
// if null the y axis will automatically be used, the y axis will be used for the tooltip.
|
// if null the y axis will automatically be used, the y axis will be used for the tooltip.
|
||||||
tooltip: z.nullable(z.array(z.string())).default(null).optional()
|
tooltip: z.nullable(z.array(z.string())).default(null).optional(),
|
||||||
})
|
})
|
||||||
.default({
|
.default({
|
||||||
x: [],
|
x: [],
|
||||||
y: [],
|
y: [],
|
||||||
y2: [],
|
y2: [],
|
||||||
category: [],
|
category: [],
|
||||||
tooltip: null
|
tooltip: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PieChartAxisSchema = z
|
export const PieChartAxisSchema = z
|
||||||
|
@ -67,19 +67,19 @@ export const PieChartAxisSchema = z
|
||||||
// the column ids to use for the y axis. If multiple column ids are provided, they will appear as rings. The LLM should NEVER set multiple y axis columns. Only the user can set this.
|
// the column ids to use for the y axis. If multiple column ids are provided, they will appear as rings. The LLM should NEVER set multiple y axis columns. Only the user can set this.
|
||||||
y: z.array(z.string()).default([]),
|
y: z.array(z.string()).default([]),
|
||||||
// if null the y axis will automatically be used, the y axis will be used for the tooltip.
|
// if null the y axis will automatically be used, the y axis will be used for the tooltip.
|
||||||
tooltip: z.nullable(z.array(z.string())).default(null)
|
tooltip: z.nullable(z.array(z.string())).default(null),
|
||||||
})
|
})
|
||||||
.default({
|
.default({
|
||||||
x: [],
|
x: [],
|
||||||
y: [],
|
y: [],
|
||||||
tooltip: null
|
tooltip: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ChartEncodesSchema = z.union([
|
export const ChartEncodesSchema = z.union([
|
||||||
BarAndLineAxisSchema,
|
BarAndLineAxisSchema,
|
||||||
ScatterAxisSchema,
|
ScatterAxisSchema,
|
||||||
PieChartAxisSchema,
|
PieChartAxisSchema,
|
||||||
ComboChartAxisSchema
|
ComboChartAxisSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Export inferred types
|
// Export inferred types
|
||||||
|
|
|
@ -12,7 +12,7 @@ export const BarChartPropsSchema = z.object({
|
||||||
// OPTIONAL: default is group. This will only apply if the columnVisualization is set to 'bar'.
|
// OPTIONAL: default is group. This will only apply if the columnVisualization is set to 'bar'.
|
||||||
barGroupType: z.nullable(z.enum(['stack', 'group', 'percentage-stack'])).default('group'),
|
barGroupType: z.nullable(z.enum(['stack', 'group', 'percentage-stack'])).default('group'),
|
||||||
// OPTIONAL: default is false. This will only apply if is is stacked and there is either a category or multiple y axis applie to the series.
|
// OPTIONAL: default is false. This will only apply if is is stacked and there is either a category or multiple y axis applie to the series.
|
||||||
barShowTotalAtTop: z.boolean().default(false)
|
barShowTotalAtTop: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type BarChartProps = z.infer<typeof BarChartPropsSchema>;
|
export type BarChartProps = z.infer<typeof BarChartPropsSchema>;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from 'vitest';
|
||||||
import { ChartConfigPropsSchema } from "./chartConfigProps";
|
import { ChartConfigPropsSchema } from './chartConfigProps';
|
||||||
import { DEFAULT_CHART_THEME } from "./configColors";
|
import { DEFAULT_CHART_THEME } from './configColors';
|
||||||
|
|
||||||
describe("DEFAULT_CHART_CONFIG", () => {
|
describe('DEFAULT_CHART_CONFIG', () => {
|
||||||
it("should conform to BusterChartConfigPropsSchema and have expected default values", () => {
|
it('should conform to BusterChartConfigPropsSchema and have expected default values', () => {
|
||||||
// Verify that DEFAULT_CHART_CONFIG is valid according to the schema
|
// Verify that DEFAULT_CHART_CONFIG is valid according to the schema
|
||||||
const parseResult = ChartConfigPropsSchema.safeParse({});
|
const parseResult = ChartConfigPropsSchema.safeParse({});
|
||||||
expect(parseResult.success).toBe(true);
|
expect(parseResult.success).toBe(true);
|
||||||
|
@ -21,9 +21,9 @@ describe("DEFAULT_CHART_CONFIG", () => {
|
||||||
expect(config.columnSettings).toEqual({});
|
expect(config.columnSettings).toEqual({});
|
||||||
expect(config.columnLabelFormats).toEqual({});
|
expect(config.columnLabelFormats).toEqual({});
|
||||||
expect(config.showLegend).toBeNull();
|
expect(config.showLegend).toBeNull();
|
||||||
expect(config.barLayout).toBe("vertical");
|
expect(config.barLayout).toBe('vertical');
|
||||||
expect(config.barSortBy).toEqual([]);
|
expect(config.barSortBy).toEqual([]);
|
||||||
expect(config.barGroupType).toBe("group");
|
expect(config.barGroupType).toBe('group');
|
||||||
expect(config.barShowTotalAtTop).toBe(false);
|
expect(config.barShowTotalAtTop).toBe(false);
|
||||||
expect(config.lineGroupType).toBeNull();
|
expect(config.lineGroupType).toBeNull();
|
||||||
expect(config.scatterAxis).toEqual({
|
expect(config.scatterAxis).toEqual({
|
||||||
|
@ -40,7 +40,7 @@ describe("DEFAULT_CHART_CONFIG", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verify the config is a complete object with all required properties
|
// Verify the config is a complete object with all required properties
|
||||||
expect(typeof config).toBe("object");
|
expect(typeof config).toBe('object');
|
||||||
expect(config).not.toBeNull();
|
expect(config).not.toBeNull();
|
||||||
|
|
||||||
// Verify it has a selectedChartType (required field)
|
// Verify it has a selectedChartType (required field)
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { ComboChartAxisSchema } from './axisInterfaces';
|
||||||
|
|
||||||
export const ComboChartPropsSchema = z.object({
|
export const ComboChartPropsSchema = z.object({
|
||||||
// Required for Combo
|
// Required for Combo
|
||||||
comboChartAxis: ComboChartAxisSchema
|
comboChartAxis: ComboChartAxisSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ComboChartProps = z.infer<typeof ComboChartPropsSchema>;
|
export type ComboChartProps = z.infer<typeof ComboChartPropsSchema>;
|
||||||
|
|
|
@ -8,5 +8,5 @@ export const DEFAULT_CHART_THEME = [
|
||||||
'#F3864F',
|
'#F3864F',
|
||||||
'#C82184',
|
'#C82184',
|
||||||
'#31FCB4',
|
'#31FCB4',
|
||||||
'#E83562'
|
'#E83562',
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { LineChartPropsSchema, type LineChartProps } from './lineChartProps';
|
import { type LineChartProps, LineChartPropsSchema } from './lineChartProps';
|
||||||
|
|
||||||
describe('LineChartPropsSchema', () => {
|
describe('LineChartPropsSchema', () => {
|
||||||
describe('valid inputs', () => {
|
describe('valid inputs', () => {
|
||||||
|
@ -81,17 +81,17 @@ describe('LineChartPropsSchema', () => {
|
||||||
describe('type inference', () => {
|
describe('type inference', () => {
|
||||||
it('should correctly infer LineChartProps type', () => {
|
it('should correctly infer LineChartProps type', () => {
|
||||||
const validData: LineChartProps = {
|
const validData: LineChartProps = {
|
||||||
lineGroupType: 'stack'
|
lineGroupType: 'stack',
|
||||||
};
|
};
|
||||||
expect(LineChartPropsSchema.parse(validData)).toEqual(validData);
|
expect(LineChartPropsSchema.parse(validData)).toEqual(validData);
|
||||||
|
|
||||||
const validDataWithNull: LineChartProps = {
|
const validDataWithNull: LineChartProps = {
|
||||||
lineGroupType: null
|
lineGroupType: null,
|
||||||
};
|
};
|
||||||
expect(LineChartPropsSchema.parse(validDataWithNull)).toEqual(validDataWithNull);
|
expect(LineChartPropsSchema.parse(validDataWithNull)).toEqual(validDataWithNull);
|
||||||
|
|
||||||
const validDataWithPercentage: LineChartProps = {
|
const validDataWithPercentage: LineChartProps = {
|
||||||
lineGroupType: 'percentage-stack'
|
lineGroupType: 'percentage-stack',
|
||||||
};
|
};
|
||||||
expect(LineChartPropsSchema.parse(validDataWithPercentage)).toEqual(validDataWithPercentage);
|
expect(LineChartPropsSchema.parse(validDataWithPercentage)).toEqual(validDataWithPercentage);
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { z } from 'zod/v4';
|
||||||
|
|
||||||
export const LineChartPropsSchema = z.object({
|
export const LineChartPropsSchema = z.object({
|
||||||
// OPTIONAL: default is null. This will only apply if the columnVisualization is set to 'line'. If this is set to stack it will stack the lines on top of each other. The UI has this labeled as "Show as %"
|
// OPTIONAL: default is null. This will only apply if the columnVisualization is set to 'line'. If this is set to stack it will stack the lines on top of each other. The UI has this labeled as "Show as %"
|
||||||
lineGroupType: z.enum(['stack', 'percentage-stack']).nullable().default(null)
|
lineGroupType: z.enum(['stack', 'percentage-stack']).nullable().default(null),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type LineChartProps = z.infer<typeof LineChartPropsSchema>;
|
export type LineChartProps = z.infer<typeof LineChartPropsSchema>;
|
||||||
|
|
|
@ -6,7 +6,7 @@ export const DerivedMetricTitleSchema = z.object({
|
||||||
// whether to display to use the key or the value in the chart
|
// whether to display to use the key or the value in the chart
|
||||||
useValue: z.boolean(),
|
useValue: z.boolean(),
|
||||||
// OPTIONAL: default is sum
|
// OPTIONAL: default is sum
|
||||||
aggregate: z.enum(['sum', 'average', 'median', 'max', 'min', 'count', 'first']).default('sum')
|
aggregate: z.enum(['sum', 'average', 'median', 'max', 'min', 'count', 'first']).default('sum'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const MetricChartPropsSchema = z.object({
|
export const MetricChartPropsSchema = z.object({
|
||||||
|
@ -21,7 +21,7 @@ export const MetricChartPropsSchema = z.object({
|
||||||
// OPTIONAL: default is ''
|
// OPTIONAL: default is ''
|
||||||
metricSubHeader: z.nullable(z.union([z.string(), DerivedMetricTitleSchema])).default(null),
|
metricSubHeader: z.nullable(z.union([z.string(), DerivedMetricTitleSchema])).default(null),
|
||||||
// OPTIONAL: default is null. If null then the metricColumnId will be used in conjunction with the metricValueAggregate. If not null, then the metricValueLabel will be used.
|
// OPTIONAL: default is null. If null then the metricColumnId will be used in conjunction with the metricValueAggregate. If not null, then the metricValueLabel will be used.
|
||||||
metricValueLabel: z.nullable(z.string()).default(null)
|
metricValueLabel: z.nullable(z.string()).default(null),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type DerivedMetricTitle = z.infer<typeof DerivedMetricTitleSchema>;
|
export type DerivedMetricTitle = z.infer<typeof DerivedMetricTitleSchema>;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { z } from "zod/v4";
|
import { z } from 'zod/v4';
|
||||||
import { PieChartAxisSchema } from "./axisInterfaces";
|
import { PieChartAxisSchema } from './axisInterfaces';
|
||||||
import { PieSortBySchema } from "./etcInterfaces";
|
import { PieSortBySchema } from './etcInterfaces';
|
||||||
|
|
||||||
export const PieChartPropsSchema = z.object({
|
export const PieChartPropsSchema = z.object({
|
||||||
// OPTIONAL: default: value
|
// OPTIONAL: default: value
|
||||||
|
@ -8,30 +8,28 @@ export const PieChartPropsSchema = z.object({
|
||||||
// Required for Pie
|
// Required for Pie
|
||||||
pieChartAxis: PieChartAxisSchema,
|
pieChartAxis: PieChartAxisSchema,
|
||||||
// OPTIONAL: default: number
|
// OPTIONAL: default: number
|
||||||
pieDisplayLabelAs: z.enum(["percent", "number"]).default("number"),
|
pieDisplayLabelAs: z.enum(['percent', 'number']).default('number'),
|
||||||
// OPTIONAL: default true if donut width is set. If the data contains a percentage, set this as false.
|
// OPTIONAL: default true if donut width is set. If the data contains a percentage, set this as false.
|
||||||
pieShowInnerLabel: z.boolean().default(true),
|
pieShowInnerLabel: z.boolean().default(true),
|
||||||
// OPTIONAL: default: sum
|
// OPTIONAL: default: sum
|
||||||
pieInnerLabelAggregate: z
|
pieInnerLabelAggregate: z
|
||||||
.enum(["sum", "average", "median", "max", "min", "count"])
|
.enum(['sum', 'average', 'median', 'max', 'min', 'count'])
|
||||||
.default("sum"),
|
.default('sum'),
|
||||||
// OPTIONAL: default is null and will be the name of the pieInnerLabelAggregate
|
// OPTIONAL: default is null and will be the name of the pieInnerLabelAggregate
|
||||||
pieInnerLabelTitle: z.string().nullable().default(null),
|
pieInnerLabelTitle: z.string().nullable().default(null),
|
||||||
// OPTIONAL: default: none
|
// OPTIONAL: default: none
|
||||||
pieLabelPosition: z
|
pieLabelPosition: z.nullable(z.enum(['inside', 'outside', 'none'])).default('none'),
|
||||||
.nullable(z.enum(["inside", "outside", "none"]))
|
|
||||||
.default("none"),
|
|
||||||
// OPTIONAL: default: 55 | range 0-65 | range represents percent size of the donut hole. If user asks for a pie this should be 0
|
// OPTIONAL: default: 55 | range 0-65 | range represents percent size of the donut hole. If user asks for a pie this should be 0
|
||||||
pieDonutWidth: z
|
pieDonutWidth: z
|
||||||
.number()
|
.number()
|
||||||
.min(0, "Donut width must be at least 0")
|
.min(0, 'Donut width must be at least 0')
|
||||||
.max(65, "Donut width must be at most 65")
|
.max(65, 'Donut width must be at most 65')
|
||||||
.default(40),
|
.default(40),
|
||||||
// OPTIONAL: default: 2.5 | range 0-100 | If there are items that are less than this percentage of the pie, they combine to form a single slice.
|
// OPTIONAL: default: 2.5 | range 0-100 | If there are items that are less than this percentage of the pie, they combine to form a single slice.
|
||||||
pieMinimumSlicePercentage: z
|
pieMinimumSlicePercentage: z
|
||||||
.number()
|
.number()
|
||||||
.min(0, "Minimum slice percentage must be at least 0")
|
.min(0, 'Minimum slice percentage must be at least 0')
|
||||||
.max(100, "Minimum slice percentage must be at most 100")
|
.max(100, 'Minimum slice percentage must be at most 100')
|
||||||
.default(0),
|
.default(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { ScatterAxisSchema } from './axisInterfaces';
|
||||||
export const ScatterChartPropsSchema = z.object({
|
export const ScatterChartPropsSchema = z.object({
|
||||||
// Required for Scatter
|
// Required for Scatter
|
||||||
scatterAxis: ScatterAxisSchema,
|
scatterAxis: ScatterAxisSchema,
|
||||||
scatterDotSize: z.tuple([z.number(), z.number()]).default([3, 15])
|
scatterDotSize: z.tuple([z.number(), z.number()]).default([3, 15]),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ScatterChartProps = z.infer<typeof ScatterChartPropsSchema>;
|
export type ScatterChartProps = z.infer<typeof ScatterChartPropsSchema>;
|
||||||
|
|
|
@ -5,7 +5,7 @@ export const TableChartPropsSchema = z.object({
|
||||||
tableColumnWidths: z.nullable(z.record(z.string(), z.number())).default(null),
|
tableColumnWidths: z.nullable(z.record(z.string(), z.number())).default(null),
|
||||||
tableHeaderBackgroundColor: z.nullable(z.string()).default(null),
|
tableHeaderBackgroundColor: z.nullable(z.string()).default(null),
|
||||||
tableHeaderFontColor: z.nullable(z.string()).default(null),
|
tableHeaderFontColor: z.nullable(z.string()).default(null),
|
||||||
tableColumnFontColor: z.nullable(z.string()).default(null)
|
tableColumnFontColor: z.nullable(z.string()).default(null),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TableChartProps = z.infer<typeof TableChartPropsSchema>;
|
export type TableChartProps = z.infer<typeof TableChartPropsSchema>;
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import {
|
import {
|
||||||
getLatestMessageForChat,
|
getLatestMessageForChat,
|
||||||
updateMessageReasoning,
|
updateMessageReasoning,
|
||||||
updateMessageResponseMessages,
|
|
||||||
updateMessageStreamingFields,
|
updateMessageStreamingFields,
|
||||||
} from '@buster/database/src/helpers/messages';
|
} from '@buster/database';
|
||||||
import { createTestMessageWithContext } from '@buster/test-utils';
|
import { createTestMessageWithContext } from '@buster/test-utils';
|
||||||
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
||||||
import { cleanupTestEnvironment, setupTestEnvironment } from './helpers';
|
import { cleanupTestEnvironment, setupTestEnvironment } from '../../src/envHelpers/env-helpers';
|
||||||
|
|
||||||
describe('Message Update Helpers', () => {
|
describe('Message Update Helpers', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
@ -17,84 +16,6 @@ describe('Message Update Helpers', () => {
|
||||||
await cleanupTestEnvironment();
|
await cleanupTestEnvironment();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('updateMessageResponseMessages', () => {
|
|
||||||
test('successfully updates responseMessages JSONB field', async () => {
|
|
||||||
const { messageId, chatId } = await createTestMessageWithContext();
|
|
||||||
|
|
||||||
const newResponseMessages = {
|
|
||||||
content: 'Updated response content',
|
|
||||||
metadata: { tokens: 150, model: 'gpt-4' },
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await updateMessageResponseMessages(messageId, newResponseMessages);
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
|
|
||||||
// Verify the update was persisted
|
|
||||||
const updatedMessage = await getLatestMessageForChat(chatId);
|
|
||||||
expect(updatedMessage?.responseMessages).toEqual(newResponseMessages);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('handles empty responseMessages object', async () => {
|
|
||||||
const { messageId, chatId } = await createTestMessageWithContext();
|
|
||||||
|
|
||||||
const emptyResponse = {};
|
|
||||||
|
|
||||||
const result = await updateMessageResponseMessages(messageId, emptyResponse);
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
|
|
||||||
const updatedMessage = await getLatestMessageForChat(chatId);
|
|
||||||
expect(updatedMessage?.responseMessages).toEqual(emptyResponse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('handles complex nested JSONB structure', async () => {
|
|
||||||
const { messageId, chatId } = await createTestMessageWithContext();
|
|
||||||
|
|
||||||
const complexResponse = {
|
|
||||||
messages: [
|
|
||||||
{ role: 'assistant', content: 'Hello' },
|
|
||||||
{ role: 'user', content: 'Hi there' },
|
|
||||||
],
|
|
||||||
metadata: {
|
|
||||||
tokens: 250,
|
|
||||||
reasoning: {
|
|
||||||
steps: ['analyze', 'respond'],
|
|
||||||
confidence: 0.95,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
charts: {
|
|
||||||
type: 'bar',
|
|
||||||
data: [1, 2, 3, 4],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await updateMessageResponseMessages(messageId, complexResponse);
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
|
|
||||||
const updatedMessage = await getLatestMessageForChat(chatId);
|
|
||||||
expect(updatedMessage?.responseMessages).toEqual(complexResponse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('throws error for non-existent message ID', async () => {
|
|
||||||
const nonExistentId = '00000000-0000-0000-0000-000000000000';
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
updateMessageResponseMessages(nonExistentId, { content: 'test' })
|
|
||||||
).rejects.toThrow(`Message not found or has been deleted: ${nonExistentId}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('throws error for invalid UUID format', async () => {
|
|
||||||
const invalidId = 'invalid-uuid';
|
|
||||||
|
|
||||||
await expect(updateMessageResponseMessages(invalidId, { content: 'test' })).rejects.toThrow(
|
|
||||||
'Failed to update response messages for message invalid-uuid'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateMessageReasoning', () => {
|
describe('updateMessageReasoning', () => {
|
||||||
test('successfully updates reasoning JSONB field', async () => {
|
test('successfully updates reasoning JSONB field', async () => {
|
||||||
const { messageId, chatId } = await createTestMessageWithContext();
|
const { messageId, chatId } = await createTestMessageWithContext();
|
||||||
|
@ -266,43 +187,4 @@ describe('Message Update Helpers', () => {
|
||||||
).rejects.toThrow(`Message not found or has been deleted: ${nonExistentId}`);
|
).rejects.toThrow(`Message not found or has been deleted: ${nonExistentId}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Performance and Concurrency', () => {
|
|
||||||
test('handles rapid sequential updates without data corruption', async () => {
|
|
||||||
const { messageId, chatId } = await createTestMessageWithContext();
|
|
||||||
|
|
||||||
// Simulate streaming updates sequentially to avoid race conditions
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
const result = await updateMessageStreamingFields(
|
|
||||||
messageId,
|
|
||||||
{ content: `update-${i}`, iteration: i },
|
|
||||||
{ step: i, timestamp: Date.now() }
|
|
||||||
);
|
|
||||||
expect(result.success).toBe(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final state should be consistent (last update applied)
|
|
||||||
const finalMessage = await getLatestMessageForChat(chatId);
|
|
||||||
expect((finalMessage?.responseMessages as any)?.iteration).toBe(4);
|
|
||||||
expect((finalMessage?.reasoning as any)?.step).toBe(4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('updates timestamp on every change', async () => {
|
|
||||||
const { messageId, chatId } = await createTestMessageWithContext();
|
|
||||||
|
|
||||||
const originalMessage = await getLatestMessageForChat(chatId);
|
|
||||||
const originalTimestamp = originalMessage?.updatedAt;
|
|
||||||
|
|
||||||
// Wait a moment to ensure timestamp difference
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
||||||
|
|
||||||
await updateMessageResponseMessages(messageId, { content: 'updated' });
|
|
||||||
|
|
||||||
const updatedMessage = await getLatestMessageForChat(chatId);
|
|
||||||
expect(updatedMessage?.updatedAt).not.toBe(originalTimestamp);
|
|
||||||
expect(new Date(updatedMessage?.updatedAt || '').getTime()).toBeGreaterThan(
|
|
||||||
new Date(originalTimestamp || '').getTime()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import {
|
import { getLatestMessageForChat, updateMessageFields } from '@buster/database';
|
||||||
getLatestMessageForChat,
|
|
||||||
updateMessageFields,
|
|
||||||
} from '@buster/database/src/helpers/messages';
|
|
||||||
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
||||||
import { createTestMessageWithContext } from '../../src';
|
import { createTestMessageWithContext } from '../../src';
|
||||||
import { cleanupTestEnvironment, setupTestEnvironment } from './helpers';
|
import { cleanupTestEnvironment, setupTestEnvironment } from '../../src/envHelpers/env-helpers';
|
||||||
|
|
||||||
describe('updateMessageFields', () => {
|
describe('updateMessageFields', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|
Loading…
Reference in New Issue