Skip to content

Commit 6671885

Browse files
mohywarecoderabbitai[bot]palisadoes
authored
Added getPledgesByUserId query and related types (PledgeWhereInput, PledgeOrderByInput) (#3436)
* feat: add getPledgesByUserId query with filtering and sorting options * Update test/graphql/types/documentNodes.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Refactor types to QueryPledgeOrderByInput and QueryPledgeWhereInput. * apply fix_code_quality * refactors * fix: update userId argument schema * feat: enhance getPledgesByUserId query with pagination and add more tests --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Peter Harrison <[email protected]>
1 parent 61a89c9 commit 6671885

File tree

9 files changed

+1672
-307
lines changed

9 files changed

+1672
-307
lines changed

schema.graphql

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,11 @@ type GetUrlResponse {
773773
presignedUrl: String
774774
}
775775

776+
type HasUserVoted {
777+
"""Type of the post vote"""
778+
type: PostVoteType!
779+
}
780+
776781
"""
777782
Possible variants of the two-letter language code defined in ISO 639-1, part of the ISO 639 standard published by the International Organization for Standardization (ISO), to represent natural languages.
778783
"""
@@ -3171,6 +3176,25 @@ A field whose value conforms to the standard E.164 format as specified in: https
31713176
"""
31723177
scalar PhoneNumber
31733178

3179+
"""
3180+
Sorting criteria, e.g., 'amount_ASC', 'amount_DESC', 'endDate_ASC', 'endDate_DESC'
3181+
"""
3182+
enum QueryPledgeOrderByInput {
3183+
amount_ASC
3184+
amount_DESC
3185+
endDate_ASC
3186+
endDate_DESC
3187+
}
3188+
3189+
"""Filter criteria for Pledges"""
3190+
input QueryPledgeWhereInput {
3191+
"""Filter pledges by the name of the creator"""
3192+
firstName_contains: String
3193+
3194+
"""Filter pledges by the name of the campaign"""
3195+
name_contains: String
3196+
}
3197+
31743198
type Post {
31753199
"""Array of attachments."""
31763200
attachments: [PostAttachment!]
@@ -3359,8 +3383,22 @@ type Query {
33593383
"""Query field to read a fund campaign pledge."""
33603384
fundCampaignPledge(input: QueryFundCampaignPledgeInput!): FundCampaignPledge
33613385

3386+
"""Query field to get fund campaign pledge associated to a user."""
3387+
getPledgesByUserId(
3388+
"""
3389+
Sorting criteria, e.g., 'amount_ASC', 'amount_DESC', 'endDate_ASC', 'endDate_DESC'
3390+
"""
3391+
orderBy: QueryPledgeOrderByInput
3392+
3393+
"""Global id of the user."""
3394+
userId: QueryUserInput!
3395+
3396+
"""Filter criteria for pledges"""
3397+
where: QueryPledgeWhereInput
3398+
): [FundCampaignPledge!]
3399+
33623400
"""Query field to read a post vote."""
3363-
hasUserVoted(input: QueryHasUserVotedInput!): hasUserVoted
3401+
hasUserVoted(input: QueryHasUserVotedInput!): HasUserVoted
33643402

33653403
"""Query field to read an organization."""
33663404
organization(input: QueryOrganizationInput!): Organization
@@ -3894,9 +3932,4 @@ type VenueEventsConnection {
38943932
type VenueEventsConnectionEdge {
38953933
cursor: String!
38963934
node: Event
3897-
}
3898-
3899-
type hasUserVoted {
3900-
"""Type of the post vote"""
3901-
type: PostVoteType!
39023935
}

src/graphql/inputs/QueryFundCampaignPledgeInput.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,33 @@ export const QueryFundCampaignPledgeInput = builder
1919
}),
2020
}),
2121
});
22+
23+
export const QueryPledgeWhereInput = builder
24+
.inputRef("QueryPledgeWhereInput")
25+
.implement({
26+
description: "Filter criteria for Pledges",
27+
fields: (t) => ({
28+
firstName_contains: t.string({
29+
description: "Filter pledges by the name of the creator",
30+
required: false,
31+
}),
32+
name_contains: t.string({
33+
description: "Filter pledges by the name of the campaign",
34+
required: false,
35+
}),
36+
}),
37+
});
38+
39+
export const QueryPledgeOrderByInput = builder.enumType(
40+
"QueryPledgeOrderByInput",
41+
{
42+
values: [
43+
"amount_ASC",
44+
"amount_DESC",
45+
"endDate_ASC",
46+
"endDate_DESC",
47+
] as const,
48+
description:
49+
"Sorting criteria, e.g., 'amount_ASC', 'amount_DESC', 'endDate_ASC', 'endDate_DESC'",
50+
},
51+
);
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import { type SQL, and, asc, desc, eq, exists, ilike, or } from "drizzle-orm";
2+
import { z } from "zod";
3+
import { fundCampaignPledgesTable } from "~/src/drizzle/tables/fundCampaignPledges";
4+
import { fundCampaignsTable } from "~/src/drizzle/tables/fundCampaigns";
5+
import { usersTable } from "~/src/drizzle/tables/users";
6+
import { builder } from "~/src/graphql/builder";
7+
import {
8+
QueryPledgeOrderByInput,
9+
QueryPledgeWhereInput,
10+
} from "~/src/graphql/inputs/QueryFundCampaignPledgeInput";
11+
import {
12+
QueryUserInput,
13+
queryUserInputSchema,
14+
} from "~/src/graphql/inputs/QueryUserInput";
15+
import { FundCampaignPledge } from "~/src/graphql/types/FundCampaignPledge/FundCampaignPledge";
16+
import { TalawaGraphQLError } from "~/src/utilities/TalawaGraphQLError";
17+
import type { ParsedDefaultGraphQLConnectionArgumentsWithWhere } from "~/src/utilities/defaultGraphQLConnection";
18+
19+
const queryFundCampaignPledgeArgumentsSchema = z.object({
20+
userId: queryUserInputSchema,
21+
});
22+
23+
builder.queryField("getPledgesByUserId", (t) =>
24+
t.field({
25+
args: {
26+
userId: t.arg({
27+
description: "Global id of the user.",
28+
required: true,
29+
type: QueryUserInput,
30+
}),
31+
where: t.arg({
32+
description: "Filter criteria for pledges",
33+
required: false,
34+
type: QueryPledgeWhereInput,
35+
}),
36+
orderBy: t.arg({
37+
description:
38+
"Sorting criteria, e.g., 'amount_ASC', 'amount_DESC', 'endDate_ASC', 'endDate_DESC'",
39+
required: false,
40+
type: QueryPledgeOrderByInput,
41+
}),
42+
limit: t.arg({
43+
description: "Maximum number of results to return.",
44+
required: false,
45+
type: "Int",
46+
}),
47+
offset: t.arg({
48+
description: "Number of results to skip.",
49+
required: false,
50+
type: "Int",
51+
}),
52+
},
53+
description:
54+
"Query field to get fund campaign pledge associated to a user.",
55+
resolve: async (_parent, args, ctx) => {
56+
if (!ctx.currentClient.isAuthenticated) {
57+
throw new TalawaGraphQLError({
58+
extensions: {
59+
code: "unauthenticated",
60+
},
61+
});
62+
}
63+
64+
const {
65+
data: parsedArgs,
66+
error,
67+
success,
68+
} = queryFundCampaignPledgeArgumentsSchema.safeParse(args);
69+
if (!success) {
70+
throw new TalawaGraphQLError({
71+
extensions: {
72+
code: "invalid_arguments",
73+
issues: error.issues.map((issue) => ({
74+
argumentPath: issue.path,
75+
message: issue.message,
76+
})),
77+
},
78+
});
79+
}
80+
81+
const UserId = parsedArgs.userId.id;
82+
const currentUserId = ctx.currentClient.user.id;
83+
84+
const currentUser = await ctx.drizzleClient.query.usersTable.findFirst({
85+
columns: {
86+
role: true,
87+
},
88+
where: (fields, operators) => operators.eq(fields.id, currentUserId),
89+
});
90+
91+
if (currentUser === undefined) {
92+
throw new TalawaGraphQLError({
93+
extensions: {
94+
code: "unauthenticated",
95+
},
96+
});
97+
}
98+
99+
// Order By
100+
const sortOrder = args.orderBy as
101+
| "amount_ASC"
102+
| "amount_DESC"
103+
| "endDate_ASC"
104+
| "endDate_DESC"
105+
| undefined;
106+
107+
let orderBy: SQL<unknown>[] = [];
108+
109+
// As FundCampaignPledge type does not contain campaign
110+
interface ExtendedFundCampaignPledge extends FundCampaignPledge {
111+
campaign: {
112+
endAt: Date;
113+
};
114+
}
115+
116+
let sortInTs:
117+
| ((
118+
a: ExtendedFundCampaignPledge,
119+
b: ExtendedFundCampaignPledge,
120+
) => number)
121+
| null = null;
122+
123+
switch (sortOrder) {
124+
case "amount_ASC":
125+
orderBy = [asc(fundCampaignPledgesTable.amount)];
126+
break;
127+
case "amount_DESC":
128+
orderBy = [desc(fundCampaignPledgesTable.amount)];
129+
break;
130+
case "endDate_ASC":
131+
sortInTs = (a, b) =>
132+
new Date(a.campaign.endAt).getTime() -
133+
new Date(b.campaign.endAt).getTime();
134+
break;
135+
case "endDate_DESC":
136+
sortInTs = (a, b) =>
137+
new Date(b.campaign.endAt).getTime() -
138+
new Date(a.campaign.endAt).getTime();
139+
break;
140+
default:
141+
orderBy = [];
142+
}
143+
144+
// Where Clause
145+
const { where } =
146+
args as unknown as ParsedDefaultGraphQLConnectionArgumentsWithWhere<
147+
{ createdAt: Date; id: string },
148+
{
149+
name_contains?: string;
150+
firstName_contains?: string;
151+
}
152+
>;
153+
154+
// Query
155+
const existingFundCampaignPledge =
156+
await ctx.drizzleClient.query.fundCampaignPledgesTable.findMany({
157+
with: {
158+
pledger: {
159+
columns: {
160+
id: true,
161+
name: true,
162+
avatarName: true,
163+
},
164+
},
165+
campaign: {
166+
columns: {
167+
name: true,
168+
startAt: true,
169+
endAt: true,
170+
currencyCode: true,
171+
},
172+
with: {
173+
fund: {
174+
with: {
175+
organization: {
176+
columns: {
177+
countryCode: true,
178+
},
179+
180+
with: {
181+
membershipsWhereOrganization: {
182+
columns: {
183+
role: true,
184+
},
185+
where: (fields, operators) =>
186+
operators.eq(fields.memberId, UserId),
187+
},
188+
},
189+
},
190+
},
191+
},
192+
},
193+
},
194+
},
195+
where: (pledges, { and: andOp }) => {
196+
const conditions = [eq(pledges.pledgerId, UserId)];
197+
if (
198+
where?.name_contains !== undefined ||
199+
where?.firstName_contains !== undefined
200+
) {
201+
conditions.push(
202+
exists(
203+
ctx.drizzleClient
204+
.select()
205+
.from(fundCampaignsTable)
206+
.leftJoin(usersTable, eq(usersTable.id, pledges.pledgerId))
207+
.where(
208+
and(
209+
eq(fundCampaignsTable.id, pledges.campaignId),
210+
or(
211+
where?.name_contains !== undefined
212+
? ilike(
213+
fundCampaignsTable.name,
214+
`%${where.name_contains}%`,
215+
)
216+
: undefined,
217+
where?.firstName_contains !== undefined
218+
? ilike(
219+
usersTable.name,
220+
`%${where.firstName_contains}%`,
221+
)
222+
: undefined,
223+
),
224+
),
225+
),
226+
),
227+
);
228+
}
229+
return andOp(...conditions);
230+
},
231+
orderBy: orderBy,
232+
limit: args.limit ?? undefined,
233+
offset: args.offset ?? undefined,
234+
});
235+
236+
// Perform in-memory sorting as nested sort still not supported in drizzle see https://github.com/drizzle-team/drizzle-orm/issues/2297 and https://www.answeroverflow.com/m/1240834016140066896
237+
if (sortInTs) {
238+
existingFundCampaignPledge.sort(sortInTs);
239+
}
240+
241+
if (!existingFundCampaignPledge.length) {
242+
throw new TalawaGraphQLError({
243+
extensions: {
244+
code: "arguments_associated_resources_not_found",
245+
issues: [
246+
{
247+
argumentPath: ["userId", "id"],
248+
},
249+
],
250+
},
251+
});
252+
}
253+
254+
const firstPledge = existingFundCampaignPledge[0];
255+
const currentUserOrganizationMembership =
256+
firstPledge?.campaign.fund.organization
257+
.membershipsWhereOrganization?.[0];
258+
259+
if (
260+
currentUser.role !== "administrator" &&
261+
(currentUserOrganizationMembership === undefined ||
262+
(currentUserOrganizationMembership.role !== "administrator" &&
263+
currentUserId !== firstPledge?.pledgerId))
264+
) {
265+
throw new TalawaGraphQLError({
266+
extensions: {
267+
code: "unauthorized_action_on_arguments_associated_resources",
268+
issues: [
269+
{
270+
argumentPath: ["userId", "id"],
271+
},
272+
],
273+
},
274+
});
275+
}
276+
277+
return existingFundCampaignPledge;
278+
},
279+
type: [FundCampaignPledge],
280+
}),
281+
);

src/graphql/types/Query/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import "./event";
1111
import "./fund";
1212
import "./fundCampaign";
1313
import "./fundCampaignPledge";
14+
import "./getPledgesByUserId";
1415
import "./organization";
1516
import "./post";
1617
import "./renewAuthenticationToken";

0 commit comments

Comments
 (0)