@headless-lms/core 0.1.0 → 0.2.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.
- package/dist/assets/events.d.ts +66 -3
- package/dist/assets/events.js +19 -3
- package/dist/assets/events.js.map +1 -1
- package/dist/assets/service.js +5 -3
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.d.ts +11 -3
- package/dist/automations/actions.js +38 -5
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.d.ts +4 -1
- package/dist/automations/catalog.js +289 -188
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/events.d.ts +46 -24
- package/dist/automations/events.js +5 -5
- package/dist/automations/events.js.map +1 -1
- package/dist/automations/index.d.ts +5 -5
- package/dist/automations/index.js +2 -2
- package/dist/automations/model.d.ts +13 -3
- package/dist/automations/model.js +23 -3
- package/dist/automations/model.js.map +1 -1
- package/dist/automations/ports.d.ts +23 -8
- package/dist/automations/service.d.ts +21 -5
- package/dist/automations/service.js +267 -47
- package/dist/automations/service.js.map +1 -1
- package/dist/automations/types.d.ts +2 -2
- package/dist/automations/webhook-signing.d.ts +7 -0
- package/dist/automations/webhook-signing.js +22 -0
- package/dist/automations/webhook-signing.js.map +1 -0
- package/dist/content/events.d.ts +401 -25
- package/dist/content/events.js +125 -19
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +5 -5
- package/dist/content/index.js +2 -2
- package/dist/content/model.d.ts +2 -2
- package/dist/content/ports.d.ts +42 -11
- package/dist/content/service.d.ts +15 -9
- package/dist/content/service.js +153 -45
- package/dist/content/service.js.map +1 -1
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/events.d.ts +60 -4
- package/dist/discussion/events.js +30 -4
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/model.d.ts +1 -2
- package/dist/discussion/ports.d.ts +1 -1
- package/dist/discussion/service.d.ts +1 -1
- package/dist/discussion/service.js +11 -10
- package/dist/discussion/service.js.map +1 -1
- package/dist/entitlements/events.d.ts +96 -12
- package/dist/entitlements/events.js +21 -4
- package/dist/entitlements/events.js.map +1 -1
- package/dist/entitlements/service.js +1 -0
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/events.d.ts +17 -3
- package/dist/identity/events.js +12 -2
- package/dist/identity/events.js.map +1 -1
- package/dist/identity/ports.d.ts +3 -0
- package/dist/identity/service.d.ts +3 -0
- package/dist/identity/service.js +12 -1
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/events.d.ts +27 -3
- package/dist/integrations/events.js +15 -3
- package/dist/integrations/events.js.map +1 -1
- package/dist/integrations/service.js +1 -1
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/events.d.ts +178 -11
- package/dist/organizations/events.js +52 -9
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +3 -3
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/ports.d.ts +6 -10
- package/dist/organizations/roles.js.map +1 -1
- package/dist/organizations/service.d.ts +9 -4
- package/dist/organizations/service.js +131 -72
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/types.d.ts +2 -2
- package/dist/progress/events.d.ts +34 -2
- package/dist/progress/events.js +15 -2
- package/dist/progress/events.js.map +1 -1
- package/dist/progress/service.d.ts +2 -2
- package/dist/progress/service.js +4 -2
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/courses/index.d.ts +4 -0
- package/dist/reporting/courses/index.js +2 -0
- package/dist/reporting/courses/model.d.ts +24 -0
- package/dist/reporting/courses/model.js +1 -0
- package/dist/reporting/courses/ports.d.ts +13 -0
- package/dist/reporting/courses/ports.js +1 -0
- package/dist/reporting/courses/service.d.ts +19 -0
- package/dist/reporting/courses/service.js +20 -0
- package/dist/reporting/courses/service.js.map +1 -0
- package/dist/reporting/learn/model.d.ts +2 -2
- package/dist/reporting/learn/ports.d.ts +2 -1
- package/dist/reporting/learn/service.d.ts +2 -1
- package/dist/reporting/learn/service.js +5 -0
- package/dist/reporting/learn/service.js.map +1 -1
- package/dist/reporting/students/index.d.ts +2 -2
- package/dist/reporting/students/model.d.ts +21 -1
- package/dist/reporting/students/ports.d.ts +3 -1
- package/dist/reporting/students/service.d.ts +2 -1
- package/dist/reporting/students/service.js +3 -0
- package/dist/reporting/students/service.js.map +1 -1
- package/dist/shared/events.d.ts +5 -3
- package/dist/shared/events.js +1 -0
- package/dist/shared/events.js.map +1 -1
- package/dist/shared/id.d.ts +1 -0
- package/dist/shared/id.js +1 -0
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/mailer.d.ts +2 -0
- package/dist/shared/mailer.js.map +1 -1
- package/dist/shared/settings.js.map +1 -1
- package/dist/types/automations.d.ts +13 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +3 -2
- package/dist/types/email-templates.d.ts +3 -1
- package/dist/types/events.d.ts +3 -3
- package/dist/types/index.d.ts +6 -6
- package/dist/types/organizations.d.ts +2 -2
- package/dist/types/ports.d.ts +1 -1
- package/dist/types/schemas/assets.d.ts +3 -0
- package/dist/types/schemas/assets.js +2 -1
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/automations.d.ts +71 -4
- package/dist/types/schemas/automations.js +35 -4
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +51 -18
- package/dist/types/schemas/content.js +40 -13
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/credentials.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +10 -2
- package/dist/types/schemas/discussion.js +12 -5
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +8 -2
- package/dist/types/schemas/entitlements.js +10 -4
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +4 -4
- package/dist/types/schemas/identity.js +5 -5
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +6 -6
- package/dist/types/schemas/index.js +6 -6
- package/dist/types/schemas/integrations.js.map +1 -1
- package/dist/types/schemas/organizations.d.ts +19 -5
- package/dist/types/schemas/organizations.js +12 -7
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/outbox.d.ts +1 -0
- package/dist/types/schemas/outbox.js +1 -0
- package/dist/types/schemas/outbox.js.map +1 -1
- package/dist/types/schemas/progress.d.ts +1 -0
- package/dist/types/schemas/progress.js +1 -0
- package/dist/types/schemas/progress.js.map +1 -1
- package/dist/types/schemas/settings.js.map +1 -1
- package/dist/types/schemas/shared.d.ts +2 -1
- package/dist/types/schemas/shared.js +3 -1
- package/dist/types/schemas/shared.js.map +1 -1
- package/dist/types/shared.d.ts +1 -1
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ declare const progressEvents: {
|
|
|
12
12
|
startedAt: Date;
|
|
13
13
|
position: JsonValue;
|
|
14
14
|
completedAt: Date | null;
|
|
15
|
+
createdAt: Date;
|
|
15
16
|
updatedAt: Date;
|
|
16
17
|
}, "progress.record.started", 1>, {
|
|
17
18
|
id: string;
|
|
@@ -22,8 +23,23 @@ declare const progressEvents: {
|
|
|
22
23
|
startedAt: unknown;
|
|
23
24
|
position: unknown;
|
|
24
25
|
completedAt: unknown;
|
|
26
|
+
createdAt: unknown;
|
|
25
27
|
updatedAt: unknown;
|
|
26
|
-
}
|
|
28
|
+
}, import("zod").ZodObject<{
|
|
29
|
+
orgId: import("zod").ZodString;
|
|
30
|
+
id: import("zod").ZodString;
|
|
31
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
32
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
33
|
+
startedAt: import("zod").ZodCoercedDate<unknown>;
|
|
34
|
+
orgUserId: import("zod").ZodString;
|
|
35
|
+
targetType: import("zod").ZodEnum<{
|
|
36
|
+
course: "course";
|
|
37
|
+
activity: "activity";
|
|
38
|
+
module: "module";
|
|
39
|
+
}>;
|
|
40
|
+
targetId: import("zod").ZodString;
|
|
41
|
+
completedAt: import("zod").ZodNullable<import("zod").ZodCoercedDate<unknown>>;
|
|
42
|
+
}, import("zod/v4/core").$strip>>;
|
|
27
43
|
progressCompleted: RuntimeEventDefinition<VersionedDomainEvent<{
|
|
28
44
|
id: string;
|
|
29
45
|
orgId: string;
|
|
@@ -33,6 +49,7 @@ declare const progressEvents: {
|
|
|
33
49
|
startedAt: Date;
|
|
34
50
|
position: JsonValue;
|
|
35
51
|
completedAt: Date | null;
|
|
52
|
+
createdAt: Date;
|
|
36
53
|
updatedAt: Date;
|
|
37
54
|
}, "progress.record.completed", 1>, {
|
|
38
55
|
id: string;
|
|
@@ -43,8 +60,23 @@ declare const progressEvents: {
|
|
|
43
60
|
startedAt: unknown;
|
|
44
61
|
position: unknown;
|
|
45
62
|
completedAt: unknown;
|
|
63
|
+
createdAt: unknown;
|
|
46
64
|
updatedAt: unknown;
|
|
47
|
-
}
|
|
65
|
+
}, import("zod").ZodObject<{
|
|
66
|
+
orgId: import("zod").ZodString;
|
|
67
|
+
id: import("zod").ZodString;
|
|
68
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
69
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
70
|
+
startedAt: import("zod").ZodCoercedDate<unknown>;
|
|
71
|
+
orgUserId: import("zod").ZodString;
|
|
72
|
+
targetType: import("zod").ZodEnum<{
|
|
73
|
+
course: "course";
|
|
74
|
+
activity: "activity";
|
|
75
|
+
module: "module";
|
|
76
|
+
}>;
|
|
77
|
+
targetId: import("zod").ZodString;
|
|
78
|
+
completedAt: import("zod").ZodNullable<import("zod").ZodCoercedDate<unknown>>;
|
|
79
|
+
}, import("zod/v4/core").$strip>>;
|
|
48
80
|
};
|
|
49
81
|
type ProgressStarted = EventOf<typeof progressEvents.progressStarted>;
|
|
50
82
|
type ProgressCompleted = EventOf<typeof progressEvents.progressCompleted>;
|
package/dist/progress/events.js
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
import { progressRecordSchema } from "../types/schemas/progress.js";
|
|
2
2
|
import { defineEvent } from "../shared/events.js";
|
|
3
3
|
//#region src/progress/events.ts
|
|
4
|
+
const publicProgressRecord = progressRecordSchema.pick({
|
|
5
|
+
id: true,
|
|
6
|
+
orgId: true,
|
|
7
|
+
orgUserId: true,
|
|
8
|
+
targetType: true,
|
|
9
|
+
targetId: true,
|
|
10
|
+
startedAt: true,
|
|
11
|
+
completedAt: true,
|
|
12
|
+
createdAt: true,
|
|
13
|
+
updatedAt: true
|
|
14
|
+
}).strip();
|
|
4
15
|
const progressEvents = {
|
|
5
16
|
progressStarted: defineEvent({
|
|
6
17
|
type: "progress.record.started",
|
|
7
18
|
version: 1,
|
|
8
|
-
data: progressRecordSchema
|
|
19
|
+
data: progressRecordSchema,
|
|
20
|
+
publicData: publicProgressRecord
|
|
9
21
|
}),
|
|
10
22
|
progressCompleted: defineEvent({
|
|
11
23
|
type: "progress.record.completed",
|
|
12
24
|
version: 1,
|
|
13
|
-
data: progressRecordSchema
|
|
25
|
+
data: progressRecordSchema,
|
|
26
|
+
publicData: publicProgressRecord
|
|
14
27
|
})
|
|
15
28
|
};
|
|
16
29
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","names":[],"sources":["../../src/progress/events.ts"],"sourcesContent":["import { progressRecordSchema } from '../types/schemas/index.js';\nimport {\n defineEvent,\n type EventOf,\n type EventOfValues,\n type NewDomainEvent,\n} from '../shared/ports.js';\n\nexport const progressEvents = {\n progressStarted: defineEvent({\n type: 'progress.record.started',\n version: 1,\n data: progressRecordSchema,\n }),\n progressCompleted: defineEvent({\n type: 'progress.record.completed',\n version: 1,\n data: progressRecordSchema,\n }),\n};\n\nexport type ProgressStarted = EventOf<typeof progressEvents.progressStarted>;\nexport type ProgressCompleted = EventOf<typeof progressEvents.progressCompleted>;\nexport type ProgressEvent = EventOfValues<typeof progressEvents>;\nexport type NewProgressEvent = NewDomainEvent<ProgressEvent>;\n"],"mappings":";;;AAQA,MAAa,iBAAiB;CAC5B,iBAAiB,YAAY;EAC3B,MAAM;EACN,SAAS;EACT,MAAM;
|
|
1
|
+
{"version":3,"file":"events.js","names":[],"sources":["../../src/progress/events.ts"],"sourcesContent":["import { progressRecordSchema } from '../types/schemas/index.js';\nimport {\n defineEvent,\n type EventOf,\n type EventOfValues,\n type NewDomainEvent,\n} from '../shared/ports.js';\n\nconst publicProgressRecord = progressRecordSchema\n .pick({\n id: true,\n orgId: true,\n orgUserId: true,\n targetType: true,\n targetId: true,\n startedAt: true,\n completedAt: true,\n createdAt: true,\n updatedAt: true,\n })\n .strip();\n\nexport const progressEvents = {\n progressStarted: defineEvent({\n type: 'progress.record.started',\n version: 1,\n data: progressRecordSchema,\n publicData: publicProgressRecord,\n }),\n progressCompleted: defineEvent({\n type: 'progress.record.completed',\n version: 1,\n data: progressRecordSchema,\n publicData: publicProgressRecord,\n }),\n};\n\nexport type ProgressStarted = EventOf<typeof progressEvents.progressStarted>;\nexport type ProgressCompleted = EventOf<typeof progressEvents.progressCompleted>;\nexport type ProgressEvent = EventOfValues<typeof progressEvents>;\nexport type NewProgressEvent = NewDomainEvent<ProgressEvent>;\n"],"mappings":";;;AAQA,MAAM,uBAAuB,qBAC1B,KAAK;CACJ,IAAI;CACJ,OAAO;CACP,WAAW;CACX,YAAY;CACZ,UAAU;CACV,WAAW;CACX,aAAa;CACb,WAAW;CACX,WAAW;AACb,CAAC,CAAC,CACD,MAAM;AAET,MAAa,iBAAiB;CAC5B,iBAAiB,YAAY;EAC3B,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;CACD,mBAAmB,YAAY;EAC7B,MAAM;EACN,SAAS;EACT,MAAM;EACN,YAAY;CACd,CAAC;AACH"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Logger } from "../types/ports.js";
|
|
2
2
|
import { ProgressRecord, ProgressTarget, ReportProgressInput } from "../types/schemas/progress.js";
|
|
3
3
|
import "../shared/ports.js";
|
|
4
|
-
import {
|
|
4
|
+
import { CourseManagementService } from "../content/ports.js";
|
|
5
5
|
import "../content/index.js";
|
|
6
6
|
import "./model.js";
|
|
7
7
|
import "./types.js";
|
|
@@ -9,7 +9,7 @@ import { ProgressRepository, ProgressService, ProgressUnitOfWork } from "./ports
|
|
|
9
9
|
//#region src/progress/service.d.ts
|
|
10
10
|
type ProgressServiceParams = {
|
|
11
11
|
repo: ProgressRepository;
|
|
12
|
-
content:
|
|
12
|
+
content: CourseManagementService;
|
|
13
13
|
uow: ProgressUnitOfWork;
|
|
14
14
|
logger?: Logger;
|
|
15
15
|
};
|
package/dist/progress/service.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { genId } from "../shared/id.js";
|
|
2
1
|
import { noopLogger } from "../shared/logger.js";
|
|
2
|
+
import { genId } from "../shared/id.js";
|
|
3
3
|
import { NotFoundError } from "../shared/errors.js";
|
|
4
4
|
import { progressEvents } from "./events.js";
|
|
5
5
|
//#region src/progress/service.ts
|
|
@@ -63,7 +63,7 @@ var ProgressServiceImpl = class {
|
|
|
63
63
|
const events = [];
|
|
64
64
|
let record = await this.ensureActivityRecord(orgId, input, scope, events);
|
|
65
65
|
const state = mergeReports(record.position, input.reports);
|
|
66
|
-
if (state.changed
|
|
66
|
+
if (state.changed) record = await scope.progress.update(orgId, record.id, { position: state.map }) ?? record;
|
|
67
67
|
if (!record.completedAt && completionSatisfied(activity.settings, state.claimed)) {
|
|
68
68
|
record = await scope.progress.update(orgId, record.id, { completedAt: /* @__PURE__ */ new Date() }) ?? record;
|
|
69
69
|
events.push(progressEvents.progressCompleted.make({
|
|
@@ -98,6 +98,7 @@ var ProgressServiceImpl = class {
|
|
|
98
98
|
startedAt: now,
|
|
99
99
|
position: null,
|
|
100
100
|
completedAt: null,
|
|
101
|
+
createdAt: now,
|
|
101
102
|
updatedAt: now
|
|
102
103
|
});
|
|
103
104
|
if (!record) {
|
|
@@ -149,6 +150,7 @@ var ProgressServiceImpl = class {
|
|
|
149
150
|
startedAt: now,
|
|
150
151
|
position: null,
|
|
151
152
|
completedAt: now,
|
|
153
|
+
createdAt: now,
|
|
152
154
|
updatedAt: now
|
|
153
155
|
});
|
|
154
156
|
if (inserted) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../src/progress/service.ts"],"sourcesContent":["// progress context — service implementation (inbound port).\n// The frontend only reports usage; this service makes every completion\n// decision. One report batch runs in one UoW transaction: ensure the record,\n// merge item payloads into the per-subject state map, evaluate the activity's\n// completion rule, then module/course against current published structure —\n// appending events with the writes. The container is resolved from the\n// hierarchy (activity → module → course), never supplied by the caller.\n// Percentage/resume are derived by readers, never stored here.\nimport { genId } from '../shared/id.js';\nimport { NotFoundError } from '../shared/errors.js';\nimport type { ProgressRecord } from './model.js';\nimport type {\n ProgressRepository,\n ProgressService,\n ProgressUnitOfWork,\n ProgressWriteScope,\n} from './ports.js';\nimport type { ProgressReportItem, ProgressTarget, ReportProgressInput } from './types.js';\nimport { progressEvents, type NewProgressEvent } from './events.js';\nimport type { Activity, ContentService, Module } from '../content/index.js';\nimport type { JsonValue } from '../types/index.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\n\n/** Mirrors reporting/learn: `settings.published === false` is the only draft signal. */\nfunction isActivityPublished(settings: unknown): boolean {\n return (settings as { published?: boolean } | null)?.published !== false;\n}\n\n/** Fold a report batch into the record's per-subject state map. Reserved keys\n * (`asset` = subject, `completed` = claim) are stripped; the rest is the asset\n * type's own vocabulary, stored opaquely — latest report per subject wins. */\nfunction mergeReports(\n position: unknown,\n items: ProgressReportItem[],\n): { map: Record<string, JsonValue>; changed: boolean; claimed: boolean } {\n const map = { ...((position as Record<string, JsonValue> | null) ?? {}) };\n let changed = false;\n let claimed = false;\n for (const item of items) {\n const { asset, completed, ...payload } = item;\n if (completed === true) {\n claimed = true;\n }\n if (Object.keys(payload).length > 0) {\n map[asset ?? 'self'] = payload;\n changed = true;\n }\n }\n return { map, changed, claimed };\n}\n\n/** The completion-rule seam. Only `manual` ships: no authored rule → the\n * learner's claim decides. Authored rules are never satisfied until their\n * evaluators exist. Future rules evaluate against the accumulated state map. */\nfunction completionSatisfied(settings: unknown, claimed: boolean): boolean {\n const rule = (settings as { completion?: { rule?: string } } | null)?.completion?.rule;\n if (!rule || rule === 'manual') {\n return claimed;\n }\n return false;\n}\n\nexport type ProgressServiceParams = {\n repo: ProgressRepository;\n content: ContentService;\n uow: ProgressUnitOfWork;\n logger?: Logger;\n};\n\nexport class ProgressServiceImpl implements ProgressService {\n private readonly repo: ProgressRepository;\n private readonly content: ContentService;\n private readonly uow: ProgressUnitOfWork;\n private readonly logger: Logger;\n\n constructor(params: ProgressServiceParams) {\n this.repo = params.repo;\n this.content = params.content;\n this.uow = params.uow;\n this.logger = params.logger ?? noopLogger;\n }\n\n async report(orgId: string, input: ReportProgressInput): Promise<ProgressRecord> {\n const activity = await this.content.getActivity(orgId, input.activityId);\n if (!activity || !isActivityPublished(activity.settings)) {\n throw new NotFoundError('Activity', input.activityId);\n }\n const courseId = activity.courseId;\n const modules = await this.content.listCourseModules(orgId, courseId);\n const courseActivities = await this.content.listCourseActivities(orgId, courseId);\n return this.uow.run(async (scope) => {\n // Serializes concurrent reports for this student+course (locks are tx-scoped;\n // both racers lock in the same order, the loser waits and then sees committed state).\n const lockIds = [\n ...courseActivities.filter((a) => isActivityPublished(a.settings)).map((a) => a.id),\n ...modules.map((m) => m.id),\n courseId,\n ];\n await scope.progress.findByTargets(orgId, input.orgUserId, lockIds, { forUpdate: true });\n const events: NewProgressEvent[] = [];\n let record = await this.ensureActivityRecord(orgId, input, scope, events);\n const state = mergeReports(record.position, input.reports);\n if (state.changed && !record.completedAt) {\n record = (await scope.progress.update(orgId, record.id, { position: state.map })) ?? record;\n }\n if (!record.completedAt && completionSatisfied(activity.settings, state.claimed)) {\n record =\n (await scope.progress.update(orgId, record.id, {\n completedAt: new Date(),\n })) ?? record;\n events.push(\n progressEvents.progressCompleted.make({ orgId, data: record }),\n );\n await this.completeContainers(orgId, input, courseId, modules, courseActivities, scope, events);\n this.logger.info('progress completed', { orgId, recordId: record.id });\n }\n if (events.length > 0) {\n await scope.outbox.append(events);\n }\n return record;\n });\n }\n\n private async ensureActivityRecord(\n orgId: string,\n input: ReportProgressInput,\n scope: ProgressWriteScope,\n events: NewProgressEvent[],\n ): Promise<ProgressRecord> {\n const target: ProgressTarget = {\n orgUserId: input.orgUserId,\n targetType: 'activity',\n targetId: input.activityId,\n };\n const existing = await scope.progress.findByTarget(orgId, target);\n if (existing) {\n return existing;\n }\n const now = new Date();\n const record = await scope.progress.insert(orgId, {\n id: genId('progress'),\n orgId,\n orgUserId: input.orgUserId,\n targetType: 'activity',\n targetId: input.activityId,\n startedAt: now,\n position: null,\n completedAt: null,\n updatedAt: now,\n });\n if (!record) {\n // Lost a concurrent first-touch insert: the winner owns the row and emitted\n // progress.started. Return its row without re-emitting.\n const winner = await scope.progress.findByTarget(orgId, target);\n if (!winner) {\n throw new Error('progress record vanished after insert conflict');\n }\n return winner;\n }\n events.push(progressEvents.progressStarted.make({ orgId, data: record }));\n this.logger.info('progress started', { orgId, recordId: record.id });\n return record;\n }\n\n /** After an activity completes: newly-complete containers get their records\n * (created complete) and a progress.completed event — same transaction. */\n private async completeContainers(\n orgId: string,\n input: ReportProgressInput,\n courseId: string,\n modules: Module[],\n courseActivities: Activity[],\n scope: ProgressWriteScope,\n events: NewProgressEvent[],\n ): Promise<void> {\n const published = courseActivities.filter((a) => isActivityPublished(a.settings));\n const byModule = modules.map((m) => ({\n id: m.id,\n activityIds: published.filter((a) => a.moduleId === m.id).map((a) => a.id),\n }));\n const allIds = byModule.flatMap((m) => m.activityIds);\n const records = await scope.progress.findByTargets(orgId, input.orgUserId, allIds);\n const done = new Set(\n records.filter((r) => r.targetType === 'activity' && r.completedAt).map((r) => r.targetId),\n );\n const containing = byModule.find((m) => m.activityIds.includes(input.activityId));\n if (containing && containing.activityIds.every((id) => done.has(id))) {\n await this.ensureContainerComplete(orgId, input, 'module', containing.id, scope, events);\n }\n if (allIds.length > 0 && allIds.every((id) => done.has(id))) {\n await this.ensureContainerComplete(orgId, input, 'course', courseId, scope, events);\n }\n }\n\n private async ensureContainerComplete(\n orgId: string,\n input: ReportProgressInput,\n targetType: 'module' | 'course',\n targetId: string,\n scope: ProgressWriteScope,\n events: NewProgressEvent[],\n ): Promise<void> {\n const target: ProgressTarget = { orgUserId: input.orgUserId, targetType, targetId };\n let existing = await scope.progress.findByTarget(orgId, target);\n if (existing?.completedAt) {\n return;\n }\n if (!existing) {\n const now = new Date();\n const inserted = await scope.progress.insert(orgId, {\n id: genId('progress'),\n orgId,\n orgUserId: input.orgUserId,\n targetType,\n targetId,\n startedAt: now,\n position: null,\n completedAt: now,\n updatedAt: now,\n });\n if (inserted) {\n events.push(\n progressEvents.progressCompleted.make({ orgId, data: inserted }),\n );\n return;\n }\n // Lost a concurrent insert: re-read the winner's row.\n existing = await scope.progress.findByTarget(orgId, target);\n if (existing?.completedAt) {\n return;\n }\n if (!existing) {\n throw new Error('progress container record vanished after insert conflict');\n }\n }\n const record =\n (await scope.progress.update(orgId, existing.id, {\n completedAt: new Date(),\n })) ?? existing;\n events.push(progressEvents.progressCompleted.make({ orgId, data: record }));\n }\n\n get(orgId: string, target: ProgressTarget): Promise<ProgressRecord | null> {\n return this.repo.findByTarget(orgId, target);\n }\n\n listByTargets(orgId: string, orgUserId: string, targetIds: string[]): Promise<ProgressRecord[]> {\n return this.repo.findByTargets(orgId, orgUserId, targetIds);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../src/progress/service.ts"],"sourcesContent":["// progress context — service implementation (inbound port).\n// The frontend only reports usage; this service makes every completion\n// decision. One report batch runs in one UoW transaction: ensure the record,\n// merge item payloads into the per-subject state map, evaluate the activity's\n// completion rule, then module/course against current published structure —\n// appending events with the writes. The container is resolved from the\n// hierarchy (activity → module → course), never supplied by the caller.\n// Percentage/resume are derived by readers, never stored here.\nimport { genId } from '../shared/id.js';\nimport { NotFoundError } from '../shared/errors.js';\nimport type { ProgressRecord } from './model.js';\nimport type {\n ProgressRepository,\n ProgressService,\n ProgressUnitOfWork,\n ProgressWriteScope,\n} from './ports.js';\nimport type { ProgressReportItem, ProgressTarget, ReportProgressInput } from './types.js';\nimport { progressEvents, type NewProgressEvent } from './events.js';\nimport type { Activity, CourseManagementService, Module } from '../content/index.js';\nimport type { JsonValue } from '../types/index.js';\nimport type { Logger } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\n\n/** Mirrors reporting/learn: `settings.published === false` is the only draft signal. */\nfunction isActivityPublished(settings: unknown): boolean {\n return (settings as { published?: boolean } | null)?.published !== false;\n}\n\n/** Fold a report batch into the record's per-subject state map. Reserved keys\n * (`asset` = subject, `completed` = claim) are stripped; the rest is the asset\n * type's own vocabulary, stored opaquely — latest report per subject wins. */\nfunction mergeReports(\n position: unknown,\n items: ProgressReportItem[],\n): { map: Record<string, JsonValue>; changed: boolean; claimed: boolean } {\n const map = { ...((position as Record<string, JsonValue> | null) ?? {}) };\n let changed = false;\n let claimed = false;\n for (const item of items) {\n const { asset, completed, ...payload } = item;\n if (completed === true) {\n claimed = true;\n }\n if (Object.keys(payload).length > 0) {\n map[asset ?? 'self'] = payload;\n changed = true;\n }\n }\n return { map, changed, claimed };\n}\n\n/** The completion-rule seam. Only `manual` ships: no authored rule → the\n * learner's claim decides. Authored rules are never satisfied until their\n * evaluators exist. Future rules evaluate against the accumulated state map. */\nfunction completionSatisfied(settings: unknown, claimed: boolean): boolean {\n const rule = (settings as { completion?: { rule?: string } } | null)?.completion?.rule;\n if (!rule || rule === 'manual') {\n return claimed;\n }\n return false;\n}\n\nexport type ProgressServiceParams = {\n repo: ProgressRepository;\n content: CourseManagementService;\n uow: ProgressUnitOfWork;\n logger?: Logger;\n};\n\nexport class ProgressServiceImpl implements ProgressService {\n private readonly repo: ProgressRepository;\n private readonly content: CourseManagementService;\n private readonly uow: ProgressUnitOfWork;\n private readonly logger: Logger;\n\n constructor(params: ProgressServiceParams) {\n this.repo = params.repo;\n this.content = params.content;\n this.uow = params.uow;\n this.logger = params.logger ?? noopLogger;\n }\n\n async report(orgId: string, input: ReportProgressInput): Promise<ProgressRecord> {\n const activity = await this.content.getActivity(orgId, input.activityId);\n if (!activity || !isActivityPublished(activity.settings)) {\n throw new NotFoundError('Activity', input.activityId);\n }\n const courseId = activity.courseId;\n const modules = await this.content.listCourseModules(orgId, courseId);\n const courseActivities = await this.content.listCourseActivities(orgId, courseId);\n return this.uow.run(async (scope) => {\n // Serializes concurrent reports for this student+course (locks are tx-scoped;\n // both racers lock in the same order, the loser waits and then sees committed state).\n const lockIds = [\n ...courseActivities.filter((a) => isActivityPublished(a.settings)).map((a) => a.id),\n ...modules.map((m) => m.id),\n courseId,\n ];\n await scope.progress.findByTargets(orgId, input.orgUserId, lockIds, { forUpdate: true });\n const events: NewProgressEvent[] = [];\n let record = await this.ensureActivityRecord(orgId, input, scope, events);\n const state = mergeReports(record.position, input.reports);\n if (state.changed) {\n record = (await scope.progress.update(orgId, record.id, { position: state.map })) ?? record;\n }\n if (!record.completedAt && completionSatisfied(activity.settings, state.claimed)) {\n record =\n (await scope.progress.update(orgId, record.id, {\n completedAt: new Date(),\n })) ?? record;\n events.push(progressEvents.progressCompleted.make({ orgId, data: record }));\n await this.completeContainers(\n orgId,\n input,\n courseId,\n modules,\n courseActivities,\n scope,\n events,\n );\n this.logger.info('progress completed', { orgId, recordId: record.id });\n }\n if (events.length > 0) {\n await scope.outbox.append(events);\n }\n return record;\n });\n }\n\n private async ensureActivityRecord(\n orgId: string,\n input: ReportProgressInput,\n scope: ProgressWriteScope,\n events: NewProgressEvent[],\n ): Promise<ProgressRecord> {\n const target: ProgressTarget = {\n orgUserId: input.orgUserId,\n targetType: 'activity',\n targetId: input.activityId,\n };\n const existing = await scope.progress.findByTarget(orgId, target);\n if (existing) {\n return existing;\n }\n const now = new Date();\n const record = await scope.progress.insert(orgId, {\n id: genId('progress'),\n orgId,\n orgUserId: input.orgUserId,\n targetType: 'activity',\n targetId: input.activityId,\n startedAt: now,\n position: null,\n completedAt: null,\n createdAt: now,\n updatedAt: now,\n });\n if (!record) {\n // Lost a concurrent first-touch insert: the winner owns the row and emitted\n // progress.started. Return its row without re-emitting.\n const winner = await scope.progress.findByTarget(orgId, target);\n if (!winner) {\n throw new Error('progress record vanished after insert conflict');\n }\n return winner;\n }\n events.push(progressEvents.progressStarted.make({ orgId, data: record }));\n this.logger.info('progress started', { orgId, recordId: record.id });\n return record;\n }\n\n /** After an activity completes: newly-complete containers get their records\n * (created complete) and a progress.completed event — same transaction. */\n private async completeContainers(\n orgId: string,\n input: ReportProgressInput,\n courseId: string,\n modules: Module[],\n courseActivities: Activity[],\n scope: ProgressWriteScope,\n events: NewProgressEvent[],\n ): Promise<void> {\n const published = courseActivities.filter((a) => isActivityPublished(a.settings));\n const byModule = modules.map((m) => ({\n id: m.id,\n activityIds: published.filter((a) => a.moduleId === m.id).map((a) => a.id),\n }));\n const allIds = byModule.flatMap((m) => m.activityIds);\n const records = await scope.progress.findByTargets(orgId, input.orgUserId, allIds);\n const done = new Set(\n records.filter((r) => r.targetType === 'activity' && r.completedAt).map((r) => r.targetId),\n );\n const containing = byModule.find((m) => m.activityIds.includes(input.activityId));\n if (containing && containing.activityIds.every((id) => done.has(id))) {\n await this.ensureContainerComplete(orgId, input, 'module', containing.id, scope, events);\n }\n if (allIds.length > 0 && allIds.every((id) => done.has(id))) {\n await this.ensureContainerComplete(orgId, input, 'course', courseId, scope, events);\n }\n }\n\n private async ensureContainerComplete(\n orgId: string,\n input: ReportProgressInput,\n targetType: 'module' | 'course',\n targetId: string,\n scope: ProgressWriteScope,\n events: NewProgressEvent[],\n ): Promise<void> {\n const target: ProgressTarget = { orgUserId: input.orgUserId, targetType, targetId };\n let existing = await scope.progress.findByTarget(orgId, target);\n if (existing?.completedAt) {\n return;\n }\n if (!existing) {\n const now = new Date();\n const inserted = await scope.progress.insert(orgId, {\n id: genId('progress'),\n orgId,\n orgUserId: input.orgUserId,\n targetType,\n targetId,\n startedAt: now,\n position: null,\n completedAt: now,\n createdAt: now,\n updatedAt: now,\n });\n if (inserted) {\n events.push(progressEvents.progressCompleted.make({ orgId, data: inserted }));\n return;\n }\n // Lost a concurrent insert: re-read the winner's row.\n existing = await scope.progress.findByTarget(orgId, target);\n if (existing?.completedAt) {\n return;\n }\n if (!existing) {\n throw new Error('progress container record vanished after insert conflict');\n }\n }\n const record =\n (await scope.progress.update(orgId, existing.id, {\n completedAt: new Date(),\n })) ?? existing;\n events.push(progressEvents.progressCompleted.make({ orgId, data: record }));\n }\n\n get(orgId: string, target: ProgressTarget): Promise<ProgressRecord | null> {\n return this.repo.findByTarget(orgId, target);\n }\n\n listByTargets(orgId: string, orgUserId: string, targetIds: string[]): Promise<ProgressRecord[]> {\n return this.repo.findByTargets(orgId, orgUserId, targetIds);\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { CourseActivityEngagement, CourseAnalytics, CourseEnrollmentPoint } from "./model.js";
|
|
2
|
+
import { CoursesReportRepository, CoursesReportService } from "./ports.js";
|
|
3
|
+
import { CoursesReportServiceImpl } from "./service.js";
|
|
4
|
+
export { type CourseActivityEngagement, type CourseAnalytics, type CourseEnrollmentPoint, type CoursesReportRepository, type CoursesReportService, CoursesReportServiceImpl };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/reporting/courses/model.d.ts
|
|
2
|
+
interface CourseActivityEngagement {
|
|
3
|
+
activityId: string;
|
|
4
|
+
title: string;
|
|
5
|
+
moduleId: string;
|
|
6
|
+
moduleTitle: string;
|
|
7
|
+
started: number;
|
|
8
|
+
completed: number;
|
|
9
|
+
}
|
|
10
|
+
interface CourseAnalytics {
|
|
11
|
+
enrolled: number;
|
|
12
|
+
started: number;
|
|
13
|
+
completed: number;
|
|
14
|
+
/** Mean of each cohort member's completed-activities percentage (0–100). */
|
|
15
|
+
avgProgress: number;
|
|
16
|
+
activities: CourseActivityEngagement[];
|
|
17
|
+
}
|
|
18
|
+
interface CourseEnrollmentPoint {
|
|
19
|
+
date: string;
|
|
20
|
+
count: number;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { CourseActivityEngagement, CourseAnalytics, CourseEnrollmentPoint };
|
|
24
|
+
//# sourceMappingURL=model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CourseAnalytics, CourseEnrollmentPoint } from "./model.js";
|
|
2
|
+
//#region src/reporting/courses/ports.d.ts
|
|
3
|
+
interface CoursesReportService {
|
|
4
|
+
analytics(orgId: string, courseId: string): Promise<CourseAnalytics | null>;
|
|
5
|
+
enrollments(orgId: string, courseId: string, days: number): Promise<CourseEnrollmentPoint[] | null>;
|
|
6
|
+
}
|
|
7
|
+
interface CoursesReportRepository {
|
|
8
|
+
analytics(orgId: string, courseId: string): Promise<CourseAnalytics | null>;
|
|
9
|
+
enrollments(orgId: string, courseId: string, days: number): Promise<CourseEnrollmentPoint[] | null>;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { CoursesReportRepository, CoursesReportService };
|
|
13
|
+
//# sourceMappingURL=ports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Logger } from "../../types/ports.js";
|
|
2
|
+
import "../../shared/ports.js";
|
|
3
|
+
import { CourseAnalytics, CourseEnrollmentPoint } from "./model.js";
|
|
4
|
+
import { CoursesReportRepository, CoursesReportService } from "./ports.js";
|
|
5
|
+
//#region src/reporting/courses/service.d.ts
|
|
6
|
+
type CoursesReportServiceParams = {
|
|
7
|
+
repo: CoursesReportRepository;
|
|
8
|
+
logger?: Logger;
|
|
9
|
+
};
|
|
10
|
+
declare class CoursesReportServiceImpl implements CoursesReportService {
|
|
11
|
+
private readonly repo;
|
|
12
|
+
private readonly logger;
|
|
13
|
+
constructor(params: CoursesReportServiceParams);
|
|
14
|
+
analytics(orgId: string, courseId: string): Promise<CourseAnalytics | null>;
|
|
15
|
+
enrollments(orgId: string, courseId: string, days: number): Promise<CourseEnrollmentPoint[] | null>;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { CoursesReportServiceImpl, CoursesReportServiceParams };
|
|
19
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { noopLogger } from "../../shared/logger.js";
|
|
2
|
+
//#region src/reporting/courses/service.ts
|
|
3
|
+
var CoursesReportServiceImpl = class {
|
|
4
|
+
repo;
|
|
5
|
+
logger;
|
|
6
|
+
constructor(params) {
|
|
7
|
+
this.repo = params.repo;
|
|
8
|
+
this.logger = params.logger ?? noopLogger;
|
|
9
|
+
}
|
|
10
|
+
analytics(orgId, courseId) {
|
|
11
|
+
return this.repo.analytics(orgId, courseId);
|
|
12
|
+
}
|
|
13
|
+
enrollments(orgId, courseId, days) {
|
|
14
|
+
return this.repo.enrollments(orgId, courseId, days);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { CoursesReportServiceImpl };
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../../src/reporting/courses/service.ts"],"sourcesContent":["// reporting/courses — service implementation (inbound port).\nimport type { CourseAnalytics, CourseEnrollmentPoint } from './model.js';\nimport type { CoursesReportRepository, CoursesReportService } from './ports.js';\nimport type { Logger } from '../../shared/ports.js';\nimport { noopLogger } from '../../shared/logger.js';\n\nexport type CoursesReportServiceParams = {\n repo: CoursesReportRepository;\n logger?: Logger;\n};\n\nexport class CoursesReportServiceImpl implements CoursesReportService {\n private readonly repo: CoursesReportRepository;\n private readonly logger: Logger;\n\n constructor(params: CoursesReportServiceParams) {\n this.repo = params.repo;\n this.logger = params.logger ?? noopLogger;\n }\n\n analytics(orgId: string, courseId: string): Promise<CourseAnalytics | null> {\n return this.repo.analytics(orgId, courseId);\n }\n\n enrollments(\n orgId: string,\n courseId: string,\n days: number,\n ): Promise<CourseEnrollmentPoint[] | null> {\n return this.repo.enrollments(orgId, courseId, days);\n }\n}\n"],"mappings":";;AAWA,IAAa,2BAAb,MAAsE;CACpE;CACA;CAEA,YAAY,QAAoC;EAC9C,KAAK,OAAO,OAAO;EACnB,KAAK,SAAS,OAAO,UAAU;CACjC;CAEA,UAAU,OAAe,UAAmD;EAC1E,OAAO,KAAK,KAAK,UAAU,OAAO,QAAQ;CAC5C;CAEA,YACE,OACA,UACA,MACyC;EACzC,OAAO,KAAK,KAAK,YAAY,OAAO,UAAU,IAAI;CACpD;AACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Course, Download, DownloadAsset, Module } from "../../types/schemas/content.js";
|
|
1
|
+
import { Activity, Course, Download, DownloadAsset, Module } from "../../types/schemas/content.js";
|
|
2
2
|
import "../../content/index.js";
|
|
3
3
|
//#region src/reporting/learn/model.d.ts
|
|
4
4
|
/** An active-entitlement pointer: the org + content item a student may consume. */
|
|
@@ -19,5 +19,5 @@ interface CourseProgressView {
|
|
|
19
19
|
positions: Record<string, unknown>;
|
|
20
20
|
}
|
|
21
21
|
//#endregion
|
|
22
|
-
export { ContentRef, type Course, CourseProgressView, type Download, type DownloadAsset, type Module };
|
|
22
|
+
export { type Activity, ContentRef, type Course, CourseProgressView, type Download, type DownloadAsset, type Module };
|
|
23
23
|
//# sourceMappingURL=model.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Course, Download, DownloadAsset, Module } from "../../types/schemas/content.js";
|
|
1
|
+
import { Activity, Course, Download, DownloadAsset, Module } from "../../types/schemas/content.js";
|
|
2
2
|
import { ContentRef, CourseProgressView } from "./model.js";
|
|
3
3
|
//#region src/reporting/learn/ports.d.ts
|
|
4
4
|
/**
|
|
@@ -9,6 +9,7 @@ interface LearnReportService {
|
|
|
9
9
|
listCourses(orgId: string, orgUserId: string): Promise<Course[]>;
|
|
10
10
|
getCourse(orgId: string, orgUserId: string, courseId: string): Promise<Course | null>;
|
|
11
11
|
listModules(orgId: string, orgUserId: string, courseId: string): Promise<Module[] | null>;
|
|
12
|
+
listActivities(orgId: string, orgUserId: string, courseId: string): Promise<Activity[] | null>;
|
|
12
13
|
courseProgress(orgId: string, orgUserId: string, courseId: string): Promise<CourseProgressView | null>;
|
|
13
14
|
listDownloads(orgId: string, orgUserId: string): Promise<Download[]>;
|
|
14
15
|
getDownload(orgId: string, orgUserId: string, downloadId: string): Promise<{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Logger } from "../../types/ports.js";
|
|
2
|
-
import { Course, Download, DownloadAsset, Module } from "../../types/schemas/content.js";
|
|
2
|
+
import { Activity, Course, Download, DownloadAsset, Module } from "../../types/schemas/content.js";
|
|
3
3
|
import "../../shared/ports.js";
|
|
4
4
|
import { AssetsService } from "../../assets/ports.js";
|
|
5
5
|
import "../../assets/index.js";
|
|
@@ -29,6 +29,7 @@ declare class LearnReportServiceImpl implements LearnReportService {
|
|
|
29
29
|
listCourses(orgId: string, orgUserId: string): Promise<Course[]>;
|
|
30
30
|
getCourse(orgId: string, orgUserId: string, courseId: string): Promise<Course | null>;
|
|
31
31
|
listModules(orgId: string, orgUserId: string, courseId: string): Promise<Module[] | null>;
|
|
32
|
+
listActivities(orgId: string, orgUserId: string, courseId: string): Promise<Activity[] | null>;
|
|
32
33
|
courseProgress(orgId: string, orgUserId: string, courseId: string): Promise<CourseProgressView | null>;
|
|
33
34
|
listDownloads(orgId: string, orgUserId: string): Promise<Download[]>;
|
|
34
35
|
getDownload(orgId: string, orgUserId: string, downloadId: string): Promise<{
|
|
@@ -33,6 +33,11 @@ var LearnReportServiceImpl = class {
|
|
|
33
33
|
if (!ref) return null;
|
|
34
34
|
return this.content.listCourseModules(ref.orgId, courseId);
|
|
35
35
|
}
|
|
36
|
+
async listActivities(orgId, orgUserId, courseId) {
|
|
37
|
+
const ref = await this.reader.activeRef(orgId, orgUserId, courseId);
|
|
38
|
+
if (!ref) return null;
|
|
39
|
+
return (await this.content.listCourseActivities(ref.orgId, courseId)).filter((a) => isActivityPublished(a.settings));
|
|
40
|
+
}
|
|
36
41
|
async courseProgress(orgId, orgUserId, courseId) {
|
|
37
42
|
const ref = await this.reader.activeRef(orgId, orgUserId, courseId);
|
|
38
43
|
if (!ref) return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../../src/reporting/learn/service.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../../src/reporting/learn/service.ts"],"sourcesContent":["import { type ContentServicePort } from '../../content/index.js';\nimport type { ProgressService } from '../../progress/index.js';\nimport type { AssetsService } from '../../assets/index.js';\nimport type {\n Activity,\n Course,\n CourseProgressView,\n Download,\n DownloadAsset,\n Module,\n} from './model.js';\nimport type { LearnEntitlementReader, LearnReportService } from './ports.js';\nimport type { Logger } from '../../shared/ports.js';\nimport { noopLogger } from '../../shared/logger.js';\n\nfunction isActivityPublished(settings: unknown): boolean {\n return (settings as { published?: boolean } | null)?.published !== false;\n}\n\nexport type LearnReportServiceParams = {\n reader: LearnEntitlementReader;\n content: ContentServicePort;\n progress: ProgressService;\n assets: AssetsService;\n deliveryExpirySeconds: number;\n logger?: Logger;\n};\n\nexport class LearnReportServiceImpl implements LearnReportService {\n private readonly reader: LearnEntitlementReader;\n private readonly content: ContentServicePort;\n private readonly progress: ProgressService;\n private readonly assets: AssetsService;\n private readonly deliveryExpirySeconds: number;\n private readonly logger: Logger;\n\n constructor(params: LearnReportServiceParams) {\n this.reader = params.reader;\n this.content = params.content;\n this.progress = params.progress;\n this.assets = params.assets;\n this.deliveryExpirySeconds = params.deliveryExpirySeconds;\n this.logger = params.logger ?? noopLogger;\n }\n\n async listCourses(orgId: string, orgUserId: string): Promise<Course[]> {\n const refs = await this.reader.activeRefs(orgId, orgUserId);\n const courses = await Promise.all(\n refs.map((ref) => this.content.getCourse(ref.orgId, ref.contentId)),\n );\n return courses.filter((c): c is Course => c !== null && c.status === 'published');\n }\n\n async getCourse(orgId: string, orgUserId: string, courseId: string): Promise<Course | null> {\n const ref = await this.reader.activeRef(orgId, orgUserId, courseId);\n if (!ref) {\n return null;\n }\n const course = await this.content.getCourse(ref.orgId, courseId);\n return course && course.status === 'published' ? course : null;\n }\n\n async listModules(orgId: string, orgUserId: string, courseId: string): Promise<Module[] | null> {\n const ref = await this.reader.activeRef(orgId, orgUserId, courseId);\n if (!ref) {\n return null;\n }\n return this.content.listCourseModules(ref.orgId, courseId);\n }\n\n async listActivities(\n orgId: string,\n orgUserId: string,\n courseId: string,\n ): Promise<Activity[] | null> {\n const ref = await this.reader.activeRef(orgId, orgUserId, courseId);\n if (!ref) {\n return null;\n }\n const activities = await this.content.listCourseActivities(ref.orgId, courseId);\n return activities.filter((a) => isActivityPublished(a.settings));\n }\n\n async courseProgress(\n orgId: string,\n orgUserId: string,\n courseId: string,\n ): Promise<CourseProgressView | null> {\n const ref = await this.reader.activeRef(orgId, orgUserId, courseId);\n if (!ref) {\n return null;\n }\n const courseActivities = await this.content.listCourseActivities(ref.orgId, courseId);\n const ids = courseActivities.filter((a) => isActivityPublished(a.settings)).map((a) => a.id);\n const records = await this.progress.listByTargets(ref.orgId, orgUserId, ids);\n const activities: CourseProgressView['activities'] = {};\n const positions: CourseProgressView['positions'] = {};\n let done = 0;\n for (const r of records) {\n if (r.targetType !== 'activity') {\n continue;\n }\n activities[r.targetId] = r.completedAt ? 'completed' : 'in-progress';\n if (r.completedAt) {\n done += 1;\n }\n if (r.position != null) {\n positions[r.targetId] = r.position;\n }\n }\n const courseRecord = await this.progress.get(ref.orgId, {\n orgUserId,\n targetType: 'course',\n targetId: courseId,\n });\n return {\n activities,\n percent: ids.length > 0 ? Math.round((done / ids.length) * 100) : 0,\n completed: courseRecord?.completedAt != null,\n positions,\n };\n }\n\n async listDownloads(orgId: string, orgUserId: string): Promise<Download[]> {\n const refs = await this.reader.activeDownloadRefs(orgId, orgUserId);\n const rows = await Promise.all(\n refs.map((ref) => this.content.getDownload(ref.orgId, ref.contentId)),\n );\n return rows.filter((d): d is Download => d !== null && d.status === 'published');\n }\n\n async getDownload(\n orgId: string,\n orgUserId: string,\n downloadId: string,\n ): Promise<{ download: Download; assets: DownloadAsset[] } | null> {\n const ref = await this.reader.activeDownloadRef(orgId, orgUserId, downloadId);\n if (!ref) {\n return null;\n }\n const download = await this.content.getDownload(ref.orgId, downloadId);\n if (!download || download.status !== 'published') {\n return null;\n }\n const assets = await this.content.listDownloadAssets(ref.orgId, downloadId);\n return { download, assets };\n }\n\n /** The paywall. Two gates, in order: an active entitlement to a published\n * download, then the asset actually belonging to that download — so a\n * student entitled to X cannot reach an asset of Y by swapping the id. */\n async downloadAssetUrl(\n orgId: string,\n orgUserId: string,\n downloadId: string,\n assetId: string,\n ): Promise<{ url: string; filename: string } | null> {\n const ref = await this.reader.activeDownloadRef(orgId, orgUserId, downloadId);\n if (!ref) {\n return null;\n }\n const linked = await this.reader.downloadHasAsset(ref.orgId, downloadId, assetId);\n if (!linked) {\n this.logger.warn('download asset not linked', { orgId, downloadId, assetId });\n return null;\n }\n const links = await this.content.listDownloadAssets(ref.orgId, downloadId);\n const link = links.find((l) => l.assetId === assetId);\n if (!link) {\n return null;\n }\n const asset = await this.assets.get(ref.orgId, assetId);\n if (!asset) {\n return null;\n }\n const filename = link.displayName ?? asset.filename;\n const ticket = await this.assets.requestDownload(\n ref.orgId,\n assetId,\n filename,\n this.deliveryExpirySeconds,\n );\n if (!ticket) {\n return null;\n }\n // Never log the signed URL — it is a bearer capability.\n this.logger.info('download asset signed', { orgId, downloadId, assetId });\n return { url: ticket.url, filename };\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Page, Student, StudentsQuery } from "./model.js";
|
|
1
|
+
import { Page, Student, StudentAnalytics, StudentCourseProgress, StudentsQuery } from "./model.js";
|
|
2
2
|
import { StudentsReportRepository, StudentsReportService } from "./ports.js";
|
|
3
3
|
import { StudentsReportServiceImpl } from "./service.js";
|
|
4
|
-
export { type Page, type Student, type StudentsQuery, type StudentsReportRepository, type StudentsReportService, StudentsReportServiceImpl };
|
|
4
|
+
export { type Page, type Student, type StudentAnalytics, type StudentCourseProgress, type StudentsQuery, type StudentsReportRepository, type StudentsReportService, StudentsReportServiceImpl };
|
|
@@ -14,6 +14,26 @@ interface Student extends OrgUserProfile {
|
|
|
14
14
|
joinedAt: string;
|
|
15
15
|
lastActiveAt: string | null;
|
|
16
16
|
}
|
|
17
|
+
interface StudentCourseProgress {
|
|
18
|
+
courseId: string;
|
|
19
|
+
title: string;
|
|
20
|
+
/** Published activities in the course. */
|
|
21
|
+
totalActivities: number;
|
|
22
|
+
completedActivities: number;
|
|
23
|
+
/** 0–100, completed published activities over the total. */
|
|
24
|
+
progress: number;
|
|
25
|
+
startedAt: string | null;
|
|
26
|
+
lastActivityAt: string | null;
|
|
27
|
+
completedAt: string | null;
|
|
28
|
+
}
|
|
29
|
+
interface StudentAnalytics {
|
|
30
|
+
enrolled: number;
|
|
31
|
+
started: number;
|
|
32
|
+
completed: number;
|
|
33
|
+
/** Mean of the per-course progress percentages (0–100). */
|
|
34
|
+
avgProgress: number;
|
|
35
|
+
courses: StudentCourseProgress[];
|
|
36
|
+
}
|
|
17
37
|
interface StudentsQuery {
|
|
18
38
|
page: number;
|
|
19
39
|
pageSize: number;
|
|
@@ -27,5 +47,5 @@ interface Page<T> {
|
|
|
27
47
|
pageSize: number;
|
|
28
48
|
}
|
|
29
49
|
//#endregion
|
|
30
|
-
export { Page, Student, StudentsQuery };
|
|
50
|
+
export { Page, Student, StudentAnalytics, StudentCourseProgress, StudentsQuery };
|
|
31
51
|
//# sourceMappingURL=model.d.ts.map
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { Page, Student, StudentsQuery } from "./model.js";
|
|
1
|
+
import { Page, Student, StudentAnalytics, StudentsQuery } from "./model.js";
|
|
2
2
|
//#region src/reporting/students/ports.d.ts
|
|
3
3
|
interface StudentsReportService {
|
|
4
4
|
list(orgId: string, query: StudentsQuery): Promise<Page<Student>>;
|
|
5
5
|
get(orgId: string, id: string): Promise<Student | null>;
|
|
6
|
+
analytics(orgId: string, id: string): Promise<StudentAnalytics | null>;
|
|
6
7
|
}
|
|
7
8
|
interface StudentsReportRepository {
|
|
8
9
|
list(orgId: string, query: StudentsQuery): Promise<Page<Student>>;
|
|
9
10
|
findById(orgId: string, id: string): Promise<Student | null>;
|
|
11
|
+
analytics(orgId: string, id: string): Promise<StudentAnalytics | null>;
|
|
10
12
|
}
|
|
11
13
|
//#endregion
|
|
12
14
|
export { StudentsReportRepository, StudentsReportService };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Logger } from "../../types/ports.js";
|
|
2
2
|
import "../../shared/ports.js";
|
|
3
|
-
import { Page, Student, StudentsQuery } from "./model.js";
|
|
3
|
+
import { Page, Student, StudentAnalytics, StudentsQuery } from "./model.js";
|
|
4
4
|
import { StudentsReportRepository, StudentsReportService } from "./ports.js";
|
|
5
5
|
//#region src/reporting/students/service.d.ts
|
|
6
6
|
type StudentsReportServiceParams = {
|
|
@@ -13,6 +13,7 @@ declare class StudentsReportServiceImpl implements StudentsReportService {
|
|
|
13
13
|
constructor(params: StudentsReportServiceParams);
|
|
14
14
|
list(orgId: string, query: StudentsQuery): Promise<Page<Student>>;
|
|
15
15
|
get(orgId: string, id: string): Promise<Student | null>;
|
|
16
|
+
analytics(orgId: string, id: string): Promise<StudentAnalytics | null>;
|
|
16
17
|
}
|
|
17
18
|
//#endregion
|
|
18
19
|
export { StudentsReportServiceImpl, StudentsReportServiceParams };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../../src/reporting/students/service.ts"],"sourcesContent":["// reporting/students — service implementation (inbound port).\nimport type { Page, Student, StudentsQuery } from './model.js';\nimport type { StudentsReportRepository, StudentsReportService } from './ports.js';\nimport type { Logger } from '../../shared/ports.js';\nimport { noopLogger } from '../../shared/logger.js';\n\nexport type StudentsReportServiceParams = {\n repo: StudentsReportRepository;\n logger?: Logger;\n};\n\nexport class StudentsReportServiceImpl implements StudentsReportService {\n private readonly repo: StudentsReportRepository;\n private readonly logger: Logger;\n\n constructor(params: StudentsReportServiceParams) {\n this.repo = params.repo;\n this.logger = params.logger ?? noopLogger;\n }\n\n list(orgId: string, query: StudentsQuery): Promise<Page<Student>> {\n return this.repo.list(orgId, query);\n }\n\n get(orgId: string, id: string): Promise<Student | null> {\n return this.repo.findById(orgId, id);\n }\n}\n"],"mappings":";;AAWA,IAAa,4BAAb,MAAwE;CACtE;CACA;CAEA,YAAY,QAAqC;EAC/C,KAAK,OAAO,OAAO;EACnB,KAAK,SAAS,OAAO,UAAU;CACjC;CAEA,KAAK,OAAe,OAA8C;EAChE,OAAO,KAAK,KAAK,KAAK,OAAO,KAAK;CACpC;CAEA,IAAI,OAAe,IAAqC;EACtD,OAAO,KAAK,KAAK,SAAS,OAAO,EAAE;CACrC;AACF"}
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../../src/reporting/students/service.ts"],"sourcesContent":["// reporting/students — service implementation (inbound port).\nimport type { Page, Student, StudentAnalytics, StudentsQuery } from './model.js';\nimport type { StudentsReportRepository, StudentsReportService } from './ports.js';\nimport type { Logger } from '../../shared/ports.js';\nimport { noopLogger } from '../../shared/logger.js';\n\nexport type StudentsReportServiceParams = {\n repo: StudentsReportRepository;\n logger?: Logger;\n};\n\nexport class StudentsReportServiceImpl implements StudentsReportService {\n private readonly repo: StudentsReportRepository;\n private readonly logger: Logger;\n\n constructor(params: StudentsReportServiceParams) {\n this.repo = params.repo;\n this.logger = params.logger ?? noopLogger;\n }\n\n list(orgId: string, query: StudentsQuery): Promise<Page<Student>> {\n return this.repo.list(orgId, query);\n }\n\n get(orgId: string, id: string): Promise<Student | null> {\n return this.repo.findById(orgId, id);\n }\n\n analytics(orgId: string, id: string): Promise<StudentAnalytics | null> {\n return this.repo.analytics(orgId, id);\n }\n}\n"],"mappings":";;AAWA,IAAa,4BAAb,MAAwE;CACtE;CACA;CAEA,YAAY,QAAqC;EAC/C,KAAK,OAAO,OAAO;EACnB,KAAK,SAAS,OAAO,UAAU;CACjC;CAEA,KAAK,OAAe,OAA8C;EAChE,OAAO,KAAK,KAAK,KAAK,OAAO,KAAK;CACpC;CAEA,IAAI,OAAe,IAAqC;EACtD,OAAO,KAAK,KAAK,SAAS,OAAO,EAAE;CACrC;CAEA,UAAU,OAAe,IAA8C;EACrE,OAAO,KAAK,KAAK,UAAU,OAAO,EAAE;CACtC;AACF"}
|
package/dist/shared/events.d.ts
CHANGED
|
@@ -14,18 +14,20 @@ type VersionedDomainEvent<TData = unknown, TType extends string = string, TVersi
|
|
|
14
14
|
readonly occurredAt: string;
|
|
15
15
|
readonly data: TData;
|
|
16
16
|
};
|
|
17
|
-
interface RuntimeEventDefinition<E extends VersionedDomainEvent, TMakeData = E['data']> extends EventDefinition<E, TMakeData> {
|
|
17
|
+
interface RuntimeEventDefinition<E extends VersionedDomainEvent, TMakeData = E['data'], TPublicSchema extends z.ZodType | undefined = z.ZodType | undefined> extends EventDefinition<E, TMakeData> {
|
|
18
18
|
readonly dataSchema: z.ZodType;
|
|
19
19
|
readonly eventSchema: z.ZodType<E>;
|
|
20
|
+
readonly publicData: TPublicSchema;
|
|
20
21
|
}
|
|
21
22
|
type EventOf<TDefinition extends EventDefinition<DomainEvent, unknown>> = TDefinition extends EventDefinition<infer E, unknown> ? E : never;
|
|
22
23
|
type EventOfValues<TDefinitions extends Record<string, EventDefinition<DomainEvent, unknown>>> = EventOf<TDefinitions[keyof TDefinitions]>;
|
|
23
|
-
interface DefineEventInput<TType extends string, TVersion extends number, TDataSchema extends z.ZodType> {
|
|
24
|
+
interface DefineEventInput<TType extends string, TVersion extends number, TDataSchema extends z.ZodType, TPublicSchema extends z.ZodType | undefined = undefined> {
|
|
24
25
|
readonly type: TType;
|
|
25
26
|
readonly version: TVersion;
|
|
26
27
|
readonly data: TDataSchema;
|
|
28
|
+
readonly publicData?: TPublicSchema;
|
|
27
29
|
}
|
|
28
|
-
declare function defineEvent<const TType extends string, const TVersion extends number, TDataSchema extends z.ZodType>(input: DefineEventInput<TType, TVersion, TDataSchema>): RuntimeEventDefinition<VersionedDomainEvent<z.output<TDataSchema>, TType, TVersion>, z.input<TDataSchema
|
|
30
|
+
declare function defineEvent<const TType extends string, const TVersion extends number, TDataSchema extends z.ZodType, TPublicSchema extends z.ZodType | undefined = undefined>(input: DefineEventInput<TType, TVersion, TDataSchema, TPublicSchema>): RuntimeEventDefinition<VersionedDomainEvent<z.output<TDataSchema>, TType, TVersion>, z.input<TDataSchema>, TPublicSchema>;
|
|
29
31
|
//#endregion
|
|
30
32
|
export { DefineEventInput, type EventDefinition, EventOf, EventOfValues, type EventParseResult, InvalidDomainEventError, type MakeEventInput, RuntimeEventDefinition, VersionedDomainEvent, defineEvent, eventIdSchema, eventJsonValueSchema, eventMetadataSchema };
|
|
31
33
|
//# sourceMappingURL=events.d.ts.map
|
package/dist/shared/events.js
CHANGED
|
@@ -36,6 +36,7 @@ function defineEvent(input) {
|
|
|
36
36
|
version: input.version,
|
|
37
37
|
dataSchema: input.data,
|
|
38
38
|
eventSchema,
|
|
39
|
+
publicData: input.publicData,
|
|
39
40
|
make(value) {
|
|
40
41
|
const result = makeInputSchema.safeParse(value);
|
|
41
42
|
if (!result.success) throw invalidEvent(input.type, result.error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","names":[],"sources":["../../src/shared/events.ts"],"sourcesContent":["import { z } from 'zod';\nimport type {\n ContractNewDomainEvent,\n DomainEvent,\n EventDefinition,\n EventMetadata,\n EventParseResult,\n JsonValue,\n MakeEventInput,\n} from '../types/index.js';\n\nexport class InvalidDomainEventError extends Error {\n constructor(type: string, detail: string) {\n super(`invalid ${type} event: ${detail}`);\n this.name = 'InvalidDomainEventError';\n }\n}\n\nexport const eventIdSchema: z.ZodString = z.string().trim().min(1);\n\nexport const eventJsonValueSchema: z.ZodType<JsonValue> = z.lazy(() =>\n z.union([\n z.null(),\n z.string(),\n z.number().finite(),\n z.boolean(),\n z.array(eventJsonValueSchema),\n z.record(z.string(), eventJsonValueSchema),\n ]),\n);\n\nexport const eventMetadataSchema: z.ZodType<EventMetadata> = z.record(\n z.string(),\n eventJsonValueSchema,\n);\n\nexport type VersionedDomainEvent<\n TData = unknown,\n TType extends string = string,\n TVersion extends number = number,\n> = DomainEvent<TData, TType, TVersion> & {\n readonly version: TVersion;\n readonly occurredAt: string;\n readonly data: TData;\n};\n\nexport interface RuntimeEventDefinition<\n E extends VersionedDomainEvent,\n TMakeData = E['data'],\n> extends EventDefinition<E, TMakeData> {\n readonly dataSchema: z.ZodType;\n readonly eventSchema: z.ZodType<E>;\n}\n\nexport type EventOf<TDefinition extends EventDefinition<DomainEvent, unknown>> =\n TDefinition extends EventDefinition<infer E, unknown> ? E : never;\n\nexport type EventOfValues
|
|
1
|
+
{"version":3,"file":"events.js","names":[],"sources":["../../src/shared/events.ts"],"sourcesContent":["import { z } from 'zod';\nimport type {\n ContractNewDomainEvent,\n DomainEvent,\n EventDefinition,\n EventMetadata,\n EventParseResult,\n JsonValue,\n MakeEventInput,\n} from '../types/index.js';\n\nexport class InvalidDomainEventError extends Error {\n constructor(type: string, detail: string) {\n super(`invalid ${type} event: ${detail}`);\n this.name = 'InvalidDomainEventError';\n }\n}\n\nexport const eventIdSchema: z.ZodString = z.string().trim().min(1);\n\nexport const eventJsonValueSchema: z.ZodType<JsonValue> = z.lazy(() =>\n z.union([\n z.null(),\n z.string(),\n z.number().finite(),\n z.boolean(),\n z.array(eventJsonValueSchema),\n z.record(z.string(), eventJsonValueSchema),\n ]),\n);\n\nexport const eventMetadataSchema: z.ZodType<EventMetadata> = z.record(\n z.string(),\n eventJsonValueSchema,\n);\n\nexport type VersionedDomainEvent<\n TData = unknown,\n TType extends string = string,\n TVersion extends number = number,\n> = DomainEvent<TData, TType, TVersion> & {\n readonly version: TVersion;\n readonly occurredAt: string;\n readonly data: TData;\n};\n\nexport interface RuntimeEventDefinition<\n E extends VersionedDomainEvent,\n TMakeData = E['data'],\n TPublicSchema extends z.ZodType | undefined = z.ZodType | undefined,\n> extends EventDefinition<E, TMakeData> {\n readonly dataSchema: z.ZodType;\n readonly eventSchema: z.ZodType<E>;\n readonly publicData: TPublicSchema;\n}\n\nexport type EventOf<TDefinition extends EventDefinition<DomainEvent, unknown>> =\n TDefinition extends EventDefinition<infer E, unknown> ? E : never;\n\nexport type EventOfValues<\n TDefinitions extends Record<string, EventDefinition<DomainEvent, unknown>>,\n> = EventOf<TDefinitions[keyof TDefinitions]>;\n\nexport interface DefineEventInput<\n TType extends string,\n TVersion extends number,\n TDataSchema extends z.ZodType,\n TPublicSchema extends z.ZodType | undefined = undefined,\n> {\n readonly type: TType;\n readonly version: TVersion;\n readonly data: TDataSchema;\n readonly publicData?: TPublicSchema;\n}\n\nexport function defineEvent<\n const TType extends string,\n const TVersion extends number,\n TDataSchema extends z.ZodType,\n TPublicSchema extends z.ZodType | undefined = undefined,\n>(\n input: DefineEventInput<TType, TVersion, TDataSchema, TPublicSchema>,\n): RuntimeEventDefinition<\n VersionedDomainEvent<z.output<TDataSchema>, TType, TVersion>,\n z.input<TDataSchema>,\n TPublicSchema\n> {\n type Event = VersionedDomainEvent<z.output<TDataSchema>, TType, TVersion>;\n const makeInputSchema = z\n .object({\n orgId: eventIdSchema,\n data: input.data,\n metadata: eventMetadataSchema.optional(),\n })\n .strict();\n const eventSchema = z\n .object({\n type: z.literal(input.type),\n version: z.literal(input.version),\n id: eventIdSchema,\n orgId: eventIdSchema,\n occurredAt: z.string().trim().min(1),\n data: input.data,\n metadata: eventMetadataSchema.optional(),\n })\n .strict();\n\n return {\n type: input.type,\n version: input.version,\n dataSchema: input.data,\n eventSchema: eventSchema as unknown as z.ZodType<Event>,\n publicData: input.publicData as TPublicSchema,\n make(value: MakeEventInput<z.input<TDataSchema>>) {\n const result = makeInputSchema.safeParse(value);\n if (!result.success) {\n throw invalidEvent(input.type, result.error);\n }\n return {\n type: input.type,\n version: input.version,\n ...result.data,\n } as ContractNewDomainEvent<Event>;\n },\n is(event: unknown): event is Event {\n return eventSchema.safeParse(event).success;\n },\n parse(event: unknown) {\n const result = eventSchema.safeParse(event);\n if (!result.success) {\n throw invalidEvent(input.type, result.error);\n }\n return result.data as Event;\n },\n safeParse(event: unknown): EventParseResult<Event> {\n const result = eventSchema.safeParse(event);\n if (!result.success) {\n return { success: false, error: invalidEvent(input.type, result.error) };\n }\n return { success: true, data: result.data as Event };\n },\n };\n}\n\nfunction invalidEvent(type: string, error: z.ZodError): InvalidDomainEventError {\n return new InvalidDomainEventError(\n type,\n error.issues.map((issue) => `${issue.path.join('.') || 'event'}: ${issue.message}`).join('; '),\n );\n}\n\nexport type { EventDefinition, EventParseResult, MakeEventInput };\n"],"mappings":";;AAWA,IAAa,0BAAb,cAA6C,MAAM;CACjD,YAAY,MAAc,QAAgB;EACxC,MAAM,WAAW,KAAK,UAAU,QAAQ;EACxC,KAAK,OAAO;CACd;AACF;AAEA,MAAa,gBAA6B,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC;AAEjE,MAAa,uBAA6C,EAAE,WAC1D,EAAE,MAAM;CACN,EAAE,KAAK;CACP,EAAE,OAAO;CACT,EAAE,OAAO,CAAC,CAAC,OAAO;CAClB,EAAE,QAAQ;CACV,EAAE,MAAM,oBAAoB;CAC5B,EAAE,OAAO,EAAE,OAAO,GAAG,oBAAoB;AAC3C,CAAC,CACH;AAEA,MAAa,sBAAgD,EAAE,OAC7D,EAAE,OAAO,GACT,oBACF;AAyCA,SAAgB,YAMd,OAKA;CAEA,MAAM,kBAAkB,EACrB,OAAO;EACN,OAAO;EACP,MAAM,MAAM;EACZ,UAAU,oBAAoB,SAAS;CACzC,CAAC,CAAC,CACD,OAAO;CACV,MAAM,cAAc,EACjB,OAAO;EACN,MAAM,EAAE,QAAQ,MAAM,IAAI;EAC1B,SAAS,EAAE,QAAQ,MAAM,OAAO;EAChC,IAAI;EACJ,OAAO;EACP,YAAY,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC;EACnC,MAAM,MAAM;EACZ,UAAU,oBAAoB,SAAS;CACzC,CAAC,CAAC,CACD,OAAO;CAEV,OAAO;EACL,MAAM,MAAM;EACZ,SAAS,MAAM;EACf,YAAY,MAAM;EACL;EACb,YAAY,MAAM;EAClB,KAAK,OAA6C;GAChD,MAAM,SAAS,gBAAgB,UAAU,KAAK;GAC9C,IAAI,CAAC,OAAO,SACV,MAAM,aAAa,MAAM,MAAM,OAAO,KAAK;GAE7C,OAAO;IACL,MAAM,MAAM;IACZ,SAAS,MAAM;IACf,GAAG,OAAO;GACZ;EACF;EACA,GAAG,OAAgC;GACjC,OAAO,YAAY,UAAU,KAAK,CAAC,CAAC;EACtC;EACA,MAAM,OAAgB;GACpB,MAAM,SAAS,YAAY,UAAU,KAAK;GAC1C,IAAI,CAAC,OAAO,SACV,MAAM,aAAa,MAAM,MAAM,OAAO,KAAK;GAE7C,OAAO,OAAO;EAChB;EACA,UAAU,OAAyC;GACjD,MAAM,SAAS,YAAY,UAAU,KAAK;GAC1C,IAAI,CAAC,OAAO,SACV,OAAO;IAAE,SAAS;IAAO,OAAO,aAAa,MAAM,MAAM,OAAO,KAAK;GAAE;GAEzE,OAAO;IAAE,SAAS;IAAM,MAAM,OAAO;GAAc;EACrD;CACF;AACF;AAEA,SAAS,aAAa,MAAc,OAA4C;CAC9E,OAAO,IAAI,wBACT,MACA,MAAM,OAAO,KAAK,UAAU,GAAG,MAAM,KAAK,KAAK,GAAG,KAAK,QAAQ,IAAI,MAAM,SAAS,CAAC,CAAC,KAAK,IAAI,CAC/F;AACF"}
|