From db6f2ca7afb061583d9da0932ff12682546deadd Mon Sep 17 00:00:00 2001 From: socallmebertille Date: Mon, 20 Jul 2026 15:33:25 +0200 Subject: [PATCH 1/4] resolve data unavailable on chart changes when story is live --- apps/backend/src/services/live-story.ts | 47 +++++++++++++++++++++++-- apps/backend/src/trpc/story.routes.ts | 13 +++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/services/live-story.ts b/apps/backend/src/services/live-story.ts index e82bb24fb..32efe1cb0 100644 --- a/apps/backend/src/services/live-story.ts +++ b/apps/backend/src/services/live-story.ts @@ -105,7 +105,8 @@ export async function getStoryQueryData( const cache = await storyQueries.getStoryDataCacheByChatAndSlug(chatId, slug); if (cache && !isCacheExpired(cache.cachedAt, cacheSchedule)) { - return { queryData: cache.queryData, cachedAt: cache.cachedAt }; + const queryData = await backfillQueryDataFromChat(chatId, code, cache.queryData); + return { queryData, cachedAt: cache.cachedAt }; } try { @@ -116,12 +117,54 @@ export async function getStoryQueryData( }; } catch { if (cache) { - return { queryData: cache.queryData, cachedAt: cache.cachedAt }; + const queryData = await backfillQueryDataFromChat(chatId, code, cache.queryData); + return { queryData, cachedAt: cache.cachedAt }; } return { queryData: await getQueryDataFromCode(chatId, code), cachedAt: null }; } } +export async function backfillQueryDataFromChat( + chatId: string, + code: string, + cached: Record | null, +): Promise | null> { + const referencedIds = extractStoryQueryIds(code); + if (referencedIds.size === 0) { + return cached; + } + + const base = cached ?? {}; + const missingIds = [...referencedIds].filter((id) => !base[id]); + if (missingIds.length === 0) { + return cached; + } + + const fromChat = await getQueryDataFromCode(chatId, code); + if (!fromChat) { + return cached; + } + + const merged = { ...base }; + for (const id of missingIds) { + if (fromChat[id]) { + merged[id] = fromChat[id]; + } + } + + return Object.keys(merged).length > 0 ? merged : cached; +} + +function extractStoryQueryIds(code: string): Set { + const ids = new Set(); + const regex = /<(?:chart|table)\s+[^>]*?\bquery_id\s*=\s*"([^"]+)"/g; + let match: RegExpExecArray | null; + while ((match = regex.exec(code)) !== null) { + ids.add(match[1]); + } + return ids; +} + async function executeRawSql( sqlQuery: string, projectFolder: string, diff --git a/apps/backend/src/trpc/story.routes.ts b/apps/backend/src/trpc/story.routes.ts index dfe66ba6c..fcf90416e 100644 --- a/apps/backend/src/trpc/story.routes.ts +++ b/apps/backend/src/trpc/story.routes.ts @@ -13,7 +13,12 @@ import * as sharedStoryQueries from '../queries/shared-story.queries'; import * as storyQueries from '../queries/story.queries'; import * as storyFolderQueries from '../queries/story-folder.queries'; import { naturalLanguageToCron } from '../services/cron-nlp'; -import { executeLiveQuery, getStoryQueryData, refreshStoryData } from '../services/live-story'; +import { + backfillQueryDataFromChat, + executeLiveQuery, + getStoryQueryData, + refreshStoryData, +} from '../services/live-story'; import { nextCronTick } from '../services/scheduler.service'; import { logAnalyticsEvent } from '../utils/analytics-event'; import { buildDownloadResponse } from '../utils/story-download'; @@ -121,7 +126,11 @@ export const storyRoutes = { }); } - return { ...story, queryData: cache?.queryData ?? null }; + const queryData = story.chatId + ? await backfillQueryDataFromChat(story.chatId, story.code, cache?.queryData ?? null) + : (cache?.queryData ?? null); + + return { ...story, queryData }; }), getLatest: chatOwnerProcedure From 9b904ec3bcf3a4afb6c10aeea492d1ff4c86716f Mon Sep 17 00:00:00 2001 From: socallmebertille Date: Mon, 20 Jul 2026 16:41:42 +0200 Subject: [PATCH 2/4] resolve from cubic review --- apps/backend/src/services/live-story.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/services/live-story.ts b/apps/backend/src/services/live-story.ts index 32efe1cb0..1ad248c1a 100644 --- a/apps/backend/src/services/live-story.ts +++ b/apps/backend/src/services/live-story.ts @@ -140,7 +140,7 @@ export async function backfillQueryDataFromChat( return cached; } - const fromChat = await getQueryDataFromCode(chatId, code); + const fromChat = await getQueryDataFromCode(chatId, code).catch(() => null); if (!fromChat) { return cached; } From 202b66e6167d24cf0869875bccf6f02ded5b27ca Mon Sep 17 00:00:00 2001 From: socallmebertille Date: Wed, 22 Jul 2026 09:43:37 +0200 Subject: [PATCH 3/4] resolve from review --- apps/backend/src/mcp/tools/context-layer.ts | 3 +- .../src/queries/shared-story.queries.ts | 9 +-- apps/backend/src/queries/story.queries.ts | 9 +-- apps/backend/src/services/automation-tools.ts | 15 +---- apps/backend/src/services/live-story.ts | 46 +------------- apps/backend/src/trpc/story.routes.ts | 10 +--- apps/backend/src/utils/story-query-data.ts | 60 +++++++++++-------- apps/frontend/src/lib/story-share.utils.ts | 10 +--- apps/shared/src/story-segments.ts | 10 ++++ 9 files changed, 62 insertions(+), 110 deletions(-) diff --git a/apps/backend/src/mcp/tools/context-layer.ts b/apps/backend/src/mcp/tools/context-layer.ts index afcfc3b5f..d3a79dcb4 100644 --- a/apps/backend/src/mcp/tools/context-layer.ts +++ b/apps/backend/src/mcp/tools/context-layer.ts @@ -276,8 +276,7 @@ async function cacheStoryQueryData( const resolvedQueryData = await resolveStoryQueryData( code, Object.keys(seededQueryData).length > 0 ? seededQueryData : null, - ctx.projectId, - ctx.userId, + { projectId: ctx.projectId, userId: ctx.userId }, ); if (!resolvedQueryData) { return; diff --git a/apps/backend/src/queries/shared-story.queries.ts b/apps/backend/src/queries/shared-story.queries.ts index dd0860bcd..390a97cde 100644 --- a/apps/backend/src/queries/shared-story.queries.ts +++ b/apps/backend/src/queries/shared-story.queries.ts @@ -1,3 +1,4 @@ +import { extractQueryIds } from '@nao/shared/story-segments'; import { and, count, desc, eq, isNull, max, or, type SQL, sql } from 'drizzle-orm'; import s, { type DBSharedStory } from '../db/abstractSchema'; @@ -130,13 +131,7 @@ export async function getQueryDataFromCode( chatId: string, code: string, ): Promise | null> { - const chartRegex = /<(?:chart|table)\s+[^>]*query_id="([^"]*)"[^>]*\/?>/g; - const queryIds = new Set(); - let match; - while ((match = chartRegex.exec(code)) !== null) { - queryIds.add(match[1]); - } - + const queryIds = extractQueryIds(code); if (queryIds.size === 0) { return null; } diff --git a/apps/backend/src/queries/story.queries.ts b/apps/backend/src/queries/story.queries.ts index ea9da345d..0e7f597a4 100644 --- a/apps/backend/src/queries/story.queries.ts +++ b/apps/backend/src/queries/story.queries.ts @@ -1,3 +1,4 @@ +import { extractQueryIds } from '@nao/shared/story-segments'; import { type StorySharingInfo } from '@nao/shared/types'; import { and, asc, desc, eq, inArray, isNull, max, or, type SQL, sql } from 'drizzle-orm'; @@ -557,13 +558,7 @@ export async function getSqlQueriesFromCode( chatId: string, code: string, ): Promise> { - const chartRegex = /<(?:chart|table)\s+[^>]*query_id="([^"]*)"[^>]*\/?>/g; - const queryIds = new Set(); - let match; - while ((match = chartRegex.exec(code)) !== null) { - queryIds.add(match[1]); - } - + const queryIds = extractQueryIds(code); if (queryIds.size === 0) { return {}; } diff --git a/apps/backend/src/services/automation-tools.ts b/apps/backend/src/services/automation-tools.ts index 9911c38af..737f573a4 100644 --- a/apps/backend/src/services/automation-tools.ts +++ b/apps/backend/src/services/automation-tools.ts @@ -1,4 +1,5 @@ import type { DateFormatSettings } from '@nao/shared/date'; +import { extractQueryIds } from '@nao/shared/story-segments'; import type { displayChart } from '@nao/shared/tools'; import { z } from 'zod/v4'; @@ -316,8 +317,8 @@ async function buildStoryPdfAttachments( } async function getStoryQueryData(context: ToolContext, code: string): Promise { - const queryIds = extractStoryQueryIds(code); - if (queryIds.length === 0) { + const queryIds = extractQueryIds(code); + if (queryIds.size === 0) { return null; } @@ -332,16 +333,6 @@ async function getStoryQueryData(context: ToolContext, code: string): Promise 0 ? queryData : null; } -function extractStoryQueryIds(code: string): string[] { - const queryIds = new Set(); - const chartRegex = /<(?:chart|table)\s+[^>]*query_id="([^"]*)"[^>]*\/?>/g; - let match: RegExpExecArray | null; - while ((match = chartRegex.exec(code)) !== null) { - queryIds.add(match[1]); - } - return [...queryIds]; -} - function appendInlineChartImages(html: string, attachments: GeneratedArtifactAttachment[]): string { const charts = attachments.filter((attachment) => attachment.kind === 'chart' && attachment.cid); if (charts.length === 0) { diff --git a/apps/backend/src/services/live-story.ts b/apps/backend/src/services/live-story.ts index 1ad248c1a..6ac30079f 100644 --- a/apps/backend/src/services/live-story.ts +++ b/apps/backend/src/services/live-story.ts @@ -13,6 +13,7 @@ import * as llmConfigQueries from '../queries/project-llm-config.queries'; import { getQueryDataFromCode } from '../queries/shared-story.queries'; import * as storyQueries from '../queries/story.queries'; import { getDefaultModelId, resolveProviderModel } from '../utils/llm'; +import { resolveStoryQueryData } from '../utils/story-query-data'; import { MAX_OUTPUT_TOKENS } from './agent'; const MAX_RENDERED_ROWS = 60; @@ -105,7 +106,7 @@ export async function getStoryQueryData( const cache = await storyQueries.getStoryDataCacheByChatAndSlug(chatId, slug); if (cache && !isCacheExpired(cache.cachedAt, cacheSchedule)) { - const queryData = await backfillQueryDataFromChat(chatId, code, cache.queryData); + const queryData = await resolveStoryQueryData(code, cache.queryData, { chatId }); return { queryData, cachedAt: cache.cachedAt }; } @@ -117,54 +118,13 @@ export async function getStoryQueryData( }; } catch { if (cache) { - const queryData = await backfillQueryDataFromChat(chatId, code, cache.queryData); + const queryData = await resolveStoryQueryData(code, cache.queryData, { chatId }); return { queryData, cachedAt: cache.cachedAt }; } return { queryData: await getQueryDataFromCode(chatId, code), cachedAt: null }; } } -export async function backfillQueryDataFromChat( - chatId: string, - code: string, - cached: Record | null, -): Promise | null> { - const referencedIds = extractStoryQueryIds(code); - if (referencedIds.size === 0) { - return cached; - } - - const base = cached ?? {}; - const missingIds = [...referencedIds].filter((id) => !base[id]); - if (missingIds.length === 0) { - return cached; - } - - const fromChat = await getQueryDataFromCode(chatId, code).catch(() => null); - if (!fromChat) { - return cached; - } - - const merged = { ...base }; - for (const id of missingIds) { - if (fromChat[id]) { - merged[id] = fromChat[id]; - } - } - - return Object.keys(merged).length > 0 ? merged : cached; -} - -function extractStoryQueryIds(code: string): Set { - const ids = new Set(); - const regex = /<(?:chart|table)\s+[^>]*?\bquery_id\s*=\s*"([^"]+)"/g; - let match: RegExpExecArray | null; - while ((match = regex.exec(code)) !== null) { - ids.add(match[1]); - } - return ids; -} - async function executeRawSql( sqlQuery: string, projectFolder: string, diff --git a/apps/backend/src/trpc/story.routes.ts b/apps/backend/src/trpc/story.routes.ts index fcf90416e..857316671 100644 --- a/apps/backend/src/trpc/story.routes.ts +++ b/apps/backend/src/trpc/story.routes.ts @@ -13,15 +13,11 @@ import * as sharedStoryQueries from '../queries/shared-story.queries'; import * as storyQueries from '../queries/story.queries'; import * as storyFolderQueries from '../queries/story-folder.queries'; import { naturalLanguageToCron } from '../services/cron-nlp'; -import { - backfillQueryDataFromChat, - executeLiveQuery, - getStoryQueryData, - refreshStoryData, -} from '../services/live-story'; +import { executeLiveQuery, getStoryQueryData, refreshStoryData } from '../services/live-story'; import { nextCronTick } from '../services/scheduler.service'; import { logAnalyticsEvent } from '../utils/analytics-event'; import { buildDownloadResponse } from '../utils/story-download'; +import { resolveStoryQueryData } from '../utils/story-query-data'; import { extractStorySummary } from '../utils/story-summary'; import { canSendProcedure, ownedResourceProcedure, projectProtectedProcedure, protectedProcedure } from './trpc'; @@ -127,7 +123,7 @@ export const storyRoutes = { } const queryData = story.chatId - ? await backfillQueryDataFromChat(story.chatId, story.code, cache?.queryData ?? null) + ? await resolveStoryQueryData(story.code, cache?.queryData ?? null, { chatId: story.chatId }) : (cache?.queryData ?? null); return { ...story, queryData }; diff --git a/apps/backend/src/utils/story-query-data.ts b/apps/backend/src/utils/story-query-data.ts index 031ce2359..3fed6530e 100644 --- a/apps/backend/src/utils/story-query-data.ts +++ b/apps/backend/src/utils/story-query-data.ts @@ -1,9 +1,13 @@ +import { extractQueryIds } from '@nao/shared/story-segments'; + import { getMcpQueryData } from '../queries/mcp-query-data.queries'; import { getQueryDataFromCode } from '../queries/shared-story.queries'; import * as storyQueries from '../queries/story.queries'; export type StoryQueryDataMap = Record; +export type StoryQueryDataSource = { chatId: string } | { projectId: string; userId?: string }; + export async function resolveStoryQueryDataForSandbox( code: string, opts: { storyId?: string; chatId?: string | null; projectId: string; userId?: string }, @@ -23,44 +27,50 @@ export async function resolveStoryQueryDataForSandbox( } } const seeded = Object.keys(seed).length > 0 ? seed : null; - return resolveStoryQueryData(code, seeded, opts.projectId, opts.userId); + return resolveStoryQueryData(code, seeded, { projectId: opts.projectId, userId: opts.userId }); } export async function resolveStoryQueryData( code: string, cachedQueryData: StoryQueryDataMap | null, - projectId: string, - userId?: string, + source: StoryQueryDataSource, ): Promise { - const referencedIds = extractQueryIdsFromStoryCode(code); - if (referencedIds.size === 0) { + const missing = [...extractQueryIds(code)].filter((id) => !cachedQueryData?.[id]); + if (missing.length === 0) { return cachedQueryData; } - const merged: StoryQueryDataMap = { ...(cachedQueryData ?? {}) }; - const missing = [...referencedIds].filter((id) => !merged[id]); - if (missing.length === 0) { - return merged; + const filled = + 'chatId' in source + ? await fetchFromChat(source.chatId, code, missing) + : await fetchFromMcp(missing, source.projectId, source.userId); + + const merged = { ...(cachedQueryData ?? {}), ...filled }; + return Object.keys(merged).length > 0 ? merged : null; +} + +async function fetchFromChat(chatId: string, code: string, ids: string[]): Promise { + const fromChat = await getQueryDataFromCode(chatId, code).catch(() => null); + + const filled: StoryQueryDataMap = {}; + for (const id of ids) { + if (fromChat?.[id]) { + filled[id] = fromChat[id]; + } } + return filled; +} +async function fetchFromMcp(ids: string[], projectId: string, userId?: string): Promise { const fetchOptions = userId ? { userId } : undefined; - const fetched = await Promise.all(missing.map((id) => getMcpQueryData(id, projectId, fetchOptions))); - missing.forEach((id, idx) => { - const row = fetched[idx]; + const rows = await Promise.all(ids.map((id) => getMcpQueryData(id, projectId, fetchOptions))); + + const filled: StoryQueryDataMap = {}; + ids.forEach((id, idx) => { + const row = rows[idx]; if (row) { - merged[id] = { columns: row.columns, data: row.data }; + filled[id] = { columns: row.columns, data: row.data }; } }); - - return Object.keys(merged).length > 0 ? merged : null; -} - -function extractQueryIdsFromStoryCode(code: string): Set { - const ids = new Set(); - const regex = /<(?:chart|table)\s+[^>]*?\bquery_id\s*=\s*"([^"]+)"/g; - let match: RegExpExecArray | null; - while ((match = regex.exec(code)) !== null) { - ids.add(match[1]); - } - return ids; + return filled; } diff --git a/apps/frontend/src/lib/story-share.utils.ts b/apps/frontend/src/lib/story-share.utils.ts index f20cbde8a..62c7030e0 100644 --- a/apps/frontend/src/lib/story-share.utils.ts +++ b/apps/frontend/src/lib/story-share.utils.ts @@ -1,3 +1,5 @@ +import { extractQueryIds } from '@nao/shared/story-segments'; + import type { UIMessage } from '@nao/backend/chat'; /** @@ -9,13 +11,7 @@ export function getQueryDataFromCodeFromMessages( messages: UIMessage[], code: string, ): Record | null { - const chartRegex = /<(?:chart|table)\s+[^>]*query_id="([^"]*)"[^>]*\/?>/g; - const queryIds = new Set(); - let match; - while ((match = chartRegex.exec(code)) !== null) { - queryIds.add(match[1]); - } - + const queryIds = extractQueryIds(code); if (queryIds.size === 0) { return null; } diff --git a/apps/shared/src/story-segments.ts b/apps/shared/src/story-segments.ts index 9bae4d942..950546aaa 100644 --- a/apps/shared/src/story-segments.ts +++ b/apps/shared/src/story-segments.ts @@ -195,6 +195,16 @@ function extractSeriesFromRawAttrs(attrString: string): ParsedChartBlock['series return null; } +export function extractQueryIds(code: string): Set { + const ids = new Set(); + const regex = /<(?:chart|table)\s+[^>]*?\bquery_id\s*=\s*"([^"]+)"/g; + let match: RegExpExecArray | null; + while ((match = regex.exec(code)) !== null) { + ids.add(match[1]); + } + return ids; +} + export function splitCodeIntoSegments(code: string): Segment[] { const segments: Segment[] = []; const blockRegex = new RegExp( From dabc452fa7bdfb04d97bd3432755b36d98f6ba07 Mon Sep 17 00:00:00 2001 From: socallmebertille Date: Wed, 22 Jul 2026 18:27:14 +0200 Subject: [PATCH 4/4] clearer renaming --- apps/backend/src/mcp/embed/sandbox-html.ts | 4 ++-- apps/backend/src/mcp/tools/context-layer.ts | 4 ++-- apps/backend/src/services/live-story.ts | 16 +++++++++++----- apps/backend/src/trpc/story.routes.ts | 4 ++-- apps/backend/src/utils/embed-story.ts | 4 ++-- apps/backend/src/utils/story-query-data.ts | 12 ++++++++---- 6 files changed, 27 insertions(+), 17 deletions(-) diff --git a/apps/backend/src/mcp/embed/sandbox-html.ts b/apps/backend/src/mcp/embed/sandbox-html.ts index d95496316..a3da10f37 100644 --- a/apps/backend/src/mcp/embed/sandbox-html.ts +++ b/apps/backend/src/mcp/embed/sandbox-html.ts @@ -1,6 +1,6 @@ import type { QueryDataMap } from '../../utils/story-download'; import { generateStoryHtml } from '../../utils/story-html'; -import { resolveStoryQueryDataForSandbox } from '../../utils/story-query-data'; +import { backfillMissingQueryDataForSandbox } from '../../utils/story-query-data'; import { storyEmbedUrls } from '../urls'; import { MAX_SANDBOX_HTML_CHARS } from './embed-payload'; import { SANDBOX_EMBED_ROOT_STYLES, SANDBOX_ICON_DOWNLOAD, SANDBOX_ICON_EXTERNAL_LINK } from './header'; @@ -28,7 +28,7 @@ export async function buildStorySandboxHtml(params: { chatId?: string | null; userId?: string; }): Promise { - const queryData = await resolveStoryQueryDataForSandbox(params.code, { + const queryData = await backfillMissingQueryDataForSandbox(params.code, { storyId: params.storyId, chatId: params.chatId, projectId: params.projectId, diff --git a/apps/backend/src/mcp/tools/context-layer.ts b/apps/backend/src/mcp/tools/context-layer.ts index d3a79dcb4..0d24679fb 100644 --- a/apps/backend/src/mcp/tools/context-layer.ts +++ b/apps/backend/src/mcp/tools/context-layer.ts @@ -12,7 +12,7 @@ import { upsertMcpQueryData } from '../../queries/mcp-query-data.queries'; import * as storyQueries from '../../queries/story.queries'; import * as storyFolderQueries from '../../queries/story-folder.queries'; import { pinQueryDataToChat, pinStoryMessageToChat } from '../../utils/chat-message-story'; -import { resolveStoryQueryData, type StoryQueryDataMap } from '../../utils/story-query-data'; +import { backfillMissingQueryData, type StoryQueryDataMap } from '../../utils/story-query-data'; import { STORY_OUTPUT_SCHEMA, type StoryMcpToolPayload } from '../embed/embed-tool-result'; import { STORY_APP_URI, uiToolMeta } from '../embed/ui-resources'; import type { McpContext } from '../logging'; @@ -273,7 +273,7 @@ async function cacheStoryQueryData( ...((existingCache?.queryData as StoryQueryDataMap | null) ?? {}), ...(queryData ?? {}), }; - const resolvedQueryData = await resolveStoryQueryData( + const resolvedQueryData = await backfillMissingQueryData( code, Object.keys(seededQueryData).length > 0 ? seededQueryData : null, { projectId: ctx.projectId, userId: ctx.userId }, diff --git a/apps/backend/src/services/live-story.ts b/apps/backend/src/services/live-story.ts index 6ac30079f..5528ff0d9 100644 --- a/apps/backend/src/services/live-story.ts +++ b/apps/backend/src/services/live-story.ts @@ -5,6 +5,7 @@ import { z } from 'zod'; import { llmTelemetry } from '../agents/telemetry'; import { LiveStoryRefreshPrompt } from '../components/ai/live-story-refresh-prompt'; +import type { DBStoryDataCache } from '../db/abstractSchema'; import { env } from '../env'; import { renderToMarkdown } from '../lib/markdown'; import * as chatQueries from '../queries/chat.queries'; @@ -13,7 +14,7 @@ import * as llmConfigQueries from '../queries/project-llm-config.queries'; import { getQueryDataFromCode } from '../queries/shared-story.queries'; import * as storyQueries from '../queries/story.queries'; import { getDefaultModelId, resolveProviderModel } from '../utils/llm'; -import { resolveStoryQueryData } from '../utils/story-query-data'; +import { backfillMissingQueryData, findMissingQueryIds } from '../utils/story-query-data'; import { MAX_OUTPUT_TOKENS } from './agent'; const MAX_RENDERED_ROWS = 60; @@ -106,8 +107,7 @@ export async function getStoryQueryData( const cache = await storyQueries.getStoryDataCacheByChatAndSlug(chatId, slug); if (cache && !isCacheExpired(cache.cachedAt, cacheSchedule)) { - const queryData = await resolveStoryQueryData(code, cache.queryData, { chatId }); - return { queryData, cachedAt: cache.cachedAt }; + return resolveFromCache(chatId, code, cache); } try { @@ -118,13 +118,19 @@ export async function getStoryQueryData( }; } catch { if (cache) { - const queryData = await resolveStoryQueryData(code, cache.queryData, { chatId }); - return { queryData, cachedAt: cache.cachedAt }; + return resolveFromCache(chatId, code, cache); } return { queryData: await getQueryDataFromCode(chatId, code), cachedAt: null }; } } +async function resolveFromCache(chatId: string, code: string, cache: DBStoryDataCache): Promise { + const missing = findMissingQueryIds(code, cache.queryData); + const queryData = + missing.length > 0 ? await backfillMissingQueryData(code, cache.queryData, { chatId }) : cache.queryData; + return { queryData, cachedAt: cache.cachedAt }; +} + async function executeRawSql( sqlQuery: string, projectFolder: string, diff --git a/apps/backend/src/trpc/story.routes.ts b/apps/backend/src/trpc/story.routes.ts index 857316671..c46df87ea 100644 --- a/apps/backend/src/trpc/story.routes.ts +++ b/apps/backend/src/trpc/story.routes.ts @@ -17,7 +17,7 @@ import { executeLiveQuery, getStoryQueryData, refreshStoryData } from '../servic import { nextCronTick } from '../services/scheduler.service'; import { logAnalyticsEvent } from '../utils/analytics-event'; import { buildDownloadResponse } from '../utils/story-download'; -import { resolveStoryQueryData } from '../utils/story-query-data'; +import { backfillMissingQueryData } from '../utils/story-query-data'; import { extractStorySummary } from '../utils/story-summary'; import { canSendProcedure, ownedResourceProcedure, projectProtectedProcedure, protectedProcedure } from './trpc'; @@ -123,7 +123,7 @@ export const storyRoutes = { } const queryData = story.chatId - ? await resolveStoryQueryData(story.code, cache?.queryData ?? null, { chatId: story.chatId }) + ? await backfillMissingQueryData(story.code, cache?.queryData ?? null, { chatId: story.chatId }) : (cache?.queryData ?? null); return { ...story, queryData }; diff --git a/apps/backend/src/utils/embed-story.ts b/apps/backend/src/utils/embed-story.ts index 80a959daf..93a369d25 100644 --- a/apps/backend/src/utils/embed-story.ts +++ b/apps/backend/src/utils/embed-story.ts @@ -4,7 +4,7 @@ import * as projectQueries from '../queries/project.queries'; import * as storyQueries from '../queries/story.queries'; import { assertProjectMcpEnabled, verifyEmbedToken } from './embed-token'; import { HandlerError } from './error'; -import { resolveStoryQueryDataForSandbox, type StoryQueryDataMap } from './story-query-data'; +import { backfillMissingQueryDataForSandbox, type StoryQueryDataMap } from './story-query-data'; export type EmbedStoryContent = { storyId: string; @@ -46,7 +46,7 @@ export async function loadEmbedStoryContent(storyId: string, token: string): Pro } const [queryData, displaySettings] = await Promise.all([ - resolveStoryQueryDataForSandbox(version.code, { + backfillMissingQueryDataForSandbox(version.code, { storyId, chatId: version.chatId, projectId, diff --git a/apps/backend/src/utils/story-query-data.ts b/apps/backend/src/utils/story-query-data.ts index 3fed6530e..2c27fe91d 100644 --- a/apps/backend/src/utils/story-query-data.ts +++ b/apps/backend/src/utils/story-query-data.ts @@ -8,7 +8,7 @@ export type StoryQueryDataMap = Record { @@ -27,15 +27,19 @@ export async function resolveStoryQueryDataForSandbox( } } const seeded = Object.keys(seed).length > 0 ? seed : null; - return resolveStoryQueryData(code, seeded, { projectId: opts.projectId, userId: opts.userId }); + return backfillMissingQueryData(code, seeded, { projectId: opts.projectId, userId: opts.userId }); } -export async function resolveStoryQueryData( +export function findMissingQueryIds(code: string, cachedQueryData: StoryQueryDataMap | null): string[] { + return [...extractQueryIds(code)].filter((id) => !cachedQueryData?.[id]); +} + +export async function backfillMissingQueryData( code: string, cachedQueryData: StoryQueryDataMap | null, source: StoryQueryDataSource, ): Promise { - const missing = [...extractQueryIds(code)].filter((id) => !cachedQueryData?.[id]); + const missing = findMissingQueryIds(code, cachedQueryData); if (missing.length === 0) { return cachedQueryData; }