From d7ebe5eac0d7cab34b4ae34f4b5ad96402e2c36e Mon Sep 17 00:00:00 2001 From: Geoffrey MERRAN Date: Fri, 6 Jan 2023 17:31:48 +0100 Subject: [PATCH 1/2] feat: add acceptedAt sort to listings --- src/Resolver/Arguments/Sort.ts | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/Resolver/Arguments/Sort.ts b/src/Resolver/Arguments/Sort.ts index b3afb69..c203b74 100644 --- a/src/Resolver/Arguments/Sort.ts +++ b/src/Resolver/Arguments/Sort.ts @@ -6,7 +6,7 @@ type TSortValue = "ASC" | "DESC" type TSortInput = Record /** - * Given an input sort (currentSort) and a default sort input, outputs the + * Given an input sort (currentSort) and a default sort input, outputs the * default sort input only if the input sort is empty, otherwise outputs the * input sort. */ @@ -26,11 +26,15 @@ export class ListingsSortInput { @Field(type => String, { nullable: true }) @IsIn(["ASC", "DESC"]) price?: "ASC" | "DESC" - + @Field(type => String, { nullable: true }) @IsIn(["ASC", "DESC"]) createdAt?: "ASC" | "DESC" + @Field(type => String, { nullable: true }) + @IsIn(["ASC", "DESC"]) + acceptedAt?: "ASC" | "DESC" + // search-related sort filter @Field(type => String, { nullable: true }) @IsIn(["DESC"]) @@ -43,7 +47,7 @@ export class OffersSortInput { @Field(type => String, { nullable: true }) @IsIn(["ASC", "DESC"]) price?: "ASC" | "DESC" - + @Field(type => String, { nullable: true }) @IsIn(["ASC", "DESC"]) createdAt?: "ASC" | "DESC" @@ -55,11 +59,11 @@ export class ObjktsSortInput { @Field(type => String, { nullable: true }) @IsIn(["ASC", "DESC"]) id?: "ASC" | "DESC" - + @Field(type => String, { nullable: true }) @IsIn(["ASC", "DESC"]) listingPrice?: "ASC" | "DESC" - + @Field(type => String, { nullable: true }) @IsIn(["ASC", "DESC"]) listingCreatedAt?: "ASC" | "DESC" @@ -75,7 +79,7 @@ export class ObjktsSortInput { @Field(type => String, { nullable: true }) @IsIn(["ASC", "DESC"]) iteration?: "ASC" | "DESC" - + @Field(type => String, { nullable: true }) @IsIn(["ASC", "DESC"]) collectedAt?: "ASC" | "DESC" @@ -91,7 +95,7 @@ export class ObjktsSortInput { } @InputType() -export class GenerativeSortInput { +export class GenerativeSortInput { @Field(type => String, { nullable: true }) @IsIn(["ASC", "DESC"]) lockEnd?: "ASC" | "DESC" @@ -99,15 +103,15 @@ export class GenerativeSortInput { @Field(type => String, { nullable: true }) @IsIn(["ASC", "DESC"]) mintOpensAt?: "ASC" | "DESC" - + @Field(type => String, { nullable: true }) @IsIn(["ASC", "DESC"]) price?: "ASC" | "DESC" - + @Field(type => String, { nullable: true }) @IsIn(["ASC", "DESC"]) supply?: "ASC" | "DESC" - + @Field(type => String, { nullable: true }) @IsIn(["ASC", "DESC"]) balance?: "ASC" | "DESC" @@ -193,11 +197,11 @@ export class ActionsSortInput { } @InputType() -export class UserSortInput { +export class UserSortInput { @Field(type => String, { nullable: true }) @IsIn(["ASC", "DESC"]) createdAt?: "ASC" | "DESC" - + // search-related sort filter @Field(type => String, { nullable: true }) @IsIn(["DESC"]) @@ -221,9 +225,9 @@ export class ArticleSortInput { @Field(type => String, { nullable: true }) @IsIn(["ASC", "DESC"]) royalties?: "ASC" | "DESC" - + // search-related sort filter @Field(type => String, { nullable: true }) @IsIn(["DESC"]) relevance?: "DESC" -} \ No newline at end of file +} From 6853f4bac7fb0def8694d7eddf2e974a15e38cc8 Mon Sep 17 00:00:00 2001 From: Geoffrey MERRAN Date: Fri, 6 Jan 2023 18:58:31 +0100 Subject: [PATCH 2/2] feat: add acceptedBy for listing --- src/Entity/Listing.ts | 26 +++++++++---- src/Entity/User.ts | 3 ++ src/Resolver/ListingResolver.ts | 34 ++++++++++++----- src/Resolver/UserResolver.ts | 68 +++++++++++++++++++++++---------- src/index.ts | 2 +- 5 files changed, 94 insertions(+), 39 deletions(-) diff --git a/src/Entity/Listing.ts b/src/Entity/Listing.ts index 6eaab14..033e151 100644 --- a/src/Entity/Listing.ts +++ b/src/Entity/Listing.ts @@ -42,13 +42,13 @@ export class Listing extends BaseEntity { @ManyToOne(() => Objkt, objkt => objkt.listings) objkt: Objkt - + @Column() objktId: number - + @ManyToOne(() => Article, article => article.listings) article?: Article - + @Column() articleId: number @@ -56,7 +56,7 @@ export class Listing extends BaseEntity { description: "The amount of the asset in the listing. For NFTs it will always be 1." }) @Column({ type: "bigint" }) - amount: number + amount: number @Field({ description: "The listing price, **in mutez**" @@ -77,7 +77,7 @@ export class Listing extends BaseEntity { @Column({ type: "timestamptz" }) @Filter([ "gte", "lte" ], type => Date) createdAt: Date - + @Field({ nullable: true, description: "When the listing was cancelled by the seller (if null, listing was never cancelled)", @@ -86,7 +86,7 @@ export class Listing extends BaseEntity { @Filter([ "gte", "lte" ], type => Date) @Filter([ "exist" ], type => Boolean) cancelledAt: Date - + @Field({ nullable: true, description: "When the listing was accepted by the buyer (if null, listing was never accepted)", @@ -96,7 +96,17 @@ export class Listing extends BaseEntity { @Filter([ "exist" ], type => Boolean) acceptedAt: Date - + @Field(type => User, { + nullable: true, + description: "The buyer that has accepted the listing", + }) + @Index() + @ManyToOne(() => User, user => user.acceptedListings) + acceptedBy: User + + @Column() + acceptedById: string + // // FILTERS FOR THE GQL ENDPOINT // @@ -117,4 +127,4 @@ export class Listing extends BaseEntity { asset: EListingAssetType } -export const FiltersListing = generateFilterType(Listing) \ No newline at end of file +export const FiltersListing = generateFilterType(Listing) diff --git a/src/Entity/User.ts b/src/Entity/User.ts index 88355b7..ce7dc97 100644 --- a/src/Entity/User.ts +++ b/src/Entity/User.ts @@ -213,6 +213,9 @@ export class User extends BaseEntity { @Column({ type: "timestamptz", transformer: DateTransformer }) updatedAt: string + @OneToMany(() => Listing, listing => listing.acceptedBy) + acceptedListings: Listing[] + // // CUSTOM FILTERS // diff --git a/src/Resolver/ListingResolver.ts b/src/Resolver/ListingResolver.ts index bc2a8e6..dc661ba 100644 --- a/src/Resolver/ListingResolver.ts +++ b/src/Resolver/ListingResolver.ts @@ -24,7 +24,7 @@ export class ListingResolver { return ctx.usersLoader.load(listing.issuerId) } - @FieldResolver(returns => Objkt, { + @FieldResolver(returns => Objkt, { nullable: true, description: "The objkt associated with the listing, if any." }) @@ -37,7 +37,7 @@ export class ListingResolver { return ctx.objktsLoader.load(listing.objktId) } - @FieldResolver(returns => Article, { + @FieldResolver(returns => Article, { nullable: true, description: "The article associated with the listing, if any." }) @@ -49,8 +49,22 @@ export class ListingResolver { if (listing.article) return listing.article return ctx.articlesLoader.load(listing.articleId) } - - @Query(returns => [Listing],{ + + @FieldResolver(returns => User, { + nullable: true, + description: "The user who bought the listing." + }) + acceptedBy( + @Root() listing: Listing, + @Ctx() ctx: RequestContext + ) { + if (listing.acceptedById === null) return null + if (listing.acceptedBy) return listing.acceptedBy + return ctx.usersLoader.load(listing.acceptedById) + } + + + @Query(returns => [Listing],{ description: "The go-to endpoint to explore Listings on the marketplace. This endpoint both returns Listings made on the old and new marketplace contracts. **By default, only returns active listings (not accepted nor cancelled)**" }) async listings( @@ -82,7 +96,7 @@ export class ListingResolver { // if their is a search string, we first make a request to the search engine to get results if (filters?.searchQuery_eq) { - const searchResults = await searchIndexMarketplace.search(filters.searchQuery_eq, { + const searchResults = await searchIndexMarketplace.search(filters.searchQuery_eq, { hitsPerPage: 5000 }) @@ -99,7 +113,7 @@ export class ListingResolver { `(listing.id, listing.version) IN(${formatted})` ) } - + // if the sort option is relevance, we remove the sort arguments as the order // of the search results needs to be preserved if (sortArgs.relevance && ids.length > 1) { @@ -135,7 +149,7 @@ export class ListingResolver { query.andWhere("token.balance > 0") } } - + // filter for author of the listing verified if (filters?.authorVerified_eq != null) { query.leftJoin("token.author", "author") @@ -160,7 +174,7 @@ export class ListingResolver { if (filters.asset_eq) { if (filters.asset_eq === EListingAssetType.ARTICLE) { query.andWhere("listing.articleId IS NOT NULL") - } + } else if (filters.asset_eq === EListingAssetType.GENTK) { query.andWhere("listing.objktId IS NOT NULL") } @@ -181,7 +195,7 @@ export class ListingResolver { return query.getMany() } - + @Query(returns => [Listing], { nullable: true, description: "Given a list of Listing identifiers (ID + contract-version), outputs the associated listings.", @@ -198,4 +212,4 @@ export class ListingResolver { return query.getMany() } -} \ No newline at end of file +} diff --git a/src/Resolver/UserResolver.ts b/src/Resolver/UserResolver.ts index 4dfeb23..87d899f 100644 --- a/src/Resolver/UserResolver.ts +++ b/src/Resolver/UserResolver.ts @@ -29,8 +29,8 @@ export class UserResolver { return mapUserAuthorizationIdsToEnum(user.authorizations) } - @FieldResolver(returns => [Objkt], { - description: "The gentks owned by the user. Can be used to explore their collection using various filters." + @FieldResolver(returns => [Objkt], { + description: "The gentks owned by the user. Can be used to explore their collection using various filters." }) async objkts( @Root() user: User, @@ -46,17 +46,17 @@ export class UserResolver { id: "DESC" } } - + let query = Objkt.createQueryBuilder("objkt") query.where("objkt.ownerId = :ownerId", { ownerId: user.id }) - // we add the issuer relationship because it's required for most of the + // we add the issuer relationship because it's required for most of the // tasks, and also requested most of the time by the API calls query.leftJoinAndSelect("objkt.issuer", "issuer") // FILTER / SORT query = await objktQueryFilter( - query, + query, { general: filters }, @@ -98,16 +98,16 @@ export class UserResolver { // FILTER / SORT query = await objktQueryFilter( - query, + query, { general: filters }, ) - + return query.getMany() } - @FieldResolver(returns => [Objkt], { + @FieldResolver(returns => [Objkt], { description: "Returns the entire collection of a user, in token ID order" }) entireCollection( @@ -116,7 +116,7 @@ export class UserResolver { ) { return ctx.userObjktsLoader.load(user.id) } - + @FieldResolver(returns => [User], { description: "Given a list of filters to apply to a user's collection, outputs a list of Authors returned by the search on the Gentks, without a limit on the number of results." }) @@ -133,12 +133,12 @@ export class UserResolver { // apply the filters query = await objktQueryFilter( - query, + query, { general: filters }, ) - + return query.getMany() } @@ -154,7 +154,7 @@ export class UserResolver { ) { // default skip/takr [skip, take] = useDefaultValues([skip, take], [0, 20]) - // default sort + // default sort if (!sort || Object.keys(sort).length === 0) { sort = { mintOpensAt: "DESC" @@ -227,6 +227,34 @@ export class UserResolver { return query.getMany() } + @FieldResolver(returns => [Listing], { + description: "The Listings accepted by the user." + }) + acceptedListings( + @Root() user: User, + @Ctx() ctx: RequestContext, + @Args() { skip, take }: PaginationArgs + ) { + [skip, take] = useDefaultValues([skip, take], [0, 20]) + + let query = Listing.createQueryBuilder("listing") + .select() + .where("listing.acceptedById = :userId", { userId: user.id }) + + query.andWhere("listing.acceptedAt is not null") + query.andWhere("listing.cancelledAt is null") + + // order results by acceptedAt time + query.addOrderBy("listing.acceptedAt", "DESC") + + // add pagination + query.skip(skip) + query.take(take) + + return query.getMany() + } + + @FieldResolver(returns => [Offer], { description: "Returns all the offers made by the user. Can be filtered." }) @@ -316,7 +344,7 @@ export class UserResolver { sortArgs = defaultSort(sortArgs, { createdAt: "DESC" }) - + // create the query let query = Action.createQueryBuilder("action").select() @@ -365,7 +393,7 @@ export class UserResolver { if (user.moderationReason) return user.moderationReason return ctx.moderationReasonsLoader.load(user.moderationReasonId) } - + @Query(returns => [User], { description: "Some unfiltered exploration of the users, with pagination." }) @@ -389,7 +417,7 @@ export class UserResolver { if (filters?.searchQuery_eq) { const searchResults = await searchIndexUser.search( filters.searchQuery_eq, - { + { hitsPerPage: 200 } ) @@ -438,13 +466,13 @@ export class UserResolver { ): Promise { let user: User|null|undefined = null if (id) - user = await User.findOne(id, { - // cache: 10000 + user = await User.findOne(id, { + // cache: 10000 }) else if (name) - user = await User.findOne({ where: { name }, - // cache: 10000 + user = await User.findOne({ where: { name }, + // cache: 10000 }) return user } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 43efc5a..37c45c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -77,4 +77,4 @@ const main = async () => { }) } -main() \ No newline at end of file +main()