Skip to content

Commit 39354f5

Browse files
committed
feat: add collections entity
Without this patch the user of our GraphQL endpoints needs to go via hyperboards to access the collections inside. Another drawback is that via this query they can only access the hypercert id, so they'll have to fire a separate query to combine the data. This patch adds collections as a top level entity and exposes full hypercert objects as child entities. This also applies to hyperboards as the standalone collections entity is being reused there too.
1 parent 8f847a6 commit 39354f5

10 files changed

Lines changed: 284 additions & 21 deletions

File tree

schema.graphql

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ input BooleanSearchOptions {
333333
"""Collection of hypercerts for reference and display purposes"""
334334
type Collection {
335335
admins: [User!]!
336+
blueprints: [Blueprint!]
336337

337338
"""Chain ID of the collection"""
338339
chain_ids: [EthBigInt!]
@@ -342,12 +343,29 @@ type Collection {
342343

343344
"""Description of the collection"""
344345
description: String!
346+
hypercerts: [Hypercert!]
345347
id: ID!
346348

347349
"""Name of the collection"""
348350
name: String!
349351
}
350352

353+
input CollectionFetchInput {
354+
by: CollectionSortOptions
355+
}
356+
357+
input CollectionSortOptions {
358+
created_at: SortOrder
359+
description: SortOrder
360+
name: SortOrder
361+
}
362+
363+
input CollectionWhereInput {
364+
description: StringSearchOptions
365+
id: IdSearchOptions
366+
name: StringSearchOptions
367+
}
368+
351369
"""Pointer to a contract deployed on a chain"""
352370
type Contract {
353371
"""The ID of the chain on which the contract is deployed"""
@@ -469,6 +487,11 @@ type GetBlueprintResponse {
469487
data: [Blueprint!]
470488
}
471489

490+
type GetCollectionsResponse {
491+
count: Int
492+
data: [Collection!]
493+
}
494+
472495
"""Pointer to a contract deployed on a chain"""
473496
type GetContractsResponse {
474497
count: Int
@@ -889,6 +912,7 @@ type Query {
889912
attestationSchemas(first: Int, offset: Int): GetAttestationsSchemaResponse!
890913
attestations(first: Int, offset: Int, sort: AttestationFetchInput, where: AttestationWhereInput): GetAttestationsResponse!
891914
blueprints(first: Int, offset: Int, sort: BlueprintFetchInput, where: BlueprintWhereInput): GetBlueprintResponse!
915+
collections(first: Int, offset: Int, sort: CollectionFetchInput, where: CollectionWhereInput): GetCollectionsResponse!
892916
contracts(first: Int, offset: Int, sort: ContractFetchInput, where: ContractWhereInput): GetContractsResponse!
893917
fractions(first: Int, offset: Int, sort: FractionFetchInput, where: FractionWhereInput): GetFractionsResponse!
894918
hyperboards(first: Int, offset: Int, sort: HyperboardFetchInput, where: HyperboardWhereInput): GetHyperboardsResponse!
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { ArgsType, Field, InputType } from "type-graphql";
2+
3+
import { BasicCollectionWhereInput } from "../inputs/collectionInput.js";
4+
import type { OrderOptions } from "../inputs/orderOptions.js";
5+
import { Collection } from "../typeDefs/collectionTypeDefs.js";
6+
import { CollectionSortOptions } from "../inputs/sortOptions.js";
7+
8+
import { withPagination } from "./baseArgs.js";
9+
10+
@InputType()
11+
export class CollectionWhereInput extends BasicCollectionWhereInput {}
12+
13+
@InputType()
14+
export class CollectionFetchInput implements OrderOptions<Collection> {
15+
@Field(() => CollectionSortOptions, { nullable: true })
16+
by?: CollectionSortOptions;
17+
}
18+
19+
@ArgsType()
20+
export class CollectionArgs {
21+
@Field(() => CollectionWhereInput, { nullable: true })
22+
where?: CollectionWhereInput;
23+
@Field(() => CollectionFetchInput, { nullable: true })
24+
sort?: CollectionFetchInput;
25+
}
26+
27+
@ArgsType()
28+
export class GetCollectionsArgs extends withPagination(CollectionArgs) {}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Field, InputType } from "type-graphql";
2+
3+
import { Collection } from "../typeDefs/collectionTypeDefs.js";
4+
5+
import { IdSearchOptions, StringSearchOptions } from "./searchOptions.js";
6+
import type { WhereOptions } from "./whereOptions.js";
7+
8+
@InputType()
9+
export class BasicCollectionWhereInput implements WhereOptions<Collection> {
10+
@Field(() => IdSearchOptions, { nullable: true })
11+
id?: IdSearchOptions | null;
12+
13+
@Field(() => StringSearchOptions, { nullable: true })
14+
name?: StringSearchOptions;
15+
16+
@Field(() => StringSearchOptions, { nullable: true })
17+
description?: StringSearchOptions;
18+
}

src/graphql/schemas/inputs/sortOptions.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Sale } from "../typeDefs/salesTypeDefs.js";
1111
import { Hyperboard } from "../typeDefs/hyperboardTypeDefs.js";
1212
import { Blueprint } from "../typeDefs/blueprintTypeDefs.js";
1313
import { SignatureRequest } from "../typeDefs/signatureRequestTypeDefs.js";
14+
import { Collection } from "../typeDefs/collectionTypeDefs.js";
1415

1516
export type SortOptions<T extends object> = {
1617
[P in keyof T]: SortOrder | null;
@@ -238,3 +239,13 @@ export class SignatureRequestSortOptions
238239
@Field(() => SortOrder, { nullable: true })
239240
purpose?: SortOrder;
240241
}
242+
243+
@InputType()
244+
export class CollectionSortOptions implements SortOptions<Collection> {
245+
@Field(() => SortOrder, { nullable: true })
246+
name?: SortOrder;
247+
@Field(() => SortOrder, { nullable: true })
248+
created_at?: SortOrder;
249+
@Field(() => SortOrder, { nullable: true })
250+
description?: SortOrder;
251+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import {
2+
Args,
3+
FieldResolver,
4+
ObjectType,
5+
Query,
6+
Resolver,
7+
Root,
8+
} from "type-graphql";
9+
10+
import { GetCollectionsArgs } from "../args/collectionArgs.js";
11+
import { Collection } from "../typeDefs/collectionTypeDefs.js";
12+
import { Blueprint } from "../typeDefs/blueprintTypeDefs.js";
13+
import { User } from "../typeDefs/userTypeDefs.js";
14+
15+
import { createBaseResolver, DataResponse } from "./baseTypes.js";
16+
import GetHypercertsResponse from "./hypercertResolver.js";
17+
18+
@ObjectType()
19+
class GetCollectionsResponse extends DataResponse(Collection) {}
20+
21+
const CollectionBaseResolver = createBaseResolver("collection");
22+
23+
@Resolver(() => Collection)
24+
class CollectionResolver extends CollectionBaseResolver {
25+
@Query(() => GetCollectionsResponse)
26+
async collections(@Args() args: GetCollectionsArgs) {
27+
try {
28+
const res = await this.supabaseDataService.getCollections(args);
29+
30+
return {
31+
data: res.data,
32+
count: res.count,
33+
};
34+
} catch (e) {
35+
console.error("[CollectionResolver::collections] Error:", e);
36+
throw new Error(`Error fetching collections: ${(e as Error).message}`);
37+
}
38+
}
39+
40+
@FieldResolver(() => GetHypercertsResponse)
41+
async hypercerts(@Root() collection: Collection) {
42+
if (!collection.id) {
43+
console.error(
44+
"[CollectionResolver::hypercerts] Collection ID is undefined",
45+
);
46+
return [];
47+
}
48+
49+
const hypercerts = await this.supabaseDataService.getCollectionHypercerts(
50+
collection.id,
51+
);
52+
53+
if (!hypercerts?.length) {
54+
return [];
55+
}
56+
57+
const hypercertIds = hypercerts
58+
.map((h) => h.hypercert_id)
59+
.filter((id): id is string => id !== undefined);
60+
61+
if (hypercertIds.length === 0) {
62+
return [];
63+
}
64+
65+
const hypercertsData = await this.getHypercerts({
66+
where: { hypercert_id: { in: hypercertIds } },
67+
});
68+
69+
return hypercertsData.data || [];
70+
}
71+
72+
@FieldResolver(() => [User])
73+
async admins(@Root() collection: Collection) {
74+
if (!collection.id) {
75+
console.error("[CollectionResolver::admins] Collection ID is undefined");
76+
return [];
77+
}
78+
79+
const admins = await this.supabaseDataService.getCollectionAdmins(
80+
collection.id,
81+
);
82+
return admins || [];
83+
}
84+
85+
@FieldResolver(() => [Blueprint])
86+
async blueprints(@Root() collection: Collection) {
87+
if (!collection.id) {
88+
console.error(
89+
"[CollectionResolver::blueprints] Collection ID is undefined",
90+
);
91+
return [];
92+
}
93+
94+
const blueprints = await this.supabaseDataService.getCollectionBlueprints(
95+
collection.id,
96+
);
97+
return blueprints || [];
98+
}
99+
}
100+
101+
export { CollectionResolver };

src/graphql/schemas/resolvers/composed.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { SalesResolver } from "./salesResolver.js";
1111
import { UserResolver } from "./userResolver.js";
1212
import { BlueprintResolver } from "./blueprintResolver.js";
1313
import { SignatureRequestResolver } from "./signatureRequestResolver.js";
14+
import { CollectionResolver } from "./collectionResolver.js";
1415

1516
export const resolvers = [
1617
ContractResolver,
@@ -26,4 +27,5 @@ export const resolvers = [
2627
UserResolver,
2728
BlueprintResolver,
2829
SignatureRequestResolver,
30+
CollectionResolver,
2931
] as const;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Field, ObjectType } from "type-graphql";
2+
3+
import { EthBigInt } from "../../scalars/ethBigInt.js";
4+
5+
import { BasicTypeDef } from "./baseTypes/basicTypeDef.js";
6+
import { User } from "./userTypeDefs.js";
7+
import { Hypercert } from "./hypercertTypeDefs.js";
8+
import { Blueprint } from "./blueprintTypeDefs.js";
9+
10+
@ObjectType({
11+
description: "Collection of hypercerts for reference and display purposes",
12+
})
13+
export class Collection extends BasicTypeDef {
14+
@Field({ description: "Creation timestamp of the collection" })
15+
created_at?: string;
16+
@Field({ description: "Name of the collection" })
17+
name?: string;
18+
@Field({ description: "Description of the collection" })
19+
description?: string;
20+
@Field(() => [EthBigInt], {
21+
nullable: true,
22+
description: "Chain ID of the collection",
23+
})
24+
chain_ids?: (bigint | number | string)[];
25+
26+
@Field(() => [User])
27+
admins?: User[];
28+
29+
@Field(() => [Hypercert], { nullable: true })
30+
hypercerts?: Hypercert[];
31+
32+
@Field(() => [Blueprint], { nullable: true })
33+
blueprints?: Blueprint[];
34+
}

src/graphql/schemas/typeDefs/hyperboardTypeDefs.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BasicTypeDef } from "./baseTypes/basicTypeDef.js";
33
import { EthBigInt } from "../../scalars/ethBigInt.js";
44
import { User } from "./userTypeDefs.js";
55
import { GraphQLBigInt } from "graphql-scalars";
6+
import { Collection } from "./collectionTypeDefs.js";
67

78
@ObjectType({
89
description: "Hyperboard of hypercerts for reference and display purposes",
@@ -48,26 +49,6 @@ class SectionResponseType {
4849
count?: number;
4950
}
5051

51-
@ObjectType({
52-
description: "Collection of hypercerts for reference and display purposes",
53-
})
54-
class Collection extends BasicTypeDef {
55-
@Field({ description: "Creation timestamp of the collection" })
56-
created_at?: string;
57-
@Field({ description: "Name of the collection" })
58-
name?: string;
59-
@Field({ description: "Description of the collection" })
60-
description?: string;
61-
@Field(() => [EthBigInt], {
62-
nullable: true,
63-
description: "Chain ID of the collection",
64-
})
65-
chain_ids?: (bigint | number | string)[];
66-
67-
@Field(() => [User])
68-
admins?: User[];
69-
}
70-
7152
@ObjectType({
7253
description: "Section representing a collection within a hyperboard",
7354
})

src/services/BaseSupabaseService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export abstract class BaseSupabaseService<DB> {
8585
}
8686

8787
// eslint-disable-next-line @typescript-eslint/no-explicit-any
88-
private applySorting(query: any, sortBy: any) {
88+
applySorting(query: any, sortBy: any) {
8989
for (const [column, direction] of Object.entries(sortBy)) {
9090
if (!column || !direction) continue;
9191
const dir: "asc" | "desc" =

0 commit comments

Comments
 (0)