Skip to content

Commit f4a3d97

Browse files
committed
feat(graphql): add comprehensive support for AllowlistRecord with enhanced querying and resolving
Implemented a complete solution for AllowlistRecord with: - Updated GraphQL schema to support hypercert relations - Refactored AllowlistQueryStrategy to handle complex filtering - Created a new GraphQL resolver with field-level hypercert resolution - Added comprehensive test coverage for AllowlistRecordService and resolver - Migrated resolver to a service folder to cleaner organisation of code - testing all the things
1 parent 0eca3f6 commit f4a3d97

11 files changed

Lines changed: 551 additions & 59 deletions

File tree

src/graphql/schemas/args/allowlistRecordArgs.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
import { ArgsType } from "type-graphql";
22
import { BaseQueryArgs } from "../../../lib/graphql/BaseQueryArgs.js";
33
import { createEntityArgs } from "../../../lib/graphql/createEntityArgs.js";
4+
import { EntityTypeDefs } from "../typeDefs/typeDefs.js";
5+
import { WhereFieldDefinitions } from "../../../lib/graphql/whereFieldDefinitions.js";
46

57
const {
68
WhereInput: AllowlistRecordWhereInput,
79
SortOptions: AllowlistRecordSortOptions,
810
} = createEntityArgs("AllowlistRecord", {
9-
hypercert_id: "string",
10-
token_id: "string",
11-
leaf: "string",
12-
entry: "number",
13-
user_address: "string",
14-
claimed: "boolean",
15-
proof: "stringArray",
16-
units: "bigint",
17-
total_units: "bigint",
18-
root: "string",
11+
...WhereFieldDefinitions.AllowlistRecord.fields,
12+
hypercert: {
13+
type: "id",
14+
references: {
15+
entity: EntityTypeDefs.Hypercert,
16+
fields: WhereFieldDefinitions.Hypercert.fields,
17+
},
18+
},
1919
});
2020

2121
@ArgsType()

src/graphql/schemas/resolvers/allowlistRecordResolver.ts

Lines changed: 0 additions & 24 deletions
This file was deleted.

src/graphql/schemas/resolvers/composed.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { AttestationResolver } from "./attestationResolver.js";
66
import { AttestationSchemaResolver } from "./attestationSchemaResolver.js";
77
import { OrderResolver } from "./orderResolver.js";
88
import { HyperboardResolver } from "./hyperboardResolver.js";
9-
import { AllowlistRecordResolver } from "./allowlistRecordResolver.js";
9+
import { AllowlistRecordResolver } from "../../../services/graphql/resolvers/allowlistRecordResolver.js";
1010
import { SalesResolver } from "./salesResolver.js";
1111
import { UserResolver } from "./userResolver.js";
1212
import { BlueprintResolver } from "./blueprintResolver.js";

src/graphql/schemas/typeDefs/allowlistRecordTypeDefs.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Field, ObjectType } from "type-graphql";
2-
import { EthBigInt } from "../../scalars/ethBigInt.js";
32
import { DataResponse } from "../../../lib/graphql/DataResponse.js";
3+
import { EthBigInt } from "../../scalars/ethBigInt.js";
4+
import { Hypercert } from "./hypercertTypeDefs.js";
5+
46
@ObjectType({
57
description: "Records of allow list entries for claimable fractions",
68
simpleResolvers: true,
@@ -58,6 +60,12 @@ export class AllowlistRecord {
5860
description: "The root of the allow list Merkle tree",
5961
})
6062
root?: string;
63+
64+
@Field(() => Hypercert, {
65+
nullable: true,
66+
description: "The hypercert that the allow list record belongs to",
67+
})
68+
hypercert?: Hypercert;
6169
}
6270

6371
@ObjectType()

src/lib/graphql/whereFieldDefinitions.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@
2121
*/
2222
// TODO: key values can be keyof EntityTypeDefs
2323
export const WhereFieldDefinitions = {
24+
AllowlistRecord: {
25+
fields: {
26+
hypercert_id: "string",
27+
token_id: "bigint",
28+
leaf: "string",
29+
entry: "number",
30+
user_address: "string",
31+
claimed: "boolean",
32+
proof: "stringArray",
33+
units: "bigint",
34+
total_units: "bigint",
35+
root: "string",
36+
},
37+
},
2438
Attestation: {
2539
fields: {
2640
id: "string",

src/services/database/entities/AllowListRecordEntityService.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,39 @@ import {
88
type EntityService,
99
} from "./EntityServiceFactory.js";
1010

11-
export type AllowlistRecordSelect = Selectable<
12-
CachingDatabase["claimable_fractions_with_proofs"]
13-
>;
14-
export type AllowlistRecordInsert = Insertable<
15-
CachingDatabase["claimable_fractions_with_proofs"]
16-
>;
17-
export type AllowlistRecordUpdate = Updateable<
18-
CachingDatabase["claimable_fractions_with_proofs"]
19-
>;
11+
/** The name of the allowlist records table */
12+
type TableName = "claimable_fractions_with_proofs";
13+
/** The type of the allowlist records table */
14+
type Table = CachingDatabase[TableName];
2015

16+
/** Type representing a selectable record from the claimable_fractions_with_proofs table */
17+
export type AllowlistRecordSelect = Selectable<Table>;
18+
19+
/** Type representing an insertable record for the claimable_fractions_with_proofs table */
20+
export type AllowlistRecordInsert = Insertable<Table>;
21+
22+
/** Type representing an updateable record for the claimable_fractions_with_proofs table */
23+
export type AllowlistRecordUpdate = Updateable<Table>;
24+
25+
/**
26+
* Service class for managing allowlist records in the claimable_fractions_with_proofs table.
27+
* This service provides methods to query and retrieve allowlist records using the EntityService pattern.
28+
*
29+
* @injectable
30+
*/
2131
@injectable()
2232
export class AllowlistRecordService {
23-
private entityService: EntityService<
24-
CachingDatabase["claimable_fractions_with_proofs"],
25-
GetAllowlistRecordsArgs
26-
>;
33+
/** The underlying entity service instance for database operations */
34+
private entityService: EntityService<Table, GetAllowlistRecordsArgs>;
2735

36+
/**
37+
* Initializes a new instance of the AllowlistRecordService.
38+
* Creates an EntityService instance for the claimable_fractions_with_proofs table.
39+
*/
2840
constructor() {
2941
this.entityService = createEntityService<
3042
CachingDatabase,
31-
"claimable_fractions_with_proofs",
43+
TableName,
3244
GetAllowlistRecordsArgs
3345
>(
3446
"claimable_fractions_with_proofs",
@@ -37,10 +49,22 @@ export class AllowlistRecordService {
3749
);
3850
}
3951

52+
/**
53+
* Retrieves multiple allowlist records based on the provided arguments.
54+
*
55+
* @param args - Query arguments for filtering allowlist records
56+
* @returns A promise that resolves to an array of allowlist records and a count of total records
57+
*/
4058
async getAllowlistRecords(args: GetAllowlistRecordsArgs) {
4159
return this.entityService.getMany(args);
4260
}
4361

62+
/**
63+
* Retrieves a single allowlist record based on the provided arguments.
64+
*
65+
* @param args - Query arguments for filtering the allowlist record
66+
* @returns A promise that resolves to a single allowlist record or null if not found
67+
*/
4468
async getAllowlistRecord(args: GetAllowlistRecordsArgs) {
4569
return this.entityService.getSingle(args);
4670
}
Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,69 @@
11
import { Kysely } from "kysely";
2+
import { GetAllowlistRecordsArgs } from "../../../graphql/schemas/args/allowlistRecordArgs.js";
3+
import { isWhereEmpty } from "../../../lib/strategies/isWhereEmpty.js";
24
import { CachingDatabase } from "../../../types/kyselySupabaseCaching.js";
35
import { QueryStrategy } from "./QueryStrategy.js";
46

57
/**
6-
* Strategy for querying allowlist records
7-
* Implements queries for the claimable_fractions_with_proofs view table
8+
* Strategy class for querying allowlist records from the claimable_fractions_with_proofs view table.
9+
* This class extends the base QueryStrategy to provide specific implementation for allowlist-related queries.
810
*/
911
export class AllowlistQueryStrategy extends QueryStrategy<
1012
CachingDatabase,
11-
"claimable_fractions_with_proofs"
13+
"claimable_fractions_with_proofs",
14+
GetAllowlistRecordsArgs
1215
> {
16+
/** The name of the table this strategy queries against */
1317
protected readonly tableName = "claimable_fractions_with_proofs" as const;
1418

15-
buildDataQuery(db: Kysely<CachingDatabase>) {
16-
return db.selectFrom(this.tableName).selectAll();
19+
/**
20+
* Builds a query to fetch allowlist records from the database.
21+
*/
22+
buildDataQuery(db: Kysely<CachingDatabase>, args?: GetAllowlistRecordsArgs) {
23+
if (!args) {
24+
return db.selectFrom(this.tableName).selectAll();
25+
}
26+
27+
return db
28+
.selectFrom(this.tableName)
29+
.$if(!isWhereEmpty(args.where?.hypercert), (qb) =>
30+
qb.where(({ exists, selectFrom }) =>
31+
exists(
32+
selectFrom("claims").whereRef(
33+
"claims.hypercert_id",
34+
"=",
35+
"claimable_fractions_with_proofs.hypercert_id",
36+
),
37+
),
38+
),
39+
)
40+
.selectAll(this.tableName);
1741
}
1842

19-
buildCountQuery(db: Kysely<CachingDatabase>) {
20-
return db.selectFrom(this.tableName).select((eb) => {
21-
return eb.fn.countAll().as("count");
22-
});
43+
/**
44+
* Builds a query to count the total number of allowlist records.
45+
*/
46+
buildCountQuery(db: Kysely<CachingDatabase>, args?: GetAllowlistRecordsArgs) {
47+
if (!args) {
48+
return db.selectFrom(this.tableName).select((eb) => {
49+
return eb.fn.countAll().as("count");
50+
});
51+
}
52+
return db
53+
.selectFrom(this.tableName)
54+
.$if(!isWhereEmpty(args.where?.hypercert), (qb) =>
55+
qb.where(({ exists, selectFrom }) =>
56+
exists(
57+
selectFrom("claims").whereRef(
58+
"claims.hypercert_id",
59+
"=",
60+
"claimable_fractions_with_proofs.hypercert_id",
61+
),
62+
),
63+
),
64+
)
65+
.select((eb) => {
66+
return eb.fn.countAll().as("count");
67+
});
2368
}
2469
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { inject, injectable } from "tsyringe";
2+
import { Args, FieldResolver, Query, Resolver, Root } from "type-graphql";
3+
import { AllowlistRecordService } from "../../database/entities/AllowListRecordEntityService.js";
4+
import { HypercertsService } from "../../database/entities/HypercertsEntityService.js";
5+
import { GetAllowlistRecordsArgs } from "../../../graphql/schemas/args/allowlistRecordArgs.js";
6+
import {
7+
AllowlistRecord,
8+
GetAllowlistRecordResponse,
9+
} from "../../../graphql/schemas/typeDefs/allowlistRecordTypeDefs.js";
10+
11+
/**
12+
* GraphQL resolver for AllowlistRecord operations.
13+
* Handles queries for allowlist records and resolves related fields.
14+
*
15+
* This resolver provides:
16+
* - Query for fetching allowlist records with optional filtering
17+
* - Field resolution for the hypercert field, which loads the associated hypercert data
18+
*
19+
* @injectable Marks the class as injectable for dependency injection with tsyringe
20+
* @resolver Marks the class as a GraphQL resolver for the AllowlistRecord type
21+
*/
22+
@injectable()
23+
@Resolver(() => AllowlistRecord)
24+
class AllowlistRecordResolver {
25+
/**
26+
* Creates a new instance of AllowlistRecordResolver.
27+
*
28+
* @param allowlistRecordService - Service for handling allowlist record operations
29+
* @param hypercertsService - Service for handling hypercert operations
30+
*/
31+
constructor(
32+
@inject(AllowlistRecordService)
33+
private allowlistRecordService: AllowlistRecordService,
34+
@inject(HypercertsService)
35+
private hypercertsService: HypercertsService,
36+
) {}
37+
38+
/**
39+
* Queries allowlist records based on provided arguments.
40+
*
41+
* @param args - Query arguments for filtering allowlist records
42+
* @returns A promise that resolves to an object containing:
43+
* - data: Array of allowlist records matching the query
44+
* - count: Total number of records matching the query
45+
*
46+
* @example
47+
* Query:
48+
* ```graphql
49+
* query {
50+
* allowlistRecords(where: { hypercert: { hypercert_id: { eq: "123" } } }) {
51+
* data {
52+
* id
53+
* hypercert_id
54+
* }
55+
* count
56+
* }
57+
* }
58+
* ```
59+
*/
60+
@Query(() => GetAllowlistRecordResponse)
61+
async allowlistRecords(@Args() args: GetAllowlistRecordsArgs) {
62+
return await this.allowlistRecordService.getAllowlistRecords(args);
63+
}
64+
65+
/**
66+
* Resolves the hypercert field for an allowlist record.
67+
* This field resolver is called automatically when the hypercert field is requested in a query.
68+
*
69+
* @param allowlistRecord - The allowlist record for which to resolve the hypercert
70+
* @returns A promise that resolves to the associated hypercert data or null if not found
71+
*
72+
* @example
73+
* Query with hypercert field:
74+
* ```graphql
75+
* query {
76+
* allowlistRecords {
77+
* data {
78+
* id
79+
* hypercert {
80+
* id
81+
* name
82+
* }
83+
* }
84+
* }
85+
* }
86+
* ```
87+
*/
88+
@FieldResolver()
89+
async hypercert(@Root() allowlistRecord: AllowlistRecord) {
90+
return await this.hypercertsService.getHypercert({
91+
where: { hypercert_id: { eq: allowlistRecord.hypercert_id } },
92+
});
93+
}
94+
}
95+
96+
export { AllowlistRecordResolver };

0 commit comments

Comments
 (0)