Skip to content

Commit e255477

Browse files
authored
feat: owners endpoint (#238)
* feat: owners endpoint * feat: created endpoint * feat: fixed request to match needs * fix: owner response types * feat/added tests for query owners * feat: added test to component * fix: pr comments * fix: pr comments * removed console.log * removed unused line * fix: pr comments * feat: added tokenId to query * fix: pr comment
1 parent e1da15c commit e255477

File tree

10 files changed

+467
-0
lines changed

10 files changed

+467
-0
lines changed

src/adapters/handlers/owners.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { IHttpServerComponent } from '@well-known-components/interfaces'
2+
import { AppComponents, Context } from '../../types'
3+
import { Params } from '../../logic/http/params'
4+
import { asJSON } from '../../logic/http/response'
5+
import { OwnersSortBy } from '../../ports/owner/types'
6+
7+
export function createOwnersHandler(
8+
components: Pick<AppComponents, 'logs' | 'owners'>
9+
): IHttpServerComponent.IRequestHandler<Context<'/owners'>> {
10+
const { owners } = components
11+
return async (context) => {
12+
const params = new Params(context.url.searchParams)
13+
14+
const contractAddress = params.getAddress('contractAddress')
15+
const itemId = params.getString('itemId')
16+
17+
const sortBy = params.getValue<OwnersSortBy>('sortBy', OwnersSortBy)
18+
const orderDirection = params.getString('orderDirection')
19+
20+
const first = params.getNumber('first')
21+
const skip = params.getNumber('skip')
22+
23+
return asJSON(() =>
24+
owners.fetchAndCount({
25+
contractAddress,
26+
itemId,
27+
orderDirection,
28+
sortBy,
29+
first,
30+
skip,
31+
})
32+
)
33+
}
34+
}

src/adapters/routes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { createTrendingHandler } from './handlers/trending'
1515
import { createVolumeHandler } from './handlers/volume'
1616
import { createPricesHandler } from './handlers/prices'
1717
import { createStatsHandler } from './handlers/stats'
18+
import { createOwnersHandler } from './handlers/owners'
1819

1920
export async function setupRoutes(globalContext: GlobalContext) {
2021
const { components } = globalContext
@@ -45,6 +46,8 @@ export async function setupRoutes(globalContext: GlobalContext) {
4546
'/contracts/:contractAddress/tokens/:tokenId',
4647
createNFTHandler(components)
4748
)
49+
router.get('/owners', createOwnersHandler(components))
50+
4851

4952
server.use(router.middleware())
5053
}

src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ import {
153153
getCollectionsAccountFragment,
154154
getCollectionsAccountOrderBy,
155155
} from './logic/accounts/collections'
156+
import { createOwnersComponent } from './ports/owner/component'
156157

157158
async function initComponents(): Promise<AppComponents> {
158159
// Default config
@@ -423,6 +424,11 @@ async function initComponents(): Promise<AppComponents> {
423424
maxCount: 1000,
424425
})
425426

427+
// owners
428+
const owners = createOwnersComponent({
429+
subgraph: collectionsSubgraph,
430+
})
431+
426432
// mints
427433
const collectionsMints = createMintsComponent({
428434
subgraph: collectionsSubgraph,
@@ -660,6 +666,7 @@ async function initComponents(): Promise<AppComponents> {
660666
collectionsSubgraph,
661667
marketplaceSubgraph,
662668
stats,
669+
owners,
663670
}
664671
}
665672

src/ports/owner/component.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { ISubgraphComponent } from '@well-known-components/thegraph-component'
2+
import { HttpError } from '../../logic/http/response'
3+
import { FetchOptions } from '../merger/types'
4+
import {
5+
IOwnerDataComponent,
6+
OwnerFragment,
7+
OwnersFilters,
8+
OwnersSortBy,
9+
} from './types'
10+
import { getOwnersQuery } from './utils'
11+
12+
export function createOwnersComponent(options: {
13+
subgraph: ISubgraphComponent
14+
}): IOwnerDataComponent {
15+
const { subgraph } = options
16+
17+
async function fetchAndCount(
18+
filters: FetchOptions<OwnersFilters, OwnersSortBy>
19+
) {
20+
if (filters.itemId === undefined || !filters.contractAddress) {
21+
throw new HttpError(
22+
'itemId and contractAddress are neccesary params.',
23+
400
24+
)
25+
}
26+
27+
const parsedFilters: FetchOptions<OwnersFilters, OwnersSortBy> = {
28+
...filters,
29+
sortBy: filters.sortBy as OwnersSortBy,
30+
}
31+
32+
const data: { nfts: OwnerFragment[] } = await subgraph.query(
33+
getOwnersQuery(parsedFilters, false)
34+
)
35+
36+
const countData: { nfts: { id: string }[] } = await subgraph.query(
37+
getOwnersQuery(parsedFilters, true)
38+
)
39+
40+
const results = data.nfts.map((owner: OwnerFragment) => ({
41+
issuedId: owner.issuedId,
42+
ownerId: owner.owner.id,
43+
orderStatus: owner.searchOrderStatus,
44+
orderExpiresAt: owner.searchOrderExpiresAt,
45+
tokenId: owner.tokenId
46+
}))
47+
48+
return { data: results, total: countData.nfts.length }
49+
}
50+
51+
return {
52+
fetchAndCount,
53+
}
54+
}

src/ports/owner/types.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { FetchOptions } from '../merger/types'
2+
3+
export type Owners = {
4+
issuedId: string
5+
ownerId: string
6+
orderStatus: string | null
7+
orderExpiresAt: string | null
8+
tokenId: string
9+
}
10+
11+
export type OwnersFilters = {
12+
contractAddress?: string,
13+
first?: number,
14+
itemId?: string,
15+
skip?: number,
16+
orderDirection?: string
17+
}
18+
19+
export enum OwnersSortBy {
20+
ISSUED_ID = 'issuedId',
21+
}
22+
23+
export type OwnerFragment = {
24+
issuedId: string
25+
owner: {
26+
id: string
27+
}
28+
searchOrderStatus: string
29+
searchOrderExpiresAt: string
30+
tokenId: string
31+
}
32+
33+
export interface IOwnerDataComponent {
34+
fetchAndCount(
35+
filters: FetchOptions<OwnersFilters, OwnersSortBy>
36+
): Promise<{data: Owners[], total: number}>
37+
}

src/ports/owner/utils.spec.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { FetchOptions } from '../merger/types'
2+
import { OwnersFilters, OwnersSortBy } from './types'
3+
import { getOwnersQuery } from './utils'
4+
5+
describe('#getOwnersQuery', () => {
6+
let queryFilters: FetchOptions<OwnersFilters, OwnersSortBy>
7+
let isCount: boolean
8+
9+
beforeEach(() => {
10+
queryFilters = {
11+
contractAddress: 'contractAddress',
12+
itemId: 'itemId',
13+
first: 10,
14+
skip: 10,
15+
sortBy: OwnersSortBy.ISSUED_ID,
16+
}
17+
isCount = false
18+
})
19+
20+
describe('when isCount is true', () => {
21+
beforeEach(() => {
22+
isCount = true
23+
})
24+
25+
it('should have first value in 1000', () => {
26+
const query = getOwnersQuery(queryFilters, isCount)
27+
28+
expect(query).toContain('first: 1000')
29+
})
30+
31+
it('should have skip value as undefined', () => {
32+
const query = getOwnersQuery(queryFilters, isCount)
33+
34+
expect(query).not.toContain('skip')
35+
})
36+
37+
it('should contain id', () => {
38+
const query = getOwnersQuery(queryFilters, isCount)
39+
40+
expect(query).toContain('id')
41+
expect(query).not.toContain('...ownerFragment')
42+
expect(query).not.toContain('fragment ownerFragment on NFT')
43+
})
44+
})
45+
46+
describe('when isCount is false', () => {
47+
it('should have first value as first prop', () => {
48+
const query = getOwnersQuery(queryFilters, isCount)
49+
50+
expect(query).toContain(`first: ${queryFilters.first}`)
51+
})
52+
53+
it('should have skip value as skip prop', () => {
54+
const query = getOwnersQuery(queryFilters, isCount)
55+
56+
expect(query).toContain(`skip: ${queryFilters.skip}`)
57+
})
58+
59+
it('should contain ownerFragment', () => {
60+
const query = getOwnersQuery(queryFilters, isCount)
61+
62+
expect(query).toContain('...ownerFragment')
63+
expect(query).toContain('fragment ownerFragment on NFT')
64+
})
65+
})
66+
67+
describe('when the sortby filter is undefined', () => {
68+
beforeEach(() => {
69+
delete queryFilters.sortBy
70+
})
71+
72+
it('should not contain orderBy', () => {
73+
const query = getOwnersQuery(queryFilters, isCount)
74+
75+
expect(query).not.toContain(`orderBy`)
76+
})
77+
})
78+
79+
describe('when sortBy is not undefined', () => {
80+
it('should have orderBy value as sortBy prop ', () => {
81+
const query = getOwnersQuery(queryFilters, isCount)
82+
83+
expect(query).toContain(`orderBy: ${queryFilters.sortBy}`)
84+
})
85+
})
86+
87+
describe('when orderDirection is undefined', () => {
88+
it('should not contain orderDirection', () => {
89+
const query = getOwnersQuery(queryFilters, isCount)
90+
91+
expect(query).not.toContain(`orderDirection`)
92+
})
93+
})
94+
95+
describe('when orderDirection is not undefined', () => {
96+
beforeEach(() => {
97+
queryFilters = { ...queryFilters, orderDirection: 'desc' }
98+
})
99+
100+
it('should have orderDirection value as orderDirection ', () => {
101+
const query = getOwnersQuery(queryFilters, isCount)
102+
103+
expect(query).toContain(`orderDirection: ${queryFilters.orderDirection}`)
104+
})
105+
})
106+
107+
it('should have contractAddress and itemId values as the ones passed by params', () => {
108+
const query = getOwnersQuery(queryFilters, isCount)
109+
110+
expect(query).toContain(`${queryFilters.contractAddress}`)
111+
expect(query).toContain(`${queryFilters.itemId}`)
112+
})
113+
})

src/ports/owner/utils.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { FetchOptions } from '../merger/types'
2+
import {
3+
OwnersFilters,
4+
OwnersSortBy,
5+
} from './types'
6+
7+
export const MAX_RESULTS = 1000
8+
9+
10+
export const getOwnerFragment = () => `
11+
fragment ownerFragment on NFT {
12+
issuedId
13+
owner {
14+
id
15+
}
16+
searchOrderStatus
17+
searchOrderExpiresAt
18+
tokenId
19+
}
20+
`
21+
22+
export function getOwnersQuery(
23+
filters: FetchOptions<OwnersFilters, OwnersSortBy>,
24+
isCount: boolean
25+
) {
26+
const first = isCount ? 1000 : filters.first
27+
const skip = isCount ? undefined : filters.skip
28+
29+
return `
30+
query OwnersData{
31+
nfts(
32+
${first ? `first: ${first}` : ''}
33+
${skip ? `skip: ${skip}` : ''}
34+
${filters.sortBy ? `orderBy: ${filters.sortBy}` : ''}
35+
${filters.orderDirection ? `orderDirection: ${filters.orderDirection}` : ''}
36+
where: {contractAddress: "${filters.contractAddress}", itemBlockchainId: "${filters.itemId}" }) {
37+
${isCount ? 'id' : `...ownerFragment`}
38+
}
39+
}
40+
${isCount ? '' : getOwnerFragment()}
41+
`
42+
}
43+

src/tests/components.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ import {
135135
getCollectionsAccountFragment,
136136
getCollectionsAccountOrderBy,
137137
} from '../logic/accounts/collections'
138+
import { createOwnersComponent } from '../ports/owner/component'
138139

139140
// start TCP port for listeners
140141
let lastUsedPort = 19000 + parseInt(process.env.JEST_WORKER_ID || '1') * 1000
@@ -564,6 +565,10 @@ export async function initComponents(): Promise<AppComponents> {
564565

565566
const statusChecks = await createStatusCheckComponent({ config, server })
566567

568+
const owners = createOwnersComponent({
569+
subgraph: collectionsSubgraph
570+
})
571+
567572
return {
568573
config,
569574
logs,
@@ -588,5 +593,6 @@ export async function initComponents(): Promise<AppComponents> {
588593
rankings,
589594
prices,
590595
stats,
596+
owners
591597
}
592598
}

0 commit comments

Comments
 (0)