Skip to content

Commit 1e3c147

Browse files
authored
Experimental object set asyncIterLinks (#2209)
* Sketch out implementation of asyncIterLinks * Add asyncIterLinks to createObjectSet * Add preview=true to platform SDK call * Fix target type and add type tests * Add e2e test * Add test for single link * Add changeset * Fix tests * Update e2e test * Add request context * Add fetchLinksPage tests * Add more detailed example of usage * Add more docs * Rename {ObjectLink,LinksForObject} -> MinimalDirectedObjectLinkInstance * Rename asyncIterLinks -> experimental_asyncIterLinks * Revert "Add changeset" This reverts commit 6620283. * Add patch changeset
1 parent c269ce5 commit 1e3c147

File tree

10 files changed

+468
-2
lines changed

10 files changed

+468
-2
lines changed

.changeset/busy-needles-follow.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@osdk/client": patch
3+
"@osdk/api": patch
4+
---
5+
6+
Add experimental_asyncIterLinks method on object sets

etc/api.report.api.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,15 @@ export const DurationMapping: {
602602
// @public (undocumented)
603603
export type DurationPrecision = "DAYS" | "HOURS" | "MINUTES" | "SECONDS" | "AUTO";
604604

605+
// @public (undocumented)
606+
export type FetchLinksPageResult<
607+
Q extends ObjectOrInterfaceDefinition,
608+
LINK_TYPE extends LinkTypeApiNamesFor<Q>
609+
> = {
610+
data: Array<MinimalDirectedObjectLinkInstance<Q, LINK_TYPE>>
611+
nextPageToken?: string
612+
};
613+
605614
// @public (undocumented)
606615
export interface FetchPageArgs<
607616
Q extends ObjectOrInterfaceDefinition,
@@ -794,6 +803,9 @@ export type LinkedType<
794803
// @public (undocumented)
795804
export type LinkNames<Q extends ObjectOrInterfaceDefinition> = Q extends InterfaceDefinition ? keyof CompileTimeMetadata<Q>["links"] : keyof CompileTimeMetadata<Q>["links"] & string;
796805

806+
// @public (undocumented)
807+
export type LinkTypeApiNamesFor<Q extends ObjectOrInterfaceDefinition> = Extract<keyof CompileTimeMetadata<Q>["links"], string>;
808+
797809
// @public (undocumented)
798810
export interface Logger {
799811
// (undocumented)
@@ -879,6 +891,16 @@ export interface MediaUpload {
879891
readonly fileName: string;
880892
}
881893

894+
// @public (undocumented)
895+
export type MinimalDirectedObjectLinkInstance<
896+
Q extends ObjectOrInterfaceDefinition,
897+
LINK_TYPE_API_NAME extends LinkTypeApiNamesFor<Q>
898+
> = {
899+
source: ObjectIdentifiers<Q>
900+
target: ObjectIdentifiers<LinkedObjectType<Q, LINK_TYPE_API_NAME>>
901+
linkType: LINK_TYPE_API_NAME
902+
};
903+
882904
// @public (undocumented)
883905
export type NotWhereClause<
884906
T extends ObjectOrInterfaceDefinition,
@@ -1858,6 +1880,7 @@ export type WirePropertyTypes = BaseWirePropertyTypes | Record<string, BaseWireP
18581880
// src/aggregate/AggregateOpts.ts:25:3 - (ae-forgotten-export) The symbol "OrderedAggregationClause" needs to be exported by the entry point index.d.ts
18591881
// src/aggregate/AggregationResultsWithGroups.ts:36:5 - (ae-forgotten-export) The symbol "MaybeNullable_2" needs to be exported by the entry point index.d.ts
18601882
// src/aggregate/AggregationResultsWithGroups.ts:36:5 - (ae-forgotten-export) The symbol "OsdkObjectPropertyTypeNotUndefined" needs to be exported by the entry point index.d.ts
1883+
// src/objectSet/ObjectSetLinks.ts:36:3 - (ae-forgotten-export) The symbol "LinkedObjectType" needs to be exported by the entry point index.d.ts
18611884

18621885
// (No @packageDocumentation comment for this package)
18631886

packages/api/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ export { isOk } from "./object/Result.js";
9191
export type { Result } from "./object/Result.js";
9292
export type { BaseObjectSet } from "./objectSet/BaseObjectSet.js";
9393
export type { ObjectSet } from "./objectSet/ObjectSet.js";
94+
export type {
95+
FetchLinksPageResult,
96+
LinkTypeApiNamesFor,
97+
MinimalDirectedObjectLinkInstance,
98+
} from "./objectSet/ObjectSetLinks.js";
9499
export type { ObjectSetSubscription } from "./objectSet/ObjectSetListener.js";
95100
export type {
96101
ActionDefinition,

packages/api/src/objectSet/ObjectSet.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { describe, expectTypeOf, it, test, vi } from "vitest";
1919
import type {
2020
DerivedProperty,
2121
NullabilityAdherence,
22+
ObjectIdentifiers,
2223
ObjectOrInterfaceDefinition,
2324
ObjectSet as $ObjectSet,
2425
Osdk,
@@ -29,6 +30,15 @@ import type { DerivedObjectOrInterfaceDefinition } from "../ontology/ObjectOrInt
2930
import { EmployeeApiTest } from "../test/EmployeeApiTest.js";
3031
import { FooInterfaceApiTest } from "../test/FooInterfaceApiTest.js";
3132

33+
async function* asyncIterateLinkOnce() {
34+
const obj = { $apiName: undefined, $primaryKey: undefined };
35+
yield Promise.resolve({
36+
source: obj,
37+
target: obj,
38+
linkTypeApiName: undefined,
39+
});
40+
}
41+
3242
export function createMockObjectSet<
3343
Q extends ObjectOrInterfaceDefinition,
3444
>(): $ObjectSet<Q, never> {
@@ -73,6 +83,7 @@ export function createMockObjectSet<
7383
nearestNeighbors: vi.fn(() => {
7484
return fauxObjectSet;
7585
}),
86+
experimental_asyncIterLinks: vi.fn(asyncIterateLinkOnce),
7687
} as any as $ObjectSet<Q>;
7788

7889
return fauxObjectSet;
@@ -1491,4 +1502,55 @@ describe("ObjectSet", () => {
14911502
>();
14921503
});
14931504
});
1505+
1506+
describe("asyncIterLinks", async () => {
1507+
it("typechecks self-referential one link", async () => {
1508+
for await (
1509+
const { source, target, linkType } of fauxObjectSet
1510+
.experimental_asyncIterLinks([
1511+
"lead",
1512+
])
1513+
) {
1514+
expectTypeOf(source).toEqualTypeOf<
1515+
ObjectIdentifiers<EmployeeApiTest>
1516+
>();
1517+
1518+
expectTypeOf(target).toEqualTypeOf<
1519+
ObjectIdentifiers<EmployeeApiTest>
1520+
>();
1521+
1522+
expectTypeOf(source.$apiName).toBeString();
1523+
expectTypeOf(target.$apiName).toBeString();
1524+
expectTypeOf(source.$primaryKey).toBeNumber();
1525+
expectTypeOf(target.$primaryKey).toBeNumber();
1526+
1527+
expectTypeOf(linkType).toEqualTypeOf<"lead">();
1528+
}
1529+
});
1530+
1531+
it("typechecks self-referential multiple links", async () => {
1532+
for await (
1533+
const { source, target, linkType } of fauxObjectSet
1534+
.experimental_asyncIterLinks([
1535+
"lead",
1536+
"peeps",
1537+
])
1538+
) {
1539+
expectTypeOf(source).toEqualTypeOf<
1540+
ObjectIdentifiers<EmployeeApiTest>
1541+
>();
1542+
1543+
expectTypeOf(target).toEqualTypeOf<
1544+
ObjectIdentifiers<EmployeeApiTest>
1545+
>();
1546+
1547+
expectTypeOf(source.$apiName).toBeString();
1548+
expectTypeOf(target.$apiName).toBeString();
1549+
expectTypeOf(source.$primaryKey).toBeNumber();
1550+
expectTypeOf(target.$primaryKey).toBeNumber();
1551+
1552+
expectTypeOf(linkType).toEqualTypeOf<"lead" | "peeps">();
1553+
}
1554+
});
1555+
});
14941556
});

packages/api/src/objectSet/ObjectSet.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ import type {
5050
import type { PageResult } from "../PageResult.js";
5151
import type { LinkedType, LinkNames } from "../util/LinkUtils.js";
5252
import type { BaseObjectSet } from "./BaseObjectSet.js";
53+
import type {
54+
LinkTypeApiNamesFor,
55+
MinimalDirectedObjectLinkInstance,
56+
} from "./ObjectSetLinks.js";
5357
import type { ObjectSetSubscription } from "./ObjectSetListener.js";
5458

5559
type MergeObjectSet<
@@ -107,7 +111,8 @@ export interface MinimalObjectSet<
107111
BaseObjectSet<Q>,
108112
FetchPage<Q, RDPs>,
109113
AsyncIter<Q, RDPs, ORDER_BY_OPTIONS>,
110-
Where<Q, RDPs>
114+
Where<Q, RDPs>,
115+
AsyncIterLinks<Q>
111116
{
112117
}
113118

@@ -586,6 +591,28 @@ type ExtractImplementingTypes<T extends InterfaceDefinition> =
586591
? (ObjectTypeDefinition & { apiName: API_NAME }) | InterfaceDefinition
587592
: InterfaceDefinition;
588593

594+
interface AsyncIterLinks<Q extends ObjectOrInterfaceDefinition> {
595+
/**
596+
* Batch load links on an object set. This is an experimental method that may change while in beta.
597+
* Use this method in conjunction with `.asyncIter()` and `.pivotTo(...).asyncIter()` to build an
598+
* object graph in memory.
599+
*
600+
* Please keep these limitations in mind:
601+
* - Links returned may be stale. For example, primary keys returned by this endpoint may not exist anymore.
602+
* - The backend API fetches pages of *n* objects at a time. If, for any page of *n* objects, there are more
603+
* than 100,000 links present, results are limited to 100,000 links and should be considered partial.
604+
* - This method does not support OSv1 links and will throw an exception if links provided are backed by OSv1.
605+
* - This method currently does not support interface links, but support will be added in the near future.
606+
*/
607+
readonly experimental_asyncIterLinks: <
608+
LINK_TYPE_API_NAME extends LinkTypeApiNamesFor<Q>,
609+
>(
610+
links: LINK_TYPE_API_NAME[],
611+
) => AsyncIterableIterator<
612+
MinimalDirectedObjectLinkInstance<Q, LINK_TYPE_API_NAME>
613+
>;
614+
}
615+
589616
interface ObjectSetCleanedTypes<
590617
Q extends ObjectOrInterfaceDefinition,
591618
D extends Record<string, SimplePropertyDef>,
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2025 Palantir Technologies, Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type { ObjectOrInterfaceDefinition } from "../ontology/ObjectOrInterface.js";
18+
import type { CompileTimeMetadata } from "../ontology/ObjectTypeDefinition.js";
19+
import type { ObjectIdentifiers } from "../OsdkBase.js";
20+
21+
export type LinkTypeApiNamesFor<Q extends ObjectOrInterfaceDefinition> =
22+
Extract<keyof CompileTimeMetadata<Q>["links"], string>;
23+
24+
type LinkedObjectType<
25+
Q extends ObjectOrInterfaceDefinition,
26+
LINK_TYPE_API_NAME extends LinkTypeApiNamesFor<Q>,
27+
> = NonNullable<
28+
CompileTimeMetadata<Q>["links"][LINK_TYPE_API_NAME]["__OsdkLinkTargetType"]
29+
>;
30+
31+
export type MinimalDirectedObjectLinkInstance<
32+
Q extends ObjectOrInterfaceDefinition,
33+
LINK_TYPE_API_NAME extends LinkTypeApiNamesFor<Q>,
34+
> = {
35+
source: ObjectIdentifiers<Q>;
36+
target: ObjectIdentifiers<LinkedObjectType<Q, LINK_TYPE_API_NAME>>;
37+
linkType: LINK_TYPE_API_NAME;
38+
};
39+
40+
export type FetchLinksPageResult<
41+
Q extends ObjectOrInterfaceDefinition,
42+
LINK_TYPE extends LinkTypeApiNamesFor<Q>,
43+
> = {
44+
data: Array<MinimalDirectedObjectLinkInstance<Q, LINK_TYPE>>;
45+
nextPageToken?: string;
46+
};

packages/client/src/objectSet/createObjectSet.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import type {
2121
InterfaceDefinition,
2222
LinkedType,
2323
LinkNames,
24+
LinkTypeApiNamesFor,
25+
MinimalDirectedObjectLinkInstance,
2426
NullabilityAdherence,
2527
ObjectOrInterfaceDefinition,
2628
ObjectSet,
@@ -52,6 +54,7 @@ import { fetchSingle, fetchSingleWithErrors } from "../object/fetchSingle.js";
5254
import { augmentRequestContext } from "../util/augmentRequestContext.js";
5355
import { resolveBaseObjectSetType } from "../util/objectSetUtils.js";
5456
import { isWireObjectSet } from "../util/WireObjectSet.js";
57+
import { fetchLinksPage } from "./fetchLinksPage.js";
5558
import { ObjectSetListenerWebsocket } from "./ObjectSetListenerWebsocket.js";
5659

5760
function isObjectTypeDefinition(
@@ -325,6 +328,32 @@ export function createObjectSet<Q extends ObjectOrInterfaceDefinition>(
325328
);
326329
},
327330

331+
experimental_asyncIterLinks: async function*<
332+
LINK_TYPE_API_NAME extends LinkTypeApiNamesFor<Q>,
333+
>(
334+
links: LINK_TYPE_API_NAME[],
335+
): AsyncIterableIterator<
336+
MinimalDirectedObjectLinkInstance<Q, LINK_TYPE_API_NAME>
337+
> {
338+
let $nextPageToken: string | undefined = undefined;
339+
do {
340+
const result = await fetchLinksPage(
341+
augmentRequestContext(
342+
clientCtx,
343+
_ => ({ finalMethodCall: "asyncIterLinks" }),
344+
),
345+
objectType,
346+
objectSet,
347+
links,
348+
);
349+
$nextPageToken = result.nextPageToken;
350+
351+
for (const obj of result.data) {
352+
yield obj;
353+
}
354+
} while ($nextPageToken != null);
355+
},
356+
328357
$objectSetInternals: {
329358
def: objectType,
330359
},
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2025 Palantir Technologies, Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { describe, expect, it } from "vitest";
18+
import { remapLinksPage, remapObjectLocator } from "./fetchLinksPage.js";
19+
20+
describe("remapObjectLocator", () => {
21+
it("works", () => {
22+
expect(
23+
remapObjectLocator({
24+
__apiName: "Foo",
25+
__primaryKey: "bar",
26+
prop: "BAZ",
27+
}),
28+
).toEqual({ $apiName: "Foo", $primaryKey: "bar" });
29+
});
30+
});
31+
32+
describe("remapLinksPage", () => {
33+
it("works", () => {
34+
expect(
35+
remapLinksPage({
36+
nextPageToken: "foo",
37+
data: [
38+
{
39+
sourceObject: { __apiName: "Object", __primaryKey: 0 },
40+
linkedObjects: [
41+
{
42+
targetObject: { __apiName: "LinkedObject", __primaryKey: 1 },
43+
linkType: "link",
44+
},
45+
],
46+
},
47+
{
48+
sourceObject: { __apiName: "Object", __primaryKey: 1 },
49+
linkedObjects: [
50+
{
51+
targetObject: { __apiName: "LinkedObject", __primaryKey: 2 },
52+
linkType: "link",
53+
},
54+
{
55+
targetObject: { __apiName: "LinkedObject", __primaryKey: 3 },
56+
linkType: "link",
57+
},
58+
],
59+
},
60+
],
61+
}),
62+
).toEqual({
63+
nextPageToken: "foo",
64+
data: [
65+
{
66+
source: { $apiName: "Object", $primaryKey: 0 },
67+
target: { $apiName: "LinkedObject", $primaryKey: 1 },
68+
linkType: "link",
69+
},
70+
{
71+
source: { $apiName: "Object", $primaryKey: 1 },
72+
target: { $apiName: "LinkedObject", $primaryKey: 2 },
73+
linkType: "link",
74+
},
75+
{
76+
source: { $apiName: "Object", $primaryKey: 1 },
77+
target: { $apiName: "LinkedObject", $primaryKey: 3 },
78+
linkType: "link",
79+
},
80+
],
81+
});
82+
});
83+
});

0 commit comments

Comments
 (0)