Skip to content

Commit

Permalink
computed view endpoint for api client
Browse files Browse the repository at this point in the history
  • Loading branch information
mkobetic committed Nov 12, 2024
1 parent 798fe22 commit 937b7b8
Show file tree
Hide file tree
Showing 10 changed files with 402 additions and 40 deletions.
5 changes: 4 additions & 1 deletion packages/api-client-core/spec/Select-type.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { AssertTrue, IsExact } from "conditional-type-checks";
import type { DeepFilterNever, Select } from "../src/types.js";
import type { DeepFilterNever, Select, Select2 } from "../src/types.js";
import type { TestSchema } from "./TestSchema.js";

describe("Select<>", () => {
type selectAny = Select2<{ a: "thing" }, any>;
type _SelectingAnyYieldsNever = AssertTrue<IsExact<selectAny, never>>;

type _SelectingProperties = AssertTrue<IsExact<Select<TestSchema, { num: true }>, { num: number }>>;

type _ConditionallySelectingProperties = AssertTrue<
Expand Down
4 changes: 2 additions & 2 deletions packages/api-client-core/spec/TestSchema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { GadgetRecord } from "src/index.js";
import type { AvailableSelection, DeepFilterNever, DefaultSelection, Select, Selectable } from "../src/types.js";
import type { AvailableSelection2, DeepFilterNever, DefaultSelection, Select, Selectable } from "../src/types.js";

export type NestedThing = {
bool: boolean;
Expand Down Expand Up @@ -48,7 +48,7 @@ export type TestSchema = {
};
};

export type AvailableTestSchemaSelection = AvailableSelection<TestSchema>;
export type AvailableTestSchemaSelection = AvailableSelection2<TestSchema>;

export const DefaultPostSelection = {
__typename: true,
Expand Down
95 changes: 87 additions & 8 deletions packages/api-client-core/spec/default-selection.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,90 @@
import type { AssertTrue, IsExact } from "conditional-type-checks";
import type { DefaultSelection } from "../src/types.js";
import type { AvailableTestSchemaSelection } from "./TestSchema.js";
import type { AssertTrue, Has, IsExact } from "conditional-type-checks";
import type { FieldSelection } from "src/FieldSelection.js";
import type { ComputedViewFunctionWithVariables, ComputedViewFunctionWithoutVariables } from "src/GadgetFunctions.js";
import type { AllFieldsSelected, AvailableSelection2, DefaultSelection, DefaultSelection2 } from "../src/types.js";
import type { AvailableTestSchemaSelection, TestSchema } from "./TestSchema.js";
import type { ExpandRecursively } from "./helpers.js";

type _NullDefault = DefaultSelection<AvailableTestSchemaSelection, { select: null }, { num: true }>;
type _TestDefaultsNullToTheDefault = AssertTrue<IsExact<_NullDefault, { num: true }>>;
test("DefaultSelection", () => {
type nullDefault = DefaultSelection<AvailableTestSchemaSelection, { select: null }, { num: true }>;
type _TestDefaultsNullToTheDefault = AssertTrue<IsExact<nullDefault, { num: true }>>;

type _NonNullDefault = DefaultSelection<AvailableTestSchemaSelection, { select: { num: false; str: true } }, { num: true }>;
type _TestRespectsTruthySelections = AssertTrue<IsExact<_NonNullDefault, { num: false; str: true }>>;
type undefinedDefault = DefaultSelection<AvailableTestSchemaSelection, { select: undefined }, { num: true }>;
type _TestDefaultsUndefinedToTheDefault = AssertTrue<IsExact<undefinedDefault, { num: true }>>;

test("true", () => undefined);
// eslint-disable-next-line @typescript-eslint/ban-types
type undefinedDefault2 = DefaultSelection<AvailableTestSchemaSelection, {}, { num: true }>;
type _TestDefaultsUndefinedToTheDefault2 = AssertTrue<IsExact<undefinedDefault2, { num: true }>>;

type anyDefault = DefaultSelection2<AvailableTestSchemaSelection, { select: any }, { num: true }>;
type _TestDefaultsAnyToTheDefault = AssertTrue<IsExact<anyDefault, { num: true }>>;

type nonDefault = DefaultSelection<AvailableTestSchemaSelection, { select: { num: false; str: true } }, { num: true }>;
type _TestRespectsTruthySelections = AssertTrue<IsExact<nonDefault, { num: false; str: true }>>;

type nonDefault2 = DefaultSelection<AvailableTestSchemaSelection, { select: { num: false } }, { num: true }>;
type _TestRespectsTruthySelections2 = AssertTrue<IsExact<nonDefault2, { num: false }>>;
});

test("AvailableSelection", () => {
type availableTestSchemaSelection = AvailableSelection2<TestSchema>;
type _TestAvailableTestSchemaSelection = Has<FieldSelection, availableTestSchemaSelection>;

type testType = { a: number; b: { c: string; d: { e: boolean } } };
type availableSelection = AvailableSelection2<testType>;
type _TestAvailableSelection = AssertTrue<
IsExact<
availableSelection,
{
a?: boolean | null | undefined;
b?:
| {
c?: boolean | null | undefined;
d?:
| {
e?: boolean | null | undefined;
}
| undefined;
}
| undefined;
}
>
>;
});

test("AllFieldsSelected", () => {
type testType = { a: number; b: { c: string; d: { e: boolean } } };
type availableSelection = AvailableSelection2<testType>;
type allFieldsSelected = ExpandRecursively<AllFieldsSelected<availableSelection>>;
type _TestAllFieldsSelected = AssertTrue<IsExact<allFieldsSelected, { a: true; b: { c: true; d: { e: true } } }>>;
});

test("ComputedViewFunction", () => {
type testType = { a: number; b: { c: string; d: { e: boolean } } };

const f: ComputedViewFunctionWithoutVariables<testType> = () => ({} as any);

const resultWithDefaultSelection = f();
type _TestResultWithDefaultSelection = AssertTrue<IsExact<Awaited<typeof resultWithDefaultSelection>, testType>>;

const resultWithDefaultSelection2 = f({});
type _TestResultWithDefaultSelection2 = AssertTrue<IsExact<Awaited<typeof resultWithDefaultSelection2>, testType>>;

const resultWithDefaultSelection3 = f({ select: null });
type _TestResultWithDefaultSelection3 = AssertTrue<IsExact<Awaited<typeof resultWithDefaultSelection3>, testType>>;

const resultWithSelection = f({ select: { a: true } });
type _TestResultWithSelection = AssertTrue<IsExact<Awaited<typeof resultWithSelection>, { a: number }>>;

const resultWithSelection2 = f({ select: { a: false, b: { c: true } } });
type _TestResultWithSelection2 = AssertTrue<IsExact<Awaited<typeof resultWithSelection2>, { b: { c: string } }>>;

type vars = { n?: number; b: boolean };
const f2: ComputedViewFunctionWithVariables<vars, testType> = () => ({} as any);

const resultWithDefaultSelection4 = f2({ b: false });
type _TestResultWithDefaultSelection4 = AssertTrue<IsExact<Awaited<typeof resultWithDefaultSelection4>, testType>>;

const resultWithSelection5 = f({ select: { a: false, b: { c: true } } });
type _TestResultWithSelection5 = AssertTrue<IsExact<Awaited<typeof resultWithSelection5>, { b: { c: string } }>>;
});
16 changes: 16 additions & 0 deletions packages/api-client-core/spec/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,19 @@ export const waitForExpectationToPass = async (run: () => void | Promise<void>,
})
);
};

/**
* Debugging type that will display a fully resolved type
* in Intellisense instead of just the type aliases
*
* @type {T} The type to expand out
*/
export type ExpandRecursively<T> = T extends (...args: infer A) => infer R
? (...args: ExpandRecursively<A>) => ExpandRecursively<R>
: T extends Array<infer E>
? ExpandRecursively<E>[]
: T extends object
? T extends infer O
? { [K in keyof O]: ExpandRecursively<O[K]> }
: never
: T;
86 changes: 86 additions & 0 deletions packages/api-client-core/spec/operationBuilders.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
actionOperation,
backgroundActionResultOperation,
computedViewOperation,
enqueueActionOperation,
findManyOperation,
findOneByFieldOperation,
Expand Down Expand Up @@ -1491,4 +1492,89 @@ describe("operation builders", () => {
`);
});
});
describe("computedViewOperation", () => {
test("global view without variables", () => {
expect(computedViewOperation("boom", { a: true, b: true })).toMatchInlineSnapshot(`
{
"query": "query boom {
boom {
a
b
__typename
}
}",
"variables": {},
}
`);
expect(computedViewOperation("boom", { a: true, b: true }, {}, { a: true })).toMatchInlineSnapshot(`
{
"query": "query boom {
boom {
a
__typename
}
}",
"variables": {},
}
`);
expect(computedViewOperation("boom", { a: true, b: true }, undefined, { a: true })).toMatchInlineSnapshot(`
{
"query": "query boom {
boom {
a
__typename
}
}",
"variables": {},
}
`);
});

test("global view without selection", () => {
expect(
computedViewOperation(
"boom",
{ a: true, b: true },
{ a: { required: false, type: "Int", value: 42 }, b: { required: false, type: "String", value: "fortytwo" } }
)
).toMatchInlineSnapshot(`
{
"query": "query boom($a: Int, $b: String) {
boom(a: $a, b: $b) {
a
b
__typename
}
}",
"variables": {
"a": 42,
"b": "fortytwo",
},
}
`);
});
test("global view with selection", () => {
expect(
computedViewOperation(
"boom",
{ a: true, b: true },
{ a: { required: false, type: "Int", value: 42 }, b: { required: false, type: "String", value: "fortytwo" } },
{ a: true }
)
).toMatchInlineSnapshot(`
{
"query": "query boom($a: Int, $b: String) {
boom(a: $a, b: $b) {
a
__typename
}
}",
"variables": {
"a": 42,
"b": "fortytwo",
},
}
`);
});
});
});
21 changes: 21 additions & 0 deletions packages/api-client-core/spec/operationRunners.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
GadgetConnection,
actionRunner,
backgroundActionResultRunner,
computedViewRunner,
enqueueActionRunner,
findManyRunner,
findOneByFieldRunner,
Expand Down Expand Up @@ -2040,4 +2041,24 @@ describe("operationRunners", () => {
});
});
});
describe("computedViewRunner", () => {
test("global view", () => {
const _promise = computedViewRunner(
connection,
"boom",
{ a: true, b: true },
{ a: { required: false, type: "Int", value: 42 }, b: { required: false, type: "String", value: "fortytwo" } },
{ a: true }
);

expect(query).toMatchInlineSnapshot(`
"query boom($a: Int, $b: String) {
boom(a: $a, b: $b) {
a
__typename
}
}"
`);
});
});
});
63 changes: 62 additions & 1 deletion packages/api-client-core/src/GadgetFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { FieldSelection } from "./FieldSelection.js";
import type { GadgetRecord, RecordShape } from "./GadgetRecord.js";
import type { GadgetRecordList } from "./GadgetRecordList.js";
import type { LimitToKnownKeys, VariablesOptions } from "./types.js";
import type { AllFieldsSelected, AvailableSelection2, DefaultSelection2, LimitToKnownKeys, Select2, VariablesOptions } from "./types.js";

export type PromiseOrLiveIterator<T> = Promise<T> | AsyncIterable<T>;
export type AsyncRecord<T extends RecordShape> = PromiseOrLiveIterator<GadgetRecord<T>>;
Expand Down Expand Up @@ -206,3 +207,63 @@ export interface GlobalActionFunction<VariablesT> {

export type AnyActionFunction = ActionFunctionMetadata<any, any, any, any, any, any> | GlobalActionFunction<any>;
export type AnyBulkActionFunction = ActionFunctionMetadata<any, any, any, any, any, true>;

// This is a function that represents a computed view that doesn't take any input parameters/variables.
// Result is an explicit type parameter defining the shape of the full result.
export type ComputedViewFunctionWithoutVariables<Result> =
// Available, Options and Defaults are inferred at call time.
<
// Available is the full FieldSelection type derived from the shape of the Result type, i.e. all possible selections.
Available extends AvailableSelection2<Result> & FieldSelection,
// Options holds the actual selection at call time.
Options extends { select?: Available | null },
// Defaults is the default selection to be used when one is not provided at call time,
// for views we default to everything being selected.
Defaults extends AllFieldsSelected<Available>
>(
options?: Options
) => Promise<Select2<Result, DefaultSelection2<Available, Options, Defaults>>>;

// Represents a computed view that doesn't take any input parameters/variables.
// It includes the view function and the view metadata.
export interface ComputedViewWithoutVariables<Result> extends ComputedViewFunctionWithoutVariables<Result> {
type: "computedView";
operationName: string;
namespace: string | string[] | null;
defaultSelection: FieldSelection;
selection?: FieldSelection;
selectionType: AvailableSelection2<Result>;
resultType: Result;
}

// This is a function that represents a computed view that takes input parameters/variables.
// Result is an explicit type parameter defining the shape of the full result.
// Variables is an explicit type parameter that describes the shape of the variables parameter.
export type ComputedViewFunctionWithVariables<Variables, Result> =
// Available, Options and Defaults are inferred at call time.
<
// Available is the full FieldSelection type derived from the shape of the Result type, i.e. all possible selections.
Available extends AvailableSelection2<Result> & FieldSelection,
// Options holds the actual selection at call time.
Options extends { select?: Available | null },
// Defaults is the default selection to be used when one is not provided at call time,
// for views we default to everything being selected.
Defaults extends AllFieldsSelected<Available>
>(
variables: Variables,
options?: Options
) => Promise<Select2<Result, DefaultSelection2<Available, Options, Defaults>>>;

// Represents a computed view that takes input parameters/variables.
// It includes the view function and the view metadata.
export interface ComputedViewWithVariables<Variables, Result> extends ComputedViewFunctionWithVariables<Variables, Result> {
type: "computedView";
operationName: string;
namespace: string | string[] | null;
variables: VariablesOptions;
variablesType: Variables;
defaultSelection: FieldSelection;
selection?: FieldSelection;
selectionType: AvailableSelection2<Result>;
resultType: Result;
}
25 changes: 24 additions & 1 deletion packages/api-client-core/src/operationBuilders.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FieldSelection as BuilderFieldSelection, BuilderOperation, Variable } from "tiny-graphql-query-compiler";
import { Call, Var, compileWithVariableValues } from "tiny-graphql-query-compiler";
import { Call, Var, compile, compileWithVariableValues } from "tiny-graphql-query-compiler";
import type { FieldSelection } from "./FieldSelection.js";
import type { AnyActionFunction, HasReturnType } from "./index.js";
import {
Expand Down Expand Up @@ -286,6 +286,29 @@ export const globalActionOperation = (
});
};

export const computedViewOperation = (
operation: string,
defaultSelection: FieldSelection,
variables?: VariablesOptions,
selection?: FieldSelection,
namespace?: string | string[] | null
) => {
let fields = {
[operation]: Call(
variables ? variableOptionsToVariables(variables) : {},
fieldSelectionToQueryCompilerFields(selection ?? defaultSelection, true)
),
};

if (namespace) {
fields = namespacify(namespace, fields);
}

return variables
? compileWithVariableValues({ type: "query", name: operation, fields })
: { query: compile({ type: "query", name: operation, fields }), variables: {} };
};

export interface GraphQLBackgroundActionOptions {
retries?: { retryCount: number };
queue?: { name: string; maxConcurrency?: number };
Expand Down
Loading

0 comments on commit 937b7b8

Please sign in to comment.