From d3dec09952e19a98d44b5d1c9baa8d20b818e4a4 Mon Sep 17 00:00:00 2001 From: "b.mucolli" Date: Thu, 5 Nov 2026 17:01:55 +0100 Subject: [PATCH 01/10] PSL-3700: List schemas for SCHEMA_REFERENCE variable assignments `list assignments --type SCHEMA_REFERENCE` returns the global schema and the data connection schemas of every data pool the caller is admin of, so Team-to-Team Copy can map Context Model schema variables. Co-Authored-By: Claude Includes-AI-Code: true --- docs/user-guide/config-commands.md | 8 ++- .../api/variable-assignment-candidates-api.ts | 4 +- .../variable-assignment-apis.constants.ts | 66 +++++++++++++++++++ .../list-assignments.spec.ts | 52 ++++++++++++++- 4 files changed, 127 insertions(+), 3 deletions(-) diff --git a/docs/user-guide/config-commands.md b/docs/user-guide/config-commands.md index 086ff699..ff3ead75 100644 --- a/docs/user-guide/config-commands.md +++ b/docs/user-guide/config-commands.md @@ -382,12 +382,18 @@ The params argument should be passed in a key value format separated by commas w --params key1=value1,key2=value2,...,keyN=valueN ``` -Supported types are `DATA_MODEL`, `CONNECTION` and `DATA_POOL_ID`. Each returns a list of assignments: +Supported types are `DATA_MODEL`, `CONNECTION`, `DATA_POOL_ID` and `SCHEMA_REFERENCE`. Each returns a list of assignments: ```json [{"value": "4b4f32c8-4529-43f1-9399-2a6de94ac13a", "name": "Order Management Pool"}] ``` +`SCHEMA_REFERENCE` lists the schemas of every data pool you are an admin of: the pool's global schema and the schema of each data connection, except data connections of managed packages such as Context Models. The value is the technical schema name: + +```json +[{"value": "4b4f32c8-4529-43f1-9399-2a6de94ac13a_sap", "name": "Order Management Pool / SAP ECC"}] +``` + ### Mapping Variables After getting the variables list (with definitions and assignments in the source team) and the assignments (possible values on the target team), you can change the value of the source team to one of the options provided when listing assignments. diff --git a/src/commands/configuration-management/api/variable-assignment-candidates-api.ts b/src/commands/configuration-management/api/variable-assignment-candidates-api.ts index 3d45fc37..1afd8fc7 100644 --- a/src/commands/configuration-management/api/variable-assignment-candidates-api.ts +++ b/src/commands/configuration-management/api/variable-assignment-candidates-api.ts @@ -20,7 +20,9 @@ export class VariableAssignmentCandidatesApi { const apiUrl: string = assignmentApi.url + (params.toString().length ? `?${params.toString()}` : ""); - const candidates = await this.httpClient().get(apiUrl).catch(e => { + const get = (url: string): Promise => this.httpClient().get(url); + const request = assignmentApi.loadCandidates ? assignmentApi.loadCandidates(get, apiUrl) : get(apiUrl); + const candidates = await request.catch(e => { throw new FatalError(`Problem getting variables assignment values for type ${type}: ${e}`); }); diff --git a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts index 1b389d06..8f34d725 100644 --- a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts +++ b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts @@ -6,6 +6,13 @@ export interface VariableAssignment { export interface VariableAssignmentApi { url: string; toAssignment: (candidate: any) => VariableAssignment; + loadCandidates?: (get: (url: string) => Promise, url: string) => Promise; +} + +interface SchemaCandidate { + schema: string; + name: string; + poolName?: string; } const connectionName = (connection: any): string => { @@ -13,6 +20,57 @@ const connectionName = (connection: any): string => { return parts.length ? parts.join(" - ") : connection?.id; }; +const DATA_POOL_ADMIN_PERMISSION = "ADMIN"; +const MANAGED_SCHEMA_SOURCE_TYPES = new Set(["pig", "context-model", "context-model-semantic"]); +const MAX_PARALLEL_DATA_SOURCE_REQUESTS = 4; +const SCHEMA_POOL_PREFIX = /^([\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})_/i; + +const toPoolSchemas = (pool: any, dataSources: any[]): SchemaCandidate[] => { + const sourceSchemas = new Map(); + for (const dataSource of dataSources) { + const schema = dataSource?.targetSchemaName || `${pool.id}_${dataSource?.id}`; + const schemaPoolId = SCHEMA_POOL_PREFIX.exec(schema)?.[1]; + if (schemaPoolId && schemaPoolId.toLowerCase() !== pool.id.toLowerCase()) { + continue; + } + const existing = sourceSchemas.get(schema); + sourceSchemas.set(schema, { + schema, + name: existing?.name ?? (dataSource?.name || schema), + poolName: pool.name, + managed: !!existing?.managed || MANAGED_SCHEMA_SOURCE_TYPES.has(dataSource?.type), + }); + } + const assignableSchemas = [...sourceSchemas.values()] + .filter(candidate => !candidate.managed) + .map(({schema, name, poolName}) => ({schema, name, poolName})); + return [{schema: pool.id, name: "Global schema", poolName: pool.name}, ...assignableSchemas]; +}; + +const mapWithConcurrency = async (items: T[], limit: number, mapper: (item: T) => Promise): Promise => { + const results: R[] = new Array(items.length); + let next = 0; + const worker = async (): Promise => { + while (next < items.length) { + const index = next++; + results[index] = await mapper(items[index]); + } + }; + await Promise.all(Array.from({length: Math.min(limit, items.length)}, worker)); + return results; +}; + +const loadSchemaCandidates = async (get: (url: string) => Promise, poolsUrl: string): Promise => { + const pools = await get(poolsUrl); + const adminPools = (Array.isArray(pools) ? pools : []) + .filter(pool => !!pool?.id && (pool.permissions ?? []).includes(DATA_POOL_ADMIN_PERMISSION)); + const schemasPerPool = await mapWithConcurrency(adminPools, MAX_PARALLEL_DATA_SOURCE_REQUESTS, async pool => { + const dataSources = await get(`/integration/api/pools/${encodeURIComponent(pool.id)}/data-sources/slim-overview`); + return toPoolSchemas(pool, Array.isArray(dataSources) ? dataSources : []); + }); + return schemasPerPool.flat(); +}; + export const variableAssignmentApis: { [key: string]: VariableAssignmentApi } = { DATA_MODEL: { url: "/package-manager/api/compute-pools/pools-with-data-models", @@ -35,4 +93,12 @@ export const variableAssignmentApis: { [key: string]: VariableAssignmentApi } = name: candidate?.name ?? candidate?.id, }), }, + SCHEMA_REFERENCE: { + url: "/integration/api/pools", + loadCandidates: loadSchemaCandidates, + toAssignment: (candidate: SchemaCandidate) => ({ + value: candidate?.schema, + name: [candidate?.poolName, candidate?.name].filter(Boolean).join(" / ") || candidate?.schema, + }), + }, }; diff --git a/tests/commands/configuration-management/list-assignments.spec.ts b/tests/commands/configuration-management/list-assignments.spec.ts index 329ceb2d..ccd34a16 100644 --- a/tests/commands/configuration-management/list-assignments.spec.ts +++ b/tests/commands/configuration-management/list-assignments.spec.ts @@ -1,4 +1,4 @@ -import { mockAxiosGet, mockedAxiosInstance } from "../../utls/http-requests-mock"; +import { mockAxiosGet, mockAxiosGetError, mockedAxiosInstance } from "../../utls/http-requests-mock"; import { VariableCommandService } from "../../../src/commands/configuration-management/variable-command.service"; import { testContext } from "../../utls/test-context"; import { loggingTestTransport } from "../../jest.setup"; @@ -10,6 +10,11 @@ describe("List assignments", () => { const DATA_MODEL_URL = "https://myTeam.celonis.cloud/package-manager/api/compute-pools/pools-with-data-models"; const CONNECTIONS_URL = "https://myTeam.celonis.cloud/process-automation-v2/api/connections?param1=value1¶m2=value2"; const DATA_POOLS_URL = "https://myTeam.celonis.cloud/integration/api/pools"; + const POOL_1 = "11111111-1111-1111-1111-111111111111"; + const POOL_2 = "22222222-2222-2222-2222-222222222222"; + const POOL_3 = "33333333-3333-3333-3333-333333333333"; + const dataSourcesUrl = (poolId: string) => + `https://myTeam.celonis.cloud/integration/api/pools/${poolId}/data-sources/slim-overview`; const dataModelCandidate = (poolName: string, dataModelId: string, dataModelName: string) => ({ pool: {id: "pool-" + dataModelId, name: poolName}, @@ -95,6 +100,51 @@ describe("List assignments", () => { expect(getJsonFromDownloadedFile()).toEqual([{value: "pool-1", name: "Pool 1"}]); }) + it("Should list the global and data connection schemas of admin pools for the SCHEMA_REFERENCE type", async () => { + mockAxiosGet(DATA_POOLS_URL, [ + {id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}, + {id: POOL_2, name: "Pool 2", permissions: ["VIEW_DATA_POOL"]}, + {id: POOL_3, name: "Pool 3", permissions: ["ADMIN", "VIEW_DATA_POOL"]}, + ]); + mockAxiosGet(dataSourcesUrl(POOL_1), [ + {id: "sap", name: "SAP ECC", type: "sap", targetSchemaName: "sap_schema"}, + {id: "jdbc", name: "", type: "jdbc"}, + {id: "ccm", name: "My Context Model", type: "context-model", targetSchemaName: "ccm_schema"}, + {id: "imported", name: "Imported", type: "imported", targetSchemaName: `${POOL_2}_other`}, + ]); + mockAxiosGet(dataSourcesUrl(POOL_3), []); + + await new VariableCommandService(testContext).listAssignments("SCHEMA_REFERENCE", true, ""); + + expect(mockedAxiosInstance.get).not.toHaveBeenCalledWith(dataSourcesUrl(POOL_2), expect.anything()); + expect(getJsonFromDownloadedFile()).toEqual([ + {value: POOL_1, name: "Pool 1 / Global schema"}, + {value: "sap_schema", name: "Pool 1 / SAP ECC"}, + {value: `${POOL_1}_jdbc`, name: `Pool 1 / ${POOL_1}_jdbc`}, + {value: POOL_3, name: "Pool 3 / Global schema"}, + ]); + }) + + it("Should not offer a schema that a managed data source shares", async () => { + mockAxiosGet(DATA_POOLS_URL, [{id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}]); + mockAxiosGet(dataSourcesUrl(POOL_1), [ + {id: "sap", name: "SAP ECC", type: "sap", targetSchemaName: "shared_schema"}, + {id: "pig", name: "PIG", type: "pig", targetSchemaName: "shared_schema"}, + ]); + + await new VariableCommandService(testContext).listAssignments("SCHEMA_REFERENCE", true, ""); + + expect(getJsonFromDownloadedFile()).toEqual([{value: POOL_1, name: "Pool 1 / Global schema"}]); + }) + + it("Should fail when the data sources of a pool cannot be loaded", async () => { + mockAxiosGet(DATA_POOLS_URL, [{id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}]); + mockAxiosGetError(dataSourcesUrl(POOL_1), 500, "Internal Server Error"); + + await expect(new VariableCommandService(testContext).listAssignments("SCHEMA_REFERENCE", true, "")) + .rejects.toThrow("Problem getting variables assignment values for type SCHEMA_REFERENCE"); + }) + it("Should throw error for unsupported variable types", async () => { const type: string = "DUMMY_UNSUPPORTED_TYPE"; From b21d37effd2f0c6401434bf1922de6da5b884f4a Mon Sep 17 00:00:00 2001 From: "b.mucolli" Date: Fri, 6 Nov 2026 10:41:05 +0100 Subject: [PATCH 02/10] PSL-3700: Match managed data source types case-insensitively The slim overview returns the PIG data source type as "PIG", so the managed schema check now ignores case. Co-Authored-By: Claude Includes-AI-Code: true --- .../interfaces/variable-assignment-apis.constants.ts | 2 +- .../commands/configuration-management/list-assignments.spec.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts index 8f34d725..67ce87c0 100644 --- a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts +++ b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts @@ -38,7 +38,7 @@ const toPoolSchemas = (pool: any, dataSources: any[]): SchemaCandidate[] => { schema, name: existing?.name ?? (dataSource?.name || schema), poolName: pool.name, - managed: !!existing?.managed || MANAGED_SCHEMA_SOURCE_TYPES.has(dataSource?.type), + managed: !!existing?.managed || MANAGED_SCHEMA_SOURCE_TYPES.has(String(dataSource?.type ?? "").toLowerCase()), }); } const assignableSchemas = [...sourceSchemas.values()] diff --git a/tests/commands/configuration-management/list-assignments.spec.ts b/tests/commands/configuration-management/list-assignments.spec.ts index ccd34a16..179c212b 100644 --- a/tests/commands/configuration-management/list-assignments.spec.ts +++ b/tests/commands/configuration-management/list-assignments.spec.ts @@ -129,7 +129,8 @@ describe("List assignments", () => { mockAxiosGet(DATA_POOLS_URL, [{id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}]); mockAxiosGet(dataSourcesUrl(POOL_1), [ {id: "sap", name: "SAP ECC", type: "sap", targetSchemaName: "shared_schema"}, - {id: "pig", name: "PIG", type: "pig", targetSchemaName: "shared_schema"}, + {id: "pig", name: "PIG", type: "PIG", targetSchemaName: "shared_schema"}, + {id: "ccm", name: "My Context Model", type: "context-model"}, ]); await new VariableCommandService(testContext).listAssignments("SCHEMA_REFERENCE", true, ""); From 42d5bd7611ddc8ad19e96a8ed6b81cae96d47e54 Mon Sep 17 00:00:00 2001 From: "b.mucolli" Date: Fri, 6 Nov 2026 14:14:31 +0100 Subject: [PATCH 03/10] PSL-3700: Only list schemas of pools on Storium for SCHEMA_REFERENCE Context Models read through Storium, so the schema list keeps only pools whose primary or secondary pool provider is Storium, like the Context Model schema picker. The engine comes from the slim overview entries; pools without entries are resolved in batches of 100 through GET /integration/api/pools/pool-provider-types. Co-Authored-By: Claude Includes-AI-Code: true --- docs/user-guide/config-commands.md | 2 +- .../variable-assignment-apis.constants.ts | 37 ++++++++++- .../list-assignments.spec.ts | 64 +++++++++++++++++-- 3 files changed, 92 insertions(+), 11 deletions(-) diff --git a/docs/user-guide/config-commands.md b/docs/user-guide/config-commands.md index eb416342..8b50a45e 100644 --- a/docs/user-guide/config-commands.md +++ b/docs/user-guide/config-commands.md @@ -414,7 +414,7 @@ Supported types are `DATA_MODEL`, `CONNECTION`, `DATA_POOL_ID`, `OBJECTS_EVENTS_ [{"value": "4b4f32c8-4529-43f1-9399-2a6de94ac13a", "name": "Order Management Pool"}] ``` -`SCHEMA_REFERENCE` lists the schemas of every data pool you are an admin of: the pool's global schema and the schema of each data connection, except data connections of managed packages such as Context Models. The value is the technical schema name: +`SCHEMA_REFERENCE` lists the schemas of every data pool on Storium that you are an admin of: the pool's global schema and the schema of each data connection, except data connections of managed packages such as Context Models. A pool counts as on Storium when Storium is its primary or, during a migration, its secondary engine. The value is the technical schema name: ```json [{"value": "4b4f32c8-4529-43f1-9399-2a6de94ac13a_sap", "name": "Order Management Pool / SAP ECC"}] diff --git a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts index 27ff1e9d..fe4c2032 100644 --- a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts +++ b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts @@ -24,6 +24,8 @@ const connectionName = (connection: any): string => { const DATA_POOL_ADMIN_PERMISSION = "ADMIN"; const MANAGED_SCHEMA_SOURCE_TYPES = new Set(["pig", "context-model", "context-model-semantic"]); const MAX_PARALLEL_DATA_SOURCE_REQUESTS = 4; +const MAX_POOL_IDS_PER_REQUEST = 100; +const STORIUM_POOL_PROVIDER_TYPE = "STORIUM"; const SCHEMA_POOL_PREFIX = /^([\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})_/i; const toPoolSchemas = (pool: any, dataSources: any[]): SchemaCandidate[] => { @@ -61,15 +63,44 @@ const mapWithConcurrency = async (items: T[], limit: number, mapper: (item return results; }; +const isStoriumBacked = (types: any): boolean => + [types?.primaryPoolProviderType, types?.secondaryPoolProviderType].includes(STORIUM_POOL_PROVIDER_TYPE); + +const loadMissingPoolProviderTypes = async (get: (url: string) => Promise, poolIds: string[]): Promise => { + const batches: string[][] = []; + for (let index = 0; index < poolIds.length; index += MAX_POOL_IDS_PER_REQUEST) { + batches.push(poolIds.slice(index, index + MAX_POOL_IDS_PER_REQUEST)); + } + const typesPerBatch = await Promise.all(batches.map(batch => { + const query = batch.map(poolId => `poolIds=${encodeURIComponent(poolId)}`).join("&"); + return get(`/integration/api/pools/pool-provider-types?${query}`); + })); + return typesPerBatch.flatMap(types => Array.isArray(types) ? types : []); +}; + const loadSchemaCandidates = async (get: (url: string) => Promise, poolsUrl: string): Promise => { const pools = await get(poolsUrl); const adminPools = (Array.isArray(pools) ? pools : []) .filter(pool => !!pool?.id && (pool.permissions ?? []).includes(DATA_POOL_ADMIN_PERMISSION)); - const schemasPerPool = await mapWithConcurrency(adminPools, MAX_PARALLEL_DATA_SOURCE_REQUESTS, async pool => { + const loadedPools = await mapWithConcurrency(adminPools, MAX_PARALLEL_DATA_SOURCE_REQUESTS, async pool => { const dataSources = await get(`/integration/api/pools/${encodeURIComponent(pool.id)}/data-sources/slim-overview`); - return toPoolSchemas(pool, Array.isArray(dataSources) ? dataSources : []); + return {pool, dataSources: Array.isArray(dataSources) ? dataSources : []}; }); - return schemasPerPool.flat(); + + const typesByPool = new Map(); + for (const {pool, dataSources} of loadedPools) { + const typed = dataSources.find(dataSource => dataSource?.primaryPoolProviderType); + if (typed) { + typesByPool.set(pool.id, typed); + } + } + const untypedPoolIds = loadedPools.map(({pool}) => pool.id).filter(poolId => !typesByPool.has(poolId)); + const loadedTypes = untypedPoolIds.length ? await loadMissingPoolProviderTypes(get, untypedPoolIds) : []; + loadedTypes.forEach(types => typesByPool.set(types?.poolId, types)); + + return loadedPools + .filter(({pool}) => isStoriumBacked(typesByPool.get(pool.id))) + .flatMap(({pool, dataSources}) => toPoolSchemas(pool, dataSources)); }; export const variableAssignmentApis: { [key: string]: VariableAssignmentApi } = { diff --git a/tests/commands/configuration-management/list-assignments.spec.ts b/tests/commands/configuration-management/list-assignments.spec.ts index 107ffc62..4639378a 100644 --- a/tests/commands/configuration-management/list-assignments.spec.ts +++ b/tests/commands/configuration-management/list-assignments.spec.ts @@ -16,6 +16,10 @@ describe("List assignments", () => { const POOL_3 = "33333333-3333-3333-3333-333333333333"; const dataSourcesUrl = (poolId: string) => `https://myTeam.celonis.cloud/integration/api/pools/${poolId}/data-sources/slim-overview`; + const poolProviderTypesUrl = (...poolIds: string[]) => + `https://myTeam.celonis.cloud/integration/api/pools/pool-provider-types?${poolIds.map(id => `poolIds=${id}`).join("&")}`; + const ON_STORIUM = {primaryPoolProviderType: "STORIUM"}; + const ON_LEGACY_ENGINE = {primaryPoolProviderType: "HDFS"}; const dataModelCandidate = (poolName: string, dataModelId: string, dataModelName: string) => ({ pool: {id: "pool-" + dataModelId, name: poolName}, @@ -132,12 +136,13 @@ describe("List assignments", () => { {id: POOL_3, name: "Pool 3", permissions: ["ADMIN", "VIEW_DATA_POOL"]}, ]); mockAxiosGet(dataSourcesUrl(POOL_1), [ - {id: "sap", name: "SAP ECC", type: "sap", targetSchemaName: "sap_schema"}, - {id: "jdbc", name: "", type: "jdbc"}, - {id: "ccm", name: "My Context Model", type: "context-model", targetSchemaName: "ccm_schema"}, - {id: "imported", name: "Imported", type: "imported", targetSchemaName: `${POOL_2}_other`}, + {id: "sap", name: "SAP ECC", type: "sap", targetSchemaName: "sap_schema", ...ON_STORIUM}, + {id: "jdbc", name: "", type: "jdbc", ...ON_STORIUM}, + {id: "ccm", name: "My Context Model", type: "context-model", targetSchemaName: "ccm_schema", ...ON_STORIUM}, + {id: "imported", name: "Imported", type: "imported", targetSchemaName: `${POOL_2}_other`, ...ON_STORIUM}, ]); mockAxiosGet(dataSourcesUrl(POOL_3), []); + mockAxiosGet(poolProviderTypesUrl(POOL_3), [{poolId: POOL_3, ...ON_STORIUM}]); await new VariableCommandService(testContext).listAssignments("SCHEMA_REFERENCE", true, ""); @@ -153,9 +158,9 @@ describe("List assignments", () => { it("Should not offer a schema that a managed data source shares", async () => { mockAxiosGet(DATA_POOLS_URL, [{id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}]); mockAxiosGet(dataSourcesUrl(POOL_1), [ - {id: "sap", name: "SAP ECC", type: "sap", targetSchemaName: "shared_schema"}, - {id: "pig", name: "PIG", type: "PIG", targetSchemaName: "shared_schema"}, - {id: "ccm", name: "My Context Model", type: "context-model"}, + {id: "sap", name: "SAP ECC", type: "sap", targetSchemaName: "shared_schema", ...ON_STORIUM}, + {id: "pig", name: "PIG", type: "PIG", targetSchemaName: "shared_schema", ...ON_STORIUM}, + {id: "ccm", name: "My Context Model", type: "context-model", ...ON_STORIUM}, ]); await new VariableCommandService(testContext).listAssignments("SCHEMA_REFERENCE", true, ""); @@ -163,6 +168,51 @@ describe("List assignments", () => { expect(getJsonFromDownloadedFile()).toEqual([{value: POOL_1, name: "Pool 1 / Global schema"}]); }) + it("Should only offer schemas of pools on Storium, also while Storium is the secondary engine", async () => { + mockAxiosGet(DATA_POOLS_URL, [ + {id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}, + {id: POOL_3, name: "Pool 3", permissions: ["ADMIN"]}, + ]); + mockAxiosGet(dataSourcesUrl(POOL_1), [{id: "sap", name: "SAP ECC", type: "sap", ...ON_LEGACY_ENGINE}]); + mockAxiosGet(dataSourcesUrl(POOL_3), [ + {id: "jdbc", name: "Postgres", type: "jdbc", primaryPoolProviderType: "HDFS", secondaryPoolProviderType: "STORIUM"}, + ]); + + await new VariableCommandService(testContext).listAssignments("SCHEMA_REFERENCE", true, ""); + + expect(getJsonFromDownloadedFile()).toEqual([ + {value: POOL_3, name: "Pool 3 / Global schema"}, + {value: `${POOL_3}_jdbc`, name: "Pool 3 / Postgres"}, + ]); + }) + + it("Should resolve the engine of pools without data sources in one request", async () => { + mockAxiosGet(DATA_POOLS_URL, [ + {id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}, + {id: POOL_3, name: "Pool 3", permissions: ["ADMIN"]}, + ]); + mockAxiosGet(dataSourcesUrl(POOL_1), []); + mockAxiosGet(dataSourcesUrl(POOL_3), []); + mockAxiosGet(poolProviderTypesUrl(POOL_1, POOL_3), [ + {poolId: POOL_1, ...ON_STORIUM}, + {poolId: POOL_3, ...ON_LEGACY_ENGINE}, + ]); + + await new VariableCommandService(testContext).listAssignments("SCHEMA_REFERENCE", true, ""); + + expect(mockedAxiosInstance.get).toHaveBeenCalledWith(poolProviderTypesUrl(POOL_1, POOL_3), expect.anything()); + expect(getJsonFromDownloadedFile()).toEqual([{value: POOL_1, name: "Pool 1 / Global schema"}]); + }) + + it("Should fail when the engine of a pool cannot be loaded", async () => { + mockAxiosGet(DATA_POOLS_URL, [{id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}]); + mockAxiosGet(dataSourcesUrl(POOL_1), []); + mockAxiosGetError(poolProviderTypesUrl(POOL_1), 404, "Not Found"); + + await expect(new VariableCommandService(testContext).listAssignments("SCHEMA_REFERENCE", true, "")) + .rejects.toThrow("Problem getting variables assignment values for type SCHEMA_REFERENCE"); + }) + it("Should fail when the data sources of a pool cannot be loaded", async () => { mockAxiosGet(DATA_POOLS_URL, [{id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}]); mockAxiosGetError(dataSourcesUrl(POOL_1), 500, "Internal Server Error"); From d14e24b79d6b701d092e9b2fe15cafe6eec7eb32 Mon Sep 17 00:00:00 2001 From: "b.mucolli" Date: Fri, 6 Nov 2026 14:17:06 +0100 Subject: [PATCH 04/10] PSL-3700: List the global schema once and skip OCDM for SCHEMA_REFERENCE The slim overview now returns virtual global and OCDM entries. The global schema is no longer listed twice, and OCDM schemas are left out, as Package Settings only assigns Data Connection and Global schemas to schema variables. Co-Authored-By: Claude Includes-AI-Code: true --- docs/user-guide/config-commands.md | 2 +- .../variable-assignment-apis.constants.ts | 11 ++++++++++- .../list-assignments.spec.ts | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/docs/user-guide/config-commands.md b/docs/user-guide/config-commands.md index 8b50a45e..c02ecc7b 100644 --- a/docs/user-guide/config-commands.md +++ b/docs/user-guide/config-commands.md @@ -414,7 +414,7 @@ Supported types are `DATA_MODEL`, `CONNECTION`, `DATA_POOL_ID`, `OBJECTS_EVENTS_ [{"value": "4b4f32c8-4529-43f1-9399-2a6de94ac13a", "name": "Order Management Pool"}] ``` -`SCHEMA_REFERENCE` lists the schemas of every data pool on Storium that you are an admin of: the pool's global schema and the schema of each data connection, except data connections of managed packages such as Context Models. A pool counts as on Storium when Storium is its primary or, during a migration, its secondary engine. The value is the technical schema name: +`SCHEMA_REFERENCE` lists the schemas of every data pool on Storium that you are an admin of: the pool's global schema and the schema of each data connection, except data connections of managed packages such as Context Models and the Objects and events (OCDM) schema. A pool counts as on Storium when Storium is its primary or, during a migration, its secondary engine. The value is the technical schema name: ```json [{"value": "4b4f32c8-4529-43f1-9399-2a6de94ac13a_sap", "name": "Order Management Pool / SAP ECC"}] diff --git a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts index fe4c2032..66de7b2c 100644 --- a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts +++ b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts @@ -23,6 +23,8 @@ const connectionName = (connection: any): string => { const DATA_POOL_ADMIN_PERMISSION = "ADMIN"; const MANAGED_SCHEMA_SOURCE_TYPES = new Set(["pig", "context-model", "context-model-semantic"]); +const GLOBAL_SCHEMA_SOURCE_TYPE = "global"; +const OCDM_SCHEMA_SOURCE_TYPE = "ocdm"; const MAX_PARALLEL_DATA_SOURCE_REQUESTS = 4; const MAX_POOL_IDS_PER_REQUEST = 100; const STORIUM_POOL_PROVIDER_TYPE = "STORIUM"; @@ -31,7 +33,14 @@ const SCHEMA_POOL_PREFIX = /^([\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f const toPoolSchemas = (pool: any, dataSources: any[]): SchemaCandidate[] => { const sourceSchemas = new Map(); for (const dataSource of dataSources) { + const type = String(dataSource?.type ?? "").toLowerCase(); + if (type === GLOBAL_SCHEMA_SOURCE_TYPE || type === OCDM_SCHEMA_SOURCE_TYPE) { + continue; + } const schema = dataSource?.targetSchemaName || `${pool.id}_${dataSource?.id}`; + if (schema.toLowerCase() === pool.id.toLowerCase()) { + continue; + } const schemaPoolId = SCHEMA_POOL_PREFIX.exec(schema)?.[1]; if (schemaPoolId && schemaPoolId.toLowerCase() !== pool.id.toLowerCase()) { continue; @@ -41,7 +50,7 @@ const toPoolSchemas = (pool: any, dataSources: any[]): SchemaCandidate[] => { schema, name: existing?.name ?? (dataSource?.name || schema), poolName: pool.name, - managed: !!existing?.managed || MANAGED_SCHEMA_SOURCE_TYPES.has(String(dataSource?.type ?? "").toLowerCase()), + managed: !!existing?.managed || MANAGED_SCHEMA_SOURCE_TYPES.has(type), }); } const assignableSchemas = [...sourceSchemas.values()] diff --git a/tests/commands/configuration-management/list-assignments.spec.ts b/tests/commands/configuration-management/list-assignments.spec.ts index 4639378a..0841e1e9 100644 --- a/tests/commands/configuration-management/list-assignments.spec.ts +++ b/tests/commands/configuration-management/list-assignments.spec.ts @@ -168,6 +168,24 @@ describe("List assignments", () => { expect(getJsonFromDownloadedFile()).toEqual([{value: POOL_1, name: "Pool 1 / Global schema"}]); }) + it("Should list the global schema once and leave out OCDM from the virtual slim overview entries", async () => { + mockAxiosGet(DATA_POOLS_URL, [{id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}]); + mockAxiosGet(dataSourcesUrl(POOL_1), [ + {id: "sap", name: "SAP ECC", type: "sap", targetSchemaName: "sap_schema", ...ON_STORIUM}, + {id: null, name: "Global Data Connection", type: "global", targetSchemaName: POOL_1, ...ON_STORIUM}, + {id: null, name: "Objects and events", type: "OCDM", targetSchemaName: `${POOL_1}_ocdm`, ...ON_STORIUM}, + {id: null, name: "Objects and events", type: "OCDM", targetSchemaName: "business_graph_schema_tenant", ...ON_STORIUM}, + ]); + + await new VariableCommandService(testContext).listAssignments("SCHEMA_REFERENCE", true, ""); + + expect(mockedAxiosInstance.get).not.toHaveBeenCalledWith(poolProviderTypesUrl(POOL_1), expect.anything()); + expect(getJsonFromDownloadedFile()).toEqual([ + {value: POOL_1, name: "Pool 1 / Global schema"}, + {value: "sap_schema", name: "Pool 1 / SAP ECC"}, + ]); + }) + it("Should only offer schemas of pools on Storium, also while Storium is the secondary engine", async () => { mockAxiosGet(DATA_POOLS_URL, [ {id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}, From b2acc10b32971811bebb64c7b07296fa4d3af804 Mon Sep 17 00:00:00 2001 From: "b.mucolli" Date: Fri, 6 Nov 2026 14:29:44 +0100 Subject: [PATCH 05/10] PSL-3700: Match the Context Model schema picker for SCHEMA_REFERENCE Offers the pool's OCDM schema like the global schema, as the picker does since the slim overview returns it, and keeps the picker's creation rule that only schemas named after their pool are offered. Co-Authored-By: Claude Includes-AI-Code: true --- docs/user-guide/config-commands.md | 2 +- .../variable-assignment-apis.constants.ts | 10 ++---- .../list-assignments.spec.ts | 31 +++++++++++++------ 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/docs/user-guide/config-commands.md b/docs/user-guide/config-commands.md index c02ecc7b..0458cc60 100644 --- a/docs/user-guide/config-commands.md +++ b/docs/user-guide/config-commands.md @@ -414,7 +414,7 @@ Supported types are `DATA_MODEL`, `CONNECTION`, `DATA_POOL_ID`, `OBJECTS_EVENTS_ [{"value": "4b4f32c8-4529-43f1-9399-2a6de94ac13a", "name": "Order Management Pool"}] ``` -`SCHEMA_REFERENCE` lists the schemas of every data pool on Storium that you are an admin of: the pool's global schema and the schema of each data connection, except data connections of managed packages such as Context Models and the Objects and events (OCDM) schema. A pool counts as on Storium when Storium is its primary or, during a migration, its secondary engine. The value is the technical schema name: +`SCHEMA_REFERENCE` lists the schemas of every data pool on Storium that you are an admin of: the pool's global schema, its Objects and events (OCDM) schema and the schema of each data connection, except data connections of managed packages such as Context Models. Only schemas named after the pool (`` or `_...`) are listed. A pool counts as on Storium when Storium is its primary or, during a migration, its secondary engine. The value is the technical schema name: ```json [{"value": "4b4f32c8-4529-43f1-9399-2a6de94ac13a_sap", "name": "Order Management Pool / SAP ECC"}] diff --git a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts index 66de7b2c..153e2da2 100644 --- a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts +++ b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts @@ -24,25 +24,19 @@ const connectionName = (connection: any): string => { const DATA_POOL_ADMIN_PERMISSION = "ADMIN"; const MANAGED_SCHEMA_SOURCE_TYPES = new Set(["pig", "context-model", "context-model-semantic"]); const GLOBAL_SCHEMA_SOURCE_TYPE = "global"; -const OCDM_SCHEMA_SOURCE_TYPE = "ocdm"; const MAX_PARALLEL_DATA_SOURCE_REQUESTS = 4; const MAX_POOL_IDS_PER_REQUEST = 100; const STORIUM_POOL_PROVIDER_TYPE = "STORIUM"; -const SCHEMA_POOL_PREFIX = /^([\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})_/i; const toPoolSchemas = (pool: any, dataSources: any[]): SchemaCandidate[] => { const sourceSchemas = new Map(); for (const dataSource of dataSources) { const type = String(dataSource?.type ?? "").toLowerCase(); - if (type === GLOBAL_SCHEMA_SOURCE_TYPE || type === OCDM_SCHEMA_SOURCE_TYPE) { + if (type === GLOBAL_SCHEMA_SOURCE_TYPE) { continue; } const schema = dataSource?.targetSchemaName || `${pool.id}_${dataSource?.id}`; - if (schema.toLowerCase() === pool.id.toLowerCase()) { - continue; - } - const schemaPoolId = SCHEMA_POOL_PREFIX.exec(schema)?.[1]; - if (schemaPoolId && schemaPoolId.toLowerCase() !== pool.id.toLowerCase()) { + if (!schema.toLowerCase().startsWith(`${pool.id.toLowerCase()}_`)) { continue; } const existing = sourceSchemas.get(schema); diff --git a/tests/commands/configuration-management/list-assignments.spec.ts b/tests/commands/configuration-management/list-assignments.spec.ts index 0841e1e9..1d661d09 100644 --- a/tests/commands/configuration-management/list-assignments.spec.ts +++ b/tests/commands/configuration-management/list-assignments.spec.ts @@ -136,9 +136,10 @@ describe("List assignments", () => { {id: POOL_3, name: "Pool 3", permissions: ["ADMIN", "VIEW_DATA_POOL"]}, ]); mockAxiosGet(dataSourcesUrl(POOL_1), [ - {id: "sap", name: "SAP ECC", type: "sap", targetSchemaName: "sap_schema", ...ON_STORIUM}, + {id: "sap", name: "SAP ECC", type: "sap", targetSchemaName: `${POOL_1}_sap`, ...ON_STORIUM}, {id: "jdbc", name: "", type: "jdbc", ...ON_STORIUM}, - {id: "ccm", name: "My Context Model", type: "context-model", targetSchemaName: "ccm_schema", ...ON_STORIUM}, + {id: "custom", name: "Custom schema name", type: "jdbc", targetSchemaName: "sap_schema", ...ON_STORIUM}, + {id: "ccm", name: "My Context Model", type: "context-model", ...ON_STORIUM}, {id: "imported", name: "Imported", type: "imported", targetSchemaName: `${POOL_2}_other`, ...ON_STORIUM}, ]); mockAxiosGet(dataSourcesUrl(POOL_3), []); @@ -149,7 +150,7 @@ describe("List assignments", () => { expect(mockedAxiosInstance.get).not.toHaveBeenCalledWith(dataSourcesUrl(POOL_2), expect.anything()); expect(getJsonFromDownloadedFile()).toEqual([ {value: POOL_1, name: "Pool 1 / Global schema"}, - {value: "sap_schema", name: "Pool 1 / SAP ECC"}, + {value: `${POOL_1}_sap`, name: "Pool 1 / SAP ECC"}, {value: `${POOL_1}_jdbc`, name: `Pool 1 / ${POOL_1}_jdbc`}, {value: POOL_3, name: "Pool 3 / Global schema"}, ]); @@ -158,8 +159,8 @@ describe("List assignments", () => { it("Should not offer a schema that a managed data source shares", async () => { mockAxiosGet(DATA_POOLS_URL, [{id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}]); mockAxiosGet(dataSourcesUrl(POOL_1), [ - {id: "sap", name: "SAP ECC", type: "sap", targetSchemaName: "shared_schema", ...ON_STORIUM}, - {id: "pig", name: "PIG", type: "PIG", targetSchemaName: "shared_schema", ...ON_STORIUM}, + {id: "sap", name: "SAP ECC", type: "sap", targetSchemaName: `${POOL_1}_shared`, ...ON_STORIUM}, + {id: "pig", name: "PIG", type: "PIG", targetSchemaName: `${POOL_1}_shared`, ...ON_STORIUM}, {id: "ccm", name: "My Context Model", type: "context-model", ...ON_STORIUM}, ]); @@ -168,13 +169,12 @@ describe("List assignments", () => { expect(getJsonFromDownloadedFile()).toEqual([{value: POOL_1, name: "Pool 1 / Global schema"}]); }) - it("Should list the global schema once and leave out OCDM from the virtual slim overview entries", async () => { + it("Should list the global schema once and offer the pool's OCDM schema from the virtual slim overview entries", async () => { mockAxiosGet(DATA_POOLS_URL, [{id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}]); mockAxiosGet(dataSourcesUrl(POOL_1), [ - {id: "sap", name: "SAP ECC", type: "sap", targetSchemaName: "sap_schema", ...ON_STORIUM}, + {id: "sap", name: "SAP ECC", type: "sap", ...ON_STORIUM}, {id: null, name: "Global Data Connection", type: "global", targetSchemaName: POOL_1, ...ON_STORIUM}, {id: null, name: "Objects and events", type: "OCDM", targetSchemaName: `${POOL_1}_ocdm`, ...ON_STORIUM}, - {id: null, name: "Objects and events", type: "OCDM", targetSchemaName: "business_graph_schema_tenant", ...ON_STORIUM}, ]); await new VariableCommandService(testContext).listAssignments("SCHEMA_REFERENCE", true, ""); @@ -182,8 +182,21 @@ describe("List assignments", () => { expect(mockedAxiosInstance.get).not.toHaveBeenCalledWith(poolProviderTypesUrl(POOL_1), expect.anything()); expect(getJsonFromDownloadedFile()).toEqual([ {value: POOL_1, name: "Pool 1 / Global schema"}, - {value: "sap_schema", name: "Pool 1 / SAP ECC"}, + {value: `${POOL_1}_sap`, name: "Pool 1 / SAP ECC"}, + {value: `${POOL_1}_ocdm`, name: "Pool 1 / Objects and events"}, + ]); + }) + + it("Should not offer schemas whose name does not start with the pool id, such as the legacy OCDM schema", async () => { + mockAxiosGet(DATA_POOLS_URL, [{id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}]); + mockAxiosGet(dataSourcesUrl(POOL_1), [ + {id: "custom", name: "Custom schema name", type: "jdbc", targetSchemaName: "sap_schema", ...ON_STORIUM}, + {id: null, name: "Objects and events", type: "OCDM", targetSchemaName: "business_graph_schema_tenant", ...ON_STORIUM}, ]); + + await new VariableCommandService(testContext).listAssignments("SCHEMA_REFERENCE", true, ""); + + expect(getJsonFromDownloadedFile()).toEqual([{value: POOL_1, name: "Pool 1 / Global schema"}]); }) it("Should only offer schemas of pools on Storium, also while Storium is the secondary engine", async () => { From 2ce9f966552253a5127c872840b4c918cfc039a0 Mon Sep 17 00:00:00 2001 From: "b.mucolli" Date: Fri, 6 Nov 2026 14:56:38 +0100 Subject: [PATCH 06/10] PSL-3700: Copy the Context Model schema picker's schema list exactly Lists the virtual global entry next to the picker's own global schema, matches managed data source types and pool-named schemas the way the picker does, and sorts by pool name, then schema name. Co-Authored-By: Claude Includes-AI-Code: true --- docs/user-guide/config-commands.md | 2 +- .../interfaces/variable-assignment-apis.constants.ts | 12 ++++-------- .../list-assignments.spec.ts | 9 +++++---- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/docs/user-guide/config-commands.md b/docs/user-guide/config-commands.md index 0458cc60..a4e05341 100644 --- a/docs/user-guide/config-commands.md +++ b/docs/user-guide/config-commands.md @@ -414,7 +414,7 @@ Supported types are `DATA_MODEL`, `CONNECTION`, `DATA_POOL_ID`, `OBJECTS_EVENTS_ [{"value": "4b4f32c8-4529-43f1-9399-2a6de94ac13a", "name": "Order Management Pool"}] ``` -`SCHEMA_REFERENCE` lists the schemas of every data pool on Storium that you are an admin of: the pool's global schema, its Objects and events (OCDM) schema and the schema of each data connection, except data connections of managed packages such as Context Models. Only schemas named after the pool (`` or `_...`) are listed. A pool counts as on Storium when Storium is its primary or, during a migration, its secondary engine. The value is the technical schema name: +`SCHEMA_REFERENCE` lists the same schemas as the Context Model schema picker in Studio, for every data pool on Storium that you are an admin of: the pool's global schema and every schema the data source overview of the pool returns, except schemas of managed packages such as Context Models. Only schemas named after the pool (`` or `_...`) are listed. A pool counts as on Storium when Storium is its primary or, during a migration, its secondary engine. The value is the technical schema name: ```json [{"value": "4b4f32c8-4529-43f1-9399-2a6de94ac13a_sap", "name": "Order Management Pool / SAP ECC"}] diff --git a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts index 153e2da2..ffe038e2 100644 --- a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts +++ b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts @@ -23,7 +23,6 @@ const connectionName = (connection: any): string => { const DATA_POOL_ADMIN_PERMISSION = "ADMIN"; const MANAGED_SCHEMA_SOURCE_TYPES = new Set(["pig", "context-model", "context-model-semantic"]); -const GLOBAL_SCHEMA_SOURCE_TYPE = "global"; const MAX_PARALLEL_DATA_SOURCE_REQUESTS = 4; const MAX_POOL_IDS_PER_REQUEST = 100; const STORIUM_POOL_PROVIDER_TYPE = "STORIUM"; @@ -31,12 +30,8 @@ const STORIUM_POOL_PROVIDER_TYPE = "STORIUM"; const toPoolSchemas = (pool: any, dataSources: any[]): SchemaCandidate[] => { const sourceSchemas = new Map(); for (const dataSource of dataSources) { - const type = String(dataSource?.type ?? "").toLowerCase(); - if (type === GLOBAL_SCHEMA_SOURCE_TYPE) { - continue; - } const schema = dataSource?.targetSchemaName || `${pool.id}_${dataSource?.id}`; - if (!schema.toLowerCase().startsWith(`${pool.id.toLowerCase()}_`)) { + if (schema !== pool.id && !schema.startsWith(`${pool.id}_`)) { continue; } const existing = sourceSchemas.get(schema); @@ -44,7 +39,7 @@ const toPoolSchemas = (pool: any, dataSources: any[]): SchemaCandidate[] => { schema, name: existing?.name ?? (dataSource?.name || schema), poolName: pool.name, - managed: !!existing?.managed || MANAGED_SCHEMA_SOURCE_TYPES.has(type), + managed: !!existing?.managed || MANAGED_SCHEMA_SOURCE_TYPES.has(dataSource?.type), }); } const assignableSchemas = [...sourceSchemas.values()] @@ -103,7 +98,8 @@ const loadSchemaCandidates = async (get: (url: string) => Promise, poolsUrl return loadedPools .filter(({pool}) => isStoriumBacked(typesByPool.get(pool.id))) - .flatMap(({pool, dataSources}) => toPoolSchemas(pool, dataSources)); + .flatMap(({pool, dataSources}) => toPoolSchemas(pool, dataSources)) + .sort((a, b) => (a.poolName ?? "").localeCompare(b.poolName ?? "") || a.name.localeCompare(b.name)); }; export const variableAssignmentApis: { [key: string]: VariableAssignmentApi } = { diff --git a/tests/commands/configuration-management/list-assignments.spec.ts b/tests/commands/configuration-management/list-assignments.spec.ts index 1d661d09..612198f6 100644 --- a/tests/commands/configuration-management/list-assignments.spec.ts +++ b/tests/commands/configuration-management/list-assignments.spec.ts @@ -149,9 +149,9 @@ describe("List assignments", () => { expect(mockedAxiosInstance.get).not.toHaveBeenCalledWith(dataSourcesUrl(POOL_2), expect.anything()); expect(getJsonFromDownloadedFile()).toEqual([ + {value: `${POOL_1}_jdbc`, name: `Pool 1 / ${POOL_1}_jdbc`}, {value: POOL_1, name: "Pool 1 / Global schema"}, {value: `${POOL_1}_sap`, name: "Pool 1 / SAP ECC"}, - {value: `${POOL_1}_jdbc`, name: `Pool 1 / ${POOL_1}_jdbc`}, {value: POOL_3, name: "Pool 3 / Global schema"}, ]); }) @@ -160,7 +160,7 @@ describe("List assignments", () => { mockAxiosGet(DATA_POOLS_URL, [{id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}]); mockAxiosGet(dataSourcesUrl(POOL_1), [ {id: "sap", name: "SAP ECC", type: "sap", targetSchemaName: `${POOL_1}_shared`, ...ON_STORIUM}, - {id: "pig", name: "PIG", type: "PIG", targetSchemaName: `${POOL_1}_shared`, ...ON_STORIUM}, + {id: "pig", name: "PIG", type: "pig", targetSchemaName: `${POOL_1}_shared`, ...ON_STORIUM}, {id: "ccm", name: "My Context Model", type: "context-model", ...ON_STORIUM}, ]); @@ -169,7 +169,7 @@ describe("List assignments", () => { expect(getJsonFromDownloadedFile()).toEqual([{value: POOL_1, name: "Pool 1 / Global schema"}]); }) - it("Should list the global schema once and offer the pool's OCDM schema from the virtual slim overview entries", async () => { + it("Should offer the virtual global and OCDM entries of the slim overview like the schema picker", async () => { mockAxiosGet(DATA_POOLS_URL, [{id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}]); mockAxiosGet(dataSourcesUrl(POOL_1), [ {id: "sap", name: "SAP ECC", type: "sap", ...ON_STORIUM}, @@ -181,9 +181,10 @@ describe("List assignments", () => { expect(mockedAxiosInstance.get).not.toHaveBeenCalledWith(poolProviderTypesUrl(POOL_1), expect.anything()); expect(getJsonFromDownloadedFile()).toEqual([ + {value: POOL_1, name: "Pool 1 / Global Data Connection"}, {value: POOL_1, name: "Pool 1 / Global schema"}, - {value: `${POOL_1}_sap`, name: "Pool 1 / SAP ECC"}, {value: `${POOL_1}_ocdm`, name: "Pool 1 / Objects and events"}, + {value: `${POOL_1}_sap`, name: "Pool 1 / SAP ECC"}, ]); }) From ed1bcbcff1dacdb6be2eaf3d8b5138dfca54e9c6 Mon Sep 17 00:00:00 2001 From: "b.mucolli" Date: Fri, 6 Nov 2026 15:21:00 +0100 Subject: [PATCH 07/10] PSL-3700: Read the pool engine from the slim overview only Drops the pool-provider-types fallback. A pool is left out only when its slim overview entries report a pool provider other than Storium; pools without reported pool provider types are kept. Co-Authored-By: Claude Includes-AI-Code: true --- docs/user-guide/config-commands.md | 2 +- .../variable-assignment-apis.constants.ts | 30 +++---------------- .../list-assignments.spec.ts | 29 +++++------------- 3 files changed, 13 insertions(+), 48 deletions(-) diff --git a/docs/user-guide/config-commands.md b/docs/user-guide/config-commands.md index a4e05341..e39f4bba 100644 --- a/docs/user-guide/config-commands.md +++ b/docs/user-guide/config-commands.md @@ -414,7 +414,7 @@ Supported types are `DATA_MODEL`, `CONNECTION`, `DATA_POOL_ID`, `OBJECTS_EVENTS_ [{"value": "4b4f32c8-4529-43f1-9399-2a6de94ac13a", "name": "Order Management Pool"}] ``` -`SCHEMA_REFERENCE` lists the same schemas as the Context Model schema picker in Studio, for every data pool on Storium that you are an admin of: the pool's global schema and every schema the data source overview of the pool returns, except schemas of managed packages such as Context Models. Only schemas named after the pool (`` or `_...`) are listed. A pool counts as on Storium when Storium is its primary or, during a migration, its secondary engine. The value is the technical schema name: +`SCHEMA_REFERENCE` lists the schemas of every data pool you are an admin of: the pool's global schema and every schema the data source overview of the pool returns, except schemas of managed packages such as Context Models. Only schemas named after the pool (`` or `_...`) are listed. Pools whose data source overview reports an engine other than Storium, as primary or secondary engine, are left out. The value is the technical schema name: ```json [{"value": "4b4f32c8-4529-43f1-9399-2a6de94ac13a_sap", "name": "Order Management Pool / SAP ECC"}] diff --git a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts index ffe038e2..900da82a 100644 --- a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts +++ b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts @@ -24,7 +24,6 @@ const connectionName = (connection: any): string => { const DATA_POOL_ADMIN_PERMISSION = "ADMIN"; const MANAGED_SCHEMA_SOURCE_TYPES = new Set(["pig", "context-model", "context-model-semantic"]); const MAX_PARALLEL_DATA_SOURCE_REQUESTS = 4; -const MAX_POOL_IDS_PER_REQUEST = 100; const STORIUM_POOL_PROVIDER_TYPE = "STORIUM"; const toPoolSchemas = (pool: any, dataSources: any[]): SchemaCandidate[] => { @@ -61,19 +60,9 @@ const mapWithConcurrency = async (items: T[], limit: number, mapper: (item return results; }; -const isStoriumBacked = (types: any): boolean => - [types?.primaryPoolProviderType, types?.secondaryPoolProviderType].includes(STORIUM_POOL_PROVIDER_TYPE); - -const loadMissingPoolProviderTypes = async (get: (url: string) => Promise, poolIds: string[]): Promise => { - const batches: string[][] = []; - for (let index = 0; index < poolIds.length; index += MAX_POOL_IDS_PER_REQUEST) { - batches.push(poolIds.slice(index, index + MAX_POOL_IDS_PER_REQUEST)); - } - const typesPerBatch = await Promise.all(batches.map(batch => { - const query = batch.map(poolId => `poolIds=${encodeURIComponent(poolId)}`).join("&"); - return get(`/integration/api/pools/pool-provider-types?${query}`); - })); - return typesPerBatch.flatMap(types => Array.isArray(types) ? types : []); +const isOfferedEngine = (dataSources: any[]): boolean => { + const typed = dataSources.find(dataSource => dataSource?.primaryPoolProviderType || dataSource?.secondaryPoolProviderType); + return !typed || [typed.primaryPoolProviderType, typed.secondaryPoolProviderType].includes(STORIUM_POOL_PROVIDER_TYPE); }; const loadSchemaCandidates = async (get: (url: string) => Promise, poolsUrl: string): Promise => { @@ -85,19 +74,8 @@ const loadSchemaCandidates = async (get: (url: string) => Promise, poolsUrl return {pool, dataSources: Array.isArray(dataSources) ? dataSources : []}; }); - const typesByPool = new Map(); - for (const {pool, dataSources} of loadedPools) { - const typed = dataSources.find(dataSource => dataSource?.primaryPoolProviderType); - if (typed) { - typesByPool.set(pool.id, typed); - } - } - const untypedPoolIds = loadedPools.map(({pool}) => pool.id).filter(poolId => !typesByPool.has(poolId)); - const loadedTypes = untypedPoolIds.length ? await loadMissingPoolProviderTypes(get, untypedPoolIds) : []; - loadedTypes.forEach(types => typesByPool.set(types?.poolId, types)); - return loadedPools - .filter(({pool}) => isStoriumBacked(typesByPool.get(pool.id))) + .filter(({dataSources}) => isOfferedEngine(dataSources)) .flatMap(({pool, dataSources}) => toPoolSchemas(pool, dataSources)) .sort((a, b) => (a.poolName ?? "").localeCompare(b.poolName ?? "") || a.name.localeCompare(b.name)); }; diff --git a/tests/commands/configuration-management/list-assignments.spec.ts b/tests/commands/configuration-management/list-assignments.spec.ts index 612198f6..56f971ba 100644 --- a/tests/commands/configuration-management/list-assignments.spec.ts +++ b/tests/commands/configuration-management/list-assignments.spec.ts @@ -16,8 +16,6 @@ describe("List assignments", () => { const POOL_3 = "33333333-3333-3333-3333-333333333333"; const dataSourcesUrl = (poolId: string) => `https://myTeam.celonis.cloud/integration/api/pools/${poolId}/data-sources/slim-overview`; - const poolProviderTypesUrl = (...poolIds: string[]) => - `https://myTeam.celonis.cloud/integration/api/pools/pool-provider-types?${poolIds.map(id => `poolIds=${id}`).join("&")}`; const ON_STORIUM = {primaryPoolProviderType: "STORIUM"}; const ON_LEGACY_ENGINE = {primaryPoolProviderType: "HDFS"}; @@ -143,7 +141,6 @@ describe("List assignments", () => { {id: "imported", name: "Imported", type: "imported", targetSchemaName: `${POOL_2}_other`, ...ON_STORIUM}, ]); mockAxiosGet(dataSourcesUrl(POOL_3), []); - mockAxiosGet(poolProviderTypesUrl(POOL_3), [{poolId: POOL_3, ...ON_STORIUM}]); await new VariableCommandService(testContext).listAssignments("SCHEMA_REFERENCE", true, ""); @@ -179,7 +176,6 @@ describe("List assignments", () => { await new VariableCommandService(testContext).listAssignments("SCHEMA_REFERENCE", true, ""); - expect(mockedAxiosInstance.get).not.toHaveBeenCalledWith(poolProviderTypesUrl(POOL_1), expect.anything()); expect(getJsonFromDownloadedFile()).toEqual([ {value: POOL_1, name: "Pool 1 / Global Data Connection"}, {value: POOL_1, name: "Pool 1 / Global schema"}, @@ -218,31 +214,22 @@ describe("List assignments", () => { ]); }) - it("Should resolve the engine of pools without data sources in one request", async () => { + it("Should offer the schemas of pools whose engine is not reported, without requesting it", async () => { mockAxiosGet(DATA_POOLS_URL, [ {id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}, {id: POOL_3, name: "Pool 3", permissions: ["ADMIN"]}, ]); - mockAxiosGet(dataSourcesUrl(POOL_1), []); + mockAxiosGet(dataSourcesUrl(POOL_1), [{id: "sap", name: "SAP ECC", type: "sap"}]); mockAxiosGet(dataSourcesUrl(POOL_3), []); - mockAxiosGet(poolProviderTypesUrl(POOL_1, POOL_3), [ - {poolId: POOL_1, ...ON_STORIUM}, - {poolId: POOL_3, ...ON_LEGACY_ENGINE}, - ]); await new VariableCommandService(testContext).listAssignments("SCHEMA_REFERENCE", true, ""); - expect(mockedAxiosInstance.get).toHaveBeenCalledWith(poolProviderTypesUrl(POOL_1, POOL_3), expect.anything()); - expect(getJsonFromDownloadedFile()).toEqual([{value: POOL_1, name: "Pool 1 / Global schema"}]); - }) - - it("Should fail when the engine of a pool cannot be loaded", async () => { - mockAxiosGet(DATA_POOLS_URL, [{id: POOL_1, name: "Pool 1", permissions: ["ADMIN"]}]); - mockAxiosGet(dataSourcesUrl(POOL_1), []); - mockAxiosGetError(poolProviderTypesUrl(POOL_1), 404, "Not Found"); - - await expect(new VariableCommandService(testContext).listAssignments("SCHEMA_REFERENCE", true, "")) - .rejects.toThrow("Problem getting variables assignment values for type SCHEMA_REFERENCE"); + expect(mockedAxiosInstance.get).not.toHaveBeenCalledWith(expect.stringContaining("pool-provider-types"), expect.anything()); + expect(getJsonFromDownloadedFile()).toEqual([ + {value: POOL_1, name: "Pool 1 / Global schema"}, + {value: `${POOL_1}_sap`, name: "Pool 1 / SAP ECC"}, + {value: POOL_3, name: "Pool 3 / Global schema"}, + ]); }) it("Should fail when the data sources of a pool cannot be loaded", async () => { From 1f991f2f1e832dcf7d39de42b23b80b317e2e986 Mon Sep 17 00:00:00 2001 From: "b.mucolli" Date: Fri, 6 Nov 2026 15:50:32 +0100 Subject: [PATCH 08/10] PSL-3700: Load the data sources of pools without awaiting in a loop Each worker takes the next pool and calls itself again, so the slim overview requests still run 4 at a time without an await inside a loop. Co-Authored-By: Claude Includes-AI-Code: true --- .../interfaces/variable-assignment-apis.constants.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts index 900da82a..b04664e7 100644 --- a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts +++ b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts @@ -51,10 +51,12 @@ const mapWithConcurrency = async (items: T[], limit: number, mapper: (item const results: R[] = new Array(items.length); let next = 0; const worker = async (): Promise => { - while (next < items.length) { - const index = next++; - results[index] = await mapper(items[index]); + if (next >= items.length) { + return; } + const index = next++; + results[index] = await mapper(items[index]); + return worker(); }; await Promise.all(Array.from({length: Math.min(limit, items.length)}, worker)); return results; From d37da8503459098e9ecae1af9decb1882a1cfe94 Mon Sep 17 00:00:00 2001 From: "b.mucolli" Date: Fri, 6 Nov 2026 16:32:35 +0100 Subject: [PATCH 09/10] PSL-3700: Shorten the SCHEMA_REFERENCE docs Co-Authored-By: Claude Includes-AI-Code: true --- docs/user-guide/config-commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user-guide/config-commands.md b/docs/user-guide/config-commands.md index e39f4bba..a9fc44b0 100644 --- a/docs/user-guide/config-commands.md +++ b/docs/user-guide/config-commands.md @@ -414,7 +414,7 @@ Supported types are `DATA_MODEL`, `CONNECTION`, `DATA_POOL_ID`, `OBJECTS_EVENTS_ [{"value": "4b4f32c8-4529-43f1-9399-2a6de94ac13a", "name": "Order Management Pool"}] ``` -`SCHEMA_REFERENCE` lists the schemas of every data pool you are an admin of: the pool's global schema and every schema the data source overview of the pool returns, except schemas of managed packages such as Context Models. Only schemas named after the pool (`` or `_...`) are listed. Pools whose data source overview reports an engine other than Storium, as primary or secondary engine, are left out. The value is the technical schema name: +`SCHEMA_REFERENCE` lists the schemas of every data pool you are an admin of: ```json [{"value": "4b4f32c8-4529-43f1-9399-2a6de94ac13a_sap", "name": "Order Management Pool / SAP ECC"}] From d3bc81c9a15ffae297e5dc5cabd77842b612b62b Mon Sep 17 00:00:00 2001 From: "b.mucolli" Date: Fri, 6 Nov 2026 16:46:24 +0100 Subject: [PATCH 10/10] PSL-3700: Move the SCHEMA_REFERENCE logic into its own file Keeps the schema listing for SCHEMA_REFERENCE in api/schema-reference-candidates.ts, so it can be replaced on its own later, and simplifies its docs to every data pool of the team. Co-Authored-By: Claude Includes-AI-Code: true --- docs/user-guide/config-commands.md | 2 +- .../api/schema-reference-candidates.ts | 73 ++++++++++++++++++ .../variable-assignment-apis.constants.ts | 74 +------------------ 3 files changed, 77 insertions(+), 72 deletions(-) create mode 100644 src/commands/configuration-management/api/schema-reference-candidates.ts diff --git a/docs/user-guide/config-commands.md b/docs/user-guide/config-commands.md index a9fc44b0..a1f82912 100644 --- a/docs/user-guide/config-commands.md +++ b/docs/user-guide/config-commands.md @@ -414,7 +414,7 @@ Supported types are `DATA_MODEL`, `CONNECTION`, `DATA_POOL_ID`, `OBJECTS_EVENTS_ [{"value": "4b4f32c8-4529-43f1-9399-2a6de94ac13a", "name": "Order Management Pool"}] ``` -`SCHEMA_REFERENCE` lists the schemas of every data pool you are an admin of: +`SCHEMA_REFERENCE` lists the schemas of every data pool of the team: ```json [{"value": "4b4f32c8-4529-43f1-9399-2a6de94ac13a_sap", "name": "Order Management Pool / SAP ECC"}] diff --git a/src/commands/configuration-management/api/schema-reference-candidates.ts b/src/commands/configuration-management/api/schema-reference-candidates.ts new file mode 100644 index 00000000..3f562843 --- /dev/null +++ b/src/commands/configuration-management/api/schema-reference-candidates.ts @@ -0,0 +1,73 @@ +import { VariableAssignment } from "../interfaces/variable-assignment-apis.constants"; + +interface SchemaCandidate { + schema: string; + name: string; + poolName?: string; +} + +const DATA_POOL_ADMIN_PERMISSION = "ADMIN"; +const MANAGED_SCHEMA_SOURCE_TYPES = new Set(["pig", "context-model", "context-model-semantic"]); +const MAX_PARALLEL_DATA_SOURCE_REQUESTS = 4; +const STORIUM_POOL_PROVIDER_TYPE = "STORIUM"; + +const toPoolSchemas = (pool: any, dataSources: any[]): SchemaCandidate[] => { + const sourceSchemas = new Map(); + for (const dataSource of dataSources) { + const schema = dataSource?.targetSchemaName || `${pool.id}_${dataSource?.id}`; + if (schema !== pool.id && !schema.startsWith(`${pool.id}_`)) { + continue; + } + const existing = sourceSchemas.get(schema); + sourceSchemas.set(schema, { + schema, + name: existing?.name ?? (dataSource?.name || schema), + poolName: pool.name, + managed: !!existing?.managed || MANAGED_SCHEMA_SOURCE_TYPES.has(dataSource?.type), + }); + } + const assignableSchemas = [...sourceSchemas.values()] + .filter(candidate => !candidate.managed) + .map(({schema, name, poolName}) => ({schema, name, poolName})); + return [{schema: pool.id, name: "Global schema", poolName: pool.name}, ...assignableSchemas]; +}; + +const mapWithConcurrency = async (items: T[], limit: number, mapper: (item: T) => Promise): Promise => { + const results: R[] = new Array(items.length); + let next = 0; + const worker = async (): Promise => { + if (next >= items.length) { + return; + } + const index = next++; + results[index] = await mapper(items[index]); + return worker(); + }; + await Promise.all(Array.from({length: Math.min(limit, items.length)}, worker)); + return results; +}; + +const isOfferedEngine = (dataSources: any[]): boolean => { + const typed = dataSources.find(dataSource => dataSource?.primaryPoolProviderType || dataSource?.secondaryPoolProviderType); + return !typed || [typed.primaryPoolProviderType, typed.secondaryPoolProviderType].includes(STORIUM_POOL_PROVIDER_TYPE); +}; + +export const loadSchemaCandidates = async (get: (url: string) => Promise, poolsUrl: string): Promise => { + const pools = await get(poolsUrl); + const adminPools = (Array.isArray(pools) ? pools : []) + .filter(pool => !!pool?.id && (pool.permissions ?? []).includes(DATA_POOL_ADMIN_PERMISSION)); + const loadedPools = await mapWithConcurrency(adminPools, MAX_PARALLEL_DATA_SOURCE_REQUESTS, async pool => { + const dataSources = await get(`/integration/api/pools/${encodeURIComponent(pool.id)}/data-sources/slim-overview`); + return {pool, dataSources: Array.isArray(dataSources) ? dataSources : []}; + }); + + return loadedPools + .filter(({dataSources}) => isOfferedEngine(dataSources)) + .flatMap(({pool, dataSources}) => toPoolSchemas(pool, dataSources)) + .sort((a, b) => (a.poolName ?? "").localeCompare(b.poolName ?? "") || a.name.localeCompare(b.name)); +}; + +export const toSchemaAssignment = (candidate: SchemaCandidate): VariableAssignment => ({ + value: candidate?.schema, + name: [candidate?.poolName, candidate?.name].filter(Boolean).join(" / ") || candidate?.schema, +}); diff --git a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts index b04664e7..c1946e72 100644 --- a/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts +++ b/src/commands/configuration-management/interfaces/variable-assignment-apis.constants.ts @@ -1,3 +1,5 @@ +import { loadSchemaCandidates, toSchemaAssignment } from "../api/schema-reference-candidates"; + export interface VariableAssignment { value: string; name: string; @@ -10,78 +12,11 @@ export interface VariableAssignmentApi { loadCandidates?: (get: (url: string) => Promise, url: string) => Promise; } -interface SchemaCandidate { - schema: string; - name: string; - poolName?: string; -} - const connectionName = (connection: any): string => { const parts = [connection?.authProvider?.label ?? connection?.authProvider?.name, connection?.username].filter(Boolean); return parts.length ? parts.join(" - ") : connection?.id; }; -const DATA_POOL_ADMIN_PERMISSION = "ADMIN"; -const MANAGED_SCHEMA_SOURCE_TYPES = new Set(["pig", "context-model", "context-model-semantic"]); -const MAX_PARALLEL_DATA_SOURCE_REQUESTS = 4; -const STORIUM_POOL_PROVIDER_TYPE = "STORIUM"; - -const toPoolSchemas = (pool: any, dataSources: any[]): SchemaCandidate[] => { - const sourceSchemas = new Map(); - for (const dataSource of dataSources) { - const schema = dataSource?.targetSchemaName || `${pool.id}_${dataSource?.id}`; - if (schema !== pool.id && !schema.startsWith(`${pool.id}_`)) { - continue; - } - const existing = sourceSchemas.get(schema); - sourceSchemas.set(schema, { - schema, - name: existing?.name ?? (dataSource?.name || schema), - poolName: pool.name, - managed: !!existing?.managed || MANAGED_SCHEMA_SOURCE_TYPES.has(dataSource?.type), - }); - } - const assignableSchemas = [...sourceSchemas.values()] - .filter(candidate => !candidate.managed) - .map(({schema, name, poolName}) => ({schema, name, poolName})); - return [{schema: pool.id, name: "Global schema", poolName: pool.name}, ...assignableSchemas]; -}; - -const mapWithConcurrency = async (items: T[], limit: number, mapper: (item: T) => Promise): Promise => { - const results: R[] = new Array(items.length); - let next = 0; - const worker = async (): Promise => { - if (next >= items.length) { - return; - } - const index = next++; - results[index] = await mapper(items[index]); - return worker(); - }; - await Promise.all(Array.from({length: Math.min(limit, items.length)}, worker)); - return results; -}; - -const isOfferedEngine = (dataSources: any[]): boolean => { - const typed = dataSources.find(dataSource => dataSource?.primaryPoolProviderType || dataSource?.secondaryPoolProviderType); - return !typed || [typed.primaryPoolProviderType, typed.secondaryPoolProviderType].includes(STORIUM_POOL_PROVIDER_TYPE); -}; - -const loadSchemaCandidates = async (get: (url: string) => Promise, poolsUrl: string): Promise => { - const pools = await get(poolsUrl); - const adminPools = (Array.isArray(pools) ? pools : []) - .filter(pool => !!pool?.id && (pool.permissions ?? []).includes(DATA_POOL_ADMIN_PERMISSION)); - const loadedPools = await mapWithConcurrency(adminPools, MAX_PARALLEL_DATA_SOURCE_REQUESTS, async pool => { - const dataSources = await get(`/integration/api/pools/${encodeURIComponent(pool.id)}/data-sources/slim-overview`); - return {pool, dataSources: Array.isArray(dataSources) ? dataSources : []}; - }); - - return loadedPools - .filter(({dataSources}) => isOfferedEngine(dataSources)) - .flatMap(({pool, dataSources}) => toPoolSchemas(pool, dataSources)) - .sort((a, b) => (a.poolName ?? "").localeCompare(b.poolName ?? "") || a.name.localeCompare(b.name)); -}; - export const variableAssignmentApis: { [key: string]: VariableAssignmentApi } = { DATA_MODEL: { url: "/package-manager/api/compute-pools/pools-with-data-models", @@ -115,9 +50,6 @@ export const variableAssignmentApis: { [key: string]: VariableAssignmentApi } = SCHEMA_REFERENCE: { url: "/integration/api/pools", loadCandidates: loadSchemaCandidates, - toAssignment: (candidate: SchemaCandidate) => ({ - value: candidate?.schema, - name: [candidate?.poolName, candidate?.name].filter(Boolean).join(" / ") || candidate?.schema, - }), + toAssignment: toSchemaAssignment, }, };