@dappermountain/payload-plugin-achievements 0.2.5 → 0.2.7
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 +34 -8
- package/dist/admin/LucideIconField.d.ts +1 -1
- package/dist/admin/LucideIconField.d.ts.map +1 -1
- package/dist/admin/LucideIconField.js +139 -24
- package/dist/catalog.d.ts +1 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +1 -1
- package/dist/cli/reconcile.d.ts.map +1 -1
- package/dist/cli/reconcile.js +1 -0
- package/dist/collections/MetricBalances/access/index.d.ts +4 -0
- package/dist/collections/MetricBalances/access/index.d.ts.map +1 -0
- package/dist/collections/MetricBalances/access/index.js +9 -0
- package/dist/collections/MetricBalances/hooks/index.d.ts +3 -0
- package/dist/collections/MetricBalances/hooks/index.d.ts.map +1 -0
- package/dist/collections/MetricBalances/hooks/index.js +1 -0
- package/dist/collections/MetricBalances/index.d.ts +3 -0
- package/dist/collections/MetricBalances/index.d.ts.map +1 -0
- package/dist/collections/MetricBalances/index.js +48 -0
- package/dist/collections/Metrics/index.d.ts.map +1 -1
- package/dist/collections/Metrics/index.js +64 -3
- package/dist/collections/fields/ruleFields.d.ts.map +1 -1
- package/dist/collections/fields/ruleFields.js +45 -2
- package/dist/collections/hooks/preventCatalogDelete.d.ts.map +1 -1
- package/dist/collections/hooks/preventCatalogDelete.js +10 -1
- 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 +4 -0
- package/dist/defaults.d.ts.map +1 -1
- package/dist/defaults.js +10 -0
- package/dist/endpoints/index.d.ts.map +1 -1
- package/dist/endpoints/index.js +4 -0
- package/dist/endpoints/leaderboard.d.ts +6 -0
- package/dist/endpoints/leaderboard.d.ts.map +1 -0
- package/dist/endpoints/leaderboard.js +59 -0
- package/dist/extensions/elapsedSinceOptions.d.ts +7 -0
- package/dist/extensions/elapsedSinceOptions.d.ts.map +1 -0
- package/dist/extensions/elapsedSinceOptions.js +16 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +9 -0
- package/dist/seed/index.d.ts +10 -0
- package/dist/seed/index.d.ts.map +1 -1
- package/dist/seed/index.js +14 -2
- package/dist/services/achievement/builtInRules.d.ts.map +1 -1
- package/dist/services/achievement/builtInRules.js +59 -21
- package/dist/services/achievement/duration.d.ts +6 -0
- package/dist/services/achievement/duration.d.ts.map +1 -0
- package/dist/services/achievement/duration.js +43 -0
- package/dist/services/achievement/getMetricLeaderboard.d.ts +30 -0
- package/dist/services/achievement/getMetricLeaderboard.d.ts.map +1 -0
- package/dist/services/achievement/getMetricLeaderboard.js +62 -0
- package/dist/services/achievement/index.d.ts +5 -0
- package/dist/services/achievement/index.d.ts.map +1 -1
- package/dist/services/achievement/index.js +4 -0
- package/dist/services/achievement/metricBalances.d.ts +42 -0
- package/dist/services/achievement/metricBalances.d.ts.map +1 -0
- package/dist/services/achievement/metricBalances.js +137 -0
- package/dist/services/achievement/reconcile.d.ts +1 -0
- package/dist/services/achievement/reconcile.d.ts.map +1 -1
- package/dist/services/achievement/reconcile.js +10 -0
- package/dist/services/achievement/recordEvent.d.ts.map +1 -1
- package/dist/services/achievement/recordEvent.js +22 -1
- package/dist/services/achievement/resolveElapsedAnchor.d.ts +15 -0
- package/dist/services/achievement/resolveElapsedAnchor.d.ts.map +1 -0
- package/dist/services/achievement/resolveElapsedAnchor.js +98 -0
- package/dist/services/achievement/resolveMetricValue.d.ts +32 -0
- package/dist/services/achievement/resolveMetricValue.d.ts.map +1 -0
- package/dist/services/achievement/resolveMetricValue.js +73 -0
- package/dist/services/achievement/validateRules.d.ts.map +1 -1
- package/dist/services/achievement/validateRules.js +19 -0
- package/dist/types.d.ts +26 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { APIError } from 'payload';
|
|
2
|
+
import { collectionOf } from '../../collections/helpers.js';
|
|
3
|
+
import { getAchievementOptions } from '../../options-store.js';
|
|
4
|
+
import { loadMetricDoc } from './resolveMetricValue.js';
|
|
5
|
+
import { relationId } from './relationId.js';
|
|
6
|
+
/**
|
|
7
|
+
* Ranked stored-metric balances for a scope (or unscoped board when `scopeId` is null).
|
|
8
|
+
*/
|
|
9
|
+
export async function getMetricLeaderboard(args) {
|
|
10
|
+
const metric = await loadMetricDoc({
|
|
11
|
+
payload: args.payload,
|
|
12
|
+
req: args.req,
|
|
13
|
+
metric: args.metric,
|
|
14
|
+
});
|
|
15
|
+
if (!metric) {
|
|
16
|
+
throw new APIError('Metric not found.', 404);
|
|
17
|
+
}
|
|
18
|
+
if ((metric.kind ?? 'stored') === 'computed') {
|
|
19
|
+
throw new APIError('Leaderboards are only available for stored metrics.', 400);
|
|
20
|
+
}
|
|
21
|
+
const limit = Math.min(Math.max(args.limit ?? 20, 1), 100);
|
|
22
|
+
const page = Math.max(args.page ?? 1, 1);
|
|
23
|
+
const scopeId = args.scopeId ?? null;
|
|
24
|
+
const scopeConfigured = Boolean(getAchievementOptions().scope?.collection);
|
|
25
|
+
const clauses = [{ metric: { equals: metric.id } }];
|
|
26
|
+
if (scopeId) {
|
|
27
|
+
clauses.push({ scope: { equals: scopeId } });
|
|
28
|
+
}
|
|
29
|
+
else if (scopeConfigured) {
|
|
30
|
+
clauses.push({
|
|
31
|
+
or: [{ scope: { exists: false } }, { scope: { equals: null } }],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const result = await args.payload.find({
|
|
35
|
+
collection: collectionOf('metricBalances'),
|
|
36
|
+
depth: 0,
|
|
37
|
+
limit,
|
|
38
|
+
page,
|
|
39
|
+
overrideAccess: true,
|
|
40
|
+
req: args.req,
|
|
41
|
+
sort: '-value,id',
|
|
42
|
+
where: { and: clauses },
|
|
43
|
+
});
|
|
44
|
+
const offset = (page - 1) * limit;
|
|
45
|
+
const docs = result.docs.map((doc, index) => {
|
|
46
|
+
const row = doc;
|
|
47
|
+
return {
|
|
48
|
+
user: relationId(row.user) ?? String(row.user ?? ''),
|
|
49
|
+
value: typeof row.value === 'number' && Number.isFinite(row.value) ? row.value : 0,
|
|
50
|
+
rank: offset + index + 1,
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
docs,
|
|
55
|
+
totalDocs: result.totalDocs,
|
|
56
|
+
page: result.page ?? page,
|
|
57
|
+
limit,
|
|
58
|
+
totalPages: result.totalPages ?? Math.max(1, Math.ceil(result.totalDocs / limit)),
|
|
59
|
+
metric: { id: metric.id, slug: metric.slug },
|
|
60
|
+
scopeId,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -3,6 +3,11 @@ export type { RuleGroup, AchievementCompleteRef } from './evaluateRules.js';
|
|
|
3
3
|
export { buildCatalogProgressGroups } from './buildCatalogProgressGroups.js';
|
|
4
4
|
export type { CatalogProgressGroup, CatalogProgressItem } from './buildCatalogProgressGroups.js';
|
|
5
5
|
export { getUserProgress } from './getUserProgress.js';
|
|
6
|
+
export { getMetricLeaderboard } from './getMetricLeaderboard.js';
|
|
7
|
+
export type { MetricLeaderboardResult, MetricLeaderboardRow } from './getMetricLeaderboard.js';
|
|
8
|
+
export { applyMetricBalanceDelta, findMetricBalance, metricBalanceKey, rebuildMetricBalance, rebuildMetricBalancesForUser, } from './metricBalances.js';
|
|
9
|
+
export { resolveMetricValue, loadMetricDoc } from './resolveMetricValue.js';
|
|
10
|
+
export { resolveElapsedAnchor } from './resolveElapsedAnchor.js';
|
|
6
11
|
export { recordEvent, recordLog, recordMetricChange, syncTierChangedLog } from './recordEvent.js';
|
|
7
12
|
export { resolveCurrentTier } from './resolveCurrentTier.js';
|
|
8
13
|
export type { ResolvedTier } from './resolveCurrentTier.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,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,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,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,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,6 +1,10 @@
|
|
|
1
1
|
export { evaluateRules, evaluateRuleProgress, normalizeRuleGroup, collectAchievementCompleteRefs } from './evaluateRules.js';
|
|
2
2
|
export { buildCatalogProgressGroups } from './buildCatalogProgressGroups.js';
|
|
3
3
|
export { getUserProgress } from './getUserProgress.js';
|
|
4
|
+
export { getMetricLeaderboard } from './getMetricLeaderboard.js';
|
|
5
|
+
export { applyMetricBalanceDelta, findMetricBalance, metricBalanceKey, rebuildMetricBalance, rebuildMetricBalancesForUser, } from './metricBalances.js';
|
|
6
|
+
export { resolveMetricValue, loadMetricDoc } from './resolveMetricValue.js';
|
|
7
|
+
export { resolveElapsedAnchor } from './resolveElapsedAnchor.js';
|
|
4
8
|
export { recordEvent, recordLog, recordMetricChange, syncTierChangedLog } from './recordEvent.js';
|
|
5
9
|
export { resolveCurrentTier } from './resolveCurrentTier.js';
|
|
6
10
|
export { resolveTierProgress } from './resolveTierProgress.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Payload, PayloadRequest } from 'payload';
|
|
2
|
+
export declare function metricBalanceKey(userId: string, scopeId: string | null, metricId: string): string;
|
|
3
|
+
export declare function findMetricBalance(args: {
|
|
4
|
+
payload: Payload;
|
|
5
|
+
req?: PayloadRequest;
|
|
6
|
+
userId: string;
|
|
7
|
+
scopeId: string | null;
|
|
8
|
+
metricId: string;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
id: string;
|
|
11
|
+
value: number;
|
|
12
|
+
} | null>;
|
|
13
|
+
export declare function applyMetricBalanceDelta(args: {
|
|
14
|
+
payload: Payload;
|
|
15
|
+
req?: PayloadRequest;
|
|
16
|
+
userId: string;
|
|
17
|
+
scopeId: string | null;
|
|
18
|
+
metricId: string;
|
|
19
|
+
change: number;
|
|
20
|
+
}): Promise<{
|
|
21
|
+
id: string;
|
|
22
|
+
value: number;
|
|
23
|
+
}>;
|
|
24
|
+
/** Sum metric.delta log changes and upsert the balance projection. */
|
|
25
|
+
export declare function rebuildMetricBalance(args: {
|
|
26
|
+
payload: Payload;
|
|
27
|
+
req?: PayloadRequest;
|
|
28
|
+
userId: string;
|
|
29
|
+
scopeId?: string | null;
|
|
30
|
+
metricId: string;
|
|
31
|
+
}): Promise<{
|
|
32
|
+
id: string;
|
|
33
|
+
value: number;
|
|
34
|
+
}>;
|
|
35
|
+
/** Rebuild balances for every distinct metric seen in this user’s metric.delta logs. */
|
|
36
|
+
export declare function rebuildMetricBalancesForUser(args: {
|
|
37
|
+
payload: Payload;
|
|
38
|
+
req?: PayloadRequest;
|
|
39
|
+
userId: string;
|
|
40
|
+
scopeId?: string | null;
|
|
41
|
+
}): Promise<number>;
|
|
42
|
+
//# sourceMappingURL=metricBalances.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metricBalances.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/metricBalances.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAMtD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEjG;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;CACjB,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAehD;AAED,wBAAsB,uBAAuB,CAAC,IAAI,EAAE;IAClD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAgCzC;AAED,sEAAsE;AACtE,wBAAsB,oBAAoB,CAAC,IAAI,EAAE;IAC/C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAA;CACjB,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAoDzC;AAED,wFAAwF;AACxF,wBAAsB,4BAA4B,CAAC,IAAI,EAAE;IACvD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgClB"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { collectionOf } from '../../collections/helpers.js';
|
|
2
|
+
import { relationId } from './relationId.js';
|
|
3
|
+
import { userScopeWhere } from './where.js';
|
|
4
|
+
export function metricBalanceKey(userId, scopeId, metricId) {
|
|
5
|
+
return `${userId}:${scopeId ?? ''}:${metricId}`;
|
|
6
|
+
}
|
|
7
|
+
export async function findMetricBalance(args) {
|
|
8
|
+
const key = metricBalanceKey(args.userId, args.scopeId, args.metricId);
|
|
9
|
+
const found = await args.payload.find({
|
|
10
|
+
collection: collectionOf('metricBalances'),
|
|
11
|
+
depth: 0,
|
|
12
|
+
limit: 1,
|
|
13
|
+
overrideAccess: true,
|
|
14
|
+
req: args.req,
|
|
15
|
+
where: { key: { equals: key } },
|
|
16
|
+
});
|
|
17
|
+
const doc = found.docs[0];
|
|
18
|
+
const id = relationId(doc);
|
|
19
|
+
if (!id)
|
|
20
|
+
return null;
|
|
21
|
+
const value = typeof doc?.value === 'number' && Number.isFinite(doc.value) ? doc.value : 0;
|
|
22
|
+
return { id, value };
|
|
23
|
+
}
|
|
24
|
+
export async function applyMetricBalanceDelta(args) {
|
|
25
|
+
const existing = await findMetricBalance(args);
|
|
26
|
+
const nextValue = (existing?.value ?? 0) + args.change;
|
|
27
|
+
const key = metricBalanceKey(args.userId, args.scopeId, args.metricId);
|
|
28
|
+
if (existing) {
|
|
29
|
+
const updated = await args.payload.update({
|
|
30
|
+
collection: collectionOf('metricBalances'),
|
|
31
|
+
id: existing.id,
|
|
32
|
+
data: { value: nextValue },
|
|
33
|
+
overrideAccess: true,
|
|
34
|
+
req: args.req,
|
|
35
|
+
});
|
|
36
|
+
return { id: existing.id, value: typeof updated.value === 'number' ? updated.value : nextValue };
|
|
37
|
+
}
|
|
38
|
+
const created = await args.payload.create({
|
|
39
|
+
collection: collectionOf('metricBalances'),
|
|
40
|
+
data: {
|
|
41
|
+
user: args.userId,
|
|
42
|
+
scope: args.scopeId ?? undefined,
|
|
43
|
+
metric: args.metricId,
|
|
44
|
+
value: nextValue,
|
|
45
|
+
key,
|
|
46
|
+
},
|
|
47
|
+
overrideAccess: true,
|
|
48
|
+
req: args.req,
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
id: String(created.id),
|
|
52
|
+
value: typeof created.value === 'number' ? created.value : nextValue,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Sum metric.delta log changes and upsert the balance projection. */
|
|
56
|
+
export async function rebuildMetricBalance(args) {
|
|
57
|
+
const scopeId = args.scopeId ?? null;
|
|
58
|
+
const logs = await args.payload.find({
|
|
59
|
+
collection: collectionOf('logs'),
|
|
60
|
+
depth: 0,
|
|
61
|
+
limit: 10_000,
|
|
62
|
+
pagination: false,
|
|
63
|
+
overrideAccess: true,
|
|
64
|
+
req: args.req,
|
|
65
|
+
where: {
|
|
66
|
+
and: [userScopeWhere(args.userId, scopeId), { metric: { equals: args.metricId } }],
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const value = logs.docs.reduce((sum, doc) => {
|
|
70
|
+
const change = doc.change;
|
|
71
|
+
return sum + (typeof change === 'number' && Number.isFinite(change) ? change : 0);
|
|
72
|
+
}, 0);
|
|
73
|
+
const existing = await findMetricBalance({
|
|
74
|
+
payload: args.payload,
|
|
75
|
+
req: args.req,
|
|
76
|
+
userId: args.userId,
|
|
77
|
+
scopeId,
|
|
78
|
+
metricId: args.metricId,
|
|
79
|
+
});
|
|
80
|
+
const key = metricBalanceKey(args.userId, scopeId, args.metricId);
|
|
81
|
+
if (existing) {
|
|
82
|
+
await args.payload.update({
|
|
83
|
+
collection: collectionOf('metricBalances'),
|
|
84
|
+
id: existing.id,
|
|
85
|
+
data: { value },
|
|
86
|
+
overrideAccess: true,
|
|
87
|
+
req: args.req,
|
|
88
|
+
});
|
|
89
|
+
return { id: existing.id, value };
|
|
90
|
+
}
|
|
91
|
+
const created = await args.payload.create({
|
|
92
|
+
collection: collectionOf('metricBalances'),
|
|
93
|
+
data: {
|
|
94
|
+
user: args.userId,
|
|
95
|
+
scope: scopeId ?? undefined,
|
|
96
|
+
metric: args.metricId,
|
|
97
|
+
value,
|
|
98
|
+
key,
|
|
99
|
+
},
|
|
100
|
+
overrideAccess: true,
|
|
101
|
+
req: args.req,
|
|
102
|
+
});
|
|
103
|
+
return { id: String(created.id), value };
|
|
104
|
+
}
|
|
105
|
+
/** Rebuild balances for every distinct metric seen in this user’s metric.delta logs. */
|
|
106
|
+
export async function rebuildMetricBalancesForUser(args) {
|
|
107
|
+
const scopeId = args.scopeId ?? null;
|
|
108
|
+
const logs = await args.payload.find({
|
|
109
|
+
collection: collectionOf('logs'),
|
|
110
|
+
depth: 0,
|
|
111
|
+
limit: 10_000,
|
|
112
|
+
pagination: false,
|
|
113
|
+
overrideAccess: true,
|
|
114
|
+
req: args.req,
|
|
115
|
+
where: {
|
|
116
|
+
and: [userScopeWhere(args.userId, scopeId), { metric: { exists: true } }],
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
const metricIds = new Set();
|
|
120
|
+
for (const doc of logs.docs) {
|
|
121
|
+
const id = relationId(doc.metric);
|
|
122
|
+
if (id)
|
|
123
|
+
metricIds.add(id);
|
|
124
|
+
}
|
|
125
|
+
let rebuilt = 0;
|
|
126
|
+
for (const metricId of metricIds) {
|
|
127
|
+
await rebuildMetricBalance({
|
|
128
|
+
payload: args.payload,
|
|
129
|
+
req: args.req,
|
|
130
|
+
userId: args.userId,
|
|
131
|
+
scopeId,
|
|
132
|
+
metricId,
|
|
133
|
+
});
|
|
134
|
+
rebuilt += 1;
|
|
135
|
+
}
|
|
136
|
+
return rebuilt;
|
|
137
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAS,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAS,MAAM,SAAS,CAAA;AAc7D,MAAM,MAAM,0BAA0B,GAAG;IACvC,YAAY,EAAE,MAAM,CAAA;IACpB,qBAAqB,EAAE,MAAM,CAAA;IAC7B,iBAAiB,EAAE,MAAM,CAAA;IACzB,0BAA0B,EAAE,MAAM,CAAA;IAClC,mBAAmB,EAAE,MAAM,CAAA;IAC3B,eAAe,EAAE,MAAM,CAAA;IACvB,qBAAqB,EAAE,MAAM,CAAA;CAC9B,CAAA;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAwFD,wFAAwF;AACxF,wBAAsB,wBAAwB,CAAC,IAAI,EAAE;IACnD,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB,GAAG,OAAO,CAAC,IAAI,CAAC,0BAA0B,EAAE,cAAc,CAAC,CAAC,CA2C5D;AA2LD;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,wBAAwB,GAC7B,OAAO,CAAC,0BAA0B,CAAC,CAgDrC"}
|
|
@@ -2,6 +2,7 @@ import { collectionOf } from '../../collections/helpers.js';
|
|
|
2
2
|
import { syncCompositeProgression, } from './grantSideEffects.js';
|
|
3
3
|
import { transitionLogData, transitionToId } from './logData.js';
|
|
4
4
|
import { recordLog, syncTierChangedLog } from './recordEvent.js';
|
|
5
|
+
import { rebuildMetricBalancesForUser } from './metricBalances.js';
|
|
5
6
|
import { relationId } from './relationId.js';
|
|
6
7
|
import { resolveCatalogId } from './resolveCatalog.js';
|
|
7
8
|
import { syncTierProgression } from './tierProgression.js';
|
|
@@ -88,6 +89,12 @@ export async function reconcileUserProgression(args) {
|
|
|
88
89
|
if (!req.context)
|
|
89
90
|
req.context = {};
|
|
90
91
|
const grantedLogsBackfilled = await backfillGrantedLogs({ req, userId: args.userId, scopeId });
|
|
92
|
+
const metricBalancesRebuilt = await rebuildMetricBalancesForUser({
|
|
93
|
+
payload: args.payload,
|
|
94
|
+
req,
|
|
95
|
+
userId: args.userId,
|
|
96
|
+
scopeId,
|
|
97
|
+
});
|
|
91
98
|
const composites = await syncCompositeProgression({
|
|
92
99
|
req,
|
|
93
100
|
userId: args.userId,
|
|
@@ -106,6 +113,7 @@ export async function reconcileUserProgression(args) {
|
|
|
106
113
|
});
|
|
107
114
|
return {
|
|
108
115
|
grantedLogsBackfilled,
|
|
116
|
+
metricBalancesRebuilt,
|
|
109
117
|
compositesGranted: composites.granted,
|
|
110
118
|
achievementRequestsEnsured: composites.requested,
|
|
111
119
|
tierRequestsEnsured: tiers.requested,
|
|
@@ -276,6 +284,7 @@ export async function reconcileProgression(args) {
|
|
|
276
284
|
achievementRequestsEnsured: 0,
|
|
277
285
|
tierRequestsEnsured: 0,
|
|
278
286
|
tierLogsWritten: 0,
|
|
287
|
+
metricBalancesRebuilt: 0,
|
|
279
288
|
};
|
|
280
289
|
for (const pair of pairs) {
|
|
281
290
|
const result = await reconcileUserProgression({
|
|
@@ -290,6 +299,7 @@ export async function reconcileProgression(args) {
|
|
|
290
299
|
totals.achievementRequestsEnsured += result.achievementRequestsEnsured;
|
|
291
300
|
totals.tierRequestsEnsured += result.tierRequestsEnsured;
|
|
292
301
|
totals.tierLogsWritten += result.tierLogsWritten;
|
|
302
|
+
totals.metricBalancesRebuilt += result.metricBalancesRebuilt;
|
|
293
303
|
}
|
|
294
304
|
return totals;
|
|
295
305
|
}
|
|
@@ -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;
|
|
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"}
|
|
@@ -2,7 +2,9 @@ import { APIError } from 'payload';
|
|
|
2
2
|
import { DEFAULT_ACHIEVEMENT_METRIC_SLUG } from '../../catalog.js';
|
|
3
3
|
import { collectionOf } from '../../collections/helpers.js';
|
|
4
4
|
import { eventTypeRequiresActor } from './eventTypeRequiresActor.js';
|
|
5
|
+
import { applyMetricBalanceDelta } from './metricBalances.js';
|
|
5
6
|
import { transitionLogData, transitionToId } from './logData.js';
|
|
7
|
+
import { loadMetricDoc } from './resolveMetricValue.js';
|
|
6
8
|
import { resolveCatalogId } from './resolveCatalog.js';
|
|
7
9
|
import { resolveCurrentTier } from './resolveCurrentTier.js';
|
|
8
10
|
import { userScopeWhere } from './where.js';
|
|
@@ -60,6 +62,17 @@ export const recordEvent = recordLog;
|
|
|
60
62
|
export async function recordMetricChange(args) {
|
|
61
63
|
const metricSlug = args.metric ?? DEFAULT_ACHIEVEMENT_METRIC_SLUG;
|
|
62
64
|
const scopeId = args.scopeId ?? null;
|
|
65
|
+
const metricDoc = await loadMetricDoc({
|
|
66
|
+
payload: args.payload,
|
|
67
|
+
req: args.req,
|
|
68
|
+
metric: metricSlug,
|
|
69
|
+
});
|
|
70
|
+
if (!metricDoc) {
|
|
71
|
+
throw new APIError(`Unknown metric "${metricSlug}".`, 400);
|
|
72
|
+
}
|
|
73
|
+
if ((metricDoc.kind ?? 'stored') === 'computed') {
|
|
74
|
+
throw new APIError('Computed metrics cannot be changed with recordMetricChange.', 400);
|
|
75
|
+
}
|
|
63
76
|
const log = await recordLog({
|
|
64
77
|
payload: args.payload,
|
|
65
78
|
req: args.req,
|
|
@@ -67,10 +80,18 @@ export async function recordMetricChange(args) {
|
|
|
67
80
|
scopeId,
|
|
68
81
|
type: 'metric.delta',
|
|
69
82
|
actorId: args.actorId,
|
|
70
|
-
metric:
|
|
83
|
+
metric: metricDoc.id,
|
|
71
84
|
change: args.change,
|
|
72
85
|
reason: args.reason,
|
|
73
86
|
});
|
|
87
|
+
await applyMetricBalanceDelta({
|
|
88
|
+
payload: args.payload,
|
|
89
|
+
req: args.req,
|
|
90
|
+
userId: args.userId,
|
|
91
|
+
scopeId,
|
|
92
|
+
metricId: metricDoc.id,
|
|
93
|
+
change: args.change,
|
|
94
|
+
});
|
|
74
95
|
if (args.req) {
|
|
75
96
|
const { syncTierProgression } = await import('./tierProgression.js');
|
|
76
97
|
await syncTierProgression({
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Payload, PayloadRequest } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the start timestamp for elapsed rules / computed metrics.
|
|
4
|
+
* Built-ins: `user-created-at`, `first-event`. Host keys: `extensions.metricAnchors`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveElapsedAnchor(args: {
|
|
7
|
+
payload: Payload;
|
|
8
|
+
req?: PayloadRequest;
|
|
9
|
+
since?: unknown;
|
|
10
|
+
eventType?: unknown;
|
|
11
|
+
eventTypeSlug?: unknown;
|
|
12
|
+
userId: string;
|
|
13
|
+
scopeId: string | null;
|
|
14
|
+
}): Promise<Date | null>;
|
|
15
|
+
//# sourceMappingURL=resolveElapsedAnchor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveElapsedAnchor.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/resolveElapsedAnchor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AA8CtD;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE;IAC/C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CA0DvB"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { collectionOf } from '../../collections/helpers.js';
|
|
2
|
+
import { getAchievementOptions } from '../../options-store.js';
|
|
3
|
+
import { parseDate } from './duration.js';
|
|
4
|
+
import { relationId } from './relationId.js';
|
|
5
|
+
import { userScopeIncludingUnscopedWhere } from './where.js';
|
|
6
|
+
async function resolveEventTypeId(args) {
|
|
7
|
+
let eventTypeId = relationId(args.eventType);
|
|
8
|
+
if (eventTypeId) {
|
|
9
|
+
const byId = await args.payload.find({
|
|
10
|
+
collection: collectionOf('eventTypes'),
|
|
11
|
+
depth: 0,
|
|
12
|
+
limit: 1,
|
|
13
|
+
overrideAccess: true,
|
|
14
|
+
req: args.req,
|
|
15
|
+
where: { id: { equals: eventTypeId } },
|
|
16
|
+
});
|
|
17
|
+
if (byId.docs[0])
|
|
18
|
+
return eventTypeId;
|
|
19
|
+
}
|
|
20
|
+
const slug = typeof args.eventTypeSlug === 'string'
|
|
21
|
+
? args.eventTypeSlug
|
|
22
|
+
: typeof args.eventType === 'string'
|
|
23
|
+
? args.eventType
|
|
24
|
+
: '';
|
|
25
|
+
if (!slug)
|
|
26
|
+
return null;
|
|
27
|
+
const found = await args.payload.find({
|
|
28
|
+
collection: collectionOf('eventTypes'),
|
|
29
|
+
depth: 0,
|
|
30
|
+
limit: 1,
|
|
31
|
+
overrideAccess: true,
|
|
32
|
+
req: args.req,
|
|
33
|
+
where: { slug: { equals: slug } },
|
|
34
|
+
});
|
|
35
|
+
return relationId(found.docs[0]);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the start timestamp for elapsed rules / computed metrics.
|
|
39
|
+
* Built-ins: `user-created-at`, `first-event`. Host keys: `extensions.metricAnchors`.
|
|
40
|
+
*/
|
|
41
|
+
export async function resolveElapsedAnchor(args) {
|
|
42
|
+
const since = String(args.since ?? 'user-created-at');
|
|
43
|
+
if (since === 'user-created-at') {
|
|
44
|
+
const usersSlug = getAchievementOptions().usersCollectionSlug ?? 'users';
|
|
45
|
+
try {
|
|
46
|
+
const user = (await args.payload.findByID({
|
|
47
|
+
collection: usersSlug,
|
|
48
|
+
id: args.userId,
|
|
49
|
+
depth: 0,
|
|
50
|
+
overrideAccess: true,
|
|
51
|
+
req: args.req,
|
|
52
|
+
}));
|
|
53
|
+
return parseDate(user?.createdAt);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (since === 'first-event') {
|
|
60
|
+
const eventTypeId = await resolveEventTypeId({
|
|
61
|
+
payload: args.payload,
|
|
62
|
+
req: args.req,
|
|
63
|
+
eventType: args.eventType,
|
|
64
|
+
eventTypeSlug: args.eventTypeSlug,
|
|
65
|
+
});
|
|
66
|
+
if (!eventTypeId)
|
|
67
|
+
return null;
|
|
68
|
+
const logs = await args.payload.find({
|
|
69
|
+
collection: collectionOf('logs'),
|
|
70
|
+
depth: 0,
|
|
71
|
+
limit: 1,
|
|
72
|
+
overrideAccess: true,
|
|
73
|
+
req: args.req,
|
|
74
|
+
sort: 'createdAt',
|
|
75
|
+
where: {
|
|
76
|
+
and: [
|
|
77
|
+
userScopeIncludingUnscopedWhere(args.userId, args.scopeId),
|
|
78
|
+
{ type: { equals: eventTypeId } },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
return parseDate(logs.docs[0]?.createdAt);
|
|
83
|
+
}
|
|
84
|
+
const hostAnchor = getAchievementOptions().extensions?.metricAnchors?.[since];
|
|
85
|
+
if (!hostAnchor)
|
|
86
|
+
return null;
|
|
87
|
+
try {
|
|
88
|
+
return parseDate(await hostAnchor({
|
|
89
|
+
payload: args.payload,
|
|
90
|
+
req: args.req,
|
|
91
|
+
userId: args.userId,
|
|
92
|
+
scopeId: args.scopeId,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Payload, PayloadRequest } from 'payload';
|
|
2
|
+
export type MetricDoc = {
|
|
3
|
+
id?: unknown;
|
|
4
|
+
slug?: string;
|
|
5
|
+
kind?: string | null;
|
|
6
|
+
compute?: string | null;
|
|
7
|
+
unit?: string | null;
|
|
8
|
+
since?: string | null;
|
|
9
|
+
eventType?: unknown;
|
|
10
|
+
};
|
|
11
|
+
export declare function loadMetricDoc(args: {
|
|
12
|
+
payload: Payload;
|
|
13
|
+
req?: PayloadRequest;
|
|
14
|
+
metric: string;
|
|
15
|
+
}): Promise<(MetricDoc & {
|
|
16
|
+
id: string;
|
|
17
|
+
}) | null>;
|
|
18
|
+
/**
|
|
19
|
+
* Current numeric value for a metric (stored balance or computed elapsed).
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveMetricValue(args: {
|
|
22
|
+
payload: Payload;
|
|
23
|
+
req?: PayloadRequest;
|
|
24
|
+
/** Metric document id or slug, or a loaded metric doc. */
|
|
25
|
+
metric: string | (MetricDoc & {
|
|
26
|
+
id: string;
|
|
27
|
+
});
|
|
28
|
+
userId: string;
|
|
29
|
+
scopeId: string | null;
|
|
30
|
+
now?: Date;
|
|
31
|
+
}): Promise<number>;
|
|
32
|
+
//# sourceMappingURL=resolveMetricValue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveMetricValue.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/resolveMetricValue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAQtD,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,CAAC,EAAE,OAAO,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC,CAAC,SAAS,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,CAAC,CA+B/C;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,GAAG,CAAC,SAAS,GAAG;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC7C,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,GAAG,CAAC,EAAE,IAAI,CAAA;CACX,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BlB"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { collectionOf } from '../../collections/helpers.js';
|
|
2
|
+
import { elapsedUnits } from './duration.js';
|
|
3
|
+
import { findMetricBalance } from './metricBalances.js';
|
|
4
|
+
import { relationId } from './relationId.js';
|
|
5
|
+
import { resolveElapsedAnchor } from './resolveElapsedAnchor.js';
|
|
6
|
+
export async function loadMetricDoc(args) {
|
|
7
|
+
const byId = relationId(args.metric);
|
|
8
|
+
if (byId && args.metric === byId) {
|
|
9
|
+
try {
|
|
10
|
+
const doc = (await args.payload.findByID({
|
|
11
|
+
collection: collectionOf('metrics'),
|
|
12
|
+
id: byId,
|
|
13
|
+
depth: 0,
|
|
14
|
+
overrideAccess: true,
|
|
15
|
+
req: args.req,
|
|
16
|
+
}));
|
|
17
|
+
if (doc)
|
|
18
|
+
return { ...doc, id: byId };
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// fall through to slug lookup
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const found = await args.payload.find({
|
|
25
|
+
collection: collectionOf('metrics'),
|
|
26
|
+
depth: 0,
|
|
27
|
+
limit: 1,
|
|
28
|
+
overrideAccess: true,
|
|
29
|
+
req: args.req,
|
|
30
|
+
where: {
|
|
31
|
+
or: [{ id: { equals: args.metric } }, { slug: { equals: args.metric } }],
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const doc = found.docs[0];
|
|
35
|
+
const id = relationId(doc);
|
|
36
|
+
if (!doc || !id)
|
|
37
|
+
return null;
|
|
38
|
+
return { ...doc, id };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Current numeric value for a metric (stored balance or computed elapsed).
|
|
42
|
+
*/
|
|
43
|
+
export async function resolveMetricValue(args) {
|
|
44
|
+
const doc = typeof args.metric === 'string'
|
|
45
|
+
? await loadMetricDoc({ payload: args.payload, req: args.req, metric: args.metric })
|
|
46
|
+
: args.metric;
|
|
47
|
+
if (!doc)
|
|
48
|
+
return 0;
|
|
49
|
+
const kind = doc.kind ?? 'stored';
|
|
50
|
+
if (kind === 'computed') {
|
|
51
|
+
if ((doc.compute ?? 'elapsed') !== 'elapsed')
|
|
52
|
+
return 0;
|
|
53
|
+
const anchor = await resolveElapsedAnchor({
|
|
54
|
+
payload: args.payload,
|
|
55
|
+
req: args.req,
|
|
56
|
+
since: doc.since,
|
|
57
|
+
eventType: doc.eventType,
|
|
58
|
+
userId: args.userId,
|
|
59
|
+
scopeId: args.scopeId,
|
|
60
|
+
});
|
|
61
|
+
if (!anchor)
|
|
62
|
+
return 0;
|
|
63
|
+
return elapsedUnits(anchor, doc.unit ?? 'days', args.now ?? new Date()) ?? 0;
|
|
64
|
+
}
|
|
65
|
+
const balance = await findMetricBalance({
|
|
66
|
+
payload: args.payload,
|
|
67
|
+
req: args.req,
|
|
68
|
+
userId: args.userId,
|
|
69
|
+
scopeId: args.scopeId,
|
|
70
|
+
metricId: doc.id,
|
|
71
|
+
});
|
|
72
|
+
return balance?.value ?? 0;
|
|
73
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validateRules.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/validateRules.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEvE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"validateRules.d.ts","sourceRoot":"","sources":["../../../src/services/achievement/validateRules.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEvE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,IAAI,CA0E7E;AAED,sFAAsF;AACtF,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,GAAG,SAAS,GAC9D,MAAM,GAAG,IAAI,CAOf;AAED,+EAA+E;AAC/E,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,GAAG,SAAS,EAC/D,KAAK,EAAE,MAAM,GACZ,IAAI,CAKN"}
|
|
@@ -49,6 +49,25 @@ export function validateRuleNode(rule) {
|
|
|
49
49
|
}
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
|
+
if (type === 'elapsed-since') {
|
|
53
|
+
const since = String(rule.since ?? 'user-created-at');
|
|
54
|
+
if (since === 'first-event') {
|
|
55
|
+
if (!relationId(rule.eventType) && typeof rule.eventTypeSlug !== 'string') {
|
|
56
|
+
return 'Elapsed since (first event) needs an event type.';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (since !== 'user-created-at') {
|
|
60
|
+
return 'Elapsed since needs a valid since source.';
|
|
61
|
+
}
|
|
62
|
+
if (typeof rule.amount !== 'number' || Number.isNaN(rule.amount) || rule.amount < 0) {
|
|
63
|
+
return 'Elapsed since needs a non-negative amount.';
|
|
64
|
+
}
|
|
65
|
+
const unit = rule.unit ?? 'days';
|
|
66
|
+
if (unit !== 'days' && unit !== 'hours' && unit !== 'minutes') {
|
|
67
|
+
return 'Elapsed since unit must be days, hours, or minutes.';
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
52
71
|
if (!type) {
|
|
53
72
|
return 'Each rule needs a type.';
|
|
54
73
|
}
|