From 85932bb87c6bec3c5a6453183c3d5fe7c79d6ce7 Mon Sep 17 00:00:00 2001 From: Ian Mungai Date: Mon, 27 Mar 2023 15:06:50 +0300 Subject: [PATCH 01/19] fix: dustin's remarks --- starters/express-apollo-prisma/.editorconfig | 3 +++ starters/express-apollo-prisma/package.json | 5 ++--- .../src/graphql/schema/technology/technology.typedefs.ts | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/starters/express-apollo-prisma/.editorconfig b/starters/express-apollo-prisma/.editorconfig index adbfc82d0..0bc6b5fd0 100644 --- a/starters/express-apollo-prisma/.editorconfig +++ b/starters/express-apollo-prisma/.editorconfig @@ -14,3 +14,6 @@ indent_size = 2 [*.md] max_line_length = off trim_trailing_whitespace = false + +[{package.json.eslintrc.json}] +indent_style = space \ No newline at end of file diff --git a/starters/express-apollo-prisma/package.json b/starters/express-apollo-prisma/package.json index 64e5837e6..f88d8b6f0 100644 --- a/starters/express-apollo-prisma/package.json +++ b/starters/express-apollo-prisma/package.json @@ -13,10 +13,9 @@ "scripts": { "codegen": "graphql-codegen && npm run format:codegen", "prepare": "npm run prisma:generate && npm run codegen", - "test": "npm run prepare && jest", + "test": "jest", "start": "npm run prepare && prisma migrate dev && nodemon", - "dev:test": "jest", - "dev:start": "nodemon", + "dev": "nodemon", "build": "npm run prepare && tsc --project tsconfig.build.json", "lint": "eslint \"src/**/*.ts\"", "lint:fix": "npm run lint --fix", diff --git a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts index a962cc26e..8f51a49fc 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts @@ -18,7 +18,7 @@ export const technologyTypeDefs = gql` """ A page of technology items """ - type TechnologyCollectionPage { + type Collection { "Identifies the total count of technology records in data source" totalCount: Int! "A list of records of the requested page" @@ -32,7 +32,7 @@ export const technologyTypeDefs = gql` "Returns a single Technology by ID" technology(id: ID!): Technology "Returns a list of Technologies" - technologies(limit: Int = 5, offset: Int = 0): TechnologyCollectionPage! + technologies(limit: Int = 5, offset: Int = 0): Collection! } input CreateTechnology { From 314dab55a7cadd757abdf16f5a8f0b6776bb4757 Mon Sep 17 00:00:00 2001 From: Ian Mungai Date: Wed, 29 Mar 2023 18:25:47 +0300 Subject: [PATCH 02/19] fix: pagination conventions --- starters/express-apollo-prisma/.editorconfig | 2 +- .../src/graphql/data-sources/technology-data-source.ts | 6 +++--- .../src/graphql/schema/technology/technology.typedefs.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/starters/express-apollo-prisma/.editorconfig b/starters/express-apollo-prisma/.editorconfig index 0bc6b5fd0..5a39c9945 100644 --- a/starters/express-apollo-prisma/.editorconfig +++ b/starters/express-apollo-prisma/.editorconfig @@ -15,5 +15,5 @@ indent_size = 2 max_line_length = off trim_trailing_whitespace = false -[{package.json.eslintrc.json}] +[{package.json, eslintrc.json}] indent_style = space \ No newline at end of file diff --git a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts index 714084b65..2a8b8ac74 100644 --- a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts +++ b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts @@ -5,7 +5,7 @@ type TechnologyEntityId = TechnologyEntity['id']; export type TechnologyEntityCollectionPage = { totalCount: number; - items: TechnologyEntity[]; + edges: TechnologyEntity[]; }; export class TechnologyDataSource { @@ -31,7 +31,7 @@ export class TechnologyDataSource { } async getTechnologies(limit: number, offset: number): Promise { - const [totalCount, items] = await this.prismaClient.$transaction([ + const [totalCount, edges] = await this.prismaClient.$transaction([ this.prismaClient.technologyEntity.count(), this.prismaClient.technologyEntity.findMany({ take: limit, @@ -40,7 +40,7 @@ export class TechnologyDataSource { ]); return { totalCount, - items, + edges, }; } diff --git a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts index 8f51a49fc..6c7bbd63a 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts @@ -18,11 +18,11 @@ export const technologyTypeDefs = gql` """ A page of technology items """ - type Collection { + type TechnologyCollection { "Identifies the total count of technology records in data source" totalCount: Int! "A list of records of the requested page" - items: [Technology]! + edges: [Technology]! } """ @@ -32,7 +32,7 @@ export const technologyTypeDefs = gql` "Returns a single Technology by ID" technology(id: ID!): Technology "Returns a list of Technologies" - technologies(limit: Int = 5, offset: Int = 0): Collection! + technologies(limit: Int = 5, offset: Int = 0): TechnologyCollection! } input CreateTechnology { From 4d0e7ffe19edd6441789e074b11eb17f5d2c42be Mon Sep 17 00:00:00 2001 From: Ian Mungai Date: Thu, 30 Mar 2023 11:24:45 +0300 Subject: [PATCH 03/19] items -> edges --- .../technology-data-source.spec.ts | 2 +- .../src/graphql/mappers/technology.spec.ts | 2 +- .../src/graphql/mappers/technology.ts | 2 +- .../schema/generated/graphql.schema.json | 8 +++---- .../graphql/schema/generated/schema.graphql | 8 +++---- .../graphql/schema/generated/types/index.ts | 24 +++++++++---------- .../technology/technology.resolvers.spec.ts | 6 ++--- .../schema/technology/technology.typedefs.ts | 2 +- .../src/mocks/technology-entity.ts | 4 ++-- 9 files changed, 29 insertions(+), 29 deletions(-) diff --git a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.spec.ts b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.spec.ts index deee7bb98..13644b62c 100644 --- a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.spec.ts +++ b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.spec.ts @@ -315,7 +315,7 @@ describe('TechnologyDataSource', () => { const EXPECTED_RESULT: TechnologyEntityCollectionPage = { totalCount: MOCK_RESULT_TOTAL_COUNT, - items: MOCK_TECHNOLOGIES, + edges: MOCK_TECHNOLOGIES, }; let result: TechnologyEntityCollectionPage; diff --git a/starters/express-apollo-prisma/src/graphql/mappers/technology.spec.ts b/starters/express-apollo-prisma/src/graphql/mappers/technology.spec.ts index 23234b979..fe8682ab6 100644 --- a/starters/express-apollo-prisma/src/graphql/mappers/technology.spec.ts +++ b/starters/express-apollo-prisma/src/graphql/mappers/technology.spec.ts @@ -50,7 +50,7 @@ describe('.mapTechnologyCollectionPage', () => { const MOCK_TECHNOLOGY = createMockTechnology(); const EXPECTED_RESULT: TechnologyCollectionPage = { totalCount: 11, - items: Array(3).fill(MOCK_TECHNOLOGY), + edges: Array(3).fill(MOCK_TECHNOLOGY), }; let result: TechnologyCollectionPage; diff --git a/starters/express-apollo-prisma/src/graphql/mappers/technology.ts b/starters/express-apollo-prisma/src/graphql/mappers/technology.ts index 3e753753c..de1c5fbe0 100644 --- a/starters/express-apollo-prisma/src/graphql/mappers/technology.ts +++ b/starters/express-apollo-prisma/src/graphql/mappers/technology.ts @@ -15,6 +15,6 @@ export const mapTechnologyCollectionPage = ( ): TechnologyCollectionPage => { return { totalCount: entityCollectionPage.totalCount, - items: entityCollectionPage.items.map(mapTechnology), + edges: entityCollectionPage.edges.map(mapTechnology), }; }; diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json b/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json index 38b9be633..52ad9dfb9 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json @@ -250,7 +250,7 @@ "name": null, "ofType": { "kind": "OBJECT", - "name": "TechnologyCollectionPage", + "name": "TechnologyCollection", "ofType": null } }, @@ -371,11 +371,11 @@ }, { "kind": "OBJECT", - "name": "TechnologyCollectionPage", - "description": "A page of technology items", + "name": "TechnologyCollection", + "description": "A page of technology edges", "fields": [ { - "name": "items", + "name": "edges", "description": "A list of records of the requested page", "args": [], "type": { diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql b/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql index 4a6e7be60..a4b622753 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql @@ -29,7 +29,7 @@ Technology queries """ type Query { "Returns a list of Technologies" - technologies(limit: Int = 5, offset: Int = 0): TechnologyCollectionPage! + technologies(limit: Int = 5, offset: Int = 0): TechnologyCollection! "Returns a single Technology by ID" technology(id: ID!): Technology } @@ -49,11 +49,11 @@ type Technology { } """ -A page of technology items +A page of technology edges """ -type TechnologyCollectionPage { +type TechnologyCollection { "A list of records of the requested page" - items: [Technology]! + edges: [Technology]! "Identifies the total count of technology records in data source" totalCount: Int! } diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts b/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts index 62e558e47..587fb2eae 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts @@ -54,7 +54,7 @@ export type MutationupdateTechnologyArgs = { export type Query = { __typename?: 'Query'; /** Returns a list of Technologies */ - technologies: TechnologyCollectionPage; + technologies: TechnologyCollection; /** Returns a single Technology by ID */ technology?: Maybe; }; @@ -83,11 +83,11 @@ export type Technology = { url?: Maybe; }; -/** A page of technology items */ -export type TechnologyCollectionPage = { - __typename?: 'TechnologyCollectionPage'; +/** A page of technology edges */ +export type TechnologyCollection = { + __typename?: 'TechnologyCollection'; /** A list of records of the requested page */ - items: Array>; + edges: Array>; /** Identifies the total count of technology records in data source */ totalCount: Scalars['Int']; }; @@ -193,7 +193,7 @@ export type ResolversTypes = { Query: ResolverTypeWrapper<{}>; String: ResolverTypeWrapper; Technology: ResolverTypeWrapper; - TechnologyCollectionPage: ResolverTypeWrapper; + TechnologyCollection: ResolverTypeWrapper; UpdateTechnology: UpdateTechnology; }; @@ -207,7 +207,7 @@ export type ResolversParentTypes = { Query: {}; String: Scalars['String']; Technology: Technology; - TechnologyCollectionPage: TechnologyCollectionPage; + TechnologyCollection: TechnologyCollection; UpdateTechnology: UpdateTechnology; }; @@ -240,7 +240,7 @@ export type QueryResolvers< ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query'] > = { technologies?: Resolver< - ResolversTypes['TechnologyCollectionPage'], + ResolversTypes['TechnologyCollection'], ParentType, ContextType, RequireFields @@ -264,11 +264,11 @@ export type TechnologyResolvers< __isTypeOf?: IsTypeOfResolverFn; }; -export type TechnologyCollectionPageResolvers< +export type TechnologyCollectionResolvers< ContextType = any, - ParentType extends ResolversParentTypes['TechnologyCollectionPage'] = ResolversParentTypes['TechnologyCollectionPage'] + ParentType extends ResolversParentTypes['TechnologyCollection'] = ResolversParentTypes['TechnologyCollection'] > = { - items?: Resolver>, ParentType, ContextType>; + edges?: Resolver>, ParentType, ContextType>; totalCount?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -277,5 +277,5 @@ export type Resolvers = { Mutation?: MutationResolvers; Query?: QueryResolvers; Technology?: TechnologyResolvers; - TechnologyCollectionPage?: TechnologyCollectionPageResolvers; + TechnologyCollection?: TechnologyCollectionResolvers; }; diff --git a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.spec.ts b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.spec.ts index 22e0c752b..58edc5489 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.spec.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.spec.ts @@ -53,7 +53,7 @@ const MOCK_QUERY_TECHNOLOGY = gql` const MOCK_QUERY_TECHNOLOGIES_PAGINATION_DEFAULT = gql` query TechnologiesQueryPaginationArgumentsDefualt { technologies { - items { + edges { description displayName id @@ -69,7 +69,7 @@ const MOCK_VARIABLES_TECHNOLOGIES_PAGINATION_DEFAULT: QuerytechnologiesArgs = {} const MOCK_QUERY_TECHNOLOGIES_PAGINATION_CUSTOM = gql` query TechnologiesQueryPaginationArgumentsCustom($limit: Int, $offset: Int) { technologies(limit: $limit, offset: $offset) { - items { + edges { description displayName id @@ -279,7 +279,7 @@ describe('technologyResolvers', () => { describe('when called', () => { const MOCK_RESULT_TECHNOLOGY_COLLECTION_PAGE: TechnologyCollectionPage = { totalCount: 987, - items: [ + edges: [ { displayName: 'MOCK_DISPLAY_NAME_RESULT', description: 'MOCK_DESCRIPTION_RESULT', diff --git a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts index 6c7bbd63a..ab90a0e88 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts @@ -16,7 +16,7 @@ export const technologyTypeDefs = gql` } """ - A page of technology items + A page of technology edges """ type TechnologyCollection { "Identifies the total count of technology records in data source" diff --git a/starters/express-apollo-prisma/src/mocks/technology-entity.ts b/starters/express-apollo-prisma/src/mocks/technology-entity.ts index 576c5e63d..3f30e7d09 100644 --- a/starters/express-apollo-prisma/src/mocks/technology-entity.ts +++ b/starters/express-apollo-prisma/src/mocks/technology-entity.ts @@ -18,9 +18,9 @@ const createMockTechnologyEntity = (): TechnologyEntity => { }; export const createMockTechnologyEntityCollectionPage = ( - itemsCount: number, + edgesCount: number, totalCount: number ): TechnologyEntityCollectionPage => ({ totalCount, - items: Array(itemsCount).fill(null).map(createMockTechnologyEntity), + edges: Array(edgesCount).fill(null).map(createMockTechnologyEntity), }); From 073ef4083ab71bf33750ca680a2a7d015ae66b8e Mon Sep 17 00:00:00 2001 From: Ian Mungai Date: Thu, 30 Mar 2023 11:29:24 +0300 Subject: [PATCH 04/19] Technology Collection --- starters/express-apollo-prisma/README.md | 2 +- .../src/graphql/mappers/technology.spec.ts | 12 +++++----- .../src/graphql/mappers/technology.ts | 6 ++--- .../technology/technology.resolvers.spec.ts | 24 +++++++++---------- .../schema/technology/technology.resolvers.ts | 4 ++-- 5 files changed, 23 insertions(+), 25 deletions(-) diff --git a/starters/express-apollo-prisma/README.md b/starters/express-apollo-prisma/README.md index fee204e89..d51b4a7df 100644 --- a/starters/express-apollo-prisma/README.md +++ b/starters/express-apollo-prisma/README.md @@ -239,7 +239,7 @@ There is a `technologies` query in `technology.typedefs.ts` file. The query uses type Query { ... "Returns a list of Technologies" - technologies(limit: Int = 5, offset: Int = 0): TechnologyCollectionPage! + technologies(limit: Int = 5, offset: Int = 0): TechnologyCollection! } ... ``` diff --git a/starters/express-apollo-prisma/src/graphql/mappers/technology.spec.ts b/starters/express-apollo-prisma/src/graphql/mappers/technology.spec.ts index fe8682ab6..fb3bb1e01 100644 --- a/starters/express-apollo-prisma/src/graphql/mappers/technology.spec.ts +++ b/starters/express-apollo-prisma/src/graphql/mappers/technology.spec.ts @@ -1,6 +1,6 @@ -import { mapTechnology, mapTechnologyCollectionPage } from './technology'; +import { mapTechnology, mapTechnologyCollection } from './technology'; import { TechnologyEntity } from '@prisma/client'; -import { Technology, TechnologyCollectionPage } from '../schema/generated/types'; +import { Technology, TechnologyCollection } from '../schema/generated/types'; import { createMockTechnologyEntityCollectionPage } from '../../mocks/technology-entity'; import { createMockTechnology } from '../../mocks/technology'; @@ -44,19 +44,19 @@ describe('.mapTechnology', () => { }); }); -describe('.mapTechnologyCollectionPage', () => { +describe('.mapTechnologyCollection', () => { describe('when called with arguments', () => { const MOCK_TECHNOLOGY_ENTITY_COLLECTION_PAGE = createMockTechnologyEntityCollectionPage(3, 11); const MOCK_TECHNOLOGY = createMockTechnology(); - const EXPECTED_RESULT: TechnologyCollectionPage = { + const EXPECTED_RESULT: TechnologyCollection = { totalCount: 11, edges: Array(3).fill(MOCK_TECHNOLOGY), }; - let result: TechnologyCollectionPage; + let result: TechnologyCollection; beforeAll(() => { SPY_MAP_TECHNOLOGY.mockReturnValue(MOCK_TECHNOLOGY); - result = mapTechnologyCollectionPage(MOCK_TECHNOLOGY_ENTITY_COLLECTION_PAGE); + result = mapTechnologyCollection(MOCK_TECHNOLOGY_ENTITY_COLLECTION_PAGE); }); afterAll(() => { diff --git a/starters/express-apollo-prisma/src/graphql/mappers/technology.ts b/starters/express-apollo-prisma/src/graphql/mappers/technology.ts index de1c5fbe0..87ca6318e 100644 --- a/starters/express-apollo-prisma/src/graphql/mappers/technology.ts +++ b/starters/express-apollo-prisma/src/graphql/mappers/technology.ts @@ -1,6 +1,6 @@ import { TechnologyEntity } from '@prisma/client'; import { TechnologyEntityCollectionPage } from '../data-sources'; -import { Technology, TechnologyCollectionPage } from '../schema/generated/types'; +import { Technology, TechnologyCollection } from '../schema/generated/types'; export const mapTechnology = (entity: TechnologyEntity): Technology => ({ __typename: 'Technology', @@ -10,9 +10,9 @@ export const mapTechnology = (entity: TechnologyEntity): Technology => ({ url: entity.url, }); -export const mapTechnologyCollectionPage = ( +export const mapTechnologyCollection = ( entityCollectionPage: TechnologyEntityCollectionPage -): TechnologyCollectionPage => { +): TechnologyCollection => { return { totalCount: entityCollectionPage.totalCount, edges: entityCollectionPage.edges.map(mapTechnology), diff --git a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.spec.ts b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.spec.ts index 58edc5489..8d89f616a 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.spec.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.spec.ts @@ -6,7 +6,7 @@ import { CreateTechnology, UpdateTechnology, QuerytechnologiesArgs, - TechnologyCollectionPage, + TechnologyCollection, } from '../generated/types'; import assert from 'assert'; import { testServerExecuteOperation } from '../../../mocks/graphql-server'; @@ -20,7 +20,7 @@ import { ApolloServerErrorCode } from '@apollo/server/errors'; import { ServerContext } from '../../server-context'; import { TechnologyEntity } from '@prisma/client'; -import { mapTechnology, mapTechnologyCollectionPage } from '../../mappers'; +import { mapTechnology, mapTechnologyCollection } from '../../mappers'; type QueryTechnology = Pick; type QueryTechnologies = Pick; @@ -145,11 +145,11 @@ jest.mock('../../mappers/technology', () => ({ description: 'MOCK_TECHNOLOGY_DESCRIPTION', url: 'MOCK_TECHNOLOGY_URL', }), - mapTechnologyCollectionPage: jest.fn(), + mapTechnologyCollection: jest.fn(), })); const MOCK_MAP_TECHNOLOGY = mapTechnology as jest.Mock; -const MOCK_MAP_TECHNOLOGY_COLLECTION_PAGE = mapTechnologyCollectionPage as jest.MockedFn< - typeof mapTechnologyCollectionPage +const MOCK_MAP_TECHNOLOGY_COLLECTION = mapTechnologyCollection as jest.MockedFn< + typeof mapTechnologyCollection >; describe('technologyResolvers', () => { @@ -277,7 +277,7 @@ describe('technologyResolvers', () => { describe('.technologies', () => { describe('when called', () => { - const MOCK_RESULT_TECHNOLOGY_COLLECTION_PAGE: TechnologyCollectionPage = { + const MOCK_RESULT_TECHNOLOGY_COLLECTION: TechnologyCollection = { totalCount: 987, edges: [ { @@ -320,9 +320,7 @@ describe('technologyResolvers', () => { beforeAll(async () => { MOCK_TECHNOLOGY_DATASOURCE.getTechnologies.mockResolvedValue(mockCollectionPage); - MOCK_MAP_TECHNOLOGY_COLLECTION_PAGE.mockReturnValue( - MOCK_RESULT_TECHNOLOGY_COLLECTION_PAGE - ); + MOCK_MAP_TECHNOLOGY_COLLECTION.mockReturnValue(MOCK_RESULT_TECHNOLOGY_COLLECTION); response = await testServerExecuteOperation( { query: mockQuery, @@ -334,7 +332,7 @@ describe('technologyResolvers', () => { afterAll(() => { MOCK_TECHNOLOGY_DATASOURCE.getTechnologies.mockReset(); - MOCK_MAP_TECHNOLOGY_COLLECTION_PAGE.mockReset(); + MOCK_MAP_TECHNOLOGY_COLLECTION.mockReset(); }); it('calls TechnologyDataSource getTechnologies method once', () => { @@ -346,8 +344,8 @@ describe('technologyResolvers', () => { }); it('calls mapTechnology mapper function for each technology entity', () => { - expect(MOCK_MAP_TECHNOLOGY_COLLECTION_PAGE).toHaveBeenCalledTimes(1); - expect(MOCK_MAP_TECHNOLOGY_COLLECTION_PAGE).toHaveBeenCalledWith(mockCollectionPage); + expect(MOCK_MAP_TECHNOLOGY_COLLECTION).toHaveBeenCalledTimes(1); + expect(MOCK_MAP_TECHNOLOGY_COLLECTION).toHaveBeenCalledWith(mockCollectionPage); }); it('returns expected result', async () => { @@ -355,7 +353,7 @@ describe('technologyResolvers', () => { assert(response.body.kind === 'single'); expect(response.body.singleResult.errors).toBeUndefined(); expect(response.body.singleResult.data).toEqual({ - technologies: MOCK_RESULT_TECHNOLOGY_COLLECTION_PAGE, + technologies: MOCK_RESULT_TECHNOLOGY_COLLECTION, }); }); } diff --git a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.ts b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.ts index f1fb79b68..5000d7eef 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.ts @@ -2,7 +2,7 @@ import { ServerContext } from '../../server-context/server-context'; import { Resolvers, UpdateTechnology } from '../generated/types'; import { ApolloServerErrorCode } from '@apollo/server/errors'; import { GraphQLError } from 'graphql'; -import { mapTechnology, mapTechnologyCollectionPage } from '../../mappers'; +import { mapTechnology, mapTechnologyCollection } from '../../mappers'; const parseTechnologyId = (id: string): number => { const idNumber = Number(id); @@ -37,7 +37,7 @@ export const technologyResolvers: Resolvers = { }, technologies: async (_parent, { limit, offset }, { dataSources: { technologyDataSource } }) => { const collectionPage = await technologyDataSource.getTechnologies(limit, offset); - return mapTechnologyCollectionPage(collectionPage); + return mapTechnologyCollection(collectionPage); }, }, Mutation: { From 42564e7656ec83754abf1a34349f0a540ec755bc Mon Sep 17 00:00:00 2001 From: Ian Mungai Date: Thu, 30 Mar 2023 11:50:18 +0300 Subject: [PATCH 05/19] chore: update collectionpage -> collection --- .../src/graphql/data-sources/technology-data-source.spec.ts | 6 +++--- .../src/graphql/data-sources/technology-data-source.ts | 4 ++-- .../src/graphql/mappers/technology.spec.ts | 4 ++-- .../express-apollo-prisma/src/graphql/mappers/technology.ts | 4 ++-- .../src/graphql/schema/generated/graphql.schema.json | 2 +- .../src/graphql/schema/generated/schema.graphql | 2 +- .../src/graphql/schema/generated/types/index.ts | 2 +- .../graphql/schema/technology/technology.resolvers.spec.ts | 6 +++--- .../src/graphql/schema/technology/technology.typedefs.ts | 2 +- .../express-apollo-prisma/src/mocks/technology-entity.ts | 6 +++--- 10 files changed, 19 insertions(+), 19 deletions(-) diff --git a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.spec.ts b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.spec.ts index 13644b62c..05d1f4a4b 100644 --- a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.spec.ts +++ b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.spec.ts @@ -1,4 +1,4 @@ -import { TechnologyDataSource, TechnologyEntityCollectionPage } from './technology-data-source'; +import { TechnologyDataSource, TechnologyEntityCollection } from './technology-data-source'; import { PrismaClient, TechnologyEntity } from '@prisma/client'; import { DeepMockProxy } from 'jest-mock-extended'; import { createMockPrismaClient } from '../../mocks/prisma-client'; @@ -313,12 +313,12 @@ describe('TechnologyDataSource', () => { const MOCK_RESULT_TOTAL_COUNT = 3; const MOCK_TECHNOLOGIES: TechnologyEntity[] = [MOCK_TECHNOLOGY]; - const EXPECTED_RESULT: TechnologyEntityCollectionPage = { + const EXPECTED_RESULT: TechnologyEntityCollection = { totalCount: MOCK_RESULT_TOTAL_COUNT, edges: MOCK_TECHNOLOGIES, }; - let result: TechnologyEntityCollectionPage; + let result: TechnologyEntityCollection; beforeAll(async () => { MOCK_PRISMA_CLIENT.$transaction.mockResolvedValue([ diff --git a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts index 2a8b8ac74..e0a2e59ce 100644 --- a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts +++ b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts @@ -3,7 +3,7 @@ import { CacheAPIWrapper } from '../../cache'; type TechnologyEntityId = TechnologyEntity['id']; -export type TechnologyEntityCollectionPage = { +export type TechnologyEntityCollection = { totalCount: number; edges: TechnologyEntity[]; }; @@ -30,7 +30,7 @@ export class TechnologyDataSource { return entity; } - async getTechnologies(limit: number, offset: number): Promise { + async getTechnologies(limit: number, offset: number): Promise { const [totalCount, edges] = await this.prismaClient.$transaction([ this.prismaClient.technologyEntity.count(), this.prismaClient.technologyEntity.findMany({ diff --git a/starters/express-apollo-prisma/src/graphql/mappers/technology.spec.ts b/starters/express-apollo-prisma/src/graphql/mappers/technology.spec.ts index fb3bb1e01..c07e07192 100644 --- a/starters/express-apollo-prisma/src/graphql/mappers/technology.spec.ts +++ b/starters/express-apollo-prisma/src/graphql/mappers/technology.spec.ts @@ -1,7 +1,7 @@ import { mapTechnology, mapTechnologyCollection } from './technology'; import { TechnologyEntity } from '@prisma/client'; import { Technology, TechnologyCollection } from '../schema/generated/types'; -import { createMockTechnologyEntityCollectionPage } from '../../mocks/technology-entity'; +import { createMockTechnologyEntityCollection } from '../../mocks/technology-entity'; import { createMockTechnology } from '../../mocks/technology'; jest.mock('./technology', () => { @@ -46,7 +46,7 @@ describe('.mapTechnology', () => { describe('.mapTechnologyCollection', () => { describe('when called with arguments', () => { - const MOCK_TECHNOLOGY_ENTITY_COLLECTION_PAGE = createMockTechnologyEntityCollectionPage(3, 11); + const MOCK_TECHNOLOGY_ENTITY_COLLECTION_PAGE = createMockTechnologyEntityCollection(3, 11); const MOCK_TECHNOLOGY = createMockTechnology(); const EXPECTED_RESULT: TechnologyCollection = { totalCount: 11, diff --git a/starters/express-apollo-prisma/src/graphql/mappers/technology.ts b/starters/express-apollo-prisma/src/graphql/mappers/technology.ts index 87ca6318e..e1fcc3c1c 100644 --- a/starters/express-apollo-prisma/src/graphql/mappers/technology.ts +++ b/starters/express-apollo-prisma/src/graphql/mappers/technology.ts @@ -1,5 +1,5 @@ import { TechnologyEntity } from '@prisma/client'; -import { TechnologyEntityCollectionPage } from '../data-sources'; +import { TechnologyEntityCollection } from '../data-sources'; import { Technology, TechnologyCollection } from '../schema/generated/types'; export const mapTechnology = (entity: TechnologyEntity): Technology => ({ @@ -11,7 +11,7 @@ export const mapTechnology = (entity: TechnologyEntity): Technology => ({ }); export const mapTechnologyCollection = ( - entityCollectionPage: TechnologyEntityCollectionPage + entityCollectionPage: TechnologyEntityCollection ): TechnologyCollection => { return { totalCount: entityCollectionPage.totalCount, diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json b/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json index 52ad9dfb9..2fbece3b5 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json @@ -372,7 +372,7 @@ { "kind": "OBJECT", "name": "TechnologyCollection", - "description": "A page of technology edges", + "description": "A collection of technologies", "fields": [ { "name": "edges", diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql b/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql index a4b622753..124ba8de4 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql @@ -49,7 +49,7 @@ type Technology { } """ -A page of technology edges +A collection of technologies """ type TechnologyCollection { "A list of records of the requested page" diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts b/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts index 587fb2eae..e4aa1e846 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts @@ -83,7 +83,7 @@ export type Technology = { url?: Maybe; }; -/** A page of technology edges */ +/** A collection of technologies */ export type TechnologyCollection = { __typename?: 'TechnologyCollection'; /** A list of records of the requested page */ diff --git a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.spec.ts b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.spec.ts index 8d89f616a..86370b2f2 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.spec.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.spec.ts @@ -12,7 +12,7 @@ import assert from 'assert'; import { testServerExecuteOperation } from '../../../mocks/graphql-server'; import { createMockTechnologyDataSource, - createMockTechnologyEntityCollectionPage, + createMockTechnologyEntityCollection, } from '../../../mocks/technology-entity'; import { GraphQLResponse } from '@apollo/server'; @@ -294,7 +294,7 @@ describe('technologyResolvers', () => { 'with default pagination arguments', MOCK_QUERY_TECHNOLOGIES_PAGINATION_DEFAULT, MOCK_VARIABLES_TECHNOLOGIES_PAGINATION_DEFAULT, - createMockTechnologyEntityCollectionPage(5, 30), + createMockTechnologyEntityCollection(5, 30), 5, 0, ], @@ -302,7 +302,7 @@ describe('technologyResolvers', () => { 'with custom pagination arguments', MOCK_QUERY_TECHNOLOGIES_PAGINATION_CUSTOM, MOCK_VARIABLES_TECHNOLOGIES_PAGINATION_CUSTOM, - createMockTechnologyEntityCollectionPage(10, 50), + createMockTechnologyEntityCollection(10, 50), Number(MOCK_VARIABLES_TECHNOLOGIES_PAGINATION_CUSTOM.limit), Number(MOCK_VARIABLES_TECHNOLOGIES_PAGINATION_CUSTOM.offset), ], diff --git a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts index ab90a0e88..16a0f031e 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts @@ -16,7 +16,7 @@ export const technologyTypeDefs = gql` } """ - A page of technology edges + A collection of technologies """ type TechnologyCollection { "Identifies the total count of technology records in data source" diff --git a/starters/express-apollo-prisma/src/mocks/technology-entity.ts b/starters/express-apollo-prisma/src/mocks/technology-entity.ts index 3f30e7d09..0d0418b00 100644 --- a/starters/express-apollo-prisma/src/mocks/technology-entity.ts +++ b/starters/express-apollo-prisma/src/mocks/technology-entity.ts @@ -1,6 +1,6 @@ import { TechnologyEntity } from '@prisma/client'; import { DeepMockProxy, mockDeep } from 'jest-mock-extended'; -import { TechnologyDataSource, TechnologyEntityCollectionPage } from '../graphql/data-sources'; +import { TechnologyDataSource, TechnologyEntityCollection } from '../graphql/data-sources'; export const createMockTechnologyDataSource = (): DeepMockProxy => mockDeep(); @@ -17,10 +17,10 @@ const createMockTechnologyEntity = (): TechnologyEntity => { }; }; -export const createMockTechnologyEntityCollectionPage = ( +export const createMockTechnologyEntityCollection = ( edgesCount: number, totalCount: number -): TechnologyEntityCollectionPage => ({ +): TechnologyEntityCollection => ({ totalCount, edges: Array(edgesCount).fill(null).map(createMockTechnologyEntity), }); From b611c3be4eb403e3dd4e72cc87716f04237c0622 Mon Sep 17 00:00:00 2001 From: Ian Mungai Date: Thu, 30 Mar 2023 13:51:52 +0300 Subject: [PATCH 06/19] chore: implement cursor based pagination --- .../data-sources/technology-data-source.ts | 27 +++++++- .../src/graphql/mappers/technology.ts | 1 + .../schema/generated/graphql.schema.json | 65 +++++++++++++++++-- .../graphql/schema/generated/schema.graphql | 14 +++- .../graphql/schema/generated/types/index.ts | 30 ++++++++- .../schema/technology/technology.resolvers.ts | 4 +- .../schema/technology/technology.typedefs.ts | 14 +++- 7 files changed, 138 insertions(+), 17 deletions(-) diff --git a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts index e0a2e59ce..e2b3ab060 100644 --- a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts +++ b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts @@ -1,11 +1,16 @@ import { PrismaClient, Prisma, TechnologyEntity } from '@prisma/client'; import { CacheAPIWrapper } from '../../cache'; +import { InputMaybe } from '../schema/generated/types'; type TechnologyEntityId = TechnologyEntity['id']; export type TechnologyEntityCollection = { totalCount: number; edges: TechnologyEntity[]; + pageInfo: { + hasNextPage: boolean; + endCursor: number | undefined | null; + }; }; export class TechnologyDataSource { @@ -30,17 +35,33 @@ export class TechnologyDataSource { return entity; } - async getTechnologies(limit: number, offset: number): Promise { + async getTechnologies( + first: number, // the number of items to return in a page + after?: InputMaybe | undefined // the cursor to start the next page from + ): Promise { + const where: Prisma.TechnologyEntityWhereInput = after ? { id: { gt: after } } : {}; + const [totalCount, edges] = await this.prismaClient.$transaction([ this.prismaClient.technologyEntity.count(), this.prismaClient.technologyEntity.findMany({ - take: limit, - skip: offset, + where, + take: first, + orderBy: { id: 'asc' }, }), ]); + + const endCursor = edges.length > 0 ? edges[edges.length - 1].id : undefined; + const hasNextPage = + (await this.prismaClient.technologyEntity.count({ + where: { id: { gt: endCursor } }, + })) > 0; return { totalCount, edges, + pageInfo: { + hasNextPage, + endCursor, + }, }; } diff --git a/starters/express-apollo-prisma/src/graphql/mappers/technology.ts b/starters/express-apollo-prisma/src/graphql/mappers/technology.ts index e1fcc3c1c..370ad1c2d 100644 --- a/starters/express-apollo-prisma/src/graphql/mappers/technology.ts +++ b/starters/express-apollo-prisma/src/graphql/mappers/technology.ts @@ -16,5 +16,6 @@ export const mapTechnologyCollection = ( return { totalCount: entityCollectionPage.totalCount, edges: entityCollectionPage.edges.map(mapTechnology), + pageInfo: entityCollectionPage.pageInfo, }; }; diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json b/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json index 2fbece3b5..0ab9bd5ae 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json @@ -211,6 +211,41 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "PaginationInformation", + "description": "Pagination Information", + "fields": [ + { + "name": "endCursor", + "description": "Next page cursor", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasNextPage", + "description": "If there is an existing page after", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "Query", @@ -221,26 +256,30 @@ "description": "Returns a list of Technologies", "args": [ { - "name": "limit", + "name": "after", "description": null, "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, - "defaultValue": "5", + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "offset", + "name": "first", "description": null, "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } }, - "defaultValue": "0", + "defaultValue": null, "isDeprecated": false, "deprecationReason": null } @@ -394,6 +433,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "pageInfo", + "description": "Pagination Information", + "args": [], + "type": { + "kind": "OBJECT", + "name": "PaginationInformation", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "totalCount", "description": "Identifies the total count of technology records in data source", diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql b/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql index 124ba8de4..397beae90 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql @@ -24,12 +24,22 @@ type Mutation { updateTechnology(id: ID!, input: UpdateTechnology!): Technology! } +""" +Pagination Information +""" +type PaginationInformation { + "Next page cursor" + endCursor: Int + "If there is an existing page after" + hasNextPage: Boolean +} + """ Technology queries """ type Query { "Returns a list of Technologies" - technologies(limit: Int = 5, offset: Int = 0): TechnologyCollection! + technologies(after: Int, first: Int!): TechnologyCollection! "Returns a single Technology by ID" technology(id: ID!): Technology } @@ -54,6 +64,8 @@ A collection of technologies type TechnologyCollection { "A list of records of the requested page" edges: [Technology]! + "Pagination Information" + pageInfo: PaginationInformation "Identifies the total count of technology records in data source" totalCount: Int! } diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts b/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts index e4aa1e846..4919c6715 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts @@ -50,6 +50,15 @@ export type MutationupdateTechnologyArgs = { input: UpdateTechnology; }; +/** Pagination Information */ +export type PaginationInformation = { + __typename?: 'PaginationInformation'; + /** Next page cursor */ + endCursor?: Maybe; + /** If there is an existing page after */ + hasNextPage?: Maybe; +}; + /** Technology queries */ export type Query = { __typename?: 'Query'; @@ -61,8 +70,8 @@ export type Query = { /** Technology queries */ export type QuerytechnologiesArgs = { - limit?: InputMaybe; - offset?: InputMaybe; + after?: InputMaybe; + first: Scalars['Int']; }; /** Technology queries */ @@ -88,6 +97,8 @@ export type TechnologyCollection = { __typename?: 'TechnologyCollection'; /** A list of records of the requested page */ edges: Array>; + /** Pagination Information */ + pageInfo?: Maybe; /** Identifies the total count of technology records in data source */ totalCount: Scalars['Int']; }; @@ -190,6 +201,7 @@ export type ResolversTypes = { ID: ResolverTypeWrapper; Int: ResolverTypeWrapper; Mutation: ResolverTypeWrapper<{}>; + PaginationInformation: ResolverTypeWrapper; Query: ResolverTypeWrapper<{}>; String: ResolverTypeWrapper; Technology: ResolverTypeWrapper; @@ -204,6 +216,7 @@ export type ResolversParentTypes = { ID: Scalars['ID']; Int: Scalars['Int']; Mutation: {}; + PaginationInformation: PaginationInformation; Query: {}; String: Scalars['String']; Technology: Technology; @@ -235,6 +248,15 @@ export type MutationResolvers< >; }; +export type PaginationInformationResolvers< + ContextType = any, + ParentType extends ResolversParentTypes['PaginationInformation'] = ResolversParentTypes['PaginationInformation'] +> = { + endCursor?: Resolver, ParentType, ContextType>; + hasNextPage?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type QueryResolvers< ContextType = any, ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query'] @@ -243,7 +265,7 @@ export type QueryResolvers< ResolversTypes['TechnologyCollection'], ParentType, ContextType, - RequireFields + RequireFields >; technology?: Resolver< Maybe, @@ -269,12 +291,14 @@ export type TechnologyCollectionResolvers< ParentType extends ResolversParentTypes['TechnologyCollection'] = ResolversParentTypes['TechnologyCollection'] > = { edges?: Resolver>, ParentType, ContextType>; + pageInfo?: Resolver, ParentType, ContextType>; totalCount?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; export type Resolvers = { Mutation?: MutationResolvers; + PaginationInformation?: PaginationInformationResolvers; Query?: QueryResolvers; Technology?: TechnologyResolvers; TechnologyCollection?: TechnologyCollectionResolvers; diff --git a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.ts b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.ts index 5000d7eef..d9e8bfe95 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.ts @@ -35,8 +35,8 @@ export const technologyResolvers: Resolvers = { } return mapTechnology(entity); }, - technologies: async (_parent, { limit, offset }, { dataSources: { technologyDataSource } }) => { - const collectionPage = await technologyDataSource.getTechnologies(limit, offset); + technologies: async (_parent, { first, after }, { dataSources: { technologyDataSource } }) => { + const collectionPage = await technologyDataSource.getTechnologies(first, after); return mapTechnologyCollection(collectionPage); }, }, diff --git a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts index 16a0f031e..a43c696d3 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts @@ -15,6 +15,16 @@ export const technologyTypeDefs = gql` url: String } + """ + Pagination Information + """ + type PaginationInformation { + "If there is an existing page after" + hasNextPage: Boolean + "Next page cursor" + endCursor: Int + } + """ A collection of technologies """ @@ -23,6 +33,8 @@ export const technologyTypeDefs = gql` totalCount: Int! "A list of records of the requested page" edges: [Technology]! + "Pagination Information" + pageInfo: PaginationInformation } """ @@ -32,7 +44,7 @@ export const technologyTypeDefs = gql` "Returns a single Technology by ID" technology(id: ID!): Technology "Returns a list of Technologies" - technologies(limit: Int = 5, offset: Int = 0): TechnologyCollection! + technologies(first: Int!, after: Int): TechnologyCollection! } input CreateTechnology { From e88439bc70c14050ad152783437a28e195bb1025 Mon Sep 17 00:00:00 2001 From: Ian Mungai Date: Thu, 30 Mar 2023 14:14:07 +0300 Subject: [PATCH 07/19] chore: add startCursor --- .../data-sources/technology-data-source.ts | 6 +++++- .../graphql/schema/generated/graphql.schema.json | 16 ++++++++++++++-- .../src/graphql/schema/generated/schema.graphql | 6 ++++-- .../src/graphql/schema/generated/types/index.ts | 7 +++++-- .../schema/technology/technology.typedefs.ts | 6 ++++-- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts index e2b3ab060..8038640a4 100644 --- a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts +++ b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts @@ -9,7 +9,8 @@ export type TechnologyEntityCollection = { edges: TechnologyEntity[]; pageInfo: { hasNextPage: boolean; - endCursor: number | undefined | null; + startCursor?: number; + endCursor?: number; }; }; @@ -50,16 +51,19 @@ export class TechnologyDataSource { }), ]); + const startCursor = edges.length > 0 ? edges[0].id : undefined; const endCursor = edges.length > 0 ? edges[edges.length - 1].id : undefined; const hasNextPage = (await this.prismaClient.technologyEntity.count({ where: { id: { gt: endCursor } }, })) > 0; + console.log(startCursor); return { totalCount, edges, pageInfo: { hasNextPage, + startCursor, endCursor, }, }; diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json b/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json index 0ab9bd5ae..355f975a0 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json @@ -218,7 +218,7 @@ "fields": [ { "name": "endCursor", - "description": "Next page cursor", + "description": "Last cursor in page", "args": [], "type": { "kind": "SCALAR", @@ -230,7 +230,7 @@ }, { "name": "hasNextPage", - "description": "If there is an existing page after", + "description": "Shows if there is a page after", "args": [], "type": { "kind": "SCALAR", @@ -239,6 +239,18 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "startCursor", + "description": "First cursor in page", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql b/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql index 397beae90..b82bc3adf 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql @@ -28,10 +28,12 @@ type Mutation { Pagination Information """ type PaginationInformation { - "Next page cursor" + "Last cursor in page" endCursor: Int - "If there is an existing page after" + "Shows if there is a page after" hasNextPage: Boolean + "First cursor in page" + startCursor: Int } """ diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts b/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts index 4919c6715..406047b25 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts @@ -53,10 +53,12 @@ export type MutationupdateTechnologyArgs = { /** Pagination Information */ export type PaginationInformation = { __typename?: 'PaginationInformation'; - /** Next page cursor */ + /** Last cursor in page */ endCursor?: Maybe; - /** If there is an existing page after */ + /** Shows if there is a page after */ hasNextPage?: Maybe; + /** First cursor in page */ + startCursor?: Maybe; }; /** Technology queries */ @@ -254,6 +256,7 @@ export type PaginationInformationResolvers< > = { endCursor?: Resolver, ParentType, ContextType>; hasNextPage?: Resolver, ParentType, ContextType>; + startCursor?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts index a43c696d3..871eac6a2 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts @@ -19,9 +19,11 @@ export const technologyTypeDefs = gql` Pagination Information """ type PaginationInformation { - "If there is an existing page after" + "Shows if there is a page after" hasNextPage: Boolean - "Next page cursor" + "First cursor in page" + startCursor: Int + "Last cursor in page" endCursor: Int } From 4e91366ca5ddeba31f36cf0f0ce298634e46c889 Mon Sep 17 00:00:00 2001 From: Ian Mungai Date: Thu, 30 Mar 2023 14:14:32 +0300 Subject: [PATCH 08/19] fix: remove console --- .../src/graphql/data-sources/technology-data-source.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts index 8038640a4..ca6e22855 100644 --- a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts +++ b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts @@ -57,7 +57,7 @@ export class TechnologyDataSource { (await this.prismaClient.technologyEntity.count({ where: { id: { gt: endCursor } }, })) > 0; - console.log(startCursor); + return { totalCount, edges, From b9bd9438e5b221c39f7c5bc55d71c88a894ab68b Mon Sep 17 00:00:00 2001 From: Maarten Bicknese Date: Thu, 30 Mar 2023 13:22:25 +0200 Subject: [PATCH 09/19] refactor: use global configuration file --- starters/express-apollo-prisma/.env.example | 40 +++-- starters/express-apollo-prisma/README.md | 27 ++- .../express-apollo-prisma/docker-compose.yaml | 34 ++-- starters/express-apollo-prisma/package.json | 2 +- .../prisma/schema.prisma | 2 +- starters/express-apollo-prisma/prisma/seed.ts | 149 +++++++++-------- .../cache/cache-api-wrapper-factory.spec.ts | 156 +++++++----------- .../src/cache/cache-api-wrapper-factory.ts | 13 +- starters/express-apollo-prisma/src/config.ts | 45 +++++ .../server-context-middleware-options.ts | 3 +- starters/express-apollo-prisma/src/main.ts | 21 +-- .../src/queue/job-generator.spec.ts | 56 ++----- .../src/queue/job-generator.ts | 10 +- .../{ => src}/queue/worker.ts | 11 +- 14 files changed, 238 insertions(+), 331 deletions(-) create mode 100644 starters/express-apollo-prisma/src/config.ts rename starters/express-apollo-prisma/{ => src}/queue/worker.ts (71%) diff --git a/starters/express-apollo-prisma/.env.example b/starters/express-apollo-prisma/.env.example index c43efbab2..80277c1b7 100644 --- a/starters/express-apollo-prisma/.env.example +++ b/starters/express-apollo-prisma/.env.example @@ -1,24 +1,22 @@ +# Application PORT=4001 -DATABASE_URL="mysql://root:root@localhost:3307/testdb" -REDIS_URL="redis://:redi$pass@localhost:6380" +CORS_ALLOWED_ORIGINS= # optional. Default value: '*'. Sample value: CORS_ALLOWED_ORIGINS=https://starter.dev,http://127.0.0.1 + +# MySQL +DB_USER=demo +DB_PASSWORD=demopass +DB_DATABASE=demodb +DB_PORT=3306 +DB_URL="mysql://root:${DB_PASSWORD}@localhost:${DB_PORT}/${DB_DATABASE}" + +# Redis +REDIS_PASSWORD='redi$pass' # important! Quotes are required when using special characters +REDIS_HOST=localhost +REDIS_PORT=6379 REDIS_CACHE_TTL_SECONDS=1800 -# For docker-compose.yaml -# mysql -DOCKER_MYSQLDB_ROOT_PASSWORD=root -DOCKER_MYSQLDB_DATABASE=testdb -DOCKER_MYSQLDB_PORT_LOCAL=3307 -DOCKER_MYSQLDB_PORT_CONTAINER=3306 -# redis -DOCKER_REDIS_PASSWORD='redi$pass' # important! single quotes required -DOCKER_REDIS_HOST=localhost -DOCKER_REDIS_PORT_LOCAL=6380 -DOCKER_REDIS_PORT_CONTAINER=6379 -# rabbitmq + +# RabbitMQ AMQP_URL=amqp://localhost:5673 -AMQP_QUEUE_JOB="jobs" -DOCKER_RABBIT_MQ_PORT_CLIENT_API_LOCAL=5673 -DOCKER_RABBIT_MQ_PORT_CLIENT_API_CONTAINER=5672 -DOCKER_RABBIT_MQ_PORT_ADMIN_API_LOCAL=15673 -DOCKER_RABBIT_MQ_PORT_ADMIN_API_CONTAINER=15672 -# cors -CORS_ALLOWED_ORIGINS= # optional. Default value: '*'. Sample value: CORS_ALLOWED_ORIGINS=https://starter.dev,http://127.0.0.1 +AMQP_QUEUE_JOB=jobs +RABBIT_MQ_PORT_CLIENT=5673 +RABBIT_MQ_PORT_ADMIN=15673 diff --git a/starters/express-apollo-prisma/README.md b/starters/express-apollo-prisma/README.md index d51b4a7df..e1bb1c43c 100644 --- a/starters/express-apollo-prisma/README.md +++ b/starters/express-apollo-prisma/README.md @@ -106,27 +106,20 @@ git clone https://github.com/thisdot/starter.dev.git ## Environment Variables - `PORT` - The port exposed to connect with the application. -- `DATABASE_URL` - The database connection URL. -- `REDIS_URL` - The Redis connection URL. +- `DATABASE_URL` - Connector for Prisma to run the migrations +- `DB_USER` - User to use on the MySQL server +- `DB_PASS` - Password for both the user and root user +- `DB_DATABASE` - Name of the database in MySQL +- `DB_PORT` - Which port to run the MySQL server on +- `REDIS_USER` - User to use on the Redis server (can be left blank) +- `REDIS_PASSWORD` - Password to authenticate Redis +- `REDIS_HOST` - Host Redis is running on +- `REDIS_PORT` - Which port to run the Redis server on - `REDIS_CACHE_TTL_SECONDS` - The remaining time(seconds) to live of a key that has a timeout. -- `DOCKER_MYSQLDB_ROOT_PASSWORD` - The MySQL root user password. -- `DOCKER_MYSQLDB_DATABASE` - The MySQL database name. -- `DOCKER_MYSQLDB_PORT_LOCAL` - The MySQL Docker host's TCP port. -- `DOCKER_MYSQLDB_PORT_CONTAINER` - The MySQL Docker container's TCP port. -- `DOCKER_REDIS_PASSWORD` - The Redis password. -- `DOCKER_REDIS_HOST` - The Redis host IP. -- `DOCKER_REDIS_PORT_LOCAL` - The Redis Docker host's TCP port. -- `DOCKER_REDIS_PORT_CONTAINER` - The Redis Docker container's TCP port. - `AMQP_URL` - The RabbitMQ connection URL. - `AMQP_QUEUE_JOB` - The RabbitMQ channel queue name. - `CORS_ALLOWED_ORIGINS` - (Optional) Comma separated Allowed Origins. Default value: '\*'. (See [CORS Cross-Origin Resource Sharing](#cors-cross-origin-resource-sharing)) -We map TCP port `DOCKER_MYSQLDB_PORT_CONTAINER` in the container to port `DOCKER_MYSQLDB_PORT_LOCAL` on the Docker host. -We also map TCP port `DOCKER_REDIS_PORT_LOCAL` in the container to port `DOCKER_REDIS_PORT_CONTAINER` on the Docker host. - -To ensure proper connection to our resources -For more information on Docker container networks: https://docs.docker.com/config/containers/container-networking/ - ### Database and Redis To start up your API in development mode with an active database connection, please follow the following steps: @@ -298,7 +291,7 @@ The data sources are located in `src/graphql/data-sources`. The data sources of ### ORM -The kit uses Prisma as a TypeScript ORM for proper data fetch and mutation from the source. It is configured with the following environment variable: `DATABASE_URL="mysql://root:root@localhost:3307/testdb"`. +The kit uses Prisma as a TypeScript ORM for proper data fetch and mutation from the source. It is configured with the `DATABASE_URL` environment variable. We use Prisma for the following: diff --git a/starters/express-apollo-prisma/docker-compose.yaml b/starters/express-apollo-prisma/docker-compose.yaml index fbadf971f..302ec4da8 100644 --- a/starters/express-apollo-prisma/docker-compose.yaml +++ b/starters/express-apollo-prisma/docker-compose.yaml @@ -2,45 +2,35 @@ version: '3.8' services: mysql: - container_name: mysql image: mysql:8.0 restart: unless-stopped environment: - - MYSQL_ROOT_PASSWORD=$DOCKER_MYSQLDB_ROOT_PASSWORD - - MYSQL_DATABASE=$DOCKER_MYSQLDB_DATABASE + - MYSQL_PASSWORD=${DB_PASSWORD:-demopass} + - MYSQL_ROOT_PASSWORD=${DB_PASSWORD:-demopass} + - MYSQL_DATABASE=${DB_DATABASE:-demodb} ports: - - $DOCKER_MYSQLDB_PORT_LOCAL:$DOCKER_MYSQLDB_PORT_CONTAINER + - '${DB_PORT:-3306}:3306' volumes: - db:/var/lib/mysql + redis: - container_name: redis - image: 'redis:7.0-alpine' + image: redis:7.0-alpine restart: unless-stopped - env_file: ./.env command: - --loglevel warning - - --requirepass "$DOCKER_REDIS_PASSWORD" + - --requirepass ${REDIS_PASSWORD:-redi$$pass} ports: - - $DOCKER_REDIS_PORT_LOCAL:$DOCKER_REDIS_PORT_CONTAINER - environment: - - REDIS_REPLICATION_MODE=master - depends_on: - - mysql + - '${REDIS_PORT:-6379}:6379' + rabbitmq: image: rabbitmq:3.8-management-alpine - container_name: 'rabbitmq' restart: unless-stopped ports: - - $DOCKER_RABBIT_MQ_PORT_CLIENT_API_LOCAL:$DOCKER_RABBIT_MQ_PORT_CLIENT_API_CONTAINER - - $DOCKER_RABBIT_MQ_PORT_ADMIN_API_LOCAL:$DOCKER_RABBIT_MQ_PORT_ADMIN_API_CONTAINER + - '${RABBIT_MQ_PORT_CLIENT:-5672}:5672' + - '${RABBIT_MQ_PORT_ADMIN:-15672}:15672' volumes: - queue:/var/lib/rabbitmq/ - - ./.docker-conf/rabbitmq/log/:/var/log/rabbitmq - networks: - - rabbitmq_nodejs -networks: - rabbitmq_nodejs: - driver: bridge + volumes: db: queue: diff --git a/starters/express-apollo-prisma/package.json b/starters/express-apollo-prisma/package.json index f88d8b6f0..f79489164 100644 --- a/starters/express-apollo-prisma/package.json +++ b/starters/express-apollo-prisma/package.json @@ -30,7 +30,7 @@ "infrastructure:up": "docker compose up -d", "infrastructure:pause": "docker compose stop", "infrastructure:down": "docker compose down --remove-orphans --volumes", - "queue:run": "ts-node queue/worker.ts" + "queue:run": "ts-node src/queue/worker.ts" }, "author": "", "devDependencies": { diff --git a/starters/express-apollo-prisma/prisma/schema.prisma b/starters/express-apollo-prisma/prisma/schema.prisma index dbb5003cd..6b33bcace 100644 --- a/starters/express-apollo-prisma/prisma/schema.prisma +++ b/starters/express-apollo-prisma/prisma/schema.prisma @@ -7,7 +7,7 @@ generator client { datasource db { provider = "mysql" - url = env("DATABASE_URL") + url = env("DB_URL") } model TechnologyEntity { diff --git a/starters/express-apollo-prisma/prisma/seed.ts b/starters/express-apollo-prisma/prisma/seed.ts index bbc511202..84128d7a4 100644 --- a/starters/express-apollo-prisma/prisma/seed.ts +++ b/starters/express-apollo-prisma/prisma/seed.ts @@ -1,83 +1,82 @@ -import { PrismaClient, Prisma } from '@prisma/client' -import * as dotenv from 'dotenv'; +import {PrismaClient, Prisma} from '@prisma/client' +import {PRISMA_CONFIG} from "../src/config"; -dotenv.config(); - -const prisma = new PrismaClient() +const prisma = new PrismaClient(PRISMA_CONFIG) const RECORDS: Prisma.TechnologyEntityCreateInput[] = [ - { - displayName: 'Node.js', - description: 'JavaScript runtime built on Chrome V8 JavaScript engine', - url: 'https://nodejs.org/' - }, - { - displayName: 'TypeScript', - description: 'Strongly typed programming language that builds on JavaScript, giving you better tooling at any scale', - url: 'https://www.typescriptlang.org/' - }, - { - displayName: 'Nodemon', - description: 'Simple monitor utility for use during development of a Node.js app, that will monitor for any changes in your source and automatically restart your server', - url: 'https://nodemon.io/' - }, - { - displayName: 'Express', - description: 'Fast, unopinionated, minimalist web framework for Node.js', - url: 'https://expressjs.com/' - }, - { - displayName: 'Apollo GrapQL', - description: 'The GraphQL developer platform', - url: 'https://www.apollographql.com/' - }, - { - displayName: 'Prisma', - description: 'Next-generation Node.js and TypeScript ORM', - url: 'https://www.prisma.io/' - }, - { - displayName: 'Redis', - description: 'The open source, in-memory data store used by millions of developers as a database, cache, streaming engine, and message broker', - url: 'https://redis.io/' - }, - { - displayName: 'RabbitMQ', - description: 'Open source message broker', - url: 'https://www.rabbitmq.com/' - }, - { - displayName: 'Jest', - description: 'JavaScript Testing Framework with a focus on simplicity', - url: 'https://jestjs.io/' - }, - { - displayName: 'Docker', - description: 'Platform designed to help developers build, share, and run modern applications', - url: 'https://www.docker.com/' - }, - { - displayName: 'Prettier', - description: 'Opinionated Code Formatter', - url: 'https://prettier.io/' - } + { + displayName: 'Node.js', + description: 'JavaScript runtime built on Chrome V8 JavaScript engine', + url: 'https://nodejs.org/' + }, + { + displayName: 'TypeScript', + description: 'Strongly typed programming language that builds on JavaScript, giving you better tooling at any scale', + url: 'https://www.typescriptlang.org/' + }, + { + displayName: 'Nodemon', + description: 'Simple monitor utility for use during development of a Node.js app, that will monitor for any changes in your source and automatically restart your server', + url: 'https://nodemon.io/' + }, + { + displayName: 'Express', + description: 'Fast, unopinionated, minimalist web framework for Node.js', + url: 'https://expressjs.com/' + }, + { + displayName: 'Apollo GrapQL', + description: 'The GraphQL developer platform', + url: 'https://www.apollographql.com/' + }, + { + displayName: 'Prisma', + description: 'Next-generation Node.js and TypeScript ORM', + url: 'https://www.prisma.io/' + }, + { + displayName: 'Redis', + description: 'The open source, in-memory data store used by millions of developers as a database, cache, streaming engine, and message broker', + url: 'https://redis.io/' + }, + { + displayName: 'RabbitMQ', + description: 'Open source message broker', + url: 'https://www.rabbitmq.com/' + }, + { + displayName: 'Jest', + description: 'JavaScript Testing Framework with a focus on simplicity', + url: 'https://jestjs.io/' + }, + { + displayName: 'Docker', + description: 'Platform designed to help developers build, share, and run modern applications', + url: 'https://www.docker.com/' + }, + { + displayName: 'Prettier', + description: 'Opinionated Code Formatter', + url: 'https://prettier.io/' + } ] async function main() { - const promises = RECORDS.map(record => prisma.technologyEntity.upsert({ - where: { displayName: record.displayName }, - update: {}, - create: record, - })); - const results = await Promise.all(promises); - console.log(`Seeding completed successfully:`, results) + const promises = RECORDS.map(record => prisma.technologyEntity.upsert({ + where: {displayName: record.displayName}, + update: {}, + create: record, + })); + const results = await Promise.all(promises); + console.log(`Seeding completed successfully:`, results) } + main() - .then(async () => { - await prisma.$disconnect() - }) - .catch(async (e) => { - console.error(e) - await prisma.$disconnect() - process.exit(1) - }) + .then(async () => { + await prisma.$disconnect() + }) + .catch(async (e) => { + console.error(e) + await prisma.$disconnect() + process.exit(1) + }) diff --git a/starters/express-apollo-prisma/src/cache/cache-api-wrapper-factory.spec.ts b/starters/express-apollo-prisma/src/cache/cache-api-wrapper-factory.spec.ts index 0dd0e1859..d7de9bd63 100644 --- a/starters/express-apollo-prisma/src/cache/cache-api-wrapper-factory.spec.ts +++ b/starters/express-apollo-prisma/src/cache/cache-api-wrapper-factory.spec.ts @@ -4,6 +4,15 @@ import { CacheAPIWrapper } from './cache-api-wrapper'; import { createCacheAPIWrapperAsync } from './cache-api-wrapper-factory'; import { connectRedisClient } from './redis'; import { RedisCacheAPIWrapper } from './redis-cache-api-wrapper'; +import { + REDIS_URL as MOCK_REDIS_URL, + REDIS_CACHE_TTL_SECONDS as MOCK_REDIS_CACHE_TTL_SECONDS, +} from '../config'; + +jest.mock('../config', () => ({ + REDIS_CACHE_TTL_SECONDS: 123, + REDIS_URL: 'MOCK_REDIS_URL', +})); jest.mock('./redis/connect-redis-client', () => ({ connectRedisClient: jest.fn(), @@ -22,115 +31,68 @@ type CacheAPIWrapperType = CacheAPIWrapper< const MOCK_REDIS_CACHE_API_WRAPPER_CONSTRUCTOR = RedisCacheAPIWrapper as unknown as jest.Mock; -const MOCK_REDIS_URL = 'MOCK_REDIS_URL'; -const MOCK_REDIS_CACHE_TTL_SECONDS_NUMBER = 123; -const MOCK_REDIS_CACHE_TTL_SECONDS = String(MOCK_REDIS_CACHE_TTL_SECONDS_NUMBER); -const MOCK_REDIS_CACHE_TTL_SECONDS_INVALID = 'MOCK_REDIS_CACHE_TTL_SECONDS_INVALID'; - const MOCK_CACHE_KEY_PREFIX = 'MOCK_CACHE_KEY_PREFIX'; const MOCK_REDIS_CLIENT = createMockRedisClient(); const MOCK_CACHE_API_WRAPPER = createMockCacheApiWrapper(); describe('.createCacheAPIWrapperAsync', () => { - let originalEnv: NodeJS.ProcessEnv; - beforeAll(() => { - originalEnv = process.env; - }); - afterAll(() => { - process.env = originalEnv; - }); - describe('when called', () => { - describe('and evironment variable REDIS_URL set', () => { - beforeAll(() => { - process.env = { REDIS_URL: MOCK_REDIS_URL }; + describe('and Redis available', () => { + let result: CacheAPIWrapperType | null; + + beforeAll(async () => { + MOCK_CONNECT_REDIS_CLIENT.mockResolvedValue(MOCK_REDIS_CLIENT); + MOCK_REDIS_CACHE_API_WRAPPER_CONSTRUCTOR.mockReturnValue(MOCK_CACHE_API_WRAPPER); + + result = await createCacheAPIWrapperAsync(MOCK_CACHE_KEY_PREFIX); + }); + + afterAll(() => { + MOCK_CONNECT_REDIS_CLIENT.mockReset(); + MOCK_REDIS_CACHE_API_WRAPPER_CONSTRUCTOR.mockReset(); + }); + + it('calls .connectRedisClient once with expected argument', async () => { + expect(MOCK_CONNECT_REDIS_CLIENT).toHaveBeenCalledTimes(1); + expect(MOCK_CONNECT_REDIS_CLIENT).toHaveBeenCalledWith(MOCK_REDIS_URL); }); - describe('and evironment variable REDIS_CACHE_TTL_SECONDS is valid', () => { - describe('and Redis available', () => { - let result: CacheAPIWrapperType | null; - - beforeAll(async () => { - process.env = { ...process.env, REDIS_CACHE_TTL_SECONDS: MOCK_REDIS_CACHE_TTL_SECONDS }; - MOCK_CONNECT_REDIS_CLIENT.mockResolvedValue(MOCK_REDIS_CLIENT); - MOCK_REDIS_CACHE_API_WRAPPER_CONSTRUCTOR.mockReturnValue(MOCK_CACHE_API_WRAPPER); - - result = await createCacheAPIWrapperAsync(MOCK_CACHE_KEY_PREFIX); - }); - - afterAll(() => { - MOCK_CONNECT_REDIS_CLIENT.mockReset(); - MOCK_REDIS_CACHE_API_WRAPPER_CONSTRUCTOR.mockReset(); - }); - - it('calls .connectRedisClient once with expected argument', async () => { - expect(MOCK_CONNECT_REDIS_CLIENT).toHaveBeenCalledTimes(1); - expect(MOCK_CONNECT_REDIS_CLIENT).toHaveBeenCalledWith(MOCK_REDIS_URL); - }); - - it('calls RedisCacheAPIWrapper constructor with expected arguments', async () => { - expect(MOCK_REDIS_CACHE_API_WRAPPER_CONSTRUCTOR).toHaveBeenCalledTimes(1); - expect(MOCK_REDIS_CACHE_API_WRAPPER_CONSTRUCTOR).toHaveBeenCalledWith( - MOCK_REDIS_CLIENT, - MOCK_CACHE_KEY_PREFIX, - MOCK_REDIS_CACHE_TTL_SECONDS_NUMBER - ); - }); - - it('returns expected result', () => { - expect(result).toBe(MOCK_CACHE_API_WRAPPER); - }); - }); - - describe('and Redis unavailable', () => { - let result: CacheAPIWrapperType | null; - - beforeAll(async () => { - process.env = { ...process.env, REDIS_CACHE_TTL_SECONDS: MOCK_REDIS_CACHE_TTL_SECONDS }; - MOCK_CONNECT_REDIS_CLIENT.mockResolvedValue(undefined); - MOCK_REDIS_CACHE_API_WRAPPER_CONSTRUCTOR.mockReturnValue(MOCK_CACHE_API_WRAPPER); - - result = await createCacheAPIWrapperAsync(MOCK_CACHE_KEY_PREFIX); - }); - - afterAll(() => { - MOCK_CONNECT_REDIS_CLIENT.mockReset(); - MOCK_REDIS_CACHE_API_WRAPPER_CONSTRUCTOR.mockReset(); - }); - - it('calls .connectRedisClient once with expected argument', async () => { - expect(MOCK_CONNECT_REDIS_CLIENT).toHaveBeenCalledTimes(1); - expect(MOCK_CONNECT_REDIS_CLIENT).toHaveBeenCalledWith(MOCK_REDIS_URL); - }); - - it('returns expected result', () => { - expect(result).toBe(null); - }); - }); + + it('calls RedisCacheAPIWrapper constructor with expected arguments', async () => { + expect(MOCK_REDIS_CACHE_API_WRAPPER_CONSTRUCTOR).toHaveBeenCalledTimes(1); + expect(MOCK_REDIS_CACHE_API_WRAPPER_CONSTRUCTOR).toHaveBeenCalledWith( + MOCK_REDIS_CLIENT, + MOCK_CACHE_KEY_PREFIX, + MOCK_REDIS_CACHE_TTL_SECONDS + ); }); - describe('and evironment variable REDIS_CACHE_TTL_SECONDS is invalid', () => { - beforeAll(async () => { - process.env = { - ...process.env, - REDIS_CACHE_TTL_SECONDS: MOCK_REDIS_CACHE_TTL_SECONDS_INVALID, - }; - }); - - it('throws expected error', async () => { - await expect(createCacheAPIWrapperAsync).rejects.toThrowError( - '[Invalid environment] Invalid variable: REDIS_CACHE_TTL_SECONDS. Should be a number' - ); - }); + + it('returns expected result', () => { + expect(result).toBe(MOCK_CACHE_API_WRAPPER); }); }); - describe('and evironment variable REDIS_URL not set', () => { - beforeAll(() => { - process.env = {}; + describe('and Redis unavailable', () => { + let result: CacheAPIWrapperType | null; + + beforeAll(async () => { + MOCK_CONNECT_REDIS_CLIENT.mockResolvedValue(undefined); + MOCK_REDIS_CACHE_API_WRAPPER_CONSTRUCTOR.mockReturnValue(MOCK_CACHE_API_WRAPPER); + + result = await createCacheAPIWrapperAsync(MOCK_CACHE_KEY_PREFIX); }); - it('throws expected error', async () => { - await expect(createCacheAPIWrapperAsync).rejects.toThrowError( - '[Invalid environment] Variable not found: REDIS_URL' - ); + + afterAll(() => { + MOCK_CONNECT_REDIS_CLIENT.mockReset(); + MOCK_REDIS_CACHE_API_WRAPPER_CONSTRUCTOR.mockReset(); + }); + + it('calls .connectRedisClient once with expected argument', async () => { + expect(MOCK_CONNECT_REDIS_CLIENT).toHaveBeenCalledTimes(1); + expect(MOCK_CONNECT_REDIS_CLIENT).toHaveBeenCalledWith(MOCK_REDIS_URL); + }); + + it('returns expected result', () => { + expect(result).toBe(null); }); }); }); diff --git a/starters/express-apollo-prisma/src/cache/cache-api-wrapper-factory.ts b/starters/express-apollo-prisma/src/cache/cache-api-wrapper-factory.ts index c134483f9..011e3c404 100644 --- a/starters/express-apollo-prisma/src/cache/cache-api-wrapper-factory.ts +++ b/starters/express-apollo-prisma/src/cache/cache-api-wrapper-factory.ts @@ -1,6 +1,7 @@ import { CacheAPIWrapper } from './cache-api-wrapper'; import { RedisCacheAPIWrapper } from './redis-cache-api-wrapper'; import { connectRedisClient } from './redis/connect-redis-client'; +import { REDIS_CACHE_TTL_SECONDS, REDIS_URL } from '../config'; export const createCacheAPIWrapperAsync = async < TEntity extends { [k: string]: number | string | null }, @@ -8,18 +9,6 @@ export const createCacheAPIWrapperAsync = async < >( cacheKeyPrefix: string ): Promise | null> => { - const REDIS_URL = process.env.REDIS_URL; - if (!REDIS_URL) { - throw new Error('[Invalid environment] Variable not found: REDIS_URL'); - } - - const REDIS_CACHE_TTL_SECONDS_STRING = process.env.REDIS_CACHE_TTL_SECONDS; - const REDIS_CACHE_TTL_SECONDS = Number(REDIS_CACHE_TTL_SECONDS_STRING); - if (REDIS_CACHE_TTL_SECONDS_STRING && isNaN(REDIS_CACHE_TTL_SECONDS)) { - throw new Error( - '[Invalid environment] Invalid variable: REDIS_CACHE_TTL_SECONDS. Should be a number' - ); - } const redisClient = await connectRedisClient(REDIS_URL); return redisClient ? new RedisCacheAPIWrapper(redisClient, cacheKeyPrefix, REDIS_CACHE_TTL_SECONDS) diff --git a/starters/express-apollo-prisma/src/config.ts b/starters/express-apollo-prisma/src/config.ts new file mode 100644 index 000000000..911823957 --- /dev/null +++ b/starters/express-apollo-prisma/src/config.ts @@ -0,0 +1,45 @@ +import * as dotenv from 'dotenv'; +import { Prisma } from '@prisma/client'; +dotenv.config(); + +// Application port to listen on +export const PORT = process.env.PORT ? Number(process.env.PORT) : 4001; + +export let CORS_ALLOWED_ORIGINS: string[] | undefined; +if (process.env.CORS_ALLOWED_ORIGINS && process.env.CORS_ALLOWED_ORIGINS !== '*') { + CORS_ALLOWED_ORIGINS = process.env.CORS_ALLOWED_ORIGINS.split(','); +} + +// Build Redis URL based on the separate values +export const REDIS_URL = [ + 'redis://', + process.env.REDIS_USER || '', + ':', + process.env.REDIS_PASSWORD || 'redi$pass', + '@', + process.env.REDIS_HOST || 'localhost', + ':', + process.env.REDIS_PORT || '6379', +].join(''); +export const REDIS_CACHE_TTL_SECONDS = process.env.REDIS_CACHE_TTL_SECONDS + ? Number(process.env.REDIS_CACHE_TTL_SECONDS) + : 3600; + +// Database configuration +export const DB_PORT = process.env.DB_PORT ? Number(process.env.DB_PORT) : 3306; +export const DB_DATABASE = process.env.DB_DATABASE || 'demodb'; +export const DB_PASSWORD = process.env.DB_DATABASE || 'demopass'; +export const DB_USER = process.env.DB_USER || 'demo'; + +export const PRISMA_CONFIG: Prisma.PrismaClientOptions = { + datasources: { + db: { + url: `mysql://${DB_USER}:${DB_PASSWORD}@localhost:${DB_PORT}/${DB_DATABASE}`, + }, + }, +}; + +// Queue +export const RABBIT_MQ_PORT_CLIENT = process.env.RABBIT_MQ_PORT_CLIENT || '5673'; +export const AMQP_URL = process.env.AMQP_URL || 'amqp://localhost:' + RABBIT_MQ_PORT_CLIENT; +export const AMQP_QUEUE_JOB = process.env.AMQP_QUEUE_JOB || 'jobs'; diff --git a/starters/express-apollo-prisma/src/graphql/server-context/server-context-middleware-options.ts b/starters/express-apollo-prisma/src/graphql/server-context/server-context-middleware-options.ts index 88c0a5389..2262451a1 100644 --- a/starters/express-apollo-prisma/src/graphql/server-context/server-context-middleware-options.ts +++ b/starters/express-apollo-prisma/src/graphql/server-context/server-context-middleware-options.ts @@ -4,11 +4,12 @@ import { WithRequired } from '@apollo/utils.withrequired'; import { TechnologyDataSource } from '../data-sources'; import { PrismaClient, TechnologyEntity } from '@prisma/client'; import { createCacheAPIWrapperAsync } from '../../cache'; +import { PRISMA_CONFIG } from '../../config'; export const createServerContextMiddlewareOptionsAsync = async (): Promise< WithRequired, 'context'> > => { - const prismaClient = new PrismaClient(); + const prismaClient = new PrismaClient(PRISMA_CONFIG); const technologyCacheAPIWrapper = await createCacheAPIWrapperAsync( 'technology' ); diff --git a/starters/express-apollo-prisma/src/main.ts b/starters/express-apollo-prisma/src/main.ts index deeee0087..2b1c473b1 100644 --- a/starters/express-apollo-prisma/src/main.ts +++ b/starters/express-apollo-prisma/src/main.ts @@ -4,28 +4,11 @@ import http from 'http'; import cors from 'cors'; import bodyParser from 'body-parser'; import { graphqlServer, createGraphqlServerMiddlewareAsync } from './graphql'; -import * as dotenv from 'dotenv'; import { connectRedisClient } from './cache/redis'; import { createHealthcheckHandler } from './healthcheck'; import { jobGeneratorHandler } from './queue/job-generator-handler'; import { PrismaClient } from '@prisma/client'; - -dotenv.config(); - -const PORT = Number(process.env.PORT); -if (isNaN(PORT)) { - throw new Error(`[Invalid environment] Variable not found: PORT`); -} - -const REDIS_URL = process.env.REDIS_URL; -if (!REDIS_URL) { - throw new Error(`[Invalid environment] Variable not found: REDIS_URL`); -} - -let CORS_ALLOWED_ORIGINS: string[] | undefined; -if (process.env.CORS_ALLOWED_ORIGINS && process.env.CORS_ALLOWED_ORIGINS !== '*') { - CORS_ALLOWED_ORIGINS = process.env.CORS_ALLOWED_ORIGINS.split(','); -} +import { CORS_ALLOWED_ORIGINS, PORT, PRISMA_CONFIG, REDIS_URL } from './config'; (async () => { // Required logic for integrating with Express @@ -54,7 +37,7 @@ if (process.env.CORS_ALLOWED_ORIGINS && process.env.CORS_ALLOWED_ORIGINS !== '*' // Set up server-related Express middleware app.use('/graphql', await createGraphqlServerMiddlewareAsync()); - const prismaClient = new PrismaClient(); + const prismaClient = new PrismaClient(PRISMA_CONFIG); app.use('/health', createHealthcheckHandler({ redisClient, prismaClient })); app.post('/example-job', jobGeneratorHandler); diff --git a/starters/express-apollo-prisma/src/queue/job-generator.spec.ts b/starters/express-apollo-prisma/src/queue/job-generator.spec.ts index dc4aa830d..febfe9245 100644 --- a/starters/express-apollo-prisma/src/queue/job-generator.spec.ts +++ b/starters/express-apollo-prisma/src/queue/job-generator.spec.ts @@ -1,10 +1,14 @@ import { connect, Replies } from 'amqplib'; import { createMockChannel, createMockConnection } from '../mocks/amqplib'; import { generateJob } from './job-generator'; +import { AMQP_QUEUE_JOB as MOCK_AMQP_QUEUE_JOB, AMQP_URL as MOCK_AMQP_URL } from '../config'; + +jest.mock('../config', () => ({ + AMQP_QUEUE_JOB: 'MOCK_ENV_AMQP_QUEUE_JOB', + AMQP_URL: 'MOCK_AMQP_URL', +})); const MOCK_MESSAGE = 'MOCK_MESSAGE'; -const MOCK_ENV_AMQP_URL = 'MOCK_AMQP_URL'; -const MOCK_ENV_AMQP_QUEUE_JOB = 'MOCK_ENV_AMQP_QUEUE_JOB'; const MOCK_AMQP_CONNECTION = createMockConnection(); const MOCK_AMPQ_CHANNEL = createMockChannel(); @@ -23,52 +27,16 @@ const MOCK_AMQP_CONNECT = connect as jest.MockedFn; const SPY_CONSOLE_WARN = jest.spyOn(console, 'warn'); describe('.generateJob', () => { - const OROGINAL_ENV = process.env; beforeAll(() => { SPY_CONSOLE_WARN.mockImplementation(jest.fn()); }); afterAll(() => { SPY_CONSOLE_WARN.mockRestore(); - process.env = OROGINAL_ENV; }); describe('when called with message', () => { - describe('and environment variable AMQP_URL not set', () => { - const REPRODUCER_FN = async () => { - await generateJob(MOCK_MESSAGE); - }; - - beforeAll(async () => { - process.env = {}; - }); - - it('throws expected error', async () => { - await expect(REPRODUCER_FN).rejects.toThrowError( - '[Invalid environment] Variable not found: AMQP_URL' - ); - }); - }); - - describe('and environment variable AMQP_QUEUE_JOB not set', () => { - const REPRODUCER_FN = async () => { - await generateJob(MOCK_MESSAGE); - }; - - beforeAll(() => { - process.env = { - AMQP_URL: MOCK_ENV_AMQP_URL, - }; - }); - - it('throws expected error', async () => { - await expect(REPRODUCER_FN).rejects.toThrowError( - '[Invalid environment] Variable not found: AMQP_QUEUE_JOB' - ); - }); - }); - - describe('and environment variables set', () => { + describe('and correctly configured', () => { const MOCK_INSTANCE_ERROR = new Error(); type ExpectedFlow = { @@ -175,10 +143,6 @@ describe('.generateJob', () => { let result: boolean; beforeAll(async () => { - process.env = { - AMQP_URL: MOCK_ENV_AMQP_URL, - AMQP_QUEUE_JOB: MOCK_ENV_AMQP_QUEUE_JOB, - }; mockAwaitedResultValue(MOCK_AMQP_CONNECT, mockAMPQConnectResult); mockAwaitedResultValue( MOCK_AMQP_CONNECTION.createChannel, @@ -202,7 +166,7 @@ describe('.generateJob', () => { it('calls amqplib.connect method once with expected argument', () => { expect(MOCK_AMQP_CONNECT).toHaveBeenCalledTimes(1); - expect(MOCK_AMQP_CONNECT).toHaveBeenCalledWith(MOCK_ENV_AMQP_URL); + expect(MOCK_AMQP_CONNECT).toHaveBeenCalledWith(MOCK_AMQP_URL); }); expectedFlow.createsChannel && @@ -213,7 +177,7 @@ describe('.generateJob', () => { expectedFlow.assertsQueue && it('calls Channel.assertQueue method once with expected argument', () => { expect(MOCK_AMPQ_CHANNEL.assertQueue).toHaveBeenCalledTimes(1); - expect(MOCK_AMPQ_CHANNEL.assertQueue).toHaveBeenCalledWith(MOCK_ENV_AMQP_QUEUE_JOB); + expect(MOCK_AMPQ_CHANNEL.assertQueue).toHaveBeenCalledWith(MOCK_AMQP_QUEUE_JOB); }); expectedFlow.sendsMessageToQueue && @@ -221,7 +185,7 @@ describe('.generateJob', () => { expect(MOCK_AMPQ_CHANNEL.sendToQueue).toHaveBeenCalledTimes(1); const expectedBuffer = Buffer.from(MOCK_MESSAGE); expect(MOCK_AMPQ_CHANNEL.sendToQueue).toHaveBeenCalledWith( - MOCK_ENV_AMQP_QUEUE_JOB, + MOCK_AMQP_QUEUE_JOB, expectedBuffer, { persistent: true, diff --git a/starters/express-apollo-prisma/src/queue/job-generator.ts b/starters/express-apollo-prisma/src/queue/job-generator.ts index 681b63b59..97654b19f 100644 --- a/starters/express-apollo-prisma/src/queue/job-generator.ts +++ b/starters/express-apollo-prisma/src/queue/job-generator.ts @@ -1,16 +1,8 @@ import { Connection, Channel, connect } from 'amqplib'; +import { AMQP_QUEUE_JOB, AMQP_URL } from '../config'; export const generateJob = async (message: string): Promise => { let success: boolean; - const AMQP_URL = process.env.AMQP_URL; - if (!AMQP_URL) { - throw new Error('[Invalid environment] Variable not found: AMQP_URL'); - } - - const AMQP_QUEUE_JOB = process.env.AMQP_QUEUE_JOB; - if (!AMQP_QUEUE_JOB) { - throw new Error('[Invalid environment] Variable not found: AMQP_QUEUE_JOB'); - } let connection: Connection | undefined; let channel: Channel | undefined; diff --git a/starters/express-apollo-prisma/queue/worker.ts b/starters/express-apollo-prisma/src/queue/worker.ts similarity index 71% rename from starters/express-apollo-prisma/queue/worker.ts rename to starters/express-apollo-prisma/src/queue/worker.ts index 02ebddab4..3ede321b4 100644 --- a/starters/express-apollo-prisma/queue/worker.ts +++ b/starters/express-apollo-prisma/src/queue/worker.ts @@ -1,19 +1,10 @@ import amqplib from 'amqplib'; import * as dotenv from 'dotenv'; +import { AMQP_QUEUE_JOB, AMQP_URL } from '../config'; dotenv.config(); (async () => { - const AMQP_URL = process.env.AMQP_URL; - if (!AMQP_URL) { - throw new Error(`[Invalid environment] Variable not found: AMQP_URL`); - } - - const AMQP_QUEUE_JOB = process.env.AMQP_QUEUE_JOB; - if (!AMQP_QUEUE_JOB) { - throw new Error(`[Invalid environment] Variable not found: AMQP_QUEUE_JOB`); - } - const connection = await amqplib.connect(AMQP_URL); const channel = await connection.createChannel(); From 5cf6aedd24b74f8bf0d6ad82e073fad00ecc3879 Mon Sep 17 00:00:00 2001 From: Ian Mungai Date: Thu, 30 Mar 2023 15:39:18 +0300 Subject: [PATCH 10/19] chore: add node --- .../data-sources/technology-data-source.ts | 29 +++++-- .../src/graphql/mappers/technology.ts | 8 +- .../schema/generated/graphql.schema.json | 81 +++++++++++++++++-- .../graphql/schema/generated/schema.graphql | 18 ++++- .../graphql/schema/generated/types/index.ts | 36 +++++++-- .../schema/technology/technology.typedefs.ts | 18 ++++- 6 files changed, 163 insertions(+), 27 deletions(-) diff --git a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts index ca6e22855..e8d523495 100644 --- a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts +++ b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts @@ -4,14 +4,20 @@ import { InputMaybe } from '../schema/generated/types'; type TechnologyEntityId = TechnologyEntity['id']; +type TechnologyNode = { + cursor: TechnologyEntityId; + node: TechnologyEntity; +}; + export type TechnologyEntityCollection = { totalCount: number; - edges: TechnologyEntity[]; pageInfo: { hasNextPage: boolean; - startCursor?: number; - endCursor?: number; + hasPreviousPage: boolean; + startCursor?: TechnologyEntityId; + endCursor?: TechnologyEntityId; }; + edges: TechnologyNode[]; }; export class TechnologyDataSource { @@ -42,7 +48,7 @@ export class TechnologyDataSource { ): Promise { const where: Prisma.TechnologyEntityWhereInput = after ? { id: { gt: after } } : {}; - const [totalCount, edges] = await this.prismaClient.$transaction([ + const [totalCount, items] = await this.prismaClient.$transaction([ this.prismaClient.technologyEntity.count(), this.prismaClient.technologyEntity.findMany({ where, @@ -51,18 +57,29 @@ export class TechnologyDataSource { }), ]); - const startCursor = edges.length > 0 ? edges[0].id : undefined; - const endCursor = edges.length > 0 ? edges[edges.length - 1].id : undefined; + const startCursor = items.length > 0 ? items[0].id : undefined; + const endCursor = items.length > 0 ? items[items.length - 1].id : undefined; const hasNextPage = (await this.prismaClient.technologyEntity.count({ where: { id: { gt: endCursor } }, })) > 0; + const hasPreviousPage = + (await this.prismaClient.technologyEntity.count({ + where: { id: { lt: items[0].id } }, + })) > 0; + + const edges = items.map((node) => ({ + cursor: node.id, + node, + })); + return { totalCount, edges, pageInfo: { hasNextPage, + hasPreviousPage, startCursor, endCursor, }, diff --git a/starters/express-apollo-prisma/src/graphql/mappers/technology.ts b/starters/express-apollo-prisma/src/graphql/mappers/technology.ts index 370ad1c2d..ba1b4a34f 100644 --- a/starters/express-apollo-prisma/src/graphql/mappers/technology.ts +++ b/starters/express-apollo-prisma/src/graphql/mappers/technology.ts @@ -1,6 +1,6 @@ import { TechnologyEntity } from '@prisma/client'; import { TechnologyEntityCollection } from '../data-sources'; -import { Technology, TechnologyCollection } from '../schema/generated/types'; +import { Technology, TechnologyCollection, TechnologyNode } from '../schema/generated/types'; export const mapTechnology = (entity: TechnologyEntity): Technology => ({ __typename: 'Technology', @@ -15,7 +15,11 @@ export const mapTechnologyCollection = ( ): TechnologyCollection => { return { totalCount: entityCollectionPage.totalCount, - edges: entityCollectionPage.edges.map(mapTechnology), + edges: entityCollectionPage.edges.map((entity) => ({ + __typename: 'TechnologyNode', + node: mapTechnology(entity.node), + cursor: entity.cursor, + })), pageInfo: entityCollectionPage.pageInfo, }; }; diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json b/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json index 355f975a0..3cc6c87ce 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json @@ -233,9 +233,29 @@ "description": "Shows if there is a page after", "args": [], "type": { - "kind": "SCALAR", - "name": "Boolean", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "hasPreviousPage", + "description": "Shows if there is a page before", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } }, "isDeprecated": false, "deprecationReason": null @@ -437,7 +457,7 @@ "name": null, "ofType": { "kind": "OBJECT", - "name": "Technology", + "name": "TechnologyNode", "ofType": null } } @@ -450,9 +470,13 @@ "description": "Pagination Information", "args": [], "type": { - "kind": "OBJECT", - "name": "PaginationInformation", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PaginationInformation", + "ofType": null + } }, "isDeprecated": false, "deprecationReason": null @@ -479,6 +503,49 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "TechnologyNode", + "description": "Pagination Technology Node", + "fields": [ + { + "name": "cursor", + "description": "Current Cursor for Entity Node", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "Technology Entity Node", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Technology", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "UpdateTechnology", diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql b/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql index b82bc3adf..3b02e6c36 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql @@ -31,7 +31,9 @@ type PaginationInformation { "Last cursor in page" endCursor: Int "Shows if there is a page after" - hasNextPage: Boolean + hasNextPage: Boolean! + "Shows if there is a page before" + hasPreviousPage: Boolean! "First cursor in page" startCursor: Int } @@ -65,13 +67,23 @@ A collection of technologies """ type TechnologyCollection { "A list of records of the requested page" - edges: [Technology]! + edges: [TechnologyNode]! "Pagination Information" - pageInfo: PaginationInformation + pageInfo: PaginationInformation! "Identifies the total count of technology records in data source" totalCount: Int! } +""" +Pagination Technology Node +""" +type TechnologyNode { + "Current Cursor for Entity Node" + cursor: Int! + "Technology Entity Node" + node: Technology! +} + input UpdateTechnology { "A brief description of the Technology" description: String diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts b/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts index 406047b25..b18f536cb 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts @@ -56,7 +56,9 @@ export type PaginationInformation = { /** Last cursor in page */ endCursor?: Maybe; /** Shows if there is a page after */ - hasNextPage?: Maybe; + hasNextPage: Scalars['Boolean']; + /** Shows if there is a page before */ + hasPreviousPage: Scalars['Boolean']; /** First cursor in page */ startCursor?: Maybe; }; @@ -98,13 +100,22 @@ export type Technology = { export type TechnologyCollection = { __typename?: 'TechnologyCollection'; /** A list of records of the requested page */ - edges: Array>; + edges: Array>; /** Pagination Information */ - pageInfo?: Maybe; + pageInfo: PaginationInformation; /** Identifies the total count of technology records in data source */ totalCount: Scalars['Int']; }; +/** Pagination Technology Node */ +export type TechnologyNode = { + __typename?: 'TechnologyNode'; + /** Current Cursor for Entity Node */ + cursor: Scalars['Int']; + /** Technology Entity Node */ + node: Technology; +}; + export type UpdateTechnology = { /** A brief description of the Technology */ description?: InputMaybe; @@ -208,6 +219,7 @@ export type ResolversTypes = { String: ResolverTypeWrapper; Technology: ResolverTypeWrapper; TechnologyCollection: ResolverTypeWrapper; + TechnologyNode: ResolverTypeWrapper; UpdateTechnology: UpdateTechnology; }; @@ -223,6 +235,7 @@ export type ResolversParentTypes = { String: Scalars['String']; Technology: Technology; TechnologyCollection: TechnologyCollection; + TechnologyNode: TechnologyNode; UpdateTechnology: UpdateTechnology; }; @@ -255,7 +268,8 @@ export type PaginationInformationResolvers< ParentType extends ResolversParentTypes['PaginationInformation'] = ResolversParentTypes['PaginationInformation'] > = { endCursor?: Resolver, ParentType, ContextType>; - hasNextPage?: Resolver, ParentType, ContextType>; + hasNextPage?: Resolver; + hasPreviousPage?: Resolver; startCursor?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -293,16 +307,26 @@ export type TechnologyCollectionResolvers< ContextType = any, ParentType extends ResolversParentTypes['TechnologyCollection'] = ResolversParentTypes['TechnologyCollection'] > = { - edges?: Resolver>, ParentType, ContextType>; - pageInfo?: Resolver, ParentType, ContextType>; + edges?: Resolver>, ParentType, ContextType>; + pageInfo?: Resolver; totalCount?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; +export type TechnologyNodeResolvers< + ContextType = any, + ParentType extends ResolversParentTypes['TechnologyNode'] = ResolversParentTypes['TechnologyNode'] +> = { + cursor?: Resolver; + node?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type Resolvers = { Mutation?: MutationResolvers; PaginationInformation?: PaginationInformationResolvers; Query?: QueryResolvers; Technology?: TechnologyResolvers; TechnologyCollection?: TechnologyCollectionResolvers; + TechnologyNode?: TechnologyNodeResolvers; }; diff --git a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts index 871eac6a2..a196e04a9 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts @@ -15,12 +15,24 @@ export const technologyTypeDefs = gql` url: String } + """ + Pagination Technology Node + """ + type TechnologyNode { + "Current Cursor for Entity Node" + cursor: Int! + "Technology Entity Node" + node: Technology! + } + """ Pagination Information """ type PaginationInformation { "Shows if there is a page after" - hasNextPage: Boolean + hasNextPage: Boolean! + "Shows if there is a page before" + hasPreviousPage: Boolean! "First cursor in page" startCursor: Int "Last cursor in page" @@ -34,9 +46,9 @@ export const technologyTypeDefs = gql` "Identifies the total count of technology records in data source" totalCount: Int! "A list of records of the requested page" - edges: [Technology]! + edges: [TechnologyNode]! "Pagination Information" - pageInfo: PaginationInformation + pageInfo: PaginationInformation! } """ From 8afc0e9acbc92c69b93e0cf493836b85efdd580c Mon Sep 17 00:00:00 2001 From: Ian Mungai Date: Thu, 30 Mar 2023 16:54:17 +0300 Subject: [PATCH 11/19] fix: PaginationInformation -> PageInformation --- .../src/graphql/mappers/technology.ts | 2 +- .../schema/generated/graphql.schema.json | 4 ++-- .../graphql/schema/generated/schema.graphql | 4 ++-- .../graphql/schema/generated/types/index.ts | 18 +++++++++--------- .../schema/technology/technology.typedefs.ts | 4 ++-- .../src/mocks/technology-entity.ts | 10 +++++++++- 6 files changed, 25 insertions(+), 17 deletions(-) diff --git a/starters/express-apollo-prisma/src/graphql/mappers/technology.ts b/starters/express-apollo-prisma/src/graphql/mappers/technology.ts index ba1b4a34f..67ca944f6 100644 --- a/starters/express-apollo-prisma/src/graphql/mappers/technology.ts +++ b/starters/express-apollo-prisma/src/graphql/mappers/technology.ts @@ -1,6 +1,6 @@ import { TechnologyEntity } from '@prisma/client'; import { TechnologyEntityCollection } from '../data-sources'; -import { Technology, TechnologyCollection, TechnologyNode } from '../schema/generated/types'; +import { Technology, TechnologyCollection } from '../schema/generated/types'; export const mapTechnology = (entity: TechnologyEntity): Technology => ({ __typename: 'Technology', diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json b/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json index 3cc6c87ce..05678c4e1 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json @@ -213,7 +213,7 @@ }, { "kind": "OBJECT", - "name": "PaginationInformation", + "name": "PageInformation", "description": "Pagination Information", "fields": [ { @@ -474,7 +474,7 @@ "name": null, "ofType": { "kind": "OBJECT", - "name": "PaginationInformation", + "name": "PageInformation", "ofType": null } }, diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql b/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql index 3b02e6c36..5eeb42cb6 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql @@ -27,7 +27,7 @@ type Mutation { """ Pagination Information """ -type PaginationInformation { +type PageInformation { "Last cursor in page" endCursor: Int "Shows if there is a page after" @@ -69,7 +69,7 @@ type TechnologyCollection { "A list of records of the requested page" edges: [TechnologyNode]! "Pagination Information" - pageInfo: PaginationInformation! + pageInfo: PageInformation! "Identifies the total count of technology records in data source" totalCount: Int! } diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts b/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts index b18f536cb..fcc1cd1f1 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts @@ -51,8 +51,8 @@ export type MutationupdateTechnologyArgs = { }; /** Pagination Information */ -export type PaginationInformation = { - __typename?: 'PaginationInformation'; +export type PageInformation = { + __typename?: 'PageInformation'; /** Last cursor in page */ endCursor?: Maybe; /** Shows if there is a page after */ @@ -102,7 +102,7 @@ export type TechnologyCollection = { /** A list of records of the requested page */ edges: Array>; /** Pagination Information */ - pageInfo: PaginationInformation; + pageInfo: PageInformation; /** Identifies the total count of technology records in data source */ totalCount: Scalars['Int']; }; @@ -214,7 +214,7 @@ export type ResolversTypes = { ID: ResolverTypeWrapper; Int: ResolverTypeWrapper; Mutation: ResolverTypeWrapper<{}>; - PaginationInformation: ResolverTypeWrapper; + PageInformation: ResolverTypeWrapper; Query: ResolverTypeWrapper<{}>; String: ResolverTypeWrapper; Technology: ResolverTypeWrapper; @@ -230,7 +230,7 @@ export type ResolversParentTypes = { ID: Scalars['ID']; Int: Scalars['Int']; Mutation: {}; - PaginationInformation: PaginationInformation; + PageInformation: PageInformation; Query: {}; String: Scalars['String']; Technology: Technology; @@ -263,9 +263,9 @@ export type MutationResolvers< >; }; -export type PaginationInformationResolvers< +export type PageInformationResolvers< ContextType = any, - ParentType extends ResolversParentTypes['PaginationInformation'] = ResolversParentTypes['PaginationInformation'] + ParentType extends ResolversParentTypes['PageInformation'] = ResolversParentTypes['PageInformation'] > = { endCursor?: Resolver, ParentType, ContextType>; hasNextPage?: Resolver; @@ -308,7 +308,7 @@ export type TechnologyCollectionResolvers< ParentType extends ResolversParentTypes['TechnologyCollection'] = ResolversParentTypes['TechnologyCollection'] > = { edges?: Resolver>, ParentType, ContextType>; - pageInfo?: Resolver; + pageInfo?: Resolver; totalCount?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -324,7 +324,7 @@ export type TechnologyNodeResolvers< export type Resolvers = { Mutation?: MutationResolvers; - PaginationInformation?: PaginationInformationResolvers; + PageInformation?: PageInformationResolvers; Query?: QueryResolvers; Technology?: TechnologyResolvers; TechnologyCollection?: TechnologyCollectionResolvers; diff --git a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts index a196e04a9..15b7eec5b 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts @@ -28,7 +28,7 @@ export const technologyTypeDefs = gql` """ Pagination Information """ - type PaginationInformation { + type PageInformation { "Shows if there is a page after" hasNextPage: Boolean! "Shows if there is a page before" @@ -48,7 +48,7 @@ export const technologyTypeDefs = gql` "A list of records of the requested page" edges: [TechnologyNode]! "Pagination Information" - pageInfo: PaginationInformation! + pageInfo: PageInformation! } """ diff --git a/starters/express-apollo-prisma/src/mocks/technology-entity.ts b/starters/express-apollo-prisma/src/mocks/technology-entity.ts index 0d0418b00..703e55779 100644 --- a/starters/express-apollo-prisma/src/mocks/technology-entity.ts +++ b/starters/express-apollo-prisma/src/mocks/technology-entity.ts @@ -22,5 +22,13 @@ export const createMockTechnologyEntityCollection = ( totalCount: number ): TechnologyEntityCollection => ({ totalCount, - edges: Array(edgesCount).fill(null).map(createMockTechnologyEntity), + edges: Array(edgesCount) + .fill(null) + .map(() => { + const technology = createMockTechnologyEntity(); + return { + node: technology, + cursor: technology.id, + }; + }), }); From a4e548921f4e5e17023a90bcbcde4f179f381bc4 Mon Sep 17 00:00:00 2001 From: Ian Mungai Date: Fri, 31 Mar 2023 10:05:53 +0300 Subject: [PATCH 12/19] chore: tests to 100 --- .../technology-data-source.spec.ts | 174 +++++++++++++----- .../data-sources/technology-data-source.ts | 21 ++- .../src/graphql/mappers/technology.spec.ts | 39 ++-- .../schema/generated/graphql.schema.json | 12 +- .../graphql/schema/generated/schema.graphql | 2 +- .../graphql/schema/generated/types/index.ts | 2 +- .../technology/technology.resolvers.spec.ts | 85 ++++++--- .../schema/technology/technology.typedefs.ts | 2 +- .../src/mocks/technology-entity.ts | 34 +++- .../src/mocks/technology.ts | 29 ++- 10 files changed, 280 insertions(+), 120 deletions(-) diff --git a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.spec.ts b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.spec.ts index 05d1f4a4b..afa76ffc2 100644 --- a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.spec.ts +++ b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.spec.ts @@ -3,6 +3,10 @@ import { PrismaClient, TechnologyEntity } from '@prisma/client'; import { DeepMockProxy } from 'jest-mock-extended'; import { createMockPrismaClient } from '../../mocks/prisma-client'; import { createMockCacheApiWrapper } from '../../mocks/cache-api-wrapper'; +import { + createMockTechnologyEntities, + createMockTechnologyNodes, +} from '../../mocks/technology-entity'; describe('TechnologyDataSource', () => { const MOCK_PRISMA_CLIENT: DeepMockProxy = createMockPrismaClient(); @@ -34,25 +38,25 @@ describe('TechnologyDataSource', () => { }); }); + const testMockTechnologyCacheSet = () => + it('calls CacheAPIWrapper.set method once with valid arguments', () => { + expect(MOCK_CACHE_API_WRAPPER.cache).toHaveBeenCalledTimes(1); + expect(MOCK_CACHE_API_WRAPPER.cache).toHaveBeenCalledWith(MOCK_TECHNOLOGY, 'id'); + }); + const GENERAL_CASES: [string, TechnologyDataSource, boolean][] = [ [ 'when instance created with PrismaClient (required) only', new TechnologyDataSource(MOCK_PRISMA_CLIENT), false, // cache disabled ], - [ - 'when instance created with PrismaClient (required) and CacheAPIWrapper (optional)', - new TechnologyDataSource(MOCK_PRISMA_CLIENT, MOCK_CACHE_API_WRAPPER), - true, // cache enabled - ], + // [ + // 'when instance created with PrismaClient (required) and CacheAPIWrapper (optional)', + // new TechnologyDataSource(MOCK_PRISMA_CLIENT, MOCK_CACHE_API_WRAPPER), + // true, // cache enabled + // ], ]; - const testMockTechnologyCacheSet = () => - it('calls CacheAPIWrapper.set method once with valid arguments', () => { - expect(MOCK_CACHE_API_WRAPPER.cache).toHaveBeenCalledTimes(1); - expect(MOCK_CACHE_API_WRAPPER.cache).toHaveBeenCalledWith(MOCK_TECHNOLOGY, 'id'); - }); - describe('#createTechnology', () => { describe.each(GENERAL_CASES)('%s', (_statement, instance, cacheEnabled) => { const EXPECTED_RESULT_CREATE = MOCK_TECHNOLOGY; @@ -308,51 +312,123 @@ describe('TechnologyDataSource', () => { describe('#getTechnologies', () => { describe.each(GENERAL_CASES)('%s', (_statement, instance) => { - const MOCK_LIMIT = 1; - const MOCK_OFFSET = 2; - const MOCK_RESULT_TOTAL_COUNT = 3; - const MOCK_TECHNOLOGIES: TechnologyEntity[] = [MOCK_TECHNOLOGY]; - - const EXPECTED_RESULT: TechnologyEntityCollection = { - totalCount: MOCK_RESULT_TOTAL_COUNT, - edges: MOCK_TECHNOLOGIES, - }; + const MOCK_TOTAL_COUNT = 4; + const MOCK_TECHNOLOGY_NODES = createMockTechnologyNodes(MOCK_TOTAL_COUNT); + const MOCK_TECHNOLOGY_ENTITIES = createMockTechnologyEntities(MOCK_TOTAL_COUNT); + + console.log(MOCK_TECHNOLOGY_NODES); + + console.log(MOCK_TECHNOLOGY_ENTITIES); + + const PAGINATION_CASES: [ + string, + number, + number | undefined, + TechnologyEntity[], + TechnologyEntityCollection + ][] = [ + // [ + // `and 'after' input is defined and items array is empty`, + // 1, + // 2, + // [], + // { + // totalCount: MOCK_TOTAL_COUNT, + // edges: [], + // pageInfo: { + // hasPreviousPage: false, + // hasNextPage: false, + // startCursor: undefined, + // endCursor: undefined, + // }, + // }, + // ], + [ + `and 'after' input is defined and items array is not empty`, + 1, + 1, + [MOCK_TECHNOLOGY_ENTITIES[2]], + { + totalCount: MOCK_TOTAL_COUNT, + edges: [MOCK_TECHNOLOGY_NODES[2]], + pageInfo: { + hasPreviousPage: true, + hasNextPage: true, + startCursor: 3, + endCursor: 3, + }, + }, + ], + // [ + // `and 'after' input is undefined and items array is empty`, + // 1, + // undefined, + // [], + // { + // totalCount: MOCK_TOTAL_COUNT, + // edges: [], + // pageInfo: { + // hasPreviousPage: false, + // hasNextPage: false, + // startCursor: undefined, + // endCursor: undefined, + // }, + // }, + // ], + [ + `and 'after' input is undefined and items array is not empty`, + 2, + undefined, + [MOCK_TECHNOLOGY_ENTITIES[0], MOCK_TECHNOLOGY_ENTITIES[1]], + { + totalCount: MOCK_TOTAL_COUNT, + edges: [MOCK_TECHNOLOGY_NODES[0], MOCK_TECHNOLOGY_NODES[1]], + pageInfo: { + hasPreviousPage: false, + hasNextPage: true, + startCursor: 1, + endCursor: 2, + }, + }, + ], + ]; - let result: TechnologyEntityCollection; + describe.each(PAGINATION_CASES)( + '%s', + (_inner_statement, MOCK_FIRST, MOCK_AFTER, MOCK_DB_DATA, EXPECTED_RESULT) => { + const MOCK_ORDER_BY = { id: 'asc' }; - beforeAll(async () => { - MOCK_PRISMA_CLIENT.$transaction.mockResolvedValue([ - MOCK_RESULT_TOTAL_COUNT, - [MOCK_TECHNOLOGY], - ]); - result = await instance.getTechnologies(MOCK_LIMIT, MOCK_OFFSET); - }); + let result: TechnologyEntityCollection; - afterAll(() => { - MOCK_PRISMA_CLIENT.$transaction.mockReset(); - MOCK_PRISMA_CLIENT.technologyEntity.count.mockReset(); - MOCK_PRISMA_CLIENT.technologyEntity.findMany.mockReset(); - }); + beforeAll(async () => { + MOCK_PRISMA_CLIENT.$transaction.mockResolvedValue([MOCK_TOTAL_COUNT, MOCK_DB_DATA]); + result = await instance.getTechnologies(MOCK_FIRST, MOCK_AFTER); + }); - it('calls PrismaClient count method once', () => { - expect(MOCK_PRISMA_CLIENT.technologyEntity.count).toHaveBeenCalledTimes(1); - }); + afterAll(() => { + MOCK_PRISMA_CLIENT.$transaction.mockReset(); + MOCK_PRISMA_CLIENT.technologyEntity.count.mockReset(); + MOCK_PRISMA_CLIENT.technologyEntity.findMany.mockReset(); + }); - it('calls PrismaClient findMany method once with expected argument', () => { - expect(MOCK_PRISMA_CLIENT.technologyEntity.findMany).toHaveBeenCalledTimes(1); - expect(MOCK_PRISMA_CLIENT.technologyEntity.findMany).toHaveBeenCalledWith({ - take: MOCK_LIMIT, - skip: MOCK_OFFSET, - }); - }); + it('calls PrismaClient count method once', () => { + expect(MOCK_PRISMA_CLIENT.technologyEntity.count).toHaveBeenCalledTimes(3); + }); - it('calls PrismaClient.$transaction method once', () => { - expect(MOCK_PRISMA_CLIENT.technologyEntity.count).toHaveBeenCalledTimes(1); - }); + it('calls PrismaClient findMany method once with expected argument', () => { + expect(MOCK_PRISMA_CLIENT.technologyEntity.findMany).toHaveBeenCalledTimes(1); + expect(MOCK_PRISMA_CLIENT.technologyEntity.findMany).toHaveBeenCalledWith({ + take: MOCK_FIRST, + orderBy: MOCK_ORDER_BY, + where: MOCK_AFTER ? { id: { gt: MOCK_AFTER } } : {}, + }); + }); - it('returns expected result', () => { - expect(result).toEqual(EXPECTED_RESULT); - }); + it('returns expected result', () => { + expect(result).toEqual(EXPECTED_RESULT); + }); + } + ); }); }); }); diff --git a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts index e8d523495..ff13a8b46 100644 --- a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts +++ b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts @@ -4,19 +4,21 @@ import { InputMaybe } from '../schema/generated/types'; type TechnologyEntityId = TechnologyEntity['id']; -type TechnologyNode = { +export type TechnologyNode = { cursor: TechnologyEntityId; node: TechnologyEntity; }; +export type PageInformation = { + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor?: TechnologyEntityId; + endCursor?: TechnologyEntityId; +}; + export type TechnologyEntityCollection = { totalCount: number; - pageInfo: { - hasNextPage: boolean; - hasPreviousPage: boolean; - startCursor?: TechnologyEntityId; - endCursor?: TechnologyEntityId; - }; + pageInfo: PageInformation; edges: TechnologyNode[]; }; @@ -59,6 +61,7 @@ export class TechnologyDataSource { const startCursor = items.length > 0 ? items[0].id : undefined; const endCursor = items.length > 0 ? items[items.length - 1].id : undefined; + const hasNextPage = (await this.prismaClient.technologyEntity.count({ where: { id: { gt: endCursor } }, @@ -66,7 +69,7 @@ export class TechnologyDataSource { const hasPreviousPage = (await this.prismaClient.technologyEntity.count({ - where: { id: { lt: items[0].id } }, + where: { id: { lt: items[0] ? items[0].id : undefined } }, })) > 0; const edges = items.map((node) => ({ @@ -74,6 +77,8 @@ export class TechnologyDataSource { node, })); + console.log(hasNextPage, hasPreviousPage); + return { totalCount, edges, diff --git a/starters/express-apollo-prisma/src/graphql/mappers/technology.spec.ts b/starters/express-apollo-prisma/src/graphql/mappers/technology.spec.ts index c07e07192..80b463c2d 100644 --- a/starters/express-apollo-prisma/src/graphql/mappers/technology.spec.ts +++ b/starters/express-apollo-prisma/src/graphql/mappers/technology.spec.ts @@ -2,7 +2,8 @@ import { mapTechnology, mapTechnologyCollection } from './technology'; import { TechnologyEntity } from '@prisma/client'; import { Technology, TechnologyCollection } from '../schema/generated/types'; import { createMockTechnologyEntityCollection } from '../../mocks/technology-entity'; -import { createMockTechnology } from '../../mocks/technology'; +import { createMockTechnologyCollectionResult } from '../../mocks/technology'; +import { PageInformation } from '../data-sources'; jest.mock('./technology', () => { const originalModule = jest.requireActual('./technology'); @@ -14,12 +15,6 @@ jest.mock('./technology', () => { }; }); -const SPY_MAP_TECHNOLOGY = mapTechnology as unknown as jest.SpyInstance< - Technology, - [entity: TechnologyEntity], - unknown ->; - describe('.mapTechnology', () => { describe('when called', () => { it('returns expected result', () => { @@ -46,23 +41,33 @@ describe('.mapTechnology', () => { describe('.mapTechnologyCollection', () => { describe('when called with arguments', () => { - const MOCK_TECHNOLOGY_ENTITY_COLLECTION_PAGE = createMockTechnologyEntityCollection(3, 11); - const MOCK_TECHNOLOGY = createMockTechnology(); - const EXPECTED_RESULT: TechnologyCollection = { - totalCount: 11, - edges: Array(3).fill(MOCK_TECHNOLOGY), + const MOCK_TOTAL_COUNT = 11; + const MOCK_FIRST_INPUT = 3; + const MOCK_PAGE_INFO: PageInformation = { + hasNextPage: true, + hasPreviousPage: false, + startCursor: 1, + endCursor: 3, }; + + const MOCK_TECHNOLOGY_ENTITY_COLLECTION_PAGE = createMockTechnologyEntityCollection( + MOCK_FIRST_INPUT, + MOCK_TOTAL_COUNT, + MOCK_PAGE_INFO + ); + + const EXPECTED_RESULT = createMockTechnologyCollectionResult( + MOCK_TOTAL_COUNT, + MOCK_FIRST_INPUT, + MOCK_PAGE_INFO + ); + let result: TechnologyCollection; beforeAll(() => { - SPY_MAP_TECHNOLOGY.mockReturnValue(MOCK_TECHNOLOGY); result = mapTechnologyCollection(MOCK_TECHNOLOGY_ENTITY_COLLECTION_PAGE); }); - afterAll(() => { - SPY_MAP_TECHNOLOGY.mockRestore(); - }); - it('returns expected result', () => { expect(result).toEqual(EXPECTED_RESULT); }); diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json b/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json index 05678c4e1..f4b02ab66 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json @@ -303,15 +303,11 @@ "name": "first", "description": null, "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - } + "kind": "SCALAR", + "name": "Int", + "ofType": null }, - "defaultValue": null, + "defaultValue": "5", "isDeprecated": false, "deprecationReason": null } diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql b/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql index 5eeb42cb6..65be9e0ed 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql @@ -43,7 +43,7 @@ Technology queries """ type Query { "Returns a list of Technologies" - technologies(after: Int, first: Int!): TechnologyCollection! + technologies(after: Int, first: Int = 5): TechnologyCollection! "Returns a single Technology by ID" technology(id: ID!): Technology } diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts b/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts index fcc1cd1f1..64c732a6a 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts @@ -75,7 +75,7 @@ export type Query = { /** Technology queries */ export type QuerytechnologiesArgs = { after?: InputMaybe; - first: Scalars['Int']; + first?: InputMaybe; }; /** Technology queries */ diff --git a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.spec.ts b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.spec.ts index 86370b2f2..94c7ae7f0 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.spec.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.resolvers.spec.ts @@ -21,6 +21,7 @@ import { ServerContext } from '../../server-context'; import { TechnologyEntity } from '@prisma/client'; import { mapTechnology, mapTechnologyCollection } from '../../mappers'; +import { PageInformation } from '../../data-sources'; type QueryTechnology = Pick; type QueryTechnologies = Pick; @@ -53,13 +54,22 @@ const MOCK_QUERY_TECHNOLOGY = gql` const MOCK_QUERY_TECHNOLOGIES_PAGINATION_DEFAULT = gql` query TechnologiesQueryPaginationArgumentsDefualt { technologies { + totalCount edges { - description - displayName - id - url + node { + id + displayName + description + url + } + cursor + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage } - totalCount } } `; @@ -67,21 +77,30 @@ const MOCK_QUERY_TECHNOLOGIES_PAGINATION_DEFAULT = gql` const MOCK_VARIABLES_TECHNOLOGIES_PAGINATION_DEFAULT: QuerytechnologiesArgs = {}; const MOCK_QUERY_TECHNOLOGIES_PAGINATION_CUSTOM = gql` - query TechnologiesQueryPaginationArgumentsCustom($limit: Int, $offset: Int) { - technologies(limit: $limit, offset: $offset) { + query Technologies($first: Int!, $after: Int) { + technologies(first: $first, after: $after) { + totalCount edges { - description - displayName - id - url + node { + id + displayName + description + url + } + cursor + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage } - totalCount } } `; const MOCK_VARIABLES_TECHNOLOGIES_PAGINATION_CUSTOM: QuerytechnologiesArgs = { - limit: 10, - offset: 20, + first: 1, + after: 3, }; const MOCK_TECHNOLOGY_DATASOURCE = createMockTechnologyDataSource(); @@ -277,16 +296,27 @@ describe('technologyResolvers', () => { describe('.technologies', () => { describe('when called', () => { + const MOCK_RESULT_PAGE_INFO: PageInformation = { + hasNextPage: true, + hasPreviousPage: false, + startCursor: 1, + endCursor: 3, + }; + const MOCK_RESULT_TECHNOLOGY_COLLECTION: TechnologyCollection = { totalCount: 987, edges: [ { - displayName: 'MOCK_DISPLAY_NAME_RESULT', - description: 'MOCK_DESCRIPTION_RESULT', - id: 'MOCK_ID_RESULT', - url: 'MOCK_URL_RESULT', + node: { + displayName: 'MOCK_DISPLAY_NAME_RESULT', + description: 'MOCK_DESCRIPTION_RESULT', + id: 'MOCK_ID_RESULT', + url: 'MOCK_URL_RESULT', + }, + cursor: 1, }, ], + pageInfo: MOCK_RESULT_PAGE_INFO, }; describe.each([ @@ -294,17 +324,17 @@ describe('technologyResolvers', () => { 'with default pagination arguments', MOCK_QUERY_TECHNOLOGIES_PAGINATION_DEFAULT, MOCK_VARIABLES_TECHNOLOGIES_PAGINATION_DEFAULT, - createMockTechnologyEntityCollection(5, 30), + createMockTechnologyEntityCollection(5, 30, MOCK_RESULT_PAGE_INFO), 5, - 0, + undefined, ], [ 'with custom pagination arguments', MOCK_QUERY_TECHNOLOGIES_PAGINATION_CUSTOM, MOCK_VARIABLES_TECHNOLOGIES_PAGINATION_CUSTOM, - createMockTechnologyEntityCollection(10, 50), - Number(MOCK_VARIABLES_TECHNOLOGIES_PAGINATION_CUSTOM.limit), - Number(MOCK_VARIABLES_TECHNOLOGIES_PAGINATION_CUSTOM.offset), + createMockTechnologyEntityCollection(10, 50, MOCK_RESULT_PAGE_INFO), + Number(MOCK_VARIABLES_TECHNOLOGIES_PAGINATION_CUSTOM.first), + Number(MOCK_VARIABLES_TECHNOLOGIES_PAGINATION_CUSTOM.after), ], ])( '%s', @@ -313,8 +343,8 @@ describe('technologyResolvers', () => { mockQuery, mockVariables, mockCollectionPage, - expectedLimit, - expectedOffset + expectedFirst, + expectedAfter ) => { let response: GraphQLResponse; @@ -338,8 +368,8 @@ describe('technologyResolvers', () => { it('calls TechnologyDataSource getTechnologies method once', () => { expect(MOCK_TECHNOLOGY_DATASOURCE.getTechnologies).toHaveBeenCalledTimes(1); expect(MOCK_TECHNOLOGY_DATASOURCE.getTechnologies).toHaveBeenCalledWith( - expectedLimit, - expectedOffset + expectedFirst, + expectedAfter ); }); @@ -352,6 +382,7 @@ describe('technologyResolvers', () => { expect(response.body.kind).toEqual('single'); assert(response.body.kind === 'single'); expect(response.body.singleResult.errors).toBeUndefined(); + expect(response.body.singleResult.data).toEqual({ technologies: MOCK_RESULT_TECHNOLOGY_COLLECTION, }); diff --git a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts index 15b7eec5b..6ce3c0f5c 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts @@ -58,7 +58,7 @@ export const technologyTypeDefs = gql` "Returns a single Technology by ID" technology(id: ID!): Technology "Returns a list of Technologies" - technologies(first: Int!, after: Int): TechnologyCollection! + technologies(first: Int = 5, after: Int): TechnologyCollection! } input CreateTechnology { diff --git a/starters/express-apollo-prisma/src/mocks/technology-entity.ts b/starters/express-apollo-prisma/src/mocks/technology-entity.ts index 703e55779..0f43ed718 100644 --- a/starters/express-apollo-prisma/src/mocks/technology-entity.ts +++ b/starters/express-apollo-prisma/src/mocks/technology-entity.ts @@ -1,14 +1,20 @@ import { TechnologyEntity } from '@prisma/client'; import { DeepMockProxy, mockDeep } from 'jest-mock-extended'; -import { TechnologyDataSource, TechnologyEntityCollection } from '../graphql/data-sources'; +import { + PageInformation, + TechnologyDataSource, + TechnologyEntityCollection, + TechnologyNode, +} from '../graphql/data-sources'; export const createMockTechnologyDataSource = (): DeepMockProxy => mockDeep(); -let technologyEntityIdCount = 0; +let technologyEntityIdCount = 1; +let alternateTechnologyEntityIdCount = 1; -const createMockTechnologyEntity = (): TechnologyEntity => { - const id = technologyEntityIdCount++; +const createMockTechnologyEntity = (idCount?: number): TechnologyEntity => { + const id = idCount ? idCount++ : technologyEntityIdCount++; return { description: `MOCK_DESCRIPTION_${id}`, displayName: `MOCK_DISPLAY_NAME_${id}`, @@ -19,9 +25,11 @@ const createMockTechnologyEntity = (): TechnologyEntity => { export const createMockTechnologyEntityCollection = ( edgesCount: number, - totalCount: number + totalCount: number, + pageInfo: PageInformation ): TechnologyEntityCollection => ({ totalCount, + pageInfo, edges: Array(edgesCount) .fill(null) .map(() => { @@ -32,3 +40,19 @@ export const createMockTechnologyEntityCollection = ( }; }), }); + +export const createMockTechnologyEntities = (totalCount: number): TechnologyEntity[] => { + return Array(totalCount).fill(null).map(createMockTechnologyEntity); +}; + +export const createMockTechnologyNodes = (totalCount: number): TechnologyNode[] => { + return Array(totalCount) + .fill(null) + .map(() => { + const technology = createMockTechnologyEntity(alternateTechnologyEntityIdCount++); + return { + node: technology, + cursor: technology.id, + }; + }); +}; diff --git a/starters/express-apollo-prisma/src/mocks/technology.ts b/starters/express-apollo-prisma/src/mocks/technology.ts index 1ff922295..4edb114b4 100644 --- a/starters/express-apollo-prisma/src/mocks/technology.ts +++ b/starters/express-apollo-prisma/src/mocks/technology.ts @@ -1,12 +1,35 @@ -import { Technology } from '../graphql/schema/generated/types'; +import { PageInformation } from '../graphql/data-sources'; +import { Technology, TechnologyCollection } from '../graphql/schema/generated/types'; -let technologyIdCount = 0; +let technologyIdCount = 1; export const createMockTechnology = (): Technology => { - const id = `MOCK_ID_${technologyIdCount++}`; + const id = `${technologyIdCount++}`; return { + __typename: 'Technology', description: `MOCK_DESCRIPTION_${id}`, displayName: `MOCK_DISPLAY_NAME_${id}`, id, url: `MOCK_URL_${id}`, }; }; + +export const createMockTechnologyCollectionResult = ( + totalCount: number, + first: number, + pageInfo: PageInformation +): TechnologyCollection => { + return { + totalCount: totalCount, + pageInfo, + edges: Array(first) + .fill(null) + .map(() => { + const MOCK_TECHNOLOGY = createMockTechnology(); + return { + __typename: 'TechnologyNode', + node: MOCK_TECHNOLOGY, + cursor: Number(MOCK_TECHNOLOGY.id), + }; + }), + }; +}; From aec6ed42a63e86d827bdff2ef37f93d01052e649 Mon Sep 17 00:00:00 2001 From: Ian Mungai Date: Fri, 31 Mar 2023 10:31:50 +0300 Subject: [PATCH 13/19] fix: tests to 100% --- .../technology-data-source.spec.ts | 152 ++++++++++++------ .../data-sources/technology-data-source.ts | 2 - 2 files changed, 103 insertions(+), 51 deletions(-) diff --git a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.spec.ts b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.spec.ts index afa76ffc2..34396bc28 100644 --- a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.spec.ts +++ b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.spec.ts @@ -3,10 +3,6 @@ import { PrismaClient, TechnologyEntity } from '@prisma/client'; import { DeepMockProxy } from 'jest-mock-extended'; import { createMockPrismaClient } from '../../mocks/prisma-client'; import { createMockCacheApiWrapper } from '../../mocks/cache-api-wrapper'; -import { - createMockTechnologyEntities, - createMockTechnologyNodes, -} from '../../mocks/technology-entity'; describe('TechnologyDataSource', () => { const MOCK_PRISMA_CLIENT: DeepMockProxy = createMockPrismaClient(); @@ -50,11 +46,11 @@ describe('TechnologyDataSource', () => { new TechnologyDataSource(MOCK_PRISMA_CLIENT), false, // cache disabled ], - // [ - // 'when instance created with PrismaClient (required) and CacheAPIWrapper (optional)', - // new TechnologyDataSource(MOCK_PRISMA_CLIENT, MOCK_CACHE_API_WRAPPER), - // true, // cache enabled - // ], + [ + 'when instance created with PrismaClient (required) and CacheAPIWrapper (optional)', + new TechnologyDataSource(MOCK_PRISMA_CLIENT, MOCK_CACHE_API_WRAPPER), + true, // cache enabled + ], ]; describe('#createTechnology', () => { @@ -312,41 +308,89 @@ describe('TechnologyDataSource', () => { describe('#getTechnologies', () => { describe.each(GENERAL_CASES)('%s', (_statement, instance) => { - const MOCK_TOTAL_COUNT = 4; - const MOCK_TECHNOLOGY_NODES = createMockTechnologyNodes(MOCK_TOTAL_COUNT); - const MOCK_TECHNOLOGY_ENTITIES = createMockTechnologyEntities(MOCK_TOTAL_COUNT); - - console.log(MOCK_TECHNOLOGY_NODES); + const MOCK_TOTAL_COUNT = 3; + + const MOCK_TECHNOLOGY_NODES = [ + { + node: { + description: 'MOCK_DESCRIPTION_1', + displayName: 'MOCK_DISPLAY_NAME_1', + id: 1, + url: 'MOCK_URL_1', + }, + cursor: 1, + }, + { + node: { + description: 'MOCK_DESCRIPTION_2', + displayName: 'MOCK_DISPLAY_NAME_2', + id: 2, + url: 'MOCK_URL_2', + }, + cursor: 2, + }, + { + node: { + description: 'MOCK_DESCRIPTION_3', + displayName: 'MOCK_DISPLAY_NAME_3', + id: 3, + url: 'MOCK_URL_3', + }, + cursor: 3, + }, + ]; - console.log(MOCK_TECHNOLOGY_ENTITIES); + const MOCK_TECHNOLOGY_ENTITIES = [ + { + description: 'MOCK_DESCRIPTION_1', + displayName: 'MOCK_DISPLAY_NAME_1', + id: 1, + url: 'MOCK_URL_1', + }, + { + description: 'MOCK_DESCRIPTION_2', + displayName: 'MOCK_DISPLAY_NAME_2', + id: 2, + url: 'MOCK_URL_2', + }, + { + description: 'MOCK_DESCRIPTION_3', + displayName: 'MOCK_DISPLAY_NAME_3', + id: 3, + url: 'MOCK_URL_3', + }, + ]; const PAGINATION_CASES: [ string, number, number | undefined, + number, TechnologyEntity[], TechnologyEntityCollection ][] = [ - // [ - // `and 'after' input is defined and items array is empty`, - // 1, - // 2, - // [], - // { - // totalCount: MOCK_TOTAL_COUNT, - // edges: [], - // pageInfo: { - // hasPreviousPage: false, - // hasNextPage: false, - // startCursor: undefined, - // endCursor: undefined, - // }, - // }, - // ], + [ + `and 'after' input is defined and items array is empty`, + 1, + 2, + 0, + [], + { + totalCount: MOCK_TOTAL_COUNT, + edges: [], + pageInfo: { + hasPreviousPage: false, + hasNextPage: false, + startCursor: undefined, + endCursor: undefined, + }, + }, + ], [ `and 'after' input is defined and items array is not empty`, 1, 1, + 1, [MOCK_TECHNOLOGY_ENTITIES[2]], { totalCount: MOCK_TOTAL_COUNT, @@ -359,32 +403,34 @@ describe('TechnologyDataSource', () => { }, }, ], - // [ - // `and 'after' input is undefined and items array is empty`, - // 1, - // undefined, - // [], - // { - // totalCount: MOCK_TOTAL_COUNT, - // edges: [], - // pageInfo: { - // hasPreviousPage: false, - // hasNextPage: false, - // startCursor: undefined, - // endCursor: undefined, - // }, - // }, - // ], + [ + `and 'after' input is undefined and items array is empty`, + 1, + undefined, + 0, + [], + { + totalCount: MOCK_TOTAL_COUNT, + edges: [], + pageInfo: { + hasPreviousPage: false, + hasNextPage: false, + startCursor: undefined, + endCursor: undefined, + }, + }, + ], [ `and 'after' input is undefined and items array is not empty`, 2, undefined, + 1, [MOCK_TECHNOLOGY_ENTITIES[0], MOCK_TECHNOLOGY_ENTITIES[1]], { totalCount: MOCK_TOTAL_COUNT, edges: [MOCK_TECHNOLOGY_NODES[0], MOCK_TECHNOLOGY_NODES[1]], pageInfo: { - hasPreviousPage: false, + hasPreviousPage: true, hasNextPage: true, startCursor: 1, endCursor: 2, @@ -395,13 +441,21 @@ describe('TechnologyDataSource', () => { describe.each(PAGINATION_CASES)( '%s', - (_inner_statement, MOCK_FIRST, MOCK_AFTER, MOCK_DB_DATA, EXPECTED_RESULT) => { + ( + _inner_statement, + MOCK_FIRST, + MOCK_AFTER, + MOCK_RESOLVED_COUNT, + MOCK_DB_DATA, + EXPECTED_RESULT + ) => { const MOCK_ORDER_BY = { id: 'asc' }; let result: TechnologyEntityCollection; beforeAll(async () => { MOCK_PRISMA_CLIENT.$transaction.mockResolvedValue([MOCK_TOTAL_COUNT, MOCK_DB_DATA]); + MOCK_PRISMA_CLIENT.technologyEntity.count.mockResolvedValue(MOCK_RESOLVED_COUNT); result = await instance.getTechnologies(MOCK_FIRST, MOCK_AFTER); }); diff --git a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts index ff13a8b46..b3975e74d 100644 --- a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts +++ b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts @@ -77,8 +77,6 @@ export class TechnologyDataSource { node, })); - console.log(hasNextPage, hasPreviousPage); - return { totalCount, edges, From c2856ed7b31371d00de28ad794960c590e2de13d Mon Sep 17 00:00:00 2001 From: Ian Mungai Date: Fri, 31 Mar 2023 10:38:23 +0300 Subject: [PATCH 14/19] chore: ignore config.ts --- starters/express-apollo-prisma/jest.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/starters/express-apollo-prisma/jest.config.ts b/starters/express-apollo-prisma/jest.config.ts index 5ad206909..415088c6d 100644 --- a/starters/express-apollo-prisma/jest.config.ts +++ b/starters/express-apollo-prisma/jest.config.ts @@ -31,6 +31,7 @@ export default { '/graphql/schema/generated', 'index.ts', '/mocks', + '/config.ts', '\\.(d.ts)$', ], From 56b21fd8828972d6d5624455e99cefed24806718 Mon Sep 17 00:00:00 2001 From: Maarten Bicknese Date: Thu, 30 Mar 2023 13:22:25 +0200 Subject: [PATCH 15/19] refactor: use global configuration file --- starters/express-apollo-prisma/README.md | 4 ++-- starters/express-apollo-prisma/docker-compose.yaml | 1 + starters/express-apollo-prisma/src/config.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/starters/express-apollo-prisma/README.md b/starters/express-apollo-prisma/README.md index e1bb1c43c..71062cea4 100644 --- a/starters/express-apollo-prisma/README.md +++ b/starters/express-apollo-prisma/README.md @@ -106,7 +106,7 @@ git clone https://github.com/thisdot/starter.dev.git ## Environment Variables - `PORT` - The port exposed to connect with the application. -- `DATABASE_URL` - Connector for Prisma to run the migrations +- `DB_URL` - Connector for Prisma to run the migrations (our example will build the correct URL from the other variables) - `DB_USER` - User to use on the MySQL server - `DB_PASS` - Password for both the user and root user - `DB_DATABASE` - Name of the database in MySQL @@ -291,7 +291,7 @@ The data sources are located in `src/graphql/data-sources`. The data sources of ### ORM -The kit uses Prisma as a TypeScript ORM for proper data fetch and mutation from the source. It is configured with the `DATABASE_URL` environment variable. +The kit uses Prisma as a TypeScript ORM for proper data fetch and mutation from the source. It is configured with the `DB_URL` environment variable. We use Prisma for the following: diff --git a/starters/express-apollo-prisma/docker-compose.yaml b/starters/express-apollo-prisma/docker-compose.yaml index 302ec4da8..05f4dc1a6 100644 --- a/starters/express-apollo-prisma/docker-compose.yaml +++ b/starters/express-apollo-prisma/docker-compose.yaml @@ -6,6 +6,7 @@ services: restart: unless-stopped environment: - MYSQL_PASSWORD=${DB_PASSWORD:-demopass} + - MYSQL_USER=${DB_USER:-demo} - MYSQL_ROOT_PASSWORD=${DB_PASSWORD:-demopass} - MYSQL_DATABASE=${DB_DATABASE:-demodb} ports: diff --git a/starters/express-apollo-prisma/src/config.ts b/starters/express-apollo-prisma/src/config.ts index 911823957..a34ff5088 100644 --- a/starters/express-apollo-prisma/src/config.ts +++ b/starters/express-apollo-prisma/src/config.ts @@ -28,7 +28,7 @@ export const REDIS_CACHE_TTL_SECONDS = process.env.REDIS_CACHE_TTL_SECONDS // Database configuration export const DB_PORT = process.env.DB_PORT ? Number(process.env.DB_PORT) : 3306; export const DB_DATABASE = process.env.DB_DATABASE || 'demodb'; -export const DB_PASSWORD = process.env.DB_DATABASE || 'demopass'; +export const DB_PASSWORD = process.env.DB_PASSWORD || 'demopass'; export const DB_USER = process.env.DB_USER || 'demo'; export const PRISMA_CONFIG: Prisma.PrismaClientOptions = { From 127f99ef62dfd8e950cc42e41d2c53dfc4199649 Mon Sep 17 00:00:00 2001 From: Ian Mungai Date: Fri, 31 Mar 2023 11:30:11 +0300 Subject: [PATCH 16/19] chore: ignore worker.ts from jest tests --- starters/express-apollo-prisma/jest.config.ts | 1 + starters/express-apollo-prisma/src/queue/worker.ts | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/starters/express-apollo-prisma/jest.config.ts b/starters/express-apollo-prisma/jest.config.ts index 415088c6d..2c4cc7da8 100644 --- a/starters/express-apollo-prisma/jest.config.ts +++ b/starters/express-apollo-prisma/jest.config.ts @@ -32,6 +32,7 @@ export default { 'index.ts', '/mocks', '/config.ts', + '/queue/worker.ts', '\\.(d.ts)$', ], diff --git a/starters/express-apollo-prisma/src/queue/worker.ts b/starters/express-apollo-prisma/src/queue/worker.ts index 3ede321b4..41963ccd7 100644 --- a/starters/express-apollo-prisma/src/queue/worker.ts +++ b/starters/express-apollo-prisma/src/queue/worker.ts @@ -1,9 +1,6 @@ import amqplib from 'amqplib'; -import * as dotenv from 'dotenv'; import { AMQP_QUEUE_JOB, AMQP_URL } from '../config'; -dotenv.config(); - (async () => { const connection = await amqplib.connect(AMQP_URL); From 29885e668d76abc543fc8292d298199ac67bc317 Mon Sep 17 00:00:00 2001 From: Ian Mungai Date: Tue, 4 Apr 2023 15:34:27 +0300 Subject: [PATCH 17/19] chore: update readme with mongodb and blog post --- starters/express-apollo-prisma/README.md | 55 ++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/starters/express-apollo-prisma/README.md b/starters/express-apollo-prisma/README.md index 71062cea4..99782c8e7 100644 --- a/starters/express-apollo-prisma/README.md +++ b/starters/express-apollo-prisma/README.md @@ -28,10 +28,12 @@ This starter kit features Express, Apollo Server and Prisma. - [Express](#express) - [Apollo Server](#apollo-server) - [ORM](#orm) + - [How to use kit with MongoDB](#how-to-use-kit-with-mongodb) - [Queueing](#queueing) - [Caching](#caching) - [Testing](#testing) - [Deployment](#deployment) + - [Build a Production Scale app with Express-Apollo-Prisma kit](#build-a-production-scale-app-with-express-apollo-prisma-kit) ## Overview @@ -300,6 +302,55 @@ We use Prisma for the following: Learn more about [Prisma](https://www.prisma.io/docs/concepts/overview/prisma-in-your-stack/is-prisma-an-orm). +#### How to use kit with MongoDB + +1. Set up a MongoDB account via the following tutorial: [Create MongoDB Account](https://www.mongodb.com/docs/guides/atlas/account/). +2. Set up MongoDB cluster. [Create Cluster](https://www.mongodb.com/docs/guides/atlas/cluster/) +3. Set up MongoDB User. [Create User](https://www.mongodb.com/docs/guides/atlas/db-user/) +4. Get the [MongoDB Connection URI](https://www.mongodb.com/docs/guides/atlas/connection-string/). +5. Replace the ``, `` and `` to your `DB_URL` in your `.env` with the username, password and database you created. + ``` + DB_URL="mongodb+srv://:@app.random.mongodb.net/database?retryWrites=true&w=majority" + ``` +6. Replace the `DB_USER`, `DB_PASSWORD` and `DB_DATABASE` in your `.env` with the username, password and database you created. +7. Edit the datasource in `prisma/schema.prisma`. + + ```prisma + datasource db { + provider = "mongodb" + url = env("DATABASE_URL") + } + ``` + +8. Edit the `PRISMA_CONFIG` in `src/config.ts` to: + + ```ts + export const PRISMA_CONFIG: Prisma.PrismaClientOptions = { + datasources: { + db: { + url: `mongodb+srv://${DB_USER}:${DB_PASSWORD}@app.random.mongodb.net/${DB_DATABASE}?retryWrites=true&w=majority`, + }, + }, + }; + ``` + +9. Finally update your `src/healthcheck/datasource-healthcheck.ts` to check the mongodb connection. + + ```ts + import { PrismaClient } from '@prisma/client'; + + export const getDataSourceHealth = async (prismaClient?: PrismaClient) => { + try { + const prismaClientPingResult = await prismaClient?.$runCommandRaw({ + ping: 1, + }); + return prismaClientPingResult?.ok == 1; + } catch { + return false; + } + }; + ``` + ### Queueing The kit provides an implementation of queueing using RabbitMQ, an open-source message broker that allows multiple applications or services to communicate with each other through queues. It's a powerful tool for handling tasks asynchronously and distributing workloads across multiple machines. @@ -357,3 +408,7 @@ npm run infrastructure:up 3. Deploy your application to your chosen provider or service using their deployment tools or services. You can use the start script to start your application in production mode. You may also need to configure any necessary proxy or routing rules to direct incoming traffic to your application. 4. Monitor your application for any issues or errors and adjust your deployment as needed. This may involve configuring load balancers, auto-scaling, or other performance optimization features, depending on your chosen provider or service. + +## Build a Production Scale app with Express-Apollo-Prisma kit + +Learn how to build a Production Scale app with Express-Apollo-Prisma kit in this [article](https://www.thisdot.co/blog/building-a-production-scale-app-with-the-express-apollo-prisma-starter-kit). We will cover what's included in the kit, how to set it up, and how to use the provided tools to create a scalable web application. We will also discuss how to extend the starter kit to add features like authentication. Finally, we will look at how to use the provided tools to ensure that your application is well-maintained and efficient From 3de58bf4b0e9b5ba7698564c96efd01da812ef1e Mon Sep 17 00:00:00 2001 From: Ian Mungai Date: Tue, 4 Apr 2023 15:59:34 +0300 Subject: [PATCH 18/19] chore: added new line editorconfig --- starters/express-apollo-prisma/.editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starters/express-apollo-prisma/.editorconfig b/starters/express-apollo-prisma/.editorconfig index 5a39c9945..3628054c6 100644 --- a/starters/express-apollo-prisma/.editorconfig +++ b/starters/express-apollo-prisma/.editorconfig @@ -16,4 +16,4 @@ max_line_length = off trim_trailing_whitespace = false [{package.json, eslintrc.json}] -indent_style = space \ No newline at end of file +indent_style = space From 414255ab89552572b4c60a9841ddc0693b9c394a Mon Sep 17 00:00:00 2001 From: Ian Mungai Date: Tue, 4 Apr 2023 17:11:38 +0300 Subject: [PATCH 19/19] chore: TechnologyNode -> TechnologyEdge --- .../data-sources/technology-data-source.ts | 4 ++-- .../src/graphql/mappers/technology.ts | 2 +- .../schema/generated/graphql.schema.json | 4 ++-- .../graphql/schema/generated/schema.graphql | 4 ++-- .../graphql/schema/generated/types/index.ts | 18 +++++++++--------- .../schema/technology/technology.typedefs.ts | 4 ++-- .../src/mocks/technology-entity.ts | 4 ++-- .../src/mocks/technology.ts | 2 +- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts index b3975e74d..ae3bbcad6 100644 --- a/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts +++ b/starters/express-apollo-prisma/src/graphql/data-sources/technology-data-source.ts @@ -4,7 +4,7 @@ import { InputMaybe } from '../schema/generated/types'; type TechnologyEntityId = TechnologyEntity['id']; -export type TechnologyNode = { +export type TechnologyEdge = { cursor: TechnologyEntityId; node: TechnologyEntity; }; @@ -19,7 +19,7 @@ export type PageInformation = { export type TechnologyEntityCollection = { totalCount: number; pageInfo: PageInformation; - edges: TechnologyNode[]; + edges: TechnologyEdge[]; }; export class TechnologyDataSource { diff --git a/starters/express-apollo-prisma/src/graphql/mappers/technology.ts b/starters/express-apollo-prisma/src/graphql/mappers/technology.ts index 67ca944f6..3b9c7dac3 100644 --- a/starters/express-apollo-prisma/src/graphql/mappers/technology.ts +++ b/starters/express-apollo-prisma/src/graphql/mappers/technology.ts @@ -16,7 +16,7 @@ export const mapTechnologyCollection = ( return { totalCount: entityCollectionPage.totalCount, edges: entityCollectionPage.edges.map((entity) => ({ - __typename: 'TechnologyNode', + __typename: 'TechnologyEdge', node: mapTechnology(entity.node), cursor: entity.cursor, })), diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json b/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json index f4b02ab66..7147379ae 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/graphql.schema.json @@ -453,7 +453,7 @@ "name": null, "ofType": { "kind": "OBJECT", - "name": "TechnologyNode", + "name": "TechnologyEdge", "ofType": null } } @@ -501,7 +501,7 @@ }, { "kind": "OBJECT", - "name": "TechnologyNode", + "name": "TechnologyEdge", "description": "Pagination Technology Node", "fields": [ { diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql b/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql index 65be9e0ed..8f92d2adc 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/schema.graphql @@ -67,7 +67,7 @@ A collection of technologies """ type TechnologyCollection { "A list of records of the requested page" - edges: [TechnologyNode]! + edges: [TechnologyEdge]! "Pagination Information" pageInfo: PageInformation! "Identifies the total count of technology records in data source" @@ -77,7 +77,7 @@ type TechnologyCollection { """ Pagination Technology Node """ -type TechnologyNode { +type TechnologyEdge { "Current Cursor for Entity Node" cursor: Int! "Technology Entity Node" diff --git a/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts b/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts index 64c732a6a..6d2b3fc80 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/generated/types/index.ts @@ -100,7 +100,7 @@ export type Technology = { export type TechnologyCollection = { __typename?: 'TechnologyCollection'; /** A list of records of the requested page */ - edges: Array>; + edges: Array>; /** Pagination Information */ pageInfo: PageInformation; /** Identifies the total count of technology records in data source */ @@ -108,8 +108,8 @@ export type TechnologyCollection = { }; /** Pagination Technology Node */ -export type TechnologyNode = { - __typename?: 'TechnologyNode'; +export type TechnologyEdge = { + __typename?: 'TechnologyEdge'; /** Current Cursor for Entity Node */ cursor: Scalars['Int']; /** Technology Entity Node */ @@ -219,7 +219,7 @@ export type ResolversTypes = { String: ResolverTypeWrapper; Technology: ResolverTypeWrapper; TechnologyCollection: ResolverTypeWrapper; - TechnologyNode: ResolverTypeWrapper; + TechnologyEdge: ResolverTypeWrapper; UpdateTechnology: UpdateTechnology; }; @@ -235,7 +235,7 @@ export type ResolversParentTypes = { String: Scalars['String']; Technology: Technology; TechnologyCollection: TechnologyCollection; - TechnologyNode: TechnologyNode; + TechnologyEdge: TechnologyEdge; UpdateTechnology: UpdateTechnology; }; @@ -307,15 +307,15 @@ export type TechnologyCollectionResolvers< ContextType = any, ParentType extends ResolversParentTypes['TechnologyCollection'] = ResolversParentTypes['TechnologyCollection'] > = { - edges?: Resolver>, ParentType, ContextType>; + edges?: Resolver>, ParentType, ContextType>; pageInfo?: Resolver; totalCount?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; -export type TechnologyNodeResolvers< +export type TechnologyEdgeResolvers< ContextType = any, - ParentType extends ResolversParentTypes['TechnologyNode'] = ResolversParentTypes['TechnologyNode'] + ParentType extends ResolversParentTypes['TechnologyEdge'] = ResolversParentTypes['TechnologyEdge'] > = { cursor?: Resolver; node?: Resolver; @@ -328,5 +328,5 @@ export type Resolvers = { Query?: QueryResolvers; Technology?: TechnologyResolvers; TechnologyCollection?: TechnologyCollectionResolvers; - TechnologyNode?: TechnologyNodeResolvers; + TechnologyEdge?: TechnologyEdgeResolvers; }; diff --git a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts index 6ce3c0f5c..6260676b8 100644 --- a/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts +++ b/starters/express-apollo-prisma/src/graphql/schema/technology/technology.typedefs.ts @@ -18,7 +18,7 @@ export const technologyTypeDefs = gql` """ Pagination Technology Node """ - type TechnologyNode { + type TechnologyEdge { "Current Cursor for Entity Node" cursor: Int! "Technology Entity Node" @@ -46,7 +46,7 @@ export const technologyTypeDefs = gql` "Identifies the total count of technology records in data source" totalCount: Int! "A list of records of the requested page" - edges: [TechnologyNode]! + edges: [TechnologyEdge]! "Pagination Information" pageInfo: PageInformation! } diff --git a/starters/express-apollo-prisma/src/mocks/technology-entity.ts b/starters/express-apollo-prisma/src/mocks/technology-entity.ts index 0f43ed718..0cf2faf08 100644 --- a/starters/express-apollo-prisma/src/mocks/technology-entity.ts +++ b/starters/express-apollo-prisma/src/mocks/technology-entity.ts @@ -4,7 +4,7 @@ import { PageInformation, TechnologyDataSource, TechnologyEntityCollection, - TechnologyNode, + TechnologyEdge, } from '../graphql/data-sources'; export const createMockTechnologyDataSource = (): DeepMockProxy => @@ -45,7 +45,7 @@ export const createMockTechnologyEntities = (totalCount: number): TechnologyEnti return Array(totalCount).fill(null).map(createMockTechnologyEntity); }; -export const createMockTechnologyNodes = (totalCount: number): TechnologyNode[] => { +export const createMockTechnologyEdges = (totalCount: number): TechnologyEdge[] => { return Array(totalCount) .fill(null) .map(() => { diff --git a/starters/express-apollo-prisma/src/mocks/technology.ts b/starters/express-apollo-prisma/src/mocks/technology.ts index 4edb114b4..2fe63cf2d 100644 --- a/starters/express-apollo-prisma/src/mocks/technology.ts +++ b/starters/express-apollo-prisma/src/mocks/technology.ts @@ -26,7 +26,7 @@ export const createMockTechnologyCollectionResult = ( .map(() => { const MOCK_TECHNOLOGY = createMockTechnology(); return { - __typename: 'TechnologyNode', + __typename: 'TechnologyEdge', node: MOCK_TECHNOLOGY, cursor: Number(MOCK_TECHNOLOGY.id), };