additional lint fixes

This commit is contained in:
Nate Kelley 2025-07-07 10:12:31 -06:00
parent 7e7aeb3480
commit 5d532a2957
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
15 changed files with 65 additions and 331 deletions

View File

@ -25,7 +25,8 @@ export type GenerateAssetMessagesInput = z.infer<typeof GenerateAssetMessagesInp
interface AssetDetails {
id: 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;
}

View File

@ -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
* Allows updating responseMessages, reasoning, and/or rawLlmMessages in a single query
@ -254,9 +105,10 @@ export async function updateMessageStreamingFields(
export async function updateMessageFields(
messageId: string,
fields: {
responseMessages?: any;
reasoning?: any;
rawLlmMessages?: any;
//TODO: Dallin let's make a type for this. It should not just be a jsonb object.
responseMessages?: unknown;
reasoning?: unknown;
rawLlmMessages?: unknown;
finalReasoningMessage?: string;
}
): Promise<{ success: boolean }> {
@ -267,7 +119,13 @@ export async function updateMessageFields(
}
// 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(),
};
@ -335,18 +193,16 @@ export async function updateMessage(
throw new Error('Reasoning cannot be null - database constraint violation');
}
// Remove undefined fields and build update object
const updateData: any = {
const updateData = {
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' in fields && fields.updatedAt !== undefined) {
updateData.updatedAt = fields.updatedAt;

View File

@ -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.
category: z.array(z.string()).default([]),
// 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({
x: [],
y: [],
category: [],
tooltip: null
tooltip: null,
});
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.
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.
tooltip: z.nullable(z.array(z.string())).default(null)
tooltip: z.nullable(z.array(z.string())).default(null),
})
.default({
x: [],
y: [],
size: [],
category: [],
tooltip: null
tooltip: null,
});
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.
category: z.array(z.string()).default([]),
// 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({
x: [],
y: [],
y2: [],
category: [],
tooltip: null
tooltip: null,
});
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.
y: z.array(z.string()).default([]),
// 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({
x: [],
y: [],
tooltip: null
tooltip: null,
});
export const ChartEncodesSchema = z.union([
BarAndLineAxisSchema,
ScatterAxisSchema,
PieChartAxisSchema,
ComboChartAxisSchema
ComboChartAxisSchema,
]);
// Export inferred types

View File

@ -12,7 +12,7 @@ export const BarChartPropsSchema = z.object({
// 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'),
// 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>;

View File

@ -1,9 +1,9 @@
import { describe, expect, it } from "vitest";
import { ChartConfigPropsSchema } from "./chartConfigProps";
import { DEFAULT_CHART_THEME } from "./configColors";
import { describe, expect, it } from 'vitest';
import { ChartConfigPropsSchema } from './chartConfigProps';
import { DEFAULT_CHART_THEME } from './configColors';
describe("DEFAULT_CHART_CONFIG", () => {
it("should conform to BusterChartConfigPropsSchema and have expected default values", () => {
describe('DEFAULT_CHART_CONFIG', () => {
it('should conform to BusterChartConfigPropsSchema and have expected default values', () => {
// Verify that DEFAULT_CHART_CONFIG is valid according to the schema
const parseResult = ChartConfigPropsSchema.safeParse({});
expect(parseResult.success).toBe(true);
@ -21,9 +21,9 @@ describe("DEFAULT_CHART_CONFIG", () => {
expect(config.columnSettings).toEqual({});
expect(config.columnLabelFormats).toEqual({});
expect(config.showLegend).toBeNull();
expect(config.barLayout).toBe("vertical");
expect(config.barLayout).toBe('vertical');
expect(config.barSortBy).toEqual([]);
expect(config.barGroupType).toBe("group");
expect(config.barGroupType).toBe('group');
expect(config.barShowTotalAtTop).toBe(false);
expect(config.lineGroupType).toBeNull();
expect(config.scatterAxis).toEqual({
@ -40,7 +40,7 @@ describe("DEFAULT_CHART_CONFIG", () => {
});
// 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();
// Verify it has a selectedChartType (required field)

View File

@ -3,7 +3,7 @@ import { ComboChartAxisSchema } from './axisInterfaces';
export const ComboChartPropsSchema = z.object({
// Required for Combo
comboChartAxis: ComboChartAxisSchema
comboChartAxis: ComboChartAxisSchema,
});
export type ComboChartProps = z.infer<typeof ComboChartPropsSchema>;

View File

@ -8,5 +8,5 @@ export const DEFAULT_CHART_THEME = [
'#F3864F',
'#C82184',
'#31FCB4',
'#E83562'
'#E83562',
];

View File

@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { LineChartPropsSchema, type LineChartProps } from './lineChartProps';
import { describe, expect, it } from 'vitest';
import { type LineChartProps, LineChartPropsSchema } from './lineChartProps';
describe('LineChartPropsSchema', () => {
describe('valid inputs', () => {
@ -81,17 +81,17 @@ describe('LineChartPropsSchema', () => {
describe('type inference', () => {
it('should correctly infer LineChartProps type', () => {
const validData: LineChartProps = {
lineGroupType: 'stack'
lineGroupType: 'stack',
};
expect(LineChartPropsSchema.parse(validData)).toEqual(validData);
const validDataWithNull: LineChartProps = {
lineGroupType: null
lineGroupType: null,
};
expect(LineChartPropsSchema.parse(validDataWithNull)).toEqual(validDataWithNull);
const validDataWithPercentage: LineChartProps = {
lineGroupType: 'percentage-stack'
lineGroupType: 'percentage-stack',
};
expect(LineChartPropsSchema.parse(validDataWithPercentage)).toEqual(validDataWithPercentage);
});

View File

@ -2,7 +2,7 @@ import { z } from 'zod/v4';
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 %"
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>;

View File

@ -6,7 +6,7 @@ export const DerivedMetricTitleSchema = z.object({
// whether to display to use the key or the value in the chart
useValue: z.boolean(),
// 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({
@ -21,7 +21,7 @@ export const MetricChartPropsSchema = z.object({
// OPTIONAL: default is ''
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.
metricValueLabel: z.nullable(z.string()).default(null)
metricValueLabel: z.nullable(z.string()).default(null),
});
export type DerivedMetricTitle = z.infer<typeof DerivedMetricTitleSchema>;

View File

@ -1,6 +1,6 @@
import { z } from "zod/v4";
import { PieChartAxisSchema } from "./axisInterfaces";
import { PieSortBySchema } from "./etcInterfaces";
import { z } from 'zod/v4';
import { PieChartAxisSchema } from './axisInterfaces';
import { PieSortBySchema } from './etcInterfaces';
export const PieChartPropsSchema = z.object({
// OPTIONAL: default: value
@ -8,30 +8,28 @@ export const PieChartPropsSchema = z.object({
// Required for Pie
pieChartAxis: PieChartAxisSchema,
// 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.
pieShowInnerLabel: z.boolean().default(true),
// OPTIONAL: default: sum
pieInnerLabelAggregate: z
.enum(["sum", "average", "median", "max", "min", "count"])
.default("sum"),
.enum(['sum', 'average', 'median', 'max', 'min', 'count'])
.default('sum'),
// OPTIONAL: default is null and will be the name of the pieInnerLabelAggregate
pieInnerLabelTitle: z.string().nullable().default(null),
// OPTIONAL: default: none
pieLabelPosition: z
.nullable(z.enum(["inside", "outside", "none"]))
.default("none"),
pieLabelPosition: z.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
pieDonutWidth: z
.number()
.min(0, "Donut width must be at least 0")
.max(65, "Donut width must be at most 65")
.min(0, 'Donut width must be at least 0')
.max(65, 'Donut width must be at most 65')
.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.
pieMinimumSlicePercentage: z
.number()
.min(0, "Minimum slice percentage must be at least 0")
.max(100, "Minimum slice percentage must be at most 100")
.min(0, 'Minimum slice percentage must be at least 0')
.max(100, 'Minimum slice percentage must be at most 100')
.default(0),
});

View File

@ -4,7 +4,7 @@ import { ScatterAxisSchema } from './axisInterfaces';
export const ScatterChartPropsSchema = z.object({
// Required for Scatter
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>;

View File

@ -5,7 +5,7 @@ export const TableChartPropsSchema = z.object({
tableColumnWidths: z.nullable(z.record(z.string(), z.number())).default(null),
tableHeaderBackgroundColor: 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>;

View File

@ -1,12 +1,11 @@
import {
getLatestMessageForChat,
updateMessageReasoning,
updateMessageResponseMessages,
updateMessageStreamingFields,
} from '@buster/database/src/helpers/messages';
} from '@buster/database';
import { createTestMessageWithContext } from '@buster/test-utils';
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', () => {
beforeEach(async () => {
@ -17,84 +16,6 @@ describe('Message Update Helpers', () => {
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', () => {
test('successfully updates reasoning JSONB field', async () => {
const { messageId, chatId } = await createTestMessageWithContext();
@ -266,43 +187,4 @@ describe('Message Update Helpers', () => {
).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()
);
});
});
});

View File

@ -1,10 +1,7 @@
import {
getLatestMessageForChat,
updateMessageFields,
} from '@buster/database/src/helpers/messages';
import { getLatestMessageForChat, updateMessageFields } from '@buster/database';
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { createTestMessageWithContext } from '../../src';
import { cleanupTestEnvironment, setupTestEnvironment } from './helpers';
import { cleanupTestEnvironment, setupTestEnvironment } from '../../src/envHelpers/env-helpers';
describe('updateMessageFields', () => {
beforeEach(async () => {