Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions nftopia-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"cache-manager-redis-store": "^3.0.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"dataloader": "^2.2.3",
"dotenv": "^17.2.3",
"graphql": "^16.13.2",
"ioredis": "^5.3.2",
Expand Down
8,830 changes: 8,830 additions & 0 deletions nftopia-backend/pnpm-lock.yaml

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion nftopia-backend/src/graphql/context/context.factory.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Injectable } from '@nestjs/common';
import type { Request, Response } from 'express';
import { DataSource } from 'typeorm';
import { GraphqlAuthMiddleware } from '../middleware/auth.middleware';
import { createLoaders } from '../loaders';
import type { GraphqlContext } from './context.interface';

@Injectable()
export class GraphqlContextFactory {
constructor(private readonly authMiddleware: GraphqlAuthMiddleware) {}
constructor(
private readonly authMiddleware: GraphqlAuthMiddleware,
private readonly dataSource: DataSource,
) {}

async create(req: Request, res: Response): Promise<GraphqlContext> {
const user = await this.authMiddleware.resolveUser(req);
Expand All @@ -14,6 +19,7 @@ export class GraphqlContextFactory {
req,
res,
user,
loaders: createLoaders(this.dataSource),
};
}
}
2 changes: 2 additions & 0 deletions nftopia-backend/src/graphql/context/context.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Request, Response } from 'express';
import type { DataLoaders } from '../loaders';

export type GraphqlUser = {
userId: string;
Expand All @@ -12,4 +13,5 @@ export interface GraphqlContext {
req: Request;
res: Response;
user?: GraphqlUser;
loaders: DataLoaders;
}
19 changes: 19 additions & 0 deletions nftopia-backend/src/graphql/loaders/auction.loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import DataLoader from 'dataloader';
import { In, type DataSource } from 'typeorm';
import { Auction } from '../../modules/auction/entities/auction.entity';
import { AuctionStatus } from '../../modules/auction/interfaces/auction.interface';

export function createAuctionByNftLoader(dataSource: DataSource) {
return new DataLoader<string, Auction[]>(async (nftTokenIds) => {
const auctions = await dataSource.getRepository(Auction).find({
where: { nftTokenId: In([...nftTokenIds]), status: AuctionStatus.ACTIVE },
});
const map = new Map<string, Auction[]>();
for (const auction of auctions) {
const arr = map.get(auction.nftTokenId) ?? [];
arr.push(auction);
map.set(auction.nftTokenId, arr);
}
return nftTokenIds.map((id) => map.get(id) ?? []);
});
}
19 changes: 19 additions & 0 deletions nftopia-backend/src/graphql/loaders/bid.loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import DataLoader from 'dataloader';
import { In, type DataSource } from 'typeorm';
import { Bid } from '../../modules/auction/entities/bid.entity';

export function createBidByAuctionLoader(dataSource: DataSource) {
return new DataLoader<string, Bid[]>(async (auctionIds) => {
const bids = await dataSource.getRepository(Bid).find({
where: { auctionId: In([...auctionIds]) },
order: { createdAt: 'DESC' },
});
const map = new Map<string, Bid[]>();
for (const bid of bids) {
const arr = map.get(bid.auctionId) ?? [];
arr.push(bid);
map.set(bid.auctionId, arr);
}
return auctionIds.map((id) => map.get(id) ?? []);
});
}
13 changes: 13 additions & 0 deletions nftopia-backend/src/graphql/loaders/collection.loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import DataLoader from 'dataloader';
import { In, type DataSource } from 'typeorm';
import { Collection } from '../../modules/collection/entities/collection.entity';

export function createCollectionLoader(dataSource: DataSource) {
return new DataLoader<string, Collection | null>(async (ids) => {
const collections = await dataSource.getRepository(Collection).find({
where: { id: In([...ids]) },
});
const map = new Map(collections.map((c) => [c.id, c]));
return ids.map((id) => map.get(id) ?? null);
});
}
38 changes: 38 additions & 0 deletions nftopia-backend/src/graphql/loaders/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type DataLoader from 'dataloader';
import type { DataSource } from 'typeorm';
import type { User } from '../../users/user.entity';
import type { Nft } from '../../modules/nft/entities/nft.entity';
import type { Collection } from '../../modules/collection/entities/collection.entity';
import type { Listing } from '../../modules/listing/entities/listing.entity';
import type { Auction } from '../../modules/auction/entities/auction.entity';
import type { Bid } from '../../modules/auction/entities/bid.entity';
import type { Order } from '../../modules/order/entities/order.entity';
import { createUserLoader } from './user.loader';
import { createNftLoader } from './nft.loader';
import { createCollectionLoader } from './collection.loader';
import { createListingByNftLoader } from './listing.loader';
import { createAuctionByNftLoader } from './auction.loader';
import { createBidByAuctionLoader } from './bid.loader';
import { createOrderByNftLoader } from './order.loader';

export interface DataLoaders {
userLoader: DataLoader<string, User | null>;
nftLoader: DataLoader<string, Nft | null>;
collectionLoader: DataLoader<string, Collection | null>;
listingByNftLoader: DataLoader<string, Listing[]>;
auctionByNftLoader: DataLoader<string, Auction[]>;
bidByAuctionLoader: DataLoader<string, Bid[]>;
orderByNftLoader: DataLoader<string, Order[]>;
}

export function createLoaders(dataSource: DataSource): DataLoaders {
return {
userLoader: createUserLoader(dataSource),
nftLoader: createNftLoader(dataSource),
collectionLoader: createCollectionLoader(dataSource),
listingByNftLoader: createListingByNftLoader(dataSource),
auctionByNftLoader: createAuctionByNftLoader(dataSource),
bidByAuctionLoader: createBidByAuctionLoader(dataSource),
orderByNftLoader: createOrderByNftLoader(dataSource),
};
}
18 changes: 18 additions & 0 deletions nftopia-backend/src/graphql/loaders/listing.loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import DataLoader from 'dataloader';
import { In, type DataSource } from 'typeorm';
import { Listing } from '../../modules/listing/entities/listing.entity';

export function createListingByNftLoader(dataSource: DataSource) {
return new DataLoader<string, Listing[]>(async (nftTokenIds) => {
const listings = await dataSource.getRepository(Listing).find({
where: { nftTokenId: In([...nftTokenIds]) },
});
const map = new Map<string, Listing[]>();
for (const listing of listings) {
const arr = map.get(listing.nftTokenId) ?? [];
arr.push(listing);
map.set(listing.nftTokenId, arr);
}
return nftTokenIds.map((id) => map.get(id) ?? []);
});
}
14 changes: 14 additions & 0 deletions nftopia-backend/src/graphql/loaders/nft.loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import DataLoader from 'dataloader';
import { In, type DataSource } from 'typeorm';
import { Nft } from '../../modules/nft/entities/nft.entity';

export function createNftLoader(dataSource: DataSource) {
return new DataLoader<string, Nft | null>(async (ids) => {
const nfts = await dataSource.getRepository(Nft).find({
where: { id: In([...ids]) },
relations: ['attributes'],
});
const map = new Map(nfts.map((n) => [n.id, n]));
return ids.map((id) => map.get(id) ?? null);
});
}
19 changes: 19 additions & 0 deletions nftopia-backend/src/graphql/loaders/order.loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import DataLoader from 'dataloader';
import { In, type DataSource } from 'typeorm';
import { Order } from '../../modules/order/entities/order.entity';

export function createOrderByNftLoader(dataSource: DataSource) {
return new DataLoader<string, Order[]>(async (nftIds) => {
const orders = await dataSource.getRepository(Order).find({
where: { nftId: In([...nftIds]) },
order: { createdAt: 'DESC' },
});
const map = new Map<string, Order[]>();
for (const order of orders) {
const arr = map.get(order.nftId) ?? [];
arr.push(order);
map.set(order.nftId, arr);
}
return nftIds.map((id) => map.get(id) ?? []);
});
}
13 changes: 13 additions & 0 deletions nftopia-backend/src/graphql/loaders/user.loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import DataLoader from 'dataloader';
import { In, type DataSource } from 'typeorm';
import { User } from '../../users/user.entity';

export function createUserLoader(dataSource: DataSource) {
return new DataLoader<string, User | null>(async (ids) => {
const users = await dataSource.getRepository(User).find({
where: { id: In([...ids]) },
});
const map = new Map(users.map((u) => [u.id, u]));
return ids.map((id) => map.get(id) ?? null);
});
}
9 changes: 9 additions & 0 deletions nftopia-backend/src/graphql/resolvers/collection.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
GraphqlCollection,
} from '../types/collection.types';
import { GraphqlNft, NFTConnection } from '../types/nft.types';
import { GraphqlUserProfile } from '../types/user.types';
import type { CollectionCursorPayload } from '../../modules/collection/interfaces/collection.interface';

@Resolver(() => GraphqlCollection)
Expand Down Expand Up @@ -132,6 +133,14 @@ export class CollectionResolver {
return this.toGraphqlCollection(collection);
}

@ResolveField('creator', () => GraphqlUserProfile, { nullable: true })
async creator(
@Parent() collection: GraphqlCollection,
@Context() { loaders }: GraphqlContext,
) {
return loaders.userLoader.load(collection.creatorId);
}

@ResolveField(() => NFTConnection, {
name: 'nfts',
description: 'Fetch NFTs belonging to a collection',
Expand Down
38 changes: 37 additions & 1 deletion nftopia-backend/src/graphql/resolvers/nft.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { Args, Context, ID, Mutation, Query, Resolver } from '@nestjs/graphql';
import {
Args,
Context,
ID,
Mutation,
Parent,
Query,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import {
BadRequestException,
UnauthorizedException,
Expand All @@ -13,6 +22,8 @@ import {
UpdateNFTMetadataInput,
} from '../inputs/nft.inputs';
import { GraphqlNft, NFTConnection } from '../types/nft.types';
import { GraphqlUserProfile } from '../types/user.types';
import { GraphqlCollection } from '../types/collection.types';
import { NftService } from '../../modules/nft/nft.service';
import type { Nft } from '../../modules/nft/entities/nft.entity';

Expand Down Expand Up @@ -146,6 +157,31 @@ export class NftResolver {
return this.toGraphqlNft(nft);
}

@ResolveField('owner', () => GraphqlUserProfile, { nullable: true })
async owner(
@Parent() nft: GraphqlNft,
@Context() { loaders }: GraphqlContext,
) {
return loaders.userLoader.load(nft.ownerId);
}

@ResolveField('creator', () => GraphqlUserProfile, { nullable: true })
async creator(
@Parent() nft: GraphqlNft,
@Context() { loaders }: GraphqlContext,
) {
return loaders.userLoader.load(nft.creatorId);
}

@ResolveField('collection', () => GraphqlCollection, { nullable: true })
async collection(
@Parent() nft: GraphqlNft,
@Context() { loaders }: GraphqlContext,
) {
if (!nft.collectionId) return null;
return loaders.collectionLoader.load(nft.collectionId);
}

private getAuthenticatedUserId(context: GraphqlContext): string {
const userId = context.user?.userId;
if (!userId) {
Expand Down
16 changes: 16 additions & 0 deletions nftopia-backend/src/graphql/types/user.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Field, ID, ObjectType } from '@nestjs/graphql';

@ObjectType('User')
export class GraphqlUserProfile {
@Field(() => ID)
id: string;

@Field({ nullable: true })
username?: string;

@Field({ nullable: true })
avatarUrl?: string;

@Field({ nullable: true })
walletAddress?: string;
}
Loading