@dappermountain/payload-plugin-achievements 0.2.6 → 0.2.8

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.
Files changed (74) hide show
  1. package/README.md +34 -8
  2. package/dist/catalog.d.ts +1 -0
  3. package/dist/catalog.d.ts.map +1 -1
  4. package/dist/catalog.js +1 -1
  5. package/dist/cli/reconcile.d.ts.map +1 -1
  6. package/dist/cli/reconcile.js +1 -0
  7. package/dist/collections/MetricBalances/access/index.d.ts +4 -0
  8. package/dist/collections/MetricBalances/access/index.d.ts.map +1 -0
  9. package/dist/collections/MetricBalances/access/index.js +9 -0
  10. package/dist/collections/MetricBalances/hooks/index.d.ts +3 -0
  11. package/dist/collections/MetricBalances/hooks/index.d.ts.map +1 -0
  12. package/dist/collections/MetricBalances/hooks/index.js +1 -0
  13. package/dist/collections/MetricBalances/index.d.ts +3 -0
  14. package/dist/collections/MetricBalances/index.d.ts.map +1 -0
  15. package/dist/collections/MetricBalances/index.js +48 -0
  16. package/dist/collections/Metrics/index.d.ts.map +1 -1
  17. package/dist/collections/Metrics/index.js +68 -3
  18. package/dist/collections/fields/ruleFields.d.ts.map +1 -1
  19. package/dist/collections/fields/ruleFields.js +48 -2
  20. package/dist/collections/hooks/preventCatalogDelete.d.ts.map +1 -1
  21. package/dist/collections/hooks/preventCatalogDelete.js +10 -1
  22. package/dist/collections/index.d.ts.map +1 -1
  23. package/dist/collections/index.js +2 -0
  24. package/dist/collections/slugs.d.ts.map +1 -1
  25. package/dist/collections/slugs.js +1 -0
  26. package/dist/defaults.d.ts +4 -0
  27. package/dist/defaults.d.ts.map +1 -1
  28. package/dist/defaults.js +10 -0
  29. package/dist/endpoints/index.d.ts.map +1 -1
  30. package/dist/endpoints/index.js +4 -0
  31. package/dist/endpoints/leaderboard.d.ts +6 -0
  32. package/dist/endpoints/leaderboard.d.ts.map +1 -0
  33. package/dist/endpoints/leaderboard.js +59 -0
  34. package/dist/extensions/elapsedSinceOptions.d.ts +7 -0
  35. package/dist/extensions/elapsedSinceOptions.d.ts.map +1 -0
  36. package/dist/extensions/elapsedSinceOptions.js +16 -0
  37. package/dist/index.d.ts +4 -4
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +2 -2
  40. package/dist/plugin.d.ts.map +1 -1
  41. package/dist/plugin.js +9 -0
  42. package/dist/seed/index.d.ts +10 -0
  43. package/dist/seed/index.d.ts.map +1 -1
  44. package/dist/seed/index.js +14 -2
  45. package/dist/services/achievement/builtInRules.d.ts.map +1 -1
  46. package/dist/services/achievement/builtInRules.js +59 -21
  47. package/dist/services/achievement/duration.d.ts +6 -0
  48. package/dist/services/achievement/duration.d.ts.map +1 -0
  49. package/dist/services/achievement/duration.js +43 -0
  50. package/dist/services/achievement/getMetricLeaderboard.d.ts +30 -0
  51. package/dist/services/achievement/getMetricLeaderboard.d.ts.map +1 -0
  52. package/dist/services/achievement/getMetricLeaderboard.js +62 -0
  53. package/dist/services/achievement/index.d.ts +5 -0
  54. package/dist/services/achievement/index.d.ts.map +1 -1
  55. package/dist/services/achievement/index.js +4 -0
  56. package/dist/services/achievement/metricBalances.d.ts +42 -0
  57. package/dist/services/achievement/metricBalances.d.ts.map +1 -0
  58. package/dist/services/achievement/metricBalances.js +137 -0
  59. package/dist/services/achievement/reconcile.d.ts +1 -0
  60. package/dist/services/achievement/reconcile.d.ts.map +1 -1
  61. package/dist/services/achievement/reconcile.js +10 -0
  62. package/dist/services/achievement/recordEvent.d.ts.map +1 -1
  63. package/dist/services/achievement/recordEvent.js +22 -1
  64. package/dist/services/achievement/resolveElapsedAnchor.d.ts +15 -0
  65. package/dist/services/achievement/resolveElapsedAnchor.d.ts.map +1 -0
  66. package/dist/services/achievement/resolveElapsedAnchor.js +98 -0
  67. package/dist/services/achievement/resolveMetricValue.d.ts +32 -0
  68. package/dist/services/achievement/resolveMetricValue.d.ts.map +1 -0
  69. package/dist/services/achievement/resolveMetricValue.js +73 -0
  70. package/dist/services/achievement/validateRules.d.ts.map +1 -1
  71. package/dist/services/achievement/validateRules.js +19 -0
  72. package/dist/types.d.ts +26 -3
  73. package/dist/types.d.ts.map +1 -1
  74. package/package.json +1 -1
@@ -0,0 +1,59 @@
1
+ import { getUserId } from '../access/helpers/index.js';
2
+ import { getMetricLeaderboard } from '../services/achievement/getMetricLeaderboard.js';
3
+ function requestUrl(req) {
4
+ return req.url ? new URL(req.url, 'http://local') : null;
5
+ }
6
+ function scopeFromRequest(req) {
7
+ const url = requestUrl(req);
8
+ const raw = url?.searchParams.get('scope') ?? url?.searchParams.get('scopeId');
9
+ return raw && raw.length > 0 ? raw : null;
10
+ }
11
+ function positiveInt(raw, fallback) {
12
+ if (raw == null || raw === '')
13
+ return fallback;
14
+ const n = Number(raw);
15
+ if (!Number.isFinite(n) || n < 1)
16
+ return fallback;
17
+ return Math.floor(n);
18
+ }
19
+ /**
20
+ * `GET` ranked stored-metric balances. Query: `metric` (required), optional `scope` / `limit` / `page`.
21
+ */
22
+ export function buildLeaderboardEndpoint(path) {
23
+ return {
24
+ path,
25
+ method: 'get',
26
+ handler: async (req) => {
27
+ const userId = getUserId(req);
28
+ if (!userId) {
29
+ return Response.json({ errors: [{ message: 'Unauthorized' }] }, { status: 401 });
30
+ }
31
+ const params = requestUrl(req)?.searchParams;
32
+ const metric = params?.get('metric')?.trim();
33
+ if (!metric) {
34
+ return Response.json({ errors: [{ message: 'Query param `metric` is required.' }] }, { status: 400 });
35
+ }
36
+ try {
37
+ const body = await getMetricLeaderboard({
38
+ payload: req.payload,
39
+ req,
40
+ metric,
41
+ scopeId: scopeFromRequest(req),
42
+ limit: positiveInt(params?.get('limit') ?? null, 20),
43
+ page: positiveInt(params?.get('page') ?? null, 1),
44
+ });
45
+ return Response.json(body);
46
+ }
47
+ catch (error) {
48
+ const status = typeof error === 'object' &&
49
+ error &&
50
+ 'status' in error &&
51
+ typeof error.status === 'number'
52
+ ? error.status
53
+ : 500;
54
+ const message = error instanceof Error ? error.message : 'Leaderboard failed.';
55
+ return Response.json({ errors: [{ message }] }, { status });
56
+ }
57
+ },
58
+ };
59
+ }
@@ -0,0 +1,7 @@
1
+ /** Admin select options for elapsed `since` (built-ins + host `metricAnchors` keys). */
2
+ export declare function elapsedSinceSelectOptions(): Array<{
3
+ label: string;
4
+ value: string;
5
+ }>;
6
+ export declare function isBuiltinElapsedSince(since: string): boolean;
7
+ //# sourceMappingURL=elapsedSinceOptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elapsedSinceOptions.d.ts","sourceRoot":"","sources":["../../src/extensions/elapsedSinceOptions.ts"],"names":[],"mappings":"AAOA,wFAAwF;AACxF,wBAAgB,yBAAyB,IAAI,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAMnF;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE5D"}
@@ -0,0 +1,16 @@
1
+ import { getAchievementOptions } from '../options-store.js';
2
+ const BUILTIN_ELAPSED_SINCE_OPTIONS = [
3
+ { label: 'User created at', value: 'user-created-at' },
4
+ { label: 'First matching event log', value: 'first-event' },
5
+ ];
6
+ /** Admin select options for elapsed `since` (built-ins + host `metricAnchors` keys). */
7
+ export function elapsedSinceSelectOptions() {
8
+ const custom = Object.keys(getAchievementOptions().extensions?.metricAnchors ?? {});
9
+ return [
10
+ ...BUILTIN_ELAPSED_SINCE_OPTIONS.map((option) => ({ ...option })),
11
+ ...custom.map((key) => ({ label: key, value: key })),
12
+ ];
13
+ }
14
+ export function isBuiltinElapsedSince(since) {
15
+ return since === 'user-created-at' || since === 'first-event';
16
+ }
package/dist/index.d.ts CHANGED
@@ -3,11 +3,11 @@ 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, grantAchievement, recordEvent, recordLog, recordMetricChange, reconcileProgression, reconcileUserProgression, releasePendingAchievementReviews, releasePendingTierReviews, resolveCurrentTier, 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, ReconcileProgressionArgs, ReconcileProgressionResult, ReleaseReviewGateResult, } from './services/achievement/index.js';
8
- export type { BuiltInEventType, BuiltInRuleType, AchievementCollectionKey, AchievementCollectionOverride, AchievementCollectionsOptions, AchievementEndpointsOptions, AchievementPluginOptions, AchievementRuleType, AchievementScopeConfig, AchievementUsersOptions, } from './types.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';
8
+ export type { BuiltInEventType, BuiltInRuleType, AchievementCollectionKey, AchievementCollectionOverride, AchievementCollectionsOptions, AchievementEndpointsOptions, AchievementMetricAnchor, 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
- export { DEFAULT_ME_ENDPOINT_PATH, DEFAULT_RECONCILE_ENDPOINT_PATH } from './defaults.js';
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
12
  export { isAchievementPluginEnabled } from './enabled.js';
13
13
  export { descriptionToPlaintext } from './fields/descriptionPlaintext.js';
@@ -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,gBAAgB,EAChB,WAAW,EACX,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,wBAAwB,EACxB,gCAAgC,EAChC,yBAAyB,EACzB,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,wBAAwB,EACxB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,iCAAiC,CAAA;AACxC,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,wBAAwB,EACxB,6BAA6B,EAC7B,6BAA6B,EAC7B,2BAA2B,EAC3B,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,EAAE,wBAAwB,EAAE,+BAA+B,EAAE,MAAM,eAAe,CAAA;AACzF,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,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,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"}
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
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, grantAchievement, recordEvent, recordLog, recordMetricChange, reconcileProgression, reconcileUserProgression, releasePendingAchievementReviews, releasePendingTierReviews, resolveCurrentTier, resolveTierProgress, reviewAchievementRequest, submitAchievementRequest, syncTierChangedLog, syncTierProgression, syncCompositeProgression, catalogRef, transitionLogData, transitionToId, validateRuleGroup, validateRuleNode, } from './services/achievement/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';
5
5
  export { DEFAULT_COLLECTION_BASE_SLUGS, DEFAULT_COLLECTION_PREFIX, DEFAULT_ADMIN_GROUP } from './collections/slugs.js';
6
- export { DEFAULT_ME_ENDPOINT_PATH, DEFAULT_RECONCILE_ENDPOINT_PATH } from './defaults.js';
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
8
  export { isAchievementPluginEnabled } from './enabled.js';
9
9
  export { descriptionToPlaintext } from './fields/descriptionPlaintext.js';
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,MAAM,EAAE,MAAM,SAAS,CAAA;AAO7C,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAE1D;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAC3B,UAAS,wBAA6B,KAAG,MA0BzC,CAAA"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,MAAM,EAAE,MAAM,SAAS,CAAA;AAO7C,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAE1D;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAC3B,UAAS,wBAA6B,KAAG,MAmCzC,CAAA"}
package/dist/plugin.js CHANGED
@@ -23,9 +23,18 @@ export const achievementPlugin = (options = {}) => (config) => {
23
23
  fields: [...(collection.fields ?? []), usersAchievementsGroup(resolved)],
24
24
  };
25
25
  });
26
+ const incomingOnInit = config.onInit;
26
27
  return {
27
28
  ...config,
28
29
  collections: [...collections, ...achievementCollections],
29
30
  endpoints: [...(config.endpoints ?? []), ...buildAchievementEndpoints(resolved)],
31
+ onInit: async (payload) => {
32
+ if (incomingOnInit)
33
+ await incomingOnInit(payload);
34
+ if (resolved.seedSystemCatalog) {
35
+ const { seedAchievementCatalog } = await import('./seed/index.js');
36
+ await seedAchievementCatalog(payload);
37
+ }
38
+ },
30
39
  };
31
40
  };
@@ -43,6 +43,16 @@ export type AchievementSeedCatalogRow = {
43
43
  slug: string;
44
44
  scope?: string;
45
45
  system?: boolean;
46
+ /** Metrics — `stored` (default) or `computed`. */
47
+ kind?: 'stored' | 'computed';
48
+ /** Computed metrics — v1 only `elapsed`. */
49
+ compute?: 'elapsed';
50
+ /** Computed elapsed — unit of the exposed number. */
51
+ unit?: 'days' | 'hours' | 'minutes';
52
+ /** Computed elapsed — built-in or host `metricAnchors` key. */
53
+ since?: string;
54
+ /** Computed elapsed with `since: first-event` — event type slug. */
55
+ eventTypeSlug?: string;
46
56
  /** Event types only — when true, logging the event requires an actor. */
47
57
  requiresActor?: boolean;
48
58
  /** Event types only — when true, logging the event requires metric + change. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/seed/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAOtC,mGAAmG;AACnG,MAAM,MAAM,4BAA4B,GAAG,MAAM,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CAAE,CAAA;AAExG,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,4BAA4B,CAAA;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,4BAA4B,CAAA;IAC1C,sFAAsF;IACtF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qFAAqF;IACrF,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,8FAA8F;IAC9F,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;KAAE,CAAA;CACrG,CAAA;AAED,MAAM,MAAM,0BAA0B,GAAG;IACvC,IAAI,EAAE,4BAA4B,CAAA;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,4BAA4B,CAAA;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;KAAE,CAAA;IACzG,qGAAqG;IACrG,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;KAAE,CAAA;CACzG,CAAA;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,4BAA4B,CAAA;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,yEAAyE;IACzE,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,gFAAgF;IAChF,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,UAAU,CAAC,EAAE,yBAAyB,EAAE,CAAA;IACxC,OAAO,CAAC,EAAE,yBAAyB,EAAE,CAAA;IACrC,KAAK,CAAC,EAAE,mBAAmB,EAAE,CAAA;IAC7B,YAAY,CAAC,EAAE,0BAA0B,EAAE,CAAA;IAC3C,gEAAgE;IAChE,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAA;CAClC,CAAA;AAmMD,2FAA2F;AAC3F,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB5F;AAiCD;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqLnG"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/seed/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAOtC,mGAAmG;AACnG,MAAM,MAAM,4BAA4B,GAAG,MAAM,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CAAE,CAAA;AAExG,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,4BAA4B,CAAA;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,4BAA4B,CAAA;IAC1C,sFAAsF;IACtF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qFAAqF;IACrF,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,8FAA8F;IAC9F,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;KAAE,CAAA;CACrG,CAAA;AAED,MAAM,MAAM,0BAA0B,GAAG;IACvC,IAAI,EAAE,4BAA4B,CAAA;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,4BAA4B,CAAA;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;KAAE,CAAA;IACzG,qGAAqG;IACrG,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;KAAE,CAAA;CACzG,CAAA;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,4BAA4B,CAAA;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,kDAAkD;IAClD,IAAI,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAA;IAC5B,4CAA4C;IAC5C,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;IACnC,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,yEAAyE;IACzE,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,gFAAgF;IAChF,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,UAAU,CAAC,EAAE,yBAAyB,EAAE,CAAA;IACxC,OAAO,CAAC,EAAE,yBAAyB,EAAE,CAAA;IACrC,KAAK,CAAC,EAAE,mBAAmB,EAAE,CAAA;IAC7B,YAAY,CAAC,EAAE,0BAA0B,EAAE,CAAA;IAC3C,gEAAgE;IAChE,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAA;CAClC,CAAA;AAiND,2FAA2F;AAC3F,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB5F;AAiCD;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqLnG"}
@@ -53,6 +53,18 @@ async function findIdBySlug(payload, key, slug) {
53
53
  async function upsertCatalog(payload, key, row) {
54
54
  const existing = await findIdBySlug(payload, key, row.slug);
55
55
  const nameParts = localizedParts(row.name);
56
+ let metricComputed = {};
57
+ if (key === 'metrics') {
58
+ metricComputed = { kind: row.kind ?? 'stored' };
59
+ if (row.kind === 'computed') {
60
+ metricComputed.compute = row.compute ?? 'elapsed';
61
+ metricComputed.unit = row.unit ?? 'days';
62
+ metricComputed.since = row.since ?? 'user-created-at';
63
+ if (row.eventTypeSlug) {
64
+ metricComputed.eventType = await findIdBySlug(payload, 'eventTypes', row.eventTypeSlug);
65
+ }
66
+ }
67
+ }
56
68
  const data = {
57
69
  name: nameParts.en ?? row.slug,
58
70
  slug: row.slug,
@@ -63,7 +75,7 @@ async function upsertCatalog(payload, key, row) {
63
75
  requiresActor: row.requiresActor ?? false,
64
76
  requiresMetric: row.requiresMetric ?? false,
65
77
  }
66
- : {}),
78
+ : metricComputed),
67
79
  };
68
80
  let id = existing;
69
81
  if (existing) {
@@ -119,7 +131,7 @@ async function resolveRuleNode(payload, rule) {
119
131
  delete next.achievementSlug;
120
132
  }
121
133
  }
122
- if (next.type === 'event-count') {
134
+ if (next.type === 'event-count' || next.type === 'elapsed-since') {
123
135
  const slug = typeof next.eventTypeSlug === 'string'
124
136
  ? next.eventTypeSlug
125
137
  : typeof next.eventType === 'string'
@@ -1 +1 @@
1
- {"version":3,"file":"builtInRules.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/builtInRules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA2B,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AA2KlF,eAAO,MAAM,gBAAgB,EAAE,mBAAmB,EAiJjD,CAAA"}
1
+ {"version":3,"file":"builtInRules.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/builtInRules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA2B,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAuJlF,eAAO,MAAM,gBAAgB,EAAE,mBAAmB,EA+LjD,CAAA"}
@@ -1,6 +1,9 @@
1
1
  import { collectionOf } from '../../collections/helpers.js';
2
+ import { durationMs } from './duration.js';
2
3
  import { relationId } from './relationId.js';
3
- import { userScopeIncludingUnscopedWhere, userScopeWhere } from './where.js';
4
+ import { resolveElapsedAnchor } from './resolveElapsedAnchor.js';
5
+ import { resolveMetricValue } from './resolveMetricValue.js';
6
+ import { userScopeIncludingUnscopedWhere } from './where.js';
4
7
  function clamp01(value) {
5
8
  if (!Number.isFinite(value))
6
9
  return 0;
@@ -22,22 +25,6 @@ async function resolveMetricId(args) {
22
25
  });
23
26
  return relationId(found.docs[0]);
24
27
  }
25
- async function metricValue(args) {
26
- const logs = await args.payload.find({
27
- collection: collectionOf('logs'),
28
- depth: 0,
29
- limit: 1000,
30
- pagination: false,
31
- overrideAccess: true,
32
- where: {
33
- and: [userScopeWhere(args.userId, args.scopeId), { metric: { equals: args.metricId } }],
34
- },
35
- });
36
- return logs.docs.reduce((sum, doc) => {
37
- const change = doc.change;
38
- return sum + (typeof change === 'number' && Number.isFinite(change) ? change : 0);
39
- }, 0);
40
- }
41
28
  async function resolveEventTypeId(args) {
42
29
  let eventTypeId = relationId(args.rule.eventType);
43
30
  if (eventTypeId) {
@@ -225,22 +212,34 @@ export const builtInRuleTypes = [
225
212
  },
226
213
  {
227
214
  type: 'metric-minimum',
228
- async evaluate({ payload, rule, userId, scopeId }) {
215
+ async evaluate({ payload, req, rule, userId, scopeId }) {
229
216
  const resolvedId = await resolveMetricId({ payload, rule });
230
217
  if (!resolvedId)
231
218
  return false;
232
219
  const minimum = Number(rule.minimum ?? 0);
233
- const value = await metricValue({ payload, userId, scopeId, metricId: resolvedId });
220
+ const value = await resolveMetricValue({
221
+ payload,
222
+ req,
223
+ metric: resolvedId,
224
+ userId,
225
+ scopeId,
226
+ });
234
227
  return value >= minimum;
235
228
  },
236
- async progress({ payload, rule, userId, scopeId }) {
229
+ async progress({ payload, req, rule, userId, scopeId }) {
237
230
  const resolvedId = await resolveMetricId({ payload, rule });
238
231
  if (!resolvedId)
239
232
  return 0;
240
233
  const minimum = Number(rule.minimum ?? 0);
241
234
  if (!(minimum > 0))
242
235
  return 1;
243
- const value = await metricValue({ payload, userId, scopeId, metricId: resolvedId });
236
+ const value = await resolveMetricValue({
237
+ payload,
238
+ req,
239
+ metric: resolvedId,
240
+ userId,
241
+ scopeId,
242
+ });
244
243
  return clamp01(value / minimum);
245
244
  },
246
245
  },
@@ -277,4 +276,43 @@ export const builtInRuleTypes = [
277
276
  return clamp01(result.totalDocs / needed);
278
277
  },
279
278
  },
279
+ {
280
+ type: 'elapsed-since',
281
+ async evaluate({ payload, req, rule, userId, scopeId }) {
282
+ const needed = durationMs(Number(rule.amount ?? NaN), rule.unit);
283
+ if (needed == null)
284
+ return false;
285
+ const anchor = await resolveElapsedAnchor({
286
+ payload,
287
+ req,
288
+ since: rule.since,
289
+ eventType: rule.eventType,
290
+ eventTypeSlug: rule.eventTypeSlug,
291
+ userId,
292
+ scopeId,
293
+ });
294
+ if (!anchor)
295
+ return false;
296
+ return Date.now() - anchor.getTime() >= needed;
297
+ },
298
+ async progress({ payload, req, rule, userId, scopeId }) {
299
+ const needed = durationMs(Number(rule.amount ?? NaN), rule.unit);
300
+ if (needed == null)
301
+ return 0;
302
+ if (!(needed > 0))
303
+ return 1;
304
+ const anchor = await resolveElapsedAnchor({
305
+ payload,
306
+ req,
307
+ since: rule.since,
308
+ eventType: rule.eventType,
309
+ eventTypeSlug: rule.eventTypeSlug,
310
+ userId,
311
+ scopeId,
312
+ });
313
+ if (!anchor)
314
+ return 0;
315
+ return clamp01((Date.now() - anchor.getTime()) / needed);
316
+ },
317
+ },
280
318
  ];
@@ -0,0 +1,6 @@
1
+ export declare function durationMs(amount: number, unit: unknown): number | null;
2
+ export declare function unitMs(unit: unknown): number | null;
3
+ export declare function parseDate(value: unknown): Date | null;
4
+ /** Whole units elapsed from anchor to `now` (floor). */
5
+ export declare function elapsedUnits(anchor: Date, unit: unknown, now?: Date): number | null;
6
+ //# sourceMappingURL=duration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duration.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/duration.ts"],"names":[],"mappings":"AAIA,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAevE;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAEnD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAOrD;AAED,wDAAwD;AACxD,wBAAgB,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,GAAE,IAAiB,GAAG,MAAM,GAAG,IAAI,CAM/F"}
@@ -0,0 +1,43 @@
1
+ const MS_PER_MINUTE = 60_000;
2
+ const MS_PER_HOUR = 60 * MS_PER_MINUTE;
3
+ const MS_PER_DAY = 24 * MS_PER_HOUR;
4
+ export function durationMs(amount, unit) {
5
+ if (!(typeof amount === 'number') || !Number.isFinite(amount) || amount < 0)
6
+ return null;
7
+ switch (unit) {
8
+ case 'minutes':
9
+ return amount * MS_PER_MINUTE;
10
+ case 'hours':
11
+ return amount * MS_PER_HOUR;
12
+ case 'days':
13
+ case undefined:
14
+ case null:
15
+ case '':
16
+ return amount * MS_PER_DAY;
17
+ default:
18
+ return null;
19
+ }
20
+ }
21
+ export function unitMs(unit) {
22
+ return durationMs(1, unit ?? 'days');
23
+ }
24
+ export function parseDate(value) {
25
+ if (value instanceof Date && !Number.isNaN(value.getTime()))
26
+ return value;
27
+ if (typeof value === 'string' || typeof value === 'number') {
28
+ const date = new Date(value);
29
+ if (!Number.isNaN(date.getTime()))
30
+ return date;
31
+ }
32
+ return null;
33
+ }
34
+ /** Whole units elapsed from anchor to `now` (floor). */
35
+ export function elapsedUnits(anchor, unit, now = new Date()) {
36
+ const ms = unitMs(unit);
37
+ if (ms == null || ms <= 0)
38
+ return null;
39
+ const delta = now.getTime() - anchor.getTime();
40
+ if (!Number.isFinite(delta) || delta < 0)
41
+ return 0;
42
+ return Math.floor(delta / ms);
43
+ }
@@ -0,0 +1,30 @@
1
+ import type { Payload, PayloadRequest } from 'payload';
2
+ export type MetricLeaderboardRow = {
3
+ user: string;
4
+ value: number;
5
+ rank: number;
6
+ };
7
+ export type MetricLeaderboardResult = {
8
+ docs: MetricLeaderboardRow[];
9
+ totalDocs: number;
10
+ page: number;
11
+ limit: number;
12
+ totalPages: number;
13
+ metric: {
14
+ id: string;
15
+ slug?: string;
16
+ };
17
+ scopeId: string | null;
18
+ };
19
+ /**
20
+ * Ranked stored-metric balances for a scope (or unscoped board when `scopeId` is null).
21
+ */
22
+ export declare function getMetricLeaderboard(args: {
23
+ payload: Payload;
24
+ req?: PayloadRequest;
25
+ metric: string;
26
+ scopeId?: string | null;
27
+ limit?: number;
28
+ page?: number;
29
+ }): Promise<MetricLeaderboardResult>;
30
+ //# sourceMappingURL=getMetricLeaderboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getMetricLeaderboard.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/getMetricLeaderboard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAS,MAAM,SAAS,CAAA;AAQ7D,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,oBAAoB,EAAE,CAAA;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACrC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB,CAAA;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE;IAC/C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAyDnC"}
@@ -0,0 +1,62 @@
1
+ import { APIError } from 'payload';
2
+ import { collectionOf } from '../../collections/helpers.js';
3
+ import { getAchievementOptions } from '../../options-store.js';
4
+ import { loadMetricDoc } from './resolveMetricValue.js';
5
+ import { relationId } from './relationId.js';
6
+ /**
7
+ * Ranked stored-metric balances for a scope (or unscoped board when `scopeId` is null).
8
+ */
9
+ export async function getMetricLeaderboard(args) {
10
+ const metric = await loadMetricDoc({
11
+ payload: args.payload,
12
+ req: args.req,
13
+ metric: args.metric,
14
+ });
15
+ if (!metric) {
16
+ throw new APIError('Metric not found.', 404);
17
+ }
18
+ if ((metric.kind ?? 'stored') === 'computed') {
19
+ throw new APIError('Leaderboards are only available for stored metrics.', 400);
20
+ }
21
+ const limit = Math.min(Math.max(args.limit ?? 20, 1), 100);
22
+ const page = Math.max(args.page ?? 1, 1);
23
+ const scopeId = args.scopeId ?? null;
24
+ const scopeConfigured = Boolean(getAchievementOptions().scope?.collection);
25
+ const clauses = [{ metric: { equals: metric.id } }];
26
+ if (scopeId) {
27
+ clauses.push({ scope: { equals: scopeId } });
28
+ }
29
+ else if (scopeConfigured) {
30
+ clauses.push({
31
+ or: [{ scope: { exists: false } }, { scope: { equals: null } }],
32
+ });
33
+ }
34
+ const result = await args.payload.find({
35
+ collection: collectionOf('metricBalances'),
36
+ depth: 0,
37
+ limit,
38
+ page,
39
+ overrideAccess: true,
40
+ req: args.req,
41
+ sort: '-value,id',
42
+ where: { and: clauses },
43
+ });
44
+ const offset = (page - 1) * limit;
45
+ const docs = result.docs.map((doc, index) => {
46
+ const row = doc;
47
+ return {
48
+ user: relationId(row.user) ?? String(row.user ?? ''),
49
+ value: typeof row.value === 'number' && Number.isFinite(row.value) ? row.value : 0,
50
+ rank: offset + index + 1,
51
+ };
52
+ });
53
+ return {
54
+ docs,
55
+ totalDocs: result.totalDocs,
56
+ page: result.page ?? page,
57
+ limit,
58
+ totalPages: result.totalPages ?? Math.max(1, Math.ceil(result.totalDocs / limit)),
59
+ metric: { id: metric.id, slug: metric.slug },
60
+ scopeId,
61
+ };
62
+ }
@@ -3,6 +3,11 @@ export type { RuleGroup, AchievementCompleteRef } from './evaluateRules.js';
3
3
  export { buildCatalogProgressGroups } from './buildCatalogProgressGroups.js';
4
4
  export type { CatalogProgressGroup, CatalogProgressItem } from './buildCatalogProgressGroups.js';
5
5
  export { getUserProgress } from './getUserProgress.js';
6
+ export { getMetricLeaderboard } from './getMetricLeaderboard.js';
7
+ export type { MetricLeaderboardResult, MetricLeaderboardRow } from './getMetricLeaderboard.js';
8
+ export { applyMetricBalanceDelta, findMetricBalance, metricBalanceKey, rebuildMetricBalance, rebuildMetricBalancesForUser, } from './metricBalances.js';
9
+ export { resolveMetricValue, loadMetricDoc } from './resolveMetricValue.js';
10
+ export { resolveElapsedAnchor } from './resolveElapsedAnchor.js';
6
11
  export { recordEvent, recordLog, recordMetricChange, syncTierChangedLog } from './recordEvent.js';
7
12
  export { resolveCurrentTier } from './resolveCurrentTier.js';
8
13
  export type { ResolvedTier } from './resolveCurrentTier.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,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,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,6 +1,10 @@
1
1
  export { evaluateRules, evaluateRuleProgress, normalizeRuleGroup, collectAchievementCompleteRefs } from './evaluateRules.js';
2
2
  export { buildCatalogProgressGroups } from './buildCatalogProgressGroups.js';
3
3
  export { getUserProgress } from './getUserProgress.js';
4
+ export { getMetricLeaderboard } from './getMetricLeaderboard.js';
5
+ export { applyMetricBalanceDelta, findMetricBalance, metricBalanceKey, rebuildMetricBalance, rebuildMetricBalancesForUser, } from './metricBalances.js';
6
+ export { resolveMetricValue, loadMetricDoc } from './resolveMetricValue.js';
7
+ export { resolveElapsedAnchor } from './resolveElapsedAnchor.js';
4
8
  export { recordEvent, recordLog, recordMetricChange, syncTierChangedLog } from './recordEvent.js';
5
9
  export { resolveCurrentTier } from './resolveCurrentTier.js';
6
10
  export { resolveTierProgress } from './resolveTierProgress.js';
@@ -0,0 +1,42 @@
1
+ import type { Payload, PayloadRequest } from 'payload';
2
+ export declare function metricBalanceKey(userId: string, scopeId: string | null, metricId: string): string;
3
+ export declare function findMetricBalance(args: {
4
+ payload: Payload;
5
+ req?: PayloadRequest;
6
+ userId: string;
7
+ scopeId: string | null;
8
+ metricId: string;
9
+ }): Promise<{
10
+ id: string;
11
+ value: number;
12
+ } | null>;
13
+ export declare function applyMetricBalanceDelta(args: {
14
+ payload: Payload;
15
+ req?: PayloadRequest;
16
+ userId: string;
17
+ scopeId: string | null;
18
+ metricId: string;
19
+ change: number;
20
+ }): Promise<{
21
+ id: string;
22
+ value: number;
23
+ }>;
24
+ /** Sum metric.delta log changes and upsert the balance projection. */
25
+ export declare function rebuildMetricBalance(args: {
26
+ payload: Payload;
27
+ req?: PayloadRequest;
28
+ userId: string;
29
+ scopeId?: string | null;
30
+ metricId: string;
31
+ }): Promise<{
32
+ id: string;
33
+ value: number;
34
+ }>;
35
+ /** Rebuild balances for every distinct metric seen in this user’s metric.delta logs. */
36
+ export declare function rebuildMetricBalancesForUser(args: {
37
+ payload: Payload;
38
+ req?: PayloadRequest;
39
+ userId: string;
40
+ scopeId?: string | null;
41
+ }): Promise<number>;
42
+ //# sourceMappingURL=metricBalances.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metricBalances.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/metricBalances.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAMtD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEjG;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;CACjB,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAehD;AAED,wBAAsB,uBAAuB,CAAC,IAAI,EAAE;IAClD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAgCzC;AAED,sEAAsE;AACtE,wBAAsB,oBAAoB,CAAC,IAAI,EAAE;IAC/C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAA;CACjB,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAoDzC;AAED,wFAAwF;AACxF,wBAAsB,4BAA4B,CAAC,IAAI,EAAE;IACvD,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,MAAM,CAAC,CAgClB"}