@dappermountain/payload-plugin-achievements 0.2.15 → 0.3.0
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 +5 -5
- package/dist/collections/Metrics/index.d.ts.map +1 -1
- package/dist/collections/Metrics/index.js +5 -3
- package/dist/collections/fields/ruleFields.d.ts.map +1 -1
- package/dist/collections/fields/ruleFields.js +2 -48
- package/dist/collections/hooks/eachRuleLeaf.d.ts +5 -0
- package/dist/collections/hooks/eachRuleLeaf.d.ts.map +1 -1
- package/dist/collections/hooks/eachRuleLeaf.js +7 -1
- package/dist/collections/hooks/preventCatalogDelete.d.ts.map +1 -1
- package/dist/collections/hooks/preventCatalogDelete.js +1 -2
- package/dist/seed/index.d.ts +1 -1
- package/dist/seed/index.d.ts.map +1 -1
- package/dist/seed/index.js +1 -1
- package/dist/services/achievement/builtInRules.d.ts.map +1 -1
- package/dist/services/achievement/builtInRules.js +0 -41
- package/dist/services/achievement/duration.d.ts +1 -0
- package/dist/services/achievement/duration.d.ts.map +1 -1
- package/dist/services/achievement/duration.js +8 -1
- package/dist/services/achievement/validateRules.d.ts.map +1 -1
- package/dist/services/achievement/validateRules.js +0 -22
- package/dist/types.d.ts +4 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,7 +107,7 @@ Unlock (tiers), eligibility, and completion (achievements) share the same rule t
|
|
|
107
107
|
|
|
108
108
|
- Top-level empty list → everyone passes
|
|
109
109
|
- Nested groups → AND (“all of these”) or OR (“any of these”)
|
|
110
|
-
- Built-in leaves → `tier-at-least`, `achievement-complete`, `metric-minimum`, `event-count
|
|
110
|
+
- Built-in leaves → `tier-at-least`, `achievement-complete`, `metric-minimum`, `event-count`
|
|
111
111
|
- Custom leaves → register via `extensions.ruleTypes`
|
|
112
112
|
|
|
113
113
|
**Current tier** is computed by walking tiers in `rank` order and evaluating each tier’s unlock rules (`resolveCurrentTier`). Tiers with **`requiresReview`** only become current after an **approved tier request**. Turning **`requiresReview` off** approves pending tier requests and writes missing `tier.changed` logs for members who already meet unlock rules (including those who never had a pending request). **Next-tier fill** uses fractional progress on the same trees (`resolveTierProgress` / `evaluateRuleProgress`):
|
|
@@ -117,12 +117,12 @@ Unlock (tiers), eligibility, and completion (achievements) share the same rule t
|
|
|
117
117
|
| AND | Equal average of *requirement* children |
|
|
118
118
|
| OR | Best child (max) |
|
|
119
119
|
| `tier-at-least` | Gate — skipped in AND averages |
|
|
120
|
-
| `event-count` / `metric-minimum`
|
|
120
|
+
| `event-count` / `metric-minimum` | Fraction of the target |
|
|
121
121
|
| `achievement-complete` | `1` if granted; otherwise rolls up that achievement’s completion (or eligibility) rules |
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
**Computed elapsed metrics** — `kind: computed` + `compute: elapsed` + `unit` (`seconds` | `minutes` | `hours` | `days` | `years`) + `since` (built-in `user-created-at` / `first-event`, or a host `metricAnchors` key). Gate tenure with `metric-minimum` against that metric. Years use a fixed 365-day length (not calendar years). System seed only creates stored `points` — product computed metrics (e.g. `days`) are host-seeded after registering anchors.
|
|
124
124
|
|
|
125
|
-
**Stored
|
|
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
127
|
**Leaderboards** — `getMetricLeaderboard` / `GET /api/achievements/leaderboard?metric=points` ranks stored balances (authenticated). Disable with `endpoints.leaderboard: false`.
|
|
128
128
|
|
|
@@ -186,7 +186,7 @@ achievementPlugin({
|
|
|
186
186
|
| `canReview` | _(denied)_ | **Important.** Without this, users cannot manage catalogs or approve requests. Receives `(user, scopeId)` where `scopeId` may be `null`. |
|
|
187
187
|
| `scope` | _(none)_ | Multi-tenant hook-up. `collection` is the relation target (e.g. `'tenants'`). `relationField` defaults to `'scope'`. Adds an optional scope field across plugin collections. |
|
|
188
188
|
| `mediaCollection` | _(none)_ | When set (e.g. `'media'`), tiers get an optional upload field for ladder badge images. Icon string keys still work without this. |
|
|
189
|
-
| `extensions.metricAnchors` | _(none)_ | Named date resolvers for computed elapsed
|
|
189
|
+
| `extensions.metricAnchors` | _(none)_ | Named date resolvers for computed elapsed metrics. Prefer `{ label, resolve }` where `label` is a Payload Admin {@link OptionLabel} (string, `{ en, es }`, or `({ t }) => t('custom:…')`). Not CMS localization. |
|
|
190
190
|
|
|
191
191
|
### `collections`
|
|
192
192
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/Metrics/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAS/C,wBAAgB,sBAAsB,IAAI,gBAAgB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/Metrics/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAS/C,wBAAgB,sBAAsB,IAAI,gBAAgB,CAkGzD"}
|
|
@@ -54,13 +54,15 @@ export function buildMetricsCollection() {
|
|
|
54
54
|
defaultValue: 'days',
|
|
55
55
|
enumName: 'ach_metric_unit',
|
|
56
56
|
options: [
|
|
57
|
-
{ label: '
|
|
58
|
-
{ label: 'Hours', value: 'hours' },
|
|
57
|
+
{ label: 'Seconds', value: 'seconds' },
|
|
59
58
|
{ label: 'Minutes', value: 'minutes' },
|
|
59
|
+
{ label: 'Hours', value: 'hours' },
|
|
60
|
+
{ label: 'Days', value: 'days' },
|
|
61
|
+
{ label: 'Years', value: 'years' },
|
|
60
62
|
],
|
|
61
63
|
admin: {
|
|
62
64
|
condition: (_, sibling) => sibling?.kind === 'computed' && sibling?.compute === 'elapsed',
|
|
63
|
-
description: 'Unit of the exposed number (
|
|
65
|
+
description: 'Unit of the exposed number (whole units since the anchor). Years use a fixed 365-day length.',
|
|
64
66
|
},
|
|
65
67
|
},
|
|
66
68
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ruleFields.d.ts","sourceRoot":"","sources":["../../../src/collections/fields/ruleFields.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"ruleFields.d.ts","sourceRoot":"","sources":["../../../src/collections/fields/ruleFields.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAqGpC;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,KAAK,EAAE,CAqB7C;AAED,mDAAmD;AACnD,wBAAgB,kBAAkB,IAAI,KAAK,EAAE,CAE5C"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { catalogFilterOptions } from './catalogFilter.js';
|
|
2
2
|
import { collectionOf } from '../helpers.js';
|
|
3
|
-
import { elapsedSinceSelectOptions } from '../../extensions/elapsedSinceOptions.js';
|
|
4
3
|
function combinatorField(visibleWhenGroup) {
|
|
5
4
|
return {
|
|
6
5
|
name: 'combinator',
|
|
@@ -26,7 +25,6 @@ function ruleTypeSelect(includeGroup) {
|
|
|
26
25
|
{ label: 'Achievement complete', value: 'achievement-complete' },
|
|
27
26
|
{ label: 'Metric minimum', value: 'metric-minimum' },
|
|
28
27
|
{ label: 'Event count', value: 'event-count' },
|
|
29
|
-
{ label: 'Elapsed since', value: 'elapsed-since' },
|
|
30
28
|
...(includeGroup ? [{ label: 'Group', value: 'group' }] : []),
|
|
31
29
|
],
|
|
32
30
|
};
|
|
@@ -77,9 +75,8 @@ function ruleParamFields() {
|
|
|
77
75
|
relationTo: collectionOf('eventTypes'),
|
|
78
76
|
filterOptions: catalogFilterOptions,
|
|
79
77
|
admin: {
|
|
80
|
-
condition: (_, sibling) => sibling?.type === 'event-count'
|
|
81
|
-
|
|
82
|
-
description: 'Event type to count, or (for elapsed since) the type whose first log starts the clock.',
|
|
78
|
+
condition: (_, sibling) => sibling?.type === 'event-count',
|
|
79
|
+
description: 'Event type to count.',
|
|
83
80
|
},
|
|
84
81
|
},
|
|
85
82
|
{
|
|
@@ -90,49 +87,6 @@ function ruleParamFields() {
|
|
|
90
87
|
description: 'How many times it needs to have happened.',
|
|
91
88
|
},
|
|
92
89
|
},
|
|
93
|
-
{
|
|
94
|
-
name: 'since',
|
|
95
|
-
type: 'select',
|
|
96
|
-
// Nested under eligibility/completion/unlock → rules → rules; keep PG enum ≤63 chars.
|
|
97
|
-
enumName: 'ach_elapsed_since',
|
|
98
|
-
defaultValue: 'user-created-at',
|
|
99
|
-
options: elapsedSinceSelectOptions(),
|
|
100
|
-
admin: {
|
|
101
|
-
condition: (_, sibling) => sibling?.type === 'elapsed-since',
|
|
102
|
-
description: 'Which timestamp starts the clock.',
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
type: 'row',
|
|
107
|
-
admin: {
|
|
108
|
-
condition: (_, sibling) => sibling?.type === 'elapsed-since',
|
|
109
|
-
},
|
|
110
|
-
fields: [
|
|
111
|
-
{
|
|
112
|
-
name: 'amount',
|
|
113
|
-
type: 'number',
|
|
114
|
-
admin: {
|
|
115
|
-
width: '50%',
|
|
116
|
-
description: 'How much time must pass.',
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
name: 'unit',
|
|
121
|
-
type: 'select',
|
|
122
|
-
enumName: 'ach_elapsed_unit',
|
|
123
|
-
defaultValue: 'days',
|
|
124
|
-
options: [
|
|
125
|
-
{ label: 'Days', value: 'days' },
|
|
126
|
-
{ label: 'Hours', value: 'hours' },
|
|
127
|
-
{ label: 'Minutes', value: 'minutes' },
|
|
128
|
-
],
|
|
129
|
-
admin: {
|
|
130
|
-
width: '50%',
|
|
131
|
-
description: 'Unit for the amount.',
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
],
|
|
135
|
-
},
|
|
136
90
|
];
|
|
137
91
|
}
|
|
138
92
|
function buildRuleLeafFields() {
|
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export type LeafVisitor = (rule: Record<string, unknown>) => void;
|
|
2
|
+
/**
|
|
3
|
+
* Visit every leaf in a rule tree.
|
|
4
|
+
* Payload array rows often include empty `rules: []` and a default `combinator` even on
|
|
5
|
+
* leaves — those must still be visited (do not treat them as groups).
|
|
6
|
+
*/
|
|
2
7
|
export declare function eachRuleLeaf(node: unknown, visit: LeafVisitor): void;
|
|
3
8
|
//# sourceMappingURL=eachRuleLeaf.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eachRuleLeaf.d.ts","sourceRoot":"","sources":["../../../src/collections/hooks/eachRuleLeaf.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;AAEjE,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"eachRuleLeaf.d.ts","sourceRoot":"","sources":["../../../src/collections/hooks/eachRuleLeaf.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;AAEjE;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAiBpE"}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visit every leaf in a rule tree.
|
|
3
|
+
* Payload array rows often include empty `rules: []` and a default `combinator` even on
|
|
4
|
+
* leaves — those must still be visited (do not treat them as groups).
|
|
5
|
+
*/
|
|
1
6
|
export function eachRuleLeaf(node, visit) {
|
|
2
7
|
if (Array.isArray(node)) {
|
|
3
8
|
for (const item of node)
|
|
@@ -11,7 +16,8 @@ export function eachRuleLeaf(node, visit) {
|
|
|
11
16
|
eachRuleLeaf(rec.rules, visit);
|
|
12
17
|
return;
|
|
13
18
|
}
|
|
14
|
-
|
|
19
|
+
// Top-level `{ combinator, rules }` with no leaf type
|
|
20
|
+
if (!rec.type && Array.isArray(rec.rules)) {
|
|
15
21
|
eachRuleLeaf(rec.rules, visit);
|
|
16
22
|
return;
|
|
17
23
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preventCatalogDelete.d.ts","sourceRoot":"","sources":["../../../src/collections/hooks/preventCatalogDelete.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAA;AA6CzD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,IAAI,EAAE,WAAW,GAAG,QAAQ,CAAA;CAC7B,GAAG,0BAA0B,
|
|
1
|
+
{"version":3,"file":"preventCatalogDelete.d.ts","sourceRoot":"","sources":["../../../src/collections/hooks/preventCatalogDelete.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAA;AA6CzD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,IAAI,EAAE,WAAW,GAAG,QAAQ,CAAA;CAC7B,GAAG,0BAA0B,CA8D7B"}
|
|
@@ -61,8 +61,7 @@ export function preventCatalogDelete(args) {
|
|
|
61
61
|
parts.push(`${logs.totalDocs} log row(s)`);
|
|
62
62
|
const rules = await countMatchingLeaves({
|
|
63
63
|
req,
|
|
64
|
-
match: (rule) =>
|
|
65
|
-
relationId(rule.eventType) === id,
|
|
64
|
+
match: (rule) => rule.type === 'event-count' && relationId(rule.eventType) === id,
|
|
66
65
|
});
|
|
67
66
|
if (rules > 0)
|
|
68
67
|
parts.push(`${rules} rule(s)`);
|
package/dist/seed/index.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ export type AchievementSeedCatalogRow = {
|
|
|
48
48
|
/** Computed metrics — v1 only `elapsed`. */
|
|
49
49
|
compute?: 'elapsed';
|
|
50
50
|
/** Computed elapsed — unit of the exposed number. */
|
|
51
|
-
unit?: '
|
|
51
|
+
unit?: 'seconds' | 'minutes' | 'hours' | 'days' | 'years';
|
|
52
52
|
/** Computed elapsed — built-in or host `metricAnchors` key. */
|
|
53
53
|
since?: string;
|
|
54
54
|
/** Computed elapsed with `since: first-event` — event type slug. */
|
package/dist/seed/index.d.ts.map
CHANGED
|
@@ -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,kDAAkD;IAClD,IAAI,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAA;IAC5B,4CAA4C;IAC5C,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,qDAAqD;IACrD,IAAI,CAAC,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/seed/index.ts"],"names":[],"mappings":"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"}
|
package/dist/seed/index.js
CHANGED
|
@@ -136,7 +136,7 @@ async function resolveRuleNode(payload, rule) {
|
|
|
136
136
|
delete next.achievementSlug;
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
-
if (next.type === 'event-count'
|
|
139
|
+
if (next.type === 'event-count') {
|
|
140
140
|
const slug = typeof next.eventTypeSlug === 'string'
|
|
141
141
|
? next.eventTypeSlug
|
|
142
142
|
: 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;
|
|
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;AAqJlF,eAAO,MAAM,gBAAgB,EAAE,mBAAmB,EA6JjD,CAAA"}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { collectionOf } from '../../collections/helpers.js';
|
|
2
|
-
import { durationMs } from './duration.js';
|
|
3
2
|
import { relationId } from './relationId.js';
|
|
4
|
-
import { resolveElapsedAnchor } from './resolveElapsedAnchor.js';
|
|
5
3
|
import { resolveMetricValue } from './resolveMetricValue.js';
|
|
6
4
|
import { userScopeIncludingUnscopedWhere } from './where.js';
|
|
7
5
|
function clamp01(value) {
|
|
@@ -276,43 +274,4 @@ export const builtInRuleTypes = [
|
|
|
276
274
|
return clamp01(result.totalDocs / needed);
|
|
277
275
|
},
|
|
278
276
|
},
|
|
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
|
-
},
|
|
318
277
|
];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type ElapsedUnit = 'seconds' | 'minutes' | 'hours' | 'days' | 'years';
|
|
1
2
|
export declare function durationMs(amount: number, unit: unknown): number | null;
|
|
2
3
|
export declare function unitMs(unit: unknown): number | null;
|
|
3
4
|
export declare function parseDate(value: unknown): Date | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"duration.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/duration.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"duration.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/duration.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAA;AAE5E,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAmBvE;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"}
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
const
|
|
1
|
+
const MS_PER_SECOND = 1_000;
|
|
2
|
+
const MS_PER_MINUTE = 60 * MS_PER_SECOND;
|
|
2
3
|
const MS_PER_HOUR = 60 * MS_PER_MINUTE;
|
|
3
4
|
const MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
5
|
+
/** Fixed-length year for elapsed floors (not calendar years). */
|
|
6
|
+
const MS_PER_YEAR = 365 * MS_PER_DAY;
|
|
4
7
|
export function durationMs(amount, unit) {
|
|
5
8
|
if (!(typeof amount === 'number') || !Number.isFinite(amount) || amount < 0)
|
|
6
9
|
return null;
|
|
7
10
|
switch (unit) {
|
|
11
|
+
case 'seconds':
|
|
12
|
+
return amount * MS_PER_SECOND;
|
|
8
13
|
case 'minutes':
|
|
9
14
|
return amount * MS_PER_MINUTE;
|
|
10
15
|
case 'hours':
|
|
11
16
|
return amount * MS_PER_HOUR;
|
|
17
|
+
case 'years':
|
|
18
|
+
return amount * MS_PER_YEAR;
|
|
12
19
|
case 'days':
|
|
13
20
|
case undefined:
|
|
14
21
|
case null:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validateRules.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/validateRules.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validateRules.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/validateRules.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEvE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,IAAI,CAuD7E;AAED,sFAAsF;AACtF,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,GAAG,SAAS,GAC9D,MAAM,GAAG,IAAI,CAOf;AAED,+EAA+E;AAC/E,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,GAAG,SAAS,EAC/D,KAAK,EAAE,MAAM,GACZ,IAAI,CAKN"}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { APIError } from 'payload';
|
|
2
|
-
import { isBuiltinElapsedSince } from '../../extensions/elapsedSinceOptions.js';
|
|
3
|
-
import { getMetricAnchorResolver } from '../../extensions/metricAnchors.js';
|
|
4
2
|
import { relationId } from './relationId.js';
|
|
5
3
|
import { normalizeRuleGroup } from './evaluateRules.js';
|
|
6
4
|
/**
|
|
@@ -51,26 +49,6 @@ export function validateRuleNode(rule) {
|
|
|
51
49
|
}
|
|
52
50
|
return null;
|
|
53
51
|
}
|
|
54
|
-
if (type === 'elapsed-since') {
|
|
55
|
-
const since = String(rule.since ?? 'user-created-at');
|
|
56
|
-
if (since === 'first-event') {
|
|
57
|
-
if (!relationId(rule.eventType) && typeof rule.eventTypeSlug !== 'string') {
|
|
58
|
-
return 'Elapsed since (first event) needs an event type.';
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
else if (!isBuiltinElapsedSince(since) && !getMetricAnchorResolver(since)) {
|
|
62
|
-
// Host anchors (e.g. membership-granted) are registered via extensions.metricAnchors.
|
|
63
|
-
return 'Elapsed since needs a valid since source.';
|
|
64
|
-
}
|
|
65
|
-
if (typeof rule.amount !== 'number' || Number.isNaN(rule.amount) || rule.amount < 0) {
|
|
66
|
-
return 'Elapsed since needs a non-negative amount.';
|
|
67
|
-
}
|
|
68
|
-
const unit = rule.unit ?? 'days';
|
|
69
|
-
if (unit !== 'days' && unit !== 'hours' && unit !== 'minutes') {
|
|
70
|
-
return 'Elapsed since unit must be days, hours, or minutes.';
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
52
|
if (!type) {
|
|
75
53
|
return 'Each rule needs a type.';
|
|
76
54
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CollectionConfig, OptionLabel, Payload, PayloadRequest, TypedUser } from 'payload';
|
|
2
|
-
/** Host-registered date anchors for computed elapsed metrics
|
|
2
|
+
/** Host-registered date anchors for computed elapsed metrics. */
|
|
3
3
|
export type AchievementMetricAnchor = (args: {
|
|
4
4
|
payload: Payload;
|
|
5
5
|
req?: PayloadRequest;
|
|
@@ -125,8 +125,8 @@ export type AchievementPluginOptions = {
|
|
|
125
125
|
/** Custom rule evaluators merged with built-ins. */
|
|
126
126
|
ruleTypes?: AchievementRuleType[];
|
|
127
127
|
/**
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
* Named date anchors for computed `elapsed` metrics.
|
|
129
|
+
* Keys appear in Admin `since` selects; missing keys resolve to null at eval time.
|
|
130
130
|
*/
|
|
131
131
|
metricAnchors?: Record<string, AchievementMetricAnchorEntry>;
|
|
132
132
|
};
|
|
@@ -150,7 +150,7 @@ export type AchievementPluginOptions = {
|
|
|
150
150
|
users?: AchievementUsersOptions;
|
|
151
151
|
usersCollectionSlug?: string;
|
|
152
152
|
};
|
|
153
|
-
export type BuiltInRuleType = 'achievement-complete' | '
|
|
153
|
+
export type BuiltInRuleType = 'achievement-complete' | 'event-count' | 'metric-minimum' | 'tier-at-least';
|
|
154
154
|
/** Engine log-type catalog slugs (seeded as system catalog rows). */
|
|
155
155
|
export type BuiltInEventType = 'achievement.granted' | 'achievement.revoked' | 'metric.delta' | 'tier.changed';
|
|
156
156
|
//# 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,WAAW,EAAE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAEhG,
|
|
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.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|