@headless-lms/core 0.2.0 → 0.3.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 +39 -21
- package/dist/assets/events.js +1 -1
- package/dist/assets/events.js.map +1 -1
- package/dist/assets/file-urls.d.ts +20 -0
- package/dist/assets/file-urls.js +25 -0
- package/dist/assets/file-urls.js.map +1 -0
- package/dist/assets/index.d.ts +5 -4
- package/dist/assets/index.js +3 -2
- package/dist/assets/model.d.ts +2 -2
- package/dist/assets/ports.d.ts +45 -15
- package/dist/assets/service.d.ts +39 -7
- package/dist/assets/service.js +291 -38
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.d.ts +13 -24
- package/dist/automations/actions.js +168 -43
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.d.ts +2 -1
- package/dist/automations/catalog.js +229 -13
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.d.ts +14 -0
- package/dist/automations/community-derivations.js +439 -0
- package/dist/automations/community-derivations.js.map +1 -0
- package/dist/automations/community-mail.d.ts +112 -0
- package/dist/automations/community-mail.js +90 -0
- package/dist/automations/community-mail.js.map +1 -0
- package/dist/automations/community-notifications.d.ts +16 -0
- package/dist/automations/community-notifications.js +84 -0
- package/dist/automations/community-notifications.js.map +1 -0
- package/dist/automations/events.d.ts +28 -0
- package/dist/automations/index.d.ts +4 -2
- package/dist/automations/index.js +3 -2
- package/dist/automations/model.d.ts +10 -3
- package/dist/automations/model.js +15 -2
- package/dist/automations/model.js.map +1 -1
- package/dist/automations/ports.d.ts +5 -1
- package/dist/automations/schedule.d.ts +6 -0
- package/dist/automations/schedule.js +41 -0
- package/dist/automations/schedule.js.map +1 -0
- package/dist/automations/seeds.d.ts +17 -0
- package/dist/automations/seeds.js +235 -0
- package/dist/automations/seeds.js.map +1 -0
- package/dist/automations/service.d.ts +22 -3
- package/dist/automations/service.js +306 -43
- package/dist/automations/service.js.map +1 -1
- package/dist/content/events.d.ts +631 -19
- package/dist/content/events.js +44 -1
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +4 -4
- package/dist/content/model.d.ts +2 -2
- package/dist/content/ports.d.ts +71 -18
- package/dist/content/service.d.ts +57 -18
- package/dist/content/service.js +502 -23
- package/dist/content/service.js.map +1 -1
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/authors.d.ts +10 -0
- package/dist/discussion/authors.js +23 -0
- package/dist/discussion/authors.js.map +1 -0
- package/dist/discussion/chats.d.ts +104 -0
- package/dist/discussion/chats.js +798 -0
- package/dist/discussion/chats.js.map +1 -0
- package/dist/discussion/events.d.ts +619 -56
- package/dist/discussion/events.js +106 -3
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +7 -3
- package/dist/discussion/index.js +4 -1
- package/dist/discussion/ports.d.ts +288 -27
- package/dist/discussion/ports.js +6 -1
- package/dist/discussion/ports.js.map +1 -0
- package/dist/discussion/posts.d.ts +103 -0
- package/dist/discussion/posts.js +723 -0
- package/dist/discussion/posts.js.map +1 -0
- package/dist/discussion/service.d.ts +32 -10
- package/dist/discussion/service.js +293 -78
- package/dist/discussion/service.js.map +1 -1
- package/dist/entitlements/events.d.ts +178 -11
- package/dist/entitlements/events.js +28 -1
- package/dist/entitlements/events.js.map +1 -1
- package/dist/entitlements/index.d.ts +5 -4
- package/dist/entitlements/index.js +2 -1
- package/dist/entitlements/model.d.ts +3 -2
- package/dist/entitlements/ports.d.ts +124 -11
- package/dist/entitlements/rules.d.ts +12 -0
- package/dist/entitlements/rules.js +17 -0
- package/dist/entitlements/rules.js.map +1 -0
- package/dist/entitlements/service.d.ts +54 -3
- package/dist/entitlements/service.js +472 -2
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/access.d.ts +6 -0
- package/dist/identity/access.js +7 -0
- package/dist/identity/access.js.map +1 -0
- package/dist/identity/index.d.ts +6 -4
- package/dist/identity/index.js +3 -1
- package/dist/identity/model.d.ts +2 -1
- package/dist/identity/notifications.d.ts +27 -0
- package/dist/identity/notifications.js +101 -0
- package/dist/identity/notifications.js.map +1 -0
- package/dist/identity/ports.d.ts +73 -3
- package/dist/identity/service.d.ts +14 -3
- package/dist/identity/service.js +62 -5
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/model.js +1 -1
- package/dist/integrations/model.js.map +1 -1
- package/dist/integrations/service.js +2 -2
- package/dist/integrations/service.js.map +1 -1
- package/dist/organizations/access-scopes.d.ts +19 -0
- package/dist/organizations/access-scopes.js +89 -0
- package/dist/organizations/access-scopes.js.map +1 -0
- package/dist/organizations/events.d.ts +74 -32
- package/dist/organizations/events.js +6 -0
- package/dist/organizations/events.js.map +1 -1
- package/dist/organizations/index.d.ts +7 -6
- package/dist/organizations/index.js +4 -3
- package/dist/organizations/members.d.ts +8 -5
- package/dist/organizations/members.js.map +1 -1
- package/dist/organizations/ports.d.ts +14 -11
- package/dist/organizations/roles.d.ts +6 -10
- package/dist/organizations/roles.js +19 -7
- package/dist/organizations/roles.js.map +1 -1
- package/dist/organizations/service.d.ts +20 -11
- package/dist/organizations/service.js +157 -47
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/types.d.ts +1 -10
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/ports.d.ts +6 -1
- package/dist/progress/service.d.ts +3 -1
- package/dist/progress/service.js +6 -2
- package/dist/progress/service.js.map +1 -1
- package/dist/reporting/admin/index.d.ts +4 -0
- package/dist/reporting/admin/index.js +2 -0
- package/dist/reporting/admin/model.d.ts +108 -0
- package/dist/reporting/admin/ports.d.ts +27 -0
- package/dist/reporting/admin/service.d.ts +22 -0
- package/dist/reporting/admin/service.js +38 -0
- package/dist/reporting/admin/service.js.map +1 -0
- package/dist/reporting/member/index.d.ts +5 -0
- package/dist/reporting/member/index.js +2 -0
- package/dist/reporting/member/model.d.ts +124 -0
- package/dist/reporting/member/ports.d.ts +45 -0
- package/dist/reporting/member/service.d.ts +73 -0
- package/dist/reporting/member/service.js +286 -0
- package/dist/reporting/member/service.js.map +1 -0
- package/dist/shared/actor.d.ts +9 -0
- package/dist/shared/errors.d.ts +7 -1
- package/dist/shared/errors.js +10 -1
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +11 -0
- package/dist/shared/id.js +12 -1
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/mailer.d.ts +4 -1
- package/dist/shared/mailer.js.map +1 -1
- package/dist/shared/network.d.ts +10 -0
- package/dist/shared/network.js +211 -0
- package/dist/shared/network.js.map +1 -0
- package/dist/types/assets.d.ts +2 -2
- package/dist/types/automations.d.ts +10 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +11 -3
- package/dist/types/email-templates.d.ts +121 -0
- package/dist/types/entitlements.d.ts +2 -2
- package/dist/types/identity.d.ts +48 -2
- package/dist/types/index.d.ts +9 -9
- package/dist/types/ports.d.ts +6 -0
- package/dist/types/schemas/assets.d.ts +65 -37
- package/dist/types/schemas/assets.js +17 -12
- package/dist/types/schemas/assets.js.map +1 -1
- package/dist/types/schemas/automations.d.ts +18 -1
- package/dist/types/schemas/automations.js +15 -6
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +335 -3
- package/dist/types/schemas/content.js +139 -3
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +370 -3
- package/dist/types/schemas/discussion.js +199 -4
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/entitlements.d.ts +36 -2
- package/dist/types/schemas/entitlements.js +23 -1
- package/dist/types/schemas/entitlements.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +111 -1
- package/dist/types/schemas/identity.js +55 -2
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +8 -8
- package/dist/types/schemas/index.js +8 -8
- package/dist/types/schemas/organizations.d.ts +8 -0
- package/dist/types/schemas/organizations.js +2 -0
- package/dist/types/schemas/organizations.js.map +1 -1
- package/dist/types/schemas/shared.d.ts +3 -1
- package/dist/types/schemas/shared.js +5 -1
- package/dist/types/schemas/shared.js.map +1 -1
- package/package.json +2 -2
- package/dist/reporting/courses/index.d.ts +0 -4
- package/dist/reporting/courses/index.js +0 -2
- package/dist/reporting/courses/model.d.ts +0 -24
- package/dist/reporting/courses/ports.d.ts +0 -13
- package/dist/reporting/courses/service.d.ts +0 -19
- package/dist/reporting/courses/service.js +0 -20
- package/dist/reporting/courses/service.js.map +0 -1
- package/dist/reporting/dashboard/index.d.ts +0 -4
- package/dist/reporting/dashboard/index.js +0 -2
- package/dist/reporting/dashboard/model.d.ts +0 -14
- package/dist/reporting/dashboard/ports.d.ts +0 -13
- package/dist/reporting/dashboard/service.d.ts +0 -19
- package/dist/reporting/dashboard/service.js +0 -20
- package/dist/reporting/dashboard/service.js.map +0 -1
- package/dist/reporting/learn/index.d.ts +0 -5
- package/dist/reporting/learn/index.js +0 -2
- package/dist/reporting/learn/model.d.ts +0 -23
- package/dist/reporting/learn/ports.d.ts +0 -43
- package/dist/reporting/learn/ports.js +0 -1
- package/dist/reporting/learn/service.d.ts +0 -49
- package/dist/reporting/learn/service.js +0 -116
- package/dist/reporting/learn/service.js.map +0 -1
- package/dist/reporting/students/index.d.ts +0 -4
- package/dist/reporting/students/index.js +0 -2
- package/dist/reporting/students/model.d.ts +0 -51
- package/dist/reporting/students/model.js +0 -1
- package/dist/reporting/students/ports.d.ts +0 -15
- package/dist/reporting/students/ports.js +0 -1
- package/dist/reporting/students/service.d.ts +0 -20
- package/dist/reporting/students/service.js +0 -23
- package/dist/reporting/students/service.js.map +0 -1
- /package/dist/reporting/{courses → admin}/model.js +0 -0
- /package/dist/reporting/{courses → admin}/ports.js +0 -0
- /package/dist/reporting/{dashboard → member}/model.js +0 -0
- /package/dist/reporting/{dashboard → member}/ports.js +0 -0
- /package/dist/{reporting/learn/model.js → shared/actor.js} +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { deliverWebhookInputSchema } from "../types/schemas/automations.js";
|
|
2
|
-
import { noopLogger } from "../shared/logger.js";
|
|
3
2
|
import { ConflictError, NotFoundError } from "../shared/errors.js";
|
|
4
|
-
import {
|
|
3
|
+
import { noopLogger } from "../shared/logger.js";
|
|
4
|
+
import { ActionFailedError, AutomationKindMismatchError, InvalidTriggerError, ReservedActionError } from "./model.js";
|
|
5
|
+
import { DELIVER_WEBHOOK_ACTION, catalogActions, catalogTriggers, publicDataSchema } from "./catalog.js";
|
|
5
6
|
import { generateWebhookSecret } from "./webhook-signing.js";
|
|
6
7
|
import { executeAction } from "./actions.js";
|
|
7
8
|
import { automationEvents } from "./events.js";
|
|
8
|
-
import {
|
|
9
|
+
import { communitySeeds, orgSeeds } from "./seeds.js";
|
|
10
|
+
import { cronSupported } from "./schedule.js";
|
|
9
11
|
//#region src/automations/service.ts
|
|
10
12
|
/** True only when `input` sets `enabled` and nothing else — emits automation.enabled|disabled instead of automation.updated. */
|
|
11
13
|
function isEnabledOnlyUpdate(input) {
|
|
@@ -13,15 +15,79 @@ function isEnabledOnlyUpdate(input) {
|
|
|
13
15
|
const { enabled: _enabled, ...rest } = input;
|
|
14
16
|
return Object.values(rest).every((v) => v === void 0);
|
|
15
17
|
}
|
|
18
|
+
function floorMinute(at) {
|
|
19
|
+
const copy = new Date(at);
|
|
20
|
+
copy.setUTCSeconds(0, 0);
|
|
21
|
+
return copy.getTime();
|
|
22
|
+
}
|
|
16
23
|
function unique(values) {
|
|
17
24
|
return [...new Set(values)];
|
|
18
25
|
}
|
|
26
|
+
function recordString(value, key) {
|
|
27
|
+
if (typeof value !== "object" || value === null || !(key in value)) return null;
|
|
28
|
+
const found = value[key];
|
|
29
|
+
return typeof found === "string" ? found : null;
|
|
30
|
+
}
|
|
31
|
+
function filterMatches(filter, data) {
|
|
32
|
+
if (filter == null) return true;
|
|
33
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return false;
|
|
34
|
+
const record = data;
|
|
35
|
+
return Object.entries(filter).every(([key, expected]) => {
|
|
36
|
+
if (!Object.prototype.hasOwnProperty.call(record, key)) return false;
|
|
37
|
+
return JSON.stringify(record[key]) === JSON.stringify(expected);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
19
40
|
function assertAuthorableTriggers(triggers) {
|
|
20
|
-
for (const trigger of triggers)
|
|
41
|
+
for (const trigger of triggers) {
|
|
42
|
+
if (trigger.startsWith("automation.")) throw new InvalidTriggerError(trigger);
|
|
43
|
+
if (trigger.startsWith("schedule:") && !cronSupported(trigger.slice(9))) throw new InvalidTriggerError(trigger, "schedule");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function assertCatalogTriggers(triggers, schedules) {
|
|
47
|
+
assertAuthorableTriggers(triggers);
|
|
48
|
+
const known = new Set(catalogTriggers().map((t) => t.type));
|
|
49
|
+
for (const trigger of triggers) if (!(schedules && trigger.startsWith("schedule:")) && !known.has(trigger)) throw new InvalidTriggerError(trigger, "unknown");
|
|
21
50
|
}
|
|
22
51
|
function assertAuthorableActions(actions) {
|
|
23
52
|
for (const action of actions) if (action.type === "deliverWebhook") throw new ReservedActionError(action.type);
|
|
24
53
|
}
|
|
54
|
+
const SCHEDULE_FIRED = "automation.schedule.fired";
|
|
55
|
+
function publicEventData(event) {
|
|
56
|
+
const parsed = event.type === SCHEDULE_FIRED ? {
|
|
57
|
+
success: true,
|
|
58
|
+
data: event.data
|
|
59
|
+
} : publicDataSchema(event.type)?.safeParse(event.data);
|
|
60
|
+
return parsed?.success ? JSON.parse(JSON.stringify(parsed.data)) : null;
|
|
61
|
+
}
|
|
62
|
+
/** A run as callers see it: its event carries only what a webhook of that event would. */
|
|
63
|
+
function toRunView(run) {
|
|
64
|
+
const { type, version, id, orgId, occurredAt } = run.event;
|
|
65
|
+
return {
|
|
66
|
+
...run,
|
|
67
|
+
event: {
|
|
68
|
+
type,
|
|
69
|
+
version,
|
|
70
|
+
id,
|
|
71
|
+
orgId,
|
|
72
|
+
occurredAt,
|
|
73
|
+
data: publicEventData(run.event)
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** An automation as callers see it: a webhook's credential reference stays internal. */
|
|
78
|
+
function toAutomationView(automation) {
|
|
79
|
+
return {
|
|
80
|
+
...automation,
|
|
81
|
+
actions: automation.actions.map((action) => {
|
|
82
|
+
if (action.type !== "deliverWebhook") return action;
|
|
83
|
+
const { secretRef: _secretRef, ...input } = action.input;
|
|
84
|
+
return {
|
|
85
|
+
...action,
|
|
86
|
+
input
|
|
87
|
+
};
|
|
88
|
+
})
|
|
89
|
+
};
|
|
90
|
+
}
|
|
25
91
|
function webhookTarget(automation) {
|
|
26
92
|
return deliverWebhookInputSchema.parse(automation.actions[0]?.input);
|
|
27
93
|
}
|
|
@@ -60,6 +126,10 @@ var AutomationsServiceImpl = class {
|
|
|
60
126
|
integrations;
|
|
61
127
|
credentials;
|
|
62
128
|
webhooks;
|
|
129
|
+
mail;
|
|
130
|
+
grants;
|
|
131
|
+
desk;
|
|
132
|
+
scheduledWork;
|
|
63
133
|
logger;
|
|
64
134
|
constructor(params) {
|
|
65
135
|
this.repo = params.repo;
|
|
@@ -71,12 +141,85 @@ var AutomationsServiceImpl = class {
|
|
|
71
141
|
this.integrations = params.integrations;
|
|
72
142
|
this.credentials = params.credentials;
|
|
73
143
|
this.webhooks = params.webhooks;
|
|
144
|
+
this.mail = {
|
|
145
|
+
...params.mail,
|
|
146
|
+
recentMessageRecipients: (orgId, automationId, chatId, since) => this.runsRepo.recentRecipients(orgId, automationId, chatId, since)
|
|
147
|
+
};
|
|
148
|
+
this.grants = params.grants;
|
|
149
|
+
this.desk = params.desk;
|
|
150
|
+
this.scheduledWork = params.scheduledWork;
|
|
74
151
|
this.logger = params.logger ?? noopLogger;
|
|
75
152
|
}
|
|
76
153
|
async handle(event) {
|
|
77
154
|
if (event.type.startsWith("automation.")) return;
|
|
155
|
+
await this.seedFor(event);
|
|
78
156
|
const automations = await this.safeListByTrigger(event);
|
|
79
|
-
for (const automation of automations
|
|
157
|
+
for (const automation of automations) {
|
|
158
|
+
const matches = filterMatches(automation.filter, event.data);
|
|
159
|
+
if (!automation.enabled || !matches) {
|
|
160
|
+
if (automation.enabled && !matches) this.logger.debug("automation filter skipped", {
|
|
161
|
+
orgId: event.orgId,
|
|
162
|
+
automationId: automation.id,
|
|
163
|
+
trigger: event.type
|
|
164
|
+
});
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
await this.dispatchOne(automation, event);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
seedsFor(event) {
|
|
171
|
+
if (event.type === "organization.created") return orgSeeds();
|
|
172
|
+
if (event.type !== "content.community.created") return null;
|
|
173
|
+
const communityId = recordString(event.data, "id");
|
|
174
|
+
if (!communityId) {
|
|
175
|
+
this.logger.error("community seed missing id", {
|
|
176
|
+
orgId: event.orgId,
|
|
177
|
+
eventId: event.id
|
|
178
|
+
});
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
return communitySeeds(communityId);
|
|
182
|
+
}
|
|
183
|
+
async seedFor(event) {
|
|
184
|
+
const rows = this.seedsFor(event);
|
|
185
|
+
if (!rows) return;
|
|
186
|
+
if (await this.uow.run(async ({ automations, outbox }) => {
|
|
187
|
+
const created = [];
|
|
188
|
+
for (const row of rows) {
|
|
189
|
+
const inserted = await automations.insertSeed(event.orgId, {
|
|
190
|
+
name: row.name,
|
|
191
|
+
description: row.description,
|
|
192
|
+
triggers: row.triggers,
|
|
193
|
+
actions: row.actions,
|
|
194
|
+
filter: row.filter,
|
|
195
|
+
kind: "workflow",
|
|
196
|
+
source: "system"
|
|
197
|
+
});
|
|
198
|
+
if (inserted) created.push(automationEvents.automationCreated.make({
|
|
199
|
+
orgId: event.orgId,
|
|
200
|
+
data: inserted
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
if (created.length > 0) {
|
|
204
|
+
await outbox.append(created);
|
|
205
|
+
this.logger.info("system automations seeded", {
|
|
206
|
+
orgId: event.orgId,
|
|
207
|
+
trigger: event.type,
|
|
208
|
+
count: created.length
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return created.length;
|
|
212
|
+
}) > 0) await this.reconcileSchedules(event.orgId);
|
|
213
|
+
}
|
|
214
|
+
async reconcileSchedules(orgId) {
|
|
215
|
+
try {
|
|
216
|
+
await this.engine.syncSchedules?.();
|
|
217
|
+
} catch (err) {
|
|
218
|
+
this.logger.error("automation schedules not reconciled", {
|
|
219
|
+
orgId,
|
|
220
|
+
err
|
|
221
|
+
});
|
|
222
|
+
}
|
|
80
223
|
}
|
|
81
224
|
async safeListByTrigger(event) {
|
|
82
225
|
try {
|
|
@@ -85,7 +228,7 @@ var AutomationsServiceImpl = class {
|
|
|
85
228
|
this.logger.error("automations: failed to load trigger matches", {
|
|
86
229
|
orgId: event.orgId,
|
|
87
230
|
trigger: event.type,
|
|
88
|
-
|
|
231
|
+
err
|
|
89
232
|
});
|
|
90
233
|
return [];
|
|
91
234
|
}
|
|
@@ -97,14 +240,15 @@ var AutomationsServiceImpl = class {
|
|
|
97
240
|
if (!run) return;
|
|
98
241
|
await this.engine.dispatch(this.dispatchFor(automation, run, event));
|
|
99
242
|
} catch (err) {
|
|
100
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
101
243
|
this.logger.error("automation run failed", {
|
|
102
244
|
orgId: event.orgId,
|
|
103
245
|
automationId: automation.id,
|
|
104
246
|
runId: run?.id,
|
|
105
|
-
|
|
247
|
+
trigger: event.type,
|
|
248
|
+
eventId: event.id,
|
|
249
|
+
err
|
|
106
250
|
});
|
|
107
|
-
if (run) await this.recordFailure(run, message);
|
|
251
|
+
if (run) await this.recordFailure(run, err instanceof Error ? err.message : String(err));
|
|
108
252
|
}
|
|
109
253
|
}
|
|
110
254
|
openRun(automation, event) {
|
|
@@ -157,8 +301,9 @@ var AutomationsServiceImpl = class {
|
|
|
157
301
|
} catch (err) {
|
|
158
302
|
this.logger.error("failed to record automation run failure", {
|
|
159
303
|
orgId: run.orgId,
|
|
304
|
+
automationId: run.automationId,
|
|
160
305
|
runId: run.id,
|
|
161
|
-
|
|
306
|
+
err,
|
|
162
307
|
originalError: error
|
|
163
308
|
});
|
|
164
309
|
return null;
|
|
@@ -167,17 +312,97 @@ var AutomationsServiceImpl = class {
|
|
|
167
312
|
async runAction(d, index) {
|
|
168
313
|
const action = d.actions[index];
|
|
169
314
|
if (!action) throw new Error(`automation ${d.automationId} run ${d.runId}: no action at index ${index}`);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
315
|
+
let outcome;
|
|
316
|
+
try {
|
|
317
|
+
outcome = await executeAction(action, d.event, {
|
|
318
|
+
mailer: this.mailer,
|
|
319
|
+
lookups: this.lookups,
|
|
320
|
+
credentials: this.credentials,
|
|
321
|
+
webhooks: this.webhooks,
|
|
322
|
+
mail: this.mail,
|
|
323
|
+
grants: this.grants,
|
|
324
|
+
desk: this.desk,
|
|
325
|
+
scheduledWork: this.scheduledWork,
|
|
326
|
+
automationId: d.automationId,
|
|
327
|
+
actionInput: action.input,
|
|
328
|
+
logger: this.logger
|
|
329
|
+
});
|
|
330
|
+
} catch (err) {
|
|
331
|
+
this.logger.error("automation action failed", {
|
|
332
|
+
orgId: d.orgId,
|
|
333
|
+
automationId: d.automationId,
|
|
334
|
+
runId: d.runId,
|
|
335
|
+
trigger: d.event.type,
|
|
336
|
+
index,
|
|
337
|
+
type: action.type,
|
|
338
|
+
err
|
|
339
|
+
});
|
|
340
|
+
throw err instanceof ActionFailedError ? err : new ActionFailedError();
|
|
341
|
+
}
|
|
342
|
+
const { recipients, failed } = outcome;
|
|
343
|
+
this.logger.info("automation action completed", {
|
|
344
|
+
orgId: d.orgId,
|
|
345
|
+
automationId: d.automationId,
|
|
346
|
+
runId: d.runId,
|
|
347
|
+
type: action.type,
|
|
348
|
+
recipients: recipients.length,
|
|
349
|
+
failed: failed.length
|
|
175
350
|
});
|
|
176
351
|
return {
|
|
177
352
|
index,
|
|
178
353
|
type: action.type,
|
|
179
|
-
status: "completed"
|
|
354
|
+
status: "completed",
|
|
355
|
+
...recipients.length > 0 ? { recipients } : {},
|
|
356
|
+
...failed.length > 0 ? { error: `delivery failed for ${failed.length} recipients: ${failed.join(", ")}` } : {}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
async listSchedules() {
|
|
360
|
+
return this.repo.listSchedules();
|
|
361
|
+
}
|
|
362
|
+
async fireSchedule(orgId, automationId, at) {
|
|
363
|
+
const firedAt = new Date(floorMinute(at));
|
|
364
|
+
const automation = await this.repo.findById(orgId, automationId);
|
|
365
|
+
if (!automation?.enabled || firedAt.getTime() < floorMinute(automation.createdAt)) {
|
|
366
|
+
this.logger.debug("schedule skipped", {
|
|
367
|
+
orgId,
|
|
368
|
+
automationId
|
|
369
|
+
});
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (!await this.scheduleHasWork(orgId, automation, firedAt)) {
|
|
373
|
+
this.logger.debug("schedule idle", {
|
|
374
|
+
orgId,
|
|
375
|
+
automationId
|
|
376
|
+
});
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const event = {
|
|
380
|
+
type: SCHEDULE_FIRED,
|
|
381
|
+
id: `schedule_${automationId}_${firedAt.toISOString()}`,
|
|
382
|
+
orgId,
|
|
383
|
+
version: 1,
|
|
384
|
+
occurredAt: firedAt.toISOString(),
|
|
385
|
+
data: {
|
|
386
|
+
automationId,
|
|
387
|
+
firedAt: firedAt.toISOString()
|
|
388
|
+
}
|
|
180
389
|
};
|
|
390
|
+
await this.dispatchOne(automation, event);
|
|
391
|
+
}
|
|
392
|
+
async scheduleHasWork(orgId, automation, firedAt) {
|
|
393
|
+
const checks = automation.actions.map((action) => this.pendingWork(action.type));
|
|
394
|
+
const known = checks.filter((check) => check !== null);
|
|
395
|
+
if (checks.length === 0 || known.length < checks.length) return true;
|
|
396
|
+
for (const check of known) if (await check(orgId, firedAt)) return true;
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
pendingWork(type) {
|
|
400
|
+
switch (type) {
|
|
401
|
+
case "publishDuePosts": return (orgId, now) => this.scheduledWork.hasDuePosts(orgId, now);
|
|
402
|
+
case "expireDueGrants": return (orgId, now) => this.scheduledWork.hasDueGrants(orgId, now);
|
|
403
|
+
case "removeUnattachedUploads": return (orgId, now) => this.scheduledWork.hasUnattachedUploads(orgId, now);
|
|
404
|
+
default: return null;
|
|
405
|
+
}
|
|
181
406
|
}
|
|
182
407
|
async finalize(d, results) {
|
|
183
408
|
const status = results.length === d.actions.length && results.every((r) => r.status === "completed") ? "completed" : "failed";
|
|
@@ -201,9 +426,39 @@ var AutomationsServiceImpl = class {
|
|
|
201
426
|
}) }));
|
|
202
427
|
await outbox.append([runEvent, ...actionFailedEvents]);
|
|
203
428
|
});
|
|
429
|
+
this.logOutcome(d, status, results);
|
|
430
|
+
}
|
|
431
|
+
logOutcome(d, status, results) {
|
|
432
|
+
const run = {
|
|
433
|
+
orgId: d.orgId,
|
|
434
|
+
automationId: d.automationId,
|
|
435
|
+
runId: d.runId,
|
|
436
|
+
trigger: d.event.type,
|
|
437
|
+
eventId: d.event.id
|
|
438
|
+
};
|
|
439
|
+
if (status === "failed") {
|
|
440
|
+
const failed = results.find((r) => r.status === "failed");
|
|
441
|
+
const index = failed?.index ?? results.length;
|
|
442
|
+
this.logger.error("automation run failed", {
|
|
443
|
+
...run,
|
|
444
|
+
index,
|
|
445
|
+
action: failed?.type ?? d.actions[index]?.type,
|
|
446
|
+
error: failed?.error ?? "not every action ran"
|
|
447
|
+
});
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const partial = results.filter((r) => r.error !== void 0);
|
|
451
|
+
if (partial.length > 0) this.logger.warn("automation run completed with failed deliveries", {
|
|
452
|
+
...run,
|
|
453
|
+
actions: partial.map(({ index, type, error }) => ({
|
|
454
|
+
index,
|
|
455
|
+
type,
|
|
456
|
+
error
|
|
457
|
+
}))
|
|
458
|
+
});
|
|
204
459
|
}
|
|
205
460
|
async create(orgId, input) {
|
|
206
|
-
|
|
461
|
+
assertCatalogTriggers(input.triggers, true);
|
|
207
462
|
assertAuthorableActions(input.actions);
|
|
208
463
|
const automation = await this.uow.run(async ({ automations, outbox }) => {
|
|
209
464
|
const created = await automations.insert(orgId, {
|
|
@@ -221,10 +476,11 @@ var AutomationsServiceImpl = class {
|
|
|
221
476
|
orgId,
|
|
222
477
|
automationId: automation.id
|
|
223
478
|
});
|
|
479
|
+
await this.reconcileSchedules(orgId);
|
|
224
480
|
return automation;
|
|
225
481
|
}
|
|
226
482
|
async update(orgId, id, input) {
|
|
227
|
-
if (input.triggers !== void 0)
|
|
483
|
+
if (input.triggers !== void 0) assertCatalogTriggers(input.triggers, true);
|
|
228
484
|
if (input.actions !== void 0) assertAuthorableActions(input.actions);
|
|
229
485
|
const existing = await this.repo.findById(orgId, id);
|
|
230
486
|
if (!existing) return null;
|
|
@@ -239,16 +495,20 @@ var AutomationsServiceImpl = class {
|
|
|
239
495
|
await outbox.append([updateEvent(orgId, input, updated)]);
|
|
240
496
|
return updated;
|
|
241
497
|
});
|
|
242
|
-
if (automation)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
498
|
+
if (automation) {
|
|
499
|
+
this.logger.info("automation updated", {
|
|
500
|
+
orgId,
|
|
501
|
+
automationId: id
|
|
502
|
+
});
|
|
503
|
+
await this.reconcileSchedules(orgId);
|
|
504
|
+
}
|
|
246
505
|
return automation;
|
|
247
506
|
}
|
|
248
507
|
async delete(orgId, id) {
|
|
249
508
|
const existing = await this.repo.findById(orgId, id);
|
|
250
509
|
if (!existing) return false;
|
|
251
510
|
if (existing.kind !== "workflow") throw new AutomationKindMismatchError(id, existing.kind);
|
|
511
|
+
if (existing.source === "system") throw new ConflictError("A system automation cannot be deleted");
|
|
252
512
|
const deleted = await this.uow.run(async ({ automations, outbox }) => {
|
|
253
513
|
const removed = await automations.delete(orgId, id);
|
|
254
514
|
if (!removed) return null;
|
|
@@ -258,20 +518,28 @@ var AutomationsServiceImpl = class {
|
|
|
258
518
|
})]);
|
|
259
519
|
return removed;
|
|
260
520
|
});
|
|
261
|
-
if (deleted)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
521
|
+
if (deleted) {
|
|
522
|
+
this.logger.info("automation deleted", {
|
|
523
|
+
orgId,
|
|
524
|
+
automationId: id
|
|
525
|
+
});
|
|
526
|
+
await this.reconcileSchedules(orgId);
|
|
527
|
+
}
|
|
265
528
|
return deleted !== null;
|
|
266
529
|
}
|
|
267
|
-
list(orgId, query) {
|
|
268
|
-
return this.repo.listByOrg(orgId, query?.kind);
|
|
530
|
+
async list(orgId, query) {
|
|
531
|
+
return (await this.repo.listByOrg(orgId, query?.kind)).map(toAutomationView);
|
|
269
532
|
}
|
|
270
|
-
get(orgId, id) {
|
|
271
|
-
|
|
533
|
+
async get(orgId, id) {
|
|
534
|
+
const automation = await this.repo.findById(orgId, id);
|
|
535
|
+
return automation ? toAutomationView(automation) : null;
|
|
272
536
|
}
|
|
273
|
-
listRuns(orgId, automationId, query) {
|
|
274
|
-
|
|
537
|
+
async listRuns(orgId, automationId, query) {
|
|
538
|
+
const page = await this.runsRepo.list(orgId, automationId, query);
|
|
539
|
+
return {
|
|
540
|
+
...page,
|
|
541
|
+
rows: page.rows.map(toRunView)
|
|
542
|
+
};
|
|
275
543
|
}
|
|
276
544
|
async rerun(orgId, automationId, runId) {
|
|
277
545
|
const existing = await this.runsRepo.findById(orgId, runId);
|
|
@@ -291,21 +559,21 @@ var AutomationsServiceImpl = class {
|
|
|
291
559
|
try {
|
|
292
560
|
await this.engine.dispatch(this.dispatchFor(automation, run, run.event));
|
|
293
561
|
} catch (err) {
|
|
294
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
295
562
|
this.logger.error("automation rerun failed", {
|
|
296
563
|
orgId,
|
|
297
564
|
automationId,
|
|
298
565
|
runId: run.id,
|
|
299
|
-
|
|
566
|
+
trigger: run.trigger,
|
|
567
|
+
err
|
|
300
568
|
});
|
|
301
|
-
return await this.recordFailure(run, message) ?? run;
|
|
569
|
+
return toRunView(await this.recordFailure(run, err instanceof Error ? err.message : String(err)) ?? run);
|
|
302
570
|
}
|
|
303
571
|
this.logger.info("automation rerun started", {
|
|
304
572
|
orgId,
|
|
305
573
|
automationId,
|
|
306
574
|
runId: run.id
|
|
307
575
|
});
|
|
308
|
-
return run;
|
|
576
|
+
return toRunView(run);
|
|
309
577
|
}
|
|
310
578
|
async listWebhooks(orgId) {
|
|
311
579
|
return (await this.repo.listByOrg(orgId, "webhook")).map(toWebhook);
|
|
@@ -316,7 +584,7 @@ var AutomationsServiceImpl = class {
|
|
|
316
584
|
}
|
|
317
585
|
async createWebhook(orgId, input) {
|
|
318
586
|
const events = unique(input.events);
|
|
319
|
-
|
|
587
|
+
assertCatalogTriggers(events, false);
|
|
320
588
|
const secret = generateWebhookSecret();
|
|
321
589
|
const automation = await this.uow.run(async ({ automations, credentials, outbox }) => {
|
|
322
590
|
const secretRef = await credentials.store(orgId, { secret });
|
|
@@ -352,7 +620,7 @@ var AutomationsServiceImpl = class {
|
|
|
352
620
|
const existing = await this.findWebhook(orgId, id);
|
|
353
621
|
if (!existing) return null;
|
|
354
622
|
const events = input.events === void 0 ? void 0 : unique(input.events);
|
|
355
|
-
if (events)
|
|
623
|
+
if (events) assertCatalogTriggers(events, false);
|
|
356
624
|
const { secretRef } = webhookTarget(existing);
|
|
357
625
|
const patch = {
|
|
358
626
|
name: input.url,
|
|
@@ -422,11 +690,6 @@ var AutomationsServiceImpl = class {
|
|
|
422
690
|
const automation = await this.repo.findById(orgId, id);
|
|
423
691
|
return automation?.kind === "webhook" ? automation : null;
|
|
424
692
|
}
|
|
425
|
-
assertSubscribableEvents(events) {
|
|
426
|
-
assertAuthorableTriggers(events);
|
|
427
|
-
const known = new Set(catalogTriggers().map((t) => t.type));
|
|
428
|
-
for (const event of events) if (!known.has(event)) throw new InvalidTriggerError(event, "unknown");
|
|
429
|
-
}
|
|
430
693
|
availableActions() {
|
|
431
694
|
return [...catalogActions(), ...this.integrations.available().flatMap((integration) => integration.actions.map((action) => ({
|
|
432
695
|
type: `${integration.id}.${action.id}`,
|