@dappermountain/payload-plugin-achievements 0.4.1 → 0.5.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 +2 -2
- package/dist/collections/EventTypes/index.d.ts.map +1 -1
- package/dist/collections/EventTypes/index.js +10 -0
- package/dist/collections/Logs/index.d.ts.map +1 -1
- package/dist/collections/Logs/index.js +24 -0
- package/dist/collections/fields/ruleFields.d.ts.map +1 -1
- package/dist/collections/fields/ruleFields.js +10 -0
- package/dist/defaults.d.ts.map +1 -1
- package/dist/defaults.js +5 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +5 -4
- package/dist/seed/index.d.ts +2 -0
- package/dist/seed/index.d.ts.map +1 -1
- package/dist/seed/index.js +9 -0
- package/dist/services/achievement/actorTierSnapshot.d.ts +40 -0
- package/dist/services/achievement/actorTierSnapshot.d.ts.map +1 -0
- package/dist/services/achievement/actorTierSnapshot.js +138 -0
- package/dist/services/achievement/buildUnlockRequirementLeaves.d.ts +1 -0
- package/dist/services/achievement/buildUnlockRequirementLeaves.d.ts.map +1 -1
- package/dist/services/achievement/buildUnlockRequirementLeaves.js +1 -0
- package/dist/services/achievement/builtInRules.d.ts.map +1 -1
- package/dist/services/achievement/builtInRules.js +77 -22
- package/dist/services/achievement/contextFlags.d.ts +2 -0
- package/dist/services/achievement/contextFlags.d.ts.map +1 -1
- package/dist/services/achievement/contextFlags.js +2 -0
- package/dist/services/achievement/eventTypeRequiresActor.d.ts +2 -0
- package/dist/services/achievement/eventTypeRequiresActor.d.ts.map +1 -1
- package/dist/services/achievement/eventTypeRequiresActor.js +5 -0
- package/dist/services/achievement/recordEvent.d.ts.map +1 -1
- package/dist/services/achievement/recordEvent.js +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ Slugs are prefixed with `achievement-` by default so they don’t collide with y
|
|
|
100
100
|
| `achievement.revoked` | Written when a grant is deleted |
|
|
101
101
|
| `tier.changed` | Written when derived tier moves (including demotions when a tier request leaves `approved`) |
|
|
102
102
|
|
|
103
|
-
Event types can require an **actor** and/or a **metric + change**. Admin only prompts for those fields when the type needs them.
|
|
103
|
+
Event types can require an **actor** and/or a **metric + change**. Admin only prompts for those fields when the type needs them. Optional **Snapshot actor tiers** stores the actor’s derived ladder rank on the log at write time (`resolveCurrentTier` only — no grants or tier requests). Unscoped logs stamp every ladder scope; scoped logs stamp that tenant plus any unscoped ranks. Use that with Event count → **Actor tier** to count only events whose actor held at least that step in the rule’s scope when the event was written. Leave Actor tier empty to count every matching event. Logs without a snapshot (legacy rows, or types that do not stamp) do not match an Actor tier filter.
|
|
104
104
|
|
|
105
105
|
### Rules engine
|
|
106
106
|
|
|
@@ -118,7 +118,7 @@ Unlock (tiers), eligibility, and completion (achievements) share the same rule t
|
|
|
118
118
|
| AND | Equal average of *requirement* children |
|
|
119
119
|
| OR | Best child (max) |
|
|
120
120
|
| `tier-at-least` | Gate — skipped in AND averages |
|
|
121
|
-
| `event-count` / `metric-minimum` | Fraction of the target |
|
|
121
|
+
| `event-count` / `metric-minimum` | Fraction of the target. `event-count` may set **Actor tier** (at-least, this scope) against the log’s actor-tier snapshot |
|
|
122
122
|
| `achievement-complete` | `1` if granted; otherwise rolls up that achievement’s completion (or eligibility) rules |
|
|
123
123
|
|
|
124
124
|
**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. System seed only creates stored `points` — product computed metrics (e.g. `days`) are host-seeded after registering anchors.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/EventTypes/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAS,MAAM,SAAS,CAAA;AAqCtD,wBAAgB,yBAAyB,IAAI,gBAAgB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/EventTypes/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAS,MAAM,SAAS,CAAA;AAqCtD,wBAAgB,yBAAyB,IAAI,gBAAgB,CA+E5D"}
|
|
@@ -41,6 +41,7 @@ export function buildEventTypesCollection() {
|
|
|
41
41
|
'name',
|
|
42
42
|
'slug',
|
|
43
43
|
'requiresActor',
|
|
44
|
+
'snapshotActorTiers',
|
|
44
45
|
'requiresMetric',
|
|
45
46
|
...(subjectFields.length > 0 ? ['requiresSubject'] : []),
|
|
46
47
|
'system',
|
|
@@ -66,6 +67,15 @@ export function buildEventTypesCollection() {
|
|
|
66
67
|
description: 'Turn this on when someone else caused the entry (for example a peer nomination). We’ll ask for that person when it’s logged.',
|
|
67
68
|
},
|
|
68
69
|
},
|
|
70
|
+
{
|
|
71
|
+
name: 'snapshotActorTiers',
|
|
72
|
+
type: 'checkbox',
|
|
73
|
+
defaultValue: false,
|
|
74
|
+
label: 'Snapshot actor tiers',
|
|
75
|
+
admin: {
|
|
76
|
+
description: 'When this entry is logged, store the actor’s derived ladder rank at write time. Unscoped logs stamp every ladder; scoped logs stamp that tenant plus unscoped ranks. Event count can then require “actor was at least this tier” in the rule’s scope. Does not grant or open requests.',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
69
79
|
{
|
|
70
80
|
name: 'requiresMetric',
|
|
71
81
|
type: 'checkbox',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/Logs/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAS,MAAM,SAAS,CAAA;AAqCtD,+EAA+E;AAC/E,wBAAgB,mBAAmB,IAAI,gBAAgB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/Logs/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAS,MAAM,SAAS,CAAA;AAqCtD,+EAA+E;AAC/E,wBAAgB,mBAAmB,IAAI,gBAAgB,CAgJtD"}
|
|
@@ -119,6 +119,30 @@ export function buildLogsCollection() {
|
|
|
119
119
|
],
|
|
120
120
|
},
|
|
121
121
|
...(subjectField ? [subjectField] : []),
|
|
122
|
+
{
|
|
123
|
+
name: 'actorTiers',
|
|
124
|
+
type: 'array',
|
|
125
|
+
admin: {
|
|
126
|
+
readOnly: true,
|
|
127
|
+
description: 'Derived ladder rank of the actor when this entry was written. Filled only when the event type snapshots actor tiers.',
|
|
128
|
+
},
|
|
129
|
+
fields: [
|
|
130
|
+
...optionalFields(scopeField()),
|
|
131
|
+
{
|
|
132
|
+
name: 'tier',
|
|
133
|
+
type: 'relationship',
|
|
134
|
+
relationTo: collectionOf('tiers'),
|
|
135
|
+
required: true,
|
|
136
|
+
admin: { description: 'Which ladder step they held.' },
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'rank',
|
|
140
|
+
type: 'number',
|
|
141
|
+
required: true,
|
|
142
|
+
admin: { description: 'Ladder rank at write time (higher is further along).' },
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
122
146
|
{
|
|
123
147
|
type: 'collapsible',
|
|
124
148
|
label: 'Details',
|
|
@@ -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;AAgHpC;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,KAAK,EAAE,CAqB7C;AAED,mDAAmD;AACnD,wBAAgB,kBAAkB,IAAI,KAAK,EAAE,CAE5C"}
|
|
@@ -87,6 +87,16 @@ function ruleParamFields() {
|
|
|
87
87
|
description: 'How many times it needs to have happened.',
|
|
88
88
|
},
|
|
89
89
|
},
|
|
90
|
+
{
|
|
91
|
+
name: 'actorTier',
|
|
92
|
+
type: 'relationship',
|
|
93
|
+
relationTo: collectionOf('tiers'),
|
|
94
|
+
filterOptions: catalogFilterOptions,
|
|
95
|
+
admin: {
|
|
96
|
+
condition: (_, sibling) => sibling?.type === 'event-count',
|
|
97
|
+
description: 'Only count events whose actor held at least this tier in this scope when the event was written. Requires the event type to snapshot actor tiers. Leave empty to count every matching event.',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
90
100
|
];
|
|
91
101
|
}
|
|
92
102
|
function buildRuleLeafFields() {
|
package/dist/defaults.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAC1D,OAAO,EAEL,sBAAsB,EACvB,MAAM,wBAAwB,CAAA;AAE/B,eAAO,MAAM,wBAAwB,UAAU,CAAA;AAC/C,eAAO,MAAM,wBAAwB,qBAAqB,CAAA;AAC1D,eAAO,MAAM,iCAAiC,8BAA8B,CAAA;AAC5E,eAAO,MAAM,+BAA+B,4BAA4B,CAAA;AAExE,MAAM,MAAM,0BAA0B,GAAG,IAAI,CAAC,wBAAwB,EAAE,OAAO,GAAG,WAAW,GAAG,UAAU,CAAC,GAAG;IAC5G,OAAO,EAAE,OAAO,CAAA;IAChB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,KAAK,EAAE;QAAE,YAAY,EAAE,OAAO,CAAA;KAAE,CAAA;IAChC,kFAAkF;IAClF,kBAAkB,EAAE,MAAM,EAAE,CAAA;IAC5B,+DAA+D;IAC/D,eAAe,EAAE,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAA;IAC1D,oEAAoE;IACpE,UAAU,EAAE,MAAM,GAAG,KAAK,CAAA;IAC1B,SAAS,EAAE;QAAE,EAAE,EAAE,MAAM,GAAG,KAAK,CAAC;QAAC,WAAW,EAAE,MAAM,GAAG,KAAK,CAAC;QAAC,SAAS,EAAE,MAAM,GAAG,KAAK,CAAA;KAAE,CAAA;CAC1F,CAAA;AAED,6EAA6E;AAC7E,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,MAAM,EAAE,CAWtF;AAQD,wBAAgB,qBAAqB,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAIzE;AAED,wBAAgB,8BAA8B,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAIpF;AAED,wBAAgB,4BAA4B,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAIlF;AAED,wBAAgB,cAAc,CAAC,OAAO,GAAE,wBAA6B,GAAG,0BAA0B,
|
|
1
|
+
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAC1D,OAAO,EAEL,sBAAsB,EACvB,MAAM,wBAAwB,CAAA;AAE/B,eAAO,MAAM,wBAAwB,UAAU,CAAA;AAC/C,eAAO,MAAM,wBAAwB,qBAAqB,CAAA;AAC1D,eAAO,MAAM,iCAAiC,8BAA8B,CAAA;AAC5E,eAAO,MAAM,+BAA+B,4BAA4B,CAAA;AAExE,MAAM,MAAM,0BAA0B,GAAG,IAAI,CAAC,wBAAwB,EAAE,OAAO,GAAG,WAAW,GAAG,UAAU,CAAC,GAAG;IAC5G,OAAO,EAAE,OAAO,CAAA;IAChB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,KAAK,EAAE;QAAE,YAAY,EAAE,OAAO,CAAA;KAAE,CAAA;IAChC,kFAAkF;IAClF,kBAAkB,EAAE,MAAM,EAAE,CAAA;IAC5B,+DAA+D;IAC/D,eAAe,EAAE,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAA;IAC1D,oEAAoE;IACpE,UAAU,EAAE,MAAM,GAAG,KAAK,CAAA;IAC1B,SAAS,EAAE;QAAE,EAAE,EAAE,MAAM,GAAG,KAAK,CAAC;QAAC,WAAW,EAAE,MAAM,GAAG,KAAK,CAAC;QAAC,SAAS,EAAE,MAAM,GAAG,KAAK,CAAA;KAAE,CAAA;CAC1F,CAAA;AAED,6EAA6E;AAC7E,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,MAAM,EAAE,CAWtF;AAQD,wBAAgB,qBAAqB,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAIzE;AAED,wBAAgB,8BAA8B,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAIpF;AAED,wBAAgB,4BAA4B,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAIlF;AAED,wBAAgB,cAAc,CAAC,OAAO,GAAE,wBAA6B,GAAG,0BAA0B,CAwBjG"}
|
package/dist/defaults.js
CHANGED
|
@@ -46,6 +46,10 @@ export function resolveReconcileEndpointPath(path) {
|
|
|
46
46
|
}
|
|
47
47
|
export function resolveOptions(options = {}) {
|
|
48
48
|
const { subjects: _subjects, ...rest } = options;
|
|
49
|
+
// Idempotent: `setAchievementOptions(resolveOptions(opts))` strips `subjects` and would
|
|
50
|
+
// otherwise wipe `subjectCollections` on the second pass.
|
|
51
|
+
const fromConfig = resolveSubjectCollections(options);
|
|
52
|
+
const alreadyResolved = options.subjectCollections;
|
|
49
53
|
return {
|
|
50
54
|
...rest,
|
|
51
55
|
enabled: options.enabled !== false,
|
|
@@ -54,7 +58,7 @@ export function resolveOptions(options = {}) {
|
|
|
54
58
|
users: {
|
|
55
59
|
includeJoins: options.users?.includeJoins !== false,
|
|
56
60
|
},
|
|
57
|
-
subjectCollections:
|
|
61
|
+
subjectCollections: options.subjects != null ? fromConfig : (alreadyResolved ?? fromConfig),
|
|
58
62
|
collectionSlugs: resolveCollectionSlugs(options),
|
|
59
63
|
adminGroup: resolveAdminGroup(options),
|
|
60
64
|
endpoints: {
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,MAAM,EAAE,MAAM,SAAS,CAAA;AAM7C,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAE1D;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAC3B,UAAS,wBAA6B,KAAG,MAqCzC,CAAA"}
|
package/dist/plugin.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { buildAchievementCollections } from './collections/index.js';
|
|
2
2
|
import { usersAchievementsGroup } from './collections/fields/usersAchievementsGroup.js';
|
|
3
|
-
import { resolveOptions } from './defaults.js';
|
|
4
3
|
import { buildAchievementEndpoints } from './endpoints/index.js';
|
|
5
|
-
import { setAchievementOptions } from './options-store.js';
|
|
4
|
+
import { getAchievementOptions, setAchievementOptions } from './options-store.js';
|
|
6
5
|
/**
|
|
7
6
|
* Payload achievements plugin — merges collections (hooks own request/grant flow)
|
|
8
7
|
* and an optional current-user grants endpoint.
|
|
9
8
|
*/
|
|
10
9
|
export const achievementPlugin = (options = {}) => (config) => {
|
|
11
|
-
|
|
12
|
-
setAchievementOptions
|
|
10
|
+
// Store the host options (not a pre-resolved object) so `subjects.collections`
|
|
11
|
+
// survives `setAchievementOptions` → `resolveOptions`.
|
|
12
|
+
setAchievementOptions(options);
|
|
13
|
+
const resolved = getAchievementOptions();
|
|
13
14
|
if (!resolved.enabled)
|
|
14
15
|
return config;
|
|
15
16
|
const achievementCollections = buildAchievementCollections();
|
package/dist/seed/index.d.ts
CHANGED
|
@@ -59,6 +59,8 @@ export type AchievementSeedCatalogRow = {
|
|
|
59
59
|
requiresMetric?: boolean;
|
|
60
60
|
/** Event types only — when true, logging the event requires a polymorphic `subject`. */
|
|
61
61
|
requiresSubject?: boolean;
|
|
62
|
+
/** Event types only — stamp the actor’s derived ladder ranks on the log at write time. */
|
|
63
|
+
snapshotActorTiers?: boolean;
|
|
62
64
|
/**
|
|
63
65
|
* Event types only — allowed host collections for `subject.relationTo`.
|
|
64
66
|
* Empty / omit = any collection from plugin `subjects.collections`.
|
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,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAA;IACzD,+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;IACxB,wFAAwF;IACxF,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAA;CAC7B,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;
|
|
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
|
@@ -75,6 +75,7 @@ async function upsertCatalog(payload, key, row) {
|
|
|
75
75
|
requiresActor: row.requiresActor ?? false,
|
|
76
76
|
requiresMetric: row.requiresMetric ?? false,
|
|
77
77
|
requiresSubject: row.requiresSubject ?? false,
|
|
78
|
+
snapshotActorTiers: row.snapshotActorTiers ?? false,
|
|
78
79
|
...(row.subjectRelationTo && row.subjectRelationTo.length > 0
|
|
79
80
|
? { subjectRelationTo: row.subjectRelationTo }
|
|
80
81
|
: { subjectRelationTo: [] }),
|
|
@@ -153,6 +154,14 @@ async function resolveRuleNode(payload, rule) {
|
|
|
153
154
|
delete next.eventTypeSlug;
|
|
154
155
|
}
|
|
155
156
|
}
|
|
157
|
+
// Optional at-least floor against the log’s actor-tier snapshot.
|
|
158
|
+
if (!next.actorTier && typeof next.actorTierSlug === 'string') {
|
|
159
|
+
const id = await findIdBySlug(payload, 'tiers', next.actorTierSlug);
|
|
160
|
+
if (id) {
|
|
161
|
+
next.actorTier = id;
|
|
162
|
+
delete next.actorTierSlug;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
156
165
|
}
|
|
157
166
|
if (next.type === 'metric-minimum') {
|
|
158
167
|
const slug = typeof next.metricSlug === 'string' ? next.metricSlug : typeof next.metric === 'string' ? next.metric : '';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Payload, PayloadRequest } from 'payload';
|
|
2
|
+
export type ActorTierSnapshotRow = {
|
|
3
|
+
scope?: string | null;
|
|
4
|
+
tier: string;
|
|
5
|
+
rank: number;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Unscoped logs stamp every ladder (a later scoped count can still see the actor’s rank).
|
|
9
|
+
* Scoped logs stamp that tenant plus any unscoped ranks.
|
|
10
|
+
*/
|
|
11
|
+
export declare function scopeIdsForActorSnapshot(args: {
|
|
12
|
+
logScopeId: string | null;
|
|
13
|
+
ladderScopeIds: Array<string | null>;
|
|
14
|
+
}): Array<string | null>;
|
|
15
|
+
/** Normalize log `actorTiers` rows for matching. */
|
|
16
|
+
export declare function readActorTierSnapshotRows(value: unknown): Array<{
|
|
17
|
+
scopeId: string | null;
|
|
18
|
+
rank: number;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* True when a snapshot includes this scope at rank ≥ `minimumRank`.
|
|
22
|
+
* Missing snapshot (legacy logs) never matches — the event was not stamped.
|
|
23
|
+
*/
|
|
24
|
+
export declare function actorSnapshotMeetsTier(args: {
|
|
25
|
+
rows: unknown;
|
|
26
|
+
scopeId: string | null;
|
|
27
|
+
minimumRank: number;
|
|
28
|
+
}): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Read-only derived ladder ranks for an actor across configured scopes.
|
|
31
|
+
* Does not reconcile, grant, or open tier requests.
|
|
32
|
+
*/
|
|
33
|
+
export declare function snapshotActorDerivedTiers(args: {
|
|
34
|
+
payload: Payload;
|
|
35
|
+
req?: PayloadRequest;
|
|
36
|
+
userId: string;
|
|
37
|
+
/** Log `scope` — `null` stamps every ladder scope. */
|
|
38
|
+
logScopeId?: string | null;
|
|
39
|
+
}): Promise<ActorTierSnapshotRow[]>;
|
|
40
|
+
//# sourceMappingURL=actorTierSnapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actorTierSnapshot.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/actorTierSnapshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAQtD,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAyBD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,cAAc,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CACrC,GAAG,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAcvB;AAED,oDAAoD;AACpD,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAAC;IAC/D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;CACb,CAAC,CAWD;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC3C,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;CACpB,GAAG,OAAO,CAIV;AAmDD;;;GAGG;AACH,wBAAsB,yBAAyB,CAAC,IAAI,EAAE;IACpD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC,CA0BlC"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { collectionOf } from '../../collections/helpers.js';
|
|
2
|
+
import { getAchievementOptions } from '../../options-store.js';
|
|
3
|
+
import { CTX_ACTOR_TIER_RESOLVE } from './contextFlags.js';
|
|
4
|
+
import { relationId } from './relationId.js';
|
|
5
|
+
import { resolveCurrentTier } from './resolveCurrentTier.js';
|
|
6
|
+
function resolveCache(req) {
|
|
7
|
+
if (!req)
|
|
8
|
+
return null;
|
|
9
|
+
const context = (req.context ?? {});
|
|
10
|
+
const existing = context[CTX_ACTOR_TIER_RESOLVE];
|
|
11
|
+
if (existing instanceof Map)
|
|
12
|
+
return existing;
|
|
13
|
+
const created = new Map();
|
|
14
|
+
req.context = { ...req.context, [CTX_ACTOR_TIER_RESOLVE]: created };
|
|
15
|
+
return created;
|
|
16
|
+
}
|
|
17
|
+
function cacheKey(userId, scopeId) {
|
|
18
|
+
return `${userId}:${scopeId ?? ''}`;
|
|
19
|
+
}
|
|
20
|
+
function pushUniqueScope(out, seen, id) {
|
|
21
|
+
const key = id ?? '';
|
|
22
|
+
if (seen.has(key))
|
|
23
|
+
return;
|
|
24
|
+
seen.add(key);
|
|
25
|
+
out.push(id);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Unscoped logs stamp every ladder (a later scoped count can still see the actor’s rank).
|
|
29
|
+
* Scoped logs stamp that tenant plus any unscoped ranks.
|
|
30
|
+
*/
|
|
31
|
+
export function scopeIdsForActorSnapshot(args) {
|
|
32
|
+
const out = [];
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
if (args.logScopeId == null) {
|
|
35
|
+
for (const id of args.ladderScopeIds)
|
|
36
|
+
pushUniqueScope(out, seen, id);
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
pushUniqueScope(out, seen, args.logScopeId);
|
|
40
|
+
for (const id of args.ladderScopeIds) {
|
|
41
|
+
if (id == null)
|
|
42
|
+
pushUniqueScope(out, seen, null);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
/** Normalize log `actorTiers` rows for matching. */
|
|
47
|
+
export function readActorTierSnapshotRows(value) {
|
|
48
|
+
if (!Array.isArray(value))
|
|
49
|
+
return [];
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const row of value) {
|
|
52
|
+
if (!row || typeof row !== 'object')
|
|
53
|
+
continue;
|
|
54
|
+
const rec = row;
|
|
55
|
+
const rank = Number(rec.rank);
|
|
56
|
+
if (!Number.isFinite(rank))
|
|
57
|
+
continue;
|
|
58
|
+
out.push({ scopeId: relationId(rec.scope) ?? null, rank });
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* True when a snapshot includes this scope at rank ≥ `minimumRank`.
|
|
64
|
+
* Missing snapshot (legacy logs) never matches — the event was not stamped.
|
|
65
|
+
*/
|
|
66
|
+
export function actorSnapshotMeetsTier(args) {
|
|
67
|
+
return readActorTierSnapshotRows(args.rows).some((row) => row.scopeId === args.scopeId && row.rank >= args.minimumRank);
|
|
68
|
+
}
|
|
69
|
+
async function listLadderScopeIds(args) {
|
|
70
|
+
if (!getAchievementOptions().scope?.collection)
|
|
71
|
+
return [null];
|
|
72
|
+
const { docs } = await args.payload.find({
|
|
73
|
+
collection: collectionOf('tiers'),
|
|
74
|
+
depth: 0,
|
|
75
|
+
overrideAccess: true,
|
|
76
|
+
pagination: false,
|
|
77
|
+
req: args.req,
|
|
78
|
+
select: { scope: true },
|
|
79
|
+
});
|
|
80
|
+
const ids = new Set();
|
|
81
|
+
let unscoped = false;
|
|
82
|
+
for (const doc of docs) {
|
|
83
|
+
const scopeId = relationId(doc.scope);
|
|
84
|
+
if (scopeId)
|
|
85
|
+
ids.add(scopeId);
|
|
86
|
+
else
|
|
87
|
+
unscoped = true;
|
|
88
|
+
}
|
|
89
|
+
const out = [...ids];
|
|
90
|
+
if (unscoped || out.length === 0)
|
|
91
|
+
out.push(null);
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
async function currentTierMemoized(args) {
|
|
95
|
+
const cache = resolveCache(args.req);
|
|
96
|
+
const key = cacheKey(args.userId, args.scopeId);
|
|
97
|
+
const hit = cache?.get(key);
|
|
98
|
+
if (hit)
|
|
99
|
+
return hit;
|
|
100
|
+
const pending = resolveCurrentTier({
|
|
101
|
+
payload: args.payload,
|
|
102
|
+
req: args.req,
|
|
103
|
+
userId: args.userId,
|
|
104
|
+
scopeId: args.scopeId,
|
|
105
|
+
});
|
|
106
|
+
cache?.set(key, pending);
|
|
107
|
+
return pending;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Read-only derived ladder ranks for an actor across configured scopes.
|
|
111
|
+
* Does not reconcile, grant, or open tier requests.
|
|
112
|
+
*/
|
|
113
|
+
export async function snapshotActorDerivedTiers(args) {
|
|
114
|
+
if (!args.userId)
|
|
115
|
+
return [];
|
|
116
|
+
const ladderScopeIds = await listLadderScopeIds({ payload: args.payload, req: args.req });
|
|
117
|
+
const scopeIds = scopeIdsForActorSnapshot({
|
|
118
|
+
logScopeId: args.logScopeId ?? null,
|
|
119
|
+
ladderScopeIds,
|
|
120
|
+
});
|
|
121
|
+
const rows = [];
|
|
122
|
+
for (const scopeId of scopeIds) {
|
|
123
|
+
const current = await currentTierMemoized({
|
|
124
|
+
payload: args.payload,
|
|
125
|
+
req: args.req,
|
|
126
|
+
userId: args.userId,
|
|
127
|
+
scopeId,
|
|
128
|
+
});
|
|
129
|
+
if (!current)
|
|
130
|
+
continue;
|
|
131
|
+
rows.push({
|
|
132
|
+
...(scopeId ? { scope: scopeId } : {}),
|
|
133
|
+
tier: current.id,
|
|
134
|
+
rank: current.rank,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return rows;
|
|
138
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Payload, PayloadRequest } from 'payload';
|
|
2
2
|
export declare const UNLOCK_REQUIREMENT_RELATION_FIELDS: {
|
|
3
3
|
readonly tier: "tiers";
|
|
4
|
+
readonly actorTier: "tiers";
|
|
4
5
|
readonly achievement: "achievements";
|
|
5
6
|
readonly metric: "metrics";
|
|
6
7
|
readonly eventType: "eventTypes";
|
|
@@ -1 +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
|
|
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;;;;;;CAMyC,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"}
|
|
@@ -4,6 +4,7 @@ import { evaluateRuleProgress, evaluateRules } from './evaluateRules.js';
|
|
|
4
4
|
import { relationId } from './relationId.js';
|
|
5
5
|
export const UNLOCK_REQUIREMENT_RELATION_FIELDS = {
|
|
6
6
|
tier: 'tiers',
|
|
7
|
+
actorTier: 'tiers',
|
|
7
8
|
achievement: 'achievements',
|
|
8
9
|
metric: 'metrics',
|
|
9
10
|
eventType: 'eventTypes',
|
|
@@ -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;AA4OlF,eAAO,MAAM,gBAAgB,EAAE,mBAAmB,EA6IjD,CAAA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { collectionOf } from '../../collections/helpers.js';
|
|
2
|
+
import { actorSnapshotMeetsTier } from './actorTierSnapshot.js';
|
|
2
3
|
import { relationId } from './relationId.js';
|
|
3
4
|
import { resolveMetricValue } from './resolveMetricValue.js';
|
|
4
5
|
import { userScopeIncludingUnscopedWhere } from './where.js';
|
|
@@ -52,6 +53,78 @@ async function resolveEventTypeId(args) {
|
|
|
52
53
|
});
|
|
53
54
|
return relationId(found.docs[0]);
|
|
54
55
|
}
|
|
56
|
+
async function resolveActorTierMinimumRank(args) {
|
|
57
|
+
const fromRelation = relationId(args.rule.actorTier);
|
|
58
|
+
const slug = typeof args.rule.actorTierSlug === 'string' && args.rule.actorTierSlug
|
|
59
|
+
? args.rule.actorTierSlug
|
|
60
|
+
: '';
|
|
61
|
+
if (!fromRelation && !slug)
|
|
62
|
+
return null;
|
|
63
|
+
if (fromRelation) {
|
|
64
|
+
try {
|
|
65
|
+
const doc = (await args.payload.findByID({
|
|
66
|
+
collection: collectionOf('tiers'),
|
|
67
|
+
id: fromRelation,
|
|
68
|
+
depth: 0,
|
|
69
|
+
overrideAccess: true,
|
|
70
|
+
select: { rank: true },
|
|
71
|
+
}));
|
|
72
|
+
if (doc && typeof doc.rank === 'number' && Number.isFinite(doc.rank))
|
|
73
|
+
return doc.rank;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* fall through to slug */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!slug)
|
|
80
|
+
return null;
|
|
81
|
+
const found = await args.payload.find({
|
|
82
|
+
collection: collectionOf('tiers'),
|
|
83
|
+
depth: 0,
|
|
84
|
+
limit: 1,
|
|
85
|
+
overrideAccess: true,
|
|
86
|
+
where: { slug: { equals: slug } },
|
|
87
|
+
select: { rank: true },
|
|
88
|
+
});
|
|
89
|
+
const rank = found.docs[0]?.rank;
|
|
90
|
+
return typeof rank === 'number' && Number.isFinite(rank) ? rank : null;
|
|
91
|
+
}
|
|
92
|
+
async function countMatchingEventLogs(args) {
|
|
93
|
+
const eventTypeId = await resolveEventTypeId({ payload: args.payload, rule: args.rule });
|
|
94
|
+
if (!eventTypeId)
|
|
95
|
+
return 0;
|
|
96
|
+
const where = {
|
|
97
|
+
and: [
|
|
98
|
+
userScopeIncludingUnscopedWhere(args.userId, args.scopeId),
|
|
99
|
+
{ type: { equals: eventTypeId } },
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
const minimumRank = await resolveActorTierMinimumRank({
|
|
103
|
+
payload: args.payload,
|
|
104
|
+
rule: args.rule,
|
|
105
|
+
});
|
|
106
|
+
if (minimumRank == null) {
|
|
107
|
+
const result = await args.payload.count({
|
|
108
|
+
collection: collectionOf('logs'),
|
|
109
|
+
overrideAccess: true,
|
|
110
|
+
where,
|
|
111
|
+
});
|
|
112
|
+
return result.totalDocs;
|
|
113
|
+
}
|
|
114
|
+
const { docs } = await args.payload.find({
|
|
115
|
+
collection: collectionOf('logs'),
|
|
116
|
+
depth: 0,
|
|
117
|
+
overrideAccess: true,
|
|
118
|
+
pagination: false,
|
|
119
|
+
select: { actorTiers: true },
|
|
120
|
+
where,
|
|
121
|
+
});
|
|
122
|
+
return docs.filter((doc) => actorSnapshotMeetsTier({
|
|
123
|
+
rows: doc.actorTiers,
|
|
124
|
+
scopeId: args.scopeId,
|
|
125
|
+
minimumRank,
|
|
126
|
+
})).length;
|
|
127
|
+
}
|
|
55
128
|
async function resolveAchievementRef(args) {
|
|
56
129
|
let requiredId = relationId(args.rule.achievement);
|
|
57
130
|
let requiredSlug = typeof args.rule.achievementSlug === 'string' ? args.rule.achievementSlug : '';
|
|
@@ -244,34 +317,16 @@ export const builtInRuleTypes = [
|
|
|
244
317
|
{
|
|
245
318
|
type: 'event-count',
|
|
246
319
|
async evaluate({ payload, rule, userId, scopeId }) {
|
|
247
|
-
const eventTypeId = await resolveEventTypeId({ payload, rule });
|
|
248
|
-
if (!eventTypeId)
|
|
249
|
-
return false;
|
|
250
320
|
const needed = Number(rule.count ?? 1);
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
overrideAccess: true,
|
|
254
|
-
where: {
|
|
255
|
-
and: [userScopeIncludingUnscopedWhere(userId, scopeId), { type: { equals: eventTypeId } }],
|
|
256
|
-
},
|
|
257
|
-
});
|
|
258
|
-
return result.totalDocs >= needed;
|
|
321
|
+
const total = await countMatchingEventLogs({ payload, rule, userId, scopeId });
|
|
322
|
+
return total >= needed;
|
|
259
323
|
},
|
|
260
324
|
async progress({ payload, rule, userId, scopeId }) {
|
|
261
|
-
const eventTypeId = await resolveEventTypeId({ payload, rule });
|
|
262
|
-
if (!eventTypeId)
|
|
263
|
-
return 0;
|
|
264
325
|
const needed = Number(rule.count ?? 1);
|
|
265
326
|
if (!(needed > 0))
|
|
266
327
|
return 1;
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
overrideAccess: true,
|
|
270
|
-
where: {
|
|
271
|
-
and: [userScopeIncludingUnscopedWhere(userId, scopeId), { type: { equals: eventTypeId } }],
|
|
272
|
-
},
|
|
273
|
-
});
|
|
274
|
-
return clamp01(result.totalDocs / needed);
|
|
328
|
+
const total = await countMatchingEventLogs({ payload, rule, userId, scopeId });
|
|
329
|
+
return clamp01(total / needed);
|
|
275
330
|
},
|
|
276
331
|
},
|
|
277
332
|
];
|
|
@@ -2,4 +2,6 @@
|
|
|
2
2
|
export declare const CTX_SKIP_GRANT_SIDE_EFFECTS = "achievementSkipGrantSideEffects";
|
|
3
3
|
export declare const CTX_CASCADING_REVOKE = "achievementCascadingRevoke";
|
|
4
4
|
export declare const CTX_SYSTEM_REQUEST = "achievementSystemRequest";
|
|
5
|
+
/** Memo for read-only {@link resolveCurrentTier} during actor-tier snapshots. */
|
|
6
|
+
export declare const CTX_ACTOR_TIER_RESOLVE = "achievementActorTierResolve";
|
|
5
7
|
//# sourceMappingURL=contextFlags.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contextFlags.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/contextFlags.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,eAAO,MAAM,2BAA2B,oCAAoC,CAAA;AAC5E,eAAO,MAAM,oBAAoB,+BAA+B,CAAA;AAChE,eAAO,MAAM,kBAAkB,6BAA6B,CAAA"}
|
|
1
|
+
{"version":3,"file":"contextFlags.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/contextFlags.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,eAAO,MAAM,2BAA2B,oCAAoC,CAAA;AAC5E,eAAO,MAAM,oBAAoB,+BAA+B,CAAA;AAChE,eAAO,MAAM,kBAAkB,6BAA6B,CAAA;AAC5D,iFAAiF;AACjF,eAAO,MAAM,sBAAsB,gCAAgC,CAAA"}
|
|
@@ -2,3 +2,5 @@
|
|
|
2
2
|
export const CTX_SKIP_GRANT_SIDE_EFFECTS = 'achievementSkipGrantSideEffects';
|
|
3
3
|
export const CTX_CASCADING_REVOKE = 'achievementCascadingRevoke';
|
|
4
4
|
export const CTX_SYSTEM_REQUEST = 'achievementSystemRequest';
|
|
5
|
+
/** Memo for read-only {@link resolveCurrentTier} during actor-tier snapshots. */
|
|
6
|
+
export const CTX_ACTOR_TIER_RESOLVE = 'achievementActorTierResolve';
|
|
@@ -3,6 +3,8 @@ export type EventTypeFlags = {
|
|
|
3
3
|
requiresActor: boolean;
|
|
4
4
|
requiresMetric: boolean;
|
|
5
5
|
requiresSubject: boolean;
|
|
6
|
+
/** Stamp the actor's derived ladder ranks on the log at write time. */
|
|
7
|
+
snapshotActorTiers: boolean;
|
|
6
8
|
/** Allowed host collections for `subject.relationTo` (empty = any plugin allowlist entry). */
|
|
7
9
|
subjectRelationTo: string[];
|
|
8
10
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eventTypeRequiresActor.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/eventTypeRequiresActor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAMtD,MAAM,MAAM,cAAc,GAAG;IAC3B,aAAa,EAAE,OAAO,CAAA;IACtB,cAAc,EAAE,OAAO,CAAA;IACvB,eAAe,EAAE,OAAO,CAAA;IACxB,8FAA8F;IAC9F,iBAAiB,EAAE,MAAM,EAAE,CAAA;CAC5B,CAAA;
|
|
1
|
+
{"version":3,"file":"eventTypeRequiresActor.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/eventTypeRequiresActor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAMtD,MAAM,MAAM,cAAc,GAAG;IAC3B,aAAa,EAAE,OAAO,CAAA;IACtB,cAAc,EAAE,OAAO,CAAA;IACvB,eAAe,EAAE,OAAO,CAAA;IACxB,uEAAuE;IACvE,kBAAkB,EAAE,OAAO,CAAA;IAC3B,8FAA8F;IAC9F,iBAAiB,EAAE,MAAM,EAAE,CAAA;CAC5B,CAAA;AAmBD,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CAyBtE;AAMD,wDAAwD;AACxD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,cAAc,CAAC,CA2C1B;AAED,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnB;AAED,wBAAsB,uBAAuB,CAAC,IAAI,EAAE;IAClD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnB;AAED,wBAAsB,wBAAwB,CAAC,IAAI,EAAE;IACnD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnB;AAED;;;GAGG;AACH,wBAAsB,0BAA0B,CAAC,IAAI,EAAE;IACrD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAOpB"}
|
|
@@ -5,6 +5,7 @@ const emptyFlags = {
|
|
|
5
5
|
requiresActor: false,
|
|
6
6
|
requiresMetric: false,
|
|
7
7
|
requiresSubject: false,
|
|
8
|
+
snapshotActorTiers: false,
|
|
8
9
|
subjectRelationTo: [],
|
|
9
10
|
};
|
|
10
11
|
function readSubjectRelationTo(value) {
|
|
@@ -25,6 +26,7 @@ export function readEventTypeFlags(doc) {
|
|
|
25
26
|
const hasFlagKeys = 'requiresActor' in row ||
|
|
26
27
|
'requiresMetric' in row ||
|
|
27
28
|
'requiresSubject' in row ||
|
|
29
|
+
'snapshotActorTiers' in row ||
|
|
28
30
|
'subjectRelationTo' in row ||
|
|
29
31
|
typeof row.slug === 'string';
|
|
30
32
|
if (!hasFlagKeys)
|
|
@@ -33,6 +35,7 @@ export function readEventTypeFlags(doc) {
|
|
|
33
35
|
requiresActor: Boolean(row.requiresActor),
|
|
34
36
|
requiresMetric: Boolean(row.requiresMetric) || row.slug === 'metric.delta',
|
|
35
37
|
requiresSubject: Boolean(row.requiresSubject),
|
|
38
|
+
snapshotActorTiers: Boolean(row.snapshotActorTiers),
|
|
36
39
|
subjectRelationTo: readSubjectRelationTo(row.subjectRelationTo),
|
|
37
40
|
};
|
|
38
41
|
}
|
|
@@ -47,6 +50,7 @@ export async function getEventTypeFlags(args) {
|
|
|
47
50
|
if ('requiresActor' in row ||
|
|
48
51
|
'requiresMetric' in row ||
|
|
49
52
|
'requiresSubject' in row ||
|
|
53
|
+
'snapshotActorTiers' in row ||
|
|
50
54
|
'subjectRelationTo' in row ||
|
|
51
55
|
'slug' in row) {
|
|
52
56
|
return embedded;
|
|
@@ -65,6 +69,7 @@ export async function getEventTypeFlags(args) {
|
|
|
65
69
|
requiresActor: true,
|
|
66
70
|
requiresMetric: true,
|
|
67
71
|
requiresSubject: true,
|
|
72
|
+
snapshotActorTiers: true,
|
|
68
73
|
subjectRelationTo: true,
|
|
69
74
|
slug: true,
|
|
70
75
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recordEvent.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/recordEvent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAUtD,OAAO,EAAE,KAAK,aAAa,EAAkB,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"recordEvent.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/recordEvent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAUtD,OAAO,EAAE,KAAK,aAAa,EAAkB,MAAM,iBAAiB,CAAA;AASpE,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,aAAa,GAAG,IAAI,CAAA;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CACtC,CAAA;AAgBD,6DAA6D;AAC7D,wBAAsB,SAAS,CAAC,IAAI,EAAE,cAAc,wEAqFnD;AAED,4CAA4C;AAC5C,eAAO,MAAM,WAAW,kBAAY,CAAA;AAEpC;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,wEAsDA;AAED,6FAA6F;AAC7F,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,iFA0DA"}
|
|
@@ -8,6 +8,7 @@ import { applyMetricBalanceDelta } from './metricBalances.js';
|
|
|
8
8
|
import { transitionLogData, transitionToId } from './logData.js';
|
|
9
9
|
import { loadMetricDoc } from './resolveMetricValue.js';
|
|
10
10
|
import { resolveCatalogId } from './resolveCatalog.js';
|
|
11
|
+
import { snapshotActorDerivedTiers } from './actorTierSnapshot.js';
|
|
11
12
|
import { resolveCurrentTier } from './resolveCurrentTier.js';
|
|
12
13
|
import { userScopeWhere } from './where.js';
|
|
13
14
|
async function catalogId(args) {
|
|
@@ -65,6 +66,14 @@ export async function recordLog(args) {
|
|
|
65
66
|
throw new APIError(`This log type only allows subjects from: ${allowed.join(', ')}.`, 400);
|
|
66
67
|
}
|
|
67
68
|
}
|
|
69
|
+
const actorTiers = flags.snapshotActorTiers && args.actorId
|
|
70
|
+
? await snapshotActorDerivedTiers({
|
|
71
|
+
payload: args.payload,
|
|
72
|
+
req: args.req,
|
|
73
|
+
userId: args.actorId,
|
|
74
|
+
logScopeId: scopeId,
|
|
75
|
+
})
|
|
76
|
+
: undefined;
|
|
68
77
|
return args.payload.create({
|
|
69
78
|
collection: collectionOf('logs'),
|
|
70
79
|
data: {
|
|
@@ -76,6 +85,7 @@ export async function recordLog(args) {
|
|
|
76
85
|
change: args.change,
|
|
77
86
|
reason: args.reason,
|
|
78
87
|
...(subject ? { subject } : {}),
|
|
88
|
+
...(actorTiers && actorTiers.length > 0 ? { actorTiers } : {}),
|
|
79
89
|
data: args.data ?? undefined,
|
|
80
90
|
},
|
|
81
91
|
overrideAccess: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dappermountain/payload-plugin-achievements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|