Skip to content

Commit c06ab1f

Browse files
committed
feat: filter member challenges by resource role
1 parent f601172 commit c06ab1f

13 files changed

Lines changed: 371 additions & 52 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,17 @@ Refer to the verification document `Verification.md`
341341
bypass. Any update that starts in or transitions to a completed or cancelled status cannot change
342342
the effective `is_test_challenge` value; omitting metadata preserves it. Normal authorization
343343
checks still apply.
344+
- Role-specific member competition searches use
345+
`GET /v6/challenges?memberId={memberId}&resourceRoleId={resourceRoleUuid}`.
346+
`resourceRoleId` is an exact UUID and requires `memberId`. The resource role is
347+
applied before all challenge filters, global sorting, totals, and pagination;
348+
omitting it preserves the existing any-resource behavior of `memberId`. This
349+
public query only narrows results and never grants access: anonymous callers
350+
retain anonymous visibility, and authenticated callers remain subject to
351+
whitelist, group, and task rules based on the caller. For “My competitions,”
352+
pass the configured Submitter resource-role UUID. Deploy migration
353+
`20260813130000_add_role_to_member_access_view` before this service version,
354+
because the generated Prisma client expects the view's new `roleId` column.
344355
- API base configuration points to v6 in dev/local and v5 in prod (for compatibility):
345356
- Dev: `work-manager/config/constants/development.js`.
346357
- Local: `work-manager/config/constants/local.js`.

docs/swagger.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ paths:
5353
- Challenges
5454
description: |
5555
Retrieve challenges in the system.
56+
57+
`resourceRoleId` may be combined with `memberId` to restrict the member's
58+
resource-associated challenges to one UUID resource role before challenge
59+
filtering, sorting, totals, and pagination. It does not grant access: results
60+
remain subject to the caller's whitelist, group, and task visibility. Anonymous
61+
callers receive only challenges visible to anonymous users.
5662
security:
5763
- bearer: []
5864
produces:
@@ -354,6 +360,12 @@ paths:
354360
description: Filter challenges memberId has access to
355361
required: false
356362
type: string
363+
- name: resourceRoleId
364+
in: query
365+
description: Filter the member resource association by exact resource-role UUID. Requires memberId. This only narrows results and does not grant challenge access.
366+
required: false
367+
type: string
368+
format: UUID
357369
- name: sortBy
358370
in: query
359371
description: Sort the results by the field.

packages/challenge-prisma-client/generated/edge.js

Lines changed: 8 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/challenge-prisma-client/generated/index-browser.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,8 @@ exports.Prisma.TimelineTemplatePhaseScalarFieldEnum = {
490490

491491
exports.Prisma.MemberChallengeAccessScalarFieldEnum = {
492492
challengeId: 'challengeId',
493-
memberId: 'memberId'
493+
memberId: 'memberId',
494+
roleId: 'roleId'
494495
};
495496

496497
exports.Prisma.SortOrder = {
@@ -756,7 +757,8 @@ exports.Prisma.TimelineTemplatePhaseOrderByRelevanceFieldEnum = {
756757

757758
exports.Prisma.MemberChallengeAccessOrderByRelevanceFieldEnum = {
758759
challengeId: 'challengeId',
759-
memberId: 'memberId'
760+
memberId: 'memberId',
761+
roleId: 'roleId'
760762
};
761763
exports.ChallengeStatusEnum = exports.$Enums.ChallengeStatusEnum = {
762764
NEW: 'NEW',

packages/challenge-prisma-client/generated/index.d.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35004,33 +35004,39 @@ export namespace Prisma {
3500435004
export type MemberChallengeAccessMinAggregateOutputType = {
3500535005
challengeId: string | null
3500635006
memberId: string | null
35007+
roleId: string | null
3500735008
}
3500835009

3500935010
export type MemberChallengeAccessMaxAggregateOutputType = {
3501035011
challengeId: string | null
3501135012
memberId: string | null
35013+
roleId: string | null
3501235014
}
3501335015

3501435016
export type MemberChallengeAccessCountAggregateOutputType = {
3501535017
challengeId: number
3501635018
memberId: number
35019+
roleId: number
3501735020
_all: number
3501835021
}
3501935022

3502035023

3502135024
export type MemberChallengeAccessMinAggregateInputType = {
3502235025
challengeId?: true
3502335026
memberId?: true
35027+
roleId?: true
3502435028
}
3502535029

3502635030
export type MemberChallengeAccessMaxAggregateInputType = {
3502735031
challengeId?: true
3502835032
memberId?: true
35033+
roleId?: true
3502935034
}
3503035035

3503135036
export type MemberChallengeAccessCountAggregateInputType = {
3503235037
challengeId?: true
3503335038
memberId?: true
35039+
roleId?: true
3503435040
_all?: true
3503535041
}
3503635042

@@ -35103,6 +35109,7 @@ export namespace Prisma {
3510335109
export type MemberChallengeAccessGroupByOutputType = {
3510435110
challengeId: string
3510535111
memberId: string
35112+
roleId: string
3510635113
_count: MemberChallengeAccessCountAggregateOutputType | null
3510735114
_min: MemberChallengeAccessMinAggregateOutputType | null
3510835115
_max: MemberChallengeAccessMaxAggregateOutputType | null
@@ -35125,6 +35132,7 @@ export namespace Prisma {
3512535132
export type MemberChallengeAccessSelect<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = $Extensions.GetSelect<{
3512635133
challengeId?: boolean
3512735134
memberId?: boolean
35135+
roleId?: boolean
3512835136
challenge?: boolean | ChallengeDefaultArgs<ExtArgs>
3512935137
}, ExtArgs["result"]["memberChallengeAccess"]>
3513035138

@@ -35133,9 +35141,10 @@ export namespace Prisma {
3513335141
export type MemberChallengeAccessSelectScalar = {
3513435142
challengeId?: boolean
3513535143
memberId?: boolean
35144+
roleId?: boolean
3513635145
}
3513735146

35138-
export type MemberChallengeAccessOmit<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = $Extensions.GetOmit<"challengeId" | "memberId", ExtArgs["result"]["memberChallengeAccess"]>
35147+
export type MemberChallengeAccessOmit<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = $Extensions.GetOmit<"challengeId" | "memberId" | "roleId", ExtArgs["result"]["memberChallengeAccess"]>
3513935148
export type MemberChallengeAccessInclude<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = {
3514035149
challenge?: boolean | ChallengeDefaultArgs<ExtArgs>
3514135150
}
@@ -35148,6 +35157,7 @@ export namespace Prisma {
3514835157
scalars: $Extensions.GetPayloadResult<{
3514935158
challengeId: string
3515035159
memberId: string
35160+
roleId: string
3515135161
}, ExtArgs["result"]["memberChallengeAccess"]>
3515235162
composites: {}
3515335163
}
@@ -35394,6 +35404,7 @@ export namespace Prisma {
3539435404
interface MemberChallengeAccessFieldRefs {
3539535405
readonly challengeId: FieldRef<"MemberChallengeAccess", 'String'>
3539635406
readonly memberId: FieldRef<"MemberChallengeAccess", 'String'>
35407+
readonly roleId: FieldRef<"MemberChallengeAccess", 'String'>
3539735408
}
3539835409

3539935410

@@ -36017,7 +36028,8 @@ export namespace Prisma {
3601736028

3601836029
export const MemberChallengeAccessScalarFieldEnum: {
3601936030
challengeId: 'challengeId',
36020-
memberId: 'memberId'
36031+
memberId: 'memberId',
36032+
roleId: 'roleId'
3602136033
};
3602236034

3602336035
export type MemberChallengeAccessScalarFieldEnum = (typeof MemberChallengeAccessScalarFieldEnum)[keyof typeof MemberChallengeAccessScalarFieldEnum]
@@ -36373,7 +36385,8 @@ export namespace Prisma {
3637336385

3637436386
export const MemberChallengeAccessOrderByRelevanceFieldEnum: {
3637536387
challengeId: 'challengeId',
36376-
memberId: 'memberId'
36388+
memberId: 'memberId',
36389+
roleId: 'roleId'
3637736390
};
3637836391

3637936392
export type MemberChallengeAccessOrderByRelevanceFieldEnum = (typeof MemberChallengeAccessOrderByRelevanceFieldEnum)[keyof typeof MemberChallengeAccessOrderByRelevanceFieldEnum]
@@ -38952,19 +38965,22 @@ export namespace Prisma {
3895238965
NOT?: MemberChallengeAccessWhereInput | MemberChallengeAccessWhereInput[]
3895338966
challengeId?: StringFilter<"MemberChallengeAccess"> | string
3895438967
memberId?: StringFilter<"MemberChallengeAccess"> | string
38968+
roleId?: StringFilter<"MemberChallengeAccess"> | string
3895538969
challenge?: XOR<ChallengeScalarRelationFilter, ChallengeWhereInput>
3895638970
}
3895738971

3895838972
export type MemberChallengeAccessOrderByWithRelationInput = {
3895938973
challengeId?: SortOrder
3896038974
memberId?: SortOrder
38975+
roleId?: SortOrder
3896138976
challenge?: ChallengeOrderByWithRelationInput
3896238977
_relevance?: MemberChallengeAccessOrderByRelevanceInput
3896338978
}
3896438979

3896538980
export type MemberChallengeAccessOrderByWithAggregationInput = {
3896638981
challengeId?: SortOrder
3896738982
memberId?: SortOrder
38983+
roleId?: SortOrder
3896838984
_count?: MemberChallengeAccessCountOrderByAggregateInput
3896938985
_max?: MemberChallengeAccessMaxOrderByAggregateInput
3897038986
_min?: MemberChallengeAccessMinOrderByAggregateInput
@@ -38976,6 +38992,7 @@ export namespace Prisma {
3897638992
NOT?: MemberChallengeAccessScalarWhereWithAggregatesInput | MemberChallengeAccessScalarWhereWithAggregatesInput[]
3897738993
challengeId?: StringWithAggregatesFilter<"MemberChallengeAccess"> | string
3897838994
memberId?: StringWithAggregatesFilter<"MemberChallengeAccess"> | string
38995+
roleId?: StringWithAggregatesFilter<"MemberChallengeAccess"> | string
3897938996
}
3898038997

3898138998
export type ChallengeCreateInput = {
@@ -43727,16 +43744,19 @@ export namespace Prisma {
4372743744
export type MemberChallengeAccessCountOrderByAggregateInput = {
4372843745
challengeId?: SortOrder
4372943746
memberId?: SortOrder
43747+
roleId?: SortOrder
4373043748
}
4373143749

4373243750
export type MemberChallengeAccessMaxOrderByAggregateInput = {
4373343751
challengeId?: SortOrder
4373443752
memberId?: SortOrder
43753+
roleId?: SortOrder
4373543754
}
4373643755

4373743756
export type MemberChallengeAccessMinOrderByAggregateInput = {
4373843757
challengeId?: SortOrder
4373943758
memberId?: SortOrder
43759+
roleId?: SortOrder
4374043760
}
4374143761

4374243762
export type ChallengeCreatecurrentPhaseNamesInput = {
@@ -46459,6 +46479,7 @@ export namespace Prisma {
4645946479
NOT?: MemberChallengeAccessWhereInput | MemberChallengeAccessWhereInput[]
4646046480
challengeId?: StringFilter<"MemberChallengeAccess"> | string
4646146481
memberId?: StringFilter<"MemberChallengeAccess"> | string
46482+
roleId?: StringFilter<"MemberChallengeAccess"> | string
4646246483
challenge?: XOR<ChallengeScalarRelationFilter, ChallengeWhereInput>
4646346484
}
4646446485

@@ -47079,6 +47100,7 @@ export namespace Prisma {
4707947100
NOT?: MemberChallengeAccessScalarWhereInput | MemberChallengeAccessScalarWhereInput[]
4708047101
challengeId?: StringFilter<"MemberChallengeAccess"> | string
4708147102
memberId?: StringFilter<"MemberChallengeAccess"> | string
47103+
roleId?: StringFilter<"MemberChallengeAccess"> | string
4708247104
}
4708347105

4708447106
export type ChallengeUserWhitelistUpsertWithWhereUniqueWithoutChallengeInput = {
@@ -53589,18 +53611,22 @@ export namespace Prisma {
5358953611

5359053612
export type MemberChallengeAccessUpdateWithoutChallengeInput = {
5359153613
memberId?: StringFieldUpdateOperationsInput | string
53614+
roleId?: StringFieldUpdateOperationsInput | string
5359253615
}
5359353616

5359453617
export type MemberChallengeAccessUncheckedUpdateWithoutChallengeInput = {
5359553618
memberId?: StringFieldUpdateOperationsInput | string
53619+
roleId?: StringFieldUpdateOperationsInput | string
5359653620
}
5359753621

5359853622
export type MemberChallengeAccessUpdateManyMutationInput = {
5359953623
memberId?: StringFieldUpdateOperationsInput | string
53624+
roleId?: StringFieldUpdateOperationsInput | string
5360053625
}
5360153626

5360253627
export type MemberChallengeAccessUncheckedUpdateManyWithoutChallengeInput = {
5360353628
memberId?: StringFieldUpdateOperationsInput | string
53629+
roleId?: StringFieldUpdateOperationsInput | string
5360453630
}
5360553631

5360653632
export type ChallengeUserWhitelistUpdateWithoutChallengeInput = {

packages/challenge-prisma-client/generated/index.js

Lines changed: 8 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/challenge-prisma-client/generated/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "prisma-client-020e5b8aa8d9fb593e6af6a930afe7395f722f2688f5daee44b26029e9b8b643",
2+
"name": "prisma-client-8f4b20580a6790c6032449023d392c74c8300c799da19bf2adf24a32fa5f9abf",
33
"main": "index.js",
44
"types": "index.d.ts",
55
"browser": "default.js",

packages/challenge-prisma-client/generated/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ model ChallengeUserWhitelist {
202202
view MemberChallengeAccess {
203203
challengeId String
204204
memberId String
205+
roleId String
205206
206207
challenge Challenge @relation(fields: [challengeId], references: [id])
207208
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
-- Expose the resource role so member-scoped challenge searches can narrow the
2+
-- candidate challenge set before applying challenge filters and pagination.
3+
DROP VIEW IF EXISTS "challenges"."MemberChallengeAccess";
4+
5+
DO $$
6+
BEGIN
7+
IF EXISTS (
8+
SELECT 1
9+
FROM information_schema.tables
10+
WHERE table_schema = 'resources'
11+
AND table_name = 'Resource'
12+
) THEN
13+
EXECUTE format(
14+
'CREATE VIEW %I.%I AS
15+
SELECT DISTINCT r."challengeId", r."memberId", r."roleId"
16+
FROM resources."Resource" r
17+
WHERE r."challengeId" IS NOT NULL
18+
AND r."memberId" IS NOT NULL
19+
AND r."roleId" IS NOT NULL',
20+
current_schema(), 'MemberChallengeAccess'
21+
);
22+
ELSE
23+
EXECUTE format(
24+
'CREATE VIEW %I.%I AS
25+
SELECT CAST(NULL AS TEXT) AS "challengeId",
26+
CAST(NULL AS TEXT) AS "memberId",
27+
CAST(NULL AS TEXT) AS "roleId"
28+
WHERE FALSE',
29+
current_schema(), 'MemberChallengeAccess'
30+
);
31+
END IF;
32+
END;
33+
$$;

prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ model ChallengeUserWhitelist {
202202
view MemberChallengeAccess {
203203
challengeId String
204204
memberId String
205+
roleId String
205206
206207
challenge Challenge @relation(fields: [challengeId], references: [id])
207208

0 commit comments

Comments
 (0)