coalesce-transform-mcp 0.2.3 → 0.4.1
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/README.md +84 -2
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +27 -3
- package/dist/client.js.map +1 -1
- package/dist/coalesce/api/environments.d.ts +0 -12
- package/dist/coalesce/api/environments.d.ts.map +1 -1
- package/dist/coalesce/api/environments.js +0 -4
- package/dist/coalesce/api/environments.js.map +1 -1
- package/dist/coalesce/api/jobs.d.ts +3 -5
- package/dist/coalesce/api/jobs.d.ts.map +1 -1
- package/dist/coalesce/api/jobs.js +3 -6
- package/dist/coalesce/api/jobs.js.map +1 -1
- package/dist/coalesce/api/nodes.d.ts +3 -3
- package/dist/coalesce/api/nodes.d.ts.map +1 -1
- package/dist/coalesce/api/nodes.js +6 -4
- package/dist/coalesce/api/nodes.js.map +1 -1
- package/dist/coalesce/api/runs.d.ts.map +1 -1
- package/dist/coalesce/api/runs.js +11 -1
- package/dist/coalesce/api/runs.js.map +1 -1
- package/dist/coalesce/api/scan.d.ts +14 -0
- package/dist/coalesce/api/scan.d.ts.map +1 -0
- package/dist/coalesce/api/scan.js +64 -0
- package/dist/coalesce/api/scan.js.map +1 -0
- package/dist/coalesce/api/subgraphs.d.ts +3 -2
- package/dist/coalesce/api/subgraphs.d.ts.map +1 -1
- package/dist/coalesce/api/subgraphs.js +3 -2
- package/dist/coalesce/api/subgraphs.js.map +1 -1
- package/dist/coalesce/run-schemas.d.ts.map +1 -1
- package/dist/coalesce/run-schemas.js +26 -16
- package/dist/coalesce/run-schemas.js.map +1 -1
- package/dist/coalesce/tool-response.d.ts +1 -13
- package/dist/coalesce/tool-response.d.ts.map +1 -1
- package/dist/coalesce/tool-response.js +20 -6
- package/dist/coalesce/tool-response.js.map +1 -1
- package/dist/coalesce/tool-schemas.d.ts +1 -2
- package/dist/coalesce/tool-schemas.d.ts.map +1 -1
- package/dist/coalesce/tool-schemas.js +368 -5
- package/dist/coalesce/tool-schemas.js.map +1 -1
- package/dist/coalesce/types.d.ts +8 -0
- package/dist/coalesce/types.d.ts.map +1 -1
- package/dist/coalesce/types.js +8 -3
- package/dist/coalesce/types.js.map +1 -1
- package/dist/constants.d.ts +18 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +21 -0
- package/dist/constants.js.map +1 -0
- package/dist/mcp/cache.d.ts +2 -1
- package/dist/mcp/cache.d.ts.map +1 -1
- package/dist/mcp/cache.js +122 -138
- package/dist/mcp/cache.js.map +1 -1
- package/dist/mcp/environments.d.ts +2 -1
- package/dist/mcp/environments.d.ts.map +1 -1
- package/dist/mcp/environments.js +56 -112
- package/dist/mcp/environments.js.map +1 -1
- package/dist/mcp/git-accounts.d.ts +2 -1
- package/dist/mcp/git-accounts.d.ts.map +1 -1
- package/dist/mcp/git-accounts.js +74 -96
- package/dist/mcp/git-accounts.js.map +1 -1
- package/dist/mcp/jobs.d.ts +2 -1
- package/dist/mcp/jobs.d.ts.map +1 -1
- package/dist/mcp/jobs.js +68 -122
- package/dist/mcp/jobs.js.map +1 -1
- package/dist/mcp/lineage.d.ts +5 -0
- package/dist/mcp/lineage.d.ts.map +1 -0
- package/dist/mcp/lineage.js +410 -0
- package/dist/mcp/lineage.js.map +1 -0
- package/dist/mcp/node-type-corpus.d.ts +2 -1
- package/dist/mcp/node-type-corpus.d.ts.map +1 -1
- package/dist/mcp/node-type-corpus.js +148 -151
- package/dist/mcp/node-type-corpus.js.map +1 -1
- package/dist/mcp/nodes.d.ts +2 -1
- package/dist/mcp/nodes.d.ts.map +1 -1
- package/dist/mcp/nodes.js +358 -464
- package/dist/mcp/nodes.js.map +1 -1
- package/dist/mcp/pipelines.d.ts +2 -1
- package/dist/mcp/pipelines.d.ts.map +1 -1
- package/dist/mcp/pipelines.js +514 -314
- package/dist/mcp/pipelines.js.map +1 -1
- package/dist/mcp/projects.d.ts +2 -1
- package/dist/mcp/projects.d.ts.map +1 -1
- package/dist/mcp/projects.js +66 -100
- package/dist/mcp/projects.js.map +1 -1
- package/dist/mcp/repo-node-types.d.ts +2 -1
- package/dist/mcp/repo-node-types.d.ts.map +1 -1
- package/dist/mcp/repo-node-types.js +92 -121
- package/dist/mcp/repo-node-types.js.map +1 -1
- package/dist/mcp/runs.d.ts +4 -1
- package/dist/mcp/runs.d.ts.map +1 -1
- package/dist/mcp/runs.js +94 -147
- package/dist/mcp/runs.js.map +1 -1
- package/dist/mcp/skills.d.ts +13 -0
- package/dist/mcp/skills.d.ts.map +1 -0
- package/dist/mcp/skills.js +85 -0
- package/dist/mcp/skills.js.map +1 -0
- package/dist/mcp/subgraphs.d.ts +2 -1
- package/dist/mcp/subgraphs.d.ts.map +1 -1
- package/dist/mcp/subgraphs.js +61 -98
- package/dist/mcp/subgraphs.js.map +1 -1
- package/dist/mcp/tool-helpers.d.ts +37 -0
- package/dist/mcp/tool-helpers.d.ts.map +1 -0
- package/dist/mcp/tool-helpers.js +82 -0
- package/dist/mcp/tool-helpers.js.map +1 -0
- package/dist/mcp/users.d.ts +2 -1
- package/dist/mcp/users.d.ts.map +1 -1
- package/dist/mcp/users.js +92 -145
- package/dist/mcp/users.js.map +1 -1
- package/dist/mcp/workshop.d.ts +2 -1
- package/dist/mcp/workshop.d.ts.map +1 -1
- package/dist/mcp/workshop.js +66 -101
- package/dist/mcp/workshop.js.map +1 -1
- package/dist/mcp/workspaces.d.ts +2 -1
- package/dist/mcp/workspaces.d.ts.map +1 -1
- package/dist/mcp/workspaces.js +19 -34
- package/dist/mcp/workspaces.js.map +1 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +85 -0
- package/dist/prompts/index.js.map +1 -1
- package/dist/resources/context/pipeline-workshop-guide.md +1 -1
- package/dist/resources/context/tool-usage.md +7 -0
- package/dist/resources/index.d.ts +13 -0
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +105 -5
- package/dist/resources/index.js.map +1 -1
- package/dist/schemas/node-payloads.d.ts +2 -2
- package/dist/server.d.ts +2 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +173 -45
- package/dist/server.js.map +1 -1
- package/dist/services/cache/snapshots.d.ts.map +1 -1
- package/dist/services/cache/snapshots.js +9 -5
- package/dist/services/cache/snapshots.js.map +1 -1
- package/dist/services/config/schema-resolver.d.ts.map +1 -1
- package/dist/services/config/schema-resolver.js +3 -6
- package/dist/services/config/schema-resolver.js.map +1 -1
- package/dist/services/lineage/lineage-cache.d.ts +53 -0
- package/dist/services/lineage/lineage-cache.d.ts.map +1 -0
- package/dist/services/lineage/lineage-cache.js +335 -0
- package/dist/services/lineage/lineage-cache.js.map +1 -0
- package/dist/services/lineage/lineage-documentation.d.ts +29 -0
- package/dist/services/lineage/lineage-documentation.d.ts.map +1 -0
- package/dist/services/lineage/lineage-documentation.js +80 -0
- package/dist/services/lineage/lineage-documentation.js.map +1 -0
- package/dist/services/lineage/lineage-propagation.d.ts +47 -0
- package/dist/services/lineage/lineage-propagation.d.ts.map +1 -0
- package/dist/services/lineage/lineage-propagation.js +176 -0
- package/dist/services/lineage/lineage-propagation.js.map +1 -0
- package/dist/services/lineage/lineage-search.d.ts +33 -0
- package/dist/services/lineage/lineage-search.d.ts.map +1 -0
- package/dist/services/lineage/lineage-search.js +133 -0
- package/dist/services/lineage/lineage-search.js.map +1 -0
- package/dist/services/lineage/lineage-traversal.d.ts +34 -0
- package/dist/services/lineage/lineage-traversal.d.ts.map +1 -0
- package/dist/services/lineage/lineage-traversal.js +283 -0
- package/dist/services/lineage/lineage-traversal.js.map +1 -0
- package/dist/services/pipelines/clause-extraction.d.ts +3 -0
- package/dist/services/pipelines/clause-extraction.d.ts.map +1 -0
- package/dist/services/pipelines/clause-extraction.js +27 -0
- package/dist/services/pipelines/clause-extraction.js.map +1 -0
- package/dist/services/pipelines/column-helpers.d.ts +8 -0
- package/dist/services/pipelines/column-helpers.d.ts.map +1 -0
- package/dist/services/pipelines/column-helpers.js +125 -0
- package/dist/services/pipelines/column-helpers.js.map +1 -0
- package/dist/services/pipelines/cte-parsing.d.ts +29 -0
- package/dist/services/pipelines/cte-parsing.d.ts.map +1 -0
- package/dist/services/pipelines/cte-parsing.js +160 -0
- package/dist/services/pipelines/cte-parsing.js.map +1 -0
- package/dist/services/pipelines/cte-planning.d.ts +22 -0
- package/dist/services/pipelines/cte-planning.d.ts.map +1 -0
- package/dist/services/pipelines/cte-planning.js +206 -0
- package/dist/services/pipelines/cte-planning.js.map +1 -0
- package/dist/services/pipelines/execution.d.ts.map +1 -1
- package/dist/services/pipelines/execution.js +0 -1
- package/dist/services/pipelines/execution.js.map +1 -1
- package/dist/services/pipelines/intent-parsing.d.ts +24 -0
- package/dist/services/pipelines/intent-parsing.d.ts.map +1 -0
- package/dist/services/pipelines/intent-parsing.js +245 -0
- package/dist/services/pipelines/intent-parsing.js.map +1 -0
- package/dist/services/pipelines/intent-resolution.d.ts +24 -0
- package/dist/services/pipelines/intent-resolution.d.ts.map +1 -0
- package/dist/services/pipelines/intent-resolution.js +141 -0
- package/dist/services/pipelines/intent-resolution.js.map +1 -0
- package/dist/services/pipelines/intent.d.ts +4 -45
- package/dist/services/pipelines/intent.d.ts.map +1 -1
- package/dist/services/pipelines/intent.js +14 -408
- package/dist/services/pipelines/intent.js.map +1 -1
- package/dist/services/pipelines/node-type-candidates.d.ts +6 -0
- package/dist/services/pipelines/node-type-candidates.d.ts.map +1 -0
- package/dist/services/pipelines/node-type-candidates.js +165 -0
- package/dist/services/pipelines/node-type-candidates.js.map +1 -0
- package/dist/services/pipelines/node-type-intent.d.ts +1 -5
- package/dist/services/pipelines/node-type-intent.d.ts.map +1 -1
- package/dist/services/pipelines/node-type-intent.js +1 -5
- package/dist/services/pipelines/node-type-intent.js.map +1 -1
- package/dist/services/pipelines/node-type-scoring.d.ts +13 -0
- package/dist/services/pipelines/node-type-scoring.d.ts.map +1 -0
- package/dist/services/pipelines/node-type-scoring.js +322 -0
- package/dist/services/pipelines/node-type-scoring.js.map +1 -0
- package/dist/services/pipelines/node-type-selection.d.ts +22 -2
- package/dist/services/pipelines/node-type-selection.d.ts.map +1 -1
- package/dist/services/pipelines/node-type-selection.js +16 -538
- package/dist/services/pipelines/node-type-selection.js.map +1 -1
- package/dist/services/pipelines/plan-builder.d.ts +33 -0
- package/dist/services/pipelines/plan-builder.d.ts.map +1 -0
- package/dist/services/pipelines/plan-builder.js +224 -0
- package/dist/services/pipelines/plan-builder.js.map +1 -0
- package/dist/services/pipelines/planning-types.d.ts +543 -0
- package/dist/services/pipelines/planning-types.d.ts.map +1 -0
- package/dist/services/pipelines/planning-types.js +85 -0
- package/dist/services/pipelines/planning-types.js.map +1 -0
- package/dist/services/pipelines/planning.d.ts +8 -537
- package/dist/services/pipelines/planning.d.ts.map +1 -1
- package/dist/services/pipelines/planning.js +10 -1956
- package/dist/services/pipelines/planning.js.map +1 -1
- package/dist/services/pipelines/review.d.ts.map +1 -1
- package/dist/services/pipelines/review.js +3 -8
- package/dist/services/pipelines/review.js.map +1 -1
- package/dist/services/pipelines/select-parsing.d.ts +7 -0
- package/dist/services/pipelines/select-parsing.d.ts.map +1 -0
- package/dist/services/pipelines/select-parsing.js +185 -0
- package/dist/services/pipelines/select-parsing.js.map +1 -0
- package/dist/services/pipelines/source-parsing.d.ts +8 -0
- package/dist/services/pipelines/source-parsing.d.ts.map +1 -0
- package/dist/services/pipelines/source-parsing.js +151 -0
- package/dist/services/pipelines/source-parsing.js.map +1 -0
- package/dist/services/pipelines/sql-parsing.d.ts +8 -0
- package/dist/services/pipelines/sql-parsing.d.ts.map +1 -0
- package/dist/services/pipelines/sql-parsing.js +9 -0
- package/dist/services/pipelines/sql-parsing.js.map +1 -0
- package/dist/services/pipelines/sql-tokenizer.d.ts +42 -0
- package/dist/services/pipelines/sql-tokenizer.d.ts.map +1 -0
- package/dist/services/pipelines/sql-tokenizer.js +493 -0
- package/dist/services/pipelines/sql-tokenizer.js.map +1 -0
- package/dist/services/pipelines/sql-utils.d.ts +30 -0
- package/dist/services/pipelines/sql-utils.d.ts.map +1 -0
- package/dist/services/pipelines/sql-utils.js +62 -0
- package/dist/services/pipelines/sql-utils.js.map +1 -0
- package/dist/services/pipelines/workshop.d.ts.map +1 -1
- package/dist/services/pipelines/workshop.js +53 -25
- package/dist/services/pipelines/workshop.js.map +1 -1
- package/dist/services/pipelines/workspace-resolution.d.ts +18 -0
- package/dist/services/pipelines/workspace-resolution.d.ts.map +1 -0
- package/dist/services/pipelines/workspace-resolution.js +279 -0
- package/dist/services/pipelines/workspace-resolution.js.map +1 -0
- package/dist/services/runs/diagnostics.d.ts.map +1 -1
- package/dist/services/runs/diagnostics.js +3 -8
- package/dist/services/runs/diagnostics.js.map +1 -1
- package/dist/services/shared/elicitation.d.ts +14 -0
- package/dist/services/shared/elicitation.d.ts.map +1 -0
- package/dist/services/shared/elicitation.js +56 -0
- package/dist/services/shared/elicitation.js.map +1 -0
- package/dist/services/workspace/join-operations.d.ts +56 -0
- package/dist/services/workspace/join-operations.d.ts.map +1 -0
- package/dist/services/workspace/join-operations.js +303 -0
- package/dist/services/workspace/join-operations.js.map +1 -0
- package/dist/services/workspace/mutations.d.ts +3 -99
- package/dist/services/workspace/mutations.d.ts.map +1 -1
- package/dist/services/workspace/mutations.js +13 -1220
- package/dist/services/workspace/mutations.js.map +1 -1
- package/dist/services/workspace/node-creation.d.ts +48 -0
- package/dist/services/workspace/node-creation.d.ts.map +1 -0
- package/dist/services/workspace/node-creation.js +448 -0
- package/dist/services/workspace/node-creation.js.map +1 -0
- package/dist/services/workspace/node-update-helpers.d.ts +89 -0
- package/dist/services/workspace/node-update-helpers.d.ts.map +1 -0
- package/dist/services/workspace/node-update-helpers.js +492 -0
- package/dist/services/workspace/node-update-helpers.js.map +1 -0
- package/dist/tasks/index.d.ts +5 -0
- package/dist/tasks/index.d.ts.map +1 -0
- package/dist/tasks/index.js +10 -0
- package/dist/tasks/index.js.map +1 -0
- package/dist/tasks/retry-and-wait-task.d.ts +4 -0
- package/dist/tasks/retry-and-wait-task.d.ts.map +1 -0
- package/dist/tasks/retry-and-wait-task.js +61 -0
- package/dist/tasks/retry-and-wait-task.js.map +1 -0
- package/dist/tasks/run-and-wait-task.d.ts +4 -0
- package/dist/tasks/run-and-wait-task.d.ts.map +1 -0
- package/dist/tasks/run-and-wait-task.js +61 -0
- package/dist/tasks/run-and-wait-task.js.map +1 -0
- package/dist/tasks/start-run-task.d.ts +4 -0
- package/dist/tasks/start-run-task.d.ts.map +1 -0
- package/dist/tasks/start-run-task.js +47 -0
- package/dist/tasks/start-run-task.js.map +1 -0
- package/dist/tasks/store.d.ts +2 -0
- package/dist/tasks/store.d.ts.map +1 -0
- package/dist/tasks/store.js +2 -0
- package/dist/tasks/store.js.map +1 -0
- package/dist/utils.d.ts +11 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +20 -1
- package/dist/utils.js.map +1 -1
- package/dist/workflows/get-environment-health.d.ts +49 -0
- package/dist/workflows/get-environment-health.d.ts.map +1 -0
- package/dist/workflows/get-environment-health.js +310 -0
- package/dist/workflows/get-environment-health.js.map +1 -0
- package/dist/workflows/get-environment-overview.d.ts +2 -1
- package/dist/workflows/get-environment-overview.d.ts.map +1 -1
- package/dist/workflows/get-environment-overview.js +13 -19
- package/dist/workflows/get-environment-overview.js.map +1 -1
- package/dist/workflows/get-run-details.d.ts +2 -2
- package/dist/workflows/get-run-details.d.ts.map +1 -1
- package/dist/workflows/get-run-details.js +14 -19
- package/dist/workflows/get-run-details.js.map +1 -1
- package/dist/workflows/poll-run.d.ts +14 -0
- package/dist/workflows/poll-run.d.ts.map +1 -0
- package/dist/workflows/poll-run.js +86 -0
- package/dist/workflows/poll-run.js.map +1 -0
- package/dist/workflows/progress.d.ts +1 -0
- package/dist/workflows/progress.d.ts.map +1 -1
- package/dist/workflows/progress.js +3 -0
- package/dist/workflows/progress.js.map +1 -1
- package/dist/workflows/retry-and-wait.d.ts +1 -1
- package/dist/workflows/retry-and-wait.d.ts.map +1 -1
- package/dist/workflows/retry-and-wait.js +18 -89
- package/dist/workflows/retry-and-wait.js.map +1 -1
- package/dist/workflows/run-and-wait.d.ts +1 -1
- package/dist/workflows/run-and-wait.d.ts.map +1 -1
- package/dist/workflows/run-and-wait.js +18 -89
- package/dist/workflows/run-and-wait.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,497 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { CoalesceApiError } from "../../client.js";
|
|
3
|
-
import { getWorkspaceNode, setWorkspaceNode, createWorkspaceNode, } from "../../coalesce/api/nodes.js";
|
|
1
|
+
import { getWorkspaceNode, setWorkspaceNode, } from "../../coalesce/api/nodes.js";
|
|
4
2
|
import { fetchAllWorkspaceNodes } from "../cache/snapshots.js";
|
|
5
3
|
import { assertNoSqlOverridePayload } from "../policies/sql-override.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return { configCompletion };
|
|
18
|
-
}
|
|
19
|
-
catch (error) {
|
|
20
|
-
if (error instanceof CoalesceApiError && [401, 403, 503].includes(error.status)) {
|
|
21
|
-
throw error;
|
|
22
|
-
}
|
|
23
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
24
|
-
return { configCompletionSkipped: `Config completion failed (${reason}) — ${CONFIG_COMPLETION_SKIP_MSG}` };
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Validates the requested nodeType against all available types from repo + workspace.
|
|
29
|
-
* Returns ranking info so the agent (and user) can see all considered options.
|
|
30
|
-
* Throws if the requested type is excluded (e.g., inputMode: 'sql').
|
|
31
|
-
* Returns null on non-critical validation failures (creation proceeds).
|
|
32
|
-
*/
|
|
33
|
-
async function validateNodeTypeChoice(client, params) {
|
|
34
|
-
try {
|
|
35
|
-
const nodeTypesResult = await listWorkspaceNodeTypes(client, {
|
|
36
|
-
workspaceID: params.workspaceID,
|
|
37
|
-
});
|
|
38
|
-
const selectionResult = selectPipelineNodeType({
|
|
39
|
-
explicitNodeType: params.nodeType,
|
|
40
|
-
sourceCount: params.predecessorCount,
|
|
41
|
-
workspaceNodeTypes: nodeTypesResult.nodeTypes,
|
|
42
|
-
workspaceNodeTypeCounts: nodeTypesResult.counts,
|
|
43
|
-
repoPath: params.repoPath,
|
|
44
|
-
goal: params.goal,
|
|
45
|
-
});
|
|
46
|
-
// Hard block: if the requested type was excluded (e.g., inputMode: 'sql'), throw
|
|
47
|
-
const exclusionWarning = selectionResult.warnings.find((w) => w.includes("is excluded because it relies on raw SQL override"));
|
|
48
|
-
if (exclusionWarning) {
|
|
49
|
-
throw new Error(exclusionWarning);
|
|
50
|
-
}
|
|
51
|
-
const topRanked = selectionResult.selection.consideredNodeTypes[0] ?? null;
|
|
52
|
-
const isTopRanked = topRanked?.nodeType === params.nodeType;
|
|
53
|
-
const validation = {
|
|
54
|
-
requestedNodeType: params.nodeType,
|
|
55
|
-
topRankedNodeType: topRanked?.nodeType ?? null,
|
|
56
|
-
isTopRanked,
|
|
57
|
-
strategy: selectionResult.selection.strategy,
|
|
58
|
-
consideredNodeTypes: selectionResult.selection.consideredNodeTypes.slice(0, 5).map((c) => ({
|
|
59
|
-
nodeType: c.nodeType,
|
|
60
|
-
displayName: c.displayName,
|
|
61
|
-
score: c.score,
|
|
62
|
-
reasons: c.reasons,
|
|
63
|
-
})),
|
|
64
|
-
};
|
|
65
|
-
if (!isTopRanked && topRanked) {
|
|
66
|
-
validation.warning =
|
|
67
|
-
`Requested nodeType "${params.nodeType}" is not the top-ranked type. ` +
|
|
68
|
-
`Consider using "${topRanked.nodeType}" (score: ${topRanked.score}) instead. ` +
|
|
69
|
-
`Call plan_pipeline first to discover and rank all available node types.`;
|
|
70
|
-
}
|
|
71
|
-
// Hard block specialized materialization patterns (Dynamic Tables, Incremental, etc.)
|
|
72
|
-
// unless the goal explicitly requests them.
|
|
73
|
-
// Build candidate signals from the matched type's display name and short name,
|
|
74
|
-
// since the raw nodeType ID (e.g., "65") won't match pattern detection.
|
|
75
|
-
const matchedCandidate = selectionResult.selection.consideredNodeTypes.find((c) => c.nodeType === params.nodeType);
|
|
76
|
-
const candidateSignals = [
|
|
77
|
-
params.nodeType,
|
|
78
|
-
matchedCandidate?.displayName ?? "",
|
|
79
|
-
matchedCandidate?.shortName ?? "",
|
|
80
|
-
].join(" ");
|
|
81
|
-
const contextText = params.goal ?? "";
|
|
82
|
-
const specializedPenalty = detectSpecializedPatternPenalty(candidateSignals, contextText);
|
|
83
|
-
if (specializedPenalty) {
|
|
84
|
-
// Hard block — specialized types require explicit context
|
|
85
|
-
const patternName = specializedPenalty.reason.split(" pattern")[0] ?? "Specialized";
|
|
86
|
-
throw new Error(`Cannot create node with nodeType "${params.nodeType}" (${matchedCandidate?.displayName ?? "unknown"}): ` +
|
|
87
|
-
`${specializedPenalty.reason}. ` +
|
|
88
|
-
`${patternName} types require an explicit use case (e.g., "${patternName.toLowerCase()}" in the goal). ` +
|
|
89
|
-
`For standard batch ETL, staging, joins, and aggregations, use a general-purpose type ` +
|
|
90
|
-
`(Stage, Work, View, Dimension, Fact). ` +
|
|
91
|
-
`Call plan_pipeline to discover the correct nodeType for your use case.`);
|
|
92
|
-
}
|
|
93
|
-
return validation;
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
// Auth and network errors indicate a broken session — let them propagate
|
|
97
|
-
if (error instanceof CoalesceApiError && [401, 403, 503].includes(error.status)) {
|
|
98
|
-
throw error;
|
|
99
|
-
}
|
|
100
|
-
// Re-throw hard block errors — exclusion and specialized pattern blocks
|
|
101
|
-
if (error instanceof Error && (error.message.includes("is excluded") ||
|
|
102
|
-
error.message.startsWith("Cannot create node"))) {
|
|
103
|
-
throw error;
|
|
104
|
-
}
|
|
105
|
-
// Log unexpected errors so they are not completely invisible
|
|
106
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
107
|
-
process.stderr.write(`[validateNodeTypeChoice] Unexpected error for nodeType "${params.nodeType}": ${reason}. Validation skipped.\n`);
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Blocks creation of "Source" node types — Source nodes are read-only data
|
|
113
|
-
* definitions created via the Coalesce UI, not downstream processing nodes.
|
|
114
|
-
* Agents often confuse "Source" with "Stage".
|
|
115
|
-
*/
|
|
116
|
-
function assertNotSourceNodeType(nodeType) {
|
|
117
|
-
const normalized = nodeType.toLowerCase().replace(/.*:::/, "");
|
|
118
|
-
if (normalized === "source") {
|
|
119
|
-
throw new Error(`Cannot create a node with nodeType "Source". Source nodes are read-only data ` +
|
|
120
|
-
`definitions — they represent external tables and are created via the Coalesce UI, ` +
|
|
121
|
-
`not via the API. You probably want "Stage" for a staging/transform node. ` +
|
|
122
|
-
`Call plan_pipeline to discover the correct nodeType.`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
function mergeWorkspaceNodeChanges(current, changes) {
|
|
126
|
-
if (Array.isArray(changes)) {
|
|
127
|
-
return changes;
|
|
128
|
-
}
|
|
129
|
-
if (isPlainObject(current) && isPlainObject(changes)) {
|
|
130
|
-
const merged = { ...current };
|
|
131
|
-
for (const [key, value] of Object.entries(changes)) {
|
|
132
|
-
merged[key] = mergeWorkspaceNodeChanges(current[key], value);
|
|
133
|
-
}
|
|
134
|
-
return merged;
|
|
135
|
-
}
|
|
136
|
-
return changes;
|
|
137
|
-
}
|
|
138
|
-
function syncNodeNameIntoMetadataSourceMapping(current, merged, changes) {
|
|
139
|
-
if (typeof changes.name !== "string") {
|
|
140
|
-
return merged;
|
|
141
|
-
}
|
|
142
|
-
const metadataChanges = isPlainObject(changes.metadata) ? changes.metadata : undefined;
|
|
143
|
-
if (metadataChanges && "sourceMapping" in metadataChanges) {
|
|
144
|
-
return merged;
|
|
145
|
-
}
|
|
146
|
-
const mergedMetadata = isPlainObject(merged.metadata) ? merged.metadata : undefined;
|
|
147
|
-
if (!mergedMetadata || !Array.isArray(mergedMetadata.sourceMapping)) {
|
|
148
|
-
return merged;
|
|
149
|
-
}
|
|
150
|
-
const previousName = typeof current.name === "string" && current.name.trim().length > 0
|
|
151
|
-
? current.name
|
|
152
|
-
: null;
|
|
153
|
-
const updateSingleUnnamedMapping = previousName === null && mergedMetadata.sourceMapping.length === 1;
|
|
154
|
-
return {
|
|
155
|
-
...merged,
|
|
156
|
-
metadata: {
|
|
157
|
-
...mergedMetadata,
|
|
158
|
-
sourceMapping: mergedMetadata.sourceMapping.map((entry) => {
|
|
159
|
-
if (!isPlainObject(entry)) {
|
|
160
|
-
return entry;
|
|
161
|
-
}
|
|
162
|
-
const shouldRename = (previousName !== null && entry.name === previousName) ||
|
|
163
|
-
updateSingleUnnamedMapping;
|
|
164
|
-
if (!shouldRename) {
|
|
165
|
-
return entry;
|
|
166
|
-
}
|
|
167
|
-
return {
|
|
168
|
-
...entry,
|
|
169
|
-
name: changes.name,
|
|
170
|
-
};
|
|
171
|
-
}),
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
export function buildUpdatedWorkspaceNodeBody(current, changes) {
|
|
176
|
-
if (!isPlainObject(current)) {
|
|
177
|
-
throw new Error("Workspace node response was not an object");
|
|
178
|
-
}
|
|
179
|
-
const merged = mergeWorkspaceNodeChanges(current, changes);
|
|
180
|
-
if (!isPlainObject(merged)) {
|
|
181
|
-
throw new Error("Merged workspace node update was not an object");
|
|
182
|
-
}
|
|
183
|
-
const synchronized = syncNodeNameIntoMetadataSourceMapping(current, merged, changes);
|
|
184
|
-
if (!isPlainObject(synchronized)) {
|
|
185
|
-
throw new Error("Synchronized workspace node update was not an object");
|
|
186
|
-
}
|
|
187
|
-
// Preserve the node name casing as provided by the user or existing node.
|
|
188
|
-
// Snowflake treats unquoted identifiers as uppercase, but users may choose
|
|
189
|
-
// lowercase names (e.g., for Databricks or personal preference) — respect that.
|
|
190
|
-
// Validate nodeType and materializationType compatibility
|
|
191
|
-
validateNodeTypeMaterializationCompatibility(synchronized.nodeType, synchronized.materializationType);
|
|
192
|
-
// Strip invalid fields from metadata before sending to Coalesce
|
|
193
|
-
if (isPlainObject(synchronized.metadata)) {
|
|
194
|
-
synchronized.metadata = cleanMetadata(synchronized.metadata);
|
|
195
|
-
}
|
|
196
|
-
// Ensure API-required fields (table, overrideSQL, columnIDs) are preserved
|
|
197
|
-
ensureRequiredApiFields(current, synchronized);
|
|
198
|
-
return synchronized;
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Reconcile external target columns against a node's auto-populated columns.
|
|
202
|
-
*
|
|
203
|
-
* For each target column:
|
|
204
|
-
* - If it matches an auto-populated predecessor column by name, the source
|
|
205
|
-
* linkage is preserved but the dataType is overridden to match the external schema.
|
|
206
|
-
* - If it has no match in the predecessor, it is added as a new column
|
|
207
|
-
* (flagged as needing a transform unless one was provided).
|
|
208
|
-
*
|
|
209
|
-
* Predecessor columns not present in the target list are dropped.
|
|
210
|
-
*/
|
|
211
|
-
function reconcileExternalSchema(autoPopulatedColumns, targetColumns) {
|
|
212
|
-
// Index auto-populated columns by normalized name
|
|
213
|
-
const existingByName = new Map();
|
|
214
|
-
for (const col of autoPopulatedColumns) {
|
|
215
|
-
if (isPlainObject(col) && typeof col.name === "string") {
|
|
216
|
-
existingByName.set(normalizeColumnName(col.name), col);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
const targetNameSet = new Set(targetColumns.map((tc) => normalizeColumnName(tc.name)));
|
|
220
|
-
const reconciliation = {
|
|
221
|
-
matched: [],
|
|
222
|
-
added: [],
|
|
223
|
-
dropped: [],
|
|
224
|
-
typeChanges: [],
|
|
225
|
-
};
|
|
226
|
-
// Find dropped columns (in predecessor, not in target)
|
|
227
|
-
for (const col of autoPopulatedColumns) {
|
|
228
|
-
if (!isPlainObject(col) || typeof col.name !== "string")
|
|
229
|
-
continue;
|
|
230
|
-
if (!targetNameSet.has(normalizeColumnName(col.name))) {
|
|
231
|
-
reconciliation.dropped.push({
|
|
232
|
-
name: col.name,
|
|
233
|
-
reason: "not in target schema",
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
// Build reconciled column list in target column order
|
|
238
|
-
const reconciledColumns = [];
|
|
239
|
-
for (const target of targetColumns) {
|
|
240
|
-
const normalizedName = normalizeColumnName(target.name);
|
|
241
|
-
const existing = existingByName.get(normalizedName);
|
|
242
|
-
if (existing) {
|
|
243
|
-
// Matched — preserve source linkage, override dataType
|
|
244
|
-
const sourceDataType = typeof existing.dataType === "string" ? existing.dataType : null;
|
|
245
|
-
const typeChanged = sourceDataType !== null && normalizeDataType(sourceDataType) !== normalizeDataType(target.dataType);
|
|
246
|
-
const reconciledCol = {
|
|
247
|
-
...structuredClone(existing),
|
|
248
|
-
name: target.name,
|
|
249
|
-
dataType: target.dataType,
|
|
250
|
-
nullable: target.nullable ?? true,
|
|
251
|
-
description: target.description ?? (typeof existing.description === "string" ? existing.description : ""),
|
|
252
|
-
};
|
|
253
|
-
if (target.transform) {
|
|
254
|
-
reconciledCol.transform = target.transform;
|
|
255
|
-
}
|
|
256
|
-
reconciledColumns.push(reconciledCol);
|
|
257
|
-
reconciliation.matched.push({
|
|
258
|
-
name: target.name,
|
|
259
|
-
sourceDataType,
|
|
260
|
-
targetDataType: target.dataType,
|
|
261
|
-
typeChanged,
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
// New column — no predecessor match
|
|
266
|
-
const needsTransform = !target.transform;
|
|
267
|
-
reconciledColumns.push({
|
|
268
|
-
name: target.name,
|
|
269
|
-
dataType: target.dataType,
|
|
270
|
-
nullable: target.nullable ?? true,
|
|
271
|
-
description: target.description ?? "",
|
|
272
|
-
...(target.transform ? { transform: target.transform } : {}),
|
|
273
|
-
});
|
|
274
|
-
reconciliation.added.push({
|
|
275
|
-
name: target.name,
|
|
276
|
-
dataType: target.dataType,
|
|
277
|
-
needsTransform,
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
// Derive typeChanges from matched entries to avoid dual-update maintenance
|
|
282
|
-
reconciliation.typeChanges = reconciliation.matched
|
|
283
|
-
.filter((m) => m.typeChanged && m.sourceDataType !== null)
|
|
284
|
-
.map((m) => ({ name: m.name, from: m.sourceDataType, to: m.targetDataType }));
|
|
285
|
-
return { columns: reconciledColumns, reconciliation };
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Valid metadata fields for the Coalesce PUT API.
|
|
289
|
-
* The GET response may include additional read-only fields (e.g., appliedNodeTests,
|
|
290
|
-
* cteString, materializationOption) that the PUT schema rejects as additional properties.
|
|
291
|
-
*/
|
|
292
|
-
const VALID_METADATA_FIELDS = new Set([
|
|
293
|
-
"columns",
|
|
294
|
-
"sourceMapping",
|
|
295
|
-
"enabledColumnTestIDs",
|
|
296
|
-
]);
|
|
297
|
-
function cleanMetadata(metadata) {
|
|
298
|
-
if (!isPlainObject(metadata)) {
|
|
299
|
-
return {};
|
|
300
|
-
}
|
|
301
|
-
const cleaned = {};
|
|
302
|
-
for (const key of Object.keys(metadata)) {
|
|
303
|
-
if (VALID_METADATA_FIELDS.has(key)) {
|
|
304
|
-
cleaned[key] = metadata[key];
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return cleaned;
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Preserves columnID, sources, columnReference, and placement from existing
|
|
311
|
-
* columns when new columns are provided by the agent.
|
|
312
|
-
*
|
|
313
|
-
* When agents provide columns in changes.metadata.columns, those columns
|
|
314
|
-
* typically lack the source linkage fields that Coalesce auto-populates
|
|
315
|
-
* from predecessors. This function restores them by matching column names.
|
|
316
|
-
*/
|
|
317
|
-
function preserveColumnLinkage(currentMetadata, mergedMetadata) {
|
|
318
|
-
if (!Array.isArray(currentMetadata.columns) || !Array.isArray(mergedMetadata.columns)) {
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
const existingByName = new Map();
|
|
322
|
-
for (const col of currentMetadata.columns) {
|
|
323
|
-
if (isPlainObject(col) && typeof col.name === "string") {
|
|
324
|
-
existingByName.set(normalizeColumnName(col.name), col);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
if (existingByName.size === 0) {
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
for (const col of mergedMetadata.columns) {
|
|
331
|
-
if (!isPlainObject(col) || typeof col.name !== "string") {
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
const existing = existingByName.get(normalizeColumnName(col.name));
|
|
335
|
-
if (!existing) {
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
// Preserve columnID if not already set
|
|
339
|
-
if ((!col.columnID || (typeof col.columnID === "string" && col.columnID.length === 0))
|
|
340
|
-
&& typeof existing.columnID === "string") {
|
|
341
|
-
col.columnID = existing.columnID;
|
|
342
|
-
}
|
|
343
|
-
// Preserve source linkage fields — these connect the column back to its
|
|
344
|
-
// predecessor node. Agents almost never provide these, so inherit from
|
|
345
|
-
// the auto-populated column.
|
|
346
|
-
if (!Array.isArray(col.sources) && Array.isArray(existing.sources)) {
|
|
347
|
-
const clonedSources = structuredClone(existing.sources);
|
|
348
|
-
col.sources = clonedSources;
|
|
349
|
-
// If the agent provided a non-passthrough transform on the top-level column
|
|
350
|
-
// (e.g., UPPER("TABLE"."COL")), propagate it into sources[*].transform so
|
|
351
|
-
// the Coalesce UI displays it correctly.
|
|
352
|
-
// The UI reads transforms from sources[0].transform, not a top-level transform field.
|
|
353
|
-
if (typeof col.transform === "string" &&
|
|
354
|
-
col.transform.trim().length > 0 &&
|
|
355
|
-
typeof col.name === "string" &&
|
|
356
|
-
!isPassthroughTransform(col.transform, col.name)) {
|
|
357
|
-
for (const source of clonedSources) {
|
|
358
|
-
if (isPlainObject(source)) {
|
|
359
|
-
source.transform = col.transform;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
if (!isPlainObject(col.columnReference) && isPlainObject(existing.columnReference)) {
|
|
365
|
-
col.columnReference = existing.columnReference;
|
|
366
|
-
}
|
|
367
|
-
// Preserve placement (column ordering metadata)
|
|
368
|
-
if (col.placement === undefined && existing.placement !== undefined) {
|
|
369
|
-
col.placement = existing.placement;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* Ensures the Coalesce API required fields (table, overrideSQL) are present
|
|
375
|
-
* in the body being sent to the PUT endpoint. These are preserved from the
|
|
376
|
-
* current node — the agent is never allowed to set overrideSQL.
|
|
377
|
-
*/
|
|
378
|
-
function ensureRequiredApiFields(current, body) {
|
|
379
|
-
// Ensure 'table' is present — required by the Coalesce PUT API.
|
|
380
|
-
// Prefer current node's value, fall back to node name.
|
|
381
|
-
if (!body.table || (typeof body.table === "string" && body.table.trim().length === 0)) {
|
|
382
|
-
if (current.table && typeof current.table === "string" && current.table.trim().length > 0) {
|
|
383
|
-
body.table = current.table;
|
|
384
|
-
}
|
|
385
|
-
else {
|
|
386
|
-
const name = typeof body.name === "string" ? body.name : typeof current.name === "string" ? current.name : "";
|
|
387
|
-
if (name.length > 0) {
|
|
388
|
-
body.table = name;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
// Preserve 'overrideSQL' from current node — agent must never set this
|
|
393
|
-
if ("overrideSQL" in current) {
|
|
394
|
-
body.overrideSQL = current.overrideSQL;
|
|
395
|
-
}
|
|
396
|
-
// Preserve columnIDs from current node's columns
|
|
397
|
-
const currentMetadata = isPlainObject(current.metadata) ? current.metadata : undefined;
|
|
398
|
-
const bodyMetadata = isPlainObject(body.metadata) ? body.metadata : undefined;
|
|
399
|
-
// Strip backslash-escaped quotes from transforms BEFORE preserveColumnLinkage,
|
|
400
|
-
// so cleaned transforms get propagated into sources[*].transform correctly.
|
|
401
|
-
if (bodyMetadata && Array.isArray(bodyMetadata.columns)) {
|
|
402
|
-
for (const col of bodyMetadata.columns) {
|
|
403
|
-
if (isPlainObject(col) && typeof col.transform === "string" && col.transform.includes("\\")) {
|
|
404
|
-
col.transform = col.transform.replace(/\\"/g, '"');
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
if (currentMetadata && bodyMetadata) {
|
|
409
|
-
preserveColumnLinkage(currentMetadata, bodyMetadata);
|
|
410
|
-
}
|
|
411
|
-
// Ensure all columns have required fields and strip invalid properties
|
|
412
|
-
if (bodyMetadata && Array.isArray(bodyMetadata.columns)) {
|
|
413
|
-
// Build lookup of current node's columns for dataType fallback
|
|
414
|
-
const currentColumns = currentMetadata && Array.isArray(currentMetadata.columns) ? currentMetadata.columns : [];
|
|
415
|
-
const currentDataTypes = new Map();
|
|
416
|
-
for (const col of currentColumns) {
|
|
417
|
-
if (isPlainObject(col) && typeof col.name === "string" && typeof col.dataType === "string") {
|
|
418
|
-
currentDataTypes.set(col.name.toUpperCase(), col.dataType);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
for (const col of bodyMetadata.columns) {
|
|
422
|
-
if (!isPlainObject(col))
|
|
423
|
-
continue;
|
|
424
|
-
// Ensure 'dataType' — required by the Coalesce PUT API.
|
|
425
|
-
// Prefer the current node's dataType for the same column name, otherwise default to "VARCHAR".
|
|
426
|
-
if (!col.dataType || (typeof col.dataType === "string" && col.dataType.trim().length === 0)) {
|
|
427
|
-
const colName = typeof col.name === "string" ? col.name.toUpperCase() : "";
|
|
428
|
-
const existingType = currentDataTypes.get(colName);
|
|
429
|
-
col.dataType = existingType ?? "VARCHAR";
|
|
430
|
-
}
|
|
431
|
-
// Ensure 'nullable' — required by the Coalesce PUT API
|
|
432
|
-
if (!("nullable" in col)) {
|
|
433
|
-
col.nullable = true;
|
|
434
|
-
}
|
|
435
|
-
// Ensure 'description' — required by the Coalesce PUT API
|
|
436
|
-
if (!("description" in col)) {
|
|
437
|
-
col.description = "";
|
|
438
|
-
}
|
|
439
|
-
// Generate columnID for columns that don't have one —
|
|
440
|
-
// the PUT API requires columnID on every column
|
|
441
|
-
if (!col.columnID || (typeof col.columnID === "string" && col.columnID.length === 0)) {
|
|
442
|
-
col.columnID = randomUUID();
|
|
443
|
-
}
|
|
444
|
-
// Strip passthrough transforms — if the transform just references
|
|
445
|
-
// the column's own name (e.g., "ALIAS"."COL" or {{ ref(...) }}."COL"),
|
|
446
|
-
// remove it so Coalesce auto-populates the source mapping.
|
|
447
|
-
if (typeof col.transform === "string" && typeof col.name === "string") {
|
|
448
|
-
if (isPassthroughTransform(col.transform, col.name)) {
|
|
449
|
-
delete col.transform;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
// Ensure computed columns (new columns with a transform but no sources) get a
|
|
453
|
-
// synthetic sources entry so the Coalesce UI displays the transform correctly.
|
|
454
|
-
// The UI reads transforms from sources[0].transform, not the top-level transform field.
|
|
455
|
-
if (typeof col.transform === "string" &&
|
|
456
|
-
col.transform.trim().length > 0 &&
|
|
457
|
-
!Array.isArray(col.sources)) {
|
|
458
|
-
col.sources = [{ transform: col.transform, columnReferences: [] }];
|
|
459
|
-
}
|
|
460
|
-
// Strip properties that are not valid Coalesce column fields.
|
|
461
|
-
// Valid: name, dataType, transform, nullable, description, columnID,
|
|
462
|
-
// sources, placement, plus columnSelector attributes (isBusinessKey, etc.)
|
|
463
|
-
delete col.primaryKey;
|
|
464
|
-
delete col.foreignKey;
|
|
465
|
-
delete col.unique;
|
|
466
|
-
delete col.index;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
// Ensure 'enabledColumnTestIDs' is present on metadata — required by the Coalesce PUT API
|
|
470
|
-
if (bodyMetadata && !Array.isArray(bodyMetadata.enabledColumnTestIDs)) {
|
|
471
|
-
const currentEnabled = currentMetadata && Array.isArray(currentMetadata.enabledColumnTestIDs)
|
|
472
|
-
? currentMetadata.enabledColumnTestIDs
|
|
473
|
-
: [];
|
|
474
|
-
bodyMetadata.enabledColumnTestIDs = currentEnabled;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
/**
|
|
478
|
-
* Validates that nodeType and materializationType are compatible
|
|
479
|
-
* Throws error with actionable message if incompatible
|
|
480
|
-
*/
|
|
481
|
-
function validateNodeTypeMaterializationCompatibility(nodeType, materializationType) {
|
|
482
|
-
if (typeof nodeType !== "string" || typeof materializationType !== "string") {
|
|
483
|
-
return; // Skip validation if either field is missing or invalid type
|
|
484
|
-
}
|
|
485
|
-
const normalizedNodeType = nodeType.toLowerCase();
|
|
486
|
-
const normalizedMaterialization = materializationType.toLowerCase();
|
|
487
|
-
// View nodes can ONLY be materialized as views
|
|
488
|
-
if (normalizedNodeType === "view" && normalizedMaterialization === "table") {
|
|
489
|
-
throw new Error(`Invalid configuration: nodeType "View" cannot use materializationType "table". ` +
|
|
490
|
-
`Either keep materializationType as "view" OR change nodeType to a table-capable type like "Dimension", "Fact", "Stage", or "Work".`);
|
|
491
|
-
}
|
|
492
|
-
// Note: Other node types (Dimension, Fact, Stage, Work, etc.) can be either table or view
|
|
493
|
-
// So we only need to check the View + table combination
|
|
494
|
-
}
|
|
4
|
+
import { isPlainObject } from "../../utils.js";
|
|
5
|
+
import { appendWhereToJoinCondition, } from "./join-helpers.js";
|
|
6
|
+
import { tryCompleteNodeConfiguration, reconcileExternalSchema, mergeWorkspaceNodeChanges, buildUpdatedWorkspaceNodeBody, } from "./node-update-helpers.js";
|
|
7
|
+
import { createWorkspaceNodeFromPredecessor } from "./node-creation.js";
|
|
8
|
+
// ── Barrel re-exports ──────────────────────────────────────────────────
|
|
9
|
+
// Preserve existing import paths: everything that used to be exported from
|
|
10
|
+
// this module is re-exported from the new files below.
|
|
11
|
+
export { tryCompleteNodeConfiguration, validateNodeTypeChoice, assertNotSourceNodeType, mergeWorkspaceNodeChanges, syncNodeNameIntoMetadataSourceMapping, buildUpdatedWorkspaceNodeBody, reconcileExternalSchema, } from "./node-update-helpers.js";
|
|
12
|
+
export { createWorkspaceNodeFromScratch, createWorkspaceNodeFromPredecessor, suggestNamingConvention, buildPostCreationNextSteps, } from "./node-creation.js";
|
|
13
|
+
export { convertJoinToAggregation, applyJoinCondition, } from "./join-operations.js";
|
|
14
|
+
// ── Group B: Core update operations (kept in this file) ────────────────
|
|
495
15
|
export async function updateWorkspaceNode(client, params) {
|
|
496
16
|
assertNoSqlOverridePayload(params.changes, "update_workspace_node changes");
|
|
497
17
|
const current = await getWorkspaceNode(client, params);
|
|
@@ -544,734 +64,7 @@ export async function replaceWorkspaceNodeColumns(client, params) {
|
|
|
544
64
|
body: updated,
|
|
545
65
|
});
|
|
546
66
|
}
|
|
547
|
-
|
|
548
|
-
let merged = params.changes ? { ...params.changes } : {};
|
|
549
|
-
if (params.name !== undefined) {
|
|
550
|
-
merged = mergeWorkspaceNodeChanges(merged, { name: params.name });
|
|
551
|
-
}
|
|
552
|
-
if (params.description !== undefined) {
|
|
553
|
-
merged = mergeWorkspaceNodeChanges(merged, {
|
|
554
|
-
description: params.description,
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
if (params.storageLocations !== undefined) {
|
|
558
|
-
merged = mergeWorkspaceNodeChanges(merged, {
|
|
559
|
-
storageLocations: params.storageLocations,
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
if (params.config !== undefined) {
|
|
563
|
-
merged = mergeWorkspaceNodeChanges(merged, {
|
|
564
|
-
config: params.config,
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
if (params.metadata !== undefined) {
|
|
568
|
-
merged = mergeWorkspaceNodeChanges(merged, {
|
|
569
|
-
metadata: params.metadata,
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
return merged;
|
|
573
|
-
}
|
|
574
|
-
function buildScratchNodeValidation(node, completionLevel, requested) {
|
|
575
|
-
const requestedName = getRequestedNodeName(requested.changes);
|
|
576
|
-
const requestedNameSatisfied = requestedName !== undefined ? node.name === requestedName : true;
|
|
577
|
-
const requestedColumnNames = getRequestedColumnNames(requested.changes);
|
|
578
|
-
const actualColumnNameSet = new Set(getNodeColumnNames(node).map((name) => normalizeColumnName(name)));
|
|
579
|
-
const requestedColumnsSatisfied = requestedColumnNames.length === 0
|
|
580
|
-
? getNodeColumnCount(node) > 0
|
|
581
|
-
: requestedColumnNames.every((name) => actualColumnNameSet.has(normalizeColumnName(name)));
|
|
582
|
-
const requestedConfig = getRequestedConfig(requested.changes);
|
|
583
|
-
const nodeConfig = isPlainObject(node.config) ? node.config : undefined;
|
|
584
|
-
const requestedConfigSatisfied = requestedConfig === undefined
|
|
585
|
-
? isPlainObject(node.config)
|
|
586
|
-
: Object.entries(requestedConfig).every(([key, value]) => nodeConfig && Object.is(nodeConfig[key], value));
|
|
587
|
-
const requestedLocationFields = getRequestedLocationFields(requested.changes);
|
|
588
|
-
const requestedLocationSatisfied = Object.entries(requestedLocationFields).every(([key, value]) => Object.is(node[key], value));
|
|
589
|
-
const nameSet = typeof node.name === "string" && node.name.trim().length > 0;
|
|
590
|
-
const storageLocationsSet = getNodeStorageLocationCount(node) > 0;
|
|
591
|
-
const columnCount = getNodeColumnCount(node);
|
|
592
|
-
const configPresent = isPlainObject(node.config);
|
|
593
|
-
const configKeyCount = getNodeConfigKeyCount(node);
|
|
594
|
-
const nameRequired = completionLevel !== "created" || requestedName !== undefined;
|
|
595
|
-
const storageLocationsRequired = requested.storageLocations !== undefined;
|
|
596
|
-
let completionSatisfied = true;
|
|
597
|
-
if (completionLevel === "named") {
|
|
598
|
-
completionSatisfied =
|
|
599
|
-
(!nameRequired || (requestedName ? requestedNameSatisfied : nameSet)) &&
|
|
600
|
-
requestedLocationSatisfied &&
|
|
601
|
-
(!storageLocationsRequired || storageLocationsSet);
|
|
602
|
-
}
|
|
603
|
-
else if (completionLevel === "configured") {
|
|
604
|
-
completionSatisfied =
|
|
605
|
-
(!nameRequired || (requestedName ? requestedNameSatisfied : nameSet)) &&
|
|
606
|
-
requestedLocationSatisfied &&
|
|
607
|
-
(!storageLocationsRequired || storageLocationsSet) &&
|
|
608
|
-
requestedColumnsSatisfied &&
|
|
609
|
-
requestedConfigSatisfied;
|
|
610
|
-
}
|
|
611
|
-
return {
|
|
612
|
-
requestedCompletionLevel: completionLevel,
|
|
613
|
-
completionSatisfied,
|
|
614
|
-
nameRequired,
|
|
615
|
-
nameSet,
|
|
616
|
-
requestedName: requestedName ?? null,
|
|
617
|
-
requestedNameSatisfied,
|
|
618
|
-
requestedLocationKeys: Object.keys(requestedLocationFields),
|
|
619
|
-
requestedLocationSatisfied,
|
|
620
|
-
storageLocationsRequired,
|
|
621
|
-
storageLocationCount: getNodeStorageLocationCount(node),
|
|
622
|
-
storageLocationsSet,
|
|
623
|
-
columnCount,
|
|
624
|
-
configPresent,
|
|
625
|
-
configKeyCount,
|
|
626
|
-
requestedColumnCount: requestedColumnNames.length,
|
|
627
|
-
requestedColumnNames,
|
|
628
|
-
requestedColumnsSatisfied,
|
|
629
|
-
requestedConfigKeys: requestedConfig ? Object.keys(requestedConfig) : [],
|
|
630
|
-
requestedConfigSatisfied,
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
|
-
function assertConfiguredScratchInput(changes) {
|
|
634
|
-
const requestedName = getRequestedNodeName(changes);
|
|
635
|
-
const requestedColumnNames = getRequestedColumnNames(changes);
|
|
636
|
-
const missing = [];
|
|
637
|
-
if (!requestedName) {
|
|
638
|
-
missing.push("name");
|
|
639
|
-
}
|
|
640
|
-
if (requestedColumnNames.length === 0) {
|
|
641
|
-
missing.push("metadata.columns");
|
|
642
|
-
}
|
|
643
|
-
if (missing.length > 0) {
|
|
644
|
-
throw new Error(`Configured scratch node creation requires ${missing.join(" and ")}. Provide them explicitly or lower completionLevel to "named" or "created".`);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
function buildScratchNodeNextSteps(nodeType, node) {
|
|
648
|
-
const steps = [];
|
|
649
|
-
const family = inferFamily([nodeType]);
|
|
650
|
-
// Naming convention
|
|
651
|
-
const currentName = typeof node.name === "string" ? node.name : "";
|
|
652
|
-
if (!currentName || currentName === nodeType || /^[A-Z]+_\d+$/.test(currentName)) {
|
|
653
|
-
steps.push(`Name this node following conventions: ${suggestNamingConvention(family)}`);
|
|
654
|
-
}
|
|
655
|
-
// Scratch nodes have no predecessors — remind to add columns if missing
|
|
656
|
-
const metadata = isPlainObject(node.metadata) ? node.metadata : {};
|
|
657
|
-
const columns = Array.isArray(metadata.columns) ? metadata.columns : [];
|
|
658
|
-
if (columns.length === 0) {
|
|
659
|
-
steps.push("This node has no columns. Add columns using replace_workspace_node_columns or update_workspace_node.");
|
|
660
|
-
}
|
|
661
|
-
// Family-specific guidance
|
|
662
|
-
if (family === "fact" || family === "dimension") {
|
|
663
|
-
steps.push(`Verify materialization: ${family === "fact" ? "Fact" : "Dimension"} nodes should typically materialize as tables, not views. ` +
|
|
664
|
-
"Check that materializationType is 'table' in the config.");
|
|
665
|
-
if (family === "dimension") {
|
|
666
|
-
steps.push("For dimensions: identify the business key (natural key from the source system) and mark it isBusinessKey = true. " +
|
|
667
|
-
"If this is a slowly changing dimension (SCD Type 2), ensure START_DATE/END_DATE/IS_CURRENT columns exist.");
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
// Verification
|
|
671
|
-
steps.push("Verify the node: call get_workspace_node to confirm columns and config are correct before proceeding.");
|
|
672
|
-
return steps;
|
|
673
|
-
}
|
|
674
|
-
export async function createWorkspaceNodeFromScratch(client, params) {
|
|
675
|
-
assertNotSourceNodeType(params.nodeType);
|
|
676
|
-
const completionLevel = params.completionLevel ?? "configured";
|
|
677
|
-
const scratchChanges = buildScratchNodeChanges(params);
|
|
678
|
-
assertNoSqlOverridePayload(scratchChanges, "create_workspace_node_from_scratch changes");
|
|
679
|
-
if (completionLevel === "configured") {
|
|
680
|
-
assertConfiguredScratchInput(scratchChanges);
|
|
681
|
-
}
|
|
682
|
-
// Validate node type choice — throws if the type is excluded (e.g., inputMode: 'sql')
|
|
683
|
-
// or if a specialized pattern is detected without matching context
|
|
684
|
-
const nodeTypeValidation = await validateNodeTypeChoice(client, {
|
|
685
|
-
workspaceID: params.workspaceID,
|
|
686
|
-
nodeType: params.nodeType,
|
|
687
|
-
predecessorCount: 0,
|
|
688
|
-
repoPath: params.repoPath,
|
|
689
|
-
goal: params.goal,
|
|
690
|
-
});
|
|
691
|
-
const created = await createWorkspaceNode(client, {
|
|
692
|
-
workspaceID: params.workspaceID,
|
|
693
|
-
nodeType: params.nodeType,
|
|
694
|
-
});
|
|
695
|
-
if (!isPlainObject(created) || typeof created.id !== "string") {
|
|
696
|
-
throw new Error("Workspace node creation did not return a node ID");
|
|
697
|
-
}
|
|
698
|
-
const createdNode = await getWorkspaceNode(client, {
|
|
699
|
-
workspaceID: params.workspaceID,
|
|
700
|
-
nodeID: created.id,
|
|
701
|
-
});
|
|
702
|
-
if (!isPlainObject(createdNode)) {
|
|
703
|
-
throw new Error("Created workspace node response was not an object");
|
|
704
|
-
}
|
|
705
|
-
let finalNode = createdNode;
|
|
706
|
-
if (Object.keys(scratchChanges).length > 0) {
|
|
707
|
-
const body = buildUpdatedWorkspaceNodeBody(createdNode, scratchChanges);
|
|
708
|
-
await setWorkspaceNode(client, {
|
|
709
|
-
workspaceID: params.workspaceID,
|
|
710
|
-
nodeID: created.id,
|
|
711
|
-
body,
|
|
712
|
-
});
|
|
713
|
-
finalNode = await getWorkspaceNode(client, {
|
|
714
|
-
workspaceID: params.workspaceID,
|
|
715
|
-
nodeID: created.id,
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
if (!isPlainObject(finalNode)) {
|
|
719
|
-
throw new Error("Final workspace node response was not an object");
|
|
720
|
-
}
|
|
721
|
-
const validation = buildScratchNodeValidation(finalNode, completionLevel, {
|
|
722
|
-
changes: scratchChanges,
|
|
723
|
-
storageLocations: params.storageLocations,
|
|
724
|
-
});
|
|
725
|
-
const nextSteps = buildScratchNodeNextSteps(params.nodeType, finalNode);
|
|
726
|
-
if (!validation.completionSatisfied) {
|
|
727
|
-
if (completionLevel === "configured") {
|
|
728
|
-
throw new Error(`Workspace node ${created.id} was created, but configured scratch validation failed. ` +
|
|
729
|
-
`Check name, metadata.columns, and config values on the saved node body. ` +
|
|
730
|
-
`To clean up, use delete_workspace_node with nodeID "${created.id}".`);
|
|
731
|
-
}
|
|
732
|
-
return {
|
|
733
|
-
node: finalNode,
|
|
734
|
-
validation,
|
|
735
|
-
nextSteps,
|
|
736
|
-
warning: "Workspace node was created, but the requested scratch completion level was not fully satisfied. Review the node body and provide any missing name, storageLocations, metadata.columns, or config fields.",
|
|
737
|
-
...(nodeTypeValidation ? { nodeTypeValidation } : {}),
|
|
738
|
-
};
|
|
739
|
-
}
|
|
740
|
-
// Automatically complete node configuration using intelligent rules (best-effort)
|
|
741
|
-
const completion = await tryCompleteNodeConfiguration(client, {
|
|
742
|
-
workspaceID: params.workspaceID,
|
|
743
|
-
nodeID: created.id,
|
|
744
|
-
repoPath: params.repoPath,
|
|
745
|
-
});
|
|
746
|
-
return {
|
|
747
|
-
node: completion.configCompletion?.node ?? finalNode,
|
|
748
|
-
validation,
|
|
749
|
-
nextSteps,
|
|
750
|
-
...completion,
|
|
751
|
-
...(nodeTypeValidation ? { nodeTypeValidation } : {}),
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
function suggestNamingConvention(family) {
|
|
755
|
-
const conventions = {
|
|
756
|
-
stage: "STG_<SOURCE_NAME> (e.g., STG_CUSTOMERS, STG_ORDERS)",
|
|
757
|
-
dimension: "DIM_<ENTITY> (e.g., DIM_CUSTOMER, DIM_PRODUCT)",
|
|
758
|
-
fact: "FACT_<BUSINESS_PROCESS> or FCT_<BUSINESS_PROCESS> (e.g., FACT_SALES, FACT_CLV)",
|
|
759
|
-
view: "V_<PURPOSE> or INT_<PURPOSE> (e.g., V_CUSTOMER_ORDERS)",
|
|
760
|
-
work: "INT_<PURPOSE> or WRK_<PURPOSE> (e.g., INT_ORDER_ENRICHMENT)",
|
|
761
|
-
hub: "HUB_<BUSINESS_KEY> (e.g., HUB_CUSTOMER)",
|
|
762
|
-
satellite: "SAT_<HUB>_<CONTEXT> (e.g., SAT_CUSTOMER_DETAILS)",
|
|
763
|
-
link: "LNK_<RELATIONSHIP> (e.g., LNK_CUSTOMER_ORDER)",
|
|
764
|
-
};
|
|
765
|
-
return conventions[family] ?? "Use a descriptive, layer-appropriate name";
|
|
766
|
-
}
|
|
767
|
-
function buildPostCreationNextSteps(predecessorCount, nodeType, joinSuggestions, node) {
|
|
768
|
-
const steps = [];
|
|
769
|
-
const family = inferFamily([nodeType]);
|
|
770
|
-
// Naming convention
|
|
771
|
-
const currentName = typeof node.name === "string" ? node.name : "";
|
|
772
|
-
if (!currentName || currentName === nodeType || /^[A-Z]+_\d+$/.test(currentName)) {
|
|
773
|
-
steps.push(`Name this node following conventions: ${suggestNamingConvention(family)}`);
|
|
774
|
-
}
|
|
775
|
-
// Multi-predecessor: join setup is REQUIRED
|
|
776
|
-
if (predecessorCount > 1) {
|
|
777
|
-
const hasCommonColumns = joinSuggestions.some((s) => s.commonColumns.length > 0);
|
|
778
|
-
steps.push("REQUIRED: Set up the join condition. This multi-predecessor node needs a FROM/JOIN/ON clause in the joinCondition. " +
|
|
779
|
-
"Review joinSuggestions above to identify join columns, then either:" +
|
|
780
|
-
"\n - Call convert_join_to_aggregation (for GROUP BY / aggregation use cases)" +
|
|
781
|
-
"\n - Call apply_join_condition (for row-level joins — auto-generates FROM/JOIN/ON with {{ ref() }} syntax)");
|
|
782
|
-
if (hasCommonColumns) {
|
|
783
|
-
steps.push("Verify join columns: joinSuggestions shows common column names between predecessors. " +
|
|
784
|
-
"Confirm these are the correct join keys (business keys, not surrogate keys). " +
|
|
785
|
-
"Choose the right join type: INNER JOIN (only matching rows), LEFT JOIN (keep all rows from first table), " +
|
|
786
|
-
"FULL OUTER JOIN (keep all rows from both).");
|
|
787
|
-
}
|
|
788
|
-
else {
|
|
789
|
-
steps.push("WARNING: No common columns found between predecessors. You may need a cross join, " +
|
|
790
|
-
"or the join columns have different names. Verify the correct join keys with the user.");
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
// Family-specific guidance
|
|
794
|
-
if (family === "fact" || family === "dimension") {
|
|
795
|
-
steps.push(`Verify materialization: ${family === "fact" ? "Fact" : "Dimension"} nodes should typically materialize as tables, not views. ` +
|
|
796
|
-
"Check that materializationType is 'table' in the config.");
|
|
797
|
-
if (family === "fact" && predecessorCount > 1) {
|
|
798
|
-
steps.push("For fact tables: define the grain (the set of columns that uniquely identify each row). " +
|
|
799
|
-
"These grain columns become your GROUP BY columns in convert_join_to_aggregation. " +
|
|
800
|
-
"Mark them as isBusinessKey = true.");
|
|
801
|
-
}
|
|
802
|
-
if (family === "dimension") {
|
|
803
|
-
steps.push("For dimensions: identify the business key (natural key from the source system) and mark it isBusinessKey = true. " +
|
|
804
|
-
"If this is a slowly changing dimension (SCD Type 2), ensure START_DATE/END_DATE/IS_CURRENT columns exist.");
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
// Single predecessor: simpler guidance
|
|
808
|
-
if (predecessorCount === 1) {
|
|
809
|
-
steps.push("Review auto-populated columns. Remove columns you don't need and add transforms where appropriate. " +
|
|
810
|
-
"Columns without transforms are pass-throughs (inherited as-is from the predecessor).");
|
|
811
|
-
}
|
|
812
|
-
// Verification
|
|
813
|
-
steps.push("Verify the node: call get_workspace_node to confirm columns, config, and join condition are correct before proceeding to downstream nodes.");
|
|
814
|
-
return steps;
|
|
815
|
-
}
|
|
816
|
-
export async function createWorkspaceNodeFromPredecessor(client, params) {
|
|
817
|
-
assertNotSourceNodeType(params.nodeType);
|
|
818
|
-
const effectivePredecessorNodeIDs = uniqueInOrder(params.predecessorNodeIDs);
|
|
819
|
-
if (params.changes) {
|
|
820
|
-
assertNoSqlOverridePayload(params.changes, "create_workspace_node_from_predecessor changes");
|
|
821
|
-
}
|
|
822
|
-
// Validate mutually exclusive params
|
|
823
|
-
if (params.columns && (params.groupByColumns || params.aggregates)) {
|
|
824
|
-
throw new Error("Cannot provide both 'columns' and 'groupByColumns'/'aggregates'. " +
|
|
825
|
-
"Use 'columns' for column replacement, or 'groupByColumns'+'aggregates' for aggregation.");
|
|
826
|
-
}
|
|
827
|
-
if (params.aggregates && !params.groupByColumns) {
|
|
828
|
-
throw new Error("'aggregates' requires 'groupByColumns' to be provided.");
|
|
829
|
-
}
|
|
830
|
-
if (params.groupByColumns && !params.aggregates) {
|
|
831
|
-
throw new Error("'groupByColumns' requires 'aggregates' to be provided.");
|
|
832
|
-
}
|
|
833
|
-
if (params.whereCondition && params.groupByColumns) {
|
|
834
|
-
throw new Error("'whereCondition' cannot be combined with 'groupByColumns'/'aggregates'. " +
|
|
835
|
-
"For aggregation nodes, WHERE/HAVING filters should be applied via a separate update_workspace_node call.");
|
|
836
|
-
}
|
|
837
|
-
// Validate node type choice and fetch predecessors in parallel
|
|
838
|
-
// validateNodeTypeChoice throws if the type is excluded (e.g., inputMode: 'sql')
|
|
839
|
-
// or if a specialized pattern is detected without matching context
|
|
840
|
-
const [nodeTypeValidation, predecessorNodes] = await Promise.all([
|
|
841
|
-
validateNodeTypeChoice(client, {
|
|
842
|
-
workspaceID: params.workspaceID,
|
|
843
|
-
nodeType: params.nodeType,
|
|
844
|
-
predecessorCount: effectivePredecessorNodeIDs.length,
|
|
845
|
-
repoPath: params.repoPath,
|
|
846
|
-
goal: params.goal,
|
|
847
|
-
}),
|
|
848
|
-
Promise.all(effectivePredecessorNodeIDs.map(async (nodeID) => {
|
|
849
|
-
const predecessor = await getWorkspaceNode(client, {
|
|
850
|
-
workspaceID: params.workspaceID,
|
|
851
|
-
nodeID,
|
|
852
|
-
});
|
|
853
|
-
if (!isPlainObject(predecessor)) {
|
|
854
|
-
throw new Error(`Predecessor node response was not an object for nodeID ${nodeID}`);
|
|
855
|
-
}
|
|
856
|
-
return buildPredecessorSummary(nodeID, predecessor);
|
|
857
|
-
})),
|
|
858
|
-
]);
|
|
859
|
-
const joinSuggestions = buildJoinSuggestions(predecessorNodes);
|
|
860
|
-
const created = await createWorkspaceNode(client, {
|
|
861
|
-
workspaceID: params.workspaceID,
|
|
862
|
-
nodeType: params.nodeType,
|
|
863
|
-
predecessorNodeIDs: effectivePredecessorNodeIDs,
|
|
864
|
-
});
|
|
865
|
-
if (!isPlainObject(created) || typeof created.id !== "string") {
|
|
866
|
-
throw new Error("Workspace node creation did not return a node ID");
|
|
867
|
-
}
|
|
868
|
-
const createdNode = await getWorkspaceNode(client, {
|
|
869
|
-
workspaceID: params.workspaceID,
|
|
870
|
-
nodeID: created.id,
|
|
871
|
-
});
|
|
872
|
-
if (!isPlainObject(createdNode)) {
|
|
873
|
-
throw new Error("Created workspace node response was not an object");
|
|
874
|
-
}
|
|
875
|
-
const referencedPredecessorNodeIDs = getReferencedPredecessorNodeIDs(createdNode, effectivePredecessorNodeIDs);
|
|
876
|
-
const allPredecessorsRepresented = referencedPredecessorNodeIDs.length === effectivePredecessorNodeIDs.length;
|
|
877
|
-
const autoPopulatedColumns = getNodeColumnCount(createdNode) > 0 &&
|
|
878
|
-
(effectivePredecessorNodeIDs.length === 1
|
|
879
|
-
? referencedPredecessorNodeIDs.length > 0
|
|
880
|
-
: allPredecessorsRepresented);
|
|
881
|
-
const validation = {
|
|
882
|
-
autoPopulatedColumns,
|
|
883
|
-
allPredecessorsRepresented,
|
|
884
|
-
columnCount: getNodeColumnCount(createdNode),
|
|
885
|
-
dependencyCount: getNodeDependencyNames(createdNode).length,
|
|
886
|
-
dependencyNames: getNodeDependencyNames(createdNode),
|
|
887
|
-
predecessorNodeIDs: effectivePredecessorNodeIDs,
|
|
888
|
-
referencedPredecessorNodeIDs,
|
|
889
|
-
};
|
|
890
|
-
// Build context-aware next steps for multi-predecessor nodes
|
|
891
|
-
const nextSteps = buildPostCreationNextSteps(effectivePredecessorNodeIDs.length, params.nodeType, joinSuggestions, createdNode);
|
|
892
|
-
if (!validation.autoPopulatedColumns) {
|
|
893
|
-
const warning = effectivePredecessorNodeIDs.length > 1
|
|
894
|
-
? "Workspace node was created from predecessor(s), but columns were not auto-populated from all requested predecessors. Review the suggested join columns and verify the node in Coalesce before proceeding."
|
|
895
|
-
: "Workspace node was created from predecessor(s), but columns were not auto-populated. Verify the node in Coalesce before proceeding.";
|
|
896
|
-
return {
|
|
897
|
-
node: createdNode,
|
|
898
|
-
predecessors: predecessorNodes,
|
|
899
|
-
joinSuggestions,
|
|
900
|
-
validation,
|
|
901
|
-
warning,
|
|
902
|
-
nextSteps,
|
|
903
|
-
...(nodeTypeValidation ? { nodeTypeValidation } : {}),
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
if (params.changes && Object.keys(params.changes).length > 0) {
|
|
907
|
-
const body = buildUpdatedWorkspaceNodeBody(createdNode, params.changes);
|
|
908
|
-
await setWorkspaceNode(client, {
|
|
909
|
-
workspaceID: params.workspaceID,
|
|
910
|
-
nodeID: created.id,
|
|
911
|
-
body,
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
// Single-call aggregation path: create + replace columns + write joinCondition + config completion
|
|
915
|
-
// convertJoinToAggregation handles all of this internally, including config completion.
|
|
916
|
-
if (params.groupByColumns && params.aggregates) {
|
|
917
|
-
const aggResult = await convertJoinToAggregation(client, {
|
|
918
|
-
workspaceID: params.workspaceID,
|
|
919
|
-
nodeID: created.id,
|
|
920
|
-
groupByColumns: params.groupByColumns,
|
|
921
|
-
aggregates: params.aggregates,
|
|
922
|
-
joinType: params.joinType,
|
|
923
|
-
repoPath: params.repoPath,
|
|
924
|
-
});
|
|
925
|
-
return {
|
|
926
|
-
node: aggResult.node,
|
|
927
|
-
predecessors: predecessorNodes,
|
|
928
|
-
joinSuggestions,
|
|
929
|
-
validation,
|
|
930
|
-
joinSQL: aggResult.joinSQL,
|
|
931
|
-
groupByAnalysis: aggResult.groupByAnalysis,
|
|
932
|
-
aggregationValidation: aggResult.validation,
|
|
933
|
-
...(aggResult.configCompletion ? { configCompletion: aggResult.configCompletion } : {}),
|
|
934
|
-
...(aggResult.configCompletionSkipped ? { configCompletionSkipped: aggResult.configCompletionSkipped } : {}),
|
|
935
|
-
nextSteps,
|
|
936
|
-
...(nodeTypeValidation ? { nodeTypeValidation } : {}),
|
|
937
|
-
};
|
|
938
|
-
}
|
|
939
|
-
// Single-call column replacement path: create + replace columns + WHERE + config completion
|
|
940
|
-
if (params.columns) {
|
|
941
|
-
await replaceWorkspaceNodeColumns(client, {
|
|
942
|
-
workspaceID: params.workspaceID,
|
|
943
|
-
nodeID: created.id,
|
|
944
|
-
columns: params.columns,
|
|
945
|
-
whereCondition: params.whereCondition,
|
|
946
|
-
});
|
|
947
|
-
}
|
|
948
|
-
// Automatically complete node configuration using intelligent rules (best-effort)
|
|
949
|
-
const completion = await tryCompleteNodeConfiguration(client, {
|
|
950
|
-
workspaceID: params.workspaceID,
|
|
951
|
-
nodeID: created.id,
|
|
952
|
-
repoPath: params.repoPath,
|
|
953
|
-
});
|
|
954
|
-
// Re-fetch the node if changes were applied but config completion failed
|
|
955
|
-
let resultNode = completion.configCompletion?.node ?? createdNode;
|
|
956
|
-
if (completion.configCompletionSkipped) {
|
|
957
|
-
const hasChanges = (params.changes && Object.keys(params.changes).length > 0) || params.columns;
|
|
958
|
-
if (hasChanges) {
|
|
959
|
-
try {
|
|
960
|
-
resultNode = await getWorkspaceNode(client, { workspaceID: params.workspaceID, nodeID: created.id });
|
|
961
|
-
}
|
|
962
|
-
catch (error) {
|
|
963
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
964
|
-
process.stderr.write(`[createWorkspaceNodeFromPredecessor] Re-fetch after config completion skip failed: ${reason}\n`);
|
|
965
|
-
resultNode = createdNode;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
return {
|
|
970
|
-
node: resultNode,
|
|
971
|
-
predecessors: predecessorNodes,
|
|
972
|
-
joinSuggestions,
|
|
973
|
-
validation,
|
|
974
|
-
...completion,
|
|
975
|
-
nextSteps,
|
|
976
|
-
...(nodeTypeValidation ? { nodeTypeValidation } : {}),
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
export async function convertJoinToAggregation(client, params) {
|
|
980
|
-
// Get the current node to analyze predecessors
|
|
981
|
-
const current = await getWorkspaceNode(client, {
|
|
982
|
-
workspaceID: params.workspaceID,
|
|
983
|
-
nodeID: params.nodeID,
|
|
984
|
-
});
|
|
985
|
-
if (!isPlainObject(current)) {
|
|
986
|
-
throw new Error("Node response was not an object");
|
|
987
|
-
}
|
|
988
|
-
const metadata = isPlainObject(current.metadata) ? current.metadata : {};
|
|
989
|
-
const predecessorNodeIDs = extractPredecessorNodeIDs(metadata);
|
|
990
|
-
// Fetch predecessor nodes to build join suggestions and ref info
|
|
991
|
-
const predecessorNodes = [];
|
|
992
|
-
const predecessorRefInfos = [];
|
|
993
|
-
const shouldMaintainJoins = params.maintainJoins !== false; // default true
|
|
994
|
-
if (shouldMaintainJoins && predecessorNodeIDs.length > 0) {
|
|
995
|
-
const fetched = await Promise.all(predecessorNodeIDs.map(async (nodeID) => ({
|
|
996
|
-
nodeID,
|
|
997
|
-
node: await getWorkspaceNode(client, { workspaceID: params.workspaceID, nodeID }),
|
|
998
|
-
})));
|
|
999
|
-
for (const { nodeID, node: predecessor } of fetched) {
|
|
1000
|
-
if (isPlainObject(predecessor)) {
|
|
1001
|
-
predecessorNodes.push(buildPredecessorSummary(nodeID, predecessor));
|
|
1002
|
-
const refInfo = extractPredecessorRefInfo(nodeID, predecessor);
|
|
1003
|
-
if (refInfo) {
|
|
1004
|
-
predecessorRefInfos.push(refInfo);
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
// Generate JOIN SQL with {{ ref() }} syntax if maintaining joins
|
|
1010
|
-
const joinSuggestions = buildJoinSuggestions(predecessorNodes);
|
|
1011
|
-
let joinSQL;
|
|
1012
|
-
if (predecessorRefInfos.length >= 2) {
|
|
1013
|
-
joinSQL = generateRefJoinSQL(predecessorRefInfos, joinSuggestions, params.joinType || "INNER JOIN");
|
|
1014
|
-
}
|
|
1015
|
-
else {
|
|
1016
|
-
// Fallback: predecessors missing locationName — use bare-name join SQL
|
|
1017
|
-
const bareJoin = generateJoinSQL(joinSuggestions, params.joinType || "INNER JOIN");
|
|
1018
|
-
joinSQL = {
|
|
1019
|
-
fromClause: bareJoin.fromClause,
|
|
1020
|
-
joinClauses: bareJoin.joinClauses.map((jc) => `${jc.type} ${jc.rightTableAlias}\n ON ${jc.onConditions.join("\n AND ")}`),
|
|
1021
|
-
fullSQL: bareJoin.fullSQL,
|
|
1022
|
-
warnings: predecessorNodeIDs.length >= 2
|
|
1023
|
-
? ["Predecessors are missing locationName — generated join uses bare table names instead of {{ ref() }} syntax. Set locationName on predecessor nodes for proper Coalesce references."]
|
|
1024
|
-
: [],
|
|
1025
|
-
};
|
|
1026
|
-
}
|
|
1027
|
-
// Build a lookup map of existing column datatypes from the current node
|
|
1028
|
-
// so GROUP BY pass-through columns can inherit their predecessor's dataType
|
|
1029
|
-
const existingColumns = Array.isArray(metadata.columns) ? metadata.columns : [];
|
|
1030
|
-
const existingDataTypeByName = new Map();
|
|
1031
|
-
for (const col of existingColumns) {
|
|
1032
|
-
if (isPlainObject(col) && typeof col.name === "string" && typeof col.dataType === "string") {
|
|
1033
|
-
existingDataTypeByName.set(normalizeColumnName(col.name), col.dataType);
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
// Build columns: group by columns + aggregates
|
|
1037
|
-
const columns = [];
|
|
1038
|
-
// Add GROUP BY columns
|
|
1039
|
-
for (const groupByCol of params.groupByColumns) {
|
|
1040
|
-
const colName = groupByCol.split(".").pop()?.replace(/"/g, "") || groupByCol;
|
|
1041
|
-
const inferredDatatype = inferDatatype(groupByCol)
|
|
1042
|
-
?? existingDataTypeByName.get(normalizeColumnName(colName))
|
|
1043
|
-
?? "VARCHAR";
|
|
1044
|
-
columns.push({
|
|
1045
|
-
name: colName,
|
|
1046
|
-
transform: groupByCol,
|
|
1047
|
-
dataType: inferredDatatype,
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
// Add aggregate columns
|
|
1051
|
-
for (const agg of params.aggregates) {
|
|
1052
|
-
const transform = `${agg.function}(${agg.expression})`;
|
|
1053
|
-
const inferredDatatype = inferDatatype(transform) ?? "VARCHAR";
|
|
1054
|
-
columns.push({
|
|
1055
|
-
name: agg.name,
|
|
1056
|
-
transform,
|
|
1057
|
-
dataType: inferredDatatype,
|
|
1058
|
-
description: agg.description,
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
// Analyze GROUP BY requirements
|
|
1062
|
-
const groupByAnalysis = analyzeColumnsForGroupBy(columns);
|
|
1063
|
-
const warnings = [];
|
|
1064
|
-
if (!groupByAnalysis.validation.valid) {
|
|
1065
|
-
warnings.push(...groupByAnalysis.validation.errors);
|
|
1066
|
-
}
|
|
1067
|
-
// Derive business key and change tracking column names
|
|
1068
|
-
// Business key = GROUP BY columns (dimensions)
|
|
1069
|
-
// Change tracking = aggregate columns (measures that change over time)
|
|
1070
|
-
const businessKeyColumnNames = new Set(params.groupByColumns.map((col) => col.split(".").pop()?.replace(/"/g, "") || col));
|
|
1071
|
-
const changeTrackingColumnNames = new Set(params.aggregates.map((agg) => agg.name));
|
|
1072
|
-
// Convert columns to metadata format with column-level attributes
|
|
1073
|
-
// columnSelector attributes (isBusinessKey, isChangeTracking) are set directly
|
|
1074
|
-
// on each column object — this is how Coalesce node type definitions work
|
|
1075
|
-
const metadataColumns = columns.map((col) => {
|
|
1076
|
-
const metadataCol = {
|
|
1077
|
-
name: col.name,
|
|
1078
|
-
dataType: col.dataType ?? "VARCHAR",
|
|
1079
|
-
transform: col.transform,
|
|
1080
|
-
nullable: true,
|
|
1081
|
-
};
|
|
1082
|
-
if (col.description) {
|
|
1083
|
-
metadataCol.description = col.description;
|
|
1084
|
-
}
|
|
1085
|
-
if (businessKeyColumnNames.has(col.name)) {
|
|
1086
|
-
metadataCol.isBusinessKey = true;
|
|
1087
|
-
}
|
|
1088
|
-
if (changeTrackingColumnNames.has(col.name)) {
|
|
1089
|
-
metadataCol.isChangeTracking = true;
|
|
1090
|
-
}
|
|
1091
|
-
return metadataCol;
|
|
1092
|
-
});
|
|
1093
|
-
// Replace columns with aggregation columns
|
|
1094
|
-
const updated = await replaceWorkspaceNodeColumns(client, {
|
|
1095
|
-
workspaceID: params.workspaceID,
|
|
1096
|
-
nodeID: params.nodeID,
|
|
1097
|
-
columns: metadataColumns,
|
|
1098
|
-
});
|
|
1099
|
-
// Write the generated JOIN SQL and/or GROUP BY directly to the node's sourceMapping.
|
|
1100
|
-
// Re-fetch the node to get fresh sourceMapping after column replacement.
|
|
1101
|
-
const hasJoinSQL = joinSQL.fullSQL.length > 0;
|
|
1102
|
-
const hasGroupBy = groupByAnalysis.groupByColumns.length > 0;
|
|
1103
|
-
if (hasJoinSQL || hasGroupBy) {
|
|
1104
|
-
const freshNode = await getWorkspaceNode(client, {
|
|
1105
|
-
workspaceID: params.workspaceID,
|
|
1106
|
-
nodeID: params.nodeID,
|
|
1107
|
-
});
|
|
1108
|
-
const freshMetadata = isPlainObject(freshNode) && isPlainObject(freshNode.metadata)
|
|
1109
|
-
? freshNode.metadata
|
|
1110
|
-
: {};
|
|
1111
|
-
const freshSourceMapping = Array.isArray(freshMetadata.sourceMapping)
|
|
1112
|
-
? freshMetadata.sourceMapping
|
|
1113
|
-
: [];
|
|
1114
|
-
const firstEntry = freshSourceMapping.find(isPlainObject);
|
|
1115
|
-
if (!firstEntry) {
|
|
1116
|
-
joinSQL.warnings.push("Could not write joinCondition — node has no sourceMapping entries. " +
|
|
1117
|
-
"The generated SQL is returned but was not persisted to the node.");
|
|
1118
|
-
}
|
|
1119
|
-
else {
|
|
1120
|
-
const existingJoin = isPlainObject(firstEntry.join) ? firstEntry.join : {};
|
|
1121
|
-
const existingJoinCondition = typeof existingJoin.joinCondition === "string"
|
|
1122
|
-
? existingJoin.joinCondition.trim()
|
|
1123
|
-
: "";
|
|
1124
|
-
let fullJoinCondition;
|
|
1125
|
-
if (hasJoinSQL) {
|
|
1126
|
-
// Multi-predecessor: use generated FROM/JOIN + GROUP BY
|
|
1127
|
-
const groupByClause = hasGroupBy
|
|
1128
|
-
? `\nGROUP BY ${groupByAnalysis.groupByColumns.join(", ")}`
|
|
1129
|
-
: "";
|
|
1130
|
-
fullJoinCondition = joinSQL.fullSQL + groupByClause;
|
|
1131
|
-
}
|
|
1132
|
-
else {
|
|
1133
|
-
// Single-predecessor aggregation: append GROUP BY to existing joinCondition
|
|
1134
|
-
const groupByClause = `\nGROUP BY ${groupByAnalysis.groupByColumns.join(", ")}`;
|
|
1135
|
-
fullJoinCondition = existingJoinCondition.length > 0
|
|
1136
|
-
? existingJoinCondition + groupByClause
|
|
1137
|
-
: groupByClause.trim();
|
|
1138
|
-
}
|
|
1139
|
-
const updatedSourceMapping = freshSourceMapping.map((entry) => entry === firstEntry
|
|
1140
|
-
? { ...firstEntry, join: { ...existingJoin, joinCondition: fullJoinCondition } }
|
|
1141
|
-
: entry);
|
|
1142
|
-
await updateWorkspaceNode(client, {
|
|
1143
|
-
workspaceID: params.workspaceID,
|
|
1144
|
-
nodeID: params.nodeID,
|
|
1145
|
-
changes: {
|
|
1146
|
-
metadata: {
|
|
1147
|
-
sourceMapping: updatedSourceMapping,
|
|
1148
|
-
},
|
|
1149
|
-
},
|
|
1150
|
-
});
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
// Complete configuration with intelligent rules (best-effort)
|
|
1154
|
-
const completion = await tryCompleteNodeConfiguration(client, {
|
|
1155
|
-
workspaceID: params.workspaceID,
|
|
1156
|
-
nodeID: params.nodeID,
|
|
1157
|
-
repoPath: params.repoPath,
|
|
1158
|
-
});
|
|
1159
|
-
return {
|
|
1160
|
-
node: completion.configCompletion?.node ?? updated,
|
|
1161
|
-
joinSQL,
|
|
1162
|
-
groupByAnalysis,
|
|
1163
|
-
validation: {
|
|
1164
|
-
valid: groupByAnalysis.validation.valid && warnings.length === 0,
|
|
1165
|
-
warnings,
|
|
1166
|
-
},
|
|
1167
|
-
...completion,
|
|
1168
|
-
};
|
|
1169
|
-
}
|
|
1170
|
-
export async function applyJoinCondition(client, params) {
|
|
1171
|
-
const joinType = params.joinType ?? "INNER JOIN";
|
|
1172
|
-
// Fetch the node to extract predecessors
|
|
1173
|
-
const current = await getWorkspaceNode(client, {
|
|
1174
|
-
workspaceID: params.workspaceID,
|
|
1175
|
-
nodeID: params.nodeID,
|
|
1176
|
-
});
|
|
1177
|
-
if (!isPlainObject(current)) {
|
|
1178
|
-
throw new Error("Node response was not an object");
|
|
1179
|
-
}
|
|
1180
|
-
const metadata = isPlainObject(current.metadata) ? current.metadata : {};
|
|
1181
|
-
const sourceMapping = Array.isArray(metadata.sourceMapping) ? metadata.sourceMapping : [];
|
|
1182
|
-
const predecessorNodeIDs = extractPredecessorNodeIDs(metadata);
|
|
1183
|
-
if (predecessorNodeIDs.length < 2) {
|
|
1184
|
-
throw new Error("apply_join_condition requires a node with 2+ predecessors. " +
|
|
1185
|
-
"This node has " + predecessorNodeIDs.length + " predecessor(s). " +
|
|
1186
|
-
"For single-predecessor nodes, set the joinCondition directly via update_workspace_node.");
|
|
1187
|
-
}
|
|
1188
|
-
// Fetch all predecessors in parallel to get their names, locationNames, and columns
|
|
1189
|
-
const predecessorRefInfos = [];
|
|
1190
|
-
const predecessorSummaries = [];
|
|
1191
|
-
const warnings = [];
|
|
1192
|
-
const fetchedPredecessors = await Promise.all(predecessorNodeIDs.map(async (nodeID) => ({
|
|
1193
|
-
nodeID,
|
|
1194
|
-
node: await getWorkspaceNode(client, { workspaceID: params.workspaceID, nodeID }),
|
|
1195
|
-
})));
|
|
1196
|
-
for (const { nodeID, node: predecessor } of fetchedPredecessors) {
|
|
1197
|
-
if (!isPlainObject(predecessor)) {
|
|
1198
|
-
warnings.push(`Could not fetch predecessor ${nodeID}`);
|
|
1199
|
-
continue;
|
|
1200
|
-
}
|
|
1201
|
-
const refInfo = extractPredecessorRefInfo(nodeID, predecessor);
|
|
1202
|
-
if (!refInfo) {
|
|
1203
|
-
const name = typeof predecessor.name === "string" ? predecessor.name : nodeID;
|
|
1204
|
-
warnings.push(`Predecessor "${name}" is missing locationName. ` +
|
|
1205
|
-
`Set it in the Coalesce UI or via update_workspace_node before applying joins.`);
|
|
1206
|
-
continue;
|
|
1207
|
-
}
|
|
1208
|
-
predecessorRefInfos.push(refInfo);
|
|
1209
|
-
predecessorSummaries.push(buildPredecessorSummary(nodeID, predecessor));
|
|
1210
|
-
}
|
|
1211
|
-
if (predecessorRefInfos.length < 2) {
|
|
1212
|
-
throw new Error("Could not resolve 2+ predecessors with valid name and locationName. " +
|
|
1213
|
-
"Ensure all predecessor nodes have a locationName set.");
|
|
1214
|
-
}
|
|
1215
|
-
// Build join suggestions from common columns
|
|
1216
|
-
const joinSuggestions = buildJoinSuggestions(predecessorSummaries);
|
|
1217
|
-
// Generate FROM/JOIN/ON with {{ ref() }} syntax
|
|
1218
|
-
const joinResult = generateRefJoinSQL(predecessorRefInfos, joinSuggestions, joinType, params.joinColumnOverrides);
|
|
1219
|
-
warnings.push(...joinResult.warnings);
|
|
1220
|
-
// Build full joinCondition: FROM/JOIN + WHERE + QUALIFY
|
|
1221
|
-
const parts = [joinResult.fullSQL];
|
|
1222
|
-
if (params.whereClause) {
|
|
1223
|
-
const trimmedWhere = params.whereClause.trim();
|
|
1224
|
-
const whereStr = /^where\b/i.test(trimmedWhere)
|
|
1225
|
-
? trimmedWhere
|
|
1226
|
-
: `WHERE ${trimmedWhere}`;
|
|
1227
|
-
parts.push(whereStr);
|
|
1228
|
-
}
|
|
1229
|
-
if (params.qualifyClause) {
|
|
1230
|
-
const trimmedQualify = params.qualifyClause.trim();
|
|
1231
|
-
const qualifyStr = /^qualify\b/i.test(trimmedQualify)
|
|
1232
|
-
? trimmedQualify
|
|
1233
|
-
: `QUALIFY ${trimmedQualify}`;
|
|
1234
|
-
parts.push(qualifyStr);
|
|
1235
|
-
}
|
|
1236
|
-
const fullJoinCondition = parts.join("\n");
|
|
1237
|
-
// Write to node's sourceMapping
|
|
1238
|
-
const firstEntry = sourceMapping.find(isPlainObject);
|
|
1239
|
-
if (!firstEntry) {
|
|
1240
|
-
warnings.push("Could not write joinCondition — node has no sourceMapping entries. " +
|
|
1241
|
-
"The generated SQL is returned but was not persisted to the node.");
|
|
1242
|
-
}
|
|
1243
|
-
else {
|
|
1244
|
-
const updatedSourceMapping = sourceMapping.map((entry) => entry === firstEntry
|
|
1245
|
-
? {
|
|
1246
|
-
...firstEntry,
|
|
1247
|
-
join: {
|
|
1248
|
-
...(isPlainObject(firstEntry.join) ? firstEntry.join : {}),
|
|
1249
|
-
joinCondition: fullJoinCondition,
|
|
1250
|
-
},
|
|
1251
|
-
}
|
|
1252
|
-
: entry);
|
|
1253
|
-
await updateWorkspaceNode(client, {
|
|
1254
|
-
workspaceID: params.workspaceID,
|
|
1255
|
-
nodeID: params.nodeID,
|
|
1256
|
-
changes: {
|
|
1257
|
-
metadata: {
|
|
1258
|
-
sourceMapping: updatedSourceMapping,
|
|
1259
|
-
},
|
|
1260
|
-
},
|
|
1261
|
-
});
|
|
1262
|
-
}
|
|
1263
|
-
return {
|
|
1264
|
-
joinCondition: fullJoinCondition,
|
|
1265
|
-
joinSuggestions,
|
|
1266
|
-
predecessors: predecessorRefInfos.map((p) => ({
|
|
1267
|
-
nodeID: p.nodeID,
|
|
1268
|
-
nodeName: p.nodeName,
|
|
1269
|
-
locationName: p.locationName,
|
|
1270
|
-
columnCount: p.columnNames.length,
|
|
1271
|
-
})),
|
|
1272
|
-
warnings,
|
|
1273
|
-
};
|
|
1274
|
-
}
|
|
67
|
+
// ── Group F: Workspace node types + external schema ────────────────────
|
|
1275
68
|
/**
|
|
1276
69
|
* Returns the distinct node types observed in existing workspace nodes.
|
|
1277
70
|
* This is intentionally observation-based and should not be treated as a true
|