Skip to content

Commit fbef9ca

Browse files
committed
Add runtime overrides and JSDoc for discovered API quirks
Discovered via live-org validation from a downstream consumer. The spec is technically correct but silent about server-side constraints and edge cases; these overrides/JSDoc capture what the API actually requires. Behavior changes: - data-model-objects.listMappings: soft-lands the "Object Source Target Map not found" 404 as an empty collection. Unrelated 404s and other errors still throw. - data-graphs: adds list() / listAll() convenience wrappers over getMetadata (the Connect API has no dedicated list endpoint). Documentation / JSDoc: - connections.list: documents connectorType casing caveats (CamelCase vs UPPERCASE varies by connector family). - connections.listSchema: documents that only a subset of connector types is supported; AwsS3/Databricks/SalesforceDotCom reject with ILLEGAL_QUERY_PARAMETER_VALUE. - connectors: class-level JSDoc explains the casing variance and the connectorInfoList collection key. - data-kits.list / listAvailableComponents: notes known tenant-side timeouts and 500s with Metadata API fallback recommendation. - data-model-objects: class-level note on the singular dataModelObject response key. - identity-resolutions: class-level JSDoc clarifying that get/delete/ patch/runNow require the id, not developerName. Tests: 3 new cases locking listMappings 404 soft-landing behavior, 3 new cases for DataGraphs list()/listAll(); fixed pre-existing createMappings and getData call shapes. 215 tests passing (+6), typecheck clean, build clean.
1 parent c97dc9f commit fbef9ca

9 files changed

Lines changed: 331 additions & 10 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ if (queryId) {
258258
| Data Action Targets | `client.dataActionTargets` | list, listAll, get, create, delete, getSigningKey, createSigningKey, resetSigningKey |
259259
| Data Actions | `client.dataActions` | list, listAll, create |
260260
| Data Clean Room | `client.dataCleanRoom` | listCollaborations, listAllCollaborations, createCollaborations, acceptInvitation, rejectInvitation, run, listCollaborationsJobs, listAllCollaborationsJobs, listProviders, listAllProviders, createProviders, getProviders, listProvidersTemplates, listAllProvidersTemplates, listSpecifications, listAllSpecifications, createSpecifications, listTemplates, listAllTemplates |
261-
| Data Graphs | `client.dataGraphs` | get, create, delete, refresh, getData, getDataByGet, getMetadata |
261+
| Data Graphs | `client.dataGraphs` | list, listAll, get, create, delete, refresh, getData, getDataByGet, getMetadata |
262262
| Data Kits | `client.dataKits` | list, create, delete, patch, createByPost, listDependencies, getDeploymentStatus, createUndeploy, listAvailableComponents, getDataKitManifest |
263263
| Data Lake Objects | `client.dataLakeObjects` | list, listAll, get, create, delete, patch |
264264
| Data Model Objects | `client.dataModelObjects` | list, listAll, get, create, delete, patch, listRelationships, listAllRelationships, createRelationships, deleteRelationships, listMappings, listAllMappings, getMappings, createMappings, deleteMappings, deleteMappingsFieldMappings, patchMappingsFieldMappings |

src/resources/connections.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,38 @@
11
import { ConnectionsServiceBase } from "../generated/services/connections.base.js";
2-
import type { RequestOptions } from "../core/types.js";
2+
import type { ConnectionsListParams } from "../generated/services/connections.base.js";
3+
import type { PaginationParams, RequestOptions } from "../core/types.js";
34
import type {
5+
ConnectionCollectionRepresentation,
46
ConnectionCreateInput,
57
ConnectionInputRepresentation,
68
ConnectionRepresentation,
9+
ConnectionSchemaCollectionRepresentation,
710
ConnectionUpdateInput,
811
ConnectionPatchInputRepresentation,
912
} from "../schemas.js";
1013

1114
export class ConnectionsService extends ConnectionsServiceBase {
15+
/**
16+
* GET /ssot/connections — Get connections.
17+
*
18+
* The `connectorType` query parameter is required and must match exactly
19+
* one of the connector `name` values returned by `connectors.list`. The
20+
* casing is backend-defined per connector family: some types accept
21+
* CamelCase (`IngestApi`, `SalesforceDotCom`, `AwsS3`, `AzureBlob`,
22+
* `Databricks`), others only accept UPPERCASE (`SNOWFLAKE`, `BIGQUERY`,
23+
* `GCS`, `SFTP`). Passing a mismatched token returns
24+
* `400 ILLEGAL_QUERY_PARAMETER_VALUE` with a message like
25+
* `ConnectorType [Snowflake] is not supported`. If you do not know the
26+
* exact token for a target connector, enumerate it via `connectors.list`
27+
* first.
28+
*/
29+
override async list(
30+
params: PaginationParams & ConnectionsListParams,
31+
options?: RequestOptions,
32+
): Promise<ConnectionCollectionRepresentation> {
33+
return super.list(params, options);
34+
}
35+
1236
/** Override create with discriminated union input type. */
1337
override async create(body: ConnectionCreateInput | ConnectionInputRepresentation, options?: RequestOptions): Promise<ConnectionRepresentation> {
1438
return this.httpClient.post(this.basePath, body, options);
@@ -23,4 +47,25 @@ export class ConnectionsService extends ConnectionsServiceBase {
2347
async update(connectionId: string, body: ConnectionUpdateInput | ConnectionPatchInputRepresentation, options?: RequestOptions): Promise<ConnectionRepresentation> {
2448
return this.httpClient.patch(`${this.basePath}/${encodeURIComponent(connectionId)}`, body, options);
2549
}
50+
51+
/**
52+
* GET /ssot/connections/{connectionId}/schema — Get connection schema.
53+
*
54+
* Only a subset of connector types is supported by this endpoint. In
55+
* practice the API accepts IngestApi connections and rejects AwsS3,
56+
* Databricks, and SalesforceDotCom connections with
57+
* `400 ILLEGAL_QUERY_PARAMETER_VALUE`
58+
* (`No enum constant ConnectionSchemaTypeEnum.<type>`).
59+
*
60+
* The SDK surface exposes the method uniformly; callers that cannot predict
61+
* the connector type of a given connection ID should wrap this call and
62+
* fall back to `getEndpoints` or parse the schema from the connection
63+
* payload returned by `get(connectionId)`.
64+
*/
65+
override async listSchema(
66+
connectionId: string,
67+
options?: RequestOptions,
68+
): Promise<ConnectionSchemaCollectionRepresentation> {
69+
return super.listSchema(connectionId, options);
70+
}
2671
}

src/resources/connectors.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,26 @@
1-
export { ConnectorsServiceBase as ConnectorsService } from "../generated/services/connectors.base.js";
1+
import { ConnectorsServiceBase } from "../generated/services/connectors.base.js";
2+
import type { PaginationParams, RequestOptions } from "../core/types.js";
3+
import type { ConnectorsListParams } from "../generated/services/connectors.base.js";
4+
import type { ConnectorInfoCollectionRepresentation } from "../schemas.js";
5+
6+
/**
7+
* Connector catalog — the list of connector types available in the org.
8+
*
9+
* Use this to discover the exact `name` value to pass as `connectorType` to
10+
* `connections.list`. The accepted token is the catalog `name`, which can be
11+
* either CamelCase (`IngestApi`, `SalesforceDotCom`, `AwsS3`, `AzureBlob`,
12+
* `Databricks`) or UPPERCASE (`SNOWFLAKE`, `BIGQUERY`, `GCS`, `SFTP`),
13+
* depending on the connector family — there is no blanket casing rule.
14+
* Treat the connector type string as an API-supplied enum and probe via
15+
* this catalog when in doubt.
16+
*
17+
* Collection response key: `connectorInfoList`.
18+
*/
19+
export class ConnectorsService extends ConnectorsServiceBase {
20+
override async list(
21+
params?: PaginationParams & ConnectorsListParams,
22+
options?: RequestOptions,
23+
): Promise<ConnectorInfoCollectionRepresentation> {
24+
return super.list(params, options);
25+
}
26+
}

src/resources/data-graphs.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,34 @@
1-
export { DataGraphsServiceBase as DataGraphsService } from "../generated/services/data-graphs.base.js";
1+
import { DataGraphsServiceBase } from "../generated/services/data-graphs.base.js";
2+
import type { DataGraphsGetMetadataParams } from "../generated/services/data-graphs.base.js";
3+
import type { RequestOptions } from "../core/types.js";
4+
import type { CdpQueryDataGraphMetadataRepresentation } from "../schemas.js";
5+
6+
export class DataGraphsService extends DataGraphsServiceBase {
7+
/**
8+
* Convenience wrapper over `getMetadata` — the Data 360 Connect API does not
9+
* expose a dedicated `GET /ssot/data-graphs` endpoint, so enumeration goes
10+
* through `GET /ssot/data-graphs/metadata` (`getMetadata`). This helper
11+
* unwraps `dataGraphMetadata` and returns the array of descriptors
12+
* directly, mirroring the shape other `.list()` methods return.
13+
*
14+
* Real-time and standard data graphs are returned in the same list and are
15+
* distinguished by fields on each descriptor, not by endpoint.
16+
*/
17+
async list(
18+
params?: DataGraphsGetMetadataParams,
19+
options?: RequestOptions,
20+
): Promise<CdpQueryDataGraphMetadataRepresentation[]> {
21+
const res = await this.getMetadata(params, options);
22+
return res.dataGraphMetadata ?? [];
23+
}
24+
25+
/** Async iterator over `list()` — emitted for parity with other services. */
26+
async *listAll(
27+
params?: DataGraphsGetMetadataParams,
28+
options?: RequestOptions,
29+
): AsyncGenerator<CdpQueryDataGraphMetadataRepresentation, void, undefined> {
30+
for (const item of await this.list(params, options)) {
31+
yield item;
32+
}
33+
}
34+
}

src/resources/data-kits.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,41 @@
1-
export { DataKitsServiceBase as DataKitsService } from "../generated/services/data-kits.base.js";
1+
import { DataKitsServiceBase } from "../generated/services/data-kits.base.js";
2+
import type { DataKitsListParams, DataKitsListAvailableComponentsParams } from "../generated/services/data-kits.base.js";
3+
import type { PaginationParams, RequestOptions } from "../core/types.js";
4+
import type {
5+
DataKitComponentCollectionRepresentation,
6+
DataKitOutputRepresentation,
7+
} from "../schemas.js";
8+
9+
export class DataKitsService extends DataKitsServiceBase {
10+
/**
11+
* GET /ssot/data-kits — Get data kits.
12+
*
13+
* Known tenant-side reliability issue: this endpoint can take > 60s to
14+
* respond or time out entirely on orgs with many installed data kits. If
15+
* you hit a timeout, inventorying data kits via the Metadata API
16+
* (`DataPackageKitDefinition` and `DataKitObjectTemplate` types) is a more
17+
* reliable alternative. The Connect API remains the correct surface for
18+
* `deploy` / `undeploy` actions.
19+
*/
20+
override async list(
21+
params?: DataKitsListParams,
22+
options?: RequestOptions,
23+
): Promise<DataKitOutputRepresentation> {
24+
return super.list(params, options);
25+
}
26+
27+
/**
28+
* GET /ssot/data-kits/available-components — Get data kit available components.
29+
*
30+
* Known tenant-side reliability issue: this endpoint has been observed
31+
* returning `500 Server Error` on orgs with large data kit inventories.
32+
* If that happens, fall back to reading `DataKitObjectTemplate` metadata
33+
* via the sf CLI Metadata API.
34+
*/
35+
override async listAvailableComponents(
36+
params?: PaginationParams & DataKitsListAvailableComponentsParams,
37+
options?: RequestOptions,
38+
): Promise<DataKitComponentCollectionRepresentation> {
39+
return super.listAvailableComponents(params, options);
40+
}
41+
}
Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,65 @@
1-
export { DataModelObjectsServiceBase as DataModelObjectsService } from "../generated/services/data-model-objects.base.js";
1+
import { DataModelObjectsServiceBase } from "../generated/services/data-model-objects.base.js";
2+
import type { DataModelObjectsListMappingsParams } from "../generated/services/data-model-objects.base.js";
3+
import { NotFoundError } from "../core/errors.js";
4+
import type { PaginationParams, RequestOptions } from "../core/types.js";
5+
import type {
6+
CdpObjectSourceTargetMapCollectionRepresentation,
7+
DataModelObjectCollectionRepresentation,
8+
} from "../schemas.js";
9+
10+
/**
11+
* DMO response-shape note: `list()` returns a body whose array is under the
12+
* **singular** key `dataModelObject` (not plural, unlike every other `list`
13+
* method in the SDK). Destructure accordingly:
14+
*
15+
* const { dataModelObject: items } = await client.dataModelObjects.list();
16+
*
17+
* This is a spec oddity that the generated types preserve; the override
18+
* below intentionally does not reshape it so raw responses remain diffable
19+
* against captured fixtures.
20+
*/
21+
export class DataModelObjectsService extends DataModelObjectsServiceBase {
22+
override async list(
23+
params?: PaginationParams,
24+
options?: RequestOptions,
25+
): Promise<DataModelObjectCollectionRepresentation> {
26+
return super.list(params, options);
27+
}
28+
29+
/**
30+
* GET /ssot/data-model-object-mappings — Get data model object mappings.
31+
*
32+
* The API returns 404 `NOT_FOUND` (`Object Source Target Map not found for
33+
* the given Target Object Dev Name`) when the DMO exists but has no
34+
* mappings — notably for any `ssot__`-prefixed or UI-graph-generated DMO.
35+
* This override catches that specific 404 and returns an empty collection
36+
* so callers can iterate without a try/catch around every call.
37+
*
38+
* Non-404 errors (including 404s for a DMO that doesn't exist at all) are
39+
* not suppressed — the body's error code disambiguates them upstream.
40+
*/
41+
override async listMappings(
42+
params: DataModelObjectsListMappingsParams,
43+
options?: RequestOptions,
44+
): Promise<CdpObjectSourceTargetMapCollectionRepresentation> {
45+
try {
46+
return await super.listMappings(params, options);
47+
} catch (e) {
48+
if (e instanceof NotFoundError && isNoMappingsError(e)) {
49+
return { objectSourceTargetMaps: [] } as CdpObjectSourceTargetMapCollectionRepresentation;
50+
}
51+
throw e;
52+
}
53+
}
54+
}
55+
56+
function isNoMappingsError(err: NotFoundError): boolean {
57+
const body = err.body as unknown;
58+
if (!Array.isArray(body)) return false;
59+
return body.some(
60+
(e) =>
61+
typeof e === "object" &&
62+
e !== null &&
63+
(e as { message?: string }).message?.startsWith("Object Source Target Map not found"),
64+
);
65+
}
Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,36 @@
1-
export { IdentityResolutionsServiceBase as IdentityResolutionsService } from "../generated/services/identity-resolutions.base.js";
1+
import { IdentityResolutionsServiceBase } from "../generated/services/identity-resolutions.base.js";
2+
import type { RequestOptions } from "../core/types.js";
3+
import type {
4+
CdpIdentityResolutionConfigPatchInput,
5+
CdpIdentityResolutionOutputRepresentation,
6+
} from "../schemas.js";
7+
8+
/**
9+
* Identity resolution operations.
10+
*
11+
* Path parameter semantics (worth calling out because the SDK parameter name
12+
* implies either option works): `get`, `delete`, `patch`, and `runNow` all
13+
* require the identity resolution's **id** (e.g. `1irKa000000KzoZIAS`), not
14+
* its `developerName`. The id is what appears in the list response. If you
15+
* only have a developer name, resolve it via `list()` first.
16+
*/
17+
export class IdentityResolutionsService extends IdentityResolutionsServiceBase {
18+
override async get(
19+
identityResolution: string,
20+
options?: RequestOptions,
21+
): Promise<CdpIdentityResolutionOutputRepresentation> {
22+
return super.get(identityResolution, options);
23+
}
24+
25+
override async delete(identityResolution: string, options?: RequestOptions): Promise<void> {
26+
return super.delete(identityResolution, options);
27+
}
28+
29+
override async patch(
30+
identityResolution: string,
31+
body: CdpIdentityResolutionConfigPatchInput,
32+
options?: RequestOptions,
33+
): Promise<CdpIdentityResolutionOutputRepresentation> {
34+
return super.patch(identityResolution, body, options);
35+
}
36+
}

tests/resources/data-graphs.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,43 @@ describe("DataGraphsService", () => {
2020
expect(httpClient.get).toHaveBeenCalled();
2121
});
2222

23+
it("list() unwraps dataGraphMetadata from getMetadata response", async () => {
24+
const items = [{ developerName: "A" }, { developerName: "B" }];
25+
const httpClient = {
26+
get: vi.fn(async () => ({ dataGraphMetadata: items })),
27+
} as unknown as HttpClient;
28+
const service = new DataGraphsService(httpClient);
29+
30+
const res = await service.list();
31+
expect(res).toEqual(items);
32+
expect(httpClient.get).toHaveBeenCalledWith(
33+
"/ssot/data-graphs/metadata",
34+
expect.objectContaining({ query: undefined }),
35+
);
36+
});
37+
38+
it("list() returns [] when dataGraphMetadata is missing", async () => {
39+
const httpClient = {
40+
get: vi.fn(async () => ({})),
41+
} as unknown as HttpClient;
42+
const service = new DataGraphsService(httpClient);
43+
44+
const res = await service.list();
45+
expect(res).toEqual([]);
46+
});
47+
48+
it("listAll() yields each descriptor", async () => {
49+
const items = [{ developerName: "A" }, { developerName: "B" }];
50+
const httpClient = {
51+
get: vi.fn(async () => ({ dataGraphMetadata: items })),
52+
} as unknown as HttpClient;
53+
const service = new DataGraphsService(httpClient);
54+
55+
const collected = [];
56+
for await (const item of service.listAll()) collected.push(item);
57+
expect(collected).toEqual(items);
58+
});
59+
2360
it("get()", async () => {
2461
const httpClient = createMockHttpClient();
2562
const service = new DataGraphsService(httpClient);
@@ -42,7 +79,7 @@ describe("DataGraphsService", () => {
4279
const httpClient = createMockHttpClient();
4380
const service = new DataGraphsService(httpClient);
4481

45-
await service.getData("test-entityName", { batchSize: 10 });
82+
await service.getData("test-entityName", { lookupKeys: "Id=abc" });
4683

4784
expect(httpClient.get).toHaveBeenCalled();
4885
});

0 commit comments

Comments
 (0)