@dappermountain/payload-plugin-achievements 0.1.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 +36 -6
- package/dist/admin/ReconcileProgressionButton.d.ts +11 -0
- package/dist/admin/ReconcileProgressionButton.d.ts.map +1 -0
- package/dist/admin/ReconcileProgressionButton.js +41 -0
- package/dist/catalog.d.ts +3 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +1 -0
- 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/AchievementRequests/hooks/prepareAchievementRequest.d.ts.map +1 -1
- package/dist/collections/AchievementRequests/hooks/prepareAchievementRequest.js +35 -15
- 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/Grants/hooks/afterGrantMutation.d.ts +4 -0
- package/dist/collections/Grants/hooks/afterGrantMutation.d.ts.map +1 -0
- package/dist/collections/Grants/hooks/afterGrantMutation.js +56 -0
- package/dist/collections/Grants/hooks/index.d.ts.map +1 -1
- package/dist/collections/Grants/hooks/index.js +3 -0
- package/dist/collections/Grants/index.d.ts.map +1 -1
- package/dist/collections/Grants/index.js +18 -1
- package/dist/collections/Logs/index.js +1 -1
- package/dist/collections/TierRequests/access/index.d.ts +3 -0
- package/dist/collections/TierRequests/access/index.d.ts.map +1 -0
- package/dist/collections/TierRequests/access/index.js +8 -0
- package/dist/collections/TierRequests/hooks/index.d.ts +3 -0
- package/dist/collections/TierRequests/hooks/index.d.ts.map +1 -0
- package/dist/collections/TierRequests/hooks/index.js +5 -0
- package/dist/collections/TierRequests/hooks/prepareTierRequest.d.ts +8 -0
- package/dist/collections/TierRequests/hooks/prepareTierRequest.d.ts.map +1 -0
- package/dist/collections/TierRequests/hooks/prepareTierRequest.js +67 -0
- package/dist/collections/TierRequests/index.d.ts +3 -0
- package/dist/collections/TierRequests/index.d.ts.map +1 -0
- package/dist/collections/TierRequests/index.js +47 -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.d.ts.map +1 -1
- package/dist/collections/Tiers/index.js +11 -3
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/collections/index.js +2 -0
- package/dist/collections/slugs.d.ts.map +1 -1
- package/dist/collections/slugs.js +1 -0
- package/dist/defaults.d.ts +3 -0
- package/dist/defaults.d.ts.map +1 -1
- package/dist/defaults.js +12 -3
- package/dist/endpoints/index.d.ts.map +1 -1
- package/dist/endpoints/index.js +9 -3
- package/dist/endpoints/reconcile.d.ts +8 -0
- package/dist/endpoints/reconcile.d.ts.map +1 -0
- package/dist/endpoints/reconcile.js +73 -0
- package/dist/exports/client.d.ts +1 -0
- package/dist/exports/client.d.ts.map +1 -1
- package/dist/exports/client.js +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/seed/index.d.ts +2 -0
- package/dist/seed/index.d.ts.map +1 -1
- package/dist/seed/index.js +1 -0
- package/dist/services/achievement/contextFlags.d.ts +5 -0
- package/dist/services/achievement/contextFlags.d.ts.map +1 -0
- package/dist/services/achievement/contextFlags.js +4 -0
- package/dist/services/achievement/grantSideEffects.d.ts +54 -0
- package/dist/services/achievement/grantSideEffects.d.ts.map +1 -0
- package/dist/services/achievement/grantSideEffects.js +269 -0
- package/dist/services/achievement/index.d.ts +6 -0
- package/dist/services/achievement/index.d.ts.map +1 -1
- package/dist/services/achievement/index.js +4 -0
- package/dist/services/achievement/reconcile.d.ts +36 -0
- package/dist/services/achievement/reconcile.d.ts.map +1 -0
- package/dist/services/achievement/reconcile.js +292 -0
- package/dist/services/achievement/recordEvent.d.ts.map +1 -1
- package/dist/services/achievement/recordEvent.js +8 -0
- 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/dist/services/achievement/resolveCurrentTier.d.ts +1 -1
- package/dist/services/achievement/resolveCurrentTier.d.ts.map +1 -1
- package/dist/services/achievement/resolveCurrentTier.js +12 -2
- package/dist/services/achievement/submitAchievementRequest.d.ts +4 -2
- package/dist/services/achievement/submitAchievementRequest.d.ts.map +1 -1
- package/dist/services/achievement/submitAchievementRequest.js +8 -101
- package/dist/services/achievement/tierApproval.d.ts +8 -0
- package/dist/services/achievement/tierApproval.d.ts.map +1 -0
- package/dist/services/achievement/tierApproval.js +21 -0
- package/dist/services/achievement/tierProgression.d.ts +22 -0
- package/dist/services/achievement/tierProgression.d.ts.map +1 -0
- package/dist/services/achievement/tierProgression.js +100 -0
- package/dist/types.d.ts +8 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -6
package/README.md
CHANGED
|
@@ -22,11 +22,14 @@ Most gamification features end up as one-off collections and scattered hooks. Th
|
|
|
22
22
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
25
|
+
Preferred package manager: **Bun**.
|
|
26
|
+
|
|
25
27
|
```bash
|
|
26
|
-
|
|
27
|
-
# or: pnpm add / yarn add / bun add
|
|
28
|
+
bun add @dappermountain/payload-plugin-achievements
|
|
28
29
|
```
|
|
29
30
|
|
|
31
|
+
Other clients also work (`npm install @dappermountain/payload-plugin-achievements`, etc.).
|
|
32
|
+
|
|
30
33
|
## Quick start
|
|
31
34
|
|
|
32
35
|
```ts
|
|
@@ -80,7 +83,8 @@ Slugs are prefixed with `achievement-` by default so they don’t collide with y
|
|
|
80
83
|
| `achievements` | `achievement-definitions` | Things a user can earn (with eligibility / completion rules). |
|
|
81
84
|
| `grants` | `achievement-grants` | One row per earned achievement per user (+ scope). |
|
|
82
85
|
| `achievementRequests` | `achievement-requests` | Request → pending / approved / rejected. |
|
|
83
|
-
| `
|
|
86
|
+
| `tierRequests` | `achievement-tier-requests` | Tier unlock awaiting review (when the tier has `requiresReview`). |
|
|
87
|
+
| `logs` | `achievement-logs` | Member activity history (grants, revokes, tier moves, metrics, custom events). |
|
|
84
88
|
|
|
85
89
|
**System catalog rows** (seeded by `seedAchievementCatalog`) are protected from deletion and slug changes:
|
|
86
90
|
|
|
@@ -89,6 +93,7 @@ Slugs are prefixed with `achievement-` by default so they don’t collide with y
|
|
|
89
93
|
| `points` | Default metric |
|
|
90
94
|
| `metric.delta` | Metric change log type (requires metric + change amount) |
|
|
91
95
|
| `achievement.granted` | Written when a grant is created |
|
|
96
|
+
| `achievement.revoked` | Written when a grant is deleted |
|
|
92
97
|
| `tier.changed` | Written when derived tier moves |
|
|
93
98
|
|
|
94
99
|
Event types can require an **actor** and/or a **metric + change**. Admin only prompts for those fields when the type needs them.
|
|
@@ -102,7 +107,7 @@ Unlock (tiers), eligibility, and completion (achievements) share the same rule t
|
|
|
102
107
|
- Built-in leaves → `tier-at-least`, `achievement-complete`, `metric-minimum`, `event-count`
|
|
103
108
|
- Custom leaves → register via `extensions.ruleTypes`
|
|
104
109
|
|
|
105
|
-
**Current tier** is computed by walking tiers in `rank` order and evaluating each tier’s unlock rules (`resolveCurrentTier`). **Next-tier fill** uses fractional progress on the same trees (`resolveTierProgress` / `evaluateRuleProgress`):
|
|
110
|
+
**Current tier** is computed by walking tiers in `rank` order and evaluating each tier’s unlock rules (`resolveCurrentTier`). Tiers with **`requiresReview`** only become current after an **approved tier request**. **Next-tier fill** uses fractional progress on the same trees (`resolveTierProgress` / `evaluateRuleProgress`):
|
|
106
111
|
|
|
107
112
|
| Combinator / leaf | Progress behavior |
|
|
108
113
|
| --- | --- |
|
|
@@ -241,6 +246,8 @@ import {
|
|
|
241
246
|
recordLog,
|
|
242
247
|
recordMetricChange,
|
|
243
248
|
grantAchievement,
|
|
249
|
+
reconcileProgression,
|
|
250
|
+
reconcileUserProgression,
|
|
244
251
|
resolveCurrentTier,
|
|
245
252
|
resolveTierProgress,
|
|
246
253
|
getUserProgress,
|
|
@@ -251,19 +258,42 @@ import {
|
|
|
251
258
|
await recordLog({ payload, userId, scopeId, type: 'host.custom-kind', actorId })
|
|
252
259
|
await recordMetricChange({ payload, userId, scopeId, metric: 'points', change: 10 })
|
|
253
260
|
const tier = await resolveCurrentTier({ payload, userId, scopeId })
|
|
261
|
+
|
|
262
|
+
// Repair users whose grants predate side-effect hooks (idempotent)
|
|
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 })
|
|
254
278
|
```
|
|
255
279
|
|
|
256
|
-
Pass catalog **slugs** (or ids) for `type` and `metric`. Metric totals are the sum of `change` on matching logs.
|
|
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`.
|
|
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
|
+
|
|
284
|
+
**Admin:** Grants list includes **Repair progression**, which `POST`s the reconcile endpoint (requires `canReview`).
|
|
257
285
|
|
|
258
286
|
## REST surface
|
|
259
287
|
|
|
260
288
|
| Goal | Request |
|
|
261
289
|
| --- | --- |
|
|
262
290
|
| Current user snapshot | `GET /api/achievements/me?scope=<scopeId>&limit=10&page=1` |
|
|
291
|
+
| Repair progression | `POST /api/achievements/reconcile` (reviewer; optional `{ userId, scopeId, achievementId\|achievementSlug, tierId\|tierSlug, limit }`) |
|
|
263
292
|
| List grants | `GET /api/achievement-grants?where[user][equals]=<userId>` |
|
|
264
293
|
| Log history | `GET /api/achievement-logs?where[user][equals]=<userId>` |
|
|
265
294
|
| Request an achievement | `POST /api/achievement-requests` with `{ achievement }` |
|
|
266
|
-
| Approve / reject | `PATCH /api/achievement-requests/:id` with `{ status: "approved" \| "rejected" }` |
|
|
295
|
+
| Approve / reject achievement | `PATCH /api/achievement-requests/:id` with `{ status: "approved" \| "rejected" }` |
|
|
296
|
+
| Approve / reject tier | `PATCH /api/achievement-tier-requests/:id` with `{ status: "approved" \| "rejected" }` |
|
|
267
297
|
| Browse catalogs | `GET /api/achievement-event-types`, `GET /api/achievement-metrics`, … |
|
|
268
298
|
|
|
269
299
|
Exact collection paths follow your `collections.prefix` / `slugs` settings.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
type ReconcileProgressionButtonProps = {
|
|
3
|
+
/** Absolute API path including `/api` prefix, e.g. `/api/achievements/reconcile`. */
|
|
4
|
+
reconcilePath: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Grants list control — runs progression reconcile for all users with grants.
|
|
8
|
+
*/
|
|
9
|
+
export declare function ReconcileProgressionButton(props: ReconcileProgressionButtonProps): React.JSX.Element;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=ReconcileProgressionButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReconcileProgressionButton.d.ts","sourceRoot":"","sources":["../../src/admin/ReconcileProgressionButton.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAgC,MAAM,OAAO,CAAA;AAEpD,KAAK,+BAA+B,GAAG;IACrC,qFAAqF;IACrF,aAAa,EAAE,MAAM,CAAA;CACtB,CAAA;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,+BAA+B,qBAoDhF"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Button, toast, useConfig } from '@payloadcms/ui';
|
|
4
|
+
import { useCallback, useState } from 'react';
|
|
5
|
+
/**
|
|
6
|
+
* Grants list control — runs progression reconcile for all users with grants.
|
|
7
|
+
*/
|
|
8
|
+
export function ReconcileProgressionButton(props) {
|
|
9
|
+
const { reconcilePath } = props;
|
|
10
|
+
const { config } = useConfig();
|
|
11
|
+
const [busy, setBusy] = useState(false);
|
|
12
|
+
const onClick = useCallback(async () => {
|
|
13
|
+
if (busy)
|
|
14
|
+
return;
|
|
15
|
+
setBusy(true);
|
|
16
|
+
try {
|
|
17
|
+
const serverURL = config.serverURL?.replace(/\/$/, '') ?? '';
|
|
18
|
+
const url = `${serverURL}${reconcilePath.startsWith('/') ? reconcilePath : `/${reconcilePath}`}`;
|
|
19
|
+
const res = await fetch(url, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
credentials: 'include',
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
body: JSON.stringify({}),
|
|
24
|
+
});
|
|
25
|
+
const json = (await res.json().catch(() => null));
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
const message = json?.errors?.[0]?.message ?? `Request failed (${res.status})`;
|
|
28
|
+
toast.error(message);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
toast.success(`Repair complete — users ${json?.usersScanned ?? 0}, grants ${json?.compositesGranted ?? 0}, logs ${json?.grantedLogsBackfilled ?? 0}, requests ${json?.achievementRequestsEnsured ?? 0}, tier requests ${json?.tierRequestsEnsured ?? 0}`);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
toast.error(error instanceof Error ? error.message : 'Repair failed');
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
setBusy(false);
|
|
38
|
+
}
|
|
39
|
+
}, [busy, config.serverURL, reconcilePath]);
|
|
40
|
+
return (_jsx("div", { style: { marginBottom: '1rem' }, children: _jsx(Button, { buttonStyle: "secondary", disabled: busy, onClick: () => void onClick(), children: busy ? 'Repairing…' : 'Repair progression' }) }));
|
|
41
|
+
}
|
package/dist/catalog.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export declare const ENGINE_EVENT_TYPES: readonly [{
|
|
2
2
|
readonly slug: "achievement.granted";
|
|
3
3
|
readonly name: "Achievement granted";
|
|
4
|
+
}, {
|
|
5
|
+
readonly slug: "achievement.revoked";
|
|
6
|
+
readonly name: "Achievement revoked";
|
|
4
7
|
}, {
|
|
5
8
|
readonly slug: "metric.delta";
|
|
6
9
|
readonly name: "Metric delta";
|
package/dist/catalog.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;EAKrB,CAAA;AAEV,eAAO,MAAM,cAAc;;;EAAgD,CAAA;AAE3E,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAA;AAC7E,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAA;AAEtE,eAAO,MAAM,+BAA+B,EAAE,gBAA2B,CAAA;AAEzE,+DAA+D;AAC/D,eAAO,MAAM,0BAA0B,UAAkC,CAAA"}
|
package/dist/catalog.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const ENGINE_EVENT_TYPES = [
|
|
2
2
|
{ slug: 'achievement.granted', name: 'Achievement granted' },
|
|
3
|
+
{ slug: 'achievement.revoked', name: 'Achievement revoked' },
|
|
3
4
|
{ slug: 'metric.delta', name: 'Metric delta', requiresMetric: true },
|
|
4
5
|
{ slug: 'tier.changed', name: 'Tier changed' },
|
|
5
6
|
];
|
|
@@ -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
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prepareAchievementRequest.d.ts","sourceRoot":"","sources":["../../../../src/collections/AchievementRequests/hooks/prepareAchievementRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EACzB,4BAA4B,EAE7B,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"prepareAchievementRequest.d.ts","sourceRoot":"","sources":["../../../../src/collections/AchievementRequests/hooks/prepareAchievementRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EACzB,4BAA4B,EAE7B,MAAM,SAAS,CAAA;AA2ChB;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,4BAqEvC,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,sBAAsB,EAAE,yBAsCpC,CAAA"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { APIError } from 'payload';
|
|
2
2
|
import { collectionOf } from '../../../collections/helpers';
|
|
3
|
+
import { CTX_SYSTEM_REQUEST } from '../../../services/achievement/contextFlags';
|
|
3
4
|
import { evaluateRules, ruleGroupHasProgressRequirements } from '../../../services/achievement/evaluateRules';
|
|
4
5
|
import { grantAchievement } from '../../../services/achievement/submitAchievementRequest';
|
|
5
6
|
import { relationId } from '../../../services/achievement/relationId';
|
|
@@ -34,30 +35,49 @@ export const prepareAchievementRequest = async ({ data, operation, req, original
|
|
|
34
35
|
if (!data)
|
|
35
36
|
return data;
|
|
36
37
|
if (operation === 'create') {
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const system = Boolean(req.context?.[CTX_SYSTEM_REQUEST]);
|
|
39
|
+
if (!system) {
|
|
40
|
+
const userId = req.user?.id;
|
|
41
|
+
if (typeof userId !== 'string')
|
|
42
|
+
throw new APIError('Unauthorized', 401);
|
|
43
|
+
data.user = userId;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const bound = relationId(data.user);
|
|
47
|
+
if (!bound)
|
|
48
|
+
throw new APIError('user required', 400);
|
|
49
|
+
data.user = bound;
|
|
50
|
+
}
|
|
51
|
+
const userId = relationId(data.user);
|
|
52
|
+
if (!userId)
|
|
53
|
+
throw new APIError('user required', 400);
|
|
41
54
|
const scopeId = scopeIdOf(data);
|
|
42
55
|
const achievement = await resolveAchievement({
|
|
43
56
|
req,
|
|
44
57
|
data: data,
|
|
45
58
|
});
|
|
46
59
|
data.achievement = achievement.id;
|
|
47
|
-
if (ruleGroupHasProgressRequirements(achievement.completionRules)) {
|
|
60
|
+
if (!system && ruleGroupHasProgressRequirements(achievement.completionRules)) {
|
|
48
61
|
throw new APIError('This achievement is granted automatically when its completion rules pass.', 400);
|
|
49
62
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
if (!system) {
|
|
64
|
+
const eligible = await evaluateRules({
|
|
65
|
+
payload: req.payload,
|
|
66
|
+
req,
|
|
67
|
+
rules: achievement.eligibilityRules,
|
|
68
|
+
userId,
|
|
69
|
+
scopeId,
|
|
70
|
+
});
|
|
71
|
+
if (!eligible) {
|
|
72
|
+
throw new APIError('Eligibility rules not met for this achievement.', 400);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (system) {
|
|
76
|
+
data.status = data.status ?? 'pending';
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
data.status = achievement.requiresReview === false ? 'approved' : 'pending';
|
|
59
80
|
}
|
|
60
|
-
data.status = achievement.requiresReview === false ? 'approved' : 'pending';
|
|
61
81
|
return data;
|
|
62
82
|
}
|
|
63
83
|
if (operation === 'update') {
|
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"afterGrantMutation.d.ts","sourceRoot":"","sources":["../../../../src/collections/Grants/hooks/afterGrantMutation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAA;AAyBnF,eAAO,MAAM,gBAAgB,EAAE,yBAmB9B,CAAA;AAED,eAAO,MAAM,gBAAgB,EAAE,yBAmB9B,CAAA"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { CTX_CASCADING_REVOKE, CTX_SKIP_GRANT_SIDE_EFFECTS, runAfterGrantCreated, runAfterGrantDeleted } from '../../../services/achievement/grantSideEffects';
|
|
2
|
+
import { relationId } from '../../../services/achievement/relationId';
|
|
3
|
+
import { collectionOf } from '../../../collections/helpers';
|
|
4
|
+
async function achievementSlug(req, achievementId) {
|
|
5
|
+
try {
|
|
6
|
+
const def = (await req.payload.findByID({
|
|
7
|
+
collection: collectionOf('achievements'),
|
|
8
|
+
id: achievementId,
|
|
9
|
+
depth: 0,
|
|
10
|
+
overrideAccess: true,
|
|
11
|
+
req,
|
|
12
|
+
select: { slug: true },
|
|
13
|
+
}));
|
|
14
|
+
return def?.slug;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export const afterGrantChange = async ({ doc, operation, req }) => {
|
|
21
|
+
if (operation !== 'create')
|
|
22
|
+
return doc;
|
|
23
|
+
if (req.context?.[CTX_SKIP_GRANT_SIDE_EFFECTS])
|
|
24
|
+
return doc;
|
|
25
|
+
const userId = relationId(doc.user);
|
|
26
|
+
const achievementId = relationId(doc.achievement);
|
|
27
|
+
if (!userId || !achievementId)
|
|
28
|
+
return doc;
|
|
29
|
+
const scopeId = relationId(doc.scope);
|
|
30
|
+
await runAfterGrantCreated({
|
|
31
|
+
req,
|
|
32
|
+
userId,
|
|
33
|
+
achievementId,
|
|
34
|
+
scopeId,
|
|
35
|
+
achievementSlug: await achievementSlug(req, achievementId),
|
|
36
|
+
});
|
|
37
|
+
return doc;
|
|
38
|
+
};
|
|
39
|
+
export const afterGrantDelete = async ({ doc, req }) => {
|
|
40
|
+
if (req.context?.[CTX_SKIP_GRANT_SIDE_EFFECTS])
|
|
41
|
+
return doc;
|
|
42
|
+
const userId = relationId(doc.user);
|
|
43
|
+
const achievementId = relationId(doc.achievement);
|
|
44
|
+
if (!userId || !achievementId)
|
|
45
|
+
return doc;
|
|
46
|
+
const scopeId = relationId(doc.scope);
|
|
47
|
+
await runAfterGrantDeleted({
|
|
48
|
+
req,
|
|
49
|
+
userId,
|
|
50
|
+
achievementId,
|
|
51
|
+
scopeId,
|
|
52
|
+
achievementSlug: await achievementSlug(req, achievementId),
|
|
53
|
+
cascading: Boolean(req.context?.[CTX_CASCADING_REVOKE]),
|
|
54
|
+
});
|
|
55
|
+
return doc;
|
|
56
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/collections/Grants/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/Grants/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAM/C,eAAO,MAAM,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAM3C,CAAA"}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { ensureUniqueGrant } from './ensureUniqueGrant';
|
|
2
2
|
import { fillGrantTitle, setGrantTitle } from './setGrantTitle';
|
|
3
|
+
import { afterGrantChange, afterGrantDelete } from './afterGrantMutation';
|
|
3
4
|
export const hooks = {
|
|
4
5
|
beforeValidate: [ensureUniqueGrant],
|
|
5
6
|
beforeChange: [setGrantTitle],
|
|
6
7
|
afterRead: [fillGrantTitle],
|
|
8
|
+
afterChange: [afterGrantChange],
|
|
9
|
+
afterDelete: [afterGrantDelete],
|
|
7
10
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/Grants/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAS,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/Grants/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAS,MAAM,SAAS,CAAA;AAQtD,wBAAgB,qBAAqB,IAAI,gBAAgB,CA4ExD"}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { collectionAdmin, collectionOf, columnsWithOptionalScope, optionalFields, slugOf } from '../helpers';
|
|
2
2
|
import { scopeField, userField } from '../fields';
|
|
3
|
+
import { getAchievementOptions } from '../../options-store';
|
|
3
4
|
import { access } from './access';
|
|
4
5
|
import { hooks } from './hooks';
|
|
5
6
|
export function buildGrantsCollection() {
|
|
7
|
+
const reconcilePath = getAchievementOptions().endpoints?.reconcile;
|
|
8
|
+
const apiPath = typeof reconcilePath === 'string' && reconcilePath.length > 0
|
|
9
|
+
? `/api${reconcilePath.startsWith('/') ? reconcilePath : `/${reconcilePath}`}`
|
|
10
|
+
: null;
|
|
6
11
|
return {
|
|
7
12
|
slug: slugOf('grants'),
|
|
8
13
|
labels: {
|
|
@@ -15,7 +20,19 @@ export function buildGrantsCollection() {
|
|
|
15
20
|
useAsTitle: 'title',
|
|
16
21
|
defaultColumns: columnsWithOptionalScope(['title', 'user', 'completedAt', 'scope']),
|
|
17
22
|
listSearchableFields: ['title'],
|
|
18
|
-
description: 'What someone has earned — one row per grant.
|
|
23
|
+
description: 'What someone has earned — one row per grant. Creating or deleting a grant updates logs and composed progression automatically.',
|
|
24
|
+
...(apiPath
|
|
25
|
+
? {
|
|
26
|
+
components: {
|
|
27
|
+
beforeListTable: [
|
|
28
|
+
{
|
|
29
|
+
path: '@dappermountain/payload-plugin-achievements/client#ReconcileProgressionButton',
|
|
30
|
+
clientProps: { reconcilePath: apiPath },
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
: {}),
|
|
19
36
|
}),
|
|
20
37
|
fields: [
|
|
21
38
|
{
|
|
@@ -20,7 +20,7 @@ export function buildLogsCollection() {
|
|
|
20
20
|
admin: collectionAdmin({
|
|
21
21
|
useAsTitle: 'id',
|
|
22
22
|
defaultColumns: ['type', 'user', 'actor', 'metric', 'change', 'createdAt'],
|
|
23
|
-
description: '
|
|
23
|
+
description: 'Member activity history — grants, revokes, score changes, tier moves, and host custom events. Not a second grant store.',
|
|
24
24
|
}),
|
|
25
25
|
fields: [
|
|
26
26
|
...optionalFields(scopeField()),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/collections/TierRequests/access/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAK/C,eAAO,MAAM,MAAM,EAAE,WAAW,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAK1D,CAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { isAuthenticated, requireOne } from '../../../access/index';
|
|
2
|
+
import { isReviewer, isSelfUser } from '../../../access/roles/index';
|
|
3
|
+
export const access = {
|
|
4
|
+
read: requireOne(isReviewer('tierRequests'), isSelfUser('user')),
|
|
5
|
+
create: isAuthenticated,
|
|
6
|
+
update: requireOne(isReviewer('tierRequests')),
|
|
7
|
+
delete: requireOne(isReviewer('tierRequests')),
|
|
8
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/collections/TierRequests/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAI/C,eAAO,MAAM,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAG3C,CAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CollectionAfterChangeHook, CollectionBeforeValidateHook } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* System-created requests (reconcile / progression sync) may set user freely.
|
|
4
|
+
* Member creates are not used for tiers in v1 — reviewers/system own the flow.
|
|
5
|
+
*/
|
|
6
|
+
export declare const prepareTierRequest: CollectionBeforeValidateHook;
|
|
7
|
+
export declare const afterTierRequestChange: CollectionAfterChangeHook;
|
|
8
|
+
//# sourceMappingURL=prepareTierRequest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prepareTierRequest.d.ts","sourceRoot":"","sources":["../../../../src/collections/TierRequests/hooks/prepareTierRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EACzB,4BAA4B,EAE7B,MAAM,SAAS,CAAA;AAYhB;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,4BAyChC,CAAA;AAED,eAAO,MAAM,sBAAsB,EAAE,yBA0BpC,CAAA"}
|