@agent-native/core 0.141.5 → 0.141.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +3 -14
- package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +20 -8
- package/corpus/templates/analytics/.agents/skills/prometheus/SKILL.md +5 -5
- package/corpus/templates/analytics/AGENTS.md +2 -2
- package/corpus/templates/analytics/actions/compose-dashboard.ts +1 -1
- package/corpus/templates/analytics/actions/get-first-party-analytics-health.ts +15 -1
- package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +135 -0
- package/corpus/templates/analytics/actions/navigate.ts +1 -1
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +2 -2
- package/corpus/templates/analytics/actions/update-dashboard.ts +0 -1
- package/corpus/templates/analytics/actions/view-screen.ts +0 -20
- package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +5 -5
- package/corpus/templates/analytics/app/i18n-data.ts +330 -90
- package/corpus/templates/analytics/app/pages/Agents.tsx +0 -3
- package/corpus/templates/analytics/app/pages/DataSources.tsx +66 -30
- package/corpus/templates/analytics/app/pages/Settings.tsx +22 -43
- package/corpus/templates/analytics/app/pages/settings/settings-search.ts +23 -20
- package/corpus/templates/analytics/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/analytics/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/analytics/app/routes/team.tsx +1 -1
- package/corpus/templates/analytics/changelog/2026-08-05-historical-analytics-rollups-now-repair-automatically-after-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-05-settings-no-longer-includes-an-unnecessary-about-section.md +6 -0
- package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +1 -1
- package/corpus/templates/analytics/netlify.toml +4 -0
- package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +109 -0
- package/corpus/templates/analytics/scripts/first-party-analytics-bigquery.sql +104 -0
- package/corpus/templates/analytics/server/jobs/analytics-rollup-backfill.ts +342 -0
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +0 -2
- package/corpus/templates/analytics/server/lib/analytics-rollup-lock.ts +8 -0
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +11 -29
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +55 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +1058 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +151 -46
- package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +15 -9
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +59 -17
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +1 -3
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +2 -4
- package/corpus/templates/analytics/server/plugins/analytics-rollup-backfill-jobs.ts +61 -0
- package/corpus/templates/analytics/server/plugins/db.ts +58 -187
- package/corpus/templates/analytics/server/routes/api/analytics-rollups/backfill.post.ts +53 -0
- package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +8 -6
- package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +16 -1
- package/corpus/templates/assets/AGENTS.md +1 -0
- package/corpus/templates/assets/actions/_helpers.ts +12 -0
- package/corpus/templates/assets/actions/generate-image.ts +9 -3
- package/corpus/templates/assets/actions/import-style-from-url.ts +115 -0
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +3 -1
- package/corpus/templates/assets/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/assets/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/assets/app/routes/settings.tsx +5 -0
- package/corpus/templates/assets/app/routes/team.tsx +1 -1
- package/corpus/templates/assets/changelog/2026-08-05-brand-kits-can-import-a-website-s-hydrated-visual-language-a.md +6 -0
- package/corpus/templates/assets/changelog/2026-08-05-generated-images-now-hand-off-embeddable-previews-to-other-w.md +6 -0
- package/corpus/templates/assets/server/lib/generation.ts +58 -0
- package/corpus/templates/assets/shared/api.ts +12 -0
- package/corpus/templates/brain/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/brain/app/routes/team.tsx +1 -1
- package/corpus/templates/brain/app/routes.ts +1 -1
- package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +3 -1
- package/corpus/templates/calendar/app/routes/_app.extensions._index.tsx +1 -1
- package/corpus/templates/calendar/app/routes/_app.settings.$.tsx +1 -0
- package/corpus/templates/calendar/app/routes/_app.team.tsx +1 -1
- package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/chat/app/hooks/use-navigation-state.ts +1 -1
- package/corpus/templates/chat/app/root.tsx +1 -1
- package/corpus/templates/chat/app/routes/agent.tsx +7 -7
- package/corpus/templates/chat/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/chat/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/chat/app/routes/team.tsx +1 -1
- package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +1 -1
- package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +1 -1
- package/corpus/templates/clips/app/routes/_app.extensions._index.tsx +1 -1
- package/corpus/templates/clips/app/routes/_app.settings.$.tsx +1 -0
- package/corpus/templates/clips/changelog/2026-08-05-clips-overlays-now-use-clean-surfaces-without-clipped-shadow.md +6 -0
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +2 -2
- package/corpus/templates/clips/desktop/src/styles.css +3 -54
- package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +3 -3
- package/corpus/templates/content/app/routes/_app.extensions._index.tsx +1 -1
- package/corpus/templates/content/app/routes/_app.settings.$.tsx +1 -0
- package/corpus/templates/content/app/routes/_app.team.tsx +1 -1
- package/corpus/templates/content/e2e/global-setup.ts +1 -1
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +6 -1
- package/corpus/templates/design/actions/analyze-brand-assets.ts +7 -7
- package/corpus/templates/design/actions/get-design-system.ts +3 -1
- package/corpus/templates/design/actions/import-from-url.ts +6 -5
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +2 -2
- package/corpus/templates/design/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/design/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/design/app/routes/team.tsx +1 -1
- package/corpus/templates/design/changelog/2026-08-05-missing-design-systems-now-return-a-clear-not-found-response.md +6 -0
- package/corpus/templates/design/changelog/2026-08-05-website-url-imports-now-capture-hydrated-browser-computed-de.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +1 -1
- package/corpus/templates/dispatch/app/hooks/use-navigation-state.ts +1 -1
- package/corpus/templates/dispatch/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/dispatch/app/routes/team.tsx +1 -1
- package/corpus/templates/factory/app/hooks/use-navigation-state.ts +1 -1
- package/corpus/templates/factory/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/factory/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/factory/app/routes/team.tsx +1 -1
- package/corpus/templates/forms/app/routes/_app.extensions._index.tsx +1 -1
- package/corpus/templates/forms/app/routes/_app.settings.$.tsx +1 -0
- package/corpus/templates/forms/app/routes/_app.team.tsx +1 -1
- package/corpus/templates/forms/shared/navigation.ts +1 -1
- package/corpus/templates/macros/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/macros/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/macros/app/routes/team.tsx +1 -1
- package/corpus/templates/mail/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/mail/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/plan/app/hooks/use-navigation-state.ts +1 -1
- package/corpus/templates/plan/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/plan/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/plan/app/routes/team.tsx +1 -1
- package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +3 -1
- package/corpus/templates/slides/actions/analyze-brand-assets.ts +7 -7
- package/corpus/templates/slides/actions/import-from-url.ts +6 -6
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +2 -2
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +3 -0
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1 -1
- package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +9 -3
- package/corpus/templates/slides/app/context/DeckContext.tsx +71 -20
- package/corpus/templates/slides/app/global.css +6 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +5 -5
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +9 -5
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +17 -0
- package/corpus/templates/slides/app/pages/Index.tsx +16 -5
- package/corpus/templates/slides/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/slides/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/slides/app/routes/team.tsx +1 -1
- package/corpus/templates/slides/changelog/2026-08-05-failed-deck-creation-and-reference-uploads-now-recover-clean.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-05-new-presentation-prompts-now-clearly-describe-presentation-g.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-05-slides-recover-completed-generation-results-after-a-sync-hic.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-05-website-url-imports-now-capture-hydrated-browser-computed-st.md +6 -0
- package/corpus/templates/tasks/app/components/layout/Header.tsx +3 -1
- package/corpus/templates/tasks/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/tasks/app/routes/settings.$.tsx +1 -0
- package/dist/agent/engine/ai-sdk-engine.js +49 -3
- package/dist/agent/production-agent.d.ts +1 -0
- package/dist/agent/production-agent.js +8 -0
- package/dist/agent/types.d.ts +7 -0
- package/dist/client/AssistantChat.js +1 -1
- package/dist/client/agent-chat-adapter.d.ts +1 -1
- package/dist/client/agent-chat-adapter.js +30 -3
- package/dist/client/agent-page/AgentWorkspaceContent.d.ts +1 -1
- package/dist/client/agent-page/AgentWorkspaceContent.js +13 -13
- package/dist/client/chat/run-recovery.d.ts +2 -1
- package/dist/client/chat/run-recovery.js +14 -3
- package/dist/client/embed-auth.js +6 -2
- package/dist/client/error-format.d.ts +1 -0
- package/dist/client/error-format.js +16 -2
- package/dist/client/integrations/IntegrationsPanel.js +40 -6
- package/dist/client/org/OrgSwitcher.js +2 -2
- package/dist/client/org/TeamPage.js +92 -72
- package/dist/client/require-session.d.ts +1 -1
- package/dist/client/require-session.js +3 -3
- package/dist/client/resources/use-mcp-servers.d.ts +5 -0
- package/dist/client/resources/use-mcp-servers.js +36 -0
- package/dist/client/settings/AccountSettingsCard.js +40 -35
- package/dist/client/settings/AutomationsSection.js +1 -1
- package/dist/client/settings/DemoModeSection.d.ts +3 -1
- package/dist/client/settings/DemoModeSection.js +4 -1
- package/dist/client/settings/ExtensionsSettingsContent.js +1 -1
- package/dist/client/settings/SchedulingTimezoneField.d.ts +3 -1
- package/dist/client/settings/SchedulingTimezoneField.js +9 -5
- package/dist/client/settings/SettingsPanel.d.ts +4 -1
- package/dist/client/settings/SettingsPanel.js +21 -21
- package/dist/client/settings/SettingsTabsPage.d.ts +2 -2
- package/dist/client/settings/SettingsTabsPage.js +98 -45
- package/dist/client/settings/VoiceTranscriptionSection.js +6 -1
- package/dist/client/settings/useSettingsPanelController.js +47 -8
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/ingestion/website.d.ts +30 -0
- package/dist/localization/default-messages.d.ts +5 -0
- package/dist/localization/default-messages.js +5 -0
- package/dist/mcp-client/oauth-routes.js +1 -1
- package/dist/mcp-client/routes.d.ts +1 -0
- package/dist/mcp-client/routes.js +49 -0
- package/dist/navigation/index.js +16 -5
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/scripts/call-agent.js +11 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.js +9 -7
- package/dist/server/identity-sso.js +2 -1
- package/dist/server/onboarding-html.js +57 -34
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/server/workspace-provider-oauth.js +1 -1
- package/dist/setup-connections/onboarding.js +1 -1
- package/dist/shared/index.d.ts +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/sign-in-journey.d.ts +5 -2
- package/dist/shared/sign-in-journey.js +10 -4
- package/dist/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/dist/templates/chat/app/hooks/use-navigation-state.ts +1 -1
- package/dist/templates/chat/app/root.tsx +1 -1
- package/dist/templates/chat/app/routes/agent.tsx +7 -7
- package/dist/templates/chat/app/routes/extensions._index.tsx +1 -1
- package/dist/templates/chat/app/routes/settings.$.tsx +1 -0
- package/dist/templates/chat/app/routes/team.tsx +1 -1
- package/dist/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +37 -6
- package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +4 -2
- package/docs/content/authentication.mdx +4 -5
- package/docs/content/template-analytics-developers.mdx +1 -1
- package/docs/content/template-calendar-developers.mdx +1 -1
- package/docs/content/template-chat-developers.mdx +2 -2
- package/docs/content/template-content-developers.mdx +1 -1
- package/docs/content/template-mail-developers.mdx +1 -1
- package/docs/content/template-plan-developers.mdx +1 -1
- package/docs/content/toolkit-command-navigation.mdx +6 -6
- package/docs/content/toolkit-org-team.mdx +1 -1
- package/docs/content/toolkit-settings.mdx +8 -8
- package/docs/content/toolkit-setup-connections.mdx +1 -1
- package/package.json +1 -1
- package/src/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/src/templates/chat/app/hooks/use-navigation-state.ts +1 -1
- package/src/templates/chat/app/root.tsx +1 -1
- package/src/templates/chat/app/routes/agent.tsx +7 -7
- package/src/templates/chat/app/routes/extensions._index.tsx +1 -1
- package/src/templates/chat/app/routes/settings.$.tsx +1 -0
- package/src/templates/chat/app/routes/team.tsx +1 -1
- package/src/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +37 -6
- package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +4 -2
- package/corpus/templates/analytics/.agents/skills/dashboard-management/references/template-catalog-and-demo.md +0 -62
- package/corpus/templates/analytics/actions/install-dashboard-template.ts +0 -369
- package/corpus/templates/analytics/actions/list-dashboard-templates.ts +0 -47
- package/corpus/templates/analytics/actions/list-native-v2-dashboard-templates.ts +0 -34
- package/corpus/templates/analytics/app/routes/catalog.tsx +0 -240
- package/corpus/templates/analytics/changelog/2026-08-05-historical-analytics-rollups-stay-complete-and-atomic.md +0 -6
- package/corpus/templates/analytics/changelog/2026-08-05-historical-rollups-stay-complete-while-live-events-arrive.md +0 -6
- package/src/templates/default/actions/navigate.spec.ts +0 -53
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { getDbExec, getDialect, isPostgres } from "@agent-native/core/db";
|
|
2
|
+
|
|
3
|
+
import { FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY } from "../lib/analytics-rollup-lock";
|
|
4
|
+
|
|
5
|
+
const BACKFILL_STATE_ID = "historical-v1";
|
|
6
|
+
const BACKFILL_LEASE_MINUTES = 15;
|
|
7
|
+
|
|
8
|
+
const POSTGRES_BACKFILL_STATEMENTS = [
|
|
9
|
+
`
|
|
10
|
+
INSERT INTO analytics_event_daily_rollups (
|
|
11
|
+
id, tenant_key, owner_email, org_id, event_date, event_name,
|
|
12
|
+
app, template, event_count
|
|
13
|
+
)
|
|
14
|
+
SELECT
|
|
15
|
+
md5(random()::text || clock_timestamp()::text), tenant_key,
|
|
16
|
+
MIN(owner_email), MIN(org_id), event_date, event_name, app, template,
|
|
17
|
+
COUNT(*)::INTEGER
|
|
18
|
+
FROM (
|
|
19
|
+
SELECT
|
|
20
|
+
CASE
|
|
21
|
+
WHEN org_id IS NOT NULL AND org_id <> '' THEN 'org:' || org_id
|
|
22
|
+
ELSE 'user:' || owner_email
|
|
23
|
+
END AS tenant_key,
|
|
24
|
+
owner_email,
|
|
25
|
+
org_id,
|
|
26
|
+
COALESCE(NULLIF(event_date, ''), substr(timestamp, 1, 10)) AS event_date,
|
|
27
|
+
event_name,
|
|
28
|
+
COALESCE(app, '') AS app,
|
|
29
|
+
COALESCE(template, '') AS template
|
|
30
|
+
FROM analytics_events
|
|
31
|
+
) AS historical_events
|
|
32
|
+
WHERE event_date <> ''
|
|
33
|
+
GROUP BY tenant_key, event_date, event_name, app, template
|
|
34
|
+
ON CONFLICT (tenant_key, event_date, event_name, app, template)
|
|
35
|
+
DO UPDATE SET event_count = GREATEST(
|
|
36
|
+
analytics_event_daily_rollups.event_count,
|
|
37
|
+
EXCLUDED.event_count
|
|
38
|
+
)
|
|
39
|
+
`,
|
|
40
|
+
`
|
|
41
|
+
INSERT INTO analytics_user_days (
|
|
42
|
+
id, tenant_key, owner_email, org_id, event_date, user_key
|
|
43
|
+
)
|
|
44
|
+
SELECT
|
|
45
|
+
md5(random()::text || clock_timestamp()::text), tenant_key,
|
|
46
|
+
owner_email, org_id, event_date, user_key
|
|
47
|
+
FROM (
|
|
48
|
+
SELECT DISTINCT
|
|
49
|
+
CASE
|
|
50
|
+
WHEN org_id IS NOT NULL AND org_id <> '' THEN 'org:' || org_id
|
|
51
|
+
ELSE 'user:' || owner_email
|
|
52
|
+
END AS tenant_key,
|
|
53
|
+
owner_email,
|
|
54
|
+
org_id,
|
|
55
|
+
COALESCE(NULLIF(event_date, ''), substr(timestamp, 1, 10)) AS event_date,
|
|
56
|
+
COALESCE(
|
|
57
|
+
NULLIF(user_key, ''),
|
|
58
|
+
NULLIF(user_id, ''),
|
|
59
|
+
NULLIF(anonymous_id, '')
|
|
60
|
+
) AS user_key
|
|
61
|
+
FROM analytics_events
|
|
62
|
+
WHERE COALESCE(
|
|
63
|
+
NULLIF(user_key, ''),
|
|
64
|
+
NULLIF(user_id, ''),
|
|
65
|
+
NULLIF(anonymous_id, '')
|
|
66
|
+
) IS NOT NULL
|
|
67
|
+
) AS historical_user_days
|
|
68
|
+
WHERE event_date <> '' AND user_key <> ''
|
|
69
|
+
ON CONFLICT (tenant_key, event_date, user_key) DO NOTHING
|
|
70
|
+
`,
|
|
71
|
+
] as const;
|
|
72
|
+
|
|
73
|
+
const SQLITE_BACKFILL_STATEMENTS = [
|
|
74
|
+
`
|
|
75
|
+
INSERT INTO analytics_event_daily_rollups (
|
|
76
|
+
id, tenant_key, owner_email, org_id, event_date, event_name,
|
|
77
|
+
app, template, event_count
|
|
78
|
+
)
|
|
79
|
+
SELECT
|
|
80
|
+
lower(hex(randomblob(16))), tenant_key, MIN(owner_email), MIN(org_id),
|
|
81
|
+
event_date, event_name, app, template, COUNT(*)
|
|
82
|
+
FROM (
|
|
83
|
+
SELECT
|
|
84
|
+
CASE
|
|
85
|
+
WHEN org_id IS NOT NULL AND org_id <> '' THEN 'org:' || org_id
|
|
86
|
+
ELSE 'user:' || owner_email
|
|
87
|
+
END AS tenant_key,
|
|
88
|
+
owner_email,
|
|
89
|
+
org_id,
|
|
90
|
+
COALESCE(NULLIF(event_date, ''), substr(timestamp, 1, 10)) AS event_date,
|
|
91
|
+
event_name,
|
|
92
|
+
COALESCE(app, '') AS app,
|
|
93
|
+
COALESCE(template, '') AS template
|
|
94
|
+
FROM analytics_events
|
|
95
|
+
) AS historical_events
|
|
96
|
+
WHERE event_date <> ''
|
|
97
|
+
GROUP BY tenant_key, event_date, event_name, app, template
|
|
98
|
+
ON CONFLICT (tenant_key, event_date, event_name, app, template)
|
|
99
|
+
DO UPDATE SET event_count = MAX(
|
|
100
|
+
analytics_event_daily_rollups.event_count,
|
|
101
|
+
excluded.event_count
|
|
102
|
+
)
|
|
103
|
+
`,
|
|
104
|
+
`
|
|
105
|
+
INSERT INTO analytics_user_days (
|
|
106
|
+
id, tenant_key, owner_email, org_id, event_date, user_key
|
|
107
|
+
)
|
|
108
|
+
SELECT
|
|
109
|
+
lower(hex(randomblob(16))), tenant_key, owner_email, org_id,
|
|
110
|
+
event_date, user_key
|
|
111
|
+
FROM (
|
|
112
|
+
SELECT DISTINCT
|
|
113
|
+
CASE
|
|
114
|
+
WHEN org_id IS NOT NULL AND org_id <> '' THEN 'org:' || org_id
|
|
115
|
+
ELSE 'user:' || owner_email
|
|
116
|
+
END AS tenant_key,
|
|
117
|
+
owner_email,
|
|
118
|
+
org_id,
|
|
119
|
+
COALESCE(NULLIF(event_date, ''), substr(timestamp, 1, 10)) AS event_date,
|
|
120
|
+
COALESCE(
|
|
121
|
+
NULLIF(user_key, ''),
|
|
122
|
+
NULLIF(user_id, ''),
|
|
123
|
+
NULLIF(anonymous_id, '')
|
|
124
|
+
) AS user_key
|
|
125
|
+
FROM analytics_events
|
|
126
|
+
WHERE COALESCE(
|
|
127
|
+
NULLIF(user_key, ''),
|
|
128
|
+
NULLIF(user_id, ''),
|
|
129
|
+
NULLIF(anonymous_id, '')
|
|
130
|
+
) IS NOT NULL
|
|
131
|
+
) AS historical_user_days
|
|
132
|
+
WHERE event_date <> '' AND user_key <> ''
|
|
133
|
+
ON CONFLICT (tenant_key, event_date, user_key) DO NOTHING
|
|
134
|
+
`,
|
|
135
|
+
] as const;
|
|
136
|
+
|
|
137
|
+
type BackfillStatus =
|
|
138
|
+
| "completed"
|
|
139
|
+
| "already-complete"
|
|
140
|
+
| "skipped-lock"
|
|
141
|
+
| "already-running";
|
|
142
|
+
|
|
143
|
+
export interface AnalyticsRollupBackfillResult {
|
|
144
|
+
status: BackfillStatus;
|
|
145
|
+
remaining: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let running = false;
|
|
149
|
+
|
|
150
|
+
function nowSql(): string {
|
|
151
|
+
return isPostgres() ? "now()::text" : "datetime('now')";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function ensureStateSql(): string {
|
|
155
|
+
return isPostgres()
|
|
156
|
+
? `INSERT INTO analytics_rollup_backfill_state (id, status, updated_at)
|
|
157
|
+
VALUES (?, 'pending', ${nowSql()})
|
|
158
|
+
ON CONFLICT (id) DO NOTHING`
|
|
159
|
+
: `INSERT OR IGNORE INTO analytics_rollup_backfill_state
|
|
160
|
+
(id, status, updated_at)
|
|
161
|
+
VALUES (?, 'pending', ${nowSql()})`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createBackfillLeaseToken(): string {
|
|
165
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
166
|
+
return globalThis.crypto.randomUUID();
|
|
167
|
+
}
|
|
168
|
+
return `analytics-rollup-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function claimStateSql(): string {
|
|
172
|
+
return `UPDATE analytics_rollup_backfill_state
|
|
173
|
+
SET status = 'running', lease_token = ?,
|
|
174
|
+
lease_expires_at = datetime('now', '+${BACKFILL_LEASE_MINUTES} minutes'),
|
|
175
|
+
updated_at = datetime('now')
|
|
176
|
+
WHERE id = ?
|
|
177
|
+
AND (
|
|
178
|
+
status = 'pending'
|
|
179
|
+
OR (
|
|
180
|
+
status = 'running'
|
|
181
|
+
AND (
|
|
182
|
+
lease_expires_at IS NULL
|
|
183
|
+
OR lease_expires_at <= datetime('now')
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
)`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function releaseStateLeaseSql(): string {
|
|
190
|
+
return `UPDATE analytics_rollup_backfill_state
|
|
191
|
+
SET status = 'pending', lease_token = NULL,
|
|
192
|
+
lease_expires_at = NULL, updated_at = datetime('now')
|
|
193
|
+
WHERE id = ? AND lease_token = ?`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function stateStatus(db: {
|
|
197
|
+
execute: (query: { sql: string; args: unknown[] }) => Promise<{
|
|
198
|
+
rows: any[];
|
|
199
|
+
rowsAffected: number;
|
|
200
|
+
}>;
|
|
201
|
+
}): Promise<string | null> {
|
|
202
|
+
const result = await db.execute({
|
|
203
|
+
sql: "SELECT status FROM analytics_rollup_backfill_state WHERE id = ?",
|
|
204
|
+
args: [BACKFILL_STATE_ID],
|
|
205
|
+
});
|
|
206
|
+
const status = result.rows[0]?.status;
|
|
207
|
+
return typeof status === "string" ? status : null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function completeState(tx: {
|
|
211
|
+
execute: (query: { sql: string; args: unknown[] }) => Promise<unknown>;
|
|
212
|
+
}): Promise<void> {
|
|
213
|
+
await tx.execute({
|
|
214
|
+
sql: `UPDATE analytics_rollup_backfill_state
|
|
215
|
+
SET status = 'completed', completed_at = ${nowSql()}, updated_at = ${nowSql()}
|
|
216
|
+
WHERE id = ?`,
|
|
217
|
+
args: [BACKFILL_STATE_ID],
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function runBackfillStatements(tx: {
|
|
222
|
+
execute: (query: string) => Promise<unknown>;
|
|
223
|
+
}): Promise<void> {
|
|
224
|
+
const statements = isPostgres()
|
|
225
|
+
? POSTGRES_BACKFILL_STATEMENTS
|
|
226
|
+
: SQLITE_BACKFILL_STATEMENTS;
|
|
227
|
+
for (const statement of statements) await tx.execute(statement);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function isHistoricalAnalyticsRollupBackfillComplete(): Promise<boolean> {
|
|
231
|
+
return (await stateStatus(getDbExec())) === "completed";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function runTransactionalBackfill(
|
|
235
|
+
db: ReturnType<typeof getDbExec>,
|
|
236
|
+
): Promise<BackfillStatus> {
|
|
237
|
+
if (!db.transaction) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
"Analytics rollup backfill requires a database transaction",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return db.transaction(async (tx) => {
|
|
244
|
+
if (isPostgres()) {
|
|
245
|
+
const lockResult = await tx.execute({
|
|
246
|
+
sql: "SELECT pg_try_advisory_xact_lock(hashtextextended(?, 0::bigint)) AS acquired",
|
|
247
|
+
args: [FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY],
|
|
248
|
+
});
|
|
249
|
+
const acquired = lockResult.rows[0]?.acquired;
|
|
250
|
+
if (acquired !== true && acquired !== "t") return "skipped-lock";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
await tx.execute({ sql: ensureStateSql(), args: [BACKFILL_STATE_ID] });
|
|
254
|
+
if ((await stateStatus(tx)) === "completed") return "already-complete";
|
|
255
|
+
|
|
256
|
+
await runBackfillStatements(tx);
|
|
257
|
+
await completeState(tx);
|
|
258
|
+
return "completed";
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function runD1Backfill(
|
|
263
|
+
db: ReturnType<typeof getDbExec>,
|
|
264
|
+
): Promise<BackfillStatus> {
|
|
265
|
+
if (!db.atomicBatch) {
|
|
266
|
+
throw new Error("D1 Analytics rollup backfill requires an atomic batch");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
await db.execute({ sql: ensureStateSql(), args: [BACKFILL_STATE_ID] });
|
|
270
|
+
const leaseToken = createBackfillLeaseToken();
|
|
271
|
+
const claim = await db.execute({
|
|
272
|
+
sql: claimStateSql(),
|
|
273
|
+
args: [leaseToken, BACKFILL_STATE_ID],
|
|
274
|
+
});
|
|
275
|
+
if (claim.rowsAffected !== 1) {
|
|
276
|
+
return (await stateStatus(db)) === "completed"
|
|
277
|
+
? "already-complete"
|
|
278
|
+
: "skipped-lock";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const results = await db.atomicBatch([
|
|
283
|
+
...SQLITE_BACKFILL_STATEMENTS,
|
|
284
|
+
{
|
|
285
|
+
sql: `UPDATE analytics_rollup_backfill_state
|
|
286
|
+
SET status = 'completed', completed_at = ${nowSql()},
|
|
287
|
+
lease_token = NULL, lease_expires_at = NULL,
|
|
288
|
+
updated_at = ${nowSql()}
|
|
289
|
+
WHERE id = ? AND status = 'running' AND lease_token = ?`,
|
|
290
|
+
args: [BACKFILL_STATE_ID, leaseToken],
|
|
291
|
+
},
|
|
292
|
+
]);
|
|
293
|
+
if (results[results.length - 1]?.rowsAffected !== 1) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
"Analytics rollup backfill lost its D1 lease before completion",
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
return "completed";
|
|
299
|
+
} catch (error) {
|
|
300
|
+
try {
|
|
301
|
+
await db.execute({
|
|
302
|
+
sql: releaseStateLeaseSql(),
|
|
303
|
+
args: [BACKFILL_STATE_ID, leaseToken],
|
|
304
|
+
});
|
|
305
|
+
} catch (releaseError) {
|
|
306
|
+
console.warn(
|
|
307
|
+
"[analytics-rollup-backfill] Failed to release D1 lease after backfill error:",
|
|
308
|
+
releaseError instanceof Error ? releaseError.message : releaseError,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Rebuild historical compact rollups outside the server boot path. The state
|
|
317
|
+
* row is completed in the same transaction as both aggregates, so a timeout
|
|
318
|
+
* or failed write leaves the job pending and a later scheduled run retries it.
|
|
319
|
+
* D1 claims a short lease before its atomic batch so concurrent isolates do not
|
|
320
|
+
* each scan the full event history.
|
|
321
|
+
*/
|
|
322
|
+
export async function runAnalyticsRollupBackfillOnce(): Promise<AnalyticsRollupBackfillResult> {
|
|
323
|
+
if (running) {
|
|
324
|
+
return { status: "already-running", remaining: 1 };
|
|
325
|
+
}
|
|
326
|
+
running = true;
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const db = getDbExec();
|
|
330
|
+
const status =
|
|
331
|
+
getDialect() === "d1"
|
|
332
|
+
? await runD1Backfill(db)
|
|
333
|
+
: await runTransactionalBackfill(db);
|
|
334
|
+
return {
|
|
335
|
+
status,
|
|
336
|
+
remaining:
|
|
337
|
+
status === "completed" || status === "already-complete" ? 0 : 1,
|
|
338
|
+
};
|
|
339
|
+
} finally {
|
|
340
|
+
running = false;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -18,13 +18,11 @@ export const INITIAL_TOOL_NAMES = [
|
|
|
18
18
|
// while the agent lazily discovers the next mutating action.
|
|
19
19
|
"get-sql-dashboard",
|
|
20
20
|
"list-sql-dashboards",
|
|
21
|
-
"list-dashboard-templates",
|
|
22
21
|
"list-extensions",
|
|
23
22
|
"get-extension",
|
|
24
23
|
"update-dashboard",
|
|
25
24
|
"mutate-dashboard",
|
|
26
25
|
"compose-dashboard",
|
|
27
|
-
"install-dashboard-template",
|
|
28
26
|
"create-extension",
|
|
29
27
|
"update-extension",
|
|
30
28
|
"extension-data-set",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared with the historical backfill so live ingest and migration work hold
|
|
3
|
+
* the same transaction-scoped Postgres lock while they update rollups.
|
|
4
|
+
*/
|
|
5
|
+
export const FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_KEY =
|
|
6
|
+
"agent-native:analytics-rollup-backfill";
|
|
7
|
+
export const FIRST_PARTY_ANALYTICS_ROLLUP_LOCK_SQL =
|
|
8
|
+
"SELECT pg_advisory_xact_lock(hashtextextended(?, 0::bigint))";
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { SqlDashboardConfig } from "../../app/pages/adhoc/sql-dashboard/types";
|
|
2
2
|
import { loadDashboardSeed } from "./dashboard-seeds";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
listDashboardSummaries,
|
|
5
|
+
type DashboardRecord,
|
|
6
|
+
type DashboardSummaryRecord,
|
|
7
|
+
} from "./dashboards-store";
|
|
4
8
|
import {
|
|
5
9
|
FIRST_PARTY_DASHBOARD_ID,
|
|
6
10
|
repairFirstPartyObservedRetentionPanels,
|
|
@@ -1021,33 +1025,13 @@ export function cloneDashboardConfig(
|
|
|
1021
1025
|
return JSON.parse(JSON.stringify(entry.buildConfig())) as SqlDashboardConfig;
|
|
1022
1026
|
}
|
|
1023
1027
|
|
|
1024
|
-
function templateIdFromConfig(config: Record<string, unknown>): string | null {
|
|
1025
|
-
const catalog = config.catalog;
|
|
1026
|
-
if (!catalog || typeof catalog !== "object" || Array.isArray(catalog)) {
|
|
1027
|
-
return null;
|
|
1028
|
-
}
|
|
1029
|
-
const templateId = (catalog as Record<string, unknown>).templateId;
|
|
1030
|
-
return typeof templateId === "string" && templateId ? templateId : null;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
function demoIdFromConfig(config: Record<string, unknown>): string | null {
|
|
1034
|
-
const demo = config.demo;
|
|
1035
|
-
if (!demo || typeof demo !== "object" || Array.isArray(demo)) {
|
|
1036
|
-
return null;
|
|
1037
|
-
}
|
|
1038
|
-
const demoId = (demo as Record<string, unknown>).id;
|
|
1039
|
-
return typeof demoId === "string" && demoId ? demoId : null;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
1028
|
function installedDashboardForTemplate(
|
|
1043
|
-
row:
|
|
1029
|
+
row: DashboardSummaryRecord,
|
|
1044
1030
|
entry: DashboardCatalogEntry,
|
|
1045
1031
|
): boolean {
|
|
1046
|
-
const templateId = templateIdFromConfig(row.config);
|
|
1047
|
-
const demoId = demoIdFromConfig(row.config);
|
|
1048
1032
|
return (
|
|
1049
|
-
|
|
1050
|
-
demoId === entry.id ||
|
|
1033
|
+
row.catalogTemplateId === entry.id ||
|
|
1034
|
+
row.demoId === entry.id ||
|
|
1051
1035
|
row.id === entry.defaultDashboardId
|
|
1052
1036
|
);
|
|
1053
1037
|
}
|
|
@@ -1055,10 +1039,11 @@ function installedDashboardForTemplate(
|
|
|
1055
1039
|
export async function listDashboardCatalog(
|
|
1056
1040
|
ctx: AccessCtx,
|
|
1057
1041
|
): Promise<DashboardCatalogTemplate[]> {
|
|
1058
|
-
const dashboards = await
|
|
1042
|
+
const dashboards = await listDashboardSummaries(ctx, {
|
|
1059
1043
|
kind: "sql",
|
|
1060
1044
|
archived: "all",
|
|
1061
1045
|
hidden: "all",
|
|
1046
|
+
includeCatalogMetadata: true,
|
|
1062
1047
|
});
|
|
1063
1048
|
|
|
1064
1049
|
return dashboardCatalogEntries
|
|
@@ -1068,10 +1053,7 @@ export async function listDashboardCatalog(
|
|
|
1068
1053
|
.filter((row) => installedDashboardForTemplate(row, entry))
|
|
1069
1054
|
.map((row) => ({
|
|
1070
1055
|
id: row.id,
|
|
1071
|
-
name:
|
|
1072
|
-
typeof row.config.name === "string" && row.config.name.trim()
|
|
1073
|
-
? row.config.name
|
|
1074
|
-
: row.title,
|
|
1056
|
+
name: row.configName?.trim() || row.name,
|
|
1075
1057
|
visibility: row.visibility,
|
|
1076
1058
|
updatedAt: row.updatedAt,
|
|
1077
1059
|
archivedAt: row.archivedAt,
|
|
@@ -62,6 +62,9 @@ export interface DashboardSummaryRecord {
|
|
|
62
62
|
id: string;
|
|
63
63
|
kind: DashboardKind;
|
|
64
64
|
name: string;
|
|
65
|
+
configName: string | null;
|
|
66
|
+
catalogTemplateId: string | null;
|
|
67
|
+
demoId: string | null;
|
|
65
68
|
parentId: string | null;
|
|
66
69
|
ownerEmail: string;
|
|
67
70
|
orgId: string | null;
|
|
@@ -287,6 +290,24 @@ function configFromSettings(data: Record<string, unknown>): {
|
|
|
287
290
|
return { title, config: data };
|
|
288
291
|
}
|
|
289
292
|
|
|
293
|
+
function stringProperty(value: unknown, key: string): string | null {
|
|
294
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
295
|
+
const property = (value as Record<string, unknown>)[key];
|
|
296
|
+
return typeof property === "string" ? property : null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function catalogMetadataFromConfig(config: Record<string, unknown>): {
|
|
300
|
+
configName: string | null;
|
|
301
|
+
catalogTemplateId: string | null;
|
|
302
|
+
demoId: string | null;
|
|
303
|
+
} {
|
|
304
|
+
return {
|
|
305
|
+
configName: typeof config.name === "string" ? config.name : null,
|
|
306
|
+
catalogTemplateId: stringProperty(config.catalog, "templateId"),
|
|
307
|
+
demoId: stringProperty(config.demo, "id"),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
290
311
|
async function migrateDashboardFromSettings(
|
|
291
312
|
id: string,
|
|
292
313
|
kind: DashboardKind,
|
|
@@ -510,11 +531,13 @@ export async function listDashboardSummaries(
|
|
|
510
531
|
kind?: DashboardKind;
|
|
511
532
|
archived?: DashboardArchiveFilter;
|
|
512
533
|
hidden?: DashboardHiddenFilter;
|
|
534
|
+
includeCatalogMetadata?: boolean;
|
|
513
535
|
},
|
|
514
536
|
): Promise<DashboardSummaryRecord[]> {
|
|
515
537
|
const db = getDb() as any;
|
|
516
538
|
const archived = filter?.archived ?? "active";
|
|
517
539
|
const hidden = filter?.hidden ?? "visible";
|
|
540
|
+
const includeCatalogMetadata = filter?.includeCatalogMetadata === true;
|
|
518
541
|
const conditions: any[] = [
|
|
519
542
|
accessFilter(schema.dashboards, schema.dashboardShares, {
|
|
520
543
|
userEmail: ctx.email,
|
|
@@ -535,11 +558,31 @@ export async function listDashboardSummaries(
|
|
|
535
558
|
: sql<
|
|
536
559
|
string | null
|
|
537
560
|
>`json_extract(${schema.dashboards.config}, '$.parentId')`;
|
|
561
|
+
const configName = isPostgres()
|
|
562
|
+
? sql<string | null>`(${schema.dashboards.config}::jsonb ->> 'name')`
|
|
563
|
+
: sql<string | null>`json_extract(${schema.dashboards.config}, '$.name')`;
|
|
564
|
+
const catalogTemplateId = isPostgres()
|
|
565
|
+
? sql<
|
|
566
|
+
string | null
|
|
567
|
+
>`(${schema.dashboards.config}::jsonb -> 'catalog' ->> 'templateId')`
|
|
568
|
+
: sql<
|
|
569
|
+
string | null
|
|
570
|
+
>`json_extract(${schema.dashboards.config}, '$.catalog.templateId')`;
|
|
571
|
+
const demoId = isPostgres()
|
|
572
|
+
? sql<
|
|
573
|
+
string | null
|
|
574
|
+
>`(${schema.dashboards.config}::jsonb -> 'demo' ->> 'id')`
|
|
575
|
+
: sql<
|
|
576
|
+
string | null
|
|
577
|
+
>`json_extract(${schema.dashboards.config}, '$.demo.id')`;
|
|
538
578
|
const rows = await db
|
|
539
579
|
.select({
|
|
540
580
|
id: schema.dashboards.id,
|
|
541
581
|
kind: schema.dashboards.kind,
|
|
542
582
|
name: schema.dashboards.title,
|
|
583
|
+
...(includeCatalogMetadata
|
|
584
|
+
? { configName, catalogTemplateId, demoId }
|
|
585
|
+
: {}),
|
|
543
586
|
parentId,
|
|
544
587
|
ownerEmail: schema.dashboards.ownerEmail,
|
|
545
588
|
orgId: schema.dashboards.orgId,
|
|
@@ -554,6 +597,10 @@ export async function listDashboardSummaries(
|
|
|
554
597
|
.where(where);
|
|
555
598
|
const out: DashboardSummaryRecord[] = rows.map((row: any) => ({
|
|
556
599
|
...row,
|
|
600
|
+
configName: typeof row.configName === "string" ? row.configName : null,
|
|
601
|
+
catalogTemplateId:
|
|
602
|
+
typeof row.catalogTemplateId === "string" ? row.catalogTemplateId : null,
|
|
603
|
+
demoId: typeof row.demoId === "string" ? row.demoId : null,
|
|
557
604
|
parentId: typeof row.parentId === "string" ? row.parentId : null,
|
|
558
605
|
orgId: row.orgId ?? null,
|
|
559
606
|
archivedAt: row.archivedAt ?? null,
|
|
@@ -590,12 +637,20 @@ export async function listDashboardSummaries(
|
|
|
590
637
|
seen.add(id);
|
|
591
638
|
const config = value as Record<string, unknown>;
|
|
592
639
|
const { title } = configFromSettings(config);
|
|
640
|
+
const catalogMetadata = includeCatalogMetadata
|
|
641
|
+
? catalogMetadataFromConfig(config)
|
|
642
|
+
: {
|
|
643
|
+
configName: null,
|
|
644
|
+
catalogTemplateId: null,
|
|
645
|
+
demoId: null,
|
|
646
|
+
};
|
|
593
647
|
const createdAt =
|
|
594
648
|
typeof config.createdAt === "string" ? config.createdAt : nowIso();
|
|
595
649
|
out.push({
|
|
596
650
|
id,
|
|
597
651
|
kind,
|
|
598
652
|
name: title,
|
|
653
|
+
...catalogMetadata,
|
|
599
654
|
parentId: typeof config.parentId === "string" ? config.parentId : null,
|
|
600
655
|
ownerEmail: ctx.email,
|
|
601
656
|
orgId,
|