@dappermountain/payload-plugin-achievements 0.3.0 → 0.3.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 +20 -5
- package/dist/endpoints/me.d.ts +2 -1
- package/dist/endpoints/me.d.ts.map +1 -1
- package/dist/endpoints/me.js +7 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/services/achievement/buildUnlockRequirementLeaves.d.ts +48 -0
- package/dist/services/achievement/buildUnlockRequirementLeaves.d.ts.map +1 -0
- package/dist/services/achievement/buildUnlockRequirementLeaves.js +192 -0
- package/dist/services/achievement/getUserProgress.d.ts +7 -0
- package/dist/services/achievement/getUserProgress.d.ts.map +1 -1
- package/dist/services/achievement/getUserProgress.js +11 -1
- package/dist/services/achievement/index.d.ts +5 -0
- package/dist/services/achievement/index.d.ts.map +1 -1
- package/dist/services/achievement/index.js +2 -0
- package/dist/services/achievement/loadUserProgressReviews.d.ts +23 -0
- package/dist/services/achievement/loadUserProgressReviews.d.ts.map +1 -0
- package/dist/services/achievement/loadUserProgressReviews.js +79 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,7 +124,11 @@ Unlock (tiers), eligibility, and completion (achievements) share the same rule t
|
|
|
124
124
|
|
|
125
125
|
**Stored metrics** — totals live in `achievement-metric-balances`, updated by `recordMetricChange` (log + balance dual-write). Logs remain the audit/rebuild source; reconcile backfills balances.
|
|
126
126
|
|
|
127
|
-
**Leaderboards** — `getMetricLeaderboard` / `GET /api/achievements/leaderboard?metric=points` ranks stored balances (authenticated). Disable with `endpoints.leaderboard: false`.
|
|
127
|
+
**Leaderboards** — `getMetricLeaderboard` / `GET /api/achievements/leaderboard?metric=points` ranks **stored** metric balances (authenticated, optional `scope`). Computed/elapsed metrics (e.g. tenure days) have no balance rows and are rejected — resolve a single user with `resolveMetricValue`, or materialize snapshots if you need a ranked computed board. Disable with `endpoints.leaderboard: false`. Hosts compose member directories (names, current tier, earned counts, who may appear) themselves; the plugin returns `{ user, value, rank }` only.
|
|
128
|
+
|
|
129
|
+
**Unlock checklist** — `buildUnlockRequirementLeaves` walks a rule tree (including Payload leaves that carry empty `rules: []`), evaluates met/progress per leaf, and returns catalog subjects (`relations`, `target`, `unit`, `since`). It does **not** build UI copy — hosts format labels. `achievement-complete` is omitted by default so those stay on catalog groups. `eachRuleLeaf` is also exported if you need the walker alone.
|
|
130
|
+
|
|
131
|
+
**Review snapshot** — `getUserProgress({ include: { reviews: true } })` or `loadUserProgressReviews` adds pending achievement keys and open/rejected tier requests so hosts do not re-query the engine for badge state.
|
|
128
132
|
|
|
129
133
|
**System catalog** — by default the plugin runs `seedAchievementCatalog` on Payload `onInit` (opt out with `seedSystemCatalog: false`). Hosts still seed product catalog + locales via `seedAchievements`.
|
|
130
134
|
|
|
@@ -230,11 +234,11 @@ achievementPlugin({
|
|
|
230
234
|
|
|
231
235
|
| Option | Default | Notes |
|
|
232
236
|
| --- | --- | --- |
|
|
233
|
-
| `me` | `'/achievements/me'` | Registers `GET /api/achievements/me` for the signed-in user’s grants snapshot (+ derived tiers and lean requests). Pass `false` to skip. Having review access does **not** widen this endpoint to other users. |
|
|
234
|
-
| `leaderboard` | `'/achievements/leaderboard'` | Registers `GET /api/achievements/leaderboard?metric=<slug>` for authenticated users (stored metrics only). Pass `false` to skip. |
|
|
237
|
+
| `me` | `'/achievements/me'` | Registers `GET /api/achievements/me` for the signed-in user’s grants snapshot (+ derived tiers and lean requests). Pass `false` to skip. Having review access does **not** widen this endpoint to other users. Optional `?reviews=1` adds `reviews`. |
|
|
238
|
+
| `leaderboard` | `'/achievements/leaderboard'` | Registers `GET /api/achievements/leaderboard?metric=<slug>` for authenticated users (**stored** metrics only). Pass `false` to skip. |
|
|
235
239
|
| `reconcile` | `'/achievements/reconcile'` | Registers `POST /api/achievements/reconcile` for reviewers. Pass `false` to skip. |
|
|
236
240
|
|
|
237
|
-
Query params (`me`): `scope`, `limit`, `page`, `requestsLimit`. Leaderboard: `metric` (required), `scope`, `limit`, `page`.
|
|
241
|
+
Query params (`me`): `scope`, `limit`, `page`, `requestsLimit`, `reviews`. Leaderboard: `metric` (required), `scope`, `limit`, `page`.
|
|
238
242
|
|
|
239
243
|
### `extensions.ruleTypes` / `metricAnchors`
|
|
240
244
|
|
|
@@ -285,14 +289,25 @@ import {
|
|
|
285
289
|
resolveCurrentTier,
|
|
286
290
|
resolveTierProgress,
|
|
287
291
|
getUserProgress,
|
|
292
|
+
loadUserProgressReviews,
|
|
293
|
+
buildUnlockRequirementLeaves,
|
|
288
294
|
submitAchievementRequest,
|
|
289
295
|
reviewAchievementRequest,
|
|
290
296
|
} from '@dappermountain/payload-plugin-achievements'
|
|
291
297
|
|
|
292
298
|
await recordLog({ payload, userId, scopeId, type: 'host.custom-kind', actorId })
|
|
293
299
|
await recordMetricChange({ payload, userId, scopeId, metric: 'points', change: 10 })
|
|
300
|
+
const points = await resolveMetricValue({ payload, userId, scopeId, metric: 'points' })
|
|
294
301
|
const board = await getMetricLeaderboard({ payload, metric: 'points', scopeId, limit: 20 })
|
|
295
302
|
const tier = await resolveCurrentTier({ payload, userId, scopeId })
|
|
303
|
+
const progress = await getUserProgress({ payload, userId, scopeId, include: { reviews: true } })
|
|
304
|
+
const unlockLeaves = await buildUnlockRequirementLeaves({
|
|
305
|
+
payload,
|
|
306
|
+
userId,
|
|
307
|
+
scopeId,
|
|
308
|
+
rules: nextTier.unlockRules,
|
|
309
|
+
ladderRank: tier?.rank ?? null,
|
|
310
|
+
})
|
|
296
311
|
|
|
297
312
|
// Repair users whose grants predate side-effect hooks (idempotent)
|
|
298
313
|
await reconcileProgression({ payload })
|
|
@@ -334,7 +349,7 @@ Turning **Requires review** off on a definition or tier **approves pending reque
|
|
|
334
349
|
|
|
335
350
|
| Goal | Request |
|
|
336
351
|
| --- | --- |
|
|
337
|
-
| Current user snapshot | `GET /api/achievements/me?scope=<scopeId>&limit=10&page=1` |
|
|
352
|
+
| Current user snapshot | `GET /api/achievements/me?scope=<scopeId>&limit=10&page=1` (`&reviews=1` for pending/rejected review keys) |
|
|
338
353
|
| Stored metric leaderboard | `GET /api/achievements/leaderboard?metric=points&scope=<scopeId>&limit=20&page=1` |
|
|
339
354
|
| Repair progression | `POST /api/achievements/reconcile` (reviewer; optional `{ userId, scopeId, achievementId\|achievementSlug, tierId\|tierSlug, limit }`) |
|
|
340
355
|
| List grants | `GET /api/achievement-grants?where[user][equals]=<userId>` |
|
package/dist/endpoints/me.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload';
|
|
2
2
|
/**
|
|
3
3
|
* `GET` current user’s snapshot. Body is Payload `find`-shaped for **grants**,
|
|
4
|
-
* plus ladder-derived `tiers` and lean `requests`. Optional `?scope=` / `limit` / `page
|
|
4
|
+
* plus ladder-derived `tiers` and lean `requests`. Optional `?scope=` / `limit` / `page`
|
|
5
|
+
* / `reviews=1` (pending achievement keys + open/rejected tier requests).
|
|
5
6
|
*/
|
|
6
7
|
export declare function buildMeEndpoint(path: string): Endpoint;
|
|
7
8
|
//# sourceMappingURL=me.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"me.d.ts","sourceRoot":"","sources":["../../src/endpoints/me.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAkB,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"me.d.ts","sourceRoot":"","sources":["../../src/endpoints/me.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAkB,MAAM,SAAS,CAAA;AAoCvD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAsBtD"}
|
package/dist/endpoints/me.js
CHANGED
|
@@ -24,9 +24,14 @@ function pagingFromRequest(req) {
|
|
|
24
24
|
requestsLimit: positiveInt(params?.get('requestsLimit') ?? null, 20),
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
+
function reviewsFromRequest(req) {
|
|
28
|
+
const raw = requestUrl(req)?.searchParams.get('reviews');
|
|
29
|
+
return raw === '1' || raw === 'true';
|
|
30
|
+
}
|
|
27
31
|
/**
|
|
28
32
|
* `GET` current user’s snapshot. Body is Payload `find`-shaped for **grants**,
|
|
29
|
-
* plus ladder-derived `tiers` and lean `requests`. Optional `?scope=` / `limit` / `page
|
|
33
|
+
* plus ladder-derived `tiers` and lean `requests`. Optional `?scope=` / `limit` / `page`
|
|
34
|
+
* / `reviews=1` (pending achievement keys + open/rejected tier requests).
|
|
30
35
|
*/
|
|
31
36
|
export function buildMeEndpoint(path) {
|
|
32
37
|
return {
|
|
@@ -43,6 +48,7 @@ export function buildMeEndpoint(path) {
|
|
|
43
48
|
userId,
|
|
44
49
|
scopeId: scopeFromRequest(req),
|
|
45
50
|
paging: pagingFromRequest(req),
|
|
51
|
+
include: reviewsFromRequest(req) ? { reviews: true } : undefined,
|
|
46
52
|
});
|
|
47
53
|
return Response.json(body);
|
|
48
54
|
},
|
package/dist/index.d.ts
CHANGED
|
@@ -3,12 +3,13 @@ export type { EngineEventTypeSlug, EngineMetricSlug } from './catalog.js';
|
|
|
3
3
|
export { achievementPlugin } from './plugin.js';
|
|
4
4
|
export { seedAchievementCatalog, seedAchievements } from './seed/index.js';
|
|
5
5
|
export type { AchievementSeedAchievement, AchievementSeedCatalogRow, AchievementSeedInput, AchievementSeedLocalizedText, AchievementSeedTier, } from './seed/index.js';
|
|
6
|
-
export { evaluateRules, evaluateRuleProgress, collectAchievementCompleteRefs, buildCatalogProgressGroups, getUserProgress, getMetricLeaderboard, grantAchievement, loadMetricDoc, recordEvent, recordLog, recordMetricChange, rebuildMetricBalance, rebuildMetricBalancesForUser, reconcileProgression, reconcileUserProgression, releasePendingAchievementReviews, releasePendingTierReviews, resolveCurrentTier, resolveElapsedAnchor, resolveMetricValue, resolveTierProgress, reviewAchievementRequest, submitAchievementRequest, syncTierChangedLog, syncTierProgression, syncCompositeProgression, catalogRef, transitionLogData, transitionToId, validateRuleGroup, validateRuleNode, } from './services/achievement/index.js';
|
|
7
|
-
export type { ResolvedTier, RuleGroup, TierLadderProgress, CatalogProgressGroup, CatalogProgressItem, CatalogRef, TransitionLogData, MetricLeaderboardResult, MetricLeaderboardRow, ReconcileProgressionArgs, ReconcileProgressionResult, ReleaseReviewGateResult, } from './services/achievement/index.js';
|
|
6
|
+
export { evaluateRules, evaluateRuleProgress, collectAchievementCompleteRefs, buildCatalogProgressGroups, buildUnlockRequirementLeaves, getUserProgress, loadUserProgressReviews, getMetricLeaderboard, grantAchievement, loadMetricDoc, recordEvent, recordLog, recordMetricChange, rebuildMetricBalance, rebuildMetricBalancesForUser, reconcileProgression, reconcileUserProgression, releasePendingAchievementReviews, releasePendingTierReviews, resolveCurrentTier, resolveElapsedAnchor, resolveMetricValue, resolveTierProgress, reviewAchievementRequest, submitAchievementRequest, syncTierChangedLog, syncTierProgression, syncCompositeProgression, catalogRef, transitionLogData, transitionToId, validateRuleGroup, validateRuleNode, } from './services/achievement/index.js';
|
|
7
|
+
export type { ResolvedTier, RuleGroup, TierLadderProgress, CatalogProgressGroup, CatalogProgressItem, UnlockRequirementLeaf, UnlockRequirementRelation, GetUserProgressInclude, UserProgressMeResponse, UserProgressReviews, UserProgressTierRequest, CatalogRef, TransitionLogData, MetricLeaderboardResult, MetricLeaderboardRow, ReconcileProgressionArgs, ReconcileProgressionResult, ReleaseReviewGateResult, } from './services/achievement/index.js';
|
|
8
8
|
export type { BuiltInEventType, BuiltInRuleType, AchievementCollectionKey, AchievementCollectionOverride, AchievementCollectionsOptions, AchievementEndpointsOptions, AchievementMetricAnchor, AchievementMetricAnchorConfig, AchievementMetricAnchorEntry, AchievementPluginOptions, AchievementRuleType, AchievementScopeConfig, AchievementUsersOptions, } from './types.js';
|
|
9
9
|
export { DEFAULT_COLLECTION_BASE_SLUGS, DEFAULT_COLLECTION_PREFIX, DEFAULT_ADMIN_GROUP } from './collections/slugs.js';
|
|
10
10
|
export { DEFAULT_ME_ENDPOINT_PATH, DEFAULT_LEADERBOARD_ENDPOINT_PATH, DEFAULT_RECONCILE_ENDPOINT_PATH, } from './defaults.js';
|
|
11
11
|
export { collectionOf, slugOf } from './collections/helpers.js';
|
|
12
|
+
export { eachRuleLeaf } from './collections/hooks/eachRuleLeaf.js';
|
|
12
13
|
export { isAchievementPluginEnabled } from './enabled.js';
|
|
13
14
|
export { descriptionToPlaintext } from './fields/descriptionPlaintext.js';
|
|
14
15
|
export { plainTextToLexical } from './fields/plainTextToLexical.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,+BAA+B,EAC/B,kBAAkB,EAClB,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAC1E,YAAY,EACV,0BAA0B,EAC1B,yBAAyB,EACzB,oBAAoB,EACpB,4BAA4B,EAC5B,mBAAmB,GACpB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,8BAA8B,EAC9B,0BAA0B,EAC1B,eAAe,EACf,oBAAoB,EACpB,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,4BAA4B,EAC5B,oBAAoB,EACpB,wBAAwB,EACxB,gCAAgC,EAChC,yBAAyB,EACzB,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,EACxB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,iCAAiC,CAAA;AACxC,YAAY,EACV,YAAY,EACZ,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,UAAU,EACV,iBAAiB,EACjB,uBAAuB,EACvB,oBAAoB,EACpB,wBAAwB,EACxB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,iCAAiC,CAAA;AACxC,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,wBAAwB,EACxB,6BAA6B,EAC7B,6BAA6B,EAC7B,2BAA2B,EAC3B,uBAAuB,EACvB,6BAA6B,EAC7B,4BAA4B,EAC5B,wBAAwB,EACxB,mBAAmB,EACnB,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,6BAA6B,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AACtH,OAAO,EACL,wBAAwB,EACxB,iCAAiC,EACjC,+BAA+B,GAChC,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAA;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,+BAA+B,EAC/B,kBAAkB,EAClB,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAC1E,YAAY,EACV,0BAA0B,EAC1B,yBAAyB,EACzB,oBAAoB,EACpB,4BAA4B,EAC5B,mBAAmB,GACpB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,8BAA8B,EAC9B,0BAA0B,EAC1B,4BAA4B,EAC5B,eAAe,EACf,uBAAuB,EACvB,oBAAoB,EACpB,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,4BAA4B,EAC5B,oBAAoB,EACpB,wBAAwB,EACxB,gCAAgC,EAChC,yBAAyB,EACzB,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,EACxB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,iCAAiC,CAAA;AACxC,YAAY,EACV,YAAY,EACZ,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EACzB,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACnB,uBAAuB,EACvB,UAAU,EACV,iBAAiB,EACjB,uBAAuB,EACvB,oBAAoB,EACpB,wBAAwB,EACxB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,iCAAiC,CAAA;AACxC,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,wBAAwB,EACxB,6BAA6B,EAC7B,6BAA6B,EAC7B,2BAA2B,EAC3B,uBAAuB,EACvB,6BAA6B,EAC7B,4BAA4B,EAC5B,wBAAwB,EACxB,mBAAmB,EACnB,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,6BAA6B,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AACtH,OAAO,EACL,wBAAwB,EACxB,iCAAiC,EACjC,+BAA+B,GAChC,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAA;AAClE,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAA;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export { DEFAULT_ACHIEVEMENT_METRIC, DEFAULT_ACHIEVEMENT_METRIC_SLUG, ENGINE_EVENT_TYPES, ENGINE_METRICS, } from './catalog.js';
|
|
2
2
|
export { achievementPlugin } from './plugin.js';
|
|
3
3
|
export { seedAchievementCatalog, seedAchievements } from './seed/index.js';
|
|
4
|
-
export { evaluateRules, evaluateRuleProgress, collectAchievementCompleteRefs, buildCatalogProgressGroups, getUserProgress, getMetricLeaderboard, grantAchievement, loadMetricDoc, recordEvent, recordLog, recordMetricChange, rebuildMetricBalance, rebuildMetricBalancesForUser, reconcileProgression, reconcileUserProgression, releasePendingAchievementReviews, releasePendingTierReviews, resolveCurrentTier, resolveElapsedAnchor, resolveMetricValue, resolveTierProgress, reviewAchievementRequest, submitAchievementRequest, syncTierChangedLog, syncTierProgression, syncCompositeProgression, catalogRef, transitionLogData, transitionToId, validateRuleGroup, validateRuleNode, } from './services/achievement/index.js';
|
|
4
|
+
export { evaluateRules, evaluateRuleProgress, collectAchievementCompleteRefs, buildCatalogProgressGroups, buildUnlockRequirementLeaves, getUserProgress, loadUserProgressReviews, getMetricLeaderboard, grantAchievement, loadMetricDoc, recordEvent, recordLog, recordMetricChange, rebuildMetricBalance, rebuildMetricBalancesForUser, reconcileProgression, reconcileUserProgression, releasePendingAchievementReviews, releasePendingTierReviews, resolveCurrentTier, resolveElapsedAnchor, resolveMetricValue, resolveTierProgress, reviewAchievementRequest, submitAchievementRequest, syncTierChangedLog, syncTierProgression, syncCompositeProgression, catalogRef, transitionLogData, transitionToId, validateRuleGroup, validateRuleNode, } from './services/achievement/index.js';
|
|
5
5
|
export { DEFAULT_COLLECTION_BASE_SLUGS, DEFAULT_COLLECTION_PREFIX, DEFAULT_ADMIN_GROUP } from './collections/slugs.js';
|
|
6
6
|
export { DEFAULT_ME_ENDPOINT_PATH, DEFAULT_LEADERBOARD_ENDPOINT_PATH, DEFAULT_RECONCILE_ENDPOINT_PATH, } from './defaults.js';
|
|
7
7
|
export { collectionOf, slugOf } from './collections/helpers.js';
|
|
8
|
+
export { eachRuleLeaf } from './collections/hooks/eachRuleLeaf.js';
|
|
8
9
|
export { isAchievementPluginEnabled } from './enabled.js';
|
|
9
10
|
export { descriptionToPlaintext } from './fields/descriptionPlaintext.js';
|
|
10
11
|
export { plainTextToLexical } from './fields/plainTextToLexical.js';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Payload, PayloadRequest } from 'payload';
|
|
2
|
+
export declare const UNLOCK_REQUIREMENT_RELATION_FIELDS: {
|
|
3
|
+
readonly tier: "tiers";
|
|
4
|
+
readonly achievement: "achievements";
|
|
5
|
+
readonly metric: "metrics";
|
|
6
|
+
readonly eventType: "eventTypes";
|
|
7
|
+
};
|
|
8
|
+
export type UnlockRequirementCollectionKey = (typeof UNLOCK_REQUIREMENT_RELATION_FIELDS)[keyof typeof UNLOCK_REQUIREMENT_RELATION_FIELDS];
|
|
9
|
+
export type UnlockRequirementRelation = {
|
|
10
|
+
field: keyof typeof UNLOCK_REQUIREMENT_RELATION_FIELDS;
|
|
11
|
+
collectionKey: UnlockRequirementCollectionKey;
|
|
12
|
+
id?: string;
|
|
13
|
+
slug?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Structured unlock/eligibility leaf — catalog names only, no host UI copy.
|
|
18
|
+
* Hosts format `label` / `detail` from these fields.
|
|
19
|
+
*/
|
|
20
|
+
export type UnlockRequirementLeaf = {
|
|
21
|
+
id: string;
|
|
22
|
+
type: string;
|
|
23
|
+
met: boolean;
|
|
24
|
+
/** 0..1 from {@link evaluateRuleProgress}. */
|
|
25
|
+
progress: number;
|
|
26
|
+
relations: UnlockRequirementRelation[];
|
|
27
|
+
target?: number;
|
|
28
|
+
unit?: string;
|
|
29
|
+
since?: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Walk every leaf in a rule tree, evaluate met/progress, and return catalog-backed
|
|
33
|
+
* subjects. Payload rows often carry empty `rules: []` + default combinator — those
|
|
34
|
+
* are still leaves ({@link eachRuleLeaf}).
|
|
35
|
+
*
|
|
36
|
+
* `achievement-complete` is omitted by default so hosts can keep those on catalog groups.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildUnlockRequirementLeaves(args: {
|
|
39
|
+
payload: Payload;
|
|
40
|
+
req?: PayloadRequest;
|
|
41
|
+
userId: string;
|
|
42
|
+
scopeId: string | null;
|
|
43
|
+
rules: unknown;
|
|
44
|
+
ladderRank?: number | null;
|
|
45
|
+
locale?: string;
|
|
46
|
+
includeAchievementComplete?: boolean;
|
|
47
|
+
}): Promise<UnlockRequirementLeaf[]>;
|
|
48
|
+
//# sourceMappingURL=buildUnlockRequirementLeaves.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildUnlockRequirementLeaves.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/buildUnlockRequirementLeaves.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAOtD,eAAO,MAAM,kCAAkC;;;;;CAKyC,CAAA;AAExF,MAAM,MAAM,8BAA8B,GACxC,CAAC,OAAO,kCAAkC,CAAC,CAAC,MAAM,OAAO,kCAAkC,CAAC,CAAA;AAE9F,MAAM,MAAM,yBAAyB,GAAG;IACtC,KAAK,EAAE,MAAM,OAAO,kCAAkC,CAAA;IACtD,aAAa,EAAE,8BAA8B,CAAA;IAC7C,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,OAAO,CAAA;IACZ,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,yBAAyB,EAAE,CAAA;IACtC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAqHD;;;;;;GAMG;AACH,wBAAsB,4BAA4B,CAAC,IAAI,EAAE;IACvD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,KAAK,EAAE,OAAO,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0BAA0B,CAAC,EAAE,OAAO,CAAA;CACrC,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC,CA8EnC"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { eachRuleLeaf } from '../../collections/hooks/eachRuleLeaf.js';
|
|
2
|
+
import { collectionOf } from '../../collections/helpers.js';
|
|
3
|
+
import { evaluateRuleProgress, evaluateRules } from './evaluateRules.js';
|
|
4
|
+
import { relationId } from './relationId.js';
|
|
5
|
+
export const UNLOCK_REQUIREMENT_RELATION_FIELDS = {
|
|
6
|
+
tier: 'tiers',
|
|
7
|
+
achievement: 'achievements',
|
|
8
|
+
metric: 'metrics',
|
|
9
|
+
eventType: 'eventTypes',
|
|
10
|
+
};
|
|
11
|
+
function relationName(value) {
|
|
12
|
+
if (value && typeof value === 'object' && 'name' in value) {
|
|
13
|
+
const name = value.name;
|
|
14
|
+
if (typeof name === 'string' && name)
|
|
15
|
+
return name;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function relationSlug(value) {
|
|
20
|
+
if (value && typeof value === 'object' && 'slug' in value) {
|
|
21
|
+
const slug = value.slug;
|
|
22
|
+
if (typeof slug === 'string' && slug)
|
|
23
|
+
return slug;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function slugField(rule, relationKey) {
|
|
28
|
+
const raw = rule[`${relationKey}Slug`];
|
|
29
|
+
return typeof raw === 'string' && raw ? raw : null;
|
|
30
|
+
}
|
|
31
|
+
function numericTarget(rule) {
|
|
32
|
+
for (const key of ['amount', 'minimum', 'count']) {
|
|
33
|
+
const value = rule[key];
|
|
34
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
async function resolveCatalogName(args) {
|
|
40
|
+
const named = relationName(args.value);
|
|
41
|
+
const slug = args.slug || relationSlug(args.value);
|
|
42
|
+
const id = relationId(args.value);
|
|
43
|
+
if (named) {
|
|
44
|
+
return {
|
|
45
|
+
...(id ? { id } : {}),
|
|
46
|
+
...(slug ? { slug } : {}),
|
|
47
|
+
name: named,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (slug) {
|
|
51
|
+
const cached = args.cache.get(`slug:${args.collectionKey}:${slug}`);
|
|
52
|
+
if (cached)
|
|
53
|
+
return { ...(id ? { id } : {}), slug, name: cached };
|
|
54
|
+
}
|
|
55
|
+
if (id) {
|
|
56
|
+
const cached = args.cache.get(`id:${args.collectionKey}:${id}`);
|
|
57
|
+
if (cached) {
|
|
58
|
+
return { id, ...(slug ? { slug } : {}), name: cached };
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const doc = (await args.payload.findByID({
|
|
62
|
+
collection: collectionOf(args.collectionKey),
|
|
63
|
+
id,
|
|
64
|
+
depth: 0,
|
|
65
|
+
overrideAccess: true,
|
|
66
|
+
req: args.req,
|
|
67
|
+
...(args.locale ? { locale: args.locale } : {}),
|
|
68
|
+
}));
|
|
69
|
+
const name = (typeof doc?.name === 'string' && doc.name) ||
|
|
70
|
+
(typeof doc?.slug === 'string' && doc.slug) ||
|
|
71
|
+
null;
|
|
72
|
+
if (name) {
|
|
73
|
+
args.cache.set(`id:${args.collectionKey}:${id}`, name);
|
|
74
|
+
if (doc?.slug)
|
|
75
|
+
args.cache.set(`slug:${args.collectionKey}:${doc.slug}`, name);
|
|
76
|
+
return { id, ...(doc?.slug ? { slug: doc.slug } : slug ? { slug } : {}), name };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* fall through */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (slug && !id) {
|
|
84
|
+
try {
|
|
85
|
+
const found = await args.payload.find({
|
|
86
|
+
collection: collectionOf(args.collectionKey),
|
|
87
|
+
depth: 0,
|
|
88
|
+
limit: 1,
|
|
89
|
+
overrideAccess: true,
|
|
90
|
+
req: args.req,
|
|
91
|
+
where: { slug: { equals: slug } },
|
|
92
|
+
...(args.locale ? { locale: args.locale } : {}),
|
|
93
|
+
});
|
|
94
|
+
const doc = found.docs[0];
|
|
95
|
+
const resolvedId = relationId(doc);
|
|
96
|
+
const name = (typeof doc?.name === 'string' && doc.name) ||
|
|
97
|
+
(typeof doc?.slug === 'string' && doc.slug) ||
|
|
98
|
+
slug;
|
|
99
|
+
if (resolvedId)
|
|
100
|
+
args.cache.set(`id:${args.collectionKey}:${resolvedId}`, name);
|
|
101
|
+
args.cache.set(`slug:${args.collectionKey}:${slug}`, name);
|
|
102
|
+
return { ...(resolvedId ? { id: resolvedId } : {}), slug, name };
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
/* fall through */
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
...(id ? { id } : {}),
|
|
110
|
+
...(slug ? { slug } : {}),
|
|
111
|
+
...(slug || id ? { name: slug || id || undefined } : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Walk every leaf in a rule tree, evaluate met/progress, and return catalog-backed
|
|
116
|
+
* subjects. Payload rows often carry empty `rules: []` + default combinator — those
|
|
117
|
+
* are still leaves ({@link eachRuleLeaf}).
|
|
118
|
+
*
|
|
119
|
+
* `achievement-complete` is omitted by default so hosts can keep those on catalog groups.
|
|
120
|
+
*/
|
|
121
|
+
export async function buildUnlockRequirementLeaves(args) {
|
|
122
|
+
const req = args.req ?? { payload: args.payload };
|
|
123
|
+
const cache = new Map();
|
|
124
|
+
const leaves = [];
|
|
125
|
+
eachRuleLeaf(args.rules, (rule) => {
|
|
126
|
+
const type = String(rule.type ?? '');
|
|
127
|
+
if (!type)
|
|
128
|
+
return;
|
|
129
|
+
if (type === 'achievement-complete' && !args.includeAchievementComplete)
|
|
130
|
+
return;
|
|
131
|
+
leaves.push(rule);
|
|
132
|
+
});
|
|
133
|
+
const out = [];
|
|
134
|
+
for (let index = 0; index < leaves.length; index += 1) {
|
|
135
|
+
const rule = leaves[index];
|
|
136
|
+
const type = String(rule.type);
|
|
137
|
+
const leafRules = [rule];
|
|
138
|
+
const met = await evaluateRules({
|
|
139
|
+
payload: args.payload,
|
|
140
|
+
req,
|
|
141
|
+
rules: leafRules,
|
|
142
|
+
userId: args.userId,
|
|
143
|
+
scopeId: args.scopeId,
|
|
144
|
+
ladderRank: args.ladderRank,
|
|
145
|
+
});
|
|
146
|
+
const progress = await evaluateRuleProgress({
|
|
147
|
+
payload: args.payload,
|
|
148
|
+
req,
|
|
149
|
+
rules: leafRules,
|
|
150
|
+
userId: args.userId,
|
|
151
|
+
scopeId: args.scopeId,
|
|
152
|
+
ladderRank: args.ladderRank,
|
|
153
|
+
});
|
|
154
|
+
const relations = [];
|
|
155
|
+
for (const [field, collectionKey] of Object.entries(UNLOCK_REQUIREMENT_RELATION_FIELDS)) {
|
|
156
|
+
const key = field;
|
|
157
|
+
if (rule[key] == null && !slugField(rule, key))
|
|
158
|
+
continue;
|
|
159
|
+
const resolved = await resolveCatalogName({
|
|
160
|
+
payload: args.payload,
|
|
161
|
+
req,
|
|
162
|
+
locale: args.locale,
|
|
163
|
+
collectionKey,
|
|
164
|
+
value: rule[key],
|
|
165
|
+
slug: slugField(rule, key),
|
|
166
|
+
cache,
|
|
167
|
+
});
|
|
168
|
+
if (!resolved.id && !resolved.slug && !resolved.name)
|
|
169
|
+
continue;
|
|
170
|
+
relations.push({ field: key, collectionKey, ...resolved });
|
|
171
|
+
}
|
|
172
|
+
const target = numericTarget(rule);
|
|
173
|
+
const unit = typeof rule.unit === 'string' && rule.unit ? rule.unit : undefined;
|
|
174
|
+
// `since` / `unit` only matter when a duration/count target is set (defaults pollute other leaves).
|
|
175
|
+
const since = target != null && typeof rule.since === 'string' && rule.since ? rule.since : undefined;
|
|
176
|
+
const subjectKey = relations
|
|
177
|
+
.map((row) => row.name || row.slug || row.id)
|
|
178
|
+
.filter(Boolean)
|
|
179
|
+
.join(':') || 'leaf';
|
|
180
|
+
out.push({
|
|
181
|
+
id: `${type}:${index}:${subjectKey}`,
|
|
182
|
+
type,
|
|
183
|
+
met,
|
|
184
|
+
progress,
|
|
185
|
+
relations,
|
|
186
|
+
...(target != null ? { target } : {}),
|
|
187
|
+
...(unit ? { unit } : {}),
|
|
188
|
+
...(since ? { since } : {}),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PaginatedDocs, Payload, PayloadRequest } from 'payload';
|
|
2
|
+
import { type UserProgressReviews } from './loadUserProgressReviews.js';
|
|
2
3
|
/** Catalog slice — `description` matches the collection (Lexical JSON, or legacy string). */
|
|
3
4
|
type LeanCatalog = {
|
|
4
5
|
id: string;
|
|
@@ -32,10 +33,15 @@ declare function leanRequest(doc: Record<string, unknown>): {
|
|
|
32
33
|
updatedAt: {} | null;
|
|
33
34
|
createdAt: {} | null;
|
|
34
35
|
};
|
|
36
|
+
export type GetUserProgressInclude = {
|
|
37
|
+
/** Pending achievement keys + open/rejected tier requests. */
|
|
38
|
+
reviews?: boolean;
|
|
39
|
+
};
|
|
35
40
|
export type UserProgressMeResponse = PaginatedDocs<ReturnType<typeof leanGrant>> & {
|
|
36
41
|
/** Ladder-derived current tier(s). */
|
|
37
42
|
tiers: LeanTier[];
|
|
38
43
|
requests: ReturnType<typeof leanRequest>[];
|
|
44
|
+
reviews?: UserProgressReviews;
|
|
39
45
|
};
|
|
40
46
|
/**
|
|
41
47
|
* Current-user snapshot. Top-level Payload `find` shape for **grants**;
|
|
@@ -49,6 +55,7 @@ export declare function getUserProgress(args: {
|
|
|
49
55
|
paging?: GetUserProgressPaging;
|
|
50
56
|
/** Content locale for localized achievement / tier names (falls back to `req.locale`). */
|
|
51
57
|
locale?: string;
|
|
58
|
+
include?: GetUserProgressInclude;
|
|
52
59
|
}): Promise<UserProgressMeResponse>;
|
|
53
60
|
export {};
|
|
54
61
|
//# sourceMappingURL=getUserProgress.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"getUserProgress.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/getUserProgress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"getUserProgress.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/getUserProgress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAGrE,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,8BAA8B,CAAA;AAKhG,6FAA6F;AAC7F,KAAK,WAAW,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,CAAA;AACtF,KAAK,QAAQ,GAAG,WAAW,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAA;AAExE,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAmBD,iBAAS,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;;;EAU9C;AAED,iBAAS,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;;;EAShD;AAoBD,MAAM,MAAM,sBAAsB,GAAG;IACnC,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG,aAAa,CAAC,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,GAAG;IACjF,sCAAsC;IACtC,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,QAAQ,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,EAAE,CAAA;IAC1C,OAAO,CAAC,EAAE,mBAAmB,CAAA;CAC9B,CAAA;AAkED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,CAAC,EAAE,qBAAqB,CAAA;IAC9B,0FAA0F;IAC1F,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,sBAAsB,CAAA;CACjC,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA0DlC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { collectionOf } from '../../collections/helpers.js';
|
|
2
|
+
import { loadUserProgressReviews } from './loadUserProgressReviews.js';
|
|
2
3
|
import { relationId } from './relationId.js';
|
|
3
4
|
import { resolveCurrentTier } from './resolveCurrentTier.js';
|
|
4
5
|
import { userScopeWhere } from './where.js';
|
|
@@ -125,7 +126,7 @@ export async function getUserProgress(args) {
|
|
|
125
126
|
const page = args.paging?.page ?? 1;
|
|
126
127
|
const requestsLimit = args.paging?.requestsLimit ?? 20;
|
|
127
128
|
const locale = args.locale ?? (args.req?.locale || undefined);
|
|
128
|
-
const [grants, requests, tiers] = await Promise.all([
|
|
129
|
+
const [grants, requests, tiers, reviews] = await Promise.all([
|
|
129
130
|
args.payload.find({
|
|
130
131
|
collection: collectionOf('grants'),
|
|
131
132
|
depth: 1,
|
|
@@ -155,10 +156,19 @@ export async function getUserProgress(args) {
|
|
|
155
156
|
scopeId,
|
|
156
157
|
locale,
|
|
157
158
|
}),
|
|
159
|
+
args.include?.reviews
|
|
160
|
+
? loadUserProgressReviews({
|
|
161
|
+
payload: args.payload,
|
|
162
|
+
req: args.req,
|
|
163
|
+
userId: args.userId,
|
|
164
|
+
scopeId,
|
|
165
|
+
})
|
|
166
|
+
: Promise.resolve(undefined),
|
|
158
167
|
]);
|
|
159
168
|
return {
|
|
160
169
|
...mapPaginatedDocs(grants, (doc) => leanGrant(doc)),
|
|
161
170
|
tiers,
|
|
162
171
|
requests: requests.docs.map((doc) => leanRequest(doc)),
|
|
172
|
+
...(reviews ? { reviews } : {}),
|
|
163
173
|
};
|
|
164
174
|
}
|
|
@@ -2,7 +2,12 @@ export { evaluateRules, evaluateRuleProgress, normalizeRuleGroup, collectAchieve
|
|
|
2
2
|
export type { RuleGroup, AchievementCompleteRef } from './evaluateRules.js';
|
|
3
3
|
export { buildCatalogProgressGroups } from './buildCatalogProgressGroups.js';
|
|
4
4
|
export type { CatalogProgressGroup, CatalogProgressItem } from './buildCatalogProgressGroups.js';
|
|
5
|
+
export { buildUnlockRequirementLeaves } from './buildUnlockRequirementLeaves.js';
|
|
6
|
+
export type { UnlockRequirementCollectionKey, UnlockRequirementLeaf, UnlockRequirementRelation, } from './buildUnlockRequirementLeaves.js';
|
|
5
7
|
export { getUserProgress } from './getUserProgress.js';
|
|
8
|
+
export type { GetUserProgressInclude, GetUserProgressPaging, UserProgressMeResponse } from './getUserProgress.js';
|
|
9
|
+
export { loadUserProgressReviews } from './loadUserProgressReviews.js';
|
|
10
|
+
export type { UserProgressReviews, UserProgressTierRequest } from './loadUserProgressReviews.js';
|
|
6
11
|
export { getMetricLeaderboard } from './getMetricLeaderboard.js';
|
|
7
12
|
export type { MetricLeaderboardResult, MetricLeaderboardRow } from './getMetricLeaderboard.js';
|
|
8
13
|
export { applyMetricBalanceDelta, findMetricBalance, metricBalanceKey, rebuildMetricBalance, rebuildMetricBalancesForUser, } from './metricBalances.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAA;AAC5H,YAAY,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAA;AAC5E,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAA;AAChG,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,YAAY,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAC9F,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,4BAA4B,GAC7B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAC3E,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACjG,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,YAAY,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAClE,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAC1F,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AACxE,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACxE,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAA;AAC/E,YAAY,EAAE,wBAAwB,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAA;AAC1F,OAAO,EACL,wBAAwB,EACxB,gCAAgC,EAChC,yBAAyB,GAC1B,MAAM,wBAAwB,CAAA;AAC/B,YAAY,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AACrE,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAA;AAC5H,YAAY,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAA;AAC5E,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAA;AAChG,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAA;AAChF,YAAY,EACV,8BAA8B,EAC9B,qBAAqB,EACrB,yBAAyB,GAC1B,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AACjH,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AACtE,YAAY,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AAChG,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,YAAY,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAC9F,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,4BAA4B,GAC7B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAC3E,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACjG,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,YAAY,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAClE,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAC1F,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AACxE,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACxE,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAA;AAC/E,YAAY,EAAE,wBAAwB,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAA;AAC1F,OAAO,EACL,wBAAwB,EACxB,gCAAgC,EAChC,yBAAyB,GAC1B,MAAM,wBAAwB,CAAA;AAC/B,YAAY,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AACrE,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { evaluateRules, evaluateRuleProgress, normalizeRuleGroup, collectAchievementCompleteRefs } from './evaluateRules.js';
|
|
2
2
|
export { buildCatalogProgressGroups } from './buildCatalogProgressGroups.js';
|
|
3
|
+
export { buildUnlockRequirementLeaves } from './buildUnlockRequirementLeaves.js';
|
|
3
4
|
export { getUserProgress } from './getUserProgress.js';
|
|
5
|
+
export { loadUserProgressReviews } from './loadUserProgressReviews.js';
|
|
4
6
|
export { getMetricLeaderboard } from './getMetricLeaderboard.js';
|
|
5
7
|
export { applyMetricBalanceDelta, findMetricBalance, metricBalanceKey, rebuildMetricBalance, rebuildMetricBalancesForUser, } from './metricBalances.js';
|
|
6
8
|
export { resolveMetricValue, loadMetricDoc } from './resolveMetricValue.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Payload, PayloadRequest } from 'payload';
|
|
2
|
+
export type UserProgressTierRequest = {
|
|
3
|
+
id: string;
|
|
4
|
+
status: 'pending' | 'rejected';
|
|
5
|
+
tierId: string | null;
|
|
6
|
+
};
|
|
7
|
+
export type UserProgressReviews = {
|
|
8
|
+
/** Achievement definition ids and slugs with a pending request. */
|
|
9
|
+
pendingAchievementKeys: string[];
|
|
10
|
+
/** Open or rejected tier requests (host decides which badge wins). */
|
|
11
|
+
tierRequests: UserProgressTierRequest[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Review-queue snapshot for member UI (pending achievement keys + open/rejected tier requests).
|
|
15
|
+
* Does not decide ready/rejected presentation — hosts apply ladder context themselves.
|
|
16
|
+
*/
|
|
17
|
+
export declare function loadUserProgressReviews(args: {
|
|
18
|
+
payload: Payload;
|
|
19
|
+
req?: PayloadRequest;
|
|
20
|
+
userId: string;
|
|
21
|
+
scopeId?: string | null;
|
|
22
|
+
}): Promise<UserProgressReviews>;
|
|
23
|
+
//# sourceMappingURL=loadUserProgressReviews.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loadUserProgressReviews.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/loadUserProgressReviews.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAS,MAAM,SAAS,CAAA;AAQ7D,MAAM,MAAM,uBAAuB,GAAG;IACpC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,SAAS,GAAG,UAAU,CAAA;IAC9B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,mEAAmE;IACnE,sBAAsB,EAAE,MAAM,EAAE,CAAA;IAChC,sEAAsE;IACtE,YAAY,EAAE,uBAAuB,EAAE,CAAA;CACxC,CAAA;AAyBD;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE;IAClD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA+C/B"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { collectionOf } from '../../collections/helpers.js';
|
|
2
|
+
import { relationId } from './relationId.js';
|
|
3
|
+
const PENDING_ACHIEVEMENT_REQUESTS_LIMIT = 200;
|
|
4
|
+
const TIER_REQUESTS_LIMIT = 50;
|
|
5
|
+
function relationSlug(value) {
|
|
6
|
+
if (value && typeof value === 'object' && 'slug' in value) {
|
|
7
|
+
const slug = value.slug;
|
|
8
|
+
if (typeof slug === 'string' && slug)
|
|
9
|
+
return slug;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function pendingAchievementWhere(userId, scopeId) {
|
|
14
|
+
const clauses = [{ user: { equals: userId } }, { status: { equals: 'pending' } }];
|
|
15
|
+
if (scopeId)
|
|
16
|
+
clauses.push({ scope: { equals: scopeId } });
|
|
17
|
+
return { and: clauses };
|
|
18
|
+
}
|
|
19
|
+
function openOrRejectedTierRequestWhere(userId, scopeId) {
|
|
20
|
+
const clauses = [
|
|
21
|
+
{ user: { equals: userId } },
|
|
22
|
+
{ status: { in: ['pending', 'rejected'] } },
|
|
23
|
+
];
|
|
24
|
+
if (scopeId)
|
|
25
|
+
clauses.push({ scope: { equals: scopeId } });
|
|
26
|
+
return { and: clauses };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Review-queue snapshot for member UI (pending achievement keys + open/rejected tier requests).
|
|
30
|
+
* Does not decide ready/rejected presentation — hosts apply ladder context themselves.
|
|
31
|
+
*/
|
|
32
|
+
export async function loadUserProgressReviews(args) {
|
|
33
|
+
const scopeId = args.scopeId ?? null;
|
|
34
|
+
const [pendingAchievementRequests, tierRequests] = await Promise.all([
|
|
35
|
+
args.payload.find({
|
|
36
|
+
collection: collectionOf('achievementRequests'),
|
|
37
|
+
depth: 1,
|
|
38
|
+
limit: PENDING_ACHIEVEMENT_REQUESTS_LIMIT,
|
|
39
|
+
pagination: false,
|
|
40
|
+
overrideAccess: true,
|
|
41
|
+
req: args.req,
|
|
42
|
+
where: pendingAchievementWhere(args.userId, scopeId),
|
|
43
|
+
}),
|
|
44
|
+
scopeId
|
|
45
|
+
? args.payload.find({
|
|
46
|
+
collection: collectionOf('tierRequests'),
|
|
47
|
+
depth: 0,
|
|
48
|
+
limit: TIER_REQUESTS_LIMIT,
|
|
49
|
+
pagination: false,
|
|
50
|
+
overrideAccess: true,
|
|
51
|
+
req: args.req,
|
|
52
|
+
where: openOrRejectedTierRequestWhere(args.userId, scopeId),
|
|
53
|
+
})
|
|
54
|
+
: Promise.resolve({ docs: [] }),
|
|
55
|
+
]);
|
|
56
|
+
const pendingAchievementKeys = new Set();
|
|
57
|
+
for (const doc of pendingAchievementRequests.docs) {
|
|
58
|
+
const id = relationId(doc.achievement);
|
|
59
|
+
const slug = relationSlug(doc.achievement);
|
|
60
|
+
if (id)
|
|
61
|
+
pendingAchievementKeys.add(id);
|
|
62
|
+
if (slug)
|
|
63
|
+
pendingAchievementKeys.add(slug);
|
|
64
|
+
}
|
|
65
|
+
const reviews = [];
|
|
66
|
+
for (const doc of tierRequests.docs) {
|
|
67
|
+
const status = doc.status === 'rejected' ? 'rejected' : doc.status === 'pending' ? 'pending' : null;
|
|
68
|
+
if (!status)
|
|
69
|
+
continue;
|
|
70
|
+
const id = typeof doc.id === 'string' && doc.id ? doc.id : relationId(doc.id);
|
|
71
|
+
if (!id)
|
|
72
|
+
continue;
|
|
73
|
+
reviews.push({ id, status, tierId: relationId(doc.tier) });
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
pendingAchievementKeys: [...pendingAchievementKeys],
|
|
77
|
+
tierRequests: reviews,
|
|
78
|
+
};
|
|
79
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dappermountain/payload-plugin-achievements",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.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",
|