@dappermountain/payload-plugin-achievements 0.2.0 → 0.2.2
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 +29 -1
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/reconcile.d.ts +18 -0
- package/dist/cli/reconcile.d.ts.map +1 -0
- package/dist/cli/reconcile.js +90 -0
- package/dist/collections/Achievements/hooks/afterRequiresReviewDisabled.d.ts +7 -0
- package/dist/collections/Achievements/hooks/afterRequiresReviewDisabled.d.ts.map +1 -0
- package/dist/collections/Achievements/hooks/afterRequiresReviewDisabled.js +15 -0
- package/dist/collections/Achievements/hooks/index.d.ts.map +1 -1
- package/dist/collections/Achievements/hooks/index.js +2 -0
- package/dist/collections/Achievements/index.d.ts.map +1 -1
- package/dist/collections/Achievements/index.js +3 -0
- package/dist/collections/Logs/index.d.ts.map +1 -1
- package/dist/collections/Logs/index.js +3 -1
- package/dist/collections/Tiers/hooks/afterRequiresReviewDisabled.d.ts +7 -0
- package/dist/collections/Tiers/hooks/afterRequiresReviewDisabled.d.ts.map +1 -0
- package/dist/collections/Tiers/hooks/afterRequiresReviewDisabled.js +15 -0
- package/dist/collections/Tiers/hooks/index.d.ts.map +1 -1
- package/dist/collections/Tiers/hooks/index.js +2 -0
- package/dist/collections/Tiers/index.js +1 -1
- package/dist/endpoints/reconcile.d.ts +2 -2
- package/dist/endpoints/reconcile.d.ts.map +1 -1
- package/dist/endpoints/reconcile.js +23 -20
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/services/achievement/grantSideEffects.d.ts.map +1 -1
- package/dist/services/achievement/grantSideEffects.js +9 -8
- package/dist/services/achievement/index.d.ts +5 -1
- package/dist/services/achievement/index.d.ts.map +1 -1
- package/dist/services/achievement/index.js +2 -0
- package/dist/services/achievement/logData.d.ts +31 -0
- package/dist/services/achievement/logData.d.ts.map +1 -0
- package/dist/services/achievement/logData.js +30 -0
- package/dist/services/achievement/reconcile.d.ts +19 -7
- package/dist/services/achievement/reconcile.d.ts.map +1 -1
- package/dist/services/achievement/reconcile.js +151 -26
- package/dist/services/achievement/recordEvent.d.ts.map +1 -1
- package/dist/services/achievement/recordEvent.js +20 -7
- package/dist/services/achievement/releaseReviewGate.d.ts +23 -0
- package/dist/services/achievement/releaseReviewGate.d.ts.map +1 -0
- package/dist/services/achievement/releaseReviewGate.js +71 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -261,10 +261,38 @@ const tier = await resolveCurrentTier({ payload, userId, scopeId })
|
|
|
261
261
|
|
|
262
262
|
// Repair users whose grants predate side-effect hooks (idempotent)
|
|
263
263
|
await reconcileProgression({ payload })
|
|
264
|
+
|
|
265
|
+
// Optional filters (ids or slugs)
|
|
266
|
+
await reconcileProgression({
|
|
267
|
+
payload,
|
|
268
|
+
userId: '…',
|
|
269
|
+
scopeId: '…',
|
|
270
|
+
achievementSlug: 'ring-grim',
|
|
271
|
+
tierSlug: 'ring-solid',
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
// Host CLI (only boot config in the app — flags live in the plugin):
|
|
275
|
+
// bun --env-file=.env ./scripts/reconcile-achievements.ts -- --user=<id> --achievement=ring-grim
|
|
276
|
+
import { runReconcileCli } from '@dappermountain/payload-plugin-achievements/cli'
|
|
277
|
+
await runReconcileCli({ config })
|
|
264
278
|
```
|
|
265
279
|
|
|
266
280
|
Pass catalog **slugs** (or ids) for `type` and `metric`. Metric totals are the sum of `change` on matching logs. Creating or deleting **grants** (Admin or API) writes `achievement.granted` / `achievement.revoked`, syncs composed achievements, and may open tier requests / write `tier.changed`.
|
|
267
281
|
|
|
282
|
+
Engine log `data` for those system events uses a shared transition envelope:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
{ from: { id, slug? } | null, to: { id, slug? } | null }
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
| type | `from` | `to` |
|
|
289
|
+
| --- | --- | --- |
|
|
290
|
+
| `tier.changed` | previous tier (null on first climb) | current tier |
|
|
291
|
+
| `achievement.granted` | `null` | achievement |
|
|
292
|
+
| `achievement.revoked` | achievement | `null` |
|
|
293
|
+
|
|
294
|
+
Turning **Requires review** off on a definition or tier **approves pending requests** for that row only (each waiting user goes through the existing request hooks).
|
|
295
|
+
|
|
268
296
|
**Admin:** Grants list includes **Repair progression**, which `POST`s the reconcile endpoint (requires `canReview`).
|
|
269
297
|
|
|
270
298
|
## REST surface
|
|
@@ -272,7 +300,7 @@ Pass catalog **slugs** (or ids) for `type` and `metric`. Metric totals are the s
|
|
|
272
300
|
| Goal | Request |
|
|
273
301
|
| --- | --- |
|
|
274
302
|
| Current user snapshot | `GET /api/achievements/me?scope=<scopeId>&limit=10&page=1` |
|
|
275
|
-
| Repair progression | `POST /api/achievements/reconcile` (reviewer; optional `{ userId, scopeId, limit }`) |
|
|
303
|
+
| Repair progression | `POST /api/achievements/reconcile` (reviewer; optional `{ userId, scopeId, achievementId\|achievementSlug, tierId\|tierSlug, limit }`) |
|
|
276
304
|
| List grants | `GET /api/achievement-grants?where[user][equals]=<userId>` |
|
|
277
305
|
| Log history | `GET /api/achievement-logs?where[user][equals]=<userId>` |
|
|
278
306
|
| Request an achievement | `POST /api/achievement-requests` with `{ achievement }` |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,eAAe,GAChB,MAAM,aAAa,CAAA;AACpB,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { parseReconcileCliArgs, printReconcileCliHelp, runReconcileCli, } from './reconcile';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type SanitizedConfig } from 'payload';
|
|
2
|
+
import { type ReconcileProgressionArgs, type ReconcileProgressionResult } from '../services/achievement/reconcile';
|
|
3
|
+
export type ReconcileCliFilters = Omit<ReconcileProgressionArgs, 'payload' | 'req'>;
|
|
4
|
+
/** Parse `--user`, `--achievement`, `--tier`, `--scope`, `--limit` (ids or slugs). */
|
|
5
|
+
export declare function parseReconcileCliArgs(argv?: string[]): ReconcileCliFilters;
|
|
6
|
+
export declare function printReconcileCliHelp(): void;
|
|
7
|
+
/**
|
|
8
|
+
* Boot Payload from the host config and repair progression.
|
|
9
|
+
* Host scripts should only import config and call this (filters via argv or `filters`).
|
|
10
|
+
*/
|
|
11
|
+
export declare function runReconcileCli(args: {
|
|
12
|
+
config: SanitizedConfig | Promise<SanitizedConfig>;
|
|
13
|
+
argv?: string[];
|
|
14
|
+
filters?: ReconcileCliFilters;
|
|
15
|
+
/** When false, return the result instead of exiting the process. Default true. */
|
|
16
|
+
exit?: boolean;
|
|
17
|
+
}): Promise<ReconcileProgressionResult>;
|
|
18
|
+
//# sourceMappingURL=reconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/cli/reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,SAAS,CAAA;AAE1D,OAAO,EAEL,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,EAChC,MAAM,mCAAmC,CAAA;AAE1C,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,wBAAwB,EAAE,SAAS,GAAG,KAAK,CAAC,CAAA;AA2BnF,sFAAsF;AACtF,wBAAgB,qBAAqB,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,mBAAmB,CAUjG;AAED,wBAAgB,qBAAqB,IAAI,IAAI,CAc5C;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;IAClD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,OAAO,CAAC,EAAE,mBAAmB,CAAA;IAC7B,kFAAkF;IAClF,IAAI,CAAC,EAAE,OAAO,CAAA;CACf,GAAG,OAAO,CAAC,0BAA0B,CAAC,CA2BtC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { getPayload } from 'payload';
|
|
2
|
+
import { reconcileProgression, } from '../services/achievement/reconcile';
|
|
3
|
+
function readFlag(argv, names) {
|
|
4
|
+
for (const name of names) {
|
|
5
|
+
const prefix = `${name}=`;
|
|
6
|
+
for (const arg of argv) {
|
|
7
|
+
if (arg === name) {
|
|
8
|
+
const idx = argv.indexOf(arg);
|
|
9
|
+
const next = argv[idx + 1];
|
|
10
|
+
if (next && !next.startsWith('-'))
|
|
11
|
+
return next;
|
|
12
|
+
}
|
|
13
|
+
if (arg.startsWith(prefix)) {
|
|
14
|
+
const value = arg.slice(prefix.length);
|
|
15
|
+
if (value)
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
function readLimit(argv) {
|
|
23
|
+
const raw = readFlag(argv, ['--limit', '-n']);
|
|
24
|
+
if (!raw)
|
|
25
|
+
return undefined;
|
|
26
|
+
const n = Number(raw);
|
|
27
|
+
return Number.isFinite(n) ? Math.floor(n) : undefined;
|
|
28
|
+
}
|
|
29
|
+
/** Parse `--user`, `--achievement`, `--tier`, `--scope`, `--limit` (ids or slugs). */
|
|
30
|
+
export function parseReconcileCliArgs(argv = process.argv.slice(2)) {
|
|
31
|
+
return {
|
|
32
|
+
userId: readFlag(argv, ['--user', '--userId']),
|
|
33
|
+
scopeId: readFlag(argv, ['--scope', '--scopeId']) ?? null,
|
|
34
|
+
achievementId: readFlag(argv, ['--achievementId']),
|
|
35
|
+
achievementSlug: readFlag(argv, ['--achievement', '--achievementSlug']),
|
|
36
|
+
tierId: readFlag(argv, ['--tierId']),
|
|
37
|
+
tierSlug: readFlag(argv, ['--tier', '--tierSlug']),
|
|
38
|
+
limit: readLimit(argv),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function printReconcileCliHelp() {
|
|
42
|
+
// eslint-disable-next-line no-console -- CLI help
|
|
43
|
+
console.log(`Usage: achievements reconcile [options]
|
|
44
|
+
|
|
45
|
+
Options:
|
|
46
|
+
--user, --userId <id>
|
|
47
|
+
--scope, --scopeId <id>
|
|
48
|
+
--achievement, --achievementSlug <slug>
|
|
49
|
+
--achievementId <id>
|
|
50
|
+
--tier, --tierSlug <slug>
|
|
51
|
+
--tierId <id>
|
|
52
|
+
--limit, -n <number>
|
|
53
|
+
--help, -h
|
|
54
|
+
`);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Boot Payload from the host config and repair progression.
|
|
58
|
+
* Host scripts should only import config and call this (filters via argv or `filters`).
|
|
59
|
+
*/
|
|
60
|
+
export async function runReconcileCli(args) {
|
|
61
|
+
const argv = args.argv ?? process.argv.slice(2);
|
|
62
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
63
|
+
printReconcileCliHelp();
|
|
64
|
+
if (args.exit !== false)
|
|
65
|
+
process.exit(0);
|
|
66
|
+
return {
|
|
67
|
+
usersScanned: 0,
|
|
68
|
+
grantedLogsBackfilled: 0,
|
|
69
|
+
compositesGranted: 0,
|
|
70
|
+
achievementRequestsEnsured: 0,
|
|
71
|
+
tierRequestsEnsured: 0,
|
|
72
|
+
tierLogsWritten: 0,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const filters = args.filters ?? parseReconcileCliArgs(argv);
|
|
76
|
+
const payload = await getPayload({ config: args.config });
|
|
77
|
+
try {
|
|
78
|
+
const result = await reconcileProgression({ payload, ...filters });
|
|
79
|
+
payload.logger.info({ msg: 'achievements reconcile complete', ...filters, ...result });
|
|
80
|
+
if (args.exit !== false)
|
|
81
|
+
process.exit(0);
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
payload.logger.error({ err: error, msg: 'achievements reconcile failed' });
|
|
86
|
+
if (args.exit !== false)
|
|
87
|
+
process.exit(1);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CollectionAfterChangeHook } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* When Requires review is turned off, approve pending requests for this achievement
|
|
4
|
+
* so each waiting user is granted via existing request hooks.
|
|
5
|
+
*/
|
|
6
|
+
export declare const afterRequiresReviewDisabled: CollectionAfterChangeHook;
|
|
7
|
+
//# sourceMappingURL=afterRequiresReviewDisabled.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"afterRequiresReviewDisabled.d.ts","sourceRoot":"","sources":["../../../../src/collections/Achievements/hooks/afterRequiresReviewDisabled.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAA;AAOxD;;;GAGG;AACH,eAAO,MAAM,2BAA2B,EAAE,yBAmBzC,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { didDisableRequiresReview, releasePendingAchievementReviews, } from '../../../services/achievement/releaseReviewGate';
|
|
2
|
+
/**
|
|
3
|
+
* When Requires review is turned off, approve pending requests for this achievement
|
|
4
|
+
* so each waiting user is granted via existing request hooks.
|
|
5
|
+
*/
|
|
6
|
+
export const afterRequiresReviewDisabled = async ({ doc, operation, previousDoc, req, }) => {
|
|
7
|
+
if (operation !== 'update')
|
|
8
|
+
return doc;
|
|
9
|
+
if (!didDisableRequiresReview(previousDoc, doc)) {
|
|
10
|
+
return doc;
|
|
11
|
+
}
|
|
12
|
+
const achievementId = String(doc.id);
|
|
13
|
+
await releasePendingAchievementReviews({ req, achievementId });
|
|
14
|
+
return doc;
|
|
15
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/collections/Achievements/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/collections/Achievements/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAK/C,eAAO,MAAM,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAG3C,CAAA"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { afterRequiresReviewDisabled } from './afterRequiresReviewDisabled';
|
|
1
2
|
import { validateAchievementRules } from './validateEligibilityRules';
|
|
2
3
|
export const hooks = {
|
|
3
4
|
beforeValidate: [validateAchievementRules],
|
|
5
|
+
afterChange: [afterRequiresReviewDisabled],
|
|
4
6
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/Achievements/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAO/C,wBAAgB,2BAA2B,IAAI,gBAAgB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/Achievements/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAO/C,wBAAgB,2BAA2B,IAAI,gBAAgB,CA8D9D"}
|
|
@@ -38,6 +38,9 @@ export function buildAchievementsCollection() {
|
|
|
38
38
|
type: 'checkbox',
|
|
39
39
|
defaultValue: true,
|
|
40
40
|
label: 'Requires review',
|
|
41
|
+
admin: {
|
|
42
|
+
description: 'When turned off, any pending requests for this achievement are approved automatically (one user at a time via existing grant hooks).',
|
|
43
|
+
},
|
|
41
44
|
},
|
|
42
45
|
{
|
|
43
46
|
name: 'eligibilityRules',
|
|
@@ -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;AAUtD,+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;AAUtD,+EAA+E;AAC/E,wBAAgB,mBAAmB,IAAI,gBAAgB,CA6GtD"}
|
|
@@ -104,7 +104,9 @@ export function buildLogsCollection() {
|
|
|
104
104
|
{
|
|
105
105
|
name: 'data',
|
|
106
106
|
type: 'json',
|
|
107
|
-
admin: {
|
|
107
|
+
admin: {
|
|
108
|
+
description: 'Engine transition envelope: `{ from, to }` each `{ id, slug? }` or null (tier.changed, achievement.granted/revoked). Hosts may add extra keys.',
|
|
109
|
+
},
|
|
108
110
|
},
|
|
109
111
|
],
|
|
110
112
|
},
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CollectionAfterChangeHook } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* When Requires review is turned off, approve pending tier requests for this step
|
|
4
|
+
* so each waiting user gets a tier.changed audit via existing request hooks.
|
|
5
|
+
*/
|
|
6
|
+
export declare const afterRequiresReviewDisabled: CollectionAfterChangeHook;
|
|
7
|
+
//# sourceMappingURL=afterRequiresReviewDisabled.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"afterRequiresReviewDisabled.d.ts","sourceRoot":"","sources":["../../../../src/collections/Tiers/hooks/afterRequiresReviewDisabled.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAA;AAOxD;;;GAGG;AACH,eAAO,MAAM,2BAA2B,EAAE,yBAmBzC,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { didDisableRequiresReview, releasePendingTierReviews, } from '../../../services/achievement/releaseReviewGate';
|
|
2
|
+
/**
|
|
3
|
+
* When Requires review is turned off, approve pending tier requests for this step
|
|
4
|
+
* so each waiting user gets a tier.changed audit via existing request hooks.
|
|
5
|
+
*/
|
|
6
|
+
export const afterRequiresReviewDisabled = async ({ doc, operation, previousDoc, req, }) => {
|
|
7
|
+
if (operation !== 'update')
|
|
8
|
+
return doc;
|
|
9
|
+
if (!didDisableRequiresReview(previousDoc, doc)) {
|
|
10
|
+
return doc;
|
|
11
|
+
}
|
|
12
|
+
const tierId = String(doc.id);
|
|
13
|
+
await releasePendingTierReviews({ req, tierId });
|
|
14
|
+
return doc;
|
|
15
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/collections/Tiers/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/collections/Tiers/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAK/C,eAAO,MAAM,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAG3C,CAAA"}
|
|
@@ -72,7 +72,7 @@ export function buildTiersCollection() {
|
|
|
72
72
|
type: 'checkbox',
|
|
73
73
|
defaultValue: false,
|
|
74
74
|
admin: {
|
|
75
|
-
description: 'When enabled, unlock rules alone are not enough — an approved tier request is required before this step becomes the member’s current tier.',
|
|
75
|
+
description: 'When enabled, unlock rules alone are not enough — an approved tier request is required before this step becomes the member’s current tier. Turning it off approves pending requests for this tier automatically.',
|
|
76
76
|
},
|
|
77
77
|
},
|
|
78
78
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Endpoint } from 'payload';
|
|
2
2
|
/**
|
|
3
|
-
* `POST` repair progression from existing grants.
|
|
4
|
-
* Body (optional): `{ userId?, scopeId?, limit? }`.
|
|
3
|
+
* `POST` repair progression from existing grants / requests.
|
|
4
|
+
* Body (optional): `{ userId?, scopeId?, achievementId?, achievementSlug?, tierId?, tierSlug?, limit? }`.
|
|
5
5
|
* Requires host `canReview`.
|
|
6
6
|
*/
|
|
7
7
|
export declare function buildReconcileEndpoint(path: string): Endpoint;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/endpoints/reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAkB,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/endpoints/reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAkB,MAAM,SAAS,CAAA;AAoCvD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CA2C7D"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getUserId } from '../access/helpers/index';
|
|
2
2
|
import { getAchievementOptions } from '../options-store';
|
|
3
|
-
import { reconcileProgression
|
|
3
|
+
import { reconcileProgression } from '../services/achievement/reconcile';
|
|
4
4
|
function requestUrl(req) {
|
|
5
5
|
return req.url ? new URL(req.url, 'http://local') : null;
|
|
6
6
|
}
|
|
@@ -17,9 +17,20 @@ async function readBody(req) {
|
|
|
17
17
|
}
|
|
18
18
|
return {};
|
|
19
19
|
}
|
|
20
|
+
function stringParam(body, url, keys) {
|
|
21
|
+
for (const key of keys) {
|
|
22
|
+
const fromBody = body[key];
|
|
23
|
+
if (typeof fromBody === 'string' && fromBody.length > 0)
|
|
24
|
+
return fromBody;
|
|
25
|
+
const fromUrl = url?.searchParams.get(key);
|
|
26
|
+
if (fromUrl && fromUrl.length > 0)
|
|
27
|
+
return fromUrl;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
20
31
|
/**
|
|
21
|
-
* `POST` repair progression from existing grants.
|
|
22
|
-
* Body (optional): `{ userId?, scopeId?, limit? }`.
|
|
32
|
+
* `POST` repair progression from existing grants / requests.
|
|
33
|
+
* Body (optional): `{ userId?, scopeId?, achievementId?, achievementSlug?, tierId?, tierSlug?, limit? }`.
|
|
23
34
|
* Requires host `canReview`.
|
|
24
35
|
*/
|
|
25
36
|
export function buildReconcileEndpoint(path) {
|
|
@@ -37,31 +48,23 @@ export function buildReconcileEndpoint(path) {
|
|
|
37
48
|
}
|
|
38
49
|
const body = await readBody(req);
|
|
39
50
|
const url = requestUrl(req);
|
|
40
|
-
const
|
|
41
|
-
url?.searchParams.get('scopeId') ||
|
|
42
|
-
url?.searchParams.get('scope') ||
|
|
43
|
-
null;
|
|
44
|
-
const scopeId = scopeIdRaw && scopeIdRaw.length > 0 ? scopeIdRaw : null;
|
|
51
|
+
const scopeId = stringParam(body, url, ['scopeId', 'scope']);
|
|
45
52
|
if (!(await canReview(req.user, scopeId))) {
|
|
46
53
|
return Response.json({ errors: [{ message: 'Forbidden' }] }, { status: 403 });
|
|
47
54
|
}
|
|
48
|
-
const
|
|
49
|
-
const limit = typeof
|
|
50
|
-
? Math.floor(
|
|
55
|
+
const limitRaw = body.limit;
|
|
56
|
+
const limit = typeof limitRaw === 'number' && Number.isFinite(limitRaw)
|
|
57
|
+
? Math.floor(limitRaw)
|
|
51
58
|
: undefined;
|
|
52
|
-
if (userId) {
|
|
53
|
-
const result = await reconcileUserProgression({
|
|
54
|
-
payload: req.payload,
|
|
55
|
-
req,
|
|
56
|
-
userId,
|
|
57
|
-
scopeId,
|
|
58
|
-
});
|
|
59
|
-
return Response.json({ ok: true, usersScanned: 1, ...result });
|
|
60
|
-
}
|
|
61
59
|
const result = await reconcileProgression({
|
|
62
60
|
payload: req.payload,
|
|
63
61
|
req,
|
|
64
62
|
scopeId,
|
|
63
|
+
userId: stringParam(body, url, ['userId', 'user']),
|
|
64
|
+
achievementId: stringParam(body, url, ['achievementId']),
|
|
65
|
+
achievementSlug: stringParam(body, url, ['achievementSlug', 'achievement']),
|
|
66
|
+
tierId: stringParam(body, url, ['tierId']),
|
|
67
|
+
tierSlug: stringParam(body, url, ['tierSlug', 'tier']),
|
|
65
68
|
limit,
|
|
66
69
|
});
|
|
67
70
|
return Response.json({ ok: true, ...result });
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export type { EngineEventTypeSlug, EngineMetricSlug } from './catalog';
|
|
|
3
3
|
export { achievementPlugin } from './plugin';
|
|
4
4
|
export { seedAchievementCatalog, seedAchievements } from './seed/index';
|
|
5
5
|
export type { AchievementSeedAchievement, AchievementSeedCatalogRow, AchievementSeedInput, AchievementSeedLocalizedText, AchievementSeedTier, } from './seed/index';
|
|
6
|
-
export { evaluateRules, evaluateRuleProgress, collectAchievementCompleteRefs, buildCatalogProgressGroups, getUserProgress, grantAchievement, recordEvent, recordLog, recordMetricChange, reconcileProgression, reconcileUserProgression, resolveCurrentTier, resolveTierProgress, reviewAchievementRequest, submitAchievementRequest, syncTierChangedLog, syncTierProgression, syncCompositeProgression, validateRuleGroup, validateRuleNode, } from './services/achievement/index';
|
|
7
|
-
export type { ResolvedTier, RuleGroup, TierLadderProgress, CatalogProgressGroup, CatalogProgressItem, ReconcileProgressionResult, } from './services/achievement/index';
|
|
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';
|
|
7
|
+
export type { ResolvedTier, RuleGroup, TierLadderProgress, CatalogProgressGroup, CatalogProgressItem, CatalogRef, TransitionLogData, ReconcileProgressionArgs, ReconcileProgressionResult, ReleaseReviewGateResult, } from './services/achievement/index';
|
|
8
8
|
export type { BuiltInEventType, BuiltInRuleType, AchievementCollectionKey, AchievementCollectionOverride, AchievementCollectionsOptions, AchievementEndpointsOptions, AchievementPluginOptions, AchievementRuleType, AchievementScopeConfig, AchievementUsersOptions, } from './types';
|
|
9
9
|
export { DEFAULT_COLLECTION_BASE_SLUGS, DEFAULT_COLLECTION_PREFIX, DEFAULT_ADMIN_GROUP } from './collections/slugs';
|
|
10
10
|
export { DEFAULT_ME_ENDPOINT_PATH, DEFAULT_RECONCILE_ENDPOINT_PATH } from './defaults';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,+BAA+B,EAC/B,kBAAkB,EAClB,cAAc,GACf,MAAM,WAAW,CAAA;AAClB,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AACtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAC5C,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACvE,YAAY,EACV,0BAA0B,EAC1B,yBAAyB,EACzB,oBAAoB,EACpB,4BAA4B,EAC5B,mBAAmB,GACpB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,8BAA8B,EAC9B,0BAA0B,EAC1B,eAAe,EACf,gBAAgB,EAChB,WAAW,EACX,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,EACxB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,8BAA8B,CAAA;AACrC,YAAY,EACV,YAAY,EACZ,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,0BAA0B,
|
|
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,WAAW,CAAA;AAClB,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AACtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAC5C,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACvE,YAAY,EACV,0BAA0B,EAC1B,yBAAyB,EACzB,oBAAoB,EACpB,4BAA4B,EAC5B,mBAAmB,GACpB,MAAM,cAAc,CAAA;AACrB,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,8BAA8B,CAAA;AACrC,YAAY,EACV,YAAY,EACZ,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,UAAU,EACV,iBAAiB,EACjB,wBAAwB,EACxB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,8BAA8B,CAAA;AACrC,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,wBAAwB,EACxB,6BAA6B,EAC7B,6BAA6B,EAC7B,2BAA2B,EAC3B,wBAAwB,EACxB,mBAAmB,EACnB,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,6BAA6B,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACnH,OAAO,EAAE,wBAAwB,EAAE,+BAA+B,EAAE,MAAM,YAAY,CAAA;AACtF,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EAAE,0BAA0B,EAAE,MAAM,WAAW,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { DEFAULT_ACHIEVEMENT_METRIC, DEFAULT_ACHIEVEMENT_METRIC_SLUG, ENGINE_EVENT_TYPES, ENGINE_METRICS, } from './catalog';
|
|
2
2
|
export { achievementPlugin } from './plugin';
|
|
3
3
|
export { seedAchievementCatalog, seedAchievements } from './seed/index';
|
|
4
|
-
export { evaluateRules, evaluateRuleProgress, collectAchievementCompleteRefs, buildCatalogProgressGroups, getUserProgress, grantAchievement, recordEvent, recordLog, recordMetricChange, reconcileProgression, reconcileUserProgression, resolveCurrentTier, resolveTierProgress, reviewAchievementRequest, submitAchievementRequest, syncTierChangedLog, syncTierProgression, syncCompositeProgression, validateRuleGroup, validateRuleNode, } from './services/achievement/index';
|
|
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';
|
|
5
5
|
export { DEFAULT_COLLECTION_BASE_SLUGS, DEFAULT_COLLECTION_PREFIX, DEFAULT_ADMIN_GROUP } from './collections/slugs';
|
|
6
6
|
export { DEFAULT_ME_ENDPOINT_PATH, DEFAULT_RECONCILE_ENDPOINT_PATH } from './defaults';
|
|
7
7
|
export { collectionOf, slugOf } from './collections/helpers';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grantSideEffects.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/grantSideEffects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAS,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"grantSideEffects.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/grantSideEffects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAS,MAAM,SAAS,CAAA;AAWpD,OAAO,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAStG,wEAAwE;AACxE,wBAAsB,cAAc,CAAC,IAAI,EAAE;IACzC,GAAG,EAAE,cAAc,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CA+BnC;AAED,6EAA6E;AAC7E,wBAAsB,wBAAwB,CAAC,IAAI,EAAE;IACnD,GAAG,EAAE,cAAc,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,GAAG,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAuC9D;AAED;;GAEG;AACH,wBAAsB,wBAAwB,CAAC,IAAI,EAAE;IACnD,GAAG,EAAE,cAAc,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAsElD;AAED,oFAAoF;AACpF,wBAAsB,0BAA0B,CAAC,IAAI,EAAE;IACrD,GAAG,EAAE,cAAc,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB,GAAG,OAAO,CAAC,MAAM,CAAC,CAiDlB;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE;IAC/C,GAAG,EAAE,cAAc,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+BhB;AAED,wBAAsB,oBAAoB,CAAC,IAAI,EAAE;IAC/C,GAAG,EAAE,cAAc,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuChB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { collectionOf } from '../../collections/helpers';
|
|
2
2
|
import { CTX_CASCADING_REVOKE, CTX_SYSTEM_REQUEST } from './contextFlags';
|
|
3
3
|
import { evaluateRules, ruleGroupHasProgressRequirements } from './evaluateRules';
|
|
4
|
+
import { transitionLogData } from './logData';
|
|
4
5
|
import { recordLog, syncTierChangedLog } from './recordEvent';
|
|
5
6
|
import { relationId } from './relationId';
|
|
6
7
|
import { syncTierProgression } from './tierProgression';
|
|
@@ -209,10 +210,10 @@ export async function runAfterGrantCreated(args) {
|
|
|
209
210
|
userId: args.userId,
|
|
210
211
|
scopeId: args.scopeId,
|
|
211
212
|
type: 'achievement.granted',
|
|
212
|
-
data: {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
},
|
|
213
|
+
data: transitionLogData({
|
|
214
|
+
from: null,
|
|
215
|
+
to: { id: args.achievementId, slug: args.achievementSlug },
|
|
216
|
+
}),
|
|
216
217
|
});
|
|
217
218
|
await syncCompositeProgression({
|
|
218
219
|
req: args.req,
|
|
@@ -238,10 +239,10 @@ export async function runAfterGrantDeleted(args) {
|
|
|
238
239
|
userId: args.userId,
|
|
239
240
|
scopeId: args.scopeId,
|
|
240
241
|
type: 'achievement.revoked',
|
|
241
|
-
data: {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
},
|
|
242
|
+
data: transitionLogData({
|
|
243
|
+
from: { id: args.achievementId, slug: args.achievementSlug },
|
|
244
|
+
to: null,
|
|
245
|
+
}),
|
|
245
246
|
});
|
|
246
247
|
if (args.cascading)
|
|
247
248
|
return;
|
|
@@ -14,5 +14,9 @@ export { validateRuleGroup, validateRuleNode } from './validateRules';
|
|
|
14
14
|
export { syncCompositeProgression } from './grantSideEffects';
|
|
15
15
|
export { syncTierProgression } from './tierProgression';
|
|
16
16
|
export { reconcileProgression, reconcileUserProgression } from './reconcile';
|
|
17
|
-
export type { ReconcileProgressionResult } from './reconcile';
|
|
17
|
+
export type { ReconcileProgressionArgs, ReconcileProgressionResult } from './reconcile';
|
|
18
|
+
export { didDisableRequiresReview, releasePendingAchievementReviews, releasePendingTierReviews, } from './releaseReviewGate';
|
|
19
|
+
export type { ReleaseReviewGateResult } from './releaseReviewGate';
|
|
20
|
+
export { catalogRef, transitionLogData, transitionToId, } from './logData';
|
|
21
|
+
export type { CatalogRef, TransitionLogData } from './logData';
|
|
18
22
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -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,iBAAiB,CAAA;AACzH,YAAY,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AACxE,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAA;AACzE,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAA;AAC7F,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAC9F,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAC/D,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAA;AACvF,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AACrE,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AAC5E,YAAY,EAAE,0BAA0B,EAAE,MAAM,aAAa,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,iBAAiB,CAAA;AACzH,YAAY,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AACxE,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAA;AACzE,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAA;AAC7F,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAC9F,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAC/D,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAA;AACvF,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AACrE,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AAC5E,YAAY,EAAE,wBAAwB,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AACvF,OAAO,EACL,wBAAwB,EACxB,gCAAgC,EAChC,yBAAyB,GAC1B,MAAM,qBAAqB,CAAA;AAC5B,YAAY,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;AAClE,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,cAAc,GACf,MAAM,WAAW,CAAA;AAClB,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA"}
|
|
@@ -10,3 +10,5 @@ export { validateRuleGroup, validateRuleNode } from './validateRules';
|
|
|
10
10
|
export { syncCompositeProgression } from './grantSideEffects';
|
|
11
11
|
export { syncTierProgression } from './tierProgression';
|
|
12
12
|
export { reconcileProgression, reconcileUserProgression } from './reconcile';
|
|
13
|
+
export { didDisableRequiresReview, releasePendingAchievementReviews, releasePendingTierReviews, } from './releaseReviewGate';
|
|
14
|
+
export { catalogRef, transitionLogData, transitionToId, } from './logData';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared engine shape for achievement-log `data` on system event types.
|
|
3
|
+
* Hosts may add extra keys; readers should treat unknown keys as opaque.
|
|
4
|
+
*
|
|
5
|
+
* Transition events always use `from` / `to` (either side may be null):
|
|
6
|
+
* - `tier.changed` — previous ladder step → current step
|
|
7
|
+
* - `achievement.granted` — `from: null`, `to: achievement`
|
|
8
|
+
* - `achievement.revoked` — `from: achievement`, `to: null`
|
|
9
|
+
*/
|
|
10
|
+
export type CatalogRef = {
|
|
11
|
+
id: string;
|
|
12
|
+
slug?: string;
|
|
13
|
+
};
|
|
14
|
+
export type TransitionLogData = {
|
|
15
|
+
from: CatalogRef | null;
|
|
16
|
+
to: CatalogRef | null;
|
|
17
|
+
};
|
|
18
|
+
export declare function catalogRef(id: string, slug?: string | null): CatalogRef;
|
|
19
|
+
export declare function transitionLogData(args: {
|
|
20
|
+
from?: {
|
|
21
|
+
id: string;
|
|
22
|
+
slug?: string | null;
|
|
23
|
+
} | null;
|
|
24
|
+
to?: {
|
|
25
|
+
id: string;
|
|
26
|
+
slug?: string | null;
|
|
27
|
+
} | null;
|
|
28
|
+
}): TransitionLogData;
|
|
29
|
+
/** Read the “current” catalog id from a log `data` blob (new or legacy shapes). */
|
|
30
|
+
export declare function transitionToId(data: unknown): string | null;
|
|
31
|
+
//# sourceMappingURL=logData.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logData.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/logData.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,UAAU,GAAG,IAAI,CAAA;IACvB,EAAE,EAAE,UAAU,GAAG,IAAI,CAAA;CACtB,CAAA;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,UAAU,CAKvE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,IAAI,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAA;IAClD,EAAE,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAA;CACjD,GAAG,iBAAiB,CAKpB;AAED,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAY3D"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function catalogRef(id, slug) {
|
|
2
|
+
return {
|
|
3
|
+
id,
|
|
4
|
+
...(typeof slug === 'string' && slug.length > 0 ? { slug } : {}),
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export function transitionLogData(args) {
|
|
8
|
+
return {
|
|
9
|
+
from: args.from ? catalogRef(args.from.id, args.from.slug) : null,
|
|
10
|
+
to: args.to ? catalogRef(args.to.id, args.to.slug) : null,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** Read the “current” catalog id from a log `data` blob (new or legacy shapes). */
|
|
14
|
+
export function transitionToId(data) {
|
|
15
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
16
|
+
return null;
|
|
17
|
+
const record = data;
|
|
18
|
+
const to = record.to;
|
|
19
|
+
if (to && typeof to === 'object' && !Array.isArray(to)) {
|
|
20
|
+
const id = to.id;
|
|
21
|
+
if (typeof id === 'string' && id)
|
|
22
|
+
return id;
|
|
23
|
+
}
|
|
24
|
+
// Legacy tier.changed / achievement.* shapes
|
|
25
|
+
if (typeof record.tier === 'string' && record.tier)
|
|
26
|
+
return record.tier;
|
|
27
|
+
if (typeof record.achievement === 'string' && record.achievement)
|
|
28
|
+
return record.achievement;
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
@@ -7,6 +7,20 @@ export type ReconcileProgressionResult = {
|
|
|
7
7
|
tierRequestsEnsured: number;
|
|
8
8
|
tierLogsWritten: number;
|
|
9
9
|
};
|
|
10
|
+
export type ReconcileProgressionArgs = {
|
|
11
|
+
payload: Payload;
|
|
12
|
+
req?: PayloadRequest;
|
|
13
|
+
/** Limit to one member. */
|
|
14
|
+
userId?: string | null;
|
|
15
|
+
scopeId?: string | null;
|
|
16
|
+
/** Limit discovery to grants / requests for this achievement (id or slug). */
|
|
17
|
+
achievementId?: string | null;
|
|
18
|
+
achievementSlug?: string | null;
|
|
19
|
+
/** Limit discovery to tier requests for this tier (id or slug). */
|
|
20
|
+
tierId?: string | null;
|
|
21
|
+
tierSlug?: string | null;
|
|
22
|
+
limit?: number;
|
|
23
|
+
};
|
|
10
24
|
/** Repair progression for one user (+ optional scope) from current grants + catalog. */
|
|
11
25
|
export declare function reconcileUserProgression(args: {
|
|
12
26
|
payload: Payload;
|
|
@@ -14,11 +28,9 @@ export declare function reconcileUserProgression(args: {
|
|
|
14
28
|
userId: string;
|
|
15
29
|
scopeId?: string | null;
|
|
16
30
|
}): Promise<Omit<ReconcileProgressionResult, 'usersScanned'>>;
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
limit?: number;
|
|
23
|
-
}): Promise<ReconcileProgressionResult>;
|
|
31
|
+
/**
|
|
32
|
+
* Scan matching (user, scope) pairs and repair progression.
|
|
33
|
+
* Optional filters: `userId`, `scopeId`, `achievementId`/`achievementSlug`, `tierId`/`tierSlug`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function reconcileProgression(args: ReconcileProgressionArgs): Promise<ReconcileProgressionResult>;
|
|
24
36
|
//# sourceMappingURL=reconcile.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAS,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAS,MAAM,SAAS,CAAA;AAa7D,MAAM,MAAM,0BAA0B,GAAG;IACvC,YAAY,EAAE,MAAM,CAAA;IACpB,qBAAqB,EAAE,MAAM,CAAA;IAC7B,iBAAiB,EAAE,MAAM,CAAA;IACzB,0BAA0B,EAAE,MAAM,CAAA;IAClC,mBAAmB,EAAE,MAAM,CAAA;IAC3B,eAAe,EAAE,MAAM,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAwFD,wFAAwF;AACxF,wBAAsB,wBAAwB,CAAC,IAAI,EAAE;IACnD,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,IAAI,CAAC,0BAA0B,EAAE,cAAc,CAAC,CAAC,CAoC5D;AA2LD;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,wBAAwB,GAC7B,OAAO,CAAC,0BAA0B,CAAC,CA8CrC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { collectionOf } from '../../collections/helpers';
|
|
2
2
|
import { syncCompositeProgression, } from './grantSideEffects';
|
|
3
|
+
import { transitionLogData, transitionToId } from './logData';
|
|
3
4
|
import { recordLog, syncTierChangedLog } from './recordEvent';
|
|
4
5
|
import { relationId } from './relationId';
|
|
5
6
|
import { resolveCatalogId } from './resolveCatalog';
|
|
@@ -33,7 +34,7 @@ async function backfillGrantedLogs(args) {
|
|
|
33
34
|
},
|
|
34
35
|
});
|
|
35
36
|
const logged = new Set(logs.docs
|
|
36
|
-
.map((log) =>
|
|
37
|
+
.map((log) => transitionToId(log.data))
|
|
37
38
|
.filter((id) => Boolean(id)));
|
|
38
39
|
let written = 0;
|
|
39
40
|
for (const grant of grants.docs) {
|
|
@@ -63,10 +64,12 @@ async function backfillGrantedLogs(args) {
|
|
|
63
64
|
scopeId: args.scopeId,
|
|
64
65
|
type: 'achievement.granted',
|
|
65
66
|
data: {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
...transitionLogData({
|
|
68
|
+
from: null,
|
|
69
|
+
to: { id: achievementId, slug },
|
|
70
|
+
}),
|
|
69
71
|
...(completedAt ? { completedAt } : {}),
|
|
72
|
+
backfilled: true,
|
|
70
73
|
},
|
|
71
74
|
});
|
|
72
75
|
logged.add(achievementId);
|
|
@@ -113,7 +116,140 @@ function scopeKey(scope) {
|
|
|
113
116
|
const id = relationId(scope);
|
|
114
117
|
return id ?? '';
|
|
115
118
|
}
|
|
116
|
-
|
|
119
|
+
function addPair(pairs, userId, scopeId, filterUserId, filterScopeId) {
|
|
120
|
+
if (!userId)
|
|
121
|
+
return;
|
|
122
|
+
if (filterUserId && userId !== filterUserId)
|
|
123
|
+
return;
|
|
124
|
+
if (filterScopeId != null && filterScopeId !== '' && scopeId !== filterScopeId)
|
|
125
|
+
return;
|
|
126
|
+
const key = `${userId}::${scopeKey(scopeId)}`;
|
|
127
|
+
if (!pairs.has(key))
|
|
128
|
+
pairs.set(key, { userId, scopeId });
|
|
129
|
+
}
|
|
130
|
+
async function resolveDefinitionId(args) {
|
|
131
|
+
if (args.id && args.id.length > 0)
|
|
132
|
+
return args.id;
|
|
133
|
+
if (!args.slug || args.slug.length === 0)
|
|
134
|
+
return null;
|
|
135
|
+
const found = await args.payload.find({
|
|
136
|
+
collection: collectionOf(args.collection),
|
|
137
|
+
depth: 0,
|
|
138
|
+
limit: 1,
|
|
139
|
+
overrideAccess: true,
|
|
140
|
+
req: args.req,
|
|
141
|
+
where: { slug: { equals: args.slug } },
|
|
142
|
+
});
|
|
143
|
+
return relationId(found.docs[0]?.id) ?? null;
|
|
144
|
+
}
|
|
145
|
+
async function collectPairs(args) {
|
|
146
|
+
const pairs = new Map();
|
|
147
|
+
const filterUserId = args.userId?.trim() || null;
|
|
148
|
+
const filterScopeId = args.scopeId ?? null;
|
|
149
|
+
const achievementId = await resolveDefinitionId({
|
|
150
|
+
payload: args.payload,
|
|
151
|
+
req: args.req,
|
|
152
|
+
collection: 'achievements',
|
|
153
|
+
id: args.achievementId,
|
|
154
|
+
slug: args.achievementSlug,
|
|
155
|
+
});
|
|
156
|
+
const tierId = await resolveDefinitionId({
|
|
157
|
+
payload: args.payload,
|
|
158
|
+
req: args.req,
|
|
159
|
+
collection: 'tiers',
|
|
160
|
+
id: args.tierId,
|
|
161
|
+
slug: args.tierSlug,
|
|
162
|
+
});
|
|
163
|
+
if (filterUserId && !achievementId && !tierId) {
|
|
164
|
+
addPair(pairs, filterUserId, filterScopeId);
|
|
165
|
+
return [...pairs.values()];
|
|
166
|
+
}
|
|
167
|
+
if (achievementId) {
|
|
168
|
+
const grantWhere = {
|
|
169
|
+
and: [
|
|
170
|
+
{ achievement: { equals: achievementId } },
|
|
171
|
+
...(filterUserId ? [{ user: { equals: filterUserId } }] : []),
|
|
172
|
+
...(filterScopeId
|
|
173
|
+
? [{ scope: { equals: filterScopeId } }]
|
|
174
|
+
: []),
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
const grants = await args.payload.find({
|
|
178
|
+
collection: collectionOf('grants'),
|
|
179
|
+
depth: 0,
|
|
180
|
+
limit: args.limit,
|
|
181
|
+
pagination: false,
|
|
182
|
+
overrideAccess: true,
|
|
183
|
+
req: args.req,
|
|
184
|
+
where: grantWhere,
|
|
185
|
+
});
|
|
186
|
+
for (const grant of grants.docs) {
|
|
187
|
+
addPair(pairs, relationId(grant.user), relationId(grant.scope), filterUserId, filterScopeId);
|
|
188
|
+
}
|
|
189
|
+
const requestWhere = {
|
|
190
|
+
and: [
|
|
191
|
+
{ achievement: { equals: achievementId } },
|
|
192
|
+
...(filterUserId ? [{ user: { equals: filterUserId } }] : []),
|
|
193
|
+
...(filterScopeId ? [{ scope: { equals: filterScopeId } }] : []),
|
|
194
|
+
],
|
|
195
|
+
};
|
|
196
|
+
const requests = await args.payload.find({
|
|
197
|
+
collection: collectionOf('achievementRequests'),
|
|
198
|
+
depth: 0,
|
|
199
|
+
limit: args.limit,
|
|
200
|
+
pagination: false,
|
|
201
|
+
overrideAccess: true,
|
|
202
|
+
req: args.req,
|
|
203
|
+
where: requestWhere,
|
|
204
|
+
});
|
|
205
|
+
for (const request of requests.docs) {
|
|
206
|
+
addPair(pairs, relationId(request.user), relationId(request.scope), filterUserId, filterScopeId);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (tierId) {
|
|
210
|
+
const requestWhere = {
|
|
211
|
+
and: [
|
|
212
|
+
{ tier: { equals: tierId } },
|
|
213
|
+
...(filterUserId ? [{ user: { equals: filterUserId } }] : []),
|
|
214
|
+
...(filterScopeId ? [{ scope: { equals: filterScopeId } }] : []),
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
const requests = await args.payload.find({
|
|
218
|
+
collection: collectionOf('tierRequests'),
|
|
219
|
+
depth: 0,
|
|
220
|
+
limit: args.limit,
|
|
221
|
+
pagination: false,
|
|
222
|
+
overrideAccess: true,
|
|
223
|
+
req: args.req,
|
|
224
|
+
where: requestWhere,
|
|
225
|
+
});
|
|
226
|
+
for (const request of requests.docs) {
|
|
227
|
+
addPair(pairs, relationId(request.user), relationId(request.scope), filterUserId, filterScopeId);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (!achievementId && !tierId) {
|
|
231
|
+
const where = filterScopeId
|
|
232
|
+
? { scope: { equals: filterScopeId } }
|
|
233
|
+
: undefined;
|
|
234
|
+
const grants = await args.payload.find({
|
|
235
|
+
collection: collectionOf('grants'),
|
|
236
|
+
depth: 0,
|
|
237
|
+
limit: args.limit,
|
|
238
|
+
pagination: false,
|
|
239
|
+
overrideAccess: true,
|
|
240
|
+
req: args.req,
|
|
241
|
+
...(where ? { where } : {}),
|
|
242
|
+
});
|
|
243
|
+
for (const grant of grants.docs) {
|
|
244
|
+
addPair(pairs, relationId(grant.user), relationId(grant.scope), filterUserId, filterScopeId);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return [...pairs.values()];
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Scan matching (user, scope) pairs and repair progression.
|
|
251
|
+
* Optional filters: `userId`, `scopeId`, `achievementId`/`achievementSlug`, `tierId`/`tierSlug`.
|
|
252
|
+
*/
|
|
117
253
|
export async function reconcileProgression(args) {
|
|
118
254
|
const req = args.req ??
|
|
119
255
|
{
|
|
@@ -122,28 +258,17 @@ export async function reconcileProgression(args) {
|
|
|
122
258
|
};
|
|
123
259
|
if (!req.context)
|
|
124
260
|
req.context = {};
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
: undefined;
|
|
128
|
-
const grants = await args.payload.find({
|
|
129
|
-
collection: collectionOf('grants'),
|
|
130
|
-
depth: 0,
|
|
131
|
-
limit: args.limit ?? 5000,
|
|
132
|
-
pagination: false,
|
|
133
|
-
overrideAccess: true,
|
|
261
|
+
const pairs = await collectPairs({
|
|
262
|
+
payload: args.payload,
|
|
134
263
|
req,
|
|
135
|
-
|
|
264
|
+
userId: args.userId,
|
|
265
|
+
scopeId: args.scopeId,
|
|
266
|
+
achievementId: args.achievementId,
|
|
267
|
+
achievementSlug: args.achievementSlug,
|
|
268
|
+
tierId: args.tierId,
|
|
269
|
+
tierSlug: args.tierSlug,
|
|
270
|
+
limit: args.limit ?? 5000,
|
|
136
271
|
});
|
|
137
|
-
const pairs = new Map();
|
|
138
|
-
for (const grant of grants.docs) {
|
|
139
|
-
const userId = relationId(grant.user);
|
|
140
|
-
if (!userId)
|
|
141
|
-
continue;
|
|
142
|
-
const scopeId = relationId(grant.scope);
|
|
143
|
-
const key = `${userId}::${scopeKey(scopeId)}`;
|
|
144
|
-
if (!pairs.has(key))
|
|
145
|
-
pairs.set(key, { userId, scopeId });
|
|
146
|
-
}
|
|
147
272
|
const totals = {
|
|
148
273
|
usersScanned: 0,
|
|
149
274
|
grantedLogsBackfilled: 0,
|
|
@@ -152,7 +277,7 @@ export async function reconcileProgression(args) {
|
|
|
152
277
|
tierRequestsEnsured: 0,
|
|
153
278
|
tierLogsWritten: 0,
|
|
154
279
|
};
|
|
155
|
-
for (const pair of pairs
|
|
280
|
+
for (const pair of pairs) {
|
|
156
281
|
const result = await reconcileUserProgression({
|
|
157
282
|
payload: args.payload,
|
|
158
283
|
req,
|
|
@@ -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;AAWtD,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,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CACtC,CAAA;AAgBD,6DAA6D;AAC7D,wBAAsB,SAAS,CAAC,IAAI,EAAE,cAAc,wEAuCnD;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,wEAiCA;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,
|
|
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;AAWtD,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,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CACtC,CAAA;AAgBD,6DAA6D;AAC7D,wBAAsB,SAAS,CAAC,IAAI,EAAE,cAAc,wEAuCnD;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,wEAiCA;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"}
|
|
@@ -2,8 +2,8 @@ import { APIError } from 'payload';
|
|
|
2
2
|
import { DEFAULT_ACHIEVEMENT_METRIC_SLUG } from '../../catalog';
|
|
3
3
|
import { collectionOf } from '../../collections/helpers';
|
|
4
4
|
import { eventTypeRequiresActor } from './eventTypeRequiresActor';
|
|
5
|
+
import { transitionLogData, transitionToId } from './logData';
|
|
5
6
|
import { resolveCatalogId } from './resolveCatalog';
|
|
6
|
-
import { relationId } from './relationId';
|
|
7
7
|
import { resolveCurrentTier } from './resolveCurrentTier';
|
|
8
8
|
import { userScopeWhere } from './where';
|
|
9
9
|
async function catalogId(args) {
|
|
@@ -113,22 +113,35 @@ export async function syncTierChangedLog(args) {
|
|
|
113
113
|
and: [userScopeWhere(args.userId, scopeId), { type: { equals: typeId } }],
|
|
114
114
|
},
|
|
115
115
|
});
|
|
116
|
-
const
|
|
116
|
+
const latestData = latest.docs[0]?.data;
|
|
117
|
+
const lastTierId = transitionToId(latestData);
|
|
117
118
|
const currentId = current?.id ?? null;
|
|
118
119
|
if (lastTierId === currentId)
|
|
119
120
|
return null;
|
|
120
121
|
if (!currentId)
|
|
121
122
|
return null;
|
|
123
|
+
let fromSlug;
|
|
124
|
+
if (latestData && typeof latestData === 'object' && !Array.isArray(latestData)) {
|
|
125
|
+
const record = latestData;
|
|
126
|
+
const to = record.to;
|
|
127
|
+
if (to && typeof to === 'object' && !Array.isArray(to)) {
|
|
128
|
+
const slug = to.slug;
|
|
129
|
+
if (typeof slug === 'string')
|
|
130
|
+
fromSlug = slug;
|
|
131
|
+
}
|
|
132
|
+
else if (typeof record.tierSlug === 'string') {
|
|
133
|
+
fromSlug = record.tierSlug;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
122
136
|
return recordLog({
|
|
123
137
|
payload: args.payload,
|
|
124
138
|
req: args.req,
|
|
125
139
|
userId: args.userId,
|
|
126
140
|
scopeId,
|
|
127
141
|
type: 'tier.changed',
|
|
128
|
-
data: {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
},
|
|
142
|
+
data: transitionLogData({
|
|
143
|
+
from: lastTierId ? { id: lastTierId, slug: fromSlug } : null,
|
|
144
|
+
to: { id: currentId, slug: current?.slug },
|
|
145
|
+
}),
|
|
133
146
|
});
|
|
134
147
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
export type ReleaseReviewGateResult = {
|
|
3
|
+
requestsApproved: number;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* When a definition stops requiring review, approve each pending request for that
|
|
7
|
+
* definition so existing grant / tier.changed hooks run for that user + scope only.
|
|
8
|
+
*/
|
|
9
|
+
export declare function releasePendingAchievementReviews(args: {
|
|
10
|
+
req: PayloadRequest;
|
|
11
|
+
achievementId: string;
|
|
12
|
+
}): Promise<ReleaseReviewGateResult>;
|
|
13
|
+
export declare function releasePendingTierReviews(args: {
|
|
14
|
+
req: PayloadRequest;
|
|
15
|
+
tierId: string;
|
|
16
|
+
}): Promise<ReleaseReviewGateResult>;
|
|
17
|
+
/** True when `requiresReview` flipped from required → not required. */
|
|
18
|
+
export declare function didDisableRequiresReview(previousDoc: {
|
|
19
|
+
requiresReview?: boolean | null;
|
|
20
|
+
} | null | undefined, doc: {
|
|
21
|
+
requiresReview?: boolean | null;
|
|
22
|
+
} | null | undefined): boolean;
|
|
23
|
+
//# sourceMappingURL=releaseReviewGate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"releaseReviewGate.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/releaseReviewGate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAK7C,MAAM,MAAM,uBAAuB,GAAG;IACpC,gBAAgB,EAAE,MAAM,CAAA;CACzB,CAAA;AAED;;;GAGG;AACH,wBAAsB,gCAAgC,CAAC,IAAI,EAAE;IAC3D,GAAG,EAAE,cAAc,CAAA;IACnB,aAAa,EAAE,MAAM,CAAA;CACtB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAgCnC;AAED,wBAAsB,yBAAyB,CAAC,IAAI,EAAE;IACpD,GAAG,EAAE,cAAc,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC,uBAAuB,CAAC,CA6BnC;AAED,uEAAuE;AACvE,wBAAgB,wBAAwB,CACtC,WAAW,EAAE;IAAE,cAAc,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,EACnE,GAAG,EAAE;IAAE,cAAc,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GAC1D,OAAO,CAET"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { collectionOf } from '../../collections/helpers';
|
|
2
|
+
import { relationId } from './relationId';
|
|
3
|
+
/**
|
|
4
|
+
* When a definition stops requiring review, approve each pending request for that
|
|
5
|
+
* definition so existing grant / tier.changed hooks run for that user + scope only.
|
|
6
|
+
*/
|
|
7
|
+
export async function releasePendingAchievementReviews(args) {
|
|
8
|
+
const pending = await args.req.payload.find({
|
|
9
|
+
collection: collectionOf('achievementRequests'),
|
|
10
|
+
depth: 0,
|
|
11
|
+
limit: 5000,
|
|
12
|
+
pagination: false,
|
|
13
|
+
overrideAccess: true,
|
|
14
|
+
req: args.req,
|
|
15
|
+
where: {
|
|
16
|
+
and: [
|
|
17
|
+
{ achievement: { equals: args.achievementId } },
|
|
18
|
+
{ status: { equals: 'pending' } },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
let requestsApproved = 0;
|
|
23
|
+
for (const doc of pending.docs) {
|
|
24
|
+
const id = relationId(doc.id);
|
|
25
|
+
if (!id)
|
|
26
|
+
continue;
|
|
27
|
+
await args.req.payload.update({
|
|
28
|
+
collection: collectionOf('achievementRequests'),
|
|
29
|
+
id,
|
|
30
|
+
data: { status: 'approved' },
|
|
31
|
+
depth: 0,
|
|
32
|
+
overrideAccess: true,
|
|
33
|
+
req: args.req,
|
|
34
|
+
});
|
|
35
|
+
requestsApproved += 1;
|
|
36
|
+
}
|
|
37
|
+
return { requestsApproved };
|
|
38
|
+
}
|
|
39
|
+
export async function releasePendingTierReviews(args) {
|
|
40
|
+
const pending = await args.req.payload.find({
|
|
41
|
+
collection: collectionOf('tierRequests'),
|
|
42
|
+
depth: 0,
|
|
43
|
+
limit: 5000,
|
|
44
|
+
pagination: false,
|
|
45
|
+
overrideAccess: true,
|
|
46
|
+
req: args.req,
|
|
47
|
+
where: {
|
|
48
|
+
and: [{ tier: { equals: args.tierId } }, { status: { equals: 'pending' } }],
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
let requestsApproved = 0;
|
|
52
|
+
for (const doc of pending.docs) {
|
|
53
|
+
const id = relationId(doc.id);
|
|
54
|
+
if (!id)
|
|
55
|
+
continue;
|
|
56
|
+
await args.req.payload.update({
|
|
57
|
+
collection: collectionOf('tierRequests'),
|
|
58
|
+
id,
|
|
59
|
+
data: { status: 'approved' },
|
|
60
|
+
depth: 0,
|
|
61
|
+
overrideAccess: true,
|
|
62
|
+
req: args.req,
|
|
63
|
+
});
|
|
64
|
+
requestsApproved += 1;
|
|
65
|
+
}
|
|
66
|
+
return { requestsApproved };
|
|
67
|
+
}
|
|
68
|
+
/** True when `requiresReview` flipped from required → not required. */
|
|
69
|
+
export function didDisableRequiresReview(previousDoc, doc) {
|
|
70
|
+
return previousDoc?.requiresReview === true && doc?.requiresReview !== true;
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dappermountain/payload-plugin-achievements",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "A robust, Admin-configured achievements engine for Payload: composable unlock rules, derived rank progress, request/review grants, metrics, and scoped multi-tenant support.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"import": "./dist/index.js",
|
|
29
29
|
"default": "./dist/index.js"
|
|
30
30
|
},
|
|
31
|
+
"./cli": {
|
|
32
|
+
"types": "./dist/cli/index.d.ts",
|
|
33
|
+
"import": "./dist/cli/index.js",
|
|
34
|
+
"default": "./dist/cli/index.js"
|
|
35
|
+
},
|
|
31
36
|
"./types": {
|
|
32
37
|
"types": "./dist/types.d.ts",
|
|
33
38
|
"import": "./dist/types.js",
|