@growthub/cli 0.14.10 → 0.14.11
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/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/callback/route.js +35 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/failure/route.js +35 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/schedule/route.js +423 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/connect/route.js +78 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/credentials/route.js +276 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/products/[productId]/resources/route.js +173 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/products/sync/route.js +347 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/sync/route.js +293 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/connect/route.js +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/sync/route.js +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/sync/route.js +197 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/apps/route.js +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +1 -49
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +3 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-api-record/route.js +3 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflow/publish/route.js +407 -290
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflows/[providerId]/route.js +209 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceAddOnsMarketplace.jsx +806 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +141 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/CeoCockpit.jsx +15 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +42 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +5 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +86 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ScheduleCockpit.jsx +363 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +8 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +322 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/add-ons-client.jsx +197 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/page.jsx +23 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/settings-shell.jsx +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +734 -61
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +15 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +2 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +2 -19
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +8 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/schedule-cockpit-console.js +287 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/scheduler-orchestration.js +449 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-secrets.js +77 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-readiness.js +583 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-callback.js +63 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-scheduler.js +519 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-ons.js +957 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +607 -63
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +21 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-operator-auth.js +32 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/provider.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/qstash.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/redis.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/search.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/vector.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/scripts/scheduler-ingress-smoke.mjs +26 -0
- package/package.json +1 -1
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* - `read-only` Vercel / Netlify-style runtimes where the bundle is
|
|
10
10
|
* immutable. `PATCH /api/workspace` returns 409 with the
|
|
11
11
|
* same `guidance` string the no-code Save UI surfaces.
|
|
12
|
-
* - `database`
|
|
13
|
-
*
|
|
14
|
-
* without
|
|
12
|
+
* - `database` Postgres-compatible hosted persistence. Supabase is
|
|
13
|
+
* used through DATABASE_URL (or POSTGRES_URL, etc.) behind
|
|
14
|
+
* the postgres adapter, without adding a provider SDK.
|
|
15
15
|
*
|
|
16
16
|
* `describePersistenceMode()` is the single source of truth the GET payload,
|
|
17
17
|
* the no-code Settings/Readiness panel, and the PATCH 409 path all read.
|
|
@@ -36,14 +36,130 @@ const PERSISTENCE_ADAPTERS = Object.freeze({
|
|
|
36
36
|
const READ_ONLY_GUIDANCE =
|
|
37
37
|
"Edit growthub.config.json locally, or set WORKSPACE_CONFIG_ALLOW_FS_WRITE=true on a writable runtime.";
|
|
38
38
|
|
|
39
|
+
const WORKSPACE_CONFIG_TABLE = "growthub_workspace_configs";
|
|
40
|
+
const DEFAULT_WORKSPACE_OWNER = "local-workspace";
|
|
41
|
+
|
|
42
|
+
function stripSandboxRowViewMetadata(row) {
|
|
43
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) return row;
|
|
44
|
+
const { views: _views, activeViewId: _activeViewId, fieldSettings: _fieldSettings, ...rest } = row;
|
|
45
|
+
return rest;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cloneJson(value) {
|
|
49
|
+
return JSON.parse(JSON.stringify(value));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Drop deprecated top-level keys so the only persistence surface is `dataModel` inside `config`. */
|
|
53
|
+
function normalizeConfigForPersistence(config) {
|
|
54
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return config;
|
|
55
|
+
const {
|
|
56
|
+
workspaceSourceRecords: _removedWorkspaceSourceRecords,
|
|
57
|
+
auxiliarySourceRecords: _removedAuxiliarySourceRecords,
|
|
58
|
+
...rest
|
|
59
|
+
} = config;
|
|
60
|
+
return rest;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function mapAllWidgetsInWorkspaceConfig(config, mapWidget) {
|
|
64
|
+
let next = { ...config };
|
|
65
|
+
if (Array.isArray(next.dashboards)) {
|
|
66
|
+
next.dashboards = next.dashboards.map((dash) => ({
|
|
67
|
+
...dash,
|
|
68
|
+
tabs: (dash.tabs || []).map((tab) => ({
|
|
69
|
+
...tab,
|
|
70
|
+
widgets: (tab.widgets || []).map(mapWidget)
|
|
71
|
+
}))
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
if (next.canvas) {
|
|
75
|
+
const canvas = { ...next.canvas };
|
|
76
|
+
if (Array.isArray(canvas.widgets)) canvas.widgets = canvas.widgets.map(mapWidget);
|
|
77
|
+
if (Array.isArray(canvas.tabs)) {
|
|
78
|
+
canvas.tabs = canvas.tabs.map((tab) => ({
|
|
79
|
+
...tab,
|
|
80
|
+
widgets: (tab.widgets || []).map(mapWidget)
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
next = { ...next, canvas };
|
|
84
|
+
}
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Keep widget bindings aligned with the governed data model row store (same JSONB row). */
|
|
89
|
+
function syncWidgetsToLiveObjectRows(config, objectId, rows, fetchedAt) {
|
|
90
|
+
const pid = String(objectId).trim();
|
|
91
|
+
return mapAllWidgetsInWorkspaceConfig(config, (widget) => {
|
|
92
|
+
const b = widget?.config?.binding;
|
|
93
|
+
if (!b || b.sourceType !== "workspace-data-model") return widget;
|
|
94
|
+
const oid = typeof b.objectId === "string" ? b.objectId.trim() : "";
|
|
95
|
+
if (oid !== pid) return widget;
|
|
96
|
+
const nextBinding = { ...b, lastFetchedAt: fetchedAt, recordCount: rows.length };
|
|
97
|
+
if ("rowSource" in nextBinding) delete nextBinding.rowSource;
|
|
98
|
+
const base = { ...widget, config: { ...(widget.config || {}), binding: nextBinding } };
|
|
99
|
+
if (widget.kind === "view") {
|
|
100
|
+
return { ...base, config: { ...base.config, rows: [...rows] } };
|
|
101
|
+
}
|
|
102
|
+
return base;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveWorkspaceDatabaseUrl() {
|
|
107
|
+
const keys = [
|
|
108
|
+
"DATABASE_URL",
|
|
109
|
+
"POSTGRES_URL",
|
|
110
|
+
"POSTGRES_PRISMA_URL",
|
|
111
|
+
"POSTGRES_URL_NON_POOLING",
|
|
112
|
+
"SUPABASE_DATABASE_URL",
|
|
113
|
+
"AGENCY_PORTAL_DATABASE_URL",
|
|
114
|
+
"GROWTHUB_WORKSPACE_DATABASE_URL"
|
|
115
|
+
];
|
|
116
|
+
for (const k of keys) {
|
|
117
|
+
const v = process.env[k];
|
|
118
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function bundleFromLiveDataModelObject(o) {
|
|
124
|
+
if (!o) return null;
|
|
125
|
+
const rows = Array.isArray(o.rows) ? o.rows : [];
|
|
126
|
+
return {
|
|
127
|
+
records: rows,
|
|
128
|
+
integrationId: o.binding?.integrationId ?? null,
|
|
129
|
+
fetchedAt: o.binding?.lastFetchedAt ?? null,
|
|
130
|
+
recordCount: typeof o.binding?.recordCount === "number" ? o.binding.recordCount : rows.length
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function auxiliarySourceRecordsFromConfig(config) {
|
|
135
|
+
return config &&
|
|
136
|
+
config.auxiliarySourceRecords &&
|
|
137
|
+
typeof config.auxiliarySourceRecords === "object" &&
|
|
138
|
+
!Array.isArray(config.auxiliarySourceRecords)
|
|
139
|
+
? cloneJson(config.auxiliarySourceRecords)
|
|
140
|
+
: {};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function withAuxiliarySourceRecords(nextConfig, rawBasis) {
|
|
144
|
+
const auxiliarySourceRecords = auxiliarySourceRecordsFromConfig(rawBasis);
|
|
145
|
+
return Object.keys(auxiliarySourceRecords).length > 0
|
|
146
|
+
? { ...nextConfig, auxiliarySourceRecords }
|
|
147
|
+
: nextConfig;
|
|
148
|
+
}
|
|
149
|
+
|
|
39
150
|
function resolveWorkspaceConfigPath() {
|
|
40
151
|
return path.resolve(/*turbopackIgnore: true*/ process.cwd(), "growthub.config.json");
|
|
41
152
|
}
|
|
42
153
|
|
|
43
154
|
async function readWorkspaceConfig() {
|
|
155
|
+
if (shouldUseDatabasePersistence()) {
|
|
156
|
+
return readDatabaseWorkspaceConfig();
|
|
157
|
+
}
|
|
44
158
|
const configPath = resolveWorkspaceConfigPath();
|
|
45
159
|
const raw = await fs.readFile(configPath, "utf8");
|
|
46
|
-
|
|
160
|
+
let c = JSON.parse(raw);
|
|
161
|
+
if (c.dataModel) c.dataModel = healDataModelObjects(c.dataModel);
|
|
162
|
+
return normalizeConfigForPersistence(c);
|
|
47
163
|
}
|
|
48
164
|
|
|
49
165
|
/**
|
|
@@ -56,6 +172,20 @@ async function readWorkspaceConfig() {
|
|
|
56
172
|
* path and gets verbatim `guidance` instead.
|
|
57
173
|
*/
|
|
58
174
|
function describePersistenceMode() {
|
|
175
|
+
if (shouldUseDatabasePersistence()) {
|
|
176
|
+
return {
|
|
177
|
+
mode: PERSISTENCE_ADAPTERS.DATABASE,
|
|
178
|
+
adapter: "postgres",
|
|
179
|
+
canSave: true,
|
|
180
|
+
saveLabel: "Save writes workspace config to Postgres (e.g. Supabase).",
|
|
181
|
+
reason:
|
|
182
|
+
"postgres workspace data adapter with a configured Postgres URL (DATABASE_URL, POSTGRES_URL, …)",
|
|
183
|
+
nextAction: null,
|
|
184
|
+
guidance: null,
|
|
185
|
+
/** Row key in `growthub_workspace_configs.workspace_id` — same as `GROWTHUB_WORKSPACE_CONFIG_ID` when set. */
|
|
186
|
+
storageWorkspaceId: process.env.GROWTHUB_WORKSPACE_CONFIG_ID || null
|
|
187
|
+
};
|
|
188
|
+
}
|
|
59
189
|
const target = process.env.GROWTHUB_WORKSPACE_DEPLOY_TARGET || process.env.AGENCY_PORTAL_DEPLOY_TARGET || "vercel";
|
|
60
190
|
const isReadOnlyDeploy = target === "vercel" || target === "netlify";
|
|
61
191
|
const allowFsWrite = process.env.WORKSPACE_CONFIG_ALLOW_FS_WRITE === "true";
|
|
@@ -89,6 +219,29 @@ function describePersistenceMode() {
|
|
|
89
219
|
return baseFilesystem("Local development");
|
|
90
220
|
}
|
|
91
221
|
|
|
222
|
+
function healDataModelObjects(dataModel) {
|
|
223
|
+
if (!dataModel || !Array.isArray(dataModel.objects)) return dataModel;
|
|
224
|
+
const healed = dataModel.objects.map((object) => {
|
|
225
|
+
let next = object;
|
|
226
|
+
if (
|
|
227
|
+
object &&
|
|
228
|
+
object.binding?.sourceStorage === "workspace-source-records" &&
|
|
229
|
+
(typeof object.sourceId !== "string" || !object.sourceId.trim()) &&
|
|
230
|
+
typeof object.id === "string" &&
|
|
231
|
+
object.id.trim()
|
|
232
|
+
) {
|
|
233
|
+
next = { ...next, sourceId: object.id.trim() };
|
|
234
|
+
}
|
|
235
|
+
if (next?.objectType === "sandbox-environment" && Array.isArray(next.rows)) {
|
|
236
|
+
const rows = next.rows.map((row) => stripSandboxRowViewMetadata(row));
|
|
237
|
+
const changed = rows.some((row, index) => row !== next.rows[index]);
|
|
238
|
+
if (changed) next = { ...next, rows };
|
|
239
|
+
}
|
|
240
|
+
return next;
|
|
241
|
+
});
|
|
242
|
+
return { ...dataModel, objects: healed };
|
|
243
|
+
}
|
|
244
|
+
|
|
92
245
|
/**
|
|
93
246
|
* Pure merge step shared by the real write path and the preflight dry-run
|
|
94
247
|
* (`POST /api/workspace/patch/preflight`). Canvas patches MERGE over the
|
|
@@ -101,7 +254,7 @@ function applyWorkspaceConfigPatch(currentConfig, patch) {
|
|
|
101
254
|
const next = { ...currentConfig };
|
|
102
255
|
if (patch.dashboards !== undefined) next.dashboards = patch.dashboards;
|
|
103
256
|
if (patch.widgetTypes !== undefined) next.widgetTypes = patch.widgetTypes;
|
|
104
|
-
if (patch.dataModel !== undefined) next.dataModel = patch.dataModel;
|
|
257
|
+
if (patch.dataModel !== undefined) next.dataModel = healDataModelObjects(patch.dataModel);
|
|
105
258
|
if (patch.canvas !== undefined && patch.canvas !== null) {
|
|
106
259
|
const patchCanvas = { ...patch.canvas };
|
|
107
260
|
if (Array.isArray(patchCanvas.tabs)) {
|
|
@@ -137,6 +290,19 @@ function applyWorkspaceConfigPatch(currentConfig, patch) {
|
|
|
137
290
|
async function writeWorkspaceConfig(patch) {
|
|
138
291
|
const persistence = describePersistenceMode();
|
|
139
292
|
const adapter = readAdapterConfig();
|
|
293
|
+
if (persistence.mode === PERSISTENCE_ADAPTERS.DATABASE && persistence.canSave) {
|
|
294
|
+
const rawCurrent = await readDatabaseWorkspaceConfigRaw();
|
|
295
|
+
const current = normalizeConfigForPersistence(rawCurrent);
|
|
296
|
+
const next = normalizeConfigForPersistence(applyWorkspaceConfigPatch(current, patch));
|
|
297
|
+
validateWorkspaceConfig({
|
|
298
|
+
dashboards: next.dashboards,
|
|
299
|
+
widgetTypes: next.widgetTypes,
|
|
300
|
+
canvas: next.canvas,
|
|
301
|
+
dataModel: next.dataModel
|
|
302
|
+
});
|
|
303
|
+
await writeDatabaseWorkspaceConfig(withAuxiliarySourceRecords(next, rawCurrent));
|
|
304
|
+
return next;
|
|
305
|
+
}
|
|
140
306
|
if (persistence.mode !== PERSISTENCE_ADAPTERS.FILESYSTEM || !persistence.canSave) {
|
|
141
307
|
const error = new Error(persistence.reason);
|
|
142
308
|
error.code = "WORKSPACE_PERSISTENCE_READ_ONLY";
|
|
@@ -144,8 +310,13 @@ async function writeWorkspaceConfig(patch) {
|
|
|
144
310
|
error.guidance = persistence.guidance || READ_ONLY_GUIDANCE;
|
|
145
311
|
throw error;
|
|
146
312
|
}
|
|
147
|
-
const
|
|
148
|
-
|
|
313
|
+
const rawCurrent = persistence.mode === PERSISTENCE_ADAPTERS.DATABASE
|
|
314
|
+
? await readDatabaseWorkspaceConfigRaw()
|
|
315
|
+
: null;
|
|
316
|
+
const current = normalizeConfigForPersistence(
|
|
317
|
+
rawCurrent || await readWorkspaceConfig()
|
|
318
|
+
);
|
|
319
|
+
const next = normalizeConfigForPersistence(applyWorkspaceConfigPatch(current, patch));
|
|
149
320
|
validateWorkspaceConfig({
|
|
150
321
|
dashboards: next.dashboards,
|
|
151
322
|
widgetTypes: next.widgetTypes,
|
|
@@ -222,7 +393,7 @@ function normalizeWorkspaceIdentityPatch(patch) {
|
|
|
222
393
|
async function writeWorkspaceIdentitySettings(patch) {
|
|
223
394
|
const persistence = describePersistenceMode();
|
|
224
395
|
const adapter = readAdapterConfig();
|
|
225
|
-
if (
|
|
396
|
+
if (![PERSISTENCE_ADAPTERS.FILESYSTEM, PERSISTENCE_ADAPTERS.DATABASE].includes(persistence.mode) || !persistence.canSave) {
|
|
226
397
|
const error = new Error(persistence.reason);
|
|
227
398
|
error.code = "WORKSPACE_PERSISTENCE_READ_ONLY";
|
|
228
399
|
error.adapter = adapter.integrationAdapter;
|
|
@@ -231,11 +402,14 @@ async function writeWorkspaceIdentitySettings(patch) {
|
|
|
231
402
|
}
|
|
232
403
|
|
|
233
404
|
const normalized = normalizeWorkspaceIdentityPatch(patch);
|
|
234
|
-
const
|
|
405
|
+
const rawCurrent = persistence.mode === PERSISTENCE_ADAPTERS.DATABASE
|
|
406
|
+
? await readDatabaseWorkspaceConfigRaw()
|
|
407
|
+
: null;
|
|
408
|
+
const current = normalizeConfigForPersistence(
|
|
409
|
+
rawCurrent || await readWorkspaceConfig()
|
|
410
|
+
);
|
|
235
411
|
const next = { ...current };
|
|
236
|
-
if (normalized.name !== undefined)
|
|
237
|
-
next.name = normalized.name;
|
|
238
|
-
}
|
|
412
|
+
if (normalized.name !== undefined) next.name = normalized.name;
|
|
239
413
|
if (normalized.branding) {
|
|
240
414
|
next.branding = {
|
|
241
415
|
...(current.branding && typeof current.branding === "object" && !Array.isArray(current.branding)
|
|
@@ -243,9 +417,7 @@ async function writeWorkspaceIdentitySettings(patch) {
|
|
|
243
417
|
: {}),
|
|
244
418
|
...normalized.branding
|
|
245
419
|
};
|
|
246
|
-
if (!next.branding.name)
|
|
247
|
-
next.branding.name = next.name || "Growthub Workspace";
|
|
248
|
-
}
|
|
420
|
+
if (!next.branding.name) next.branding.name = next.name || "Growthub Workspace";
|
|
249
421
|
}
|
|
250
422
|
|
|
251
423
|
validateWorkspaceConfig({
|
|
@@ -255,6 +427,11 @@ async function writeWorkspaceIdentitySettings(patch) {
|
|
|
255
427
|
dataModel: next.dataModel
|
|
256
428
|
});
|
|
257
429
|
|
|
430
|
+
if (persistence.mode === PERSISTENCE_ADAPTERS.DATABASE) {
|
|
431
|
+
await writeDatabaseWorkspaceConfig(withAuxiliarySourceRecords(next, rawCurrent));
|
|
432
|
+
return next;
|
|
433
|
+
}
|
|
434
|
+
|
|
258
435
|
const configPath = resolveWorkspaceConfigPath();
|
|
259
436
|
const expectedDir = path.resolve(/*turbopackIgnore: true*/ process.cwd());
|
|
260
437
|
if (path.dirname(configPath) !== expectedDir) {
|
|
@@ -309,7 +486,7 @@ function normalizeApiWebhookRefs(refs) {
|
|
|
309
486
|
async function writeWorkspaceApiWebhookSettings(patch) {
|
|
310
487
|
const persistence = describePersistenceMode();
|
|
311
488
|
const adapter = readAdapterConfig();
|
|
312
|
-
if (
|
|
489
|
+
if (![PERSISTENCE_ADAPTERS.FILESYSTEM, PERSISTENCE_ADAPTERS.DATABASE].includes(persistence.mode) || !persistence.canSave) {
|
|
313
490
|
const error = new Error(persistence.reason);
|
|
314
491
|
error.code = "WORKSPACE_PERSISTENCE_READ_ONLY";
|
|
315
492
|
error.adapter = adapter.integrationAdapter;
|
|
@@ -318,7 +495,12 @@ async function writeWorkspaceApiWebhookSettings(patch) {
|
|
|
318
495
|
}
|
|
319
496
|
|
|
320
497
|
const refs = normalizeApiWebhookRefs(patch?.refs);
|
|
321
|
-
const
|
|
498
|
+
const rawCurrent = persistence.mode === PERSISTENCE_ADAPTERS.DATABASE
|
|
499
|
+
? await readDatabaseWorkspaceConfigRaw()
|
|
500
|
+
: null;
|
|
501
|
+
const current = normalizeConfigForPersistence(
|
|
502
|
+
rawCurrent || await readWorkspaceConfig()
|
|
503
|
+
);
|
|
322
504
|
const existing = Array.isArray(current.integrations) ? current.integrations : [];
|
|
323
505
|
const next = {
|
|
324
506
|
...current,
|
|
@@ -335,6 +517,11 @@ async function writeWorkspaceApiWebhookSettings(patch) {
|
|
|
335
517
|
dataModel: next.dataModel
|
|
336
518
|
});
|
|
337
519
|
|
|
520
|
+
if (persistence.mode === PERSISTENCE_ADAPTERS.DATABASE) {
|
|
521
|
+
await writeDatabaseWorkspaceConfig(withAuxiliarySourceRecords(next, rawCurrent));
|
|
522
|
+
return next.integrations.filter((item) => item?.sourceType === "custom-api-webhooks");
|
|
523
|
+
}
|
|
524
|
+
|
|
338
525
|
const configPath = resolveWorkspaceConfigPath();
|
|
339
526
|
const expectedDir = path.resolve(/*turbopackIgnore: true*/ process.cwd());
|
|
340
527
|
if (path.dirname(configPath) !== expectedDir) {
|
|
@@ -346,43 +533,170 @@ async function writeWorkspaceApiWebhookSettings(patch) {
|
|
|
346
533
|
return next.integrations.filter((item) => item?.sourceType === "custom-api-webhooks");
|
|
347
534
|
}
|
|
348
535
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
* fetches live data for a source with `binding.sourceStorage: "workspace-source-records"`.
|
|
354
|
-
*
|
|
355
|
-
* Persistence is keyed by `sourceId` and stored in a JSON sidecar file
|
|
356
|
-
* (`growthub.source-records.json`) beside `growthub.config.json`. The same
|
|
357
|
-
* filesystem / read-only / database mode rules apply: in read-only mode writes
|
|
358
|
-
* are rejected gracefully so the refresh button surface is disabled.
|
|
359
|
-
*
|
|
360
|
-
* Shape: { [sourceId]: { records: Record[], integrationId: string, fetchedAt: string } }
|
|
361
|
-
*/
|
|
536
|
+
function shouldUseDatabasePersistence() {
|
|
537
|
+
const adapter = readAdapterConfig();
|
|
538
|
+
return adapter.dataAdapter === "postgres" && Boolean(resolveWorkspaceDatabaseUrl());
|
|
539
|
+
}
|
|
362
540
|
|
|
363
|
-
|
|
541
|
+
/** Hosted workspace row key: explicit env, then Growthub bridge user id (same value many kits already set in `.env`). */
|
|
542
|
+
function resolveWorkspaceOwnerIdFromEnv() {
|
|
543
|
+
const id =
|
|
544
|
+
process.env.GROWTHUB_WORKSPACE_USER_ID ||
|
|
545
|
+
process.env.AGENCY_PORTAL_WORKSPACE_USER_ID ||
|
|
546
|
+
process.env.GROWTHUB_BRIDGE_USER_ID ||
|
|
547
|
+
"";
|
|
548
|
+
return typeof id === "string" && id.trim() ? id.trim() : DEFAULT_WORKSPACE_OWNER;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function readWorkspaceOwner() {
|
|
552
|
+
const sessionPath = process.env.GROWTHUB_WORKSPACE_SESSION_PATH || process.env.AGENCY_PORTAL_WORKSPACE_SESSION_PATH;
|
|
553
|
+
if (sessionPath) {
|
|
554
|
+
try {
|
|
555
|
+
const raw = await fs.readFile(path.resolve(/*turbopackIgnore: true*/ sessionPath), "utf8");
|
|
556
|
+
const session = JSON.parse(raw);
|
|
557
|
+
const sid = session.userId || session.email;
|
|
558
|
+
return {
|
|
559
|
+
ownerId: (typeof sid === "string" && sid.trim() ? sid.trim() : null) || resolveWorkspaceOwnerIdFromEnv(),
|
|
560
|
+
ownerEmail: session.email || null,
|
|
561
|
+
hostedBaseUrl: session.hostedBaseUrl || null,
|
|
562
|
+
sessionPath
|
|
563
|
+
};
|
|
564
|
+
} catch {
|
|
565
|
+
return {
|
|
566
|
+
ownerId: resolveWorkspaceOwnerIdFromEnv(),
|
|
567
|
+
ownerEmail: null,
|
|
568
|
+
hostedBaseUrl: null,
|
|
569
|
+
sessionPath
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
ownerId: resolveWorkspaceOwnerIdFromEnv(),
|
|
575
|
+
ownerEmail: null,
|
|
576
|
+
hostedBaseUrl: null,
|
|
577
|
+
sessionPath: null
|
|
578
|
+
};
|
|
579
|
+
}
|
|
364
580
|
|
|
365
|
-
function
|
|
366
|
-
|
|
581
|
+
async function connectPostgres() {
|
|
582
|
+
const { Client } = await import("pg");
|
|
583
|
+
const rawUrl = resolveWorkspaceDatabaseUrl();
|
|
584
|
+
const connectionString = normalizePostgresConnectionString(rawUrl);
|
|
585
|
+
const usesSsl = process.env.DATABASE_SSL === "true" || /supabase\.(co|com)|sslmode=require/.test(String(connectionString || ""));
|
|
586
|
+
const client = new Client({
|
|
587
|
+
connectionString,
|
|
588
|
+
ssl: usesSsl ? { rejectUnauthorized: process.env.DATABASE_SSL_REJECT_UNAUTHORIZED === "true" } : undefined
|
|
589
|
+
});
|
|
590
|
+
await client.connect();
|
|
591
|
+
return client;
|
|
367
592
|
}
|
|
368
593
|
|
|
369
|
-
|
|
370
|
-
|
|
594
|
+
function normalizePostgresConnectionString(connectionString) {
|
|
595
|
+
if (!connectionString) return connectionString;
|
|
371
596
|
try {
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return all[sourceId] || null;
|
|
376
|
-
}
|
|
377
|
-
return all;
|
|
597
|
+
const url = new URL(connectionString);
|
|
598
|
+
url.searchParams.delete("sslmode");
|
|
599
|
+
return url.toString();
|
|
378
600
|
} catch {
|
|
379
|
-
return
|
|
601
|
+
return connectionString;
|
|
380
602
|
}
|
|
381
603
|
}
|
|
382
604
|
|
|
383
|
-
async function
|
|
605
|
+
async function ensureWorkspaceConfigTable(client) {
|
|
606
|
+
await client.query(`
|
|
607
|
+
create table if not exists ${WORKSPACE_CONFIG_TABLE} (
|
|
608
|
+
owner_id text not null,
|
|
609
|
+
workspace_id text not null,
|
|
610
|
+
owner_email text,
|
|
611
|
+
hosted_base_url text,
|
|
612
|
+
config jsonb not null,
|
|
613
|
+
created_at timestamptz not null default now(),
|
|
614
|
+
updated_at timestamptz not null default now(),
|
|
615
|
+
primary key (owner_id, workspace_id)
|
|
616
|
+
)
|
|
617
|
+
`);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function readDatabaseWorkspaceConfigRaw() {
|
|
621
|
+
const owner = await readWorkspaceOwner();
|
|
622
|
+
const seed = JSON.parse(await fs.readFile(resolveWorkspaceConfigPath(), "utf8"));
|
|
623
|
+
const workspaceId = process.env.GROWTHUB_WORKSPACE_CONFIG_ID || seed.id || "default";
|
|
624
|
+
const client = await connectPostgres();
|
|
625
|
+
try {
|
|
626
|
+
await ensureWorkspaceConfigTable(client);
|
|
627
|
+
const result = await client.query(
|
|
628
|
+
`select config from ${WORKSPACE_CONFIG_TABLE} where owner_id = $1 and workspace_id = $2 limit 1`,
|
|
629
|
+
[owner.ownerId, workspaceId]
|
|
630
|
+
);
|
|
631
|
+
if (result.rows[0]?.config) {
|
|
632
|
+
return cloneJson(result.rows[0].config);
|
|
633
|
+
}
|
|
634
|
+
await client.query(
|
|
635
|
+
`insert into ${WORKSPACE_CONFIG_TABLE} (owner_id, workspace_id, owner_email, hosted_base_url, config)
|
|
636
|
+
values ($1, $2, $3, $4, $5::jsonb)
|
|
637
|
+
on conflict (owner_id, workspace_id) do nothing`,
|
|
638
|
+
[owner.ownerId, workspaceId, owner.ownerEmail, owner.hostedBaseUrl, JSON.stringify(seed)]
|
|
639
|
+
);
|
|
640
|
+
return cloneJson(seed);
|
|
641
|
+
} finally {
|
|
642
|
+
await client.end();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async function readDatabaseWorkspaceConfig() {
|
|
647
|
+
const config = normalizeConfigForPersistence(await readDatabaseWorkspaceConfigRaw());
|
|
648
|
+
if (config.dataModel) config.dataModel = healDataModelObjects(config.dataModel);
|
|
649
|
+
return config;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async function writeDatabaseWorkspaceConfig(config) {
|
|
653
|
+
const owner = await readWorkspaceOwner();
|
|
654
|
+
const workspaceId = process.env.GROWTHUB_WORKSPACE_CONFIG_ID || config.id || "default";
|
|
655
|
+
const client = await connectPostgres();
|
|
656
|
+
try {
|
|
657
|
+
await ensureWorkspaceConfigTable(client);
|
|
658
|
+
await client.query(
|
|
659
|
+
`insert into ${WORKSPACE_CONFIG_TABLE} (owner_id, workspace_id, owner_email, hosted_base_url, config)
|
|
660
|
+
values ($1, $2, $3, $4, $5::jsonb)
|
|
661
|
+
on conflict (owner_id, workspace_id)
|
|
662
|
+
do update set
|
|
663
|
+
owner_email = excluded.owner_email,
|
|
664
|
+
hosted_base_url = excluded.hosted_base_url,
|
|
665
|
+
config = excluded.config,
|
|
666
|
+
updated_at = now()`,
|
|
667
|
+
[owner.ownerId, workspaceId, owner.ownerEmail, owner.hostedBaseUrl, JSON.stringify(config)]
|
|
668
|
+
);
|
|
669
|
+
} finally {
|
|
670
|
+
await client.end();
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Read normalized live rows for a refresh `sourceId` / object id from
|
|
676
|
+
* `dataModel.objects[]` (same document as workspace config — Postgres JSONB or
|
|
677
|
+
* growthub.config.json). No sidecar files or auxiliary tables.
|
|
678
|
+
*/
|
|
679
|
+
async function readLiveDataModelBundle(sourceId) {
|
|
680
|
+
const config = await readWorkspaceConfig();
|
|
681
|
+
const objects = config.dataModel?.objects || [];
|
|
682
|
+
if (sourceId) {
|
|
683
|
+
const sid = sourceId.trim();
|
|
684
|
+
const o = objects.find((obj) => obj.id === sid || obj.sourceId === sid);
|
|
685
|
+
if (!o || o.binding?.sourceStorage !== "workspace-source-records") return null;
|
|
686
|
+
return bundleFromLiveDataModelObject(o);
|
|
687
|
+
}
|
|
688
|
+
const all = {};
|
|
689
|
+
for (const o of objects) {
|
|
690
|
+
if (o.binding?.sourceStorage !== "workspace-source-records") continue;
|
|
691
|
+
const key = typeof o.sourceId === "string" && o.sourceId.trim() ? o.sourceId.trim() : o.id;
|
|
692
|
+
all[key] = bundleFromLiveDataModelObject(o);
|
|
693
|
+
}
|
|
694
|
+
return all;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async function writeLiveDataModelRows(sourceId, records, metadata = {}) {
|
|
384
698
|
const persistence = describePersistenceMode();
|
|
385
|
-
if (
|
|
699
|
+
if (![PERSISTENCE_ADAPTERS.FILESYSTEM, PERSISTENCE_ADAPTERS.DATABASE].includes(persistence.mode) || !persistence.canSave) {
|
|
386
700
|
const error = new Error(persistence.reason);
|
|
387
701
|
error.code = "WORKSPACE_PERSISTENCE_READ_ONLY";
|
|
388
702
|
error.guidance = persistence.guidance || READ_ONLY_GUIDANCE;
|
|
@@ -390,38 +704,266 @@ async function writeWorkspaceSourceRecords(sourceId, records, metadata = {}) {
|
|
|
390
704
|
}
|
|
391
705
|
if (typeof sourceId !== "string" || !sourceId.trim()) {
|
|
392
706
|
const error = new Error("sourceId must be a non-empty string");
|
|
393
|
-
error.code = "
|
|
707
|
+
error.code = "INVALID_LIVE_DATA_MODEL_ROWS";
|
|
394
708
|
throw error;
|
|
395
709
|
}
|
|
396
710
|
if (!Array.isArray(records)) {
|
|
397
711
|
const error = new Error("records must be an array");
|
|
398
|
-
error.code = "
|
|
712
|
+
error.code = "INVALID_LIVE_DATA_MODEL_ROWS";
|
|
399
713
|
throw error;
|
|
400
714
|
}
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
715
|
+
|
|
716
|
+
const sid = sourceId.trim();
|
|
717
|
+
const fetchedAt = metadata.fetchedAt || new Date().toISOString();
|
|
718
|
+
const integrationId = metadata.integrationId ?? null;
|
|
719
|
+
|
|
720
|
+
const rawBasis = persistence.mode === PERSISTENCE_ADAPTERS.DATABASE
|
|
721
|
+
? await readDatabaseWorkspaceConfigRaw()
|
|
722
|
+
: null;
|
|
723
|
+
const basis = normalizeConfigForPersistence(rawBasis || await readWorkspaceConfig());
|
|
724
|
+
if (!Array.isArray(basis.dataModel?.objects)) {
|
|
725
|
+
const error = new Error("workspace config has no dataModel.objects");
|
|
726
|
+
error.code = "LIVE_DATA_MODEL_NO_OBJECTS";
|
|
406
727
|
throw error;
|
|
407
728
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
729
|
+
|
|
730
|
+
let found = false;
|
|
731
|
+
const objects = basis.dataModel.objects.map((o) => {
|
|
732
|
+
if (o.id !== sid && o.sourceId !== sid) return o;
|
|
733
|
+
if (o.binding?.sourceStorage !== "workspace-source-records") return o;
|
|
734
|
+
found = true;
|
|
735
|
+
const nextBinding = {
|
|
736
|
+
...o.binding,
|
|
737
|
+
lastFetchedAt: fetchedAt,
|
|
738
|
+
recordCount: records.length
|
|
739
|
+
};
|
|
740
|
+
if ("rowSource" in nextBinding) delete nextBinding.rowSource;
|
|
741
|
+
return {
|
|
742
|
+
...o,
|
|
743
|
+
rows: records,
|
|
744
|
+
binding: nextBinding
|
|
745
|
+
};
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
if (!found) {
|
|
749
|
+
const error = new Error(`no live-backed data model object for sourceId: ${sid}`);
|
|
750
|
+
error.code = "LIVE_DATA_MODEL_OBJECT_NOT_FOUND";
|
|
751
|
+
throw error;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
let next = normalizeConfigForPersistence({
|
|
755
|
+
...basis,
|
|
756
|
+
dataModel: { ...basis.dataModel, objects }
|
|
757
|
+
});
|
|
758
|
+
next = syncWidgetsToLiveObjectRows(next, sid, records, fetchedAt);
|
|
759
|
+
if (next.dataModel) next.dataModel = healDataModelObjects(next.dataModel);
|
|
760
|
+
|
|
761
|
+
validateWorkspaceConfig({
|
|
762
|
+
dashboards: next.dashboards,
|
|
763
|
+
widgetTypes: next.widgetTypes,
|
|
764
|
+
canvas: next.canvas,
|
|
765
|
+
dataModel: next.dataModel
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
if (persistence.mode === PERSISTENCE_ADAPTERS.DATABASE) {
|
|
769
|
+
await writeDatabaseWorkspaceConfig(withAuxiliarySourceRecords(next, rawBasis));
|
|
770
|
+
} else {
|
|
771
|
+
const configPath = resolveWorkspaceConfigPath();
|
|
772
|
+
const expectedDir = path.resolve(/*turbopackIgnore: true*/ process.cwd());
|
|
773
|
+
if (path.dirname(configPath) !== expectedDir) {
|
|
774
|
+
const error = new Error(`refused to write outside workspace cwd: ${configPath}`);
|
|
775
|
+
error.code = "WORKSPACE_PERSISTENCE_PATH_REFUSED";
|
|
776
|
+
throw error;
|
|
777
|
+
}
|
|
778
|
+
await fs.writeFile(configPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
return { records, integrationId, fetchedAt, recordCount: records.length };
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Kit artifact parity: sidecar keyed by stable `sourceId` (live integrations + sandbox run-history keys). */
|
|
785
|
+
const SOURCE_RECORDS_FILENAME = "growthub.source-records.json";
|
|
786
|
+
|
|
787
|
+
function resolveSourceRecordsSidecarPath() {
|
|
788
|
+
return path.resolve(/* turbopackIgnore: true */ process.cwd(), SOURCE_RECORDS_FILENAME);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Canonical read contract for keyed source records (`refresh-sources`,
|
|
793
|
+
* sandbox run history keyed as `sandbox:<objectId>:<slug>`, reference pickers).
|
|
794
|
+
*
|
|
795
|
+
* Lookup order per key:
|
|
796
|
+
* 1. Live-backed `workspace-source-records` **rows inside** `dataModel.objects[]`
|
|
797
|
+
* (Postgres JSONB or growthub.config.json).
|
|
798
|
+
* 2. **Supabase/hosted Postgres** — `auxiliarySourceRecords` keyed map on the workspace
|
|
799
|
+
* document persisted with config JSON (same Postgres row).
|
|
800
|
+
* 3. Filesystem **`growthub.source-records.json`** — same keyed shape as exported kits.
|
|
801
|
+
*/
|
|
802
|
+
async function readWorkspaceSourceRecords(sourceId) {
|
|
803
|
+
if (!sourceId) {
|
|
804
|
+
const liveAllRaw = await readLiveDataModelBundle();
|
|
805
|
+
const liveAll =
|
|
806
|
+
typeof liveAllRaw === "object" &&
|
|
807
|
+
liveAllRaw !== null &&
|
|
808
|
+
!Array.isArray(liveAllRaw)
|
|
809
|
+
? liveAllRaw
|
|
810
|
+
: {};
|
|
811
|
+
const config = shouldUseDatabasePersistence()
|
|
812
|
+
? await readDatabaseWorkspaceConfigRaw()
|
|
813
|
+
: await readWorkspaceConfig();
|
|
814
|
+
const auxPlain =
|
|
815
|
+
config.auxiliarySourceRecords &&
|
|
816
|
+
typeof config.auxiliarySourceRecords === "object" &&
|
|
817
|
+
!Array.isArray(config.auxiliarySourceRecords)
|
|
818
|
+
? config.auxiliarySourceRecords
|
|
819
|
+
: {};
|
|
820
|
+
let merged = { ...auxPlain, ...liveAll };
|
|
821
|
+
|
|
822
|
+
if (!shouldUseDatabasePersistence()) {
|
|
823
|
+
try {
|
|
824
|
+
const raw = await fs.readFile(resolveSourceRecordsSidecarPath(), "utf8");
|
|
825
|
+
const all = JSON.parse(raw);
|
|
826
|
+
if (typeof all === "object" && all !== null && !Array.isArray(all)) merged = { ...all, ...merged };
|
|
827
|
+
} catch {
|
|
828
|
+
/* no sidecar */
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return merged;
|
|
414
832
|
}
|
|
415
|
-
|
|
833
|
+
|
|
834
|
+
const sid = sourceId.trim();
|
|
835
|
+
const liveOne = await readLiveDataModelBundle(sid);
|
|
836
|
+
if (liveOne && typeof liveOne === "object") return liveOne;
|
|
837
|
+
|
|
838
|
+
const cfg = shouldUseDatabasePersistence()
|
|
839
|
+
? await readDatabaseWorkspaceConfigRaw()
|
|
840
|
+
: await readWorkspaceConfig();
|
|
841
|
+
const auxHit =
|
|
842
|
+
cfg.auxiliarySourceRecords &&
|
|
843
|
+
typeof cfg.auxiliarySourceRecords === "object" &&
|
|
844
|
+
!Array.isArray(cfg.auxiliarySourceRecords)
|
|
845
|
+
? cfg.auxiliarySourceRecords[sid]
|
|
846
|
+
: null;
|
|
847
|
+
if (auxHit && typeof auxHit === "object" && Array.isArray(auxHit.records)) return auxHit;
|
|
848
|
+
|
|
849
|
+
if (!shouldUseDatabasePersistence()) {
|
|
850
|
+
try {
|
|
851
|
+
const raw = await fs.readFile(resolveSourceRecordsSidecarPath(), "utf8");
|
|
852
|
+
const all = JSON.parse(raw);
|
|
853
|
+
return all[sid] || null;
|
|
854
|
+
} catch {
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Persist keyed records: live integrations update governed object rows when
|
|
864
|
+
* `binding.sourceStorage === "workspace-source-records"` matches; sandbox and
|
|
865
|
+
* other keyed stores use Postgres-embedded auxiliary map or filesystem sidecar.
|
|
866
|
+
*/
|
|
867
|
+
async function writeWorkspaceSourceRecords(sourceId, records, metadata = {}) {
|
|
868
|
+
const persistence = describePersistenceMode();
|
|
869
|
+
|
|
870
|
+
const sid = typeof sourceId === "string" ? sourceId.trim() : "";
|
|
871
|
+
if (!sid) {
|
|
872
|
+
const error = new Error("sourceId must be a non-empty string");
|
|
873
|
+
error.code = "INVALID_SOURCE_RECORDS_WRITE";
|
|
874
|
+
throw error;
|
|
875
|
+
}
|
|
876
|
+
if (!Array.isArray(records)) {
|
|
877
|
+
const error = new Error("records must be an array");
|
|
878
|
+
error.code = "INVALID_SOURCE_RECORDS_WRITE";
|
|
879
|
+
throw error;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const fetchedAt = metadata.fetchedAt || new Date().toISOString();
|
|
883
|
+
const integrationId = metadata.integrationId ?? null;
|
|
884
|
+
const entry = {
|
|
416
885
|
records,
|
|
417
|
-
integrationId
|
|
418
|
-
fetchedAt
|
|
886
|
+
integrationId,
|
|
887
|
+
fetchedAt,
|
|
419
888
|
recordCount: records.length
|
|
420
889
|
};
|
|
421
|
-
await fs.writeFile(recordsPath, `${JSON.stringify(all, null, 2)}\n`, "utf8");
|
|
422
|
-
return all[sourceId.trim()];
|
|
423
|
-
}
|
|
424
890
|
|
|
891
|
+
const basis = shouldUseDatabasePersistence()
|
|
892
|
+
? await readDatabaseWorkspaceConfigRaw()
|
|
893
|
+
: await readWorkspaceConfig();
|
|
894
|
+
const hasLiveBackedObject =
|
|
895
|
+
Array.isArray(basis.dataModel?.objects) &&
|
|
896
|
+
basis.dataModel.objects.some(
|
|
897
|
+
(o) =>
|
|
898
|
+
o &&
|
|
899
|
+
o.binding?.sourceStorage === "workspace-source-records" &&
|
|
900
|
+
(o.id === sid || o.sourceId === sid)
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
if (hasLiveBackedObject) {
|
|
904
|
+
return writeLiveDataModelRows(sid, records, { integrationId, fetchedAt });
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (!persistence.canSave) {
|
|
908
|
+
const error = new Error(persistence.reason);
|
|
909
|
+
error.code = "WORKSPACE_PERSISTENCE_READ_ONLY";
|
|
910
|
+
error.guidance = persistence.guidance || READ_ONLY_GUIDANCE;
|
|
911
|
+
throw error;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (persistence.mode === PERSISTENCE_ADAPTERS.DATABASE) {
|
|
915
|
+
const aux = cloneJson(
|
|
916
|
+
basis.auxiliarySourceRecords &&
|
|
917
|
+
typeof basis.auxiliarySourceRecords === "object" &&
|
|
918
|
+
!Array.isArray(basis.auxiliarySourceRecords)
|
|
919
|
+
? basis.auxiliarySourceRecords
|
|
920
|
+
: {}
|
|
921
|
+
);
|
|
922
|
+
aux[sid] = entry;
|
|
923
|
+
const next = normalizeConfigForPersistence({
|
|
924
|
+
...basis,
|
|
925
|
+
auxiliarySourceRecords: aux
|
|
926
|
+
});
|
|
927
|
+
validateWorkspaceConfig({
|
|
928
|
+
dashboards: next.dashboards,
|
|
929
|
+
widgetTypes: next.widgetTypes,
|
|
930
|
+
canvas: next.canvas,
|
|
931
|
+
dataModel: next.dataModel
|
|
932
|
+
});
|
|
933
|
+
await writeDatabaseWorkspaceConfig({
|
|
934
|
+
...next,
|
|
935
|
+
auxiliarySourceRecords: aux
|
|
936
|
+
});
|
|
937
|
+
return entry;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (persistence.mode === PERSISTENCE_ADAPTERS.FILESYSTEM) {
|
|
941
|
+
const recordsPath = resolveSourceRecordsSidecarPath();
|
|
942
|
+
const expectedDir = path.resolve(/* turbopackIgnore: true */ process.cwd());
|
|
943
|
+
if (path.dirname(recordsPath) !== expectedDir) {
|
|
944
|
+
const error = new Error(`refused to write outside workspace cwd: ${recordsPath}`);
|
|
945
|
+
error.code = "WORKSPACE_PERSISTENCE_PATH_REFUSED";
|
|
946
|
+
throw error;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
let all = {};
|
|
950
|
+
try {
|
|
951
|
+
const raw = await fs.readFile(recordsPath, "utf8");
|
|
952
|
+
all = JSON.parse(raw);
|
|
953
|
+
} catch {
|
|
954
|
+
all = {};
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
all[sid] = entry;
|
|
958
|
+
await fs.writeFile(recordsPath, `${JSON.stringify(all, null, 2)}\n`, "utf8");
|
|
959
|
+
return entry;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const error = new Error(persistence.reason || "cannot persist source records");
|
|
963
|
+
error.code = "WORKSPACE_PERSISTENCE_READ_ONLY";
|
|
964
|
+
error.guidance = persistence.guidance || READ_ONLY_GUIDANCE;
|
|
965
|
+
throw error;
|
|
966
|
+
}
|
|
425
967
|
export {
|
|
426
968
|
GRID_COLUMNS,
|
|
427
969
|
GRID_ROWS,
|
|
@@ -432,10 +974,12 @@ export {
|
|
|
432
974
|
describePersistenceMode,
|
|
433
975
|
readWorkspaceConfig,
|
|
434
976
|
readWorkspaceSourceRecords,
|
|
977
|
+
readLiveDataModelBundle,
|
|
435
978
|
resolveWorkspaceConfigPath,
|
|
436
979
|
validateWorkspaceConfig,
|
|
437
980
|
writeWorkspaceConfig,
|
|
438
981
|
writeWorkspaceApiWebhookSettings,
|
|
439
982
|
writeWorkspaceIdentitySettings,
|
|
983
|
+
writeLiveDataModelRows,
|
|
440
984
|
writeWorkspaceSourceRecords
|
|
441
985
|
};
|