@dappermountain/payload-plugin-achievements 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -6
- package/dist/admin/ReconcileProgressionButton.d.ts +11 -0
- package/dist/admin/ReconcileProgressionButton.d.ts.map +1 -0
- package/dist/admin/ReconcileProgressionButton.js +41 -0
- package/dist/catalog.d.ts +3 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/reconcile.d.ts +18 -0
- package/dist/cli/reconcile.d.ts.map +1 -0
- package/dist/cli/reconcile.js +90 -0
- package/dist/collections/AchievementRequests/hooks/prepareAchievementRequest.d.ts.map +1 -1
- package/dist/collections/AchievementRequests/hooks/prepareAchievementRequest.js +35 -15
- package/dist/collections/Achievements/hooks/afterRequiresReviewDisabled.d.ts +7 -0
- package/dist/collections/Achievements/hooks/afterRequiresReviewDisabled.d.ts.map +1 -0
- package/dist/collections/Achievements/hooks/afterRequiresReviewDisabled.js +15 -0
- package/dist/collections/Achievements/hooks/index.d.ts.map +1 -1
- package/dist/collections/Achievements/hooks/index.js +2 -0
- package/dist/collections/Achievements/index.d.ts.map +1 -1
- package/dist/collections/Achievements/index.js +3 -0
- package/dist/collections/Grants/hooks/afterGrantMutation.d.ts +4 -0
- package/dist/collections/Grants/hooks/afterGrantMutation.d.ts.map +1 -0
- package/dist/collections/Grants/hooks/afterGrantMutation.js +56 -0
- package/dist/collections/Grants/hooks/index.d.ts.map +1 -1
- package/dist/collections/Grants/hooks/index.js +3 -0
- package/dist/collections/Grants/index.d.ts.map +1 -1
- package/dist/collections/Grants/index.js +18 -1
- package/dist/collections/Logs/index.js +1 -1
- package/dist/collections/TierRequests/access/index.d.ts +3 -0
- package/dist/collections/TierRequests/access/index.d.ts.map +1 -0
- package/dist/collections/TierRequests/access/index.js +8 -0
- package/dist/collections/TierRequests/hooks/index.d.ts +3 -0
- package/dist/collections/TierRequests/hooks/index.d.ts.map +1 -0
- package/dist/collections/TierRequests/hooks/index.js +5 -0
- package/dist/collections/TierRequests/hooks/prepareTierRequest.d.ts +8 -0
- package/dist/collections/TierRequests/hooks/prepareTierRequest.d.ts.map +1 -0
- package/dist/collections/TierRequests/hooks/prepareTierRequest.js +67 -0
- package/dist/collections/TierRequests/index.d.ts +3 -0
- package/dist/collections/TierRequests/index.d.ts.map +1 -0
- package/dist/collections/TierRequests/index.js +47 -0
- package/dist/collections/Tiers/hooks/afterRequiresReviewDisabled.d.ts +7 -0
- package/dist/collections/Tiers/hooks/afterRequiresReviewDisabled.d.ts.map +1 -0
- package/dist/collections/Tiers/hooks/afterRequiresReviewDisabled.js +15 -0
- package/dist/collections/Tiers/hooks/index.d.ts.map +1 -1
- package/dist/collections/Tiers/hooks/index.js +2 -0
- package/dist/collections/Tiers/index.d.ts.map +1 -1
- package/dist/collections/Tiers/index.js +11 -3
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/collections/index.js +2 -0
- package/dist/collections/slugs.d.ts.map +1 -1
- package/dist/collections/slugs.js +1 -0
- package/dist/defaults.d.ts +3 -0
- package/dist/defaults.d.ts.map +1 -1
- package/dist/defaults.js +12 -3
- package/dist/endpoints/index.d.ts.map +1 -1
- package/dist/endpoints/index.js +9 -3
- package/dist/endpoints/reconcile.d.ts +8 -0
- package/dist/endpoints/reconcile.d.ts.map +1 -0
- package/dist/endpoints/reconcile.js +73 -0
- package/dist/exports/client.d.ts +1 -0
- package/dist/exports/client.d.ts.map +1 -1
- package/dist/exports/client.js +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/seed/index.d.ts +2 -0
- package/dist/seed/index.d.ts.map +1 -1
- package/dist/seed/index.js +1 -0
- package/dist/services/achievement/contextFlags.d.ts +5 -0
- package/dist/services/achievement/contextFlags.d.ts.map +1 -0
- package/dist/services/achievement/contextFlags.js +4 -0
- package/dist/services/achievement/grantSideEffects.d.ts +54 -0
- package/dist/services/achievement/grantSideEffects.d.ts.map +1 -0
- package/dist/services/achievement/grantSideEffects.js +269 -0
- package/dist/services/achievement/index.d.ts +6 -0
- package/dist/services/achievement/index.d.ts.map +1 -1
- package/dist/services/achievement/index.js +4 -0
- package/dist/services/achievement/reconcile.d.ts +36 -0
- package/dist/services/achievement/reconcile.d.ts.map +1 -0
- package/dist/services/achievement/reconcile.js +292 -0
- package/dist/services/achievement/recordEvent.d.ts.map +1 -1
- package/dist/services/achievement/recordEvent.js +8 -0
- package/dist/services/achievement/releaseReviewGate.d.ts +23 -0
- package/dist/services/achievement/releaseReviewGate.d.ts.map +1 -0
- package/dist/services/achievement/releaseReviewGate.js +71 -0
- package/dist/services/achievement/resolveCurrentTier.d.ts +1 -1
- package/dist/services/achievement/resolveCurrentTier.d.ts.map +1 -1
- package/dist/services/achievement/resolveCurrentTier.js +12 -2
- package/dist/services/achievement/submitAchievementRequest.d.ts +4 -2
- package/dist/services/achievement/submitAchievementRequest.d.ts.map +1 -1
- package/dist/services/achievement/submitAchievementRequest.js +8 -101
- package/dist/services/achievement/tierApproval.d.ts +8 -0
- package/dist/services/achievement/tierApproval.d.ts.map +1 -0
- package/dist/services/achievement/tierApproval.js +21 -0
- package/dist/services/achievement/tierProgression.d.ts +22 -0
- package/dist/services/achievement/tierProgression.d.ts.map +1 -0
- package/dist/services/achievement/tierProgression.js +100 -0
- package/dist/types.d.ts +8 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -6
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { collectionOf } from '../../collections/helpers';
|
|
2
|
+
import { relationId } from './relationId';
|
|
3
|
+
/**
|
|
4
|
+
* When a definition stops requiring review, approve each pending request for that
|
|
5
|
+
* definition so existing grant / tier.changed hooks run for that user + scope only.
|
|
6
|
+
*/
|
|
7
|
+
export async function releasePendingAchievementReviews(args) {
|
|
8
|
+
const pending = await args.req.payload.find({
|
|
9
|
+
collection: collectionOf('achievementRequests'),
|
|
10
|
+
depth: 0,
|
|
11
|
+
limit: 5000,
|
|
12
|
+
pagination: false,
|
|
13
|
+
overrideAccess: true,
|
|
14
|
+
req: args.req,
|
|
15
|
+
where: {
|
|
16
|
+
and: [
|
|
17
|
+
{ achievement: { equals: args.achievementId } },
|
|
18
|
+
{ status: { equals: 'pending' } },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
let requestsApproved = 0;
|
|
23
|
+
for (const doc of pending.docs) {
|
|
24
|
+
const id = relationId(doc.id);
|
|
25
|
+
if (!id)
|
|
26
|
+
continue;
|
|
27
|
+
await args.req.payload.update({
|
|
28
|
+
collection: collectionOf('achievementRequests'),
|
|
29
|
+
id,
|
|
30
|
+
data: { status: 'approved' },
|
|
31
|
+
depth: 0,
|
|
32
|
+
overrideAccess: true,
|
|
33
|
+
req: args.req,
|
|
34
|
+
});
|
|
35
|
+
requestsApproved += 1;
|
|
36
|
+
}
|
|
37
|
+
return { requestsApproved };
|
|
38
|
+
}
|
|
39
|
+
export async function releasePendingTierReviews(args) {
|
|
40
|
+
const pending = await args.req.payload.find({
|
|
41
|
+
collection: collectionOf('tierRequests'),
|
|
42
|
+
depth: 0,
|
|
43
|
+
limit: 5000,
|
|
44
|
+
pagination: false,
|
|
45
|
+
overrideAccess: true,
|
|
46
|
+
req: args.req,
|
|
47
|
+
where: {
|
|
48
|
+
and: [{ tier: { equals: args.tierId } }, { status: { equals: 'pending' } }],
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
let requestsApproved = 0;
|
|
52
|
+
for (const doc of pending.docs) {
|
|
53
|
+
const id = relationId(doc.id);
|
|
54
|
+
if (!id)
|
|
55
|
+
continue;
|
|
56
|
+
await args.req.payload.update({
|
|
57
|
+
collection: collectionOf('tierRequests'),
|
|
58
|
+
id,
|
|
59
|
+
data: { status: 'approved' },
|
|
60
|
+
depth: 0,
|
|
61
|
+
overrideAccess: true,
|
|
62
|
+
req: args.req,
|
|
63
|
+
});
|
|
64
|
+
requestsApproved += 1;
|
|
65
|
+
}
|
|
66
|
+
return { requestsApproved };
|
|
67
|
+
}
|
|
68
|
+
/** True when `requiresReview` flipped from required → not required. */
|
|
69
|
+
export function didDisableRequiresReview(previousDoc, doc) {
|
|
70
|
+
return previousDoc?.requiresReview === true && doc?.requiresReview !== true;
|
|
71
|
+
}
|
|
@@ -8,7 +8,7 @@ export type ResolvedTier = {
|
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
10
10
|
* Derive current tier by walking unlock rules in rank order (lowest first).
|
|
11
|
-
* Empty unlock rules pass. `
|
|
11
|
+
* Empty unlock rules pass. Tiers with `requiresReview` only count once an approved tier request exists.
|
|
12
12
|
*/
|
|
13
13
|
export declare function resolveCurrentTier(args: {
|
|
14
14
|
payload: Payload;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveCurrentTier.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/resolveCurrentTier.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"resolveCurrentTier.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/resolveCurrentTier.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAOtD,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB,CAAA;AAYD;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CA6D/B"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { collectionOf } from '../../collections/helpers';
|
|
2
2
|
import { evaluateRules } from './evaluateRules';
|
|
3
3
|
import { relationId } from './relationId';
|
|
4
|
+
import { hasApprovedTierRequest } from './tierApproval';
|
|
4
5
|
/**
|
|
5
6
|
* Derive current tier by walking unlock rules in rank order (lowest first).
|
|
6
|
-
* Empty unlock rules pass. `
|
|
7
|
+
* Empty unlock rules pass. Tiers with `requiresReview` only count once an approved tier request exists.
|
|
7
8
|
*/
|
|
8
9
|
export async function resolveCurrentTier(args) {
|
|
9
10
|
if (!args.userId)
|
|
@@ -25,7 +26,6 @@ export async function resolveCurrentTier(args) {
|
|
|
25
26
|
...(where ? { where } : {}),
|
|
26
27
|
});
|
|
27
28
|
const tiers = [...result.docs].sort((a, b) => Number(a.rank ?? 0) - Number(b.rank ?? 0));
|
|
28
|
-
// evaluateRules requires a PayloadRequest; synthesize a minimal one when only payload is passed.
|
|
29
29
|
const req = (args.req ?? { payload: args.payload });
|
|
30
30
|
let current = null;
|
|
31
31
|
for (const tier of tiers) {
|
|
@@ -39,6 +39,16 @@ export async function resolveCurrentTier(args) {
|
|
|
39
39
|
});
|
|
40
40
|
if (!unlocked)
|
|
41
41
|
continue;
|
|
42
|
+
if (tier.requiresReview === true) {
|
|
43
|
+
const approved = await hasApprovedTierRequest({
|
|
44
|
+
req,
|
|
45
|
+
userId: args.userId,
|
|
46
|
+
tierId: String(tier.id),
|
|
47
|
+
scopeId,
|
|
48
|
+
});
|
|
49
|
+
if (!approved)
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
42
52
|
current = {
|
|
43
53
|
id: String(tier.id),
|
|
44
54
|
rank: Number(tier.rank ?? 0),
|
|
@@ -10,12 +10,14 @@ export declare function submitAchievementRequest(args: {
|
|
|
10
10
|
scopeId?: string | null;
|
|
11
11
|
note?: string;
|
|
12
12
|
}): Promise<import("payload").JsonObject & import("payload").TypeWithID>;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Create a grant row. Side effects (logs, composites, tiers) run in Grants hooks.
|
|
15
|
+
*/
|
|
14
16
|
export declare function grantAchievement(args: {
|
|
15
17
|
req: PayloadRequest;
|
|
16
18
|
userId: string;
|
|
17
19
|
achievementId: string;
|
|
18
20
|
scopeId?: string | null;
|
|
19
21
|
note?: string | null;
|
|
20
|
-
}): Promise<
|
|
22
|
+
}): Promise<Record<string, unknown>>;
|
|
21
23
|
//# sourceMappingURL=submitAchievementRequest.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"submitAchievementRequest.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/submitAchievementRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,
|
|
1
|
+
{"version":3,"file":"submitAchievementRequest.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/submitAchievementRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAK7C;;;GAGG;AACH,wBAAsB,wBAAwB,CAAC,IAAI,EAAE;IACnD,GAAG,EAAE,cAAc,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,wEAcA;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,GAAG,EAAE,cAAc,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB,oCAQA"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { collectionOf } from '../../collections/helpers';
|
|
2
|
-
import {
|
|
3
|
-
import { recordLog, syncTierChangedLog } from './recordEvent';
|
|
2
|
+
import { createGrantRow } from './grantSideEffects';
|
|
4
3
|
/**
|
|
5
4
|
* Create an achievement request (or auto-approved grant) via the collection.
|
|
6
5
|
* Hooks bind the requesting user, check eligibility, and grant when approved.
|
|
@@ -9,7 +8,6 @@ export async function submitAchievementRequest(args) {
|
|
|
9
8
|
return args.req.payload.create({
|
|
10
9
|
collection: collectionOf('achievementRequests'),
|
|
11
10
|
data: {
|
|
12
|
-
// user is overwritten from req.user in beforeValidate
|
|
13
11
|
user: args.userId,
|
|
14
12
|
scope: args.scopeId ?? undefined,
|
|
15
13
|
achievement: args.achievementId,
|
|
@@ -21,106 +19,15 @@ export async function submitAchievementRequest(args) {
|
|
|
21
19
|
user: args.req.user ?? undefined,
|
|
22
20
|
});
|
|
23
21
|
}
|
|
24
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Create a grant row. Side effects (logs, composites, tiers) run in Grants hooks.
|
|
24
|
+
*/
|
|
25
25
|
export async function grantAchievement(args) {
|
|
26
|
-
|
|
27
|
-
const achievementId = args.achievementId;
|
|
28
|
-
const definition = (await args.req.payload.findByID({
|
|
29
|
-
collection: collectionOf('achievements'),
|
|
30
|
-
id: achievementId,
|
|
31
|
-
depth: 0,
|
|
32
|
-
overrideAccess: true,
|
|
33
|
-
req: args.req,
|
|
34
|
-
}));
|
|
35
|
-
const clauses = [
|
|
36
|
-
{ user: { equals: args.userId } },
|
|
37
|
-
{ achievement: { equals: achievementId } },
|
|
38
|
-
];
|
|
39
|
-
if (scopeId)
|
|
40
|
-
clauses.push({ scope: { equals: scopeId } });
|
|
41
|
-
else
|
|
42
|
-
clauses.push({ scope: { exists: false } });
|
|
43
|
-
const existing = await args.req.payload.find({
|
|
44
|
-
collection: collectionOf('grants'),
|
|
45
|
-
depth: 0,
|
|
46
|
-
limit: 1,
|
|
47
|
-
overrideAccess: true,
|
|
48
|
-
req: args.req,
|
|
49
|
-
where: { and: clauses },
|
|
50
|
-
});
|
|
51
|
-
if (existing.docs[0])
|
|
52
|
-
return existing.docs[0];
|
|
53
|
-
const result = await args.req.payload.create({
|
|
54
|
-
collection: collectionOf('grants'),
|
|
55
|
-
data: {
|
|
56
|
-
user: args.userId,
|
|
57
|
-
scope: scopeId ?? undefined,
|
|
58
|
-
achievement: achievementId,
|
|
59
|
-
completedAt: new Date().toISOString(),
|
|
60
|
-
...(args.note ? { note: args.note } : {}),
|
|
61
|
-
},
|
|
62
|
-
overrideAccess: true,
|
|
63
|
-
req: args.req,
|
|
64
|
-
});
|
|
65
|
-
await recordLog({
|
|
66
|
-
payload: args.req.payload,
|
|
26
|
+
return createGrantRow({
|
|
67
27
|
req: args.req,
|
|
68
28
|
userId: args.userId,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
achievement: achievementId,
|
|
73
|
-
...(definition?.slug ? { achievementSlug: definition.slug } : {}),
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
await syncCompositeGrants({
|
|
77
|
-
req: args.req,
|
|
78
|
-
userId: args.userId,
|
|
79
|
-
scopeId,
|
|
80
|
-
});
|
|
81
|
-
await syncTierChangedLog({
|
|
82
|
-
payload: args.req.payload,
|
|
83
|
-
req: args.req,
|
|
84
|
-
userId: args.userId,
|
|
85
|
-
scopeId,
|
|
86
|
-
});
|
|
87
|
-
return result;
|
|
88
|
-
}
|
|
89
|
-
async function syncCompositeGrants(args) {
|
|
90
|
-
const where = args.scopeId
|
|
91
|
-
? { or: [{ scope: { equals: args.scopeId } }, { scope: { exists: false } }] }
|
|
92
|
-
: undefined;
|
|
93
|
-
const catalog = await args.req.payload.find({
|
|
94
|
-
collection: collectionOf('achievements'),
|
|
95
|
-
depth: 0,
|
|
96
|
-
limit: 200,
|
|
97
|
-
pagination: false,
|
|
98
|
-
overrideAccess: true,
|
|
99
|
-
req: args.req,
|
|
100
|
-
...(where ? { where } : {}),
|
|
29
|
+
achievementId: args.achievementId,
|
|
30
|
+
scopeId: args.scopeId,
|
|
31
|
+
note: args.note,
|
|
101
32
|
});
|
|
102
|
-
for (const doc of catalog.docs) {
|
|
103
|
-
const def = doc;
|
|
104
|
-
if (def.requiresReview !== false)
|
|
105
|
-
continue;
|
|
106
|
-
if (!ruleGroupHasProgressRequirements(def.completionRules))
|
|
107
|
-
continue;
|
|
108
|
-
if (String(def.id) === '')
|
|
109
|
-
continue;
|
|
110
|
-
const complete = await evaluateRules({
|
|
111
|
-
payload: args.req.payload,
|
|
112
|
-
req: args.req,
|
|
113
|
-
rules: def.completionRules,
|
|
114
|
-
userId: args.userId,
|
|
115
|
-
scopeId: args.scopeId,
|
|
116
|
-
});
|
|
117
|
-
if (!complete)
|
|
118
|
-
continue;
|
|
119
|
-
await grantAchievement({
|
|
120
|
-
req: args.req,
|
|
121
|
-
userId: args.userId,
|
|
122
|
-
achievementId: String(def.id),
|
|
123
|
-
scopeId: args.scopeId,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
33
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tierApproval.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/tierApproval.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAS,MAAM,SAAS,CAAA;AAIpD,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,GAAG,EAAE,cAAc,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB,GAAG,OAAO,CAAC,OAAO,CAAC,CAkBnB"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { collectionOf } from '../../collections/helpers';
|
|
2
|
+
export async function hasApprovedTierRequest(args) {
|
|
3
|
+
const clauses = [
|
|
4
|
+
{ user: { equals: args.userId } },
|
|
5
|
+
{ tier: { equals: args.tierId } },
|
|
6
|
+
{ status: { equals: 'approved' } },
|
|
7
|
+
];
|
|
8
|
+
if (args.scopeId)
|
|
9
|
+
clauses.push({ scope: { equals: args.scopeId } });
|
|
10
|
+
else
|
|
11
|
+
clauses.push({ scope: { exists: false } });
|
|
12
|
+
const existing = await args.req.payload.find({
|
|
13
|
+
collection: collectionOf('tierRequests'),
|
|
14
|
+
depth: 0,
|
|
15
|
+
limit: 1,
|
|
16
|
+
overrideAccess: true,
|
|
17
|
+
req: args.req,
|
|
18
|
+
where: { and: clauses },
|
|
19
|
+
});
|
|
20
|
+
return Boolean(existing.docs[0]);
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
export declare function ensureTierRequest(args: {
|
|
3
|
+
req: PayloadRequest;
|
|
4
|
+
userId: string;
|
|
5
|
+
tierId: string;
|
|
6
|
+
scopeId: string | null;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
doc: Record<string, unknown>;
|
|
9
|
+
created: boolean;
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* When unlock rules pass and the tier requires review, ensure a pending/approved request exists.
|
|
13
|
+
*/
|
|
14
|
+
export declare function syncTierProgression(args: {
|
|
15
|
+
req: PayloadRequest;
|
|
16
|
+
userId: string;
|
|
17
|
+
scopeId: string | null;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
requested: number;
|
|
20
|
+
}>;
|
|
21
|
+
export { hasApprovedTierRequest } from './tierApproval';
|
|
22
|
+
//# sourceMappingURL=tierProgression.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tierProgression.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/tierProgression.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAS,MAAM,SAAS,CAAA;AAgBpD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,GAAG,EAAE,cAAc,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB,GAAG,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAsC9D;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC9C,GAAG,EAAE,cAAc,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB,GAAG,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CA0DjC;AAED,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { collectionOf } from '../../collections/helpers';
|
|
2
|
+
import { CTX_SYSTEM_REQUEST } from './contextFlags';
|
|
3
|
+
import { evaluateRules } from './evaluateRules';
|
|
4
|
+
import { hasApprovedTierRequest } from './tierApproval';
|
|
5
|
+
export async function ensureTierRequest(args) {
|
|
6
|
+
const clauses = [
|
|
7
|
+
{ user: { equals: args.userId } },
|
|
8
|
+
{ tier: { equals: args.tierId } },
|
|
9
|
+
{ status: { in: ['pending', 'approved'] } },
|
|
10
|
+
];
|
|
11
|
+
if (args.scopeId)
|
|
12
|
+
clauses.push({ scope: { equals: args.scopeId } });
|
|
13
|
+
else
|
|
14
|
+
clauses.push({ scope: { exists: false } });
|
|
15
|
+
const existing = await args.req.payload.find({
|
|
16
|
+
collection: collectionOf('tierRequests'),
|
|
17
|
+
depth: 0,
|
|
18
|
+
limit: 1,
|
|
19
|
+
overrideAccess: true,
|
|
20
|
+
req: args.req,
|
|
21
|
+
where: { and: clauses },
|
|
22
|
+
});
|
|
23
|
+
if (existing.docs[0]) {
|
|
24
|
+
return { doc: existing.docs[0], created: false };
|
|
25
|
+
}
|
|
26
|
+
args.req.context[CTX_SYSTEM_REQUEST] = true;
|
|
27
|
+
try {
|
|
28
|
+
const doc = (await args.req.payload.create({
|
|
29
|
+
collection: collectionOf('tierRequests'),
|
|
30
|
+
data: {
|
|
31
|
+
user: args.userId,
|
|
32
|
+
scope: args.scopeId ?? undefined,
|
|
33
|
+
tier: args.tierId,
|
|
34
|
+
status: 'pending',
|
|
35
|
+
},
|
|
36
|
+
overrideAccess: true,
|
|
37
|
+
req: args.req,
|
|
38
|
+
}));
|
|
39
|
+
return { doc, created: true };
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
delete args.req.context[CTX_SYSTEM_REQUEST];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* When unlock rules pass and the tier requires review, ensure a pending/approved request exists.
|
|
47
|
+
*/
|
|
48
|
+
export async function syncTierProgression(args) {
|
|
49
|
+
const scopeId = args.scopeId;
|
|
50
|
+
const where = scopeId
|
|
51
|
+
? { or: [{ scope: { equals: scopeId } }, { scope: { exists: false } }] }
|
|
52
|
+
: undefined;
|
|
53
|
+
const result = await args.req.payload.find({
|
|
54
|
+
collection: collectionOf('tiers'),
|
|
55
|
+
depth: 0,
|
|
56
|
+
limit: 100,
|
|
57
|
+
pagination: false,
|
|
58
|
+
overrideAccess: true,
|
|
59
|
+
req: args.req,
|
|
60
|
+
sort: 'rank',
|
|
61
|
+
...(where ? { where } : {}),
|
|
62
|
+
});
|
|
63
|
+
const tiers = [...result.docs].sort((a, b) => Number(a.rank ?? 0) - Number(b.rank ?? 0));
|
|
64
|
+
let requested = 0;
|
|
65
|
+
let ladderRank = null;
|
|
66
|
+
for (const tier of tiers) {
|
|
67
|
+
const unlocked = await evaluateRules({
|
|
68
|
+
payload: args.req.payload,
|
|
69
|
+
req: args.req,
|
|
70
|
+
rules: tier.unlockRules,
|
|
71
|
+
userId: args.userId,
|
|
72
|
+
scopeId,
|
|
73
|
+
ladderRank,
|
|
74
|
+
});
|
|
75
|
+
if (!unlocked)
|
|
76
|
+
continue;
|
|
77
|
+
if (tier.requiresReview === true) {
|
|
78
|
+
const { created } = await ensureTierRequest({
|
|
79
|
+
req: args.req,
|
|
80
|
+
userId: args.userId,
|
|
81
|
+
tierId: String(tier.id),
|
|
82
|
+
scopeId,
|
|
83
|
+
});
|
|
84
|
+
const approved = await hasApprovedTierRequest({
|
|
85
|
+
req: args.req,
|
|
86
|
+
userId: args.userId,
|
|
87
|
+
tierId: String(tier.id),
|
|
88
|
+
scopeId,
|
|
89
|
+
});
|
|
90
|
+
if (!approved) {
|
|
91
|
+
if (created)
|
|
92
|
+
requested += 1;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
ladderRank = Number(tier.rank ?? 0);
|
|
97
|
+
}
|
|
98
|
+
return { requested };
|
|
99
|
+
}
|
|
100
|
+
export { hasApprovedTierRequest } from './tierApproval';
|
package/dist/types.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export type AchievementRuleType = {
|
|
|
33
33
|
progressRole?: 'requirement' | 'gate';
|
|
34
34
|
};
|
|
35
35
|
/** Logical collection ids — map to configurable slugs via `collections`. */
|
|
36
|
-
export type AchievementCollectionKey = 'tiers' | 'achievements' | 'grants' | 'achievementRequests' | 'logs' | 'eventTypes' | 'metrics';
|
|
36
|
+
export type AchievementCollectionKey = 'tiers' | 'achievements' | 'grants' | 'achievementRequests' | 'tierRequests' | 'logs' | 'eventTypes' | 'metrics';
|
|
37
37
|
/**
|
|
38
38
|
* Host overrides layered onto a plugin collection (Form Builder–style).
|
|
39
39
|
* Access/admin merge shallowly; hook arrays are appended after plugin hooks.
|
|
@@ -80,6 +80,12 @@ export type AchievementEndpointsOptions = {
|
|
|
80
80
|
* Pass `false` to skip registering the endpoint.
|
|
81
81
|
*/
|
|
82
82
|
me?: string | false;
|
|
83
|
+
/**
|
|
84
|
+
* Root REST path to repair progression from existing grants (`POST`).
|
|
85
|
+
* Default: `'/achievements/reconcile'`. Requires host `canReview`.
|
|
86
|
+
* Pass `false` to skip.
|
|
87
|
+
*/
|
|
88
|
+
reconcile?: string | false;
|
|
83
89
|
};
|
|
84
90
|
export type AchievementPluginOptions = {
|
|
85
91
|
/**
|
|
@@ -113,5 +119,5 @@ export type AchievementPluginOptions = {
|
|
|
113
119
|
};
|
|
114
120
|
export type BuiltInRuleType = 'achievement-complete' | 'event-count' | 'metric-minimum' | 'tier-at-least';
|
|
115
121
|
/** Engine log-type catalog slugs (seeded as system catalog rows). */
|
|
116
|
-
export type BuiltInEventType = 'achievement.granted' | 'metric.delta' | 'tier.changed';
|
|
122
|
+
export type BuiltInEventType = 'achievement.granted' | 'achievement.revoked' | 'metric.delta' | 'tier.changed';
|
|
117
123
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAE1E,MAAM,MAAM,sBAAsB,GAAG;IACnC,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,cAAc,CAAC,SAAS,CAAC,CAAA;IAClC,GAAG,EAAE,cAAc,CAAA;IACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,+FAA+F;IAC/F,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAC9B,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,CAAC,IAAI,EAAE,uBAAuB,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAA;IACvE;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,uBAAuB,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAA;IACtE;;;OAGG;IACH,YAAY,CAAC,EAAE,aAAa,GAAG,MAAM,CAAA;CACtC,CAAA;AAED,4EAA4E;AAC5E,MAAM,MAAM,wBAAwB,GAChC,OAAO,GACP,cAAc,GACd,QAAQ,GACR,qBAAqB,GACrB,MAAM,GACN,YAAY,GACZ,SAAS,CAAA;AAEb;;;GAGG;AACH,MAAM,MAAM,6BAA6B,GAAG;IAC1C,MAAM,CAAC,EAAE,gBAAgB,CAAC,QAAQ,CAAC,CAAA;IACnC,KAAK,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;IACjC,KAAK,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;IACjC,MAAM,CAAC,EAAE,gBAAgB,CAAC,QAAQ,CAAC,CAAA;CACpC,CAAA;AAED,MAAM,MAAM,6BAA6B,GAAG;IAC1C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAC3B,8DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,wBAAwB,EAAE,MAAM,CAAC,CAAC,CAAA;IACzD;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,wBAAwB,EAAE,6BAA6B,CAAC,CAAC,CAAA;CACrF,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC;;;;OAIG;IACH,EAAE,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dappermountain/payload-plugin-achievements",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A robust, Admin-configured achievements engine for Payload: composable unlock rules, derived rank progress, request/review grants, metrics, and scoped multi-tenant support.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"import": "./dist/index.js",
|
|
29
29
|
"default": "./dist/index.js"
|
|
30
30
|
},
|
|
31
|
+
"./cli": {
|
|
32
|
+
"types": "./dist/cli/index.d.ts",
|
|
33
|
+
"import": "./dist/cli/index.js",
|
|
34
|
+
"default": "./dist/cli/index.js"
|
|
35
|
+
},
|
|
31
36
|
"./types": {
|
|
32
37
|
"types": "./dist/types.d.ts",
|
|
33
38
|
"import": "./dist/types.js",
|
|
@@ -69,15 +74,15 @@
|
|
|
69
74
|
"react": "^19.0.0"
|
|
70
75
|
},
|
|
71
76
|
"devDependencies": {
|
|
77
|
+
"@dappermountain/agent-payload": "^0.1.5",
|
|
78
|
+
"@dappermountain/agent-practices": "^0.1.2",
|
|
72
79
|
"@payloadcms/ui": "3.88.0",
|
|
80
|
+
"@types/bun": "1.4.0",
|
|
73
81
|
"@types/react": "^19.0.0",
|
|
74
82
|
"payload": "3.88.0",
|
|
75
|
-
"react": "19.2.4",
|
|
76
|
-
"typescript": "5.9.2",
|
|
77
83
|
"prettier": "^3.6.2",
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"@dappermountain/agent-payload": "^0.1.0"
|
|
84
|
+
"react": "19.2.4",
|
|
85
|
+
"typescript": "5.9.2"
|
|
81
86
|
},
|
|
82
87
|
"engines": {
|
|
83
88
|
"node": ">=20"
|