@dappermountain/payload-plugin-achievements 0.2.11 → 0.2.13

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 CHANGED
@@ -97,7 +97,7 @@ Slugs are prefixed with `achievement-` by default so they don’t collide with y
97
97
  | `metric.delta` | Metric change log type (requires metric + change amount) |
98
98
  | `achievement.granted` | Written when a grant is created |
99
99
  | `achievement.revoked` | Written when a grant is deleted |
100
- | `tier.changed` | Written when derived tier moves |
100
+ | `tier.changed` | Written when derived tier moves (including demotions when a tier request leaves `approved`) |
101
101
 
102
102
  Event types can require an **actor** and/or a **metric + change**. Admin only prompts for those fields when the type needs them.
103
103
 
@@ -1 +1 @@
1
- {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../../../src/collections/Logs/fields/validators.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAQvC,eAAO,MAAM,aAAa,EAAE,QAW3B,CAAA;AAED,eAAO,MAAM,cAAc,EAAE,QAW5B,CAAA;AAED,eAAO,MAAM,cAAc,EAAE,QAW5B,CAAA"}
1
+ {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../../../src/collections/Logs/fields/validators.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AASvC,eAAO,MAAM,aAAa,EAAE,QAW3B,CAAA;AAED,eAAO,MAAM,cAAc,EAAE,QA0B5B,CAAA;AAED,eAAO,MAAM,cAAc,EAAE,QAW5B,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { collectionOf } from '../../../collections/helpers.js';
1
2
  import { eventTypeRequiresActor, eventTypeRequiresMetric, } from '../../../services/achievement/eventTypeRequiresActor.js';
2
3
  import { relationId } from '../../../services/achievement/relationId.js';
3
4
  export const validateActor = async (value, { data, req }) => {
@@ -23,9 +24,21 @@ export const validateMetric = async (value, { data, req }) => {
23
24
  if (!(await eventTypeRequiresMetric({ payload: req.payload, req, typeIdOrDoc: typeId }))) {
24
25
  return true;
25
26
  }
26
- if (!relationId(value)) {
27
+ const metricId = relationId(value);
28
+ if (!metricId) {
27
29
  return 'This log type needs a metric.';
28
30
  }
31
+ const metric = (await req.payload.findByID({
32
+ collection: collectionOf('metrics'),
33
+ id: metricId,
34
+ depth: 0,
35
+ overrideAccess: true,
36
+ req,
37
+ select: { kind: true, name: true, slug: true },
38
+ }));
39
+ if (metric && (metric.kind ?? 'stored') === 'computed') {
40
+ return 'Computed metrics (for example Days) cannot be changed with a metric delta. Use a stored metric such as Points.';
41
+ }
29
42
  return true;
30
43
  };
31
44
  export const validateChange = async (value, { data, req }) => {
@@ -1 +1 @@
1
- {"version":3,"file":"ensureTypeRequirements.d.ts","sourceRoot":"","sources":["../../../../src/collections/Logs/hooks/ensureTypeRequirements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAA;AAS3D,eAAO,MAAM,sBAAsB,EAAE,4BAkCpC,CAAA"}
1
+ {"version":3,"file":"ensureTypeRequirements.d.ts","sourceRoot":"","sources":["../../../../src/collections/Logs/hooks/ensureTypeRequirements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAA;AAU3D,eAAO,MAAM,sBAAsB,EAAE,4BAmDpC,CAAA"}
@@ -1,4 +1,5 @@
1
1
  import { APIError } from 'payload';
2
+ import { collectionOf } from '../../../collections/helpers.js';
2
3
  import { eventTypeRequiresActor, eventTypeRequiresMetric, } from '../../../services/achievement/eventTypeRequiresActor.js';
3
4
  import { relationId } from '../../../services/achievement/relationId.js';
4
5
  export const ensureTypeRequirements = async ({ data, req }) => {
@@ -23,13 +24,25 @@ export const ensureTypeRequirements = async ({ data, req }) => {
23
24
  throw new APIError('This log type needs an actor (who caused it).', 400);
24
25
  }
25
26
  if (flags.requiresMetric) {
26
- if (!relationId(data.metric)) {
27
+ const metricId = relationId(data.metric);
28
+ if (!metricId) {
27
29
  throw new APIError('This log type needs a metric.', 400);
28
30
  }
29
31
  const change = data.change;
30
32
  if (typeof change !== 'number' || Number.isNaN(change)) {
31
33
  throw new APIError('This log type needs a change amount.', 400);
32
34
  }
35
+ const metric = (await req.payload.findByID({
36
+ collection: collectionOf('metrics'),
37
+ id: metricId,
38
+ depth: 0,
39
+ overrideAccess: true,
40
+ req,
41
+ select: { kind: true },
42
+ }));
43
+ if (metric && (metric.kind ?? 'stored') === 'computed') {
44
+ throw new APIError('Computed metrics cannot be changed with a metric delta. Use a stored metric such as Points.', 400);
45
+ }
33
46
  }
34
47
  return data;
35
48
  };
@@ -1,4 +1,4 @@
1
- import { catalogFilterOptions } from '../fields/catalogFilter.js';
1
+ import { catalogFilterOptions, metricLogFilterOptions } from '../fields/catalogFilter.js';
2
2
  import { collectionAdmin, collectionOf, optionalFields, slugOf } from '../helpers.js';
3
3
  import { getAchievementOptions } from '../../options-store.js';
4
4
  import { scopeField, userField } from '../fields/index.js';
@@ -58,11 +58,11 @@ export function buildLogsCollection() {
58
58
  name: 'metric',
59
59
  type: 'relationship',
60
60
  relationTo: collectionOf('metrics'),
61
- filterOptions: catalogFilterOptions,
61
+ filterOptions: metricLogFilterOptions,
62
62
  validate: validateMetric,
63
63
  admin: {
64
64
  width: '50%',
65
- description: 'Which score moved.',
65
+ description: 'Which stored score moved (computed metrics like Days cannot be chosen here).',
66
66
  components: {
67
67
  Field: {
68
68
  path: '@dappermountain/payload-plugin-achievements/client#MetricField',
@@ -1 +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"}
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,yBAoBpC,CAAA"}
@@ -46,17 +46,14 @@ export const prepareTierRequest = async ({ data, operation, req, originalDoc, })
46
46
  }
47
47
  return data;
48
48
  };
49
- export const afterTierRequestChange = async ({ doc, operation, previousDoc, req, }) => {
50
- const status = doc.status;
51
- if (status !== 'approved')
52
- return doc;
53
- const previousStatus = operation === 'update' ? previousDoc?.status : undefined;
54
- if (previousStatus === 'approved')
55
- return doc;
49
+ export const afterTierRequestChange = async ({ doc, req, }) => {
56
50
  const userId = relationId(doc.user);
57
51
  if (!userId)
58
52
  return doc;
59
53
  const scopeId = scopeIdOf(doc);
54
+ // Any status transition (or create as pending/rejected/approved) can change the
55
+ // live ladder — including demotions when approved → pending/rejected. Sync is
56
+ // idempotent when the latest audit already matches resolveCurrentTier.
60
57
  await syncTierChangedLog({
61
58
  payload: req.payload,
62
59
  req: req,
@@ -1,8 +1,14 @@
1
- import type { Where } from 'payload';
1
+ import type { FilterOptions, Where } from 'payload';
2
2
  /** Relationship picker: same scope as the parent doc, or unscoped (global) catalog rows. */
3
3
  export declare function catalogFilterOptions({ data }: {
4
4
  data?: {
5
5
  scope?: unknown;
6
6
  };
7
7
  }): Where | true;
8
+ /**
9
+ * Metric picker on achievement logs: same scope rules as other catalog fields,
10
+ * but when the log type needs a metric (e.g. `metric.delta`), only **stored**
11
+ * metrics are offered — computed metrics (like Days) cannot receive deltas.
12
+ */
13
+ export declare const metricLogFilterOptions: FilterOptions;
8
14
  //# sourceMappingURL=catalogFilter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"catalogFilter.d.ts","sourceRoot":"","sources":["../../../src/collections/fields/catalogFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAIpC,4FAA4F;AAC5F,wBAAgB,oBAAoB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GAAG,KAAK,GAAG,IAAI,CAM3F"}
1
+ {"version":3,"file":"catalogFilter.d.ts","sourceRoot":"","sources":["../../../src/collections/fields/catalogFilter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAKnD,4FAA4F;AAC5F,wBAAgB,oBAAoB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GAAG,KAAK,GAAG,IAAI,CAM3F;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,aAqBpC,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { eventTypeRequiresMetric } from '../../services/achievement/eventTypeRequiresActor.js';
1
2
  import { relationId } from '../../services/achievement/relationId.js';
2
3
  /** Relationship picker: same scope as the parent doc, or unscoped (global) catalog rows. */
3
4
  export function catalogFilterOptions({ data }) {
@@ -8,3 +9,28 @@ export function catalogFilterOptions({ data }) {
8
9
  or: [{ scope: { equals: scopeId } }, { scope: { exists: false } }, { scope: { equals: null } }],
9
10
  };
10
11
  }
12
+ /**
13
+ * Metric picker on achievement logs: same scope rules as other catalog fields,
14
+ * but when the log type needs a metric (e.g. `metric.delta`), only **stored**
15
+ * metrics are offered — computed metrics (like Days) cannot receive deltas.
16
+ */
17
+ export const metricLogFilterOptions = async ({ data, req }) => {
18
+ const scopeFilter = catalogFilterOptions({ data: data });
19
+ const typeId = relationId(data?.type);
20
+ let storedOnly = false;
21
+ if (typeId && req?.payload) {
22
+ storedOnly = await eventTypeRequiresMetric({
23
+ payload: req.payload,
24
+ req,
25
+ typeIdOrDoc: typeId,
26
+ });
27
+ }
28
+ if (!storedOnly)
29
+ return scopeFilter;
30
+ const storedWhere = {
31
+ or: [{ kind: { equals: 'stored' } }, { kind: { exists: false } }, { kind: { equals: null } }],
32
+ };
33
+ if (scopeFilter === true)
34
+ return storedWhere;
35
+ return { and: [scopeFilter, storedWhere] };
36
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dappermountain/payload-plugin-achievements",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
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",