From b3563e035903dd4f490f6e1be5c192877284a999 Mon Sep 17 00:00:00 2001 From: Eric Anderson Date: Wed, 12 Jun 2024 12:03:24 -0400 Subject: [PATCH] OSDK learns __EXPERIMENTAL_strictNonNull to throw, drop objects, or return `| undefined` for properties, allowing for correct typesafety. (#387) --- .changeset/purple-cameras-invite.md | 5 + .../basic/cli/src/demoStrictness.ts | 61 ++++++ .../api/src/ontology/ObjectOrInterface.ts | 1 + packages/client/src/Definitions.test.ts | 55 ++++++ packages/client/src/Definitions.ts | 12 +- packages/client/src/OsdkObjectFrom.test.ts | 88 ++++++++- packages/client/src/OsdkObjectFrom.ts | 66 +++++-- packages/client/src/object/FetchPageArgs.ts | 9 +- packages/client/src/object/FetchPageResult.ts | 53 +++++- .../object/convertWireToOsdkObjects.test.ts | 178 ++++++++++++++++- .../src/object/convertWireToOsdkObjects.ts | 130 +++++++++++-- .../createOsdkObject.ts | 3 +- packages/client/src/object/fetchPage.test.ts | 31 ++- packages/client/src/object/fetchPage.ts | 44 +++-- .../client/src/objectSet/ObjectSet.test.ts | 180 +++++++++++++++++- packages/client/src/objectSet/ObjectSet.ts | 39 ++-- .../client/src/objectSet/createObjectSet.ts | 4 +- .../AggregationResultsWithGroups.ts | 9 +- .../src/__e2e_tests__/loadObjects.test.ts | 10 +- .../src/__e2e_tests__/objectSet.test.ts | 10 +- .../shared.test/src/stubs/objectSetRequest.ts | 22 ++- packages/shared.test/src/stubs/objects.ts | 14 ++ 22 files changed, 912 insertions(+), 112 deletions(-) create mode 100644 .changeset/purple-cameras-invite.md create mode 100644 examples-extra/basic/cli/src/demoStrictness.ts create mode 100644 packages/client/src/Definitions.test.ts diff --git a/.changeset/purple-cameras-invite.md b/.changeset/purple-cameras-invite.md new file mode 100644 index 000000000..9d658db8d --- /dev/null +++ b/.changeset/purple-cameras-invite.md @@ -0,0 +1,5 @@ +--- +"@osdk/client": minor +--- + +OSDK learns \_\_EXPERIMENTAL_strictNonNull to throw, drop objects, or return `| undefined` for properties, allowing for correct typesafety. diff --git a/examples-extra/basic/cli/src/demoStrictness.ts b/examples-extra/basic/cli/src/demoStrictness.ts new file mode 100644 index 000000000..f4eb5394d --- /dev/null +++ b/examples-extra/basic/cli/src/demoStrictness.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2024 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Osdk } from "@osdk/client"; +import { + BoundariesUsState, + Employee, + FooInterface, +} from "@osdk/examples.basic.sdk"; +import { expectType } from "ts-expect"; +import { client } from "./client.js"; + +export async function demoStrictnessObject() { + const { data: defaultResults } = await client(BoundariesUsState) + .fetchPage(); + expectType(defaultResults[0].usState); + + const { data: dropResults } = await client(BoundariesUsState) + .fetchPage({ $__EXPERIMENTAL_strictNonNull: "drop" }); + expectType(dropResults[0].usState); + + const { data: notStrictResults } = await client(BoundariesUsState) + .fetchPage({ $__EXPERIMENTAL_strictNonNull: false }); + expectType(notStrictResults[0].usState); + + const { data: throwResults } = await client(BoundariesUsState) + .fetchPage({ $__EXPERIMENTAL_strictNonNull: "throw" }); + expectType(throwResults[0].usState); + + const { data: fooDataNotStrict } = await client(FooInterface) + .fetchPage({ $__EXPERIMENTAL_strictNonNull: false }); + + const employeeNotStrict = fooDataNotStrict[0].$as(Employee); +} + +export async function demoStrictnessInterface() { + const { data: fooDataNotStrict } = await client(FooInterface) + .fetchPage({ $__EXPERIMENTAL_strictNonNull: false }); + + const employeeNotStrict = fooDataNotStrict[0].$as(Employee); + expectType< + Osdk< + Employee, + "$notStrict" | "firstName" | "email", + "$notStrict" | "firstName" | "email" + > + >(employeeNotStrict); +} diff --git a/packages/api/src/ontology/ObjectOrInterface.ts b/packages/api/src/ontology/ObjectOrInterface.ts index 744628de9..dcd6ec557 100644 --- a/packages/api/src/ontology/ObjectOrInterface.ts +++ b/packages/api/src/ontology/ObjectOrInterface.ts @@ -39,6 +39,7 @@ export type ObjectOrInterfaceDefinition< | ObjectTypeDefinition | InterfaceDefinition; +/** @deprecated */ export type ObjectOrInterfacePropertyKeysFrom< O extends OntologyDefinition, K extends ObjectOrInterfaceKeysFrom, diff --git a/packages/client/src/Definitions.test.ts b/packages/client/src/Definitions.test.ts new file mode 100644 index 000000000..359919f9f --- /dev/null +++ b/packages/client/src/Definitions.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2024 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ObjectTypePropertyDefinition } from "@osdk/api"; +import { describe, expectTypeOf, it } from "vitest"; +import type { OsdkObjectPropertyType } from "./Definitions.js"; + +describe("OsdkObjectPropertyType", () => { + describe("{ nullable: false } property", () => { + const nonNullDef = { + type: "string", + nullable: false, + } satisfies ObjectTypePropertyDefinition; + + it("is `| undefined` for `false`", () => { + expectTypeOf>() + .toEqualTypeOf(); + }); + + it("is not `| undefined` for `true`", () => { + expectTypeOf>() + .toEqualTypeOf(); + }); + }); + + describe("{ nullable: true } property", () => { + const nullableDef = { + type: "string", + nullable: true, + } satisfies ObjectTypePropertyDefinition; + + it("is | undefined for `false`", () => { + expectTypeOf>() + .toEqualTypeOf(); + }); + + it("is `| undefined` for `true`", () => { + expectTypeOf>() + .toEqualTypeOf(); + }); + }); +}); diff --git a/packages/client/src/Definitions.ts b/packages/client/src/Definitions.ts index 3ac084b98..907983e3f 100644 --- a/packages/client/src/Definitions.ts +++ b/packages/client/src/Definitions.ts @@ -27,8 +27,16 @@ type MaybeNullable = type Raw = T extends Array ? T[0] : T; type Converted = T extends Array ? T[1] : T; -export type OsdkObjectPropertyType = - MaybeNullable< +/** + * @param {T} ObjectTypePropertyDefinition in literal form + * @param {STRICTLY_ENFORCE_NULLABLE} S for strict. If false, always `|undefined` + */ +export type OsdkObjectPropertyType< + T extends ObjectTypePropertyDefinition, + STRICTLY_ENFORCE_NULLABLE extends boolean = true, +> = STRICTLY_ENFORCE_NULLABLE extends false + ? MaybeArray> | undefined + : MaybeNullable< T, MaybeArray> >; diff --git a/packages/client/src/OsdkObjectFrom.test.ts b/packages/client/src/OsdkObjectFrom.test.ts index e1222b3eb..175a2f9f1 100644 --- a/packages/client/src/OsdkObjectFrom.test.ts +++ b/packages/client/src/OsdkObjectFrom.test.ts @@ -30,6 +30,24 @@ describe("ConvertProps", () => { expectTypeOf>() .toEqualTypeOf<"fullName">(); }); + + it("converts non-strict nullness for handles single prop", () => { + expectTypeOf< + ConvertProps< + FooInterface, + Employee, + | "fooSpt" + | "$notStrict" + > + >() + .toEqualTypeOf<"fullName" | "$notStrict">(); + }); + it("converts non-strict nullness for $all", () => { + expectTypeOf< + ConvertProps + >() + .toEqualTypeOf<"fullName" | "$notStrict">(); + }); }); describe("converts from a concrete to an interface", () => { @@ -43,6 +61,27 @@ describe("ConvertProps", () => { expectTypeOf>() .toEqualTypeOf(); }); + + it("resolves to never for unused when not strict", () => { + expectTypeOf< + ConvertProps + >() + .toEqualTypeOf(); + }); + + it("converts non-strict nullness for single prop", () => { + expectTypeOf< + ConvertProps + >() + .toEqualTypeOf<"fooSpt" | "$notStrict">(); + }); + + it("converts non-strict nullness for $all", () => { + expectTypeOf< + ConvertProps + >() + .toEqualTypeOf<"$all" | "$notStrict">(); + }); }); describe("multiprop", () => { @@ -52,6 +91,17 @@ describe("ConvertProps", () => { >() .toEqualTypeOf<"fooSpt">(); }); + + it("resolves to known only when not strict", () => { + expectTypeOf< + ConvertProps< + Employee, + FooInterface, + "peeps" | "fullName" | "$notStrict" + > + >() + .toEqualTypeOf<"fooSpt" | "$notStrict">(); + }); }); it("handles $all", () => { @@ -88,7 +138,7 @@ describe("Osdk", () => { it("can't properly compare the retained types without it", () => { type InvalidPropertyName = "not a real prop"; - // This should fail but it doesnt. We dont actively capture Z + // This should fail but it doesn't. We don't actively capture Z // (and we cant) expectTypeOf< OsdkAsHelper @@ -117,18 +167,42 @@ describe("Osdk", () => { }); }); + describe("$notStrict", () => { + describe("is present", () => { + it("makes { nullable: false } as `|undefined`", () => { + expectTypeOf< + Osdk["employeeId"] + >() + .toEqualTypeOf(); + expectTypeOf< + GetUnderlyingProps< + OsdkAsHelper + > + >().toEqualTypeOf<"fullName">(); + }); + }); + + describe("is absent", () => { + it("makes { nullable: false } as NOT `|undefined`", () => { + expectTypeOf< + Osdk["employeeId"] + >() + .toEqualTypeOf(); + expectTypeOf< + GetUnderlyingProps< + OsdkAsHelper + > + >().toEqualTypeOf<"fullName">(); + }); + }); + }); + it("Converts into self properly", () => { expectTypeOf< GetUnderlyingProps< OsdkAsHelper > >().toEqualTypeOf<"fullName">(); - - type z = Osdk; - type FM = T extends (infer P)[] ? P : never; - type zz = Awaited< - Promise> - >["data"]; }); it("retains original props if set", () => { diff --git a/packages/client/src/OsdkObjectFrom.ts b/packages/client/src/OsdkObjectFrom.ts index 9058589c7..284341ab2 100644 --- a/packages/client/src/OsdkObjectFrom.ts +++ b/packages/client/src/OsdkObjectFrom.ts @@ -22,8 +22,17 @@ import type { import type { OsdkBase, OsdkObjectPrimaryKeyType } from "@osdk/client.api"; import type { OsdkObjectPropertyType } from "./Definitions.js"; import type { OsdkObjectLinksObject } from "./definitions/LinkDefinitions.js"; +import type { UnionIfTrue } from "./object/FetchPageResult.js"; -type DropRidAndAll = Exclude; +type DropDollarOptions = Exclude< + T, + "$rid" | "$all" | "$notStrict" +>; + +type DropDollarAll = Exclude< + T, + "$all" +>; type ApiNameAsString = NonNullable< T["apiName"]["__Unbranded"] @@ -42,24 +51,35 @@ export type ConvertProps< > = TO extends FROM ? P : TO extends ObjectTypeDefinition ? ( ( - TO["interfaceMap"][ApiNameAsString][ - P extends "$all" - ? keyof FROM["properties"] extends - keyof TO["interfaceMap"][ApiNameAsString] - ? keyof FROM["properties"] - : never - : DropRidAndAll

- ] + UnionIfTrue< + TO["interfaceMap"][ApiNameAsString][ + P extends "$all" ? ( + keyof FROM["properties"] extends + keyof TO["interfaceMap"][ApiNameAsString] + ? keyof FROM["properties"] + : never + ) + : DropDollarOptions

+ ], + P extends "$notStrict" ? true : false, + "$notStrict" + > ) ) - : TO extends InterfaceDefinition ? P extends "$all" ? "$all" - : FROM extends ObjectTypeDefinition - ? DropRidAndAll

extends keyof FROM["inverseInterfaceMap"][ - ApiNameAsString - ] ? FROM["inverseInterfaceMap"][ApiNameAsString][DropRidAndAll

] + : UnionIfTrue< + TO extends InterfaceDefinition ? P extends "$all" ? "$all" + : FROM extends ObjectTypeDefinition + ? DropDollarOptions

extends keyof FROM["inverseInterfaceMap"][ + ApiNameAsString + ] ? FROM["inverseInterfaceMap"][ApiNameAsString][ + DropDollarOptions

+ ] + : never : never - : never - : never; + : never, + P extends "$notStrict" ? true : false, + "$notStrict" + >; /** DO NOT EXPORT FROM PACKAGE */ export type ValidToFrom = FROM extends @@ -67,6 +87,10 @@ export type ValidToFrom = FROM extends ? ObjectTypeDefinition | InterfaceDefinition : InterfaceDefinition; +/** + * @param P The properties to add from Q + * @param Z The existing underlying properties + */ type UnderlyingProps< Q extends ObjectOrInterfaceDefinition, P extends string, @@ -98,8 +122,14 @@ export type Osdk< ) ]: IsNever

extends true // when we don't know what properties, we will show all but ensure they are `| undefined` - ? OsdkObjectPropertyType | undefined - : OsdkObjectPropertyType; + ? OsdkObjectPropertyType< + Q["properties"][PP], + false // P is never so we do not have to check for "$notStrict" + > + : OsdkObjectPropertyType< + Q["properties"][PP], + P extends "$notStrict" ? false : true + >; } & { /** @deprecated use $apiName */ diff --git a/packages/client/src/object/FetchPageArgs.ts b/packages/client/src/object/FetchPageArgs.ts index 2044f021b..644d91c92 100644 --- a/packages/client/src/object/FetchPageArgs.ts +++ b/packages/client/src/object/FetchPageArgs.ts @@ -20,14 +20,20 @@ import type { ObjectOrInterfaceDefinition, ObjectOrInterfacePropertyKeysFrom2, } from "@osdk/api"; + +export type NullabilityAdherence = false | "throw" | "drop"; +export type NullabilityAdherenceDefault = "throw"; + export interface SelectArg< Q extends ObjectOrInterfaceDefinition, L extends ObjectOrInterfacePropertyKeysFrom2 = ObjectOrInterfacePropertyKeysFrom2, R extends boolean = false, + S extends NullabilityAdherence = NullabilityAdherenceDefault, > { $select?: readonly L[]; $includeRid?: R; + $__EXPERIMENTAL_strictNonNull?: S; } export interface OrderByArg< @@ -53,8 +59,9 @@ export interface FetchPageArgs< ObjectOrInterfacePropertyKeysFrom2, R extends boolean = false, A extends Augments = {}, + S extends NullabilityAdherence = NullabilityAdherenceDefault, > extends - SelectArg, + SelectArg, OrderByArg> { $nextPageToken?: string; diff --git a/packages/client/src/object/FetchPageResult.ts b/packages/client/src/object/FetchPageResult.ts index 9239f86ff..6e43d0960 100644 --- a/packages/client/src/object/FetchPageResult.ts +++ b/packages/client/src/object/FetchPageResult.ts @@ -18,16 +18,59 @@ import type { ObjectOrInterfaceDefinition, ObjectOrInterfacePropertyKeysFrom2, } from "@osdk/api"; +import type { IsNever } from "type-fest"; import type { DefaultToFalse } from "../definitions/LinkDefinitions.js"; import type { Osdk } from "../OsdkObjectFrom.js"; import type { PageResult } from "../PageResult.js"; +import type { NullabilityAdherence } from "./FetchPageArgs.js"; + +/** @internal exposed for a test */ +export type RespectNullability = S extends false + ? false + : true; + +/** @internal exposed for a test */ +export type UnionIfFalse = + IsNever extends true ? never + : JUST_S_IF_TRUE extends true ? S + : S | E; + +/** @internal exposed for a test */ +export type UnionIfTrue = + IsNever extends true ? never + : UNION_IF_TRUE extends true ? S | E + : S; export type FetchPageResult< Q extends ObjectOrInterfaceDefinition, L extends ObjectOrInterfacePropertyKeysFrom2, R extends boolean, -> = PageResult< - ObjectOrInterfacePropertyKeysFrom2 extends L - ? (DefaultToFalse extends false ? Osdk : Osdk) - : (DefaultToFalse extends false ? Osdk : Osdk) ->; + S extends NullabilityAdherence, +> = PageResult>; + +export type SingleOsdkResult< + Q extends ObjectOrInterfaceDefinition, + L extends ObjectOrInterfacePropertyKeysFrom2, + R extends boolean, + S extends NullabilityAdherence, +> = ObjectOrInterfacePropertyKeysFrom2 extends L ? ( + [DefaultToFalse, RespectNullability] extends [false, true] ? Osdk + : Osdk< + Q, + UnionIfTrue< + UnionIfFalse<"$all", RespectNullability, "$notStrict">, + DefaultToFalse, + "$rid" + > + > + ) + : ([DefaultToFalse, RespectNullability] extends [false, true] + ? Osdk + : Osdk< + Q, + UnionIfTrue< + UnionIfFalse, "$notStrict">, + DefaultToFalse, + "$rid" + > + >); diff --git a/packages/client/src/object/convertWireToOsdkObjects.test.ts b/packages/client/src/object/convertWireToOsdkObjects.test.ts index 2930e1b35..1488f0880 100644 --- a/packages/client/src/object/convertWireToOsdkObjects.test.ts +++ b/packages/client/src/object/convertWireToOsdkObjects.test.ts @@ -20,9 +20,9 @@ import { Ontology as MockOntology, } from "@osdk/client.test.ontology"; import type { OntologyObjectV2 } from "@osdk/internal.foundry"; +import { symbolClientContext } from "@osdk/shared.client"; import { createSharedClientContext } from "@osdk/shared.client.impl"; import { apiServer } from "@osdk/shared.test"; -import * as util from "node:util"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import type { Client } from "../Client.js"; import { createClient } from "../createClient.js"; @@ -130,6 +130,9 @@ describe("convertWireToOsdkObjects", () => { clientCtx, [object], undefined, + undefined, + undefined, + false, ); const prototypeAfter = Object.getPrototypeOf(object2); @@ -234,4 +237,177 @@ describe("convertWireToOsdkObjects", () => { } `); }); + + describe("selection keys", () => { + it("throws when required is missing", async () => { + let object = { + __apiName: "Employee", + __primaryKey: 0, + } as const; + + await expect(() => + convertWireToOsdkObjects( + client[symbolClientContext], + [object], + undefined, + undefined, + ["employeeId"], + "throw", + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Unable to safely convert objects as some non nullable properties are null]`, + ); + }); + + it("does not throw when optional is missing", async () => { + let object = { + __apiName: "Employee", + __primaryKey: 0, + } as const; + + await expect( + convertWireToOsdkObjects( + client[symbolClientContext], + [object], + undefined, + undefined, + ["fullName"], + "throw", + ), + ).resolves.to.not.toBeUndefined(); + }); + + it("filters when it should", async () => { + const object = { + __apiName: "Employee", + __primaryKey: 0, + } as const; + + const result = await convertWireToOsdkObjects( + client[symbolClientContext], + [object], + undefined, + undefined, + ["employeeId"], + "drop", + ); + + expect(result.length).toBe(0); + }); + + it("does not filter when it shouldn't", async () => { + const object = { + __apiName: "Employee", + __primaryKey: 0, + } as const; + + const result = await convertWireToOsdkObjects( + client[symbolClientContext], + [object], + undefined, + undefined, + ["fullName"], + "drop", + ); + + expect(result.length).toBe(1); + }); + }); + + describe("without selection keys", () => { + it("throws when required is missing", async () => { + let object = { + __apiName: "Employee", + __primaryKey: 0, + } as const; + + await expect(() => + convertWireToOsdkObjects( + client[symbolClientContext], + [object], + undefined, + undefined, + undefined, + "throw", + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Unable to safely convert objects as some non nullable properties are null]`, + ); + }); + + it("does not throw when required is present", async () => { + let object = { + __apiName: "Employee", + __primaryKey: 0, + "employeeId": 0, + } as const; + + await expect( + convertWireToOsdkObjects( + client[symbolClientContext], + [object], + undefined, + undefined, + undefined, + "throw", + ), + ).resolves.to.not.toBeUndefined(); + }); + + it("filters when it should", async () => { + const object = { + __apiName: "Employee", + __primaryKey: 0, + } as const; + + const result = await convertWireToOsdkObjects( + client[symbolClientContext], + [object], + undefined, + undefined, + undefined, + "drop", + ); + + expect(result.length).toBe(0); + }); + + it("does not filter when it shouldn't", async () => { + const object = { + __apiName: "Employee", + __primaryKey: 0, + "employeeId": 0, + } as const; + + const result = await convertWireToOsdkObjects( + client[symbolClientContext], + [object], + undefined, + undefined, + undefined, + "drop", + ); + + expect(result.length).toBe(1); + }); + }); + + it("behaves correctly when converting", async () => { + const object = { + __apiName: "Employee", + __primaryKey: 0, + fooSpt: "hi", + } as const; + + const result = await convertWireToOsdkObjects( + client[symbolClientContext], + [object], + "FooInterface", + undefined, + ["fooSpt"], + "drop", + ); + + expect(result.length).toBe(1); + }); }); diff --git a/packages/client/src/object/convertWireToOsdkObjects.ts b/packages/client/src/object/convertWireToOsdkObjects.ts index 424daeda3..fab0dd655 100644 --- a/packages/client/src/object/convertWireToOsdkObjects.ts +++ b/packages/client/src/object/convertWireToOsdkObjects.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import type { ObjectOrInterfaceDefinition } from "@osdk/api"; +import type { + InterfaceDefinition, + ObjectOrInterfaceDefinition, + ObjectTypeDefinition, +} from "@osdk/api"; import type { OntologyObjectV2 } from "@osdk/internal.foundry"; import invariant from "tiny-invariant"; import type { MinimalClient } from "../MinimalClientContext.js"; @@ -24,6 +28,7 @@ import { } from "../ontology/OntologyProvider.js"; import type { Osdk } from "../OsdkObjectFrom.js"; import { createOsdkObject } from "./convertWireToOsdkObjects/createOsdkObject.js"; +import type { NullabilityAdherence } from "./FetchPageArgs.js"; /** * If interfaceApiName is not undefined, converts the instances of the @@ -46,10 +51,19 @@ export async function convertWireToOsdkObjects( objects: OntologyObjectV2[], interfaceApiName: string | undefined, forceRemoveRid: boolean = false, + selectedProps?: ReadonlyArray, + strictNonNull: NullabilityAdherence = false, ): Promise[]> { client.logger?.debug(`START convertWireToOsdkObjects()`); - fixObjectPropertiesInline(objects, forceRemoveRid); + fixObjectPropertiesInPlace(objects, forceRemoveRid); + + const ifaceDef = interfaceApiName + ? await client.ontologyProvider.getInterfaceDefinition(interfaceApiName) + : undefined; + const ifaceSelected = ifaceDef + ? (selectedProps ?? Object.keys(ifaceDef.properties)) + : undefined; const ret = []; for (const rawObj of objects) { @@ -58,19 +72,61 @@ export async function convertWireToOsdkObjects( ); invariant(objectDef, `Missing definition for '${rawObj.$apiName}'`); - if (interfaceApiName !== undefined) { + // default value for when we are checking an object + let objProps; + + let conforming = true; + if (ifaceDef && ifaceSelected) { // API returns interface spt names but we cache by real values - reframeAsObjectInPlace(objectDef, interfaceApiName, client, rawObj); + invariantInterfacesAsViews(objectDef, ifaceDef.apiName, client); + + conforming &&= isConforming(client, ifaceDef, rawObj, ifaceSelected); + + reframeAsObjectInPlace(objectDef, ifaceDef.apiName, rawObj); + + objProps = convertInterfacePropNamesToObjectPropNames( + objectDef, + ifaceDef.apiName, + ifaceSelected, + ); + } else { + objProps = selectedProps ?? Object.keys(objectDef.properties); + } + + conforming &&= isConforming(client, objectDef, rawObj, objProps); + + if (strictNonNull === "throw" && !conforming) { + throw new Error( + "Unable to safely convert objects as some non nullable properties are null", + ); + } else if (strictNonNull === "drop" && !conforming) { + continue; } - const osdkObject: any = createOsdkObject(client, objectDef, rawObj); - ret.push(interfaceApiName ? osdkObject.$as(interfaceApiName) : osdkObject); + let osdkObject = createOsdkObject(client, objectDef, rawObj); + if (interfaceApiName) osdkObject = osdkObject.$as(interfaceApiName); + + ret.push(osdkObject); } client.logger?.debug(`END convertWireToOsdkObjects()`); return ret; } +/** + * Utility function that lets us take down selected property names from an interface + * and convert them to an array of property names on an object. + */ +function convertInterfacePropNamesToObjectPropNames( + objectDef: FetchedObjectTypeDefinition & { interfaceMap: {} }, + interfaceApiName: string, + ifacePropsToMap: readonly string[], +) { + return ifacePropsToMap.map((ifaceProp) => + objectDef.interfaceMap[interfaceApiName][ifaceProp] + ); +} + /** * Takes a raw object from the wire (contextually as an interface) and * updates the fields to reflect the underlying objectDef instead @@ -80,23 +136,10 @@ export async function convertWireToOsdkObjects( * @param rawObj */ function reframeAsObjectInPlace( - objectDef: FetchedObjectTypeDefinition, + objectDef: FetchedObjectTypeDefinition & { interfaceMap: {} }, interfaceApiName: string, - client: MinimalClient, rawObj: OntologyObjectV2, ) { - if (objectDef.interfaceMap?.[interfaceApiName] == null) { - const warning = - "Interfaces are only supported 'as views' but your metadata object is missing the correct information. This suggests your interfaces have not been migrated to the newer version yet and you cannot use this version of the SDK."; - if (client.logger) { - client.logger.warn(warning); - } else { - // eslint-disable-next-line no-console - console.error(`WARNING! ${warning}`); - } - throw new Error(warning); - } - const newProps: Record = {}; for ( const [sptProp, regularProp] of Object.entries( @@ -118,7 +161,52 @@ function reframeAsObjectInPlace( } } -function fixObjectPropertiesInline( +function isConforming( + client: MinimalClient, + def: + | InterfaceDefinition + | ObjectTypeDefinition, + obj: OntologyObjectV2, + propsToCheck: readonly string[], +) { + for (const propName of propsToCheck) { + if (def.properties[propName].nullable === false && obj[propName] == null) { + if (process.env.NODE_ENV !== "production") { + client.logger?.debug( + { + obj: { + $objectType: obj["$objectType"], + $primaryKey: obj["$primaryKey"], + }, + }, + `Found object that does not conform to its definition. Expected ${def.apiName}'s ${propName} to not be null.`, + ); + } + return false; + } + } + return true; +} + +function invariantInterfacesAsViews( + objectDef: FetchedObjectTypeDefinition, + interfaceApiName: string, + client: MinimalClient, +): asserts objectDef is typeof objectDef & { interfaceMap: {} } { + if (objectDef.interfaceMap?.[interfaceApiName] == null) { + const warning = + "Interfaces are only supported 'as views' but your metadata object is missing the correct information. This suggests your interfaces have not been migrated to the newer version yet and you cannot use this version of the SDK."; + if (client.logger) { + client.logger.warn(warning); + } else { + // eslint-disable-next-line no-console + console.error(`WARNING! ${warning}`); + } + throw new Error(warning); + } +} + +function fixObjectPropertiesInPlace( objs: OntologyObjectV2[], forceRemoveRid: boolean, ) { diff --git a/packages/client/src/object/convertWireToOsdkObjects/createOsdkObject.ts b/packages/client/src/object/convertWireToOsdkObjects/createOsdkObject.ts index 657e81d62..9a54689e3 100644 --- a/packages/client/src/object/convertWireToOsdkObjects/createOsdkObject.ts +++ b/packages/client/src/object/convertWireToOsdkObjects/createOsdkObject.ts @@ -17,6 +17,7 @@ import type { OntologyObjectV2 } from "@osdk/internal.foundry"; import type { MinimalClient } from "../../MinimalClientContext.js"; import type { FetchedObjectTypeDefinition } from "../../ontology/OntologyProvider.js"; +import type { Osdk } from "../../OsdkObjectFrom.js"; import { Attachment } from "../Attachment.js"; import { createClientCache } from "../Cache.js"; import { get$as } from "./getDollarAs.js"; @@ -70,7 +71,7 @@ export function createOsdkObject< client: MinimalClient, objectDef: Q, rawObj: OntologyObjectV2, -) { +): Osdk { // We use multiple layers of prototypes to maximize reuse and also to keep // [RawObject] out of `ownKeys`. This keeps the code in the proxy below simpler. const objectHolderPrototype = Object.create( diff --git a/packages/client/src/object/fetchPage.test.ts b/packages/client/src/object/fetchPage.test.ts index 9ec572d01..17374ba5e 100644 --- a/packages/client/src/object/fetchPage.test.ts +++ b/packages/client/src/object/fetchPage.test.ts @@ -44,7 +44,7 @@ describe(fetchPage, () => { L extends SelectArgToKeys, R extends A["$includeRid"] extends true ? true : false, >() { - return fetchPage({} as any, {} as any, {} as any); + return fetchPage({} as any, {} as any, {} as any); } } @@ -156,45 +156,58 @@ describe(fetchPage, () => { describe("includeRid", () => { it("properly returns the correct string for includeRid", () => { - expectTypeOf>>() + expectTypeOf>>() .toEqualTypeOf<{ data: Osdk[]; nextPageToken: string | undefined; }>(); - expectTypeOf>>() + const a: Awaited> = + 1 as any; + + expectTypeOf>>() .toEqualTypeOf<{ - data: Osdk[]; + data: Osdk[]; nextPageToken: string | undefined; }>(); }); it("works with $all", () => { - expectTypeOf>>() + expectTypeOf< + Awaited> + >() .toEqualTypeOf<{ data: Osdk[]; nextPageToken: string | undefined; }>(); - expectTypeOf>>() + expectTypeOf< + Awaited> + >() .toEqualTypeOf<{ data: Osdk[]; nextPageToken: string | undefined; }>(); - expectTypeOf>>() + expectTypeOf< + Awaited> + >() .toEqualTypeOf<{ data: Osdk[]; nextPageToken: string | undefined; }>(); - expectTypeOf>>() + expectTypeOf< + Awaited> + >() .toEqualTypeOf<{ data: Osdk[]; nextPageToken: string | undefined; }>(); - expectTypeOf>>() + expectTypeOf< + Awaited> + >() .toEqualTypeOf<{ data: Osdk[]; nextPageToken: string | undefined; diff --git a/packages/client/src/object/fetchPage.ts b/packages/client/src/object/fetchPage.ts index 973ca50b7..fd19680a2 100644 --- a/packages/client/src/object/fetchPage.ts +++ b/packages/client/src/object/fetchPage.ts @@ -34,7 +34,12 @@ import { OntologiesV2 } from "@osdk/internal.foundry"; import type { MinimalClient } from "../MinimalClientContext.js"; import { addUserAgent } from "../util/addUserAgent.js"; import { convertWireToOsdkObjects } from "./convertWireToOsdkObjects.js"; -import type { Augment, Augments, FetchPageArgs } from "./FetchPageArgs.js"; +import type { + Augment, + Augments, + FetchPageArgs, + NullabilityAdherence, +} from "./FetchPageArgs.js"; import type { FetchPageResult } from "./FetchPageResult.js"; import type { Result } from "./Result.js"; @@ -82,12 +87,13 @@ async function fetchInterfacePage< Q extends InterfaceDefinition, L extends ObjectOrInterfacePropertyKeysFrom2, R extends boolean, + S extends NullabilityAdherence, >( client: MinimalClient, interfaceType: Q, - args: FetchPageArgs, + args: FetchPageArgs, objectSet: ObjectSet, -): Promise> { +): Promise> { const result = await OntologiesV2.OntologyObjectsV2.searchObjectsForInterface( addUserAgent(client, interfaceType), client.ontologyRid, @@ -117,12 +123,13 @@ export async function fetchPageInternal< L extends ObjectOrInterfacePropertyKeysFrom2, R extends boolean, A extends Augments, + S extends NullabilityAdherence, >( client: MinimalClient, objectType: Q, objectSet: ObjectSet, - args: FetchPageArgs = {}, -): Promise> { + args: FetchPageArgs = {}, +): Promise> { if (objectType.type === "interface") { return await fetchInterfacePage(client, objectType, args, objectSet) as any; // fixme } else { @@ -136,12 +143,13 @@ export async function fetchPageWithErrorsInternal< L extends ObjectOrInterfacePropertyKeysFrom2, R extends boolean, A extends Augments, + S extends NullabilityAdherence, >( client: MinimalClient, objectType: Q, objectSet: ObjectSet, - args: FetchPageArgs = {}, -): Promise>> { + args: FetchPageArgs = {}, +): Promise>> { try { const result = await fetchPageInternal(client, objectType, objectSet, args); return { value: result }; @@ -157,15 +165,16 @@ export async function fetchPage< Q extends ObjectOrInterfaceDefinition, L extends ObjectOrInterfacePropertyKeysFrom2, R extends boolean, + S extends NullabilityAdherence, >( client: MinimalClient, objectType: Q, - args: FetchPageArgs, + args: FetchPageArgs, objectSet: ObjectSet = { type: "base", objectType: objectType["apiName"] as string, }, -): Promise> { +): Promise> { return fetchPageInternal(client, objectType, objectSet, args); } @@ -173,15 +182,16 @@ export async function fetchPageWithErrors< Q extends ObjectOrInterfaceDefinition, L extends ObjectOrInterfacePropertyKeysFrom2, R extends boolean, + S extends NullabilityAdherence, >( client: MinimalClient, objectType: Q, - args: FetchPageArgs, + args: FetchPageArgs, objectSet: ObjectSet = { type: "base", objectType: objectType["apiName"] as string, }, -): Promise>> { +): Promise>> { return fetchPageWithErrorsInternal(client, objectType, objectSet, args); } @@ -192,7 +202,7 @@ function applyFetchArgs< pageSize?: PageSize; }, >( - args: FetchPageArgs, + args: FetchPageArgs, body: X, ): X { if (args?.$nextPageToken) { @@ -219,12 +229,13 @@ export async function fetchObjectPage< Q extends ObjectTypeDefinition, L extends ObjectOrInterfacePropertyKeysFrom2, R extends boolean, + S extends NullabilityAdherence, >( client: MinimalClient, objectType: Q, - args: FetchPageArgs, + args: FetchPageArgs, objectSet: ObjectSet, -): Promise> { +): Promise> { const r = await OntologiesV2.OntologyObjectSets.loadObjectSetV2( addUserAgent(client, objectType), client.ontologyRid, @@ -241,7 +252,10 @@ export async function fetchObjectPage< client, r.data as OntologyObjectV2[], undefined, + undefined, + args.$select, + args.$__EXPERIMENTAL_strictNonNull, ), nextPageToken: r.nextPageToken, - }) as Promise>; + }) as Promise>; } diff --git a/packages/client/src/objectSet/ObjectSet.test.ts b/packages/client/src/objectSet/ObjectSet.test.ts index f8013bd53..87586e26d 100644 --- a/packages/client/src/objectSet/ObjectSet.test.ts +++ b/packages/client/src/objectSet/ObjectSet.test.ts @@ -15,7 +15,11 @@ */ import type { ObjectOrInterfacePropertyKeysFrom2 } from "@osdk/api"; -import { Employee, Ontology as MockOntology } from "@osdk/client.test.ontology"; +import { + Employee, + FooInterface, + Ontology as MockOntology, +} from "@osdk/client.test.ontology"; import { apiServer, stubData } from "@osdk/shared.test"; import { afterAll, @@ -25,6 +29,7 @@ import { expectTypeOf, it, } from "vitest"; +import type { InterfaceDefinition } from "../../../api/build/cjs/index.cjs"; import type { Client } from "../Client.js"; import { createClient } from "../createClient.js"; import type { Result } from "../object/Result.js"; @@ -207,7 +212,7 @@ describe("ObjectSet", () => { .fetchOneWithErrors(-1); expectTypeOf().toEqualTypeOf< - Result>> + Result> >; expect("error" in employeeResult); @@ -248,4 +253,175 @@ describe("ObjectSet", () => { expect(iter).toEqual(2); } }); + + describe.each(["fetchPage", "fetchPageWithErrors"] as const)("%s", (k) => { + describe("strictNonNull: \"drop\"", () => { + describe("includeRid: true", () => { + it("drops bad data", async () => { + const opts = { + $__EXPERIMENTAL_strictNonNull: "drop", + $includeRid: true, + } as const; + const result = k === "fetchPage" + ? await client(Employee).fetchPage(opts) + : (await client(Employee).fetchPageWithErrors(opts)).value!; + + expect(result.data).toHaveLength(3); + expectTypeOf(result.data[0]).toEqualTypeOf< + Osdk + >(); + }); + }); + + describe("includeRid: false", () => { + it("drops bad data", async () => { + const opts = { + $__EXPERIMENTAL_strictNonNull: "drop", + $includeRid: false, + } as const; + const result = k === "fetchPage" + ? await client(Employee).fetchPage(opts) + : (await client(Employee).fetchPageWithErrors(opts)).value!; + + expect(result.data).toHaveLength(3); + expectTypeOf(result.data[0]).toEqualTypeOf>(); + }); + }); + }); + + describe("strictNonNull: false", () => { + describe("includeRid: true", () => { + it("returns bad data", async () => { + const opts = { + $__EXPERIMENTAL_strictNonNull: false, + $includeRid: true, + } as const; + const result = k === "fetchPage" + ? await client(Employee).fetchPage(opts) + : (await client(Employee).fetchPageWithErrors(opts)).value!; + + expect(result.data).toHaveLength(4); + expectTypeOf(result.data[0]).toEqualTypeOf< + Osdk + >(); + }); + }); + + describe("includeRid: false", () => { + it("returns bad data", async () => { + const opts = { + $__EXPERIMENTAL_strictNonNull: false, + includeRid: false, + } as const; + const result = k === "fetchPage" + ? await client(Employee).fetchPage(opts) + : (await client(Employee).fetchPageWithErrors(opts)).value!; + + expect(result.data).toHaveLength(4); + expectTypeOf(result.data[0]).toEqualTypeOf< + Osdk + >(); + }); + }); + }); + }); + + describe("strictNonNull: \"throw\"", () => { + it("throws when getting bad data", () => { + expect(() => + client(Employee).fetchPage({ + $__EXPERIMENTAL_strictNonNull: "throw", + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Unable to safely convert objects as some non nullable properties are null]`, + ); + }); + }); + + describe.each(["fetchOne", "fetchOneWithErrors"] as const)("%s", (k) => { + describe("strictNonNull: false", () => { + describe("includeRid: true", () => { + it("returns bad data", async () => { + const opts = { + $__EXPERIMENTAL_strictNonNull: false, + $includeRid: true, + } as const; + const result = k === "fetchOne" + ? await client(Employee).fetchOne(50033, opts) + : (await client(Employee).fetchOneWithErrors(50033, opts)).value!; + + expect(result).not.toBeUndefined(); + expectTypeOf(result).toEqualTypeOf< + Osdk + >(); + }); + }); + + describe("includeRid: false", () => { + it("returns bad data", async () => { + const opts = { + $__EXPERIMENTAL_strictNonNull: false, + includeRid: false, + } as const; + const result = k === "fetchOne" + ? await client(Employee).fetchOne(50033, opts) + : (await client(Employee).fetchOneWithErrors(50033, opts)).value!; + + expect(result).not.toBeUndefined(); + expectTypeOf(result).toEqualTypeOf< + Osdk + >(); + }); + }); + }); + }); + + describe("strictNonNull: \"throw\"", () => { + it("throws when getting bad data", () => { + expect(() => + client(Employee).fetchPage({ + $__EXPERIMENTAL_strictNonNull: "throw", + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Unable to safely convert objects as some non nullable properties are null]`, + ); + }); + }); + + describe("conversions", () => { + describe("strictNonNull: false", () => { + it("returns bad data", async () => { + const result = await client(Employee).fetchPage({ + $__EXPERIMENTAL_strictNonNull: false, + }); + + const empNotStrict = result.data[0]; + + expectTypeOf(empNotStrict).toEqualTypeOf< + Osdk + >(); + expectTypeOf(empNotStrict.employeeId).toEqualTypeOf< + number | undefined + >(); + + // We don't have a proper definition that has + // a non-null property on an interface so + // we cheese it here to be sure the types work + type CheesedProp< + T extends InterfaceDefinition, + K extends keyof T["properties"], + > = T & { properties: { [KK in K]: { nullable: false } } }; + + type CheesedFoo = CheesedProp; + const CheesedFoo: CheesedFoo = FooInterface as CheesedFoo; + + const cheesedFooNotStrict = result.data[0].$as(CheesedFoo); + expectTypeOf(cheesedFooNotStrict).toEqualTypeOf< + Osdk + >(); + + cheesedFooNotStrict.fooSpt; + }); + }); + }); }); diff --git a/packages/client/src/objectSet/ObjectSet.ts b/packages/client/src/objectSet/ObjectSet.ts index e62e19193..e328efbf5 100644 --- a/packages/client/src/objectSet/ObjectSet.ts +++ b/packages/client/src/objectSet/ObjectSet.ts @@ -28,9 +28,14 @@ import type { AggregateOptsThatErrors } from "../object/AggregateOptsThatErrors. import type { Augments, FetchPageArgs, + NullabilityAdherence, + NullabilityAdherenceDefault, SelectArg, } from "../object/FetchPageArgs.js"; -import type { FetchPageResult } from "../object/FetchPageResult.js"; +import type { + FetchPageResult, + SingleOsdkResult, +} from "../object/FetchPageResult.js"; import type { Result } from "../object/Result.js"; import type { Osdk } from "../OsdkObjectFrom.js"; import type { AggregateOpts } from "../query/aggregations/AggregateOpts.js"; @@ -45,17 +50,19 @@ export interface MinimalObjectSet L extends ObjectOrInterfacePropertyKeysFrom2, R extends boolean, const A extends Augments, + S extends NullabilityAdherence = NullabilityAdherenceDefault, >( - args?: FetchPageArgs, - ) => Promise>; + args?: FetchPageArgs, + ) => Promise>; fetchPageWithErrors: < L extends ObjectOrInterfacePropertyKeysFrom2, R extends boolean, const A extends Augments, + S extends NullabilityAdherence = NullabilityAdherenceDefault, >( - args?: FetchPageArgs, - ) => Promise>>; + args?: FetchPageArgs, + ) => Promise>>; where: ( clause: WhereClause, @@ -94,17 +101,23 @@ export interface ObjectSet pivotTo: >(type: L) => ObjectSet>; - fetchOne: Q extends ObjectTypeDefinition - ? >( + fetchOne: Q extends ObjectTypeDefinition ? < + L extends ObjectOrInterfacePropertyKeysFrom2, + R extends boolean, + S extends false | "throw" = NullabilityAdherenceDefault, + >( primaryKey: PropertyValueClientToWire[Q["primaryKeyType"]], - options?: SelectArg, - ) => Promise> + options?: SelectArg, + ) => Promise> : never; - fetchOneWithErrors: Q extends ObjectTypeDefinition - ? >( + fetchOneWithErrors: Q extends ObjectTypeDefinition ? < + L extends ObjectOrInterfacePropertyKeysFrom2, + R extends boolean, + S extends false | "throw" = NullabilityAdherenceDefault, + >( primaryKey: PropertyValueClientToWire[Q["primaryKeyType"]], - options?: SelectArg, - ) => Promise>> + options?: SelectArg, + ) => Promise>> : never; } diff --git a/packages/client/src/objectSet/createObjectSet.ts b/packages/client/src/objectSet/createObjectSet.ts index 8d933ad06..957abc89c 100644 --- a/packages/client/src/objectSet/createObjectSet.ts +++ b/packages/client/src/objectSet/createObjectSet.ts @@ -141,7 +141,9 @@ export function createObjectSet( do { const result = await base.fetchPage({ $nextPageToken }); - for (const obj of result.data) { + for ( + const obj of result.data + ) { yield obj as Osdk; } } while ($nextPageToken != null); diff --git a/packages/client/src/query/aggregations/AggregationResultsWithGroups.ts b/packages/client/src/query/aggregations/AggregationResultsWithGroups.ts index 296af21b6..06969cd96 100644 --- a/packages/client/src/query/aggregations/AggregationResultsWithGroups.ts +++ b/packages/client/src/query/aggregations/AggregationResultsWithGroups.ts @@ -31,13 +31,8 @@ export type AggregationResultsWithGroups< & { $group: { [P in keyof G & keyof Q["properties"]]: G[P] extends - { $ranges: GroupByRange[] } - ? { startValue: number; endValue: number } - : G[P] extends { $ranges: GroupByRange[] } - ? { startValue: string; endValue: string } - : OsdkObjectPropertyType< - Q["properties"][P] - >; + { $ranges: GroupByRange[] } ? { startValue: T; endValue: T } + : OsdkObjectPropertyType; }; } & AggregationCountResult diff --git a/packages/foundry-sdk-generator/src/__e2e_tests__/loadObjects.test.ts b/packages/foundry-sdk-generator/src/__e2e_tests__/loadObjects.test.ts index 6a2b9716a..c79b81b42 100644 --- a/packages/foundry-sdk-generator/src/__e2e_tests__/loadObjects.test.ts +++ b/packages/foundry-sdk-generator/src/__e2e_tests__/loadObjects.test.ts @@ -267,7 +267,7 @@ describe("LoadObjects", () => { pageToken: employees.nextPageToken, }); const secondEmployeesPage = assertOkOrThrow(secondResult); - expect(secondEmployeesPage.data.length).toEqual(1); + expect(secondEmployeesPage.data.length).toEqual(2); expect(secondEmployeesPage.data[0].employeeId).toEqual(50032); }); @@ -277,10 +277,10 @@ describe("LoadObjects", () => { .objects.Employee.all(); const employees = assertOkOrThrow(result); for (const emp of employees) { - expect(emp.employeeId).toEqual(50030 + iter); + expect(emp.$primaryKey).toEqual(50030 + iter); iter += 1; } - expect(iter).toEqual(3); + expect(iter).toEqual(4); }); it("Gets All Objects with async iter", async () => { @@ -290,10 +290,10 @@ describe("LoadObjects", () => { .objects.Employee.asyncIter(), ); for (const emp of employees) { - expect(emp.employeeId).toEqual(50030 + iter); + expect(emp.$primaryKey).toEqual(50030 + iter); iter += 1; } - expect(iter).toEqual(3); + expect(iter).toEqual(4); }); it("Links with a cardinality of ONE are loaded properly", async () => { diff --git a/packages/foundry-sdk-generator/src/__e2e_tests__/objectSet.test.ts b/packages/foundry-sdk-generator/src/__e2e_tests__/objectSet.test.ts index 2a89d91b1..a55707284 100644 --- a/packages/foundry-sdk-generator/src/__e2e_tests__/objectSet.test.ts +++ b/packages/foundry-sdk-generator/src/__e2e_tests__/objectSet.test.ts @@ -61,10 +61,14 @@ describe("Object Sets", () => { const employeesPage = assertOkOrThrow(result); const employees = employeesPage.data; for (const emp of employees) { - expect(emp.employeeId).toEqual(50030 + iter); + expect(emp.$primaryKey).toEqual(50030 + iter); + if (emp.employeeId) { + // one of the objects is invalid intentionally, so we don't want to check it + expect(emp.employeeId).toEqual(50030 + iter); + } iter += 1; } - expect(iter).toEqual(3); + expect(iter).toEqual(4); }); it("objects set union", async () => { @@ -214,7 +218,7 @@ describe("Object Sets", () => { it("object set page gets all elements", async () => { const objectSet: ObjectSet = client.ontology.objects.Employee; const pageResults = await fetchAllPages(objectSet, 1); - expect(pageResults.length).toEqual(3); + expect(pageResults.length).toEqual(4); }); it("object set page respects page size", async () => { diff --git a/packages/shared.test/src/stubs/objectSetRequest.ts b/packages/shared.test/src/stubs/objectSetRequest.ts index 7c181135a..24d382bc2 100644 --- a/packages/shared.test/src/stubs/objectSetRequest.ts +++ b/packages/shared.test/src/stubs/objectSetRequest.ts @@ -23,6 +23,7 @@ import { employee1, employee2, employee3, + employeeFailsStrict, nycOffice, objectWithAllPropertyTypes1, objectWithAllPropertyTypesEmptyEntries, @@ -93,6 +94,19 @@ const eqSearchBody: LoadObjectSetRequestV2 = { select: [], }; +const eqSearchBodyBadObject: LoadObjectSetRequestV2 = { + objectSet: { + type: "filter", + objectSet: { type: "base", objectType: employeeObjectType.apiName }, + where: { + type: "eq", + field: "employeeId", + value: 50033, + }, + }, + select: [], +}; + const eqSearchBodyWithSelect: LoadObjectSetRequestV2 = { objectSet: { type: "filter", @@ -409,11 +423,17 @@ const employee2ToToEmployee1PeepByPk: LoadObjectSetRequestV2 = { export const loadObjectSetRequestHandlers: { [key: string]: LoadObjectSetResponseV2["data"]; } = { - [stableStringify(baseObjectSet)]: [employee1, employee2, employee3], + [stableStringify(baseObjectSet)]: [ + employee1, + employee2, + employee3, + employeeFailsStrict, + ], [stableStringify(unionedObjectSet)]: [employee1, employee2], [stableStringify(intersectedObjectSet)]: [employee3], [stableStringify(subtractedObjectSet)]: [employee2, employee3], [stableStringify(eqSearchBody)]: [employee1], + [stableStringify(eqSearchBodyBadObject)]: [employeeFailsStrict], [stableStringify(eqSearchBodyWithSelect)]: [employee1], [stableStringify(andSearchBody)]: [employee2], [stableStringify(geoPointSearchBody)]: [nycOffice], diff --git a/packages/shared.test/src/stubs/objects.ts b/packages/shared.test/src/stubs/objects.ts index b0ed0e005..c23c270ac 100644 --- a/packages/shared.test/src/stubs/objects.ts +++ b/packages/shared.test/src/stubs/objects.ts @@ -55,6 +55,19 @@ export const employee3 = { employeeStatus: "TimeSeries", }; +export const employeeFailsStrict = { + __rid: + "ri.phonograph2-objects.main.object.b9a0b2b0-0a2b-0b8b-9e4b-a9a9b9a0b9a0", + __primaryKey: 50033, + __apiName: "Employee", + employeeId: undefined, + fullName: "Jack Smith", + office: "LON", + class: "Red", + startDate: "2015-05-15", + employeeStatus: "TimeSeries", +}; + export const officeAreaGeoJson: GeoJsonObject = { coordinates: [ [ @@ -170,6 +183,7 @@ export const objectLoadResponseMap: { [employee1.__primaryKey.toString()]: employee1, [employee2.__primaryKey.toString()]: employee2, [employee3.__primaryKey.toString()]: employee3, + [employeeFailsStrict.__primaryKey.toString()]: employeeFailsStrict, }, Office: { [nycOffice.__primaryKey.toString()]: nycOffice,