@dappermountain/payload-plugin-achievements 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +71 -7
  2. package/dist/admin/SubjectField.d.ts +14 -0
  3. package/dist/admin/SubjectField.d.ts.map +1 -0
  4. package/dist/admin/SubjectField.js +38 -0
  5. package/dist/admin/useEventTypeFlags.d.ts +2 -0
  6. package/dist/admin/useEventTypeFlags.d.ts.map +1 -1
  7. package/dist/admin/useEventTypeFlags.js +21 -3
  8. package/dist/cli/reconcile.d.ts +1 -1
  9. package/dist/cli/reconcile.d.ts.map +1 -1
  10. package/dist/collections/EventTypes/index.d.ts.map +1 -1
  11. package/dist/collections/EventTypes/index.js +30 -0
  12. package/dist/collections/Logs/fields/validators.d.ts +1 -0
  13. package/dist/collections/Logs/fields/validators.d.ts.map +1 -1
  14. package/dist/collections/Logs/fields/validators.js +37 -1
  15. package/dist/collections/Logs/hooks/ensureTypeRequirements.d.ts.map +1 -1
  16. package/dist/collections/Logs/hooks/ensureTypeRequirements.js +22 -13
  17. package/dist/collections/Logs/index.d.ts.map +1 -1
  18. package/dist/collections/Logs/index.js +34 -3
  19. package/dist/defaults.d.ts +5 -1
  20. package/dist/defaults.d.ts.map +1 -1
  21. package/dist/defaults.js +17 -1
  22. package/dist/exports/client.d.ts +1 -0
  23. package/dist/exports/client.d.ts.map +1 -1
  24. package/dist/exports/client.js +1 -0
  25. package/dist/index.d.ts +4 -4
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -2
  28. package/dist/seed/index.d.ts +7 -0
  29. package/dist/seed/index.d.ts.map +1 -1
  30. package/dist/seed/index.js +4 -0
  31. package/dist/services/achievement/duration.js +1 -1
  32. package/dist/services/achievement/eventTypeRequiresActor.d.ts +17 -0
  33. package/dist/services/achievement/eventTypeRequiresActor.d.ts.map +1 -1
  34. package/dist/services/achievement/eventTypeRequiresActor.js +56 -5
  35. package/dist/services/achievement/index.d.ts +2 -0
  36. package/dist/services/achievement/index.d.ts.map +1 -1
  37. package/dist/services/achievement/index.js +1 -0
  38. package/dist/services/achievement/logSubject.d.ts +13 -0
  39. package/dist/services/achievement/logSubject.d.ts.map +1 -0
  40. package/dist/services/achievement/logSubject.js +20 -0
  41. package/dist/services/achievement/recordEvent.d.ts +6 -0
  42. package/dist/services/achievement/recordEvent.d.ts.map +1 -1
  43. package/dist/services/achievement/recordEvent.js +32 -4
  44. package/dist/types.d.ts +19 -3
  45. package/dist/types.d.ts.map +1 -1
  46. package/package.json +7 -7
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@dappermountain/payload-plugin-achievements`
2
2
 
3
- A Payload CMS plugin for a **rule-based achievements and ranking system** — tiers, badges, points, grants, review workflows, and an append-only activity log — configured in Admin, not hard-coded in your app.
3
+ A Payload CMS plugin for a **rule-based achievements and ranking system** — tiers, badges, points, grants, review workflows, and an append-only activity log — configured in Admin.
4
4
 
5
5
  Most gamification features end up as one-off collections and scattered hooks. This plugin gives you a reusable engine: define *what* can be earned and *how* it unlocks in Payload Admin, then record events from your application code. Current tier and progress are **derived** from rules and history, so you don’t maintain a separate progress table that drifts out of sync.
6
6
 
@@ -16,8 +16,9 @@ Most gamification features end up as one-off collections and scattered hooks. Th
16
16
 
17
17
  ## Requirements
18
18
 
19
- - Payload `^3.88`
20
- - `@payloadcms/ui` `^3.88`
19
+ - Payload `^3.89`
20
+ - `@payloadcms/ui` `^3.89`
21
+ - `@payloadcms/richtext-lexical` `^3.89`
21
22
  - React `^19`
22
23
 
23
24
  ## Install
@@ -120,7 +121,7 @@ Unlock (tiers), eligibility, and completion (achievements) share the same rule t
120
121
  | `event-count` / `metric-minimum` | Fraction of the target |
121
122
  | `achievement-complete` | `1` if granted; otherwise rolls up that achievement’s completion (or eligibility) rules |
122
123
 
123
- **Computed elapsed metrics** — `kind: computed` + `compute: elapsed` + `unit` (`seconds` | `minutes` | `hours` | `days` | `years`) + `since` (built-in `user-created-at` / `first-event`, or a host `metricAnchors` key). Gate tenure with `metric-minimum` against that metric. Years use a fixed 365-day length (not calendar years). System seed only creates stored `points` — product computed metrics (e.g. `days`) are host-seeded after registering anchors.
124
+ **Computed elapsed metrics** — `kind: computed` + `compute: elapsed` + `unit` (`seconds` | `minutes` | `hours` | `days` | `years`) + `since` (built-in `user-created-at` / `first-event`, or a host `metricAnchors` key). Gate tenure with `metric-minimum` against that metric. Years use a fixed 365-day length. System seed only creates stored `points` — product computed metrics (e.g. `days`) are host-seeded after registering anchors.
124
125
 
125
126
  **Stored metrics** — totals live in `achievement-metric-balances`, updated by `recordMetricChange` (log + balance dual-write). Logs remain the audit/rebuild source; reconcile backfills balances.
126
127
 
@@ -230,6 +231,50 @@ achievementPlugin({
230
231
  | --- | --- | --- |
231
232
  | `includeJoins` | `true` | Injects an `achievements` group on the users collection with joins to grants and requests. Set `false` if you own that UI yourself. |
232
233
 
234
+ ### `subjects`
235
+
236
+ Host documents that logs may point at via a polymorphic `subject` field (same idea as `requiresActor` / `requiresMetric`).
237
+
238
+ | Option | Default | Notes |
239
+ | --- | --- | --- |
240
+ | `collections` | `[]` | Allowlist of collection slugs (e.g. `posts`, `comments`). Empty / omit → subject feature is off (no log field, no event-type subject flags). |
241
+
242
+ When configured:
243
+
244
+ - Event types gain **Requires a subject** + optional **Subject collections** (`subjectRelationTo`).
245
+ - Logs gain a polymorphic **Subject** relationship (Admin links to the host doc).
246
+ - `recordLog({ subject: { relationTo, value } })` (or `logSubject(relationTo, value)`) writes the ref.
247
+ - Prefer `subject` over stuffing ids into `data` JSON.
248
+
249
+ ```ts
250
+ achievementPlugin({
251
+ canReview,
252
+ subjects: {
253
+ collections: ['posts', 'comments', 'endorsements'],
254
+ },
255
+ })
256
+
257
+ await seedAchievements(payload, {
258
+ eventTypes: [
259
+ {
260
+ name: 'Comment created',
261
+ slug: 'comment.created',
262
+ requiresSubject: true,
263
+ subjectRelationTo: ['comments'],
264
+ },
265
+ ],
266
+ })
267
+
268
+ await recordLog({
269
+ payload,
270
+ userId,
271
+ scopeId,
272
+ type: 'comment.created',
273
+ actorId: userId,
274
+ subject: logSubject('comments', commentId),
275
+ })
276
+ ```
277
+
233
278
  ### `endpoints`
234
279
 
235
280
  | Option | Default | Notes |
@@ -250,7 +295,7 @@ achievementPlugin({
250
295
  extensions: {
251
296
  metricAnchors: {
252
297
  'membership-granted': {
253
- // Admin UI i18n (Payload label), not a CMS localized field
298
+ // Admin UI i18n (Payload label / OptionLabel)
254
299
  label: ({ t }) => t('custom:achievements:anchors:membershipGranted'),
255
300
  resolve: async ({ payload, userId, scopeId }) => {
256
301
  // return a Date from host data, or null
@@ -275,7 +320,7 @@ achievementPlugin({
275
320
 
276
321
  ## Server helpers
277
322
 
278
- Use these from hooks, jobs, and trusted endpoints — not from the public client:
323
+ Use these from hooks, jobs, and trusted endpoints:
279
324
 
280
325
  ```ts
281
326
  import {
@@ -293,9 +338,18 @@ import {
293
338
  buildUnlockRequirementLeaves,
294
339
  submitAchievementRequest,
295
340
  reviewAchievementRequest,
341
+ logSubject,
296
342
  } from '@dappermountain/payload-plugin-achievements'
297
343
 
298
344
  await recordLog({ payload, userId, scopeId, type: 'host.custom-kind', actorId })
345
+ await recordLog({
346
+ payload,
347
+ userId,
348
+ scopeId,
349
+ type: 'comment.created',
350
+ actorId: userId,
351
+ subject: logSubject('comments', commentId),
352
+ })
299
353
  await recordMetricChange({ payload, userId, scopeId, metric: 'points', change: 10 })
300
354
  const points = await resolveMetricValue({ payload, userId, scopeId, metric: 'points' })
301
355
  const board = await getMetricLeaderboard({ payload, metric: 'points', scopeId, limit: 20 })
@@ -341,6 +395,8 @@ Engine log `data` for those system events uses a shared transition envelope:
341
395
  | `achievement.granted` | `null` | achievement |
342
396
  | `achievement.revoked` | achievement | `null` |
343
397
 
398
+ Host document links belong on polymorphic **`subject`** (when `subjects.collections` is set), not inside `data`.
399
+
344
400
  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).
345
401
 
346
402
  **Admin:** Grants list includes **Repair progression**, which `POST`s the reconcile endpoint (requires `canReview`).
@@ -372,7 +428,15 @@ import {
372
428
  await seedAchievementCatalog(payload) // system metrics + event types (English)
373
429
 
374
430
  await seedAchievements(payload, {
375
- eventTypes: [{ name: 'Custom kind', slug: 'host.custom-kind', requiresActor: true }],
431
+ eventTypes: [
432
+ { name: 'Custom kind', slug: 'host.custom-kind', requiresActor: true },
433
+ {
434
+ name: 'Comment created',
435
+ slug: 'comment.created',
436
+ requiresSubject: true,
437
+ subjectRelationTo: ['comments'],
438
+ },
439
+ ],
376
440
  metrics: [{ name: 'Streak days', slug: 'streak-days' }],
377
441
  tiers: [
378
442
  {
@@ -0,0 +1,14 @@
1
+ import type { RelationshipFieldClientProps } from 'payload';
2
+ import React from 'react';
3
+ type SubjectFieldProps = RelationshipFieldClientProps & {
4
+ eventTypesSlug: string;
5
+ /** Plugin allowlist — used when the event type leaves `subjectRelationTo` empty. */
6
+ subjectCollections: string[];
7
+ };
8
+ /**
9
+ * Polymorphic subject — only when the selected event type requires a host document.
10
+ * Clears the value when the type does not require a subject.
11
+ */
12
+ export declare function SubjectField(props: SubjectFieldProps): React.JSX.Element | null;
13
+ export {};
14
+ //# sourceMappingURL=SubjectField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SubjectField.d.ts","sourceRoot":"","sources":["../../src/admin/SubjectField.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,SAAS,CAAA;AAE3D,OAAO,KAA6B,MAAM,OAAO,CAAA;AAIjD,KAAK,iBAAiB,GAAG,4BAA4B,GAAG;IACtD,cAAc,EAAE,MAAM,CAAA;IACtB,oFAAoF;IACpF,kBAAkB,EAAE,MAAM,EAAE,CAAA;CAC7B,CAAA;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,4BAoCpD"}
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { RelationshipField, useField } from '@payloadcms/ui';
4
+ import { useEffect, useMemo } from 'react';
5
+ import { useEventTypeFlags } from './useEventTypeFlags.js';
6
+ /**
7
+ * Polymorphic subject — only when the selected event type requires a host document.
8
+ * Clears the value when the type does not require a subject.
9
+ */
10
+ export function SubjectField(props) {
11
+ const { eventTypesSlug, subjectCollections, ...fieldArgs } = props;
12
+ const { requiresSubject, subjectRelationTo } = useEventTypeFlags(eventTypesSlug);
13
+ const { setValue, value } = useField({ path: fieldArgs.path });
14
+ const allowedCollections = useMemo(() => {
15
+ if (subjectRelationTo.length > 0) {
16
+ return subjectRelationTo.filter((slug) => subjectCollections.includes(slug));
17
+ }
18
+ return subjectCollections;
19
+ }, [subjectCollections, subjectRelationTo]);
20
+ useEffect(() => {
21
+ if (!requiresSubject && value != null && value !== '') {
22
+ setValue(null);
23
+ return;
24
+ }
25
+ if (!requiresSubject || !value || typeof value !== 'object')
26
+ return;
27
+ const relationTo = value.relationTo;
28
+ if (typeof relationTo === 'string' && allowedCollections.length > 0 && !allowedCollections.includes(relationTo)) {
29
+ setValue(null);
30
+ }
31
+ }, [requiresSubject, setValue, value, allowedCollections]);
32
+ if (!requiresSubject || allowedCollections.length === 0)
33
+ return null;
34
+ return (_jsx(RelationshipField, { ...fieldArgs, field: {
35
+ ...fieldArgs.field,
36
+ relationTo: allowedCollections,
37
+ } }));
38
+ }
@@ -1,6 +1,8 @@
1
1
  export type EventTypeFlags = {
2
2
  requiresActor: boolean;
3
3
  requiresMetric: boolean;
4
+ requiresSubject: boolean;
5
+ subjectRelationTo: string[];
4
6
  };
5
7
  /** Resolve event-type flags for the selected `type` field (fetch when only an id is present). */
6
8
  export declare function useEventTypeFlags(eventTypesSlug: string): EventTypeFlags;
@@ -1 +1 @@
1
- {"version":3,"file":"useEventTypeFlags.d.ts","sourceRoot":"","sources":["../../src/admin/useEventTypeFlags.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,cAAc,GAAG;IAC3B,aAAa,EAAE,OAAO,CAAA;IACtB,cAAc,EAAE,OAAO,CAAA;CACxB,CAAA;AA2BD,iGAAiG;AACjG,wBAAgB,iBAAiB,CAAC,cAAc,EAAE,MAAM,GAAG,cAAc,CAyCxE"}
1
+ {"version":3,"file":"useEventTypeFlags.d.ts","sourceRoot":"","sources":["../../src/admin/useEventTypeFlags.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,cAAc,GAAG;IAC3B,aAAa,EAAE,OAAO,CAAA;IACtB,cAAc,EAAE,OAAO,CAAA;IACvB,eAAe,EAAE,OAAO,CAAA;IACxB,iBAAiB,EAAE,MAAM,EAAE,CAAA;CAC5B,CAAA;AAqDD,iGAAiG;AACjG,wBAAgB,iBAAiB,CAAC,cAAc,EAAE,MAAM,GAAG,cAAc,CA4CxE"}
@@ -15,19 +15,35 @@ function relationId(value) {
15
15
  }
16
16
  return null;
17
17
  }
18
+ function readSubjectRelationTo(value) {
19
+ if (!Array.isArray(value))
20
+ return [];
21
+ return value.filter((entry) => typeof entry === 'string' && entry.trim().length > 0);
22
+ }
18
23
  function flagsFromValue(typeValue) {
19
24
  if (!typeValue || typeof typeValue !== 'object')
20
25
  return null;
21
- if (!('requiresActor' in typeValue || 'requiresMetric' in typeValue || 'slug' in typeValue)) {
26
+ if (!('requiresActor' in typeValue ||
27
+ 'requiresMetric' in typeValue ||
28
+ 'requiresSubject' in typeValue ||
29
+ 'subjectRelationTo' in typeValue ||
30
+ 'slug' in typeValue)) {
22
31
  return null;
23
32
  }
24
33
  const row = typeValue;
25
34
  return {
26
35
  requiresActor: Boolean(row.requiresActor),
27
36
  requiresMetric: Boolean(row.requiresMetric) || row.slug === 'metric.delta',
37
+ requiresSubject: Boolean(row.requiresSubject),
38
+ subjectRelationTo: readSubjectRelationTo(row.subjectRelationTo),
28
39
  };
29
40
  }
30
- const empty = { requiresActor: false, requiresMetric: false };
41
+ const empty = {
42
+ requiresActor: false,
43
+ requiresMetric: false,
44
+ requiresSubject: false,
45
+ subjectRelationTo: [],
46
+ };
31
47
  /** Resolve event-type flags for the selected `type` field (fetch when only an id is present). */
32
48
  export function useEventTypeFlags(eventTypesSlug) {
33
49
  const { config } = useConfig();
@@ -47,7 +63,9 @@ export function useEventTypeFlags(eventTypesSlug) {
47
63
  const controller = new AbortController();
48
64
  const api = config.routes?.api ?? '/api';
49
65
  const serverURL = config.serverURL ?? '';
50
- const url = `${serverURL}${api}/${eventTypesSlug}/${typeId}?depth=0&select[requiresActor]=true&select[requiresMetric]=true&select[slug]=true`;
66
+ const url = `${serverURL}${api}/${eventTypesSlug}/${typeId}` +
67
+ `?depth=0&select[requiresActor]=true&select[requiresMetric]=true` +
68
+ `&select[requiresSubject]=true&select[subjectRelationTo]=true&select[slug]=true`;
51
69
  void fetch(url, { credentials: 'include', signal: controller.signal })
52
70
  .then(async (res) => {
53
71
  if (!res.ok) {
@@ -12,7 +12,7 @@ export declare function runReconcileCli(args: {
12
12
  config: SanitizedConfig | Promise<SanitizedConfig>;
13
13
  argv?: string[];
14
14
  filters?: ReconcileCliFilters;
15
- /** When false, return the result instead of exiting the process. Default true. */
15
+ /** When false, return the result and leave the process running. Default true. */
16
16
  exit?: boolean;
17
17
  }): Promise<ReconcileProgressionResult>;
18
18
  //# sourceMappingURL=reconcile.d.ts.map
@@ -1 +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,sCAAsC,CAAA;AAE7C,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,CA4BtC"}
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,sCAAsC,CAAA;AAE7C,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,iFAAiF;IACjF,IAAI,CAAC,EAAE,OAAO,CAAA;CACf,GAAG,OAAO,CAAC,0BAA0B,CAAC,CA4BtC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/EventTypes/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAS,MAAM,SAAS,CAAA;AAOtD,wBAAgB,yBAAyB,IAAI,gBAAgB,CAgE5D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/EventTypes/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAS,MAAM,SAAS,CAAA;AAqCtD,wBAAgB,yBAAyB,IAAI,gBAAgB,CAoE5D"}
@@ -1,8 +1,36 @@
1
+ import { getAchievementOptions } from '../../options-store.js';
1
2
  import { collectionAdmin, collectionOf, columnsWithOptionalScope, optionalFields, slugOf } from '../helpers.js';
2
3
  import { localizedNameField, scopeField } from '../fields/index.js';
3
4
  import { access } from './access/index.js';
4
5
  import { hooks } from './hooks/index.js';
6
+ function subjectCatalogFields() {
7
+ const collections = getAchievementOptions().subjectCollections;
8
+ if (collections.length === 0)
9
+ return [];
10
+ return [
11
+ {
12
+ name: 'requiresSubject',
13
+ type: 'checkbox',
14
+ defaultValue: false,
15
+ label: 'Requires a subject',
16
+ admin: {
17
+ description: 'Turn this on when the log points at a host document (for example a post or comment). We’ll ask for that document when it’s logged.',
18
+ },
19
+ },
20
+ {
21
+ name: 'subjectRelationTo',
22
+ type: 'select',
23
+ hasMany: true,
24
+ options: collections.map((slug) => ({ label: slug, value: slug })),
25
+ admin: {
26
+ condition: (_, siblingData) => Boolean(siblingData?.requiresSubject),
27
+ description: 'Which host collections this type may reference. Leave empty to allow any collection from the plugin subjects allowlist.',
28
+ },
29
+ },
30
+ ];
31
+ }
5
32
  export function buildEventTypesCollection() {
33
+ const subjectFields = subjectCatalogFields();
6
34
  return {
7
35
  slug: slugOf('eventTypes'),
8
36
  access: { ...access },
@@ -14,6 +42,7 @@ export function buildEventTypesCollection() {
14
42
  'slug',
15
43
  'requiresActor',
16
44
  'requiresMetric',
45
+ ...(subjectFields.length > 0 ? ['requiresSubject'] : []),
17
46
  'system',
18
47
  'scope',
19
48
  ]),
@@ -46,6 +75,7 @@ export function buildEventTypesCollection() {
46
75
  description: 'Turn this on when the entry changes a score (like points). We’ll ask which metric and by how much.',
47
76
  },
48
77
  },
78
+ ...subjectFields,
49
79
  {
50
80
  name: 'system',
51
81
  type: 'checkbox',
@@ -2,4 +2,5 @@ import type { Validate } from 'payload';
2
2
  export declare const validateActor: Validate;
3
3
  export declare const validateMetric: Validate;
4
4
  export declare const validateChange: Validate;
5
+ export declare const validateSubject: Validate;
5
6
  //# sourceMappingURL=validators.d.ts.map
@@ -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;AASvC,eAAO,MAAM,aAAa,EAAE,QAW3B,CAAA;AAED,eAAO,MAAM,cAAc,EAAE,QA0B5B,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;AAavC,eAAO,MAAM,aAAa,EAAE,QAW3B,CAAA;AAED,eAAO,MAAM,cAAc,EAAE,QA0B5B,CAAA;AAED,eAAO,MAAM,cAAc,EAAE,QAW5B,CAAA;AAED,eAAO,MAAM,eAAe,EAAE,QAqC7B,CAAA"}
@@ -1,5 +1,7 @@
1
+ import { getAchievementOptions } from '../../../options-store.js';
1
2
  import { collectionOf } from '../../../collections/helpers.js';
2
- import { eventTypeRequiresActor, eventTypeRequiresMetric, } from '../../../services/achievement/eventTypeRequiresActor.js';
3
+ import { eventTypeRequiresActor, eventTypeRequiresMetric, eventTypeSubjectRelationTo, getEventTypeFlags, } from '../../../services/achievement/eventTypeRequiresActor.js';
4
+ import { readLogSubject } from '../../../services/achievement/logSubject.js';
3
5
  import { relationId } from '../../../services/achievement/relationId.js';
4
6
  export const validateActor = async (value, { data, req }) => {
5
7
  if (!req?.payload)
@@ -55,3 +57,37 @@ export const validateChange = async (value, { data, req }) => {
55
57
  }
56
58
  return true;
57
59
  };
60
+ export const validateSubject = async (value, { data, req }) => {
61
+ if (!req?.payload)
62
+ return true;
63
+ const allowlist = getAchievementOptions().subjectCollections;
64
+ if (allowlist.length === 0)
65
+ return true;
66
+ const typeId = relationId(data?.type);
67
+ if (!typeId)
68
+ return true;
69
+ const flags = await getEventTypeFlags({
70
+ payload: req.payload,
71
+ req,
72
+ typeIdOrDoc: typeId,
73
+ });
74
+ const subject = readLogSubject(value);
75
+ if (!flags.requiresSubject) {
76
+ return true;
77
+ }
78
+ if (!subject) {
79
+ return 'This log type needs a subject (the host document it is about).';
80
+ }
81
+ if (!allowlist.includes(subject.relationTo)) {
82
+ return `Subject collection "${subject.relationTo}" is not in the plugin subjects allowlist.`;
83
+ }
84
+ const allowed = await eventTypeSubjectRelationTo({
85
+ payload: req.payload,
86
+ req,
87
+ typeIdOrDoc: typeId,
88
+ });
89
+ if (allowed.length > 0 && !allowed.includes(subject.relationTo)) {
90
+ return `This log type only allows subjects from: ${allowed.join(', ')}.`;
91
+ }
92
+ return true;
93
+ };
@@ -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;AAU3D,eAAO,MAAM,sBAAsB,EAAE,4BAmDpC,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;AAc3D,eAAO,MAAM,sBAAsB,EAAE,4BA+DpC,CAAA"}
@@ -1,6 +1,8 @@
1
1
  import { APIError } from 'payload';
2
+ import { getAchievementOptions } from '../../../options-store.js';
2
3
  import { collectionOf } from '../../../collections/helpers.js';
3
- import { eventTypeRequiresActor, eventTypeRequiresMetric, } from '../../../services/achievement/eventTypeRequiresActor.js';
4
+ import { eventTypeSubjectRelationTo, getEventTypeFlags, } from '../../../services/achievement/eventTypeRequiresActor.js';
5
+ import { readLogSubject } from '../../../services/achievement/logSubject.js';
4
6
  import { relationId } from '../../../services/achievement/relationId.js';
5
7
  export const ensureTypeRequirements = async ({ data, req }) => {
6
8
  if (!data)
@@ -8,18 +10,11 @@ export const ensureTypeRequirements = async ({ data, req }) => {
8
10
  const typeId = relationId(data.type);
9
11
  if (!typeId)
10
12
  return data;
11
- const flags = {
12
- requiresActor: await eventTypeRequiresActor({
13
- payload: req.payload,
14
- req,
15
- typeIdOrDoc: typeId,
16
- }),
17
- requiresMetric: await eventTypeRequiresMetric({
18
- payload: req.payload,
19
- req,
20
- typeIdOrDoc: typeId,
21
- }),
22
- };
13
+ const flags = await getEventTypeFlags({
14
+ payload: req.payload,
15
+ req,
16
+ typeIdOrDoc: typeId,
17
+ });
23
18
  if (flags.requiresActor && !relationId(data.actor)) {
24
19
  throw new APIError('This log type needs an actor (who caused it).', 400);
25
20
  }
@@ -44,5 +39,19 @@ export const ensureTypeRequirements = async ({ data, req }) => {
44
39
  throw new APIError('Computed metrics cannot be changed with a metric delta. Use a stored metric such as Points.', 400);
45
40
  }
46
41
  }
42
+ if (flags.requiresSubject && getAchievementOptions().subjectCollections.length > 0) {
43
+ const subject = readLogSubject(data.subject);
44
+ if (!subject) {
45
+ throw new APIError('This log type needs a subject (the host document it is about).', 400);
46
+ }
47
+ const allowed = await eventTypeSubjectRelationTo({
48
+ payload: req.payload,
49
+ req,
50
+ typeIdOrDoc: typeId,
51
+ });
52
+ if (allowed.length > 0 && !allowed.includes(subject.relationTo)) {
53
+ throw new APIError(`This log type only allows subjects from: ${allowed.join(', ')}.`, 400);
54
+ }
55
+ }
47
56
  return data;
48
57
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/Logs/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAS,MAAM,SAAS,CAAA;AAUtD,+EAA+E;AAC/E,wBAAgB,mBAAmB,IAAI,gBAAgB,CA6GtD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/collections/Logs/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAS,MAAM,SAAS,CAAA;AAqCtD,+EAA+E;AAC/E,wBAAgB,mBAAmB,IAAI,gBAAgB,CAuHtD"}
@@ -3,12 +3,34 @@ import { collectionAdmin, collectionOf, optionalFields, slugOf } from '../helper
3
3
  import { getAchievementOptions } from '../../options-store.js';
4
4
  import { scopeField, userField } from '../fields/index.js';
5
5
  import { access } from './access/index.js';
6
- import { validateActor, validateChange, validateMetric } from './fields/validators.js';
6
+ import { validateActor, validateChange, validateMetric, validateSubject, } from './fields/validators.js';
7
7
  import { hooks } from './hooks/index.js';
8
+ function subjectLogField(eventTypesSlug) {
9
+ const collections = getAchievementOptions().subjectCollections;
10
+ if (collections.length === 0)
11
+ return null;
12
+ return {
13
+ name: 'subject',
14
+ type: 'relationship',
15
+ relationTo: collections,
16
+ index: true,
17
+ validate: validateSubject,
18
+ admin: {
19
+ description: 'Host document this entry is about (post, comment, …).',
20
+ components: {
21
+ Field: {
22
+ path: '@dappermountain/payload-plugin-achievements/client#SubjectField',
23
+ clientProps: { eventTypesSlug, subjectCollections: collections },
24
+ },
25
+ },
26
+ },
27
+ };
28
+ }
8
29
  /** Append-only occurrence log. `type` / `metric` are catalog relationships. */
9
30
  export function buildLogsCollection() {
10
31
  const users = getAchievementOptions().usersCollectionSlug ?? 'users';
11
32
  const eventTypesSlug = collectionOf('eventTypes');
33
+ const subjectField = subjectLogField(eventTypesSlug);
12
34
  return {
13
35
  slug: slugOf('logs'),
14
36
  labels: {
@@ -19,7 +41,15 @@ export function buildLogsCollection() {
19
41
  hooks,
20
42
  admin: collectionAdmin({
21
43
  useAsTitle: 'id',
22
- defaultColumns: ['type', 'user', 'actor', 'metric', 'change', 'createdAt'],
44
+ defaultColumns: [
45
+ 'type',
46
+ 'user',
47
+ 'actor',
48
+ ...(subjectField ? ['subject'] : []),
49
+ 'metric',
50
+ 'change',
51
+ 'createdAt',
52
+ ],
23
53
  description: 'Member activity history — grants, revokes, score changes, tier moves, and host custom events. Not a second grant store.',
24
54
  }),
25
55
  fields: [
@@ -88,6 +118,7 @@ export function buildLogsCollection() {
88
118
  },
89
119
  ],
90
120
  },
121
+ ...(subjectField ? [subjectField] : []),
91
122
  {
92
123
  type: 'collapsible',
93
124
  label: 'Details',
@@ -105,7 +136,7 @@ export function buildLogsCollection() {
105
136
  name: 'data',
106
137
  type: 'json',
107
138
  admin: {
108
- description: 'Engine transition envelope: `{ from, to }` each `{ id, slug? }` or null (tier.changed, achievement.granted/revoked). Hosts may add extra keys.',
139
+ description: 'Engine transition envelope: `{ from, to }` each `{ id, slug? }` or null (tier.changed, achievement.granted/revoked). Prefer `subject` for host document links.',
109
140
  },
110
141
  },
111
142
  ],
@@ -4,13 +4,15 @@ export declare const DEFAULT_USERS_COLLECTION = "users";
4
4
  export declare const DEFAULT_ME_ENDPOINT_PATH = "/achievements/me";
5
5
  export declare const DEFAULT_LEADERBOARD_ENDPOINT_PATH = "/achievements/leaderboard";
6
6
  export declare const DEFAULT_RECONCILE_ENDPOINT_PATH = "/achievements/reconcile";
7
- export type ResolvedAchievementOptions = Omit<AchievementPluginOptions, 'users' | 'endpoints'> & {
7
+ export type ResolvedAchievementOptions = Omit<AchievementPluginOptions, 'users' | 'endpoints' | 'subjects'> & {
8
8
  enabled: boolean;
9
9
  seedSystemCatalog: boolean;
10
10
  usersCollectionSlug: string;
11
11
  users: {
12
12
  includeJoins: boolean;
13
13
  };
14
+ /** Host collections allowed as polymorphic log subjects (empty = feature off). */
15
+ subjectCollections: string[];
14
16
  /** Resolved collection slug map (after prefix + overrides). */
15
17
  collectionSlugs: ReturnType<typeof resolveCollectionSlugs>;
16
18
  /** Admin nav group for plugin collections (`false` = ungrouped). */
@@ -21,6 +23,8 @@ export type ResolvedAchievementOptions = Omit<AchievementPluginOptions, 'users'
21
23
  reconcile: string | false;
22
24
  };
23
25
  };
26
+ /** Unique, non-empty collection slugs from plugin `subjects.collections`. */
27
+ export declare function resolveSubjectCollections(options?: AchievementPluginOptions): string[];
24
28
  export declare function resolveMeEndpointPath(me?: string | false): string | false;
25
29
  export declare function resolveLeaderboardEndpointPath(path?: string | false): string | false;
26
30
  export declare function resolveReconcileEndpointPath(path?: string | false): string | false;
@@ -1 +1 @@
1
- {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAC1D,OAAO,EAEL,sBAAsB,EACvB,MAAM,wBAAwB,CAAA;AAE/B,eAAO,MAAM,wBAAwB,UAAU,CAAA;AAC/C,eAAO,MAAM,wBAAwB,qBAAqB,CAAA;AAC1D,eAAO,MAAM,iCAAiC,8BAA8B,CAAA;AAC5E,eAAO,MAAM,+BAA+B,4BAA4B,CAAA;AAExE,MAAM,MAAM,0BAA0B,GAAG,IAAI,CAAC,wBAAwB,EAAE,OAAO,GAAG,WAAW,CAAC,GAAG;IAC/F,OAAO,EAAE,OAAO,CAAA;IAChB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,KAAK,EAAE;QAAE,YAAY,EAAE,OAAO,CAAA;KAAE,CAAA;IAChC,+DAA+D;IAC/D,eAAe,EAAE,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAA;IAC1D,oEAAoE;IACpE,UAAU,EAAE,MAAM,GAAG,KAAK,CAAA;IAC1B,SAAS,EAAE;QAAE,EAAE,EAAE,MAAM,GAAG,KAAK,CAAC;QAAC,WAAW,EAAE,MAAM,GAAG,KAAK,CAAC;QAAC,SAAS,EAAE,MAAM,GAAG,KAAK,CAAA;KAAE,CAAA;CAC1F,CAAA;AAQD,wBAAgB,qBAAqB,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAIzE;AAED,wBAAgB,8BAA8B,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAIpF;AAED,wBAAgB,4BAA4B,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAIlF;AAED,wBAAgB,cAAc,CAAC,OAAO,GAAE,wBAA6B,GAAG,0BAA0B,CAiBjG"}
1
+ {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAC1D,OAAO,EAEL,sBAAsB,EACvB,MAAM,wBAAwB,CAAA;AAE/B,eAAO,MAAM,wBAAwB,UAAU,CAAA;AAC/C,eAAO,MAAM,wBAAwB,qBAAqB,CAAA;AAC1D,eAAO,MAAM,iCAAiC,8BAA8B,CAAA;AAC5E,eAAO,MAAM,+BAA+B,4BAA4B,CAAA;AAExE,MAAM,MAAM,0BAA0B,GAAG,IAAI,CAAC,wBAAwB,EAAE,OAAO,GAAG,WAAW,GAAG,UAAU,CAAC,GAAG;IAC5G,OAAO,EAAE,OAAO,CAAA;IAChB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,KAAK,EAAE;QAAE,YAAY,EAAE,OAAO,CAAA;KAAE,CAAA;IAChC,kFAAkF;IAClF,kBAAkB,EAAE,MAAM,EAAE,CAAA;IAC5B,+DAA+D;IAC/D,eAAe,EAAE,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAA;IAC1D,oEAAoE;IACpE,UAAU,EAAE,MAAM,GAAG,KAAK,CAAA;IAC1B,SAAS,EAAE;QAAE,EAAE,EAAE,MAAM,GAAG,KAAK,CAAC;QAAC,WAAW,EAAE,MAAM,GAAG,KAAK,CAAC;QAAC,SAAS,EAAE,MAAM,GAAG,KAAK,CAAA;KAAE,CAAA;CAC1F,CAAA;AAED,6EAA6E;AAC7E,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,MAAM,EAAE,CAWtF;AAQD,wBAAgB,qBAAqB,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAIzE;AAED,wBAAgB,8BAA8B,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAIpF;AAED,wBAAgB,4BAA4B,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,CAIlF;AAED,wBAAgB,cAAc,CAAC,OAAO,GAAE,wBAA6B,GAAG,0BAA0B,CAmBjG"}
package/dist/defaults.js CHANGED
@@ -3,6 +3,20 @@ export const DEFAULT_USERS_COLLECTION = 'users';
3
3
  export const DEFAULT_ME_ENDPOINT_PATH = '/achievements/me';
4
4
  export const DEFAULT_LEADERBOARD_ENDPOINT_PATH = '/achievements/leaderboard';
5
5
  export const DEFAULT_RECONCILE_ENDPOINT_PATH = '/achievements/reconcile';
6
+ /** Unique, non-empty collection slugs from plugin `subjects.collections`. */
7
+ export function resolveSubjectCollections(options) {
8
+ const raw = options?.subjects?.collections ?? [];
9
+ const seen = new Set();
10
+ const out = [];
11
+ for (const slug of raw) {
12
+ const trimmed = typeof slug === 'string' ? slug.trim() : '';
13
+ if (!trimmed || seen.has(trimmed))
14
+ continue;
15
+ seen.add(trimmed);
16
+ out.push(trimmed);
17
+ }
18
+ return out;
19
+ }
6
20
  function normalizeEndpointPath(path, fallback) {
7
21
  const trimmed = path.trim();
8
22
  if (!trimmed)
@@ -31,14 +45,16 @@ export function resolveReconcileEndpointPath(path) {
31
45
  return normalizeEndpointPath(path, DEFAULT_RECONCILE_ENDPOINT_PATH);
32
46
  }
33
47
  export function resolveOptions(options = {}) {
48
+ const { subjects: _subjects, ...rest } = options;
34
49
  return {
35
- ...options,
50
+ ...rest,
36
51
  enabled: options.enabled !== false,
37
52
  seedSystemCatalog: options.seedSystemCatalog !== false,
38
53
  usersCollectionSlug: options.usersCollectionSlug ?? DEFAULT_USERS_COLLECTION,
39
54
  users: {
40
55
  includeJoins: options.users?.includeJoins !== false,
41
56
  },
57
+ subjectCollections: resolveSubjectCollections(options),
42
58
  collectionSlugs: resolveCollectionSlugs(options),
43
59
  adminGroup: resolveAdminGroup(options),
44
60
  endpoints: {
@@ -3,4 +3,5 @@ export { LucideIconField } from '../admin/LucideIconField.js';
3
3
  export { MetricChangeField } from '../admin/MetricChangeField.js';
4
4
  export { MetricField } from '../admin/MetricField.js';
5
5
  export { ReconcileProgressionButton } from '../admin/ReconcileProgressionButton.js';
6
+ export { SubjectField } from '../admin/SubjectField.js';
6
7
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/exports/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AACrD,OAAO,EAAE,0BAA0B,EAAE,MAAM,wCAAwC,CAAA"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/exports/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AACrD,OAAO,EAAE,0BAA0B,EAAE,MAAM,wCAAwC,CAAA;AACnF,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA"}
@@ -3,3 +3,4 @@ export { LucideIconField } from '../admin/LucideIconField.js';
3
3
  export { MetricChangeField } from '../admin/MetricChangeField.js';
4
4
  export { MetricField } from '../admin/MetricField.js';
5
5
  export { ReconcileProgressionButton } from '../admin/ReconcileProgressionButton.js';
6
+ export { SubjectField } from '../admin/SubjectField.js';
package/dist/index.d.ts CHANGED
@@ -3,11 +3,11 @@ export type { EngineEventTypeSlug, EngineMetricSlug } from './catalog.js';
3
3
  export { achievementPlugin } from './plugin.js';
4
4
  export { seedAchievementCatalog, seedAchievements } from './seed/index.js';
5
5
  export type { AchievementSeedAchievement, AchievementSeedCatalogRow, AchievementSeedInput, AchievementSeedLocalizedText, AchievementSeedTier, } from './seed/index.js';
6
- export { evaluateRules, evaluateRuleProgress, collectAchievementCompleteRefs, buildCatalogProgressGroups, buildUnlockRequirementLeaves, getUserProgress, loadUserProgressReviews, getMetricLeaderboard, grantAchievement, loadMetricDoc, recordEvent, recordLog, recordMetricChange, rebuildMetricBalance, rebuildMetricBalancesForUser, reconcileProgression, reconcileUserProgression, releasePendingAchievementReviews, releasePendingTierReviews, resolveCurrentTier, resolveElapsedAnchor, resolveMetricValue, resolveTierProgress, reviewAchievementRequest, submitAchievementRequest, syncTierChangedLog, syncTierProgression, syncCompositeProgression, catalogRef, transitionLogData, transitionToId, validateRuleGroup, validateRuleNode, } from './services/achievement/index.js';
7
- export type { ResolvedTier, RuleGroup, TierLadderProgress, CatalogProgressGroup, CatalogProgressItem, UnlockRequirementLeaf, UnlockRequirementRelation, GetUserProgressInclude, UserProgressMeResponse, UserProgressReviews, UserProgressTierRequest, CatalogRef, TransitionLogData, MetricLeaderboardResult, MetricLeaderboardRow, ReconcileProgressionArgs, ReconcileProgressionResult, ReleaseReviewGateResult, } from './services/achievement/index.js';
8
- export type { BuiltInEventType, BuiltInRuleType, AchievementCollectionKey, AchievementCollectionOverride, AchievementCollectionsOptions, AchievementEndpointsOptions, AchievementMetricAnchor, AchievementMetricAnchorConfig, AchievementMetricAnchorEntry, AchievementPluginOptions, AchievementRuleType, AchievementScopeConfig, AchievementUsersOptions, } from './types.js';
6
+ export { evaluateRules, evaluateRuleProgress, collectAchievementCompleteRefs, buildCatalogProgressGroups, buildUnlockRequirementLeaves, getUserProgress, loadUserProgressReviews, getMetricLeaderboard, grantAchievement, loadMetricDoc, recordEvent, recordLog, recordMetricChange, rebuildMetricBalance, rebuildMetricBalancesForUser, reconcileProgression, reconcileUserProgression, releasePendingAchievementReviews, releasePendingTierReviews, resolveCurrentTier, resolveElapsedAnchor, resolveMetricValue, resolveTierProgress, reviewAchievementRequest, submitAchievementRequest, syncTierChangedLog, syncTierProgression, syncCompositeProgression, catalogRef, transitionLogData, transitionToId, logSubject, readLogSubject, validateRuleGroup, validateRuleNode, } from './services/achievement/index.js';
7
+ export type { ResolvedTier, RuleGroup, TierLadderProgress, CatalogProgressGroup, CatalogProgressItem, UnlockRequirementLeaf, UnlockRequirementRelation, GetUserProgressInclude, UserProgressMeResponse, UserProgressReviews, UserProgressTierRequest, CatalogRef, TransitionLogData, LogSubjectRef, MetricLeaderboardResult, MetricLeaderboardRow, ReconcileProgressionArgs, ReconcileProgressionResult, ReleaseReviewGateResult, } from './services/achievement/index.js';
8
+ export type { BuiltInEventType, BuiltInRuleType, AchievementCollectionKey, AchievementCollectionOverride, AchievementCollectionsOptions, AchievementEndpointsOptions, AchievementMetricAnchor, AchievementMetricAnchorConfig, AchievementMetricAnchorEntry, AchievementPluginOptions, AchievementRuleType, AchievementScopeConfig, AchievementSubjectsOptions, AchievementUsersOptions, } from './types.js';
9
9
  export { DEFAULT_COLLECTION_BASE_SLUGS, DEFAULT_COLLECTION_PREFIX, DEFAULT_ADMIN_GROUP } from './collections/slugs.js';
10
- export { DEFAULT_ME_ENDPOINT_PATH, DEFAULT_LEADERBOARD_ENDPOINT_PATH, DEFAULT_RECONCILE_ENDPOINT_PATH, } from './defaults.js';
10
+ export { DEFAULT_ME_ENDPOINT_PATH, DEFAULT_LEADERBOARD_ENDPOINT_PATH, DEFAULT_RECONCILE_ENDPOINT_PATH, resolveSubjectCollections, } from './defaults.js';
11
11
  export { collectionOf, slugOf } from './collections/helpers.js';
12
12
  export { eachRuleLeaf } from './collections/hooks/eachRuleLeaf.js';
13
13
  export { isAchievementPluginEnabled } from './enabled.js';
@@ -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,cAAc,CAAA;AACrB,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAC1E,YAAY,EACV,0BAA0B,EAC1B,yBAAyB,EACzB,oBAAoB,EACpB,4BAA4B,EAC5B,mBAAmB,GACpB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,8BAA8B,EAC9B,0BAA0B,EAC1B,4BAA4B,EAC5B,eAAe,EACf,uBAAuB,EACvB,oBAAoB,EACpB,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,4BAA4B,EAC5B,oBAAoB,EACpB,wBAAwB,EACxB,gCAAgC,EAChC,yBAAyB,EACzB,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,EACxB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,iCAAiC,CAAA;AACxC,YAAY,EACV,YAAY,EACZ,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EACzB,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACnB,uBAAuB,EACvB,UAAU,EACV,iBAAiB,EACjB,uBAAuB,EACvB,oBAAoB,EACpB,wBAAwB,EACxB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,iCAAiC,CAAA;AACxC,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,wBAAwB,EACxB,6BAA6B,EAC7B,6BAA6B,EAC7B,2BAA2B,EAC3B,uBAAuB,EACvB,6BAA6B,EAC7B,4BAA4B,EAC5B,wBAAwB,EACxB,mBAAmB,EACnB,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,6BAA6B,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AACtH,OAAO,EACL,wBAAwB,EACxB,iCAAiC,EACjC,+BAA+B,GAChC,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAA;AAClE,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAA;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA"}
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,cAAc,CAAA;AACrB,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAC1E,YAAY,EACV,0BAA0B,EAC1B,yBAAyB,EACzB,oBAAoB,EACpB,4BAA4B,EAC5B,mBAAmB,GACpB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,8BAA8B,EAC9B,0BAA0B,EAC1B,4BAA4B,EAC5B,eAAe,EACf,uBAAuB,EACvB,oBAAoB,EACpB,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,4BAA4B,EAC5B,oBAAoB,EACpB,wBAAwB,EACxB,gCAAgC,EAChC,yBAAyB,EACzB,kBAAkB,EAClB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,EACxB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,iCAAiC,CAAA;AACxC,YAAY,EACV,YAAY,EACZ,SAAS,EACT,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,yBAAyB,EACzB,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACnB,uBAAuB,EACvB,UAAU,EACV,iBAAiB,EACjB,aAAa,EACb,uBAAuB,EACvB,oBAAoB,EACpB,wBAAwB,EACxB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,iCAAiC,CAAA;AACxC,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,wBAAwB,EACxB,6BAA6B,EAC7B,6BAA6B,EAC7B,2BAA2B,EAC3B,uBAAuB,EACvB,6BAA6B,EAC7B,4BAA4B,EAC5B,wBAAwB,EACxB,mBAAmB,EACnB,sBAAsB,EACtB,0BAA0B,EAC1B,uBAAuB,GACxB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,6BAA6B,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AACtH,OAAO,EACL,wBAAwB,EACxB,iCAAiC,EACjC,+BAA+B,EAC/B,yBAAyB,GAC1B,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAA;AAClE,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAA;AACzE,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA"}
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  export { DEFAULT_ACHIEVEMENT_METRIC, DEFAULT_ACHIEVEMENT_METRIC_SLUG, ENGINE_EVENT_TYPES, ENGINE_METRICS, } from './catalog.js';
2
2
  export { achievementPlugin } from './plugin.js';
3
3
  export { seedAchievementCatalog, seedAchievements } from './seed/index.js';
4
- export { evaluateRules, evaluateRuleProgress, collectAchievementCompleteRefs, buildCatalogProgressGroups, buildUnlockRequirementLeaves, getUserProgress, loadUserProgressReviews, getMetricLeaderboard, grantAchievement, loadMetricDoc, recordEvent, recordLog, recordMetricChange, rebuildMetricBalance, rebuildMetricBalancesForUser, reconcileProgression, reconcileUserProgression, releasePendingAchievementReviews, releasePendingTierReviews, resolveCurrentTier, resolveElapsedAnchor, resolveMetricValue, resolveTierProgress, reviewAchievementRequest, submitAchievementRequest, syncTierChangedLog, syncTierProgression, syncCompositeProgression, catalogRef, transitionLogData, transitionToId, validateRuleGroup, validateRuleNode, } from './services/achievement/index.js';
4
+ export { evaluateRules, evaluateRuleProgress, collectAchievementCompleteRefs, buildCatalogProgressGroups, buildUnlockRequirementLeaves, getUserProgress, loadUserProgressReviews, getMetricLeaderboard, grantAchievement, loadMetricDoc, recordEvent, recordLog, recordMetricChange, rebuildMetricBalance, rebuildMetricBalancesForUser, reconcileProgression, reconcileUserProgression, releasePendingAchievementReviews, releasePendingTierReviews, resolveCurrentTier, resolveElapsedAnchor, resolveMetricValue, resolveTierProgress, reviewAchievementRequest, submitAchievementRequest, syncTierChangedLog, syncTierProgression, syncCompositeProgression, catalogRef, transitionLogData, transitionToId, logSubject, readLogSubject, validateRuleGroup, validateRuleNode, } from './services/achievement/index.js';
5
5
  export { DEFAULT_COLLECTION_BASE_SLUGS, DEFAULT_COLLECTION_PREFIX, DEFAULT_ADMIN_GROUP } from './collections/slugs.js';
6
- export { DEFAULT_ME_ENDPOINT_PATH, DEFAULT_LEADERBOARD_ENDPOINT_PATH, DEFAULT_RECONCILE_ENDPOINT_PATH, } from './defaults.js';
6
+ export { DEFAULT_ME_ENDPOINT_PATH, DEFAULT_LEADERBOARD_ENDPOINT_PATH, DEFAULT_RECONCILE_ENDPOINT_PATH, resolveSubjectCollections, } from './defaults.js';
7
7
  export { collectionOf, slugOf } from './collections/helpers.js';
8
8
  export { eachRuleLeaf } from './collections/hooks/eachRuleLeaf.js';
9
9
  export { isAchievementPluginEnabled } from './enabled.js';
@@ -57,6 +57,13 @@ export type AchievementSeedCatalogRow = {
57
57
  requiresActor?: boolean;
58
58
  /** Event types only — when true, logging the event requires metric + change. */
59
59
  requiresMetric?: boolean;
60
+ /** Event types only — when true, logging the event requires a polymorphic `subject`. */
61
+ requiresSubject?: boolean;
62
+ /**
63
+ * Event types only — allowed host collections for `subject.relationTo`.
64
+ * Empty / omit = any collection from plugin `subjects.collections`.
65
+ */
66
+ subjectRelationTo?: string[];
60
67
  };
61
68
  export type AchievementSeedInput = {
62
69
  eventTypes?: AchievementSeedCatalogRow[];
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/seed/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/seed/index.ts"],"names":[],"mappings":"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"}
@@ -74,6 +74,10 @@ async function upsertCatalog(payload, key, row) {
74
74
  ? {
75
75
  requiresActor: row.requiresActor ?? false,
76
76
  requiresMetric: row.requiresMetric ?? false,
77
+ requiresSubject: row.requiresSubject ?? false,
78
+ ...(row.subjectRelationTo && row.subjectRelationTo.length > 0
79
+ ? { subjectRelationTo: row.subjectRelationTo }
80
+ : { subjectRelationTo: [] }),
77
81
  }
78
82
  : metricComputed),
79
83
  };
@@ -2,7 +2,7 @@ const MS_PER_SECOND = 1_000;
2
2
  const MS_PER_MINUTE = 60 * MS_PER_SECOND;
3
3
  const MS_PER_HOUR = 60 * MS_PER_MINUTE;
4
4
  const MS_PER_DAY = 24 * MS_PER_HOUR;
5
- /** Fixed-length year for elapsed floors (not calendar years). */
5
+ /** Fixed-length year for elapsed floors (365-day year). */
6
6
  const MS_PER_YEAR = 365 * MS_PER_DAY;
7
7
  export function durationMs(amount, unit) {
8
8
  if (!(typeof amount === 'number') || !Number.isFinite(amount) || amount < 0)
@@ -2,6 +2,9 @@ import type { Payload, PayloadRequest } from 'payload';
2
2
  export type EventTypeFlags = {
3
3
  requiresActor: boolean;
4
4
  requiresMetric: boolean;
5
+ requiresSubject: boolean;
6
+ /** Allowed host collections for `subject.relationTo` (empty = any plugin allowlist entry). */
7
+ subjectRelationTo: string[];
5
8
  };
6
9
  /** Read flags from a loaded event-type document (or populated relationship). */
7
10
  export declare function readEventTypeFlags(doc: unknown): EventTypeFlags | null;
@@ -21,4 +24,18 @@ export declare function eventTypeRequiresMetric(args: {
21
24
  req?: PayloadRequest;
22
25
  typeIdOrDoc: unknown;
23
26
  }): Promise<boolean>;
27
+ export declare function eventTypeRequiresSubject(args: {
28
+ payload: Payload;
29
+ req?: PayloadRequest;
30
+ typeIdOrDoc: unknown;
31
+ }): Promise<boolean>;
32
+ /**
33
+ * Allowed `subject.relationTo` values for this event type.
34
+ * Empty event-type list → full plugin allowlist. Feature off → [].
35
+ */
36
+ export declare function eventTypeSubjectRelationTo(args: {
37
+ payload: Payload;
38
+ req?: PayloadRequest;
39
+ typeIdOrDoc: unknown;
40
+ }): Promise<string[]>;
24
41
  //# sourceMappingURL=eventTypeRequiresActor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"eventTypeRequiresActor.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/eventTypeRequiresActor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAKtD,MAAM,MAAM,cAAc,GAAG;IAC3B,aAAa,EAAE,OAAO,CAAA;IACtB,cAAc,EAAE,OAAO,CAAA;CACxB,CAAA;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CAUtE;AAMD,wDAAwD;AACxD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,cAAc,CAAC,CAsB1B;AAED,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnB;AAED,wBAAsB,uBAAuB,CAAC,IAAI,EAAE;IAClD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnB"}
1
+ {"version":3,"file":"eventTypeRequiresActor.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/eventTypeRequiresActor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAMtD,MAAM,MAAM,cAAc,GAAG;IAC3B,aAAa,EAAE,OAAO,CAAA;IACtB,cAAc,EAAE,OAAO,CAAA;IACvB,eAAe,EAAE,OAAO,CAAA;IACxB,8FAA8F;IAC9F,iBAAiB,EAAE,MAAM,EAAE,CAAA;CAC5B,CAAA;AAkBD,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CAsBtE;AAMD,wDAAwD;AACxD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,cAAc,CAAC,CAwC1B;AAED,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnB;AAED,wBAAsB,uBAAuB,CAAC,IAAI,EAAE;IAClD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnB;AAED,wBAAsB,wBAAwB,CAAC,IAAI,EAAE;IACnD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnB;AAED;;;GAGG;AACH,wBAAsB,0BAA0B,CAAC,IAAI,EAAE;IACrD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,WAAW,EAAE,OAAO,CAAA;CACrB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAOpB"}
@@ -1,16 +1,39 @@
1
+ import { getAchievementOptions } from '../../options-store.js';
1
2
  import { collectionOf } from '../../collections/helpers.js';
2
3
  import { relationId } from './relationId.js';
4
+ const emptyFlags = {
5
+ requiresActor: false,
6
+ requiresMetric: false,
7
+ requiresSubject: false,
8
+ subjectRelationTo: [],
9
+ };
10
+ function readSubjectRelationTo(value) {
11
+ if (!Array.isArray(value))
12
+ return [];
13
+ const out = [];
14
+ for (const entry of value) {
15
+ if (typeof entry === 'string' && entry.trim())
16
+ out.push(entry.trim());
17
+ }
18
+ return out;
19
+ }
3
20
  /** Read flags from a loaded event-type document (or populated relationship). */
4
21
  export function readEventTypeFlags(doc) {
5
22
  if (!doc || typeof doc !== 'object')
6
23
  return null;
7
24
  const row = doc;
8
- const hasFlagKeys = 'requiresActor' in row || 'requiresMetric' in row || typeof row.slug === 'string';
25
+ const hasFlagKeys = 'requiresActor' in row ||
26
+ 'requiresMetric' in row ||
27
+ 'requiresSubject' in row ||
28
+ 'subjectRelationTo' in row ||
29
+ typeof row.slug === 'string';
9
30
  if (!hasFlagKeys)
10
31
  return null;
11
32
  return {
12
33
  requiresActor: Boolean(row.requiresActor),
13
34
  requiresMetric: Boolean(row.requiresMetric) || row.slug === 'metric.delta',
35
+ requiresSubject: Boolean(row.requiresSubject),
36
+ subjectRelationTo: readSubjectRelationTo(row.subjectRelationTo),
14
37
  };
15
38
  }
16
39
  function flagsFromDoc(doc) {
@@ -21,22 +44,32 @@ export async function getEventTypeFlags(args) {
21
44
  const embedded = flagsFromDoc(args.typeIdOrDoc);
22
45
  if (embedded && args.typeIdOrDoc && typeof args.typeIdOrDoc === 'object') {
23
46
  const row = args.typeIdOrDoc;
24
- if ('requiresActor' in row || 'requiresMetric' in row || 'slug' in row) {
47
+ if ('requiresActor' in row ||
48
+ 'requiresMetric' in row ||
49
+ 'requiresSubject' in row ||
50
+ 'subjectRelationTo' in row ||
51
+ 'slug' in row) {
25
52
  return embedded;
26
53
  }
27
54
  }
28
55
  const typeId = relationId(args.typeIdOrDoc);
29
56
  if (!typeId)
30
- return { requiresActor: false, requiresMetric: false };
57
+ return { ...emptyFlags };
31
58
  const found = (await args.payload.findByID({
32
59
  collection: collectionOf('eventTypes'),
33
60
  id: typeId,
34
61
  depth: 0,
35
62
  overrideAccess: true,
36
63
  req: args.req,
37
- select: { requiresActor: true, requiresMetric: true, slug: true },
64
+ select: {
65
+ requiresActor: true,
66
+ requiresMetric: true,
67
+ requiresSubject: true,
68
+ subjectRelationTo: true,
69
+ slug: true,
70
+ },
38
71
  }));
39
- return flagsFromDoc(found) ?? { requiresActor: false, requiresMetric: false };
72
+ return flagsFromDoc(found) ?? { ...emptyFlags };
40
73
  }
41
74
  export async function eventTypeRequiresActor(args) {
42
75
  return (await getEventTypeFlags(args)).requiresActor;
@@ -44,3 +77,21 @@ export async function eventTypeRequiresActor(args) {
44
77
  export async function eventTypeRequiresMetric(args) {
45
78
  return (await getEventTypeFlags(args)).requiresMetric;
46
79
  }
80
+ export async function eventTypeRequiresSubject(args) {
81
+ return (await getEventTypeFlags(args)).requiresSubject;
82
+ }
83
+ /**
84
+ * Allowed `subject.relationTo` values for this event type.
85
+ * Empty event-type list → full plugin allowlist. Feature off → [].
86
+ */
87
+ export async function eventTypeSubjectRelationTo(args) {
88
+ const allowlist = getAchievementOptions().subjectCollections;
89
+ if (allowlist.length === 0)
90
+ return [];
91
+ const flags = await getEventTypeFlags(args);
92
+ if (!flags.requiresSubject)
93
+ return [];
94
+ if (flags.subjectRelationTo.length === 0)
95
+ return [...allowlist];
96
+ return flags.subjectRelationTo.filter((slug) => allowlist.includes(slug));
97
+ }
@@ -14,6 +14,8 @@ export { applyMetricBalanceDelta, findMetricBalance, metricBalanceKey, rebuildMe
14
14
  export { resolveMetricValue, loadMetricDoc } from './resolveMetricValue.js';
15
15
  export { resolveElapsedAnchor } from './resolveElapsedAnchor.js';
16
16
  export { recordEvent, recordLog, recordMetricChange, syncTierChangedLog } from './recordEvent.js';
17
+ export { logSubject, readLogSubject } from './logSubject.js';
18
+ export type { LogSubjectRef } from './logSubject.js';
17
19
  export { resolveCurrentTier } from './resolveCurrentTier.js';
18
20
  export type { ResolvedTier } from './resolveCurrentTier.js';
19
21
  export { resolveTierProgress } from './resolveTierProgress.js';
@@ -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,oBAAoB,CAAA;AAC5H,YAAY,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAA;AAC5E,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAA;AAChG,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAA;AAChF,YAAY,EACV,8BAA8B,EAC9B,qBAAqB,EACrB,yBAAyB,GAC1B,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AACjH,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AACtE,YAAY,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AAChG,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,YAAY,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAC9F,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,4BAA4B,GAC7B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAC3E,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACjG,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,YAAY,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAClE,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAC1F,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AACxE,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACxE,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAA;AAC/E,YAAY,EAAE,wBAAwB,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAA;AAC1F,OAAO,EACL,wBAAwB,EACxB,gCAAgC,EAChC,yBAAyB,GAC1B,MAAM,wBAAwB,CAAA;AAC/B,YAAY,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AACrE,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,cAAc,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,oBAAoB,CAAA;AAC5H,YAAY,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAA;AAC5E,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAA;AAChG,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAA;AAChF,YAAY,EACV,8BAA8B,EAC9B,qBAAqB,EACrB,yBAAyB,GAC1B,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAA;AACjH,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AACtE,YAAY,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AAChG,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,YAAY,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAC9F,OAAO,EACL,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,4BAA4B,GAC7B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAC3E,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACjG,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAC5D,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC5D,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,YAAY,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAClE,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAC1F,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AACxE,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACxE,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAA;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC1D,OAAO,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAA;AAC/E,YAAY,EAAE,wBAAwB,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAA;AAC1F,OAAO,EACL,wBAAwB,EACxB,gCAAgC,EAChC,yBAAyB,GAC1B,MAAM,wBAAwB,CAAA;AAC/B,YAAY,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AACrE,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA"}
@@ -8,6 +8,7 @@ export { applyMetricBalanceDelta, findMetricBalance, metricBalanceKey, rebuildMe
8
8
  export { resolveMetricValue, loadMetricDoc } from './resolveMetricValue.js';
9
9
  export { resolveElapsedAnchor } from './resolveElapsedAnchor.js';
10
10
  export { recordEvent, recordLog, recordMetricChange, syncTierChangedLog } from './recordEvent.js';
11
+ export { logSubject, readLogSubject } from './logSubject.js';
11
12
  export { resolveCurrentTier } from './resolveCurrentTier.js';
12
13
  export { resolveTierProgress } from './resolveTierProgress.js';
13
14
  export { grantAchievement, submitAchievementRequest } from './submitAchievementRequest.js';
@@ -0,0 +1,13 @@
1
+ /** Polymorphic relationship value (Payload `relationTo` + `value`). */
2
+ export type LogSubjectRef = {
3
+ relationTo: string;
4
+ value: string;
5
+ };
6
+ /** Build a log subject ref for `recordLog({ subject })`. */
7
+ export declare function logSubject(relationTo: string, value: string): LogSubjectRef;
8
+ /**
9
+ * Normalize a polymorphic relationship (Admin / Local API shape) to `{ relationTo, value }`.
10
+ * Accepts nested docs (`value: { id }`) and plain ids under `value`.
11
+ */
12
+ export declare function readLogSubject(value: unknown): LogSubjectRef | null;
13
+ //# sourceMappingURL=logSubject.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logSubject.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/logSubject.ts"],"names":[],"mappings":"AAEA,uEAAuE;AACvE,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,4DAA4D;AAC5D,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAE3E;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CAOnE"}
@@ -0,0 +1,20 @@
1
+ import { relationId } from './relationId.js';
2
+ /** Build a log subject ref for `recordLog({ subject })`. */
3
+ export function logSubject(relationTo, value) {
4
+ return { relationTo, value };
5
+ }
6
+ /**
7
+ * Normalize a polymorphic relationship (Admin / Local API shape) to `{ relationTo, value }`.
8
+ * Accepts nested docs (`value: { id }`) and plain ids under `value`.
9
+ */
10
+ export function readLogSubject(value) {
11
+ if (!value || typeof value !== 'object')
12
+ return null;
13
+ const row = value;
14
+ if (typeof row.relationTo !== 'string' || !row.relationTo.trim())
15
+ return null;
16
+ const id = relationId(row.value);
17
+ if (!id)
18
+ return null;
19
+ return { relationTo: row.relationTo.trim(), value: id };
20
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Payload, PayloadRequest } from 'payload';
2
+ import { type LogSubjectRef } from './logSubject.js';
2
3
  export type RecordLogInput = {
3
4
  payload: Payload;
4
5
  req?: PayloadRequest;
@@ -12,6 +13,11 @@ export type RecordLogInput = {
12
13
  /** Signed amount the metric moved (for metric.delta logs). */
13
14
  change?: number;
14
15
  reason?: string;
16
+ /**
17
+ * Host document this entry is about (polymorphic).
18
+ * Preferred over stuffing ids into `data` when the host configured `subjects.collections`.
19
+ */
20
+ subject?: LogSubjectRef | null;
15
21
  data?: Record<string, unknown> | null;
16
22
  };
17
23
  /** Append a catalogued occurrence to the achievement log. */
@@ -1 +1 @@
1
- {"version":3,"file":"recordEvent.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/recordEvent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAatD,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CACtC,CAAA;AAgBD,6DAA6D;AAC7D,wBAAsB,SAAS,CAAC,IAAI,EAAE,cAAc,wEAuCnD;AAED,4CAA4C;AAC5C,eAAO,MAAM,WAAW,kBAAY,CAAA;AAEpC;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,wEAsDA;AAED,6FAA6F;AAC7F,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,iFA0DA"}
1
+ {"version":3,"file":"recordEvent.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/recordEvent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAUtD,OAAO,EAAE,KAAK,aAAa,EAAkB,MAAM,iBAAiB,CAAA;AAQpE,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,OAAO,CAAC,EAAE,aAAa,GAAG,IAAI,CAAA;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CACtC,CAAA;AAgBD,6DAA6D;AAC7D,wBAAsB,SAAS,CAAC,IAAI,EAAE,cAAc,wEA0EnD;AAED,4CAA4C;AAC5C,eAAO,MAAM,WAAW,kBAAY,CAAA;AAEpC;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,wEAsDA;AAED,6FAA6F;AAC7F,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,iFA0DA"}
@@ -1,7 +1,9 @@
1
1
  import { APIError } from 'payload';
2
2
  import { DEFAULT_ACHIEVEMENT_METRIC_SLUG } from '../../catalog.js';
3
3
  import { collectionOf } from '../../collections/helpers.js';
4
- import { eventTypeRequiresActor } from './eventTypeRequiresActor.js';
4
+ import { getAchievementOptions } from '../../options-store.js';
5
+ import { eventTypeSubjectRelationTo, getEventTypeFlags, } from './eventTypeRequiresActor.js';
6
+ import { readLogSubject } from './logSubject.js';
5
7
  import { applyMetricBalanceDelta } from './metricBalances.js';
6
8
  import { transitionLogData, transitionToId } from './logData.js';
7
9
  import { loadMetricDoc } from './resolveMetricValue.js';
@@ -33,9 +35,34 @@ export async function recordLog(args) {
33
35
  value: args.metric,
34
36
  })
35
37
  : undefined;
36
- if (await eventTypeRequiresActor({ payload: args.payload, req: args.req, typeIdOrDoc: typeId })) {
37
- if (!args.actorId) {
38
- throw new APIError('This log type needs an actor (who caused it).', 400);
38
+ const flags = await getEventTypeFlags({
39
+ payload: args.payload,
40
+ req: args.req,
41
+ typeIdOrDoc: typeId,
42
+ });
43
+ if (flags.requiresActor && !args.actorId) {
44
+ throw new APIError('This log type needs an actor (who caused it).', 400);
45
+ }
46
+ const allowlist = getAchievementOptions().subjectCollections;
47
+ let subject;
48
+ if (args.subject) {
49
+ const parsed = readLogSubject(args.subject) ?? args.subject;
50
+ subject = parsed;
51
+ }
52
+ if (flags.requiresSubject && allowlist.length > 0) {
53
+ if (!subject) {
54
+ throw new APIError('This log type needs a subject (the host document it is about).', 400);
55
+ }
56
+ if (!allowlist.includes(subject.relationTo)) {
57
+ throw new APIError(`Subject collection "${subject.relationTo}" is not in the plugin subjects allowlist.`, 400);
58
+ }
59
+ const allowed = await eventTypeSubjectRelationTo({
60
+ payload: args.payload,
61
+ req: args.req,
62
+ typeIdOrDoc: typeId,
63
+ });
64
+ if (allowed.length > 0 && !allowed.includes(subject.relationTo)) {
65
+ throw new APIError(`This log type only allows subjects from: ${allowed.join(', ')}.`, 400);
39
66
  }
40
67
  }
41
68
  return args.payload.create({
@@ -48,6 +75,7 @@ export async function recordLog(args) {
48
75
  metric: metricId,
49
76
  change: args.change,
50
77
  reason: args.reason,
78
+ ...(subject ? { subject } : {}),
51
79
  data: args.data ?? undefined,
52
80
  },
53
81
  overrideAccess: true,
package/dist/types.d.ts CHANGED
@@ -9,7 +9,7 @@ export type AchievementMetricAnchor = (args: {
9
9
  /**
10
10
  * Anchor with an Admin label.
11
11
  * `label` is a Payload Admin UI string ({@link OptionLabel}): plain string, `{ en, es, … }`,
12
- * or `({ t }) => t('custom:…')` — not CMS `localization`.
12
+ * or `({ t }) => t('custom:…')` (Admin UI / OptionLabel).
13
13
  */
14
14
  export type AchievementMetricAnchorConfig = {
15
15
  label: OptionLabel;
@@ -40,11 +40,11 @@ export type AchievementRuleType = {
40
40
  /**
41
41
  * Optional fractional progress toward this leaf (0..1).
42
42
  * When omitted, progress falls back to `evaluate` → 0 or 1.
43
- * Used for next-tier ladder fill and host UIs — not for unlock boolean.
43
+ * Used for next-tier ladder fill and host UIs (progress display).
44
44
  */
45
45
  progress?: (args: AchievementRuleEvalArgs) => Promise<number> | number;
46
46
  /**
47
- * `gate` rules (e.g. `tier-at-least`) are prerequisites, not progress.
47
+ * `gate` rules (e.g. `tier-at-least`) are prerequisites that must already hold.
48
48
  * They are omitted from AND averages. Default: `requirement`.
49
49
  */
50
50
  progressRole?: 'requirement' | 'gate';
@@ -110,6 +110,17 @@ export type AchievementEndpointsOptions = {
110
110
  */
111
111
  reconcile?: string | false;
112
112
  };
113
+ /**
114
+ * Host documents that achievement logs may point at (polymorphic `subject`).
115
+ * Empty / omitted → subject field is not registered (engine stays host-agnostic).
116
+ */
117
+ export type AchievementSubjectsOptions = {
118
+ /**
119
+ * Collection slugs allowed on log `subject.relationTo` (e.g. `posts`, `comments`).
120
+ * Event types pick a subset via `subjectRelationTo`; validation rejects anything outside this list.
121
+ */
122
+ collections?: string[];
123
+ };
113
124
  export type AchievementPluginOptions = {
114
125
  /**
115
126
  * Whether the user may review requests and manage achievement catalogs for a scope.
@@ -121,6 +132,11 @@ export type AchievementPluginOptions = {
121
132
  enabled?: boolean;
122
133
  /** Custom root API routes registered by the plugin. */
123
134
  endpoints?: AchievementEndpointsOptions;
135
+ /**
136
+ * Polymorphic log subjects — host allowlist of collections.
137
+ * When set, event types gain `requiresSubject` / `subjectRelationTo` and logs gain `subject`.
138
+ */
139
+ subjects?: AchievementSubjectsOptions;
124
140
  extensions?: {
125
141
  /** Custom rule evaluators merged with built-ins. */
126
142
  ruleTypes?: AchievementRuleType[];
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dappermountain/payload-plugin-achievements",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "A robust, Admin-configured achievements engine for Payload: composable unlock rules, derived rank progress, request/review grants, metrics, and scoped multi-tenant support.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -69,21 +69,21 @@
69
69
  "skills:check": "bunx skills check -p"
70
70
  },
71
71
  "peerDependencies": {
72
- "@payloadcms/richtext-lexical": "^3.88.0",
73
- "@payloadcms/ui": "^3.88.0",
72
+ "@payloadcms/richtext-lexical": "^3.89.0",
73
+ "@payloadcms/ui": "^3.89.0",
74
74
  "lucide-react": "^0.543.0",
75
- "payload": "^3.88.0",
75
+ "payload": "^3.89.0",
76
76
  "react": "^19.0.0"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@dappermountain/agent-payload": "^0.1.5",
80
80
  "@dappermountain/agent-practices": "^0.1.2",
81
- "@payloadcms/richtext-lexical": "3.88.0",
82
- "@payloadcms/ui": "3.88.0",
81
+ "@payloadcms/richtext-lexical": "3.89.0",
82
+ "@payloadcms/ui": "3.89.0",
83
83
  "@types/bun": "1.4.0",
84
84
  "@types/react": "^19.0.0",
85
85
  "lucide-react": "0.543.0",
86
- "payload": "3.88.0",
86
+ "payload": "3.89.0",
87
87
  "prettier": "^3.6.2",
88
88
  "react": "19.2.4",
89
89
  "typescript": "5.9.2"