diff --git a/apps/server/src/api/v2/reports/[id]/GET.ts b/apps/server/src/api/v2/reports/[id]/GET.ts index 9f4659676..965193dd7 100644 --- a/apps/server/src/api/v2/reports/[id]/GET.ts +++ b/apps/server/src/api/v2/reports/[id]/GET.ts @@ -9,31 +9,22 @@ export async function getReportHandler( reportId: string, user: { id: string } ): Promise { - try { - const report = await getReport({ reportId, userId: user.id }); + const report = await getReport({ reportId, userId: user.id }); - const platejsResult = await markdownToPlatejs(report.content); + const platejsResult = await markdownToPlatejs(report.content); - if (platejsResult.error) { - throw new HTTPException(500, { - message: `Error converting markdown to PlateJS: ${platejsResult.error.message}`, - }); - } - - const content = platejsResult.elements ?? []; - - const response: GetReportIndividualResponse = { - ...report, - content, - }; - - return response; - } catch (error) { - console.error('Error getting report:', error); - throw new HTTPException(500, { - message: `Error getting report: ${error instanceof Error ? error.message : 'Unknown error'}`, - }); + if (platejsResult.error) { + throw platejsResult.error; } + + const content = platejsResult.elements ?? []; + + const response: GetReportIndividualResponse = { + ...report, + content, + }; + + return response; } const app = new Hono() diff --git a/apps/server/turbo.json b/apps/server/turbo.json index 685a9120a..131cdff88 100644 --- a/apps/server/turbo.json +++ b/apps/server/turbo.json @@ -17,7 +17,8 @@ "@buster/data-source#dev", "@buster-app/trigger#dev", "@buster-app/electric-server#dev", - "@buster-app/api-legacy#dev" + "@buster-app/api-legacy#dev", + "@buster/server-utils#dev" ] } } diff --git a/packages/database/src/schema-types/report-elements.ts b/packages/database/src/schema-types/report-elements.ts index 762bd027e..1c94dc556 100644 --- a/packages/database/src/schema-types/report-elements.ts +++ b/packages/database/src/schema-types/report-elements.ts @@ -322,8 +322,8 @@ const ListTypeEnum = z.enum(['ul', 'ol']); // Nested list item for complex lists const NestedListElementSchema = z.object({ - type: z.enum(['li', 'lic', 'lii']), - children: z.array(z.union([TextSchema, ParagraphElementSchema])).default([]), + type: z.enum(['li', 'lic', 'lii', 'ul', 'ol']), + children: z.array(z.any()).default([]), }); // List container (unordered or ordered) diff --git a/packages/server-utils/src/report/platejs-conversion.test.ts b/packages/server-utils/src/report/platejs-conversion.test.ts index d5ddf03dc..b013731a5 100644 --- a/packages/server-utils/src/report/platejs-conversion.test.ts +++ b/packages/server-utils/src/report/platejs-conversion.test.ts @@ -71,6 +71,37 @@ Here's an unordered list: const platejs = await markdownToPlatejs(markdown); expect(platejs).toBeDefined(); }); + + it('real world markdown', async () => { + const markdown = `Our most popular mountain bike over the last 12 months is Mountain-200 Black, 38 with 825 units sold. +## Key Findings +- The top-selling mountain bike model is **Mountain-200 Black, 38**. +- It sold **825 units** in the last 12 months. +## Metric + +## Context +- I focused specifically on complete bicycle products in the **Mountain Bikes** subcategory within the broader **Bikes** category to avoid counting components or frames. +- I measured popularity by **units sold**, which reflects the number of bikes customers purchased. +- Timeframe defaults to the **last 12 months** to show a current view. +## Methodology +- Data sources: Sales order lines and headers, and product catalog tables in the operational analytics database. +- Filters: + - Product Category = "Bikes" + - Product Subcategory = "Mountain Bikes" + - Order Date between CURRENT_DATE - 12 months and CURRENT_DATE +- Calculation: + - For each mountain bike product, sum of sales order quantities from sales order details. + - Select the product with the highest total units sold. +- Notes on definitions: + - "Most popular" defined as highest **units sold**; alternative definitions could use revenue or number of distinct orders, but units sold most directly represents product popularity by volume. + - Product names are used as the display label to identify the specific model. +- Alternatives considered: + - Using revenue-based popularity could favor higher-priced bikes; I chose units to avoid price bias. + - Using the riding discipline filter (e.g., Mountain) was considered, but I used the explicit Mountain Bikes subcategory to exclude components. +`; + const platejs = await markdownToPlatejs(markdown); + expect(platejs).toBeDefined(); + }); }); describe('platejsToMarkdown', () => { @@ -144,4 +175,394 @@ describe('platejsToMarkdown', () => { const expectedMarkdown = `This is a simple paragraph.\n\n`; expect(markdownFromPlatejs).toBe(expectedMarkdown); }); + + it('should convert callout platejs element to markdown', async () => { + const elements: ReportElements = [ + { + children: [ + { + text: 'Our most popular mountain bike over the last 12 months is Mountain-200 Black, 38 with 825 units sold.', + }, + ], + type: 'p', + }, + { + children: [ + { + text: 'Key Findings', + }, + ], + type: 'h2', + }, + { + children: [ + { + children: [ + { + children: [ + { + text: 'The top-selling mountain bike model is ', + }, + { + bold: true, + text: 'Mountain-200 Black, 38', + }, + { + text: '.', + }, + ], + type: 'lic', + }, + ], + type: 'li', + }, + { + children: [ + { + children: [ + { + text: 'It sold ', + }, + { + bold: true, + text: '825 units', + }, + { + text: ' in the last 12 months.', + }, + ], + type: 'lic', + }, + ], + type: 'li', + }, + ], + type: 'ul', + }, + { + children: [ + { + text: 'Metric', + }, + ], + type: 'h2', + }, + { + children: [ + { + text: '\n', + }, + { + text: '', + }, + { + text: '\n', + }, + ], + type: 'p', + }, + { + children: [ + { + text: 'Context', + }, + ], + type: 'h2', + }, + { + children: [ + { + children: [ + { + children: [ + { + text: 'I focused specifically on complete bicycle products in the ', + }, + { + bold: true, + text: 'Mountain Bikes', + }, + { + text: ' subcategory within the broader ', + }, + { + bold: true, + text: 'Bikes', + }, + { + text: ' category to avoid counting components or frames.', + }, + ], + type: 'lic', + }, + ], + type: 'li', + }, + { + children: [ + { + children: [ + { + text: 'I measured popularity by ', + }, + { + bold: true, + text: 'units sold', + }, + { + text: ', which reflects the number of bikes customers purchased.', + }, + ], + type: 'lic', + }, + ], + type: 'li', + }, + { + children: [ + { + children: [ + { + text: 'Timeframe defaults to the ', + }, + { + bold: true, + text: 'last 12 months', + }, + { + text: ' to show a current view.', + }, + ], + type: 'lic', + }, + ], + type: 'li', + }, + ], + type: 'ul', + }, + { + children: [ + { + text: 'Methodology', + }, + ], + type: 'h2', + }, + { + children: [ + { + children: [ + { + children: [ + { + text: 'Data sources: Sales order lines and headers, and product catalog tables in the operational analytics database.', + }, + ], + type: 'lic', + }, + ], + type: 'li', + }, + { + children: [ + { + children: [ + { + text: 'Filters:', + }, + ], + type: 'lic', + }, + { + children: [ + { + children: [ + { + children: [ + { + text: 'Product Category = "Bikes"', + }, + ], + type: 'lic', + }, + ], + type: 'li', + }, + { + children: [ + { + children: [ + { + text: 'Product Subcategory = "Mountain Bikes"', + }, + ], + type: 'lic', + }, + ], + type: 'li', + }, + { + children: [ + { + children: [ + { + text: 'Order Date between CURRENT_DATE - 12 months and CURRENT_DATE', + }, + ], + type: 'lic', + }, + ], + type: 'li', + }, + ], + type: 'ul', + }, + ], + type: 'li', + }, + { + children: [ + { + children: [ + { + text: 'Calculation:', + }, + ], + type: 'lic', + }, + { + children: [ + { + children: [ + { + children: [ + { + text: 'For each mountain bike product, sum of sales order quantities from sales order details.', + }, + ], + type: 'lic', + }, + ], + type: 'li', + }, + { + children: [ + { + children: [ + { + text: 'Select the product with the highest total units sold.', + }, + ], + type: 'lic', + }, + ], + type: 'li', + }, + ], + type: 'ul', + }, + ], + type: 'li', + }, + { + children: [ + { + children: [ + { + text: 'Notes on definitions:', + }, + ], + type: 'lic', + }, + { + children: [ + { + children: [ + { + children: [ + { + text: '"Most popular" defined as highest ', + }, + { + bold: true, + text: 'units sold', + }, + { + text: '; alternative definitions could use revenue or number of distinct orders, but units sold most directly represents product popularity by volume.', + }, + ], + type: 'lic', + }, + ], + type: 'li', + }, + { + children: [ + { + children: [ + { + text: 'Product names are used as the display label to identify the specific model.', + }, + ], + type: 'lic', + }, + ], + type: 'li', + }, + ], + type: 'ul', + }, + ], + type: 'li', + }, + { + children: [ + { + children: [ + { + text: 'Alternatives considered:', + }, + ], + type: 'lic', + }, + { + children: [ + { + children: [ + { + children: [ + { + text: 'Using revenue-based popularity could favor higher-priced bikes; I chose units to avoid price bias.', + }, + ], + type: 'lic', + }, + ], + type: 'li', + }, + { + children: [ + { + children: [ + { + text: 'Using the riding discipline filter (e.g., Mountain) was considered, but I used the explicit Mountain Bikes subcategory to exclude components.', + }, + ], + type: 'lic', + }, + ], + type: 'li', + }, + ], + type: 'ul', + }, + ], + type: 'li', + }, + ], + type: 'ul', + }, + ]; + }); }); diff --git a/packages/server-utils/src/report/platejs-conversions.ts b/packages/server-utils/src/report/platejs-conversions.ts index 3ba1ea3f9..f406dc8b1 100644 --- a/packages/server-utils/src/report/platejs-conversions.ts +++ b/packages/server-utils/src/report/platejs-conversions.ts @@ -5,9 +5,14 @@ import { SERVER_EDITOR } from './server-editor'; export const markdownToPlatejs = async (markdown: string) => { try { const descendants = SERVER_EDITOR.api.markdown.deserialize(markdown); + console.log('descendants', descendants); + + console.log('descendants.json', JSON.stringify(descendants, null, 2)); const safeParsedElements = ReportElementsSchema.safeParse(descendants); + console.log('safeParsedElements', safeParsedElements); + return { error: safeParsedElements.error, elements: safeParsedElements.data, diff --git a/packages/server-utils/turbo.json b/packages/server-utils/turbo.json new file mode 100644 index 000000000..7698f75c3 --- /dev/null +++ b/packages/server-utils/turbo.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "inputs": [ + "src/**/*", + "!src/**/*.test.{ts,tsx,js,jsx}", + "!src/**/*.spec.{ts,tsx,js,jsx}", + "package.json", + "tsconfig.json" + ], + "outputs": ["dist/**/*"] + }, + "dev": { + "cache": false, + "persistent": true, + "dependsOn": ["^build"], + "with": [] + } + } +}