Skip to content

Commit

Permalink
feat: add collections entity
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pheuberger committed Jan 20, 2025
1 parent 05b1e8d commit 268f190
Show file tree
Hide file tree
Showing 9 changed files with 301 additions and 20 deletions.
24 changes: 24 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ input BooleanSearchOptions {
"""Collection of hypercerts for reference and display purposes"""
type Collection {
admins: [User!]!
blueprints: [Blueprint!]

"""Chain ID of the collection"""
chain_ids: [EthBigInt!]
Expand All @@ -286,12 +287,29 @@ type Collection {

"""Description of the collection"""
description: String!
hypercerts: [Hypercert!]
id: ID

"""Name of the collection"""
name: String!
}

input CollectionFetchInput {
by: CollectionSortOptions
}

input CollectionSortOptions {
created_at: SortOrder
description: SortOrder
name: SortOrder
}

input CollectionWhereInput {
description: StringSearchOptions
id: IdSearchOptions
name: StringSearchOptions
}

"""Pointer to a contract deployed on a chain"""
type Contract {
"""The ID of the chain on which the contract is deployed"""
Expand Down Expand Up @@ -413,6 +431,11 @@ type GetBlueprintResponse {
data: [Blueprint!]
}

type GetCollectionsResponse {
count: Int
data: [Collection!]
}

"""Pointer to a contract deployed on a chain"""
type GetContractsResponse {
count: Int
Expand Down Expand Up @@ -833,6 +856,7 @@ type Query {
attestationSchemas(first: Int, offset: Int): GetAttestationsSchemaResponse!
attestations(first: Int, offset: Int, sort: AttestationFetchInput, where: AttestationWhereInput): GetAttestationsResponse!
blueprints(first: Int, offset: Int, sort: BlueprintFetchInput, where: BlueprintWhereInput): GetBlueprintResponse!
collections(first: Int, offset: Int, sort: CollectionFetchInput, where: CollectionWhereInput): GetCollectionsResponse!
contracts(first: Int, offset: Int, sort: ContractFetchInput, where: ContractWhereInput): GetContractsResponse!
fractions(first: Int, offset: Int, sort: FractionFetchInput, where: FractionWhereInput): GetFractionsResponse!
hyperboards(first: Int, offset: Int, sort: HyperboardFetchInput, where: HyperboardWhereInput): GetHyperboardsResponse!
Expand Down
28 changes: 28 additions & 0 deletions src/graphql/schemas/args/collectionArgs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ArgsType, Field, InputType } from "type-graphql";

import { BasicCollectionWhereInput } from "../inputs/collectionInput.js";
import type { OrderOptions } from "../inputs/orderOptions.js";
import { Collection } from "../typeDefs/collectionTypeDefs.js";
import { CollectionSortOptions } from "../inputs/sortOptions.js";

import { withPagination } from "./baseArgs.js";

@InputType()
export class CollectionWhereInput extends BasicCollectionWhereInput {}

@InputType()
export class CollectionFetchInput implements OrderOptions<Collection> {
@Field(() => CollectionSortOptions, { nullable: true })
by?: CollectionSortOptions;
}

@ArgsType()
export class CollectionArgs {
@Field(() => CollectionWhereInput, { nullable: true })
where?: CollectionWhereInput;
@Field(() => CollectionFetchInput, { nullable: true })
sort?: CollectionFetchInput;
}

@ArgsType()
export class GetCollectionsArgs extends withPagination(CollectionArgs) {}
18 changes: 18 additions & 0 deletions src/graphql/schemas/inputs/collectionInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Field, InputType } from "type-graphql";

import { Collection } from "../typeDefs/collectionTypeDefs.js";

import { IdSearchOptions, StringSearchOptions } from "./searchOptions.js";
import type { WhereOptions } from "./whereOptions.js";

@InputType()
export class BasicCollectionWhereInput implements WhereOptions<Collection> {
@Field(() => IdSearchOptions, { nullable: true })
id?: IdSearchOptions | null;

@Field(() => StringSearchOptions, { nullable: true })
name?: StringSearchOptions;

@Field(() => StringSearchOptions, { nullable: true })
description?: StringSearchOptions;
}
11 changes: 11 additions & 0 deletions src/graphql/schemas/inputs/sortOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Sale } from "../typeDefs/salesTypeDefs.js";
import { Hyperboard } from "../typeDefs/hyperboardTypeDefs.js";
import { Blueprint } from "../typeDefs/blueprintTypeDefs.js";
import { SignatureRequest } from "../typeDefs/signatureRequestTypeDefs.js";
import { Collection } from "../typeDefs/collectionTypeDefs.js";

export type SortOptions<T extends object> = {
[P in keyof T]: SortOrder | null;
Expand Down Expand Up @@ -238,3 +239,13 @@ export class SignatureRequestSortOptions
@Field(() => SortOrder, { nullable: true })
purpose?: SortOrder;
}

@InputType()
export class CollectionSortOptions implements SortOptions<Collection> {
@Field(() => SortOrder, { nullable: true })
name?: SortOrder;
@Field(() => SortOrder, { nullable: true })
created_at?: SortOrder;
@Field(() => SortOrder, { nullable: true })
description?: SortOrder;
}
101 changes: 101 additions & 0 deletions src/graphql/schemas/resolvers/collectionResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {
Args,
FieldResolver,
ObjectType,
Query,
Resolver,
Root,
} from "type-graphql";

import { GetCollectionsArgs } from "../args/collectionArgs.js";
import { Collection } from "../typeDefs/collectionTypeDefs.js";
import { Blueprint } from "../typeDefs/blueprintTypeDefs.js";
import { User } from "../typeDefs/userTypeDefs.js";

import { createBaseResolver, DataResponse } from "./baseTypes.js";
import GetHypercertsResponse from "./hypercertResolver.js";

@ObjectType()
class GetCollectionsResponse extends DataResponse(Collection) {}

const CollectionBaseResolver = createBaseResolver("collection");

@Resolver(() => Collection)
class CollectionResolver extends CollectionBaseResolver {
@Query(() => GetCollectionsResponse)
async collections(@Args() args: GetCollectionsArgs) {
try {
const res = await this.supabaseDataService.getCollections(args);

return {
data: res.data,
count: res.count,
};
} catch (e) {
console.error("[CollectionResolver::collections] Error:", e);
throw new Error(`Error fetching collections: ${(e as Error).message}`);
}
}

@FieldResolver(() => GetHypercertsResponse)
async hypercerts(@Root() collection: Collection) {
if (!collection.id) {
console.error(
"[CollectionResolver::hypercerts] Collection ID is undefined",
);
return [];
}

const hypercerts = await this.supabaseDataService.getCollectionHypercerts(
collection.id,
);

if (!hypercerts?.length) {
return [];
}

const hypercertIds = hypercerts
.map((h) => h.hypercert_id)
.filter((id): id is string => id !== undefined);

if (hypercertIds.length === 0) {
return [];
}

const hypercertsData = await this.getHypercerts({
where: { hypercert_id: { in: hypercertIds } },
});

return hypercertsData.data || [];
}

@FieldResolver(() => [User])
async admins(@Root() collection: Collection) {
if (!collection.id) {
console.error("[CollectionResolver::admins] Collection ID is undefined");
return [];
}

const admins = await this.supabaseDataService.getCollectionAdmins(
collection.id,
);
return admins || [];
}

@FieldResolver(() => [Blueprint])
async blueprints(@Root() collection: Collection) {
if (!collection.id) {
console.error(
"[CollectionResolver::blueprints] Collection ID is undefined",
);
return [];
}

const blueprints = await this.supabaseDataService.getCollectionBlueprints(
collection.id,
);
return blueprints || [];
}
}

export { CollectionResolver };
2 changes: 2 additions & 0 deletions src/graphql/schemas/resolvers/composed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { SalesResolver } from "./salesResolver.js";
import { UserResolver } from "./userResolver.js";
import { BlueprintResolver } from "./blueprintResolver.js";
import { SignatureRequestResolver } from "./signatureRequestResolver.js";
import { CollectionResolver } from "./collectionResolver.js";

export const resolvers = [
ContractResolver,
Expand All @@ -26,4 +27,5 @@ export const resolvers = [
UserResolver,
BlueprintResolver,
SignatureRequestResolver,
CollectionResolver,
] as const;
34 changes: 34 additions & 0 deletions src/graphql/schemas/typeDefs/collectionTypeDefs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Field, ObjectType } from "type-graphql";

import { EthBigInt } from "../../scalars/ethBigInt.js";

import { BasicTypeDef } from "./baseTypes/basicTypeDef.js";
import { User } from "./userTypeDefs.js";
import { Hypercert } from "./hypercertTypeDefs.js";
import { Blueprint } from "./blueprintTypeDefs.js";

@ObjectType({
description: "Collection of hypercerts for reference and display purposes",
})
export class Collection extends BasicTypeDef {
@Field({ description: "Creation timestamp of the collection" })
created_at?: string;
@Field({ description: "Name of the collection" })
name?: string;
@Field({ description: "Description of the collection" })
description?: string;
@Field(() => [EthBigInt], {
nullable: true,
description: "Chain ID of the collection",
})
chain_ids?: (bigint | number | string)[];

@Field(() => [User])
admins?: User[];

@Field(() => [Hypercert], { nullable: true })
hypercerts?: Hypercert[];

@Field(() => [Blueprint], { nullable: true })
blueprints?: Blueprint[];
}
21 changes: 1 addition & 20 deletions src/graphql/schemas/typeDefs/hyperboardTypeDefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BasicTypeDef } from "./baseTypes/basicTypeDef.js";
import { EthBigInt } from "../../scalars/ethBigInt.js";
import { User } from "./userTypeDefs.js";
import { GraphQLBigInt } from "graphql-scalars";
import { Collection } from "./collectionTypeDefs.js";

@ObjectType({
description: "Hyperboard of hypercerts for reference and display purposes",
Expand Down Expand Up @@ -48,26 +49,6 @@ class SectionResponseType {
count?: number;
}

@ObjectType({
description: "Collection of hypercerts for reference and display purposes",
})
class Collection extends BasicTypeDef {
@Field({ description: "Creation timestamp of the collection" })
created_at?: string;
@Field({ description: "Name of the collection" })
name?: string;
@Field({ description: "Description of the collection" })
description?: string;
@Field(() => [EthBigInt], {
nullable: true,
description: "Chain ID of the collection",
})
chain_ids?: (bigint | number | string)[];

@Field(() => [User])
admins?: User[];
}

@ObjectType({
description: "Section representing a collection within a hyperboard",
})
Expand Down
Loading

0 comments on commit 268f190

Please sign in to comment.