From 8af4b8ae94086f9b22c89fac686cd40b19519df5 Mon Sep 17 00:00:00 2001 From: Miguel Albernaz Date: Fri, 19 Nov 2021 18:32:28 -0300 Subject: [PATCH] New tests --- packages/bindings/.babelrc | 7 - packages/bindings/.babelrc.json | 22 ++ packages/bindings/__tests__/index.ts | 365 ++++++++++--------------- packages/bindings/package.json | 3 +- packages/bindings/schema.graphql | 269 +++++++++++++++++- packages/bindings/src/bindings.ts | 12 +- packages/bindings/src/types.ts | 11 +- packages/test-utils/src/mock-server.ts | 3 +- 8 files changed, 444 insertions(+), 248 deletions(-) delete mode 100644 packages/bindings/.babelrc create mode 100644 packages/bindings/.babelrc.json diff --git a/packages/bindings/.babelrc b/packages/bindings/.babelrc deleted file mode 100644 index ba0dd976..00000000 --- a/packages/bindings/.babelrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "presets": [ - ["@babel/preset-env", { "targets": { "node": "current" } }], - "@babel/preset-typescript" - ], - "plugins": [["module:@grafoo/babel-plugin", { "schema": "schema.graphql", "idFields": ["id"] }]] -} diff --git a/packages/bindings/.babelrc.json b/packages/bindings/.babelrc.json new file mode 100644 index 00000000..c8010361 --- /dev/null +++ b/packages/bindings/.babelrc.json @@ -0,0 +1,22 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "current" + } + } + ], + "@babel/preset-typescript" + ], + "plugins": [ + [ + "module:@grafoo/babel-plugin", + { + "schema": "schema.graphql", + "idFields": ["id"] + } + ] + ] +} diff --git a/packages/bindings/__tests__/index.ts b/packages/bindings/__tests__/index.ts index 0c4fc641..04ce2e94 100644 --- a/packages/bindings/__tests__/index.ts +++ b/packages/bindings/__tests__/index.ts @@ -1,160 +1,112 @@ -import createBindings from "../src"; -import fetch from "node-fetch"; +import createBindings, { makeGrafooConfig } from "../src"; import graphql from "@grafoo/core/tag"; import createClient, { GrafooClient } from "@grafoo/core"; -import { mockQueryRequest, createTransport } from "@grafoo/test-utils"; - -// @ts-ignore -globalThis.fetch = fetch; - -type Post = { - title: string; - content: string; - id: string; - __typename?: string; - author: Author; -}; - -type Author = { - name: string; - id: string; - __typename?: string; - posts?: Array; -}; - -type AuthorsQuery = { - authors: Author[]; -}; +import { + mockQueryRequest, + createTransport, + Query, + Mutation, + CreateAuthorInput, + DeleteAuthorInput, + UpdateAuthorInput +} from "@grafoo/test-utils"; + +type AuthorsQuery = Pick; let AUTHORS = graphql` query { authors { - name - posts { - title - body - } - } - } -`; - -type AuthorQuery = { - author: Author; -}; - -type AuthorQueryInput = { - id: string; -}; - -let AUTHOR = graphql` - query ($id: ID!) { - author(id: $id) { - name - posts { - title - body + edges { + node { + name + posts { + edges { + node { + body + title + } + } + } + } } } } `; -type PostsQuery = { - posts: Post[]; -}; - -type PostsAndAuthorsQuery = AuthorsQuery & PostsQuery; +type PostsAndAuthorsQuery = Pick; let POSTS_AND_AUTHORS = graphql` query { posts { - title - body - author { - name + edges { + node { + title + body + author { + name + } + } } } authors { - name - posts { - title - body + edges { + node { + name + posts { + edges { + node { + body + title + } + } + } + } } } } `; -type CreateAuthorMutation = { - createAuthor: { - name: string; - id: string; - __typename: string; - posts?: Array; - }; +type CreateAuthorMutation = Pick; +type CreateAuthorMutationVariables = { + input: CreateAuthorInput; }; -type CreateAuthorInput = { - name: string; -}; - -let CREATE_AUTHOR = graphql` - mutation ($name: String!) { - createAuthor(name: $name) { - name - posts { - title - body +let CREATE_AUTHOR = graphql` + mutation ($input: CreateAuthorInput!) { + createAuthor(input: $input) { + author { + name } } } `; -type DeleteAuthorMutation = { - deleteAuthor: { - name: string; - id: string; - __typename: string; - posts?: Array; - }; +type DeleteAuthorMutation = Pick; +type DeleteAuthorMutationVariables = { + input: DeleteAuthorInput; }; -type DeleteAuthorInput = { - id: string; -}; - -let DELETE_AUTHOR = graphql` - mutation ($id: ID!) { - deleteAuthor(id: $id) { - name - posts { - title - body +let DELETE_AUTHOR = graphql` + mutation ($input: DeleteAuthorInput!) { + deleteAuthor(input: $input) { + author { + name } } } `; -type UpdateAuthorMutation = { - updateAuthor: { - name: string; - id: string; - __typename: string; - posts?: Array; - }; -}; - -type UpdateAuthorInput = { - id?: string; - name?: string; +type UpdateAuthorMutation = Pick; +type UpdateAuthorMutationVariables = { + input: UpdateAuthorInput; }; -let UPDATE_AUTHOR = graphql` - mutation ($id: ID!, $name: String) { - updateAuthor(id: $id, name: $name) { - name - posts { - title - body +let UPDATE_AUTHOR = graphql` + mutation ($input: UpdateAuthorInput!) { + updateAuthor { + author { + name } } } @@ -181,36 +133,28 @@ describe("@grafoo/bindings", () => { it("should not provide any data if no query or mutation is given", () => { let bindings = createBindings(client, () => {}, {}); + let state = bindings.getState(); - let props = bindings.getState(); - - expect(props).toEqual({ loaded: false, loading: false }); + expect(state).toEqual({ loaded: false, loading: false }); }); it("should execute a query", async () => { let { data } = await mockQueryRequest(AUTHORS); - let renderFn = jest.fn(); - let bindings = createBindings(client, renderFn, { query: AUTHORS }); - expect(bindings.getState()).toEqual({ loaded: false, loading: false }); + expect(bindings.getState()).toEqual({ loaded: false, loading: true }); await bindings.load(); let results = renderFn.mock.calls.map((c) => c[0]); - expect(results).toEqual([ - { loaded: false, loading: true }, - { ...data, loaded: true, loading: false } - ]); + expect(results).toEqual([{ ...data, loaded: true, loading: false }]); }); it("should notify a loading state", async () => { let { data } = await mockQueryRequest(AUTHORS); - let renderFn = jest.fn(); - let bindings = createBindings(client, renderFn, { query: AUTHORS }); await bindings.load(); @@ -219,7 +163,6 @@ describe("@grafoo/bindings", () => { let results = renderFn.mock.calls.map((c) => c[0]); expect(results).toEqual([ - { loaded: false, loading: true }, { loaded: true, loading: false, ...data }, { loaded: true, loading: true, ...data }, { loaded: true, loading: false, ...data } @@ -243,20 +186,7 @@ describe("@grafoo/bindings", () => { let bindings = createBindings(client, () => {}, { query: POSTS_AND_AUTHORS }); - expect(bindings.getState()).toEqual({ ...data, loaded: false, loading: false }); - }); - - it("should trigger updater function if the cache has been updated", async () => { - let { data } = await mockQueryRequest(AUTHORS); - - let renderFn = jest.fn(); - - createBindings(client, renderFn, { query: AUTHORS }); - - client.write(AUTHORS, data); - - expect(renderFn).toHaveBeenCalledTimes(1); - expect(renderFn).toHaveBeenCalledWith({ ...data, loaded: true, loading: false }); + expect(bindings.getState()).toEqual({ ...data, loaded: false, loading: true }); }); it("should provide the state for a cached query", async () => { @@ -265,7 +195,6 @@ describe("@grafoo/bindings", () => { client.write(AUTHORS, data); let renderFn = jest.fn(); - let bindings = createBindings(client, renderFn, { query: AUTHORS }); expect(bindings.getState()).toEqual({ ...data, loaded: true, loading: false }); @@ -273,36 +202,28 @@ describe("@grafoo/bindings", () => { it("should stop updating if unbind has been called", async () => { let { data } = await mockQueryRequest(AUTHORS); - - let renderFn = jest.fn(); - - let bindings = createBindings(client, renderFn, { query: AUTHORS }); + let bindings = createBindings(client, () => {}, { query: AUTHORS }); await bindings.load(); - bindings.unbind(); - client.write(AUTHORS, { - authors: data.authors.map((a, i) => (!i ? { ...a, name: "Homer" } : a)) - }); + let clonedData: AuthorsQuery = JSON.parse(JSON.stringify(data)); + clonedData.authors.edges[0].node.name = "Homer"; - expect(client.read(AUTHORS).data.authors[0].name).toBe("Homer"); - expect(renderFn).toHaveBeenCalledTimes(2); + client.write(AUTHORS, clonedData); + expect(client.read(AUTHORS).data.authors.edges[0].node.name).toBe("Homer"); expect(bindings.getState().authors).toEqual(data.authors); }); it("should provide errors on bad request", async () => { let failedQuery = { ...AUTHORS, document: AUTHORS.document.substr(1) }; - let { errors } = await mockQueryRequest(failedQuery); - let renderFn = jest.fn(); - let bindings = createBindings(client, renderFn, { query: failedQuery }); await bindings.load(); - expect(renderFn).toHaveBeenCalledTimes(2); + expect(renderFn).toHaveBeenCalledTimes(1); expect(bindings.getState()).toEqual({ loading: false, loaded: false, errors }); }); @@ -314,11 +235,8 @@ describe("@grafoo/bindings", () => { }); let props = bindings.getState(); - - let variables = { name: "Bart" }; - + let variables = { input: { name: "Bart" } }; let { data } = await mockQueryRequest(CREATE_AUTHOR, variables); - let { data: mutationData } = await props.createAuthor(variables); expect(mutationData).toEqual(data); @@ -327,29 +245,30 @@ describe("@grafoo/bindings", () => { it("should perform mutation with a cache update", async () => { await mockQueryRequest(AUTHORS); - let mutations = { - createAuthor: { - query: CREATE_AUTHOR, - update: ({ authors }, data) => ({ - authors: [data.createAuthor, ...authors] - }) + let init = makeGrafooConfig({ + query: AUTHORS, + mutations: { + createAuthor: { + query: CREATE_AUTHOR, + update: ({ authors }, data) => ({ + authors: { + edges: [{ node: data.createAuthor.author }, ...authors.edges] + } + }) + } } - }; - - let update = jest.spyOn(mutations.createAuthor, "update"); - - let bindings = createBindings(client, () => {}, { query: AUTHORS, mutations }); + }); + let update = jest.spyOn(init.mutations.createAuthor, "update"); + let bindings = createBindings(client, () => {}, init); let props = bindings.getState(); expect(typeof props.createAuthor).toBe("function"); await bindings.load(); - let variables = { name: "Homer" }; - + let variables = { input: { name: "homer" } }; let { data } = await mockQueryRequest(CREATE_AUTHOR, variables); - let { authors } = bindings.getState(); await props.createAuthor(variables); @@ -360,35 +279,40 @@ describe("@grafoo/bindings", () => { it("should perform optimistic update", async () => { await mockQueryRequest(AUTHORS); - let mutations = { - createAuthor: { - query: CREATE_AUTHOR, - optimisticUpdate: ({ authors }, variables) => ({ - authors: [{ ...variables, id: "tempID" }, ...authors] - }), - update: ({ authors }, data) => ({ - authors: authors.map((p) => (p.id === "tempID" ? data.createAuthor : p)) - }) + let init = makeGrafooConfig({ + query: AUTHORS, + mutations: { + createAuthor: { + query: CREATE_AUTHOR, + optimisticUpdate: ({ authors }, variables) => ({ + authors: { + ...authors, + edges: [{ node: { ...variables.input, id: "tempID" } }, ...authors.edges] + } + }), + update: ({ authors }, data) => ({ + authors: { + edges: authors.edges.map((p) => + p.node.id === "tempID" ? { node: data.createAuthor.author } : p + ) + } + }) + } } - }; - - let optimisticUpdate = jest.spyOn(mutations.createAuthor, "optimisticUpdate"); - let update = jest.spyOn(mutations.createAuthor, "update"); - - let bindings = createBindings(client, () => {}, { query: AUTHORS, mutations }); + }); + let optimisticUpdate = jest.spyOn(init.mutations.createAuthor, "optimisticUpdate"); + let update = jest.spyOn(init.mutations.createAuthor, "update"); + let bindings = createBindings(client, () => {}, init); let props = bindings.getState(); expect(typeof props.createAuthor).toBe("function"); await bindings.load(); - let variables = { name: "Peter" }; - + let variables = { input: { name: "marge" } }; let { data } = await mockQueryRequest(CREATE_AUTHOR, variables); - let { authors } = bindings.getState(); - let createAuthorPromise = props.createAuthor(variables); expect(optimisticUpdate).toHaveBeenCalledWith({ authors }, variables); @@ -402,8 +326,10 @@ describe("@grafoo/bindings", () => { it("should update if query objects has less keys then nextObjects", async () => { let { - data: { createAuthor: author } - } = await mockQueryRequest(CREATE_AUTHOR, { name: "gustav" }); + data: { createAuthor } + } = await mockQueryRequest(CREATE_AUTHOR, { + input: { name: "flanders" } + }); let { data } = await mockQueryRequest(AUTHORS); client.write(AUTHORS, data); @@ -415,17 +341,19 @@ describe("@grafoo/bindings", () => { mutations: { removeAuthor: { query: DELETE_AUTHOR, - optimisticUpdate: ({ authors }, { id }) => ({ - authors: authors.filter((author) => author.id !== id) + optimisticUpdate: ({ authors }, { input: { id } }) => ({ + authors: { + edges: authors.edges.filter((author) => author.node.id !== id) + } }) } } }); let { removeAuthor } = bindings.getState(); + let variables = { input: { id: createAuthor.author.id } }; - let variables = { id: author.id }; - + await mockQueryRequest(DELETE_AUTHOR, variables); await removeAuthor(variables); expect(renderFn).toHaveBeenCalled(); @@ -433,31 +361,34 @@ describe("@grafoo/bindings", () => { it("should update if query objects is modified", async () => { let { - data: { createAuthor: author } - } = await mockQueryRequest(CREATE_AUTHOR, { name: "sven" }); + data: { createAuthor } + } = await mockQueryRequest(CREATE_AUTHOR, { name: "milhouse" }); let { data } = await mockQueryRequest(AUTHORS); client.write(AUTHORS, data); - let mutations = { - updateAuthor: { - query: UPDATE_AUTHOR, - optimisticUpdate: ({ authors }, variables) => ({ - authors: authors.map((author) => (author.id === variables.id ? variables : author)) - }) + let init = makeGrafooConfig({ + query: AUTHORS, + mutations: { + updateAuthor: { + query: UPDATE_AUTHOR, + optimisticUpdate: ({ authors }, variables) => ({ + authors: { + edges: authors.edges.map((author) => + author.node.id === variables.input.id ? { node: variables.input } : author + ) + } + }) + } } - }; + }); let renderFn = jest.fn(); - - let bindings = createBindings(client, renderFn, { query: AUTHORS, mutations }); - + let bindings = createBindings(client, renderFn, init); let { updateAuthor } = bindings.getState(); - - let variables = { ...author, name: "johan" }; + let variables = { input: { ...createAuthor.author, name: "moe" } }; await mockQueryRequest(UPDATE_AUTHOR, variables); - await updateAuthor(variables); expect(renderFn).toHaveBeenCalled(); diff --git a/packages/bindings/package.json b/packages/bindings/package.json index 1694c062..c1ad9772 100644 --- a/packages/bindings/package.json +++ b/packages/bindings/package.json @@ -32,9 +32,8 @@ "transform": { "^.+\\.(ts|tsx|js)$": "/../../scripts/jest-setup.js" }, - "resolver": "/../../scripts/resolver.js", "transformIgnorePatterns": [ - "node_modules/(?!(lowdb|steno|node-fetch|fetch-blob)/)" + "node_modules/(?!(lowdb|steno|node-fetch|fetch-blob|data-uri-to-buffer|formdata-polyfill)/)" ] }, "dependencies": { diff --git a/packages/bindings/schema.graphql b/packages/bindings/schema.graphql index 8c99e2cc..26c7e6bf 100644 --- a/packages/bindings/schema.graphql +++ b/packages/bindings/schema.graphql @@ -1,28 +1,267 @@ type Query { - author(id: ID!): Author! - authors(from: Int, to: Int): [Author!]! - post(id: ID!): Post! - posts(from: Int, to: Int): [Post!]! -} + author(id: ID!): Author + authors( + """ + Returns the items in the list that come after the specified cursor. + """ + after: String -type Mutation { - createAuthor(name: String!): Author! - updateAuthor(id: ID!, name: String): Author! - deleteAuthor(id: ID!): Author! - createPost(title: String!, body: String!, author: ID!): Post! - updatePost(id: ID!, title: String, body: String): Post! - deletePost(id: ID!): Post! + """ + Returns the first n items from the list. + """ + first: Int + + """ + Returns the items in the list that come before the specified cursor. + """ + before: String + + """ + Returns the last n items from the list. + """ + last: Int + ): AuthorConnection + post(id: ID!): Post + posts( + """ + Returns the items in the list that come after the specified cursor. + """ + after: String + + """ + Returns the first n items from the list. + """ + first: Int + + """ + Returns the items in the list that come before the specified cursor. + """ + before: String + + """ + Returns the last n items from the list. + """ + last: Int + ): PostConnection + + """ + Fetches an object given its ID + """ + node( + """ + The ID of an object + """ + id: ID! + ): Node } -type Author { +type Author implements Node { + """ + The ID of an object + """ id: ID! name: String! - posts(from: Int, to: Int): [Post!] + bio: String + posts( + """ + Returns the items in the list that come after the specified cursor. + """ + after: String + + """ + Returns the first n items from the list. + """ + first: Int + + """ + Returns the items in the list that come before the specified cursor. + """ + before: String + + """ + Returns the last n items from the list. + """ + last: Int + ): PostConnection +} + +""" +An object with an ID +""" +interface Node { + """ + The id of the object. + """ + id: ID! +} + +""" +A connection to a list of items. +""" +type PostConnection { + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + A list of edges. + """ + edges: [PostEdge] +} + +""" +Information about pagination in a connection. +""" +type PageInfo { + """ + When paginating forwards, are there more items? + """ + hasNextPage: Boolean! + + """ + When paginating backwards, are there more items? + """ + hasPreviousPage: Boolean! + + """ + When paginating backwards, the cursor to continue. + """ + startCursor: String + + """ + When paginating forwards, the cursor to continue. + """ + endCursor: String +} + +""" +An edge in a connection. +""" +type PostEdge { + """ + The item at the end of the edge + """ + node: Post + + """ + A cursor for use in pagination + """ + cursor: String! } -type Post { +type Post implements Node { + """ + The ID of an object + """ id: ID! title: String! body: String! author: Author! } + +""" +A connection to a list of items. +""" +type AuthorConnection { + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + A list of edges. + """ + edges: [AuthorEdge] +} + +""" +An edge in a connection. +""" +type AuthorEdge { + """ + The item at the end of the edge + """ + node: Author + + """ + A cursor for use in pagination + """ + cursor: String! +} + +type Mutation { + createAuthor(input: CreateAuthorInput!): CreateAuthorPayload + updateAuthor(input: UpdateAuthorInput!): UpdateAuthorPayload + deleteAuthor(input: DeleteAuthorInput!): DeleteAuthorPayload + createPost(input: CreatePostInput!): CreatePostPayload + updatePost(input: UpdatePostInput!): UpdatePostPayload + deletePost(input: DeletePostInput!): DeletePostPayload +} + +type CreateAuthorPayload { + author: Author + clientMutationId: String +} + +input CreateAuthorInput { + name: String! + bio: String + clientMutationId: String +} + +type UpdateAuthorPayload { + author: Author + clientMutationId: String +} + +input UpdateAuthorInput { + id: ID + name: String + bio: String + clientMutationId: String +} + +type DeleteAuthorPayload { + author: Author + clientMutationId: String +} + +input DeleteAuthorInput { + id: ID! + clientMutationId: String +} + +type CreatePostPayload { + post: Post + clientMutationId: String +} + +input CreatePostInput { + title: String! + body: String + authorId: ID! + clientMutationId: String +} + +type UpdatePostPayload { + post: Post + clientMutationId: String +} + +input UpdatePostInput { + id: ID! + title: String + body: String + clientMutationId: String +} + +type DeletePostPayload { + post: Post + clientMutationId: String +} + +input DeletePostInput { + id: ID! + clientMutationId: String +} diff --git a/packages/bindings/src/bindings.ts b/packages/bindings/src/bindings.ts index 37650539..36e13973 100644 --- a/packages/bindings/src/bindings.ts +++ b/packages/bindings/src/bindings.ts @@ -16,11 +16,11 @@ export default function createBindings< props: GrafooConsumerProps ) { type CP = GrafooConsumerProps; - let { query, variables, mutations, skip } = props; + let { query, variables, mutations, skip = false } = props; let data: CP["query"]["_queryType"]; let errors: GraphQlError[]; let boundMutations = {} as GrafooBoundMutations; - let records: GrafooRecords; + let records: GrafooRecords = {}; let partial = false; let unbind = () => {}; let preventListenUpdate = true; @@ -68,11 +68,15 @@ export default function createBindings< } } - let state = { loaded: !!data && !partial, loading: !!query || !data || skip }; + let state = { loaded: hasData(), loading: !!query && !skip && !hasData() }; + + function hasData() { + return !!Object.keys(data ?? {}).length && !partial; + } function getUpdateFromClient() { ({ data, partial } = client.read(query, variables)); - Object.assign(state, { loaded: !!data && !partial }); + Object.assign(state, { loaded: hasData() }); updater(getState()); } diff --git a/packages/bindings/src/types.ts b/packages/bindings/src/types.ts index 91886be1..e3dd0b66 100644 --- a/packages/bindings/src/types.ts +++ b/packages/bindings/src/types.ts @@ -1,5 +1,9 @@ import { GraphQlError, GraphQlPayload, GrafooQuery } from "@grafoo/core"; +type DeepPartial = { + [P in keyof T]?: DeepPartial; +}; + export type GrafooBoundMutations> = { [U in keyof T]: ( variables: T[U]["_variablesType"] @@ -18,8 +22,11 @@ export type GrafooBoundState< export type GrafooMutation = { query: U; - update?: (props: T["_queryType"], data: U["_queryType"]) => T["_queryType"]; - optimisticUpdate?: (props: T["_queryType"], variables: U["_variablesType"]) => T["_queryType"]; + update?: (props: T["_queryType"], data: U["_queryType"]) => DeepPartial; + optimisticUpdate?: ( + props: T["_queryType"], + variables: U["_variablesType"] + ) => DeepPartial; }; export type GrafooMutations> = { diff --git a/packages/test-utils/src/mock-server.ts b/packages/test-utils/src/mock-server.ts index 9261776d..676dcbb4 100644 --- a/packages/test-utils/src/mock-server.ts +++ b/packages/test-utils/src/mock-server.ts @@ -165,7 +165,8 @@ let deleteAuthor = mutationWithClientMutationId({ } }, mutateAndGetPayload: (args) => { - let author = db.data.authors.find(args); + let { id } = fromGlobalId(args.id); + let author = db.data.authors.find((a) => a.id === id); db.data.authors = db.data.authors.filter((a) => a.id !== author.id); db.data.posts = db.data.posts.filter((p) => p.author !== author.id); db.write();