From d5fb12f338554b6f42982da223adb396cd3d78d2 Mon Sep 17 00:00:00 2001 From: Tim Kendall Date: Sun, 12 Sep 2021 19:31:25 -0700 Subject: [PATCH] Implement Selector and selector APIs --- src/Query.ts | 46 ++++++++++++++++++ src/Selector.ts | 61 ++++++++++++++++++++++++ src/__tests__/Selector.test.ts | 86 ++++++++++++++++++++++++++++++++++ starwars.example.ts | 6 +-- 4 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 src/Query.ts create mode 100644 src/Selector.ts create mode 100644 src/__tests__/Selector.test.ts diff --git a/src/Query.ts b/src/Query.ts new file mode 100644 index 0000000..b4a80dd --- /dev/null +++ b/src/Query.ts @@ -0,0 +1,46 @@ +import { ISelector } from "./Selector"; + +// export const operation = >( +// type: "query" | "mutation" | "subscription", +// selector: S +// ) => >( +// name: string, +// select: (t: S) => T +// ): Operation> => +// new Operation(name, type, new SelectionSet(select(selector))); + +// ``` +// // or new Query({ ... }) +// const operation = query({ +// name: 'Example', +// variables: { +// id: t.string, +// }, +// selection: (t, v) => [ +// t.user({ id: v.id }, t => [ +// t.id, +// t.name, +// t.friends(t => [ +// t.id, +// t.firstName, +// ]) +// ]) +// ] +// extensions: [] +// }) +// ``` + +export interface QueryConfig { + name?: string; + variables?: Record; + selection: ISelector; + // extensions?: Array +} + +export class Query { + // ast?: DocumentNode or OperationDefinitionNode + + constructor(config: QueryConfig) {} + + // toString() +} diff --git a/src/Selector.ts b/src/Selector.ts new file mode 100644 index 0000000..3e6dc53 --- /dev/null +++ b/src/Selector.ts @@ -0,0 +1,61 @@ +import { + Field, + SelectionSet, + Selection, + Argument, + Primitive, +} from "./Operation"; + +export type ISelector = { + // @todo use Paramaters to support variables + [F in keyof T]: T[F] extends Primitive + ? (variables?: Record) => Field + : >( + arg0?: ((selector: ISelector) => S) | Record | never, + arg1?: ((selector: ISelector) => S) | never + ) => Field>; +}; + +export class Selector { + constructor(public readonly select: (t: ISelector) => Array) {} + + toSelectionSet() { + return new SelectionSet(this.select(selector())); + } +} + +export const selector = (): ISelector => + new Proxy(Object.create(null), { + get(target, field) /*: FieldFn*/ { + return function fieldFn(...args: any[]) { + if (args.length === 0) { + return new Field(field); + } else if (typeof args[0] === "function") { + return new Field( + field, + undefined, + new SelectionSet(args[0](selector())) + ); + } else if (typeof args[0] === "object") { + if (typeof args[1] === "function") { + return new Field( + field, + Object.entries(args[0]).map( + ([name, value]) => new Argument(name, value) + ), + new SelectionSet(args[1](selector())) + ); + } else { + return new Field( + field, + Object.entries(args[0]).map( + ([name, value]) => new Argument(name, value) + ) + ); + } + } + + throw new Error(`Unable to derive field function from arguments.`); + }; + }, + }); diff --git a/src/__tests__/Selector.test.ts b/src/__tests__/Selector.test.ts new file mode 100644 index 0000000..884322a --- /dev/null +++ b/src/__tests__/Selector.test.ts @@ -0,0 +1,86 @@ +import { print } from "graphql"; + +import { selector, Selector } from "../Selector"; +import { SelectionSet } from "../Operation"; +import { Result } from "../Result"; + +describe("Selector", () => { + describe("type-saftey", () => { + it("supports selecting against an interface", () => { + interface Query { + foo: string; + bar: number; + baz: { + id: string; + }; + } + + const { foo, bar, baz } = selector(); + + const selection = [foo(), bar(), baz((t) => [t.id()])]; + type ExampleResult = Result>; + + const query = print(new SelectionSet(selection).ast); + + expect(query).toMatchInlineSnapshot(` + "{ + foo + bar + baz { + id + } + }" + `); + }); + }); + + describe("class API", () => { + it("supports arbitrarily deep seletions", () => { + const selector = new Selector((t) => [ + t.user({ id: "foo" }, (t) => [t.id()]), + ]); + + const query = print(selector.toSelectionSet().ast); + + expect(query).toMatchInlineSnapshot(` + "{ + user(id: \\"foo\\") { + id + } + }" + `); + }); + }); + + describe("function API", () => { + it("supports arbitrarily deep selections", () => { + const { authors, user } = selector(); + + const selection = [ + authors((t) => [t.name(), t.address((t) => [t.country()])]), + + user({ id: "foo" }, (t) => [t.foo((t) => [t.bar((t) => [t.baz()])])]), + ]; + + const query = print(new SelectionSet(selection).ast); + + expect(query).toMatchInlineSnapshot(` + "{ + authors { + name + address { + country + } + } + user(id: \\"foo\\") { + foo { + bar { + baz + } + } + } + }" + `); + }); + }); +}); diff --git a/starwars.example.ts b/starwars.example.ts index a694803..123d175 100644 --- a/starwars.example.ts +++ b/starwars.example.ts @@ -1,7 +1,7 @@ import { Executor } from "./src"; import { Starwars } from "./starwars.api"; (async () => { - const starwars = new Starwars(new Executor("")); + const starwars = new Starwars(new Executor({ uri: "" })); const character = await starwars.query.character({ id: "frf" }, (t) => [ t.__typename(), @@ -11,9 +11,7 @@ import { Starwars } from "./starwars.api"; t.friends((t) => [t.id()]), ]), - // t.on('Droid', t => [ - // t.primaryFunction(), - // ]) + t.on("Droid", (t) => [t.primaryFunction()]), ]); character.data.character.__typename;