@growthub/cli 0.9.10 → 0.9.12

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.
Files changed (32) hide show
  1. package/README.md +1 -1
  2. package/assets/worker-kits/creative-strategist-v1/kit.json +5 -2
  3. package/assets/worker-kits/growthub-agency-portal-starter-v1/kit.json +4 -1
  4. package/assets/worker-kits/growthub-ai-website-cloner-v1/kit.json +6 -3
  5. package/assets/worker-kits/growthub-creative-video-pipeline-v1/kit.json +4 -1
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +4 -4
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integration-entities/route.js +50 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/route.js +1 -1
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +389 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +362 -15
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +5 -2
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +5 -5
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +625 -56
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/growthub-connection-normalizer.js +12 -16
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/index.js +61 -11
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/domain/integrations.js +31 -1
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +3 -1
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +433 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +112 -14
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -2
  21. package/assets/worker-kits/growthub-email-marketing-v1/kit.json +5 -2
  22. package/assets/worker-kits/growthub-geo-seo-v1/kit.json +5 -2
  23. package/assets/worker-kits/growthub-hyperframes-studio-v1/kit.json +5 -2
  24. package/assets/worker-kits/growthub-marketing-skills-v1/kit.json +6 -3
  25. package/assets/worker-kits/growthub-open-higgsfield-studio-v1/kit.json +5 -2
  26. package/assets/worker-kits/growthub-open-montage-studio-v1/kit.json +6 -3
  27. package/assets/worker-kits/growthub-postiz-social-v1/kit.json +5 -2
  28. package/assets/worker-kits/growthub-twenty-crm-v1/kit.json +6 -3
  29. package/assets/worker-kits/growthub-video-use-studio-v1/kit.json +5 -2
  30. package/assets/worker-kits/growthub-zernio-social-v1/kit.json +5 -2
  31. package/dist/index.js +1750 -433
  32. package/package.json +1 -1
@@ -1,15 +1,12 @@
1
- const providerAliases = {
2
- ga4: "google-analytics",
3
- google_analytics: "google-analytics",
4
- google_drive: "google-drive",
5
- ghl: "go-high-level",
6
- gohighlevel: "go-high-level",
7
- meta: "meta-ads",
8
- meta_ads: "meta-ads"
9
- };
10
1
  function normalizeProviderId(provider) {
11
- const normalized = provider.trim().toLowerCase().replaceAll("_", "-");
12
- return providerAliases[normalized] || normalized;
2
+ return provider.trim().toLowerCase().replaceAll("_", "-");
3
+ }
4
+ function providerLabel(provider) {
5
+ return normalizeProviderId(provider)
6
+ .split("-")
7
+ .filter(Boolean)
8
+ .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
9
+ .join(" ");
13
10
  }
14
11
  function isHostedRecord(row) {
15
12
  return "provider" in row && ("ready" in row || "connectedAt" in row || "scopes" in row || "handle" in row);
@@ -41,10 +38,10 @@ function normalizeMcpAccount(account) {
41
38
  const isVerified = account.isVerified === true;
42
39
  const isConnected = isActive;
43
40
  return {
44
- id: provider,
45
- provider,
46
- label: account.connectionName || void 0,
47
- name: account.connectionName || void 0,
41
+ id: provider,
42
+ provider,
43
+ label: providerLabel(provider),
44
+ name: providerLabel(provider),
48
45
  authType: normalizeConnectionType(account.connectionType),
49
46
  status: isConnected ? "connected" : "needs-connection",
50
47
  isConnected,
@@ -54,7 +51,6 @@ function normalizeMcpAccount(account) {
54
51
  setupMode: "hosted-authority",
55
52
  connectionMetadata: {
56
53
  source: "growthub-mcp-accounts",
57
- accountId: account.id,
58
54
  connectionName: account.connectionName,
59
55
  connectionType: account.connectionType,
60
56
  isVerified,
@@ -1,6 +1,7 @@
1
1
  import { readAdapterConfig } from "@/lib/adapters/env";
2
2
  import {
3
- governedWorkspaceIntegrationCatalog
3
+ governedWorkspaceIntegrationCatalog,
4
+ normalizeIntegrationEntities
4
5
  } from "@/lib/domain/integrations";
5
6
  import {
6
7
  normalizeGrowthubBridgePayload
@@ -136,10 +137,10 @@ function mergeBridgeRows(rows) {
136
137
  });
137
138
  if (!row) return catalogItem;
138
139
  seenProviders.add(row.provider || row.id || catalogItem.provider);
139
- return {
140
- ...catalogItem,
141
- label: row.label || row.name || catalogItem.label,
142
- name: row.name || row.label || catalogItem.name,
140
+ return {
141
+ ...catalogItem,
142
+ label: catalogItem.label,
143
+ name: catalogItem.name,
143
144
  icon: row.icon || catalogItem.icon,
144
145
  description: row.description || catalogItem.description,
145
146
  category: row.category || catalogItem.category,
@@ -150,7 +151,6 @@ function mergeBridgeRows(rows) {
150
151
  setupMode: row.setupMode || catalogItem.setupMode,
151
152
  status: row.status || (row.isConnected || row.isActive ? "connected" : catalogItem.status),
152
153
  connectionId: row.connectionId,
153
- accountId: row.accountId,
154
154
  secretEnvName: row.secretEnvName,
155
155
  connectionMetadata: row.connectionMetadata || row.metadata,
156
156
  metadata: row.metadata || row.connectionMetadata
@@ -167,8 +167,9 @@ function mergeBridgeRows(rows) {
167
167
  function toDiscoveredIntegration(row) {
168
168
  const provider = row.provider || row.id || "unknown-provider";
169
169
  const label = row.label || row.name || provider;
170
- const isDataPipeline = ["windsor-ai", "google-sheets", "google-analytics", "shopify", "meta-ads"].includes(provider);
171
170
  const isConnected = row.isConnected ?? row.status === "connected";
171
+ const lane = typeof row.lane === "string" && row.lane ? row.lane : "workspace-integration";
172
+ const objectType = typeof row.objectType === "string" && row.objectType ? row.objectType : "mcp-connection";
172
173
  return {
173
174
  id: row.id || provider,
174
175
  label,
@@ -180,19 +181,68 @@ function toDiscoveredIntegration(row) {
180
181
  authType: row.authType || "oauth_first_party",
181
182
  isConnected,
182
183
  isActive: row.isActive ?? isConnected,
183
- lane: isDataPipeline ? "data-source" : "workspace-integration",
184
- objectType: isDataPipeline ? "data-pipeline" : "mcp-connection",
184
+ lane,
185
+ objectType,
185
186
  status: row.status || (isConnected ? "connected" : "needs-connection"),
186
187
  authPath: row.authPath || "growthub-mcp-bridge",
187
188
  setupMode: row.setupMode || "hosted-authority",
188
189
  connectionId: row.connectionId,
189
- accountId: row.accountId,
190
190
  secretEnvName: row.secretEnvName,
191
191
  connectionMetadata: row.connectionMetadata || row.metadata,
192
192
  metadata: row.metadata || row.connectionMetadata
193
193
  };
194
194
  }
195
+ /**
196
+ * Governed Integration Reference Binding — entity metadata resolution.
197
+ *
198
+ * Returns NormalizedIntegrationEntity[] for the requested integration when a
199
+ * server-side object resolver is available. Bridge connection discovery alone
200
+ * does not fabricate provider objects.
201
+ *
202
+ * Authority invariant: this function runs server-side only (API route).
203
+ * The browser NEVER calls provider APIs, holds tokens, or resolves entities.
204
+ */
205
+ async function listEntityMetadataForIntegration(integrationId) {
206
+ if (!integrationId) return [];
207
+ const config = readAdapterConfig();
208
+
209
+ if (config.integrationAdapter === "growthub-bridge" &&
210
+ config.growthubBridge?.baseUrl &&
211
+ process.env.GROWTHUB_BRIDGE_ACCESS_TOKEN) {
212
+ try {
213
+ const baseUrl = config.growthubBridge.baseUrl;
214
+ const entitiesPath = `/api/integrations/${encodeURIComponent(integrationId)}/entities`;
215
+ const url = new URL(entitiesPath, baseUrl);
216
+ const headers = {
217
+ accept: "application/json",
218
+ authorization: `Bearer ${process.env.GROWTHUB_BRIDGE_ACCESS_TOKEN}`
219
+ };
220
+ if (config.growthubBridge.userId) {
221
+ headers["x-user-id"] = config.growthubBridge.userId;
222
+ }
223
+ const response = await fetch(url, {
224
+ headers,
225
+ next: { revalidate: 30 }
226
+ });
227
+ if (response.ok) {
228
+ const payload = await response.json();
229
+ const entities = Array.isArray(payload.entities) ? payload.entities :
230
+ Array.isArray(payload.objects) ? payload.objects :
231
+ Array.isArray(payload.data) ? payload.data :
232
+ Array.isArray(payload) ? payload : [];
233
+ const normalized = normalizeIntegrationEntities(entities);
234
+ if (normalized.length) return normalized;
235
+ }
236
+ } catch {
237
+ // No fallback object data. The UI must surface the missing resolver.
238
+ }
239
+ }
240
+
241
+ return [];
242
+ }
243
+
195
244
  export {
196
245
  describeIntegrationAdapter,
197
- listGovernedWorkspaceIntegrations
246
+ listGovernedWorkspaceIntegrations,
247
+ listEntityMetadataForIntegration
198
248
  };
@@ -173,13 +173,43 @@ const workspaceIntegrations = [
173
173
  }
174
174
  ];
175
175
  const governedWorkspaceIntegrationCatalog = [...dataSources, ...workspaceIntegrations];
176
+
176
177
  function groupIntegrationsByLane(integrations) {
177
178
  return {
178
179
  dataSources: integrations.filter((item) => item.lane === "data-source"),
179
180
  workspaceIntegrations: integrations.filter((item) => item.lane === "workspace-integration")
180
181
  };
181
182
  }
183
+
184
+ function normalizeIntegrationEntity(entity) {
185
+ if (!entity || typeof entity !== "object" || Array.isArray(entity)) return null;
186
+ const id = typeof entity.id === "string" ? entity.id.trim() : "";
187
+ const label = typeof entity.label === "string" && entity.label.trim()
188
+ ? entity.label.trim()
189
+ : id;
190
+ if (!id || !label) return null;
191
+ const normalized = {
192
+ id,
193
+ label,
194
+ secondaryLabel: typeof entity.secondaryLabel === "string" ? entity.secondaryLabel : id,
195
+ entityType: typeof entity.entityType === "string" ? entity.entityType : undefined,
196
+ provider: typeof entity.provider === "string" ? entity.provider : undefined,
197
+ lane: typeof entity.lane === "string" ? entity.lane : undefined,
198
+ status: typeof entity.status === "string" ? entity.status : undefined,
199
+ metadata: entity.metadata && typeof entity.metadata === "object" && !Array.isArray(entity.metadata)
200
+ ? entity.metadata
201
+ : undefined
202
+ };
203
+ return Object.fromEntries(Object.entries(normalized).filter(([, value]) => value !== undefined));
204
+ }
205
+
206
+ function normalizeIntegrationEntities(entities) {
207
+ if (!Array.isArray(entities)) return [];
208
+ return entities.map(normalizeIntegrationEntity).filter(Boolean);
209
+ }
210
+
182
211
  export {
183
212
  governedWorkspaceIntegrationCatalog,
184
- groupIntegrationsByLane
213
+ groupIntegrationsByLane,
214
+ normalizeIntegrationEntities
185
215
  };
@@ -93,6 +93,7 @@ function applyPatch(currentConfig, patch) {
93
93
  const next = { ...currentConfig };
94
94
  if (patch.dashboards !== undefined) next.dashboards = patch.dashboards;
95
95
  if (patch.widgetTypes !== undefined) next.widgetTypes = patch.widgetTypes;
96
+ if (patch.dataModel !== undefined) next.dataModel = patch.dataModel;
96
97
  if (patch.canvas !== undefined && patch.canvas !== null) {
97
98
  const patchCanvas = { ...patch.canvas };
98
99
  if (Array.isArray(patchCanvas.tabs)) {
@@ -140,7 +141,8 @@ async function writeWorkspaceConfig(patch) {
140
141
  validateWorkspaceConfig({
141
142
  dashboards: next.dashboards,
142
143
  widgetTypes: next.widgetTypes,
143
- canvas: next.canvas
144
+ canvas: next.canvas,
145
+ dataModel: next.dataModel
144
146
  });
145
147
  const configPath = resolveWorkspaceConfigPath();
146
148
  const expectedDir = path.resolve(/*turbopackIgnore: true*/ process.cwd());
@@ -0,0 +1,433 @@
1
+ function parseCsv(text) {
2
+ const lines = String(text || "").trim().split("\n").filter(Boolean);
3
+ if (!lines.length) return { columns: [], rows: [] };
4
+ const parseLine = (line) => {
5
+ const cells = [];
6
+ let value = "";
7
+ let quoted = false;
8
+ for (let index = 0; index < line.length; index += 1) {
9
+ const char = line[index];
10
+ if (char === '"') {
11
+ if (quoted && line[index + 1] === '"') {
12
+ value += '"';
13
+ index += 1;
14
+ } else {
15
+ quoted = !quoted;
16
+ }
17
+ } else if (char === "," && !quoted) {
18
+ cells.push(value);
19
+ value = "";
20
+ } else {
21
+ value += char;
22
+ }
23
+ }
24
+ cells.push(value);
25
+ return cells;
26
+ };
27
+ const columns = parseLine(lines[0]).map((cell) => cell.trim()).filter(Boolean);
28
+ const rows = lines.slice(1).map((line) => {
29
+ const cells = parseLine(line);
30
+ return columns.reduce((record, column, index) => {
31
+ record[column] = (cells[index] || "").trim();
32
+ return record;
33
+ }, {});
34
+ });
35
+ return { columns, rows };
36
+ }
37
+
38
+ function toCsv(columns, rows) {
39
+ const escape = (value) => {
40
+ const text = String(value ?? "");
41
+ return /[",\n\r]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
42
+ };
43
+ const header = columns.map(escape).join(",");
44
+ const body = rows.map((row) => columns.map((column) => escape(row?.[column])).join(",")).join("\n");
45
+ return body ? `${header}\n${body}` : header;
46
+ }
47
+
48
+ function parseJsonRows(text) {
49
+ try {
50
+ const parsed = JSON.parse(text || "[]");
51
+ const rows = Array.isArray(parsed) ? parsed.filter((row) => row && typeof row === "object" && !Array.isArray(row)) : [];
52
+ const columns = Array.from(rows.reduce((set, row) => {
53
+ Object.keys(row).forEach((key) => set.add(key));
54
+ return set;
55
+ }, new Set()));
56
+ return { columns, rows };
57
+ } catch {
58
+ return { columns: [], rows: [] };
59
+ }
60
+ }
61
+
62
+ function listWidgetEntries(workspaceConfig) {
63
+ const entries = [];
64
+ const seen = new Set();
65
+ const push = (widget, location) => {
66
+ if (!widget?.id || seen.has(widget.id)) return;
67
+ seen.add(widget.id);
68
+ entries.push({ widget, location });
69
+ };
70
+
71
+ for (const dashboard of workspaceConfig?.dashboards || []) {
72
+ for (const tab of dashboard.tabs || []) {
73
+ for (const widget of tab.widgets || []) {
74
+ push(widget, {
75
+ dashboardId: dashboard.id,
76
+ dashboardName: dashboard.name,
77
+ tabId: tab.id,
78
+ tabName: tab.name,
79
+ widgetId: widget.id
80
+ });
81
+ }
82
+ }
83
+ }
84
+
85
+ const canvas = workspaceConfig?.canvas;
86
+ for (const tab of canvas?.tabs || []) {
87
+ for (const widget of tab.widgets || []) {
88
+ push(widget, { dashboardId: null, dashboardName: null, tabId: tab.id, tabName: tab.name, widgetId: widget.id });
89
+ }
90
+ }
91
+ for (const widget of canvas?.widgets || []) {
92
+ push(widget, { dashboardId: null, dashboardName: null, tabId: null, tabName: canvas.name || "Tab 1", widgetId: widget.id });
93
+ }
94
+
95
+ return entries;
96
+ }
97
+
98
+ function bindingColumns(binding) {
99
+ const fields = Array.isArray(binding?.fields) ? binding.fields : [];
100
+ return Array.from(new Set([...fields, "id", "label", "entityType", "provider", "lane", "status"])).filter(Boolean);
101
+ }
102
+
103
+ function deriveWidgetTable(widget, location) {
104
+ const config = widget.config || {};
105
+ const binding = config.binding && typeof config.binding === "object" && !Array.isArray(config.binding) ? config.binding : null;
106
+
107
+ if (widget.kind === "view") {
108
+ if (binding?.sourceType === "workspace-data-model") return null;
109
+ const source = config.source || widget.title || "Untitled";
110
+ const integration = binding?.mode === "integration";
111
+ const columns = integration ? bindingColumns(binding) : (Array.isArray(config.columns) ? config.columns : []);
112
+ const rows = integration && (binding.entityId || binding.entityLabel)
113
+ ? [{ id: binding.entityId || "", label: binding.entityLabel || binding.entityId || "", entityType: binding.entityType || "", provider: binding.provider || "", lane: binding.lane || "", status: binding.status || "" }]
114
+ : (Array.isArray(config.rows) ? config.rows : []);
115
+ return { source, columns, rows, binding: binding || { mode: "manual", source: "Manual rows" }, mutable: !integration, storage: "view" };
116
+ }
117
+
118
+ if (binding?.mode === "integration") {
119
+ const source = binding.entityLabel || binding.source || widget.title || "Integration reference";
120
+ const rows = binding.entityId || binding.entityLabel
121
+ ? [{ id: binding.entityId || "", label: binding.entityLabel || binding.entityId || "", entityType: binding.entityType || "", provider: binding.provider || "", lane: binding.lane || "", status: binding.status || "" }]
122
+ : [];
123
+ return { source, columns: bindingColumns(binding), rows, binding, mutable: false, storage: "integration" };
124
+ }
125
+
126
+ if (binding?.mode === "json" && typeof binding.json === "string") {
127
+ const parsed = parseJsonRows(binding.json);
128
+ return { source: binding.source || widget.title || "JSON binding", columns: parsed.columns, rows: parsed.rows, binding, mutable: true, storage: "json" };
129
+ }
130
+
131
+ if (binding?.mode === "csv" && typeof binding.csv === "string") {
132
+ const parsed = parseCsv(binding.csv);
133
+ return { source: binding.source || widget.title || "CSV binding", columns: parsed.columns, rows: parsed.rows, binding, mutable: true, storage: "csv" };
134
+ }
135
+
136
+ if (binding?.mode === "manual" && Array.isArray(binding.rows)) {
137
+ const columns = Array.from(binding.rows.reduce((set, row) => {
138
+ Object.keys(row || {}).forEach((key) => set.add(key));
139
+ return set;
140
+ }, new Set()));
141
+ return { source: binding.source || widget.title || "Manual rows", columns, rows: binding.rows, binding, mutable: true, storage: "manual-binding" };
142
+ }
143
+
144
+ if (widget.kind === "chart" && Array.isArray(config.values)) {
145
+ return {
146
+ source: widget.title || "Chart values",
147
+ columns: ["Index", "Value"],
148
+ rows: config.values.map((value, index) => ({ Index: index + 1, Value: value })),
149
+ binding: binding || { mode: "manual", source: "Chart values" },
150
+ mutable: true,
151
+ storage: "chart-values"
152
+ };
153
+ }
154
+
155
+ return null;
156
+ }
157
+
158
+ function tableId(source, columns) {
159
+ return `table:${source}:${columns.join("\0")}`;
160
+ }
161
+
162
+ function normalizeManualObjects(workspaceConfig) {
163
+ return Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
164
+ }
165
+
166
+ function deriveManualObjectTable(object) {
167
+ const columns = Array.isArray(object.columns) ? object.columns.filter(Boolean) : [];
168
+ const rows = Array.isArray(object.rows) ? object.rows.filter((row) => row && typeof row === "object" && !Array.isArray(row)) : [];
169
+ const source = object.source || object.label || object.name || "Manual object";
170
+ return {
171
+ id: `manual-object:${object.id || source}`,
172
+ label: object.label || object.name || source,
173
+ source,
174
+ columns,
175
+ rows,
176
+ binding: object.binding || { mode: "manual", source: "Data Model" },
177
+ mutable: true,
178
+ storage: "manual-object",
179
+ objectId: object.id,
180
+ widgetRefs: [],
181
+ fieldSettings: {
182
+ hidden: Array.isArray(object.fieldSettings?.hidden) ? object.fieldSettings.hidden : [],
183
+ order: Array.isArray(object.fieldSettings?.order) ? object.fieldSettings.order : columns
184
+ }
185
+ };
186
+ }
187
+
188
+ function listWorkspaceDataModelTables(workspaceConfig) {
189
+ const widgetEntries = listWidgetEntries(workspaceConfig);
190
+ const refsByObjectId = widgetEntries.reduce((map, { widget, location }) => {
191
+ const binding = widget?.config?.binding;
192
+ if (binding?.sourceType !== "workspace-data-model" || !binding.objectId) return map;
193
+ const refs = map.get(binding.objectId) || [];
194
+ refs.push({
195
+ ...location,
196
+ widgetTitle: widget.title,
197
+ widgetKind: widget.kind
198
+ });
199
+ map.set(binding.objectId, refs);
200
+ return map;
201
+ }, new Map());
202
+ const manualObjects = normalizeManualObjects(workspaceConfig).map((object) => {
203
+ const table = deriveManualObjectTable(object);
204
+ return { ...table, widgetRefs: refsByObjectId.get(object.id) || [] };
205
+ });
206
+ const widgetTables = widgetEntries
207
+ .map(({ widget, location }) => {
208
+ const table = deriveWidgetTable(widget, location);
209
+ if (!table) return null;
210
+ return {
211
+ id: tableId(table.source, table.columns),
212
+ label: table.source,
213
+ source: table.source,
214
+ columns: table.columns,
215
+ rows: table.rows,
216
+ binding: table.binding,
217
+ mutable: table.mutable,
218
+ storage: table.storage,
219
+ widgetRefs: [{
220
+ ...location,
221
+ widgetTitle: widget.title,
222
+ widgetKind: widget.kind
223
+ }],
224
+ fieldSettings: {
225
+ hidden: Array.isArray(widget.config?.fieldSettings?.hidden) ? widget.config.fieldSettings.hidden : [],
226
+ order: Array.isArray(widget.config?.fieldSettings?.order) ? widget.config.fieldSettings.order : table.columns
227
+ }
228
+ };
229
+ })
230
+ .filter(Boolean);
231
+ return [...manualObjects, ...widgetTables];
232
+ }
233
+
234
+ function writeTableConfig(config, storage, columns, rows) {
235
+ if (storage === "view") {
236
+ const binding = config.binding?.mode === "manual" ? { ...config.binding, rows } : config.binding;
237
+ return { ...config, columns, rows, binding, fieldSettings: { hidden: config.fieldSettings?.hidden || [], order: columns } };
238
+ }
239
+ if (storage === "json") {
240
+ return { ...config, binding: { ...config.binding, json: JSON.stringify(rows, null, 2) } };
241
+ }
242
+ if (storage === "csv") {
243
+ return { ...config, binding: { ...config.binding, csv: toCsv(columns, rows) } };
244
+ }
245
+ if (storage === "manual-binding") {
246
+ return { ...config, binding: { ...config.binding, rows } };
247
+ }
248
+ if (storage === "chart-values") {
249
+ return { ...config, values: rows.map((row) => Number(row.Value)).filter((value) => Number.isFinite(value)) };
250
+ }
251
+ return config;
252
+ }
253
+
254
+ function applyTableMutation(workspaceConfig, table, mutate) {
255
+ if (table.storage === "manual-object") {
256
+ const objects = normalizeManualObjects(workspaceConfig);
257
+ const dataModel = workspaceConfig.dataModel && typeof workspaceConfig.dataModel === "object" && !Array.isArray(workspaceConfig.dataModel)
258
+ ? workspaceConfig.dataModel
259
+ : {};
260
+ return {
261
+ ...workspaceConfig,
262
+ dataModel: {
263
+ ...dataModel,
264
+ objects: objects.map((object) => {
265
+ if (object.id !== table.objectId) return object;
266
+ const current = deriveManualObjectTable(object);
267
+ const next = mutate({ columns: current.columns, rows: current.rows });
268
+ return {
269
+ ...object,
270
+ columns: next.columns,
271
+ rows: next.rows,
272
+ fieldSettings: { ...(object.fieldSettings || {}), order: next.columns }
273
+ };
274
+ })
275
+ }
276
+ };
277
+ }
278
+
279
+ const ids = new Set((table.widgetRefs || []).map((ref) => ref.widgetId));
280
+ const mutateWidgets = (widgets) => (widgets || []).map((widget) => {
281
+ if (!ids.has(widget.id)) return widget;
282
+ const current = deriveWidgetTable(widget, { widgetId: widget.id });
283
+ if (!current?.mutable) return widget;
284
+ const next = mutate({ columns: current.columns, rows: current.rows });
285
+ return { ...widget, config: writeTableConfig(widget.config || {}, current.storage, next.columns, next.rows) };
286
+ });
287
+
288
+ const dashboards = (workspaceConfig.dashboards || []).map((dashboard) => ({
289
+ ...dashboard,
290
+ tabs: (dashboard.tabs || []).map((tab) => ({ ...tab, widgets: mutateWidgets(tab.widgets) }))
291
+ }));
292
+ let canvas = workspaceConfig.canvas ? { ...workspaceConfig.canvas } : {};
293
+ if (Array.isArray(canvas.widgets)) canvas = { ...canvas, widgets: mutateWidgets(canvas.widgets) };
294
+ if (Array.isArray(canvas.tabs)) canvas = { ...canvas, tabs: canvas.tabs.map((tab) => ({ ...tab, widgets: mutateWidgets(tab.widgets) })) };
295
+ return { ...workspaceConfig, dashboards, canvas };
296
+ }
297
+
298
+ function slugifyObjectName(name) {
299
+ return String(name || "")
300
+ .trim()
301
+ .toLowerCase()
302
+ .replace(/[^a-z0-9]+/g, "-")
303
+ .replace(/^-+|-+$/g, "")
304
+ || "object";
305
+ }
306
+
307
+ function uniqueObjectId(workspaceConfig, name) {
308
+ const base = slugifyObjectName(name);
309
+ const used = new Set(normalizeManualObjects(workspaceConfig).map((object) => object.id));
310
+ if (!used.has(base)) return base;
311
+ let index = 2;
312
+ while (used.has(`${base}-${index}`)) index += 1;
313
+ return `${base}-${index}`;
314
+ }
315
+
316
+ function createManualBusinessObject(workspaceConfig, { name, fields } = {}) {
317
+ const label = String(name || "").trim();
318
+ const columns = Array.from(new Set((Array.isArray(fields) ? fields : String(fields || "").split(","))
319
+ .map((field) => String(field || "").trim())
320
+ .filter(Boolean)));
321
+ if (!label || !columns.length) return workspaceConfig;
322
+ const dataModel = workspaceConfig.dataModel && typeof workspaceConfig.dataModel === "object" && !Array.isArray(workspaceConfig.dataModel)
323
+ ? workspaceConfig.dataModel
324
+ : {};
325
+ const id = uniqueObjectId(workspaceConfig, label);
326
+ const object = {
327
+ id,
328
+ label,
329
+ source: label,
330
+ columns,
331
+ rows: [],
332
+ binding: { mode: "manual", source: "Data Model" },
333
+ fieldSettings: { hidden: [], order: columns }
334
+ };
335
+ return {
336
+ ...workspaceConfig,
337
+ dataModel: {
338
+ ...dataModel,
339
+ objects: [...normalizeManualObjects(workspaceConfig), object]
340
+ }
341
+ };
342
+ }
343
+
344
+ function addTableField(workspaceConfig, table, fieldName) {
345
+ const name = String(fieldName || "").trim();
346
+ if (!name || !table.mutable) return workspaceConfig;
347
+ return applyTableMutation(workspaceConfig, table, ({ columns, rows }) => {
348
+ if (columns.includes(name)) return { columns, rows };
349
+ return { columns: [...columns, name], rows: rows.map((row) => ({ ...row, [name]: "" })) };
350
+ });
351
+ }
352
+
353
+ function addTableRow(workspaceConfig, table) {
354
+ if (!table.mutable) return workspaceConfig;
355
+ return applyTableMutation(workspaceConfig, table, ({ columns, rows }) => ({
356
+ columns,
357
+ rows: [...rows, Object.fromEntries(columns.map((column) => [column, ""]))]
358
+ }));
359
+ }
360
+
361
+ function updateTableCell(workspaceConfig, table, rowIndex, fieldName, value) {
362
+ if (!table.mutable) return workspaceConfig;
363
+ return applyTableMutation(workspaceConfig, table, ({ columns, rows }) => ({
364
+ columns,
365
+ rows: rows.map((row, index) => index === rowIndex ? { ...row, [fieldName]: value } : row)
366
+ }));
367
+ }
368
+
369
+ function deleteTableRow(workspaceConfig, table, rowIndex) {
370
+ if (!table.mutable) return workspaceConfig;
371
+ return applyTableMutation(workspaceConfig, table, ({ columns, rows }) => ({
372
+ columns,
373
+ rows: rows.filter((_, index) => index !== rowIndex)
374
+ }));
375
+ }
376
+
377
+ function duplicateTableRow(workspaceConfig, table, rowIndex) {
378
+ if (!table.mutable) return workspaceConfig;
379
+ return applyTableMutation(workspaceConfig, table, ({ columns, rows }) => {
380
+ const next = [...rows];
381
+ if (rows[rowIndex]) next.splice(rowIndex + 1, 0, { ...rows[rowIndex] });
382
+ return { columns, rows: next };
383
+ });
384
+ }
385
+
386
+ function appendRowsToTable(workspaceConfig, table, rowsToAppend) {
387
+ if (!table.mutable || !Array.isArray(rowsToAppend)) return workspaceConfig;
388
+ return applyTableMutation(workspaceConfig, table, ({ columns, rows }) => ({ columns, rows: [...rows, ...rowsToAppend] }));
389
+ }
390
+
391
+ function replaceTableContent(workspaceConfig, table, { columns = [], rows = [] } = {}) {
392
+ if (!table.mutable) return workspaceConfig;
393
+ return applyTableMutation(workspaceConfig, table, () => ({ columns, rows }));
394
+ }
395
+
396
+ function exportTableAsCsv(table) {
397
+ return toCsv(table.columns || [], table.rows || []);
398
+ }
399
+
400
+ function importTableFromCsv(text) {
401
+ return parseCsv(text);
402
+ }
403
+
404
+ function describeBindingLane(binding) {
405
+ if (binding?.mode === "integration" && binding.lane === "data-source") return "data-source";
406
+ if (binding?.mode === "integration" && binding.lane === "workspace-integration") return "workspace-integration";
407
+ if (binding?.mode === "integration") return "integration";
408
+ return "manual";
409
+ }
410
+
411
+ function describeBindingMode(binding) {
412
+ const lane = describeBindingLane(binding);
413
+ if (lane === "data-source") return { label: "Data source scope", description: "Integration reference selected in the existing widget source flow. Dynamic data resolves through the governed server-side integration path." };
414
+ if (lane === "workspace-integration") return { label: "Workspace tool scope", description: "Workspace integration reference selected in the existing widget source flow." };
415
+ if (lane === "integration") return { label: "Integration scope", description: "Integration reference stored on widget.config.binding." };
416
+ return { label: "Manual local table", description: "Rows and fields live in the existing widget config and travel with workspace export/import." };
417
+ }
418
+
419
+ export {
420
+ addTableField,
421
+ addTableRow,
422
+ appendRowsToTable,
423
+ createManualBusinessObject,
424
+ deleteTableRow,
425
+ describeBindingLane,
426
+ describeBindingMode,
427
+ duplicateTableRow,
428
+ exportTableAsCsv,
429
+ importTableFromCsv,
430
+ listWorkspaceDataModelTables,
431
+ replaceTableContent,
432
+ updateTableCell
433
+ };