@dappermountain/payload-plugin-achievements 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -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/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/index.d.ts +3 -1
- package/dist/services/achievement/index.d.ts.map +1 -1
- package/dist/services/achievement/index.js +1 -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 +144 -22
- 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,26 @@ 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
|
+
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).
|
|
283
|
+
|
|
268
284
|
**Admin:** Grants list includes **Repair progression**, which `POST`s the reconcile endpoint (requires `canReview`).
|
|
269
285
|
|
|
270
286
|
## REST surface
|
|
@@ -272,7 +288,7 @@ Pass catalog **slugs** (or ids) for `type` and `metric`. Metric totals are the s
|
|
|
272
288
|
| Goal | Request |
|
|
273
289
|
| --- | --- |
|
|
274
290
|
| 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 }`) |
|
|
291
|
+
| Repair progression | `POST /api/achievements/reconcile` (reviewer; optional `{ userId, scopeId, achievementId\|achievementSlug, tierId\|tierSlug, limit }`) |
|
|
276
292
|
| List grants | `GET /api/achievement-grants?where[user][equals]=<userId>` |
|
|
277
293
|
| Log history | `GET /api/achievement-logs?where[user][equals]=<userId>` |
|
|
278
294
|
| 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',
|
|
@@ -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, validateRuleGroup, validateRuleNode, } from './services/achievement/index';
|
|
7
|
+
export type { ResolvedTier, RuleGroup, TierLadderProgress, CatalogProgressGroup, CatalogProgressItem, 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,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,8BAA8B,CAAA;AACrC,YAAY,EACV,YAAY,EACZ,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,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, 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';
|
|
@@ -14,5 +14,7 @@ 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';
|
|
18
20
|
//# 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"}
|
|
@@ -10,3 +10,4 @@ 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';
|
|
@@ -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;AAY7D,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;
|
|
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;AAY7D,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;AAsFD,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"}
|
|
@@ -113,7 +113,140 @@ function scopeKey(scope) {
|
|
|
113
113
|
const id = relationId(scope);
|
|
114
114
|
return id ?? '';
|
|
115
115
|
}
|
|
116
|
-
|
|
116
|
+
function addPair(pairs, userId, scopeId, filterUserId, filterScopeId) {
|
|
117
|
+
if (!userId)
|
|
118
|
+
return;
|
|
119
|
+
if (filterUserId && userId !== filterUserId)
|
|
120
|
+
return;
|
|
121
|
+
if (filterScopeId != null && filterScopeId !== '' && scopeId !== filterScopeId)
|
|
122
|
+
return;
|
|
123
|
+
const key = `${userId}::${scopeKey(scopeId)}`;
|
|
124
|
+
if (!pairs.has(key))
|
|
125
|
+
pairs.set(key, { userId, scopeId });
|
|
126
|
+
}
|
|
127
|
+
async function resolveDefinitionId(args) {
|
|
128
|
+
if (args.id && args.id.length > 0)
|
|
129
|
+
return args.id;
|
|
130
|
+
if (!args.slug || args.slug.length === 0)
|
|
131
|
+
return null;
|
|
132
|
+
const found = await args.payload.find({
|
|
133
|
+
collection: collectionOf(args.collection),
|
|
134
|
+
depth: 0,
|
|
135
|
+
limit: 1,
|
|
136
|
+
overrideAccess: true,
|
|
137
|
+
req: args.req,
|
|
138
|
+
where: { slug: { equals: args.slug } },
|
|
139
|
+
});
|
|
140
|
+
return relationId(found.docs[0]?.id) ?? null;
|
|
141
|
+
}
|
|
142
|
+
async function collectPairs(args) {
|
|
143
|
+
const pairs = new Map();
|
|
144
|
+
const filterUserId = args.userId?.trim() || null;
|
|
145
|
+
const filterScopeId = args.scopeId ?? null;
|
|
146
|
+
const achievementId = await resolveDefinitionId({
|
|
147
|
+
payload: args.payload,
|
|
148
|
+
req: args.req,
|
|
149
|
+
collection: 'achievements',
|
|
150
|
+
id: args.achievementId,
|
|
151
|
+
slug: args.achievementSlug,
|
|
152
|
+
});
|
|
153
|
+
const tierId = await resolveDefinitionId({
|
|
154
|
+
payload: args.payload,
|
|
155
|
+
req: args.req,
|
|
156
|
+
collection: 'tiers',
|
|
157
|
+
id: args.tierId,
|
|
158
|
+
slug: args.tierSlug,
|
|
159
|
+
});
|
|
160
|
+
if (filterUserId && !achievementId && !tierId) {
|
|
161
|
+
addPair(pairs, filterUserId, filterScopeId);
|
|
162
|
+
return [...pairs.values()];
|
|
163
|
+
}
|
|
164
|
+
if (achievementId) {
|
|
165
|
+
const grantWhere = {
|
|
166
|
+
and: [
|
|
167
|
+
{ achievement: { equals: achievementId } },
|
|
168
|
+
...(filterUserId ? [{ user: { equals: filterUserId } }] : []),
|
|
169
|
+
...(filterScopeId
|
|
170
|
+
? [{ scope: { equals: filterScopeId } }]
|
|
171
|
+
: []),
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
const grants = await args.payload.find({
|
|
175
|
+
collection: collectionOf('grants'),
|
|
176
|
+
depth: 0,
|
|
177
|
+
limit: args.limit,
|
|
178
|
+
pagination: false,
|
|
179
|
+
overrideAccess: true,
|
|
180
|
+
req: args.req,
|
|
181
|
+
where: grantWhere,
|
|
182
|
+
});
|
|
183
|
+
for (const grant of grants.docs) {
|
|
184
|
+
addPair(pairs, relationId(grant.user), relationId(grant.scope), filterUserId, filterScopeId);
|
|
185
|
+
}
|
|
186
|
+
const requestWhere = {
|
|
187
|
+
and: [
|
|
188
|
+
{ achievement: { equals: achievementId } },
|
|
189
|
+
...(filterUserId ? [{ user: { equals: filterUserId } }] : []),
|
|
190
|
+
...(filterScopeId ? [{ scope: { equals: filterScopeId } }] : []),
|
|
191
|
+
],
|
|
192
|
+
};
|
|
193
|
+
const requests = await args.payload.find({
|
|
194
|
+
collection: collectionOf('achievementRequests'),
|
|
195
|
+
depth: 0,
|
|
196
|
+
limit: args.limit,
|
|
197
|
+
pagination: false,
|
|
198
|
+
overrideAccess: true,
|
|
199
|
+
req: args.req,
|
|
200
|
+
where: requestWhere,
|
|
201
|
+
});
|
|
202
|
+
for (const request of requests.docs) {
|
|
203
|
+
addPair(pairs, relationId(request.user), relationId(request.scope), filterUserId, filterScopeId);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (tierId) {
|
|
207
|
+
const requestWhere = {
|
|
208
|
+
and: [
|
|
209
|
+
{ tier: { equals: tierId } },
|
|
210
|
+
...(filterUserId ? [{ user: { equals: filterUserId } }] : []),
|
|
211
|
+
...(filterScopeId ? [{ scope: { equals: filterScopeId } }] : []),
|
|
212
|
+
],
|
|
213
|
+
};
|
|
214
|
+
const requests = await args.payload.find({
|
|
215
|
+
collection: collectionOf('tierRequests'),
|
|
216
|
+
depth: 0,
|
|
217
|
+
limit: args.limit,
|
|
218
|
+
pagination: false,
|
|
219
|
+
overrideAccess: true,
|
|
220
|
+
req: args.req,
|
|
221
|
+
where: requestWhere,
|
|
222
|
+
});
|
|
223
|
+
for (const request of requests.docs) {
|
|
224
|
+
addPair(pairs, relationId(request.user), relationId(request.scope), filterUserId, filterScopeId);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (!achievementId && !tierId) {
|
|
228
|
+
const where = filterScopeId
|
|
229
|
+
? { scope: { equals: filterScopeId } }
|
|
230
|
+
: undefined;
|
|
231
|
+
const grants = await args.payload.find({
|
|
232
|
+
collection: collectionOf('grants'),
|
|
233
|
+
depth: 0,
|
|
234
|
+
limit: args.limit,
|
|
235
|
+
pagination: false,
|
|
236
|
+
overrideAccess: true,
|
|
237
|
+
req: args.req,
|
|
238
|
+
...(where ? { where } : {}),
|
|
239
|
+
});
|
|
240
|
+
for (const grant of grants.docs) {
|
|
241
|
+
addPair(pairs, relationId(grant.user), relationId(grant.scope), filterUserId, filterScopeId);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return [...pairs.values()];
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Scan matching (user, scope) pairs and repair progression.
|
|
248
|
+
* Optional filters: `userId`, `scopeId`, `achievementId`/`achievementSlug`, `tierId`/`tierSlug`.
|
|
249
|
+
*/
|
|
117
250
|
export async function reconcileProgression(args) {
|
|
118
251
|
const req = args.req ??
|
|
119
252
|
{
|
|
@@ -122,28 +255,17 @@ export async function reconcileProgression(args) {
|
|
|
122
255
|
};
|
|
123
256
|
if (!req.context)
|
|
124
257
|
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,
|
|
258
|
+
const pairs = await collectPairs({
|
|
259
|
+
payload: args.payload,
|
|
134
260
|
req,
|
|
135
|
-
|
|
261
|
+
userId: args.userId,
|
|
262
|
+
scopeId: args.scopeId,
|
|
263
|
+
achievementId: args.achievementId,
|
|
264
|
+
achievementSlug: args.achievementSlug,
|
|
265
|
+
tierId: args.tierId,
|
|
266
|
+
tierSlug: args.tierSlug,
|
|
267
|
+
limit: args.limit ?? 5000,
|
|
136
268
|
});
|
|
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
269
|
const totals = {
|
|
148
270
|
usersScanned: 0,
|
|
149
271
|
grantedLogsBackfilled: 0,
|
|
@@ -152,7 +274,7 @@ export async function reconcileProgression(args) {
|
|
|
152
274
|
tierRequestsEnsured: 0,
|
|
153
275
|
tierLogsWritten: 0,
|
|
154
276
|
};
|
|
155
|
-
for (const pair of pairs
|
|
277
|
+
for (const pair of pairs) {
|
|
156
278
|
const result = await reconcileUserProgression({
|
|
157
279
|
payload: args.payload,
|
|
158
280
|
req,
|
|
@@ -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.1",
|
|
4
4
|
"description": "A robust, Admin-configured achievements engine for Payload: composable unlock rules, derived rank progress, request/review grants, metrics, and scoped multi-tenant support.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -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",
|