diff --git a/apps/core/src/resources/resolvers.ts b/apps/core/src/resources/resolvers.ts index e51d498b..745def1d 100644 --- a/apps/core/src/resources/resolvers.ts +++ b/apps/core/src/resources/resolvers.ts @@ -10,9 +10,9 @@ import { subscribeToNewsletter } from './newsletter/mutations/subscribe'; import { createSnippet } from './snippets/mutations/create-snippet'; import { deleteSnippet } from './snippets/mutations/delete-snippet'; import { updateSnippet } from './snippets/mutations/update-snippet'; -import { allSnippets } from './snippets/queries/all-snippets'; import { findSnippet } from './snippets/queries/find-snippet'; import { mySnippets } from './snippets/queries/my-snippets'; +import { publicSnippets } from './snippets/queries/public-snippets'; import { dateScalar } from './types/date'; import { loginUser } from './users/mutations/login-user'; import { logoutUser } from './users/mutations/logout-user'; @@ -46,7 +46,6 @@ const resolvers: Resolvers = { updateSnippet, }, Query: { - allSnippets, authenticatedUser, findFolder, findSnippet, @@ -55,6 +54,7 @@ const resolvers: Resolvers = { listFolders, mySnippets, ping: () => 'pong', + publicSnippets, }, Snippet: { folder: (snippet, _args, context) => { diff --git a/apps/core/src/resources/snippets/queries/all-snippets.ts b/apps/core/src/resources/snippets/queries/all-snippets.ts deleted file mode 100644 index 9f0b8746..00000000 --- a/apps/core/src/resources/snippets/queries/all-snippets.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { QueryResolvers } from '../../../types/graphql'; - -export const allSnippets: QueryResolvers['allSnippets'] = (_parent, _args, context) => { - return context.db.snippet.findPublicSnippet(); -}; diff --git a/apps/core/src/resources/snippets/queries/public-snippets.ts b/apps/core/src/resources/snippets/queries/public-snippets.ts new file mode 100644 index 00000000..9be0fd31 --- /dev/null +++ b/apps/core/src/resources/snippets/queries/public-snippets.ts @@ -0,0 +1,19 @@ +import { QueryResolvers } from '../../../types/graphql'; + +export const publicSnippets: QueryResolvers['publicSnippets'] = async (_parent, input, context) => { + const { + args: { itemPerPage, nextToken }, + } = input; + + const result = await context.db.snippet.findPublicSnippet({ + cursor: nextToken, + itemPerPage: itemPerPage ?? 10, + }); + + return { + hasMore: result.hasMore, + itemPerPage, + items: result.items, + nextToken: result.nextCursor, + }; +}; diff --git a/apps/core/src/resources/snippets/schema.graphql.ts b/apps/core/src/resources/snippets/schema.graphql.ts index 1d7a88f4..1f6f261e 100644 --- a/apps/core/src/resources/snippets/schema.graphql.ts +++ b/apps/core/src/resources/snippets/schema.graphql.ts @@ -6,6 +6,13 @@ export default gql` paths: [Folder!]! } + type PublicSnippetsResult { + items: [Snippet!]! + hasMore: Boolean! + itemPerPage: Int + nextToken: String + } + input CreateSnippetInput { folderId: String! name: String! @@ -27,6 +34,11 @@ export default gql` theme: String! } + input PublicSnippetsArgs { + nextToken: String + itemPerPage: Int + } + extend type Mutation { createSnippet(input: CreateSnippetInput!): Snippet! updateSnippet(id: ID!, input: UpdateSnippetInput!): Snippet! @@ -34,7 +46,7 @@ export default gql` } extend type Query { - allSnippets: [Snippet!]! + publicSnippets(args: PublicSnippetsArgs!): PublicSnippetsResult! mySnippets: [Snippet!]! findSnippet(snippetId: String!): SnippetInfo! } diff --git a/apps/core/src/types/graphql.d.ts b/apps/core/src/types/graphql.d.ts index b2760795..51d2f192 100644 --- a/apps/core/src/types/graphql.d.ts +++ b/apps/core/src/types/graphql.d.ts @@ -10,7 +10,7 @@ export type ResolverFn = ( parent: TParent, args: TArgs, context: TContext, - info?: GraphQLResolveInfo + info?: GraphQLResolveInfo, ) => Promise | TResult; export type Omit = Pick>; @@ -80,49 +80,40 @@ export type Mutation = { updateSnippet: Snippet; }; - export type MutationCreateFolderArgs = { input: CreateFolderInput; }; - export type MutationCreateSnippetArgs = { input: CreateSnippetInput; }; - export type MutationDeleteFoldersArgs = { folderIds: Array; }; - export type MutationDeleteSnippetArgs = { id: Scalars['ID']; }; - export type MutationLoginUserArgs = { email: Scalars['String']; password: Scalars['String']; }; - export type MutationSignupUserArgs = { input: SignupUserInput; }; - export type MutationSubscribeToNewsletterArgs = { email: Scalars['String']; }; - export type MutationUpdateFolderArgs = { id: Scalars['ID']; input: UpdateFolderInput; }; - export type MutationUpdateSnippetArgs = { id: Scalars['ID']; input: UpdateSnippetInput; @@ -131,13 +122,25 @@ export type MutationUpdateSnippetArgs = { export const OauthProvider = { Github: 'github', Stackoverflow: 'stackoverflow', - Twitter: 'twitter' + Twitter: 'twitter', } as const; export type OauthProvider = typeof OauthProvider[keyof typeof OauthProvider]; +export type PublicSnippetsArgs = { + itemPerPage?: InputMaybe; + nextToken?: InputMaybe; +}; + +export type PublicSnippetsResult = { + __typename?: 'PublicSnippetsResult'; + hasMore: Scalars['Boolean']; + itemPerPage?: Maybe; + items: Array; + nextToken?: Maybe; +}; + export type Query = { __typename?: 'Query'; - allSnippets: Array; authenticatedUser: User; findFolder: Folder; findSnippet: SnippetInfo; @@ -148,28 +151,29 @@ export type Query = { mySnippets: Array; /** @deprecated https://stackoverflow.com/questions/59868942/graphql-a-schema-must-have-a-query-operation-defined */ ping?: Maybe; + publicSnippets: PublicSnippetsResult; }; - export type QueryFindFolderArgs = { folderId: Scalars['String']; }; - export type QueryFindSnippetArgs = { snippetId: Scalars['String']; }; - export type QueryListDirectoryArgs = { folderId: Scalars['String']; }; - export type QueryListFoldersArgs = { folderId?: InputMaybe; }; +export type QueryPublicSnippetsArgs = { + args: PublicSnippetsArgs; +}; + export type Result = { __typename?: 'Result'; message: Scalars['String']; @@ -187,7 +191,7 @@ export type Role = { export const RoleName = { Admin: 'admin', - User: 'user' + User: 'user', } as const; export type RoleName = typeof RoleName[keyof typeof RoleName]; @@ -227,7 +231,7 @@ export type SnippetInfo = { export const SnippetVisibility = { Private: 'private', - Public: 'public' + Public: 'public', } as const; export type SnippetVisibility = typeof SnippetVisibility[keyof typeof SnippetVisibility]; @@ -262,28 +266,27 @@ export type User = { username?: Maybe; }; - - export type ResolverTypeWrapper = Promise | T; - export type ResolverWithResolve = { resolve: ResolverFn; }; -export type Resolver = ResolverFn | ResolverWithResolve; +export type Resolver = + | ResolverFn + | ResolverWithResolve; export type SubscriptionSubscribeFn = ( parent: TParent, args: TArgs, context: TContext, - info: GraphQLResolveInfo + info: GraphQLResolveInfo, ) => AsyncIterable | Promise>; export type SubscriptionResolveFn = ( parent: TParent, args: TArgs, context: TContext, - info: GraphQLResolveInfo + info: GraphQLResolveInfo, ) => TResult | Promise; export interface SubscriptionSubscriberObject { @@ -307,10 +310,14 @@ export type SubscriptionResolver = ( parent: TParent, context: TContext, - info: GraphQLResolveInfo + info: GraphQLResolveInfo, ) => Maybe | Promise>; -export type IsTypeOfResolverFn = (obj: T, context: TContext, info: GraphQLResolveInfo) => boolean | Promise; +export type IsTypeOfResolverFn = ( + obj: T, + context: TContext, + info: GraphQLResolveInfo, +) => boolean | Promise; export type NextResolverFn = () => Promise; @@ -319,7 +326,7 @@ export type DirectiveResolverFn TResult | Promise; /** Mapping between all available schema types and the resolvers types */ @@ -328,13 +335,23 @@ export type ResolversTypes = { CreateFolderInput: CreateFolderInput; CreateSnippetInput: CreateSnippetInput; Date: ResolverTypeWrapper; - Directory: ResolverTypeWrapper & { folders: Array, paths: Array, snippets: Array }>; + Directory: ResolverTypeWrapper< + Omit & { + folders: Array; + paths: Array; + snippets: Array; + } + >; Folder: ResolverTypeWrapper; ID: ResolverTypeWrapper; Int: ResolverTypeWrapper; LoginResult: ResolverTypeWrapper; Mutation: ResolverTypeWrapper<{}>; OauthProvider: OauthProvider; + PublicSnippetsArgs: PublicSnippetsArgs; + PublicSnippetsResult: ResolverTypeWrapper< + Omit & { items: Array } + >; Query: ResolverTypeWrapper<{}>; Result: ResolverTypeWrapper; Role: ResolverTypeWrapper; @@ -342,7 +359,12 @@ export type ResolversTypes = { SignupUserInput: SignupUserInput; SignupUserResult: ResolverTypeWrapper; Snippet: ResolverTypeWrapper; - SnippetInfo: ResolverTypeWrapper & { paths: Array, snippet: ResolversTypes['Snippet'] }>; + SnippetInfo: ResolverTypeWrapper< + Omit & { + paths: Array; + snippet: ResolversTypes['Snippet']; + } + >; SnippetVisibility: SnippetVisibility; String: ResolverTypeWrapper; UpdateFolderInput: UpdateFolderInput; @@ -356,19 +378,28 @@ export type ResolversParentTypes = { CreateFolderInput: CreateFolderInput; CreateSnippetInput: CreateSnippetInput; Date: Scalars['Date']; - Directory: Omit & { folders: Array, paths: Array, snippets: Array }; + Directory: Omit & { + folders: Array; + paths: Array; + snippets: Array; + }; Folder: Folder; ID: Scalars['ID']; Int: Scalars['Int']; LoginResult: LoginResult; Mutation: {}; + PublicSnippetsArgs: PublicSnippetsArgs; + PublicSnippetsResult: Omit & { items: Array }; Query: {}; Result: Result; Role: Role; SignupUserInput: SignupUserInput; SignupUserResult: SignupUserResult; Snippet: Snippet; - SnippetInfo: Omit & { paths: Array, snippet: ResolversParentTypes['Snippet'] }; + SnippetInfo: Omit & { + paths: Array; + snippet: ResolversParentTypes['Snippet']; + }; String: Scalars['String']; UpdateFolderInput: UpdateFolderInput; UpdateSnippetInput: UpdateSnippetInput; @@ -379,14 +410,20 @@ export interface DateScalarConfig extends GraphQLScalarTypeConfig = { +export type DirectoryResolvers< + ContextType = AppContext, + ParentType extends ResolversParentTypes['Directory'] = ResolversParentTypes['Directory'], +> = { folders?: Resolver, ParentType, ContextType>; paths?: Resolver, ParentType, ContextType>; snippets?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; -export type FolderResolvers = { +export type FolderResolvers< + ContextType = AppContext, + ParentType extends ResolversParentTypes['Folder'] = ResolversParentTypes['Folder'], +> = { createdAt?: Resolver; id?: Resolver; isFavorite?: Resolver; @@ -399,42 +436,133 @@ export type FolderResolvers; }; -export type LoginResultResolvers = { +export type LoginResultResolvers< + ContextType = AppContext, + ParentType extends ResolversParentTypes['LoginResult'] = ResolversParentTypes['LoginResult'], +> = { token?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; -export type MutationResolvers = { - createFolder?: Resolver>; - createSnippet?: Resolver>; - deleteFolders?: Resolver>; - deleteSnippet?: Resolver>; - loginUser?: Resolver>; +export type MutationResolvers< + ContextType = AppContext, + ParentType extends ResolversParentTypes['Mutation'] = ResolversParentTypes['Mutation'], +> = { + createFolder?: Resolver< + ResolversTypes['Folder'], + ParentType, + ContextType, + RequireFields + >; + createSnippet?: Resolver< + ResolversTypes['Snippet'], + ParentType, + ContextType, + RequireFields + >; + deleteFolders?: Resolver< + ResolversTypes['Boolean'], + ParentType, + ContextType, + RequireFields + >; + deleteSnippet?: Resolver< + ResolversTypes['Boolean'], + ParentType, + ContextType, + RequireFields + >; + loginUser?: Resolver< + ResolversTypes['LoginResult'], + ParentType, + ContextType, + RequireFields + >; logoutUser?: Resolver; - signupUser?: Resolver>; - subscribeToNewsletter?: Resolver>; - updateFolder?: Resolver>; - updateSnippet?: Resolver>; + signupUser?: Resolver< + ResolversTypes['SignupUserResult'], + ParentType, + ContextType, + RequireFields + >; + subscribeToNewsletter?: Resolver< + ResolversTypes['Result'], + ParentType, + ContextType, + RequireFields + >; + updateFolder?: Resolver< + ResolversTypes['Folder'], + ParentType, + ContextType, + RequireFields + >; + updateSnippet?: Resolver< + ResolversTypes['Snippet'], + ParentType, + ContextType, + RequireFields + >; +}; + +export type PublicSnippetsResultResolvers< + ContextType = AppContext, + ParentType extends ResolversParentTypes['PublicSnippetsResult'] = ResolversParentTypes['PublicSnippetsResult'], +> = { + hasMore?: Resolver; + itemPerPage?: Resolver, ParentType, ContextType>; + items?: Resolver, ParentType, ContextType>; + nextToken?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; }; -export type QueryResolvers = { - allSnippets?: Resolver, ParentType, ContextType>; +export type QueryResolvers< + ContextType = AppContext, + ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query'], +> = { authenticatedUser?: Resolver; - findFolder?: Resolver>; - findSnippet?: Resolver>; + findFolder?: Resolver< + ResolversTypes['Folder'], + ParentType, + ContextType, + RequireFields + >; + findSnippet?: Resolver< + ResolversTypes['SnippetInfo'], + ParentType, + ContextType, + RequireFields + >; hello?: Resolver; - listDirectory?: Resolver, ParentType, ContextType, RequireFields>; + listDirectory?: Resolver< + Maybe, + ParentType, + ContextType, + RequireFields + >; listFolders?: Resolver, ParentType, ContextType, Partial>; mySnippets?: Resolver, ParentType, ContextType>; ping?: Resolver, ParentType, ContextType>; -}; - -export type ResultResolvers = { + publicSnippets?: Resolver< + ResolversTypes['PublicSnippetsResult'], + ParentType, + ContextType, + RequireFields + >; +}; + +export type ResultResolvers< + ContextType = AppContext, + ParentType extends ResolversParentTypes['Result'] = ResolversParentTypes['Result'], +> = { message?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; -export type RoleResolvers = { +export type RoleResolvers< + ContextType = AppContext, + ParentType extends ResolversParentTypes['Role'] = ResolversParentTypes['Role'], +> = { createdAt?: Resolver; description?: Resolver, ParentType, ContextType>; id?: Resolver; @@ -444,12 +572,18 @@ export type RoleResolvers; }; -export type SignupUserResultResolvers = { +export type SignupUserResultResolvers< + ContextType = AppContext, + ParentType extends ResolversParentTypes['SignupUserResult'] = ResolversParentTypes['SignupUserResult'], +> = { message?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; -export type SnippetResolvers = { +export type SnippetResolvers< + ContextType = AppContext, + ParentType extends ResolversParentTypes['Snippet'] = ResolversParentTypes['Snippet'], +> = { content?: Resolver; createdAt?: Resolver; description?: Resolver, ParentType, ContextType>; @@ -466,13 +600,19 @@ export type SnippetResolvers; }; -export type SnippetInfoResolvers = { +export type SnippetInfoResolvers< + ContextType = AppContext, + ParentType extends ResolversParentTypes['SnippetInfo'] = ResolversParentTypes['SnippetInfo'], +> = { paths?: Resolver, ParentType, ContextType>; snippet?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; -export type UserResolvers = { +export type UserResolvers< + ContextType = AppContext, + ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'], +> = { createdAt?: Resolver; email?: Resolver; folders?: Resolver, ParentType, ContextType>; @@ -495,6 +635,7 @@ export type Resolvers = { Folder?: FolderResolvers; LoginResult?: LoginResultResolvers; Mutation?: MutationResolvers; + PublicSnippetsResult?: PublicSnippetsResultResolvers; Query?: QueryResolvers; Result?: ResultResolvers; Role?: RoleResolvers; @@ -503,4 +644,3 @@ export type Resolvers = { SnippetInfo?: SnippetInfoResolvers; User?: UserResolvers; }; - diff --git a/packages/domain/src/snippets/snippet.service.ts b/packages/domain/src/snippets/snippet.service.ts index 7c61d106..d9c2ccc0 100644 --- a/packages/domain/src/snippets/snippet.service.ts +++ b/packages/domain/src/snippets/snippet.service.ts @@ -5,6 +5,8 @@ import CreateSnippetDto from './dtos/create-snippet-dto'; import DeleteSnippetDto from './dtos/delete-snippet-dto'; import UpdateSnippetDto from './dtos/update-snippet-dto'; +const MAX_ITEM_PER_PAGE = 50; + export default class SnippetService { async create(createSnippetDto: CreateSnippetDto): Promise { const isSnippetExist = await this.isSnippetExistInFolder(createSnippetDto.folderId, createSnippetDto.name); @@ -56,11 +58,35 @@ export default class SnippetService { }); } - async findPublicSnippet(): Promise { - return dbClient.snippet.findMany({ + async findPublicSnippet(args: { + cursor?: string | null; + itemPerPage: number; + }): Promise<{ hasMore: boolean; items: Snippet[]; nextCursor: string | null }> { + const { cursor, itemPerPage } = args; + + const limit = Math.min(MAX_ITEM_PER_PAGE, itemPerPage); + + // If the use has 20 we fetch 21 which help to know if there still more in the table + const limitPlusOne = limit + 1; + + const snippets = await dbClient.snippet.findMany({ orderBy: { createdAt: 'desc' }, - where: { visibility: 'public' }, + take: limitPlusOne, + where: { + createdAt: cursor + ? { + lt: new Date(parseInt(cursor, 10)), + } + : undefined, + visibility: 'public', + }, }); + + return { + hasMore: snippets.length === limitPlusOne, + items: snippets.slice(0, limit), + nextCursor: snippets.length > 0 ? snippets[snippets.length - 1].createdAt.getTime().toString() : null, + }; } async delete(deleteSnippetDto: DeleteSnippetDto): Promise {