@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.
Files changed (225) hide show
  1. package/dist/assets/events.d.ts +39 -21
  2. package/dist/assets/events.js +1 -1
  3. package/dist/assets/events.js.map +1 -1
  4. package/dist/assets/file-urls.d.ts +20 -0
  5. package/dist/assets/file-urls.js +25 -0
  6. package/dist/assets/file-urls.js.map +1 -0
  7. package/dist/assets/index.d.ts +5 -4
  8. package/dist/assets/index.js +3 -2
  9. package/dist/assets/model.d.ts +2 -2
  10. package/dist/assets/ports.d.ts +45 -15
  11. package/dist/assets/service.d.ts +39 -7
  12. package/dist/assets/service.js +291 -38
  13. package/dist/assets/service.js.map +1 -1
  14. package/dist/automations/actions.d.ts +13 -24
  15. package/dist/automations/actions.js +168 -43
  16. package/dist/automations/actions.js.map +1 -1
  17. package/dist/automations/catalog.d.ts +2 -1
  18. package/dist/automations/catalog.js +229 -13
  19. package/dist/automations/catalog.js.map +1 -1
  20. package/dist/automations/community-derivations.d.ts +14 -0
  21. package/dist/automations/community-derivations.js +439 -0
  22. package/dist/automations/community-derivations.js.map +1 -0
  23. package/dist/automations/community-mail.d.ts +112 -0
  24. package/dist/automations/community-mail.js +90 -0
  25. package/dist/automations/community-mail.js.map +1 -0
  26. package/dist/automations/community-notifications.d.ts +16 -0
  27. package/dist/automations/community-notifications.js +84 -0
  28. package/dist/automations/community-notifications.js.map +1 -0
  29. package/dist/automations/events.d.ts +28 -0
  30. package/dist/automations/index.d.ts +4 -2
  31. package/dist/automations/index.js +3 -2
  32. package/dist/automations/model.d.ts +10 -3
  33. package/dist/automations/model.js +15 -2
  34. package/dist/automations/model.js.map +1 -1
  35. package/dist/automations/ports.d.ts +5 -1
  36. package/dist/automations/schedule.d.ts +6 -0
  37. package/dist/automations/schedule.js +41 -0
  38. package/dist/automations/schedule.js.map +1 -0
  39. package/dist/automations/seeds.d.ts +17 -0
  40. package/dist/automations/seeds.js +235 -0
  41. package/dist/automations/seeds.js.map +1 -0
  42. package/dist/automations/service.d.ts +22 -3
  43. package/dist/automations/service.js +306 -43
  44. package/dist/automations/service.js.map +1 -1
  45. package/dist/content/events.d.ts +631 -19
  46. package/dist/content/events.js +44 -1
  47. package/dist/content/events.js.map +1 -1
  48. package/dist/content/index.d.ts +4 -4
  49. package/dist/content/model.d.ts +2 -2
  50. package/dist/content/ports.d.ts +71 -18
  51. package/dist/content/service.d.ts +57 -18
  52. package/dist/content/service.js +502 -23
  53. package/dist/content/service.js.map +1 -1
  54. package/dist/content/types.d.ts +2 -2
  55. package/dist/discussion/authors.d.ts +10 -0
  56. package/dist/discussion/authors.js +23 -0
  57. package/dist/discussion/authors.js.map +1 -0
  58. package/dist/discussion/chats.d.ts +104 -0
  59. package/dist/discussion/chats.js +798 -0
  60. package/dist/discussion/chats.js.map +1 -0
  61. package/dist/discussion/events.d.ts +619 -56
  62. package/dist/discussion/events.js +106 -3
  63. package/dist/discussion/events.js.map +1 -1
  64. package/dist/discussion/index.d.ts +7 -3
  65. package/dist/discussion/index.js +4 -1
  66. package/dist/discussion/ports.d.ts +288 -27
  67. package/dist/discussion/ports.js +6 -1
  68. package/dist/discussion/ports.js.map +1 -0
  69. package/dist/discussion/posts.d.ts +103 -0
  70. package/dist/discussion/posts.js +723 -0
  71. package/dist/discussion/posts.js.map +1 -0
  72. package/dist/discussion/service.d.ts +32 -10
  73. package/dist/discussion/service.js +293 -78
  74. package/dist/discussion/service.js.map +1 -1
  75. package/dist/entitlements/events.d.ts +178 -11
  76. package/dist/entitlements/events.js +28 -1
  77. package/dist/entitlements/events.js.map +1 -1
  78. package/dist/entitlements/index.d.ts +5 -4
  79. package/dist/entitlements/index.js +2 -1
  80. package/dist/entitlements/model.d.ts +3 -2
  81. package/dist/entitlements/ports.d.ts +124 -11
  82. package/dist/entitlements/rules.d.ts +12 -0
  83. package/dist/entitlements/rules.js +17 -0
  84. package/dist/entitlements/rules.js.map +1 -0
  85. package/dist/entitlements/service.d.ts +54 -3
  86. package/dist/entitlements/service.js +472 -2
  87. package/dist/entitlements/service.js.map +1 -1
  88. package/dist/identity/access.d.ts +6 -0
  89. package/dist/identity/access.js +7 -0
  90. package/dist/identity/access.js.map +1 -0
  91. package/dist/identity/index.d.ts +6 -4
  92. package/dist/identity/index.js +3 -1
  93. package/dist/identity/model.d.ts +2 -1
  94. package/dist/identity/notifications.d.ts +27 -0
  95. package/dist/identity/notifications.js +101 -0
  96. package/dist/identity/notifications.js.map +1 -0
  97. package/dist/identity/ports.d.ts +73 -3
  98. package/dist/identity/service.d.ts +14 -3
  99. package/dist/identity/service.js +62 -5
  100. package/dist/identity/service.js.map +1 -1
  101. package/dist/integrations/model.js +1 -1
  102. package/dist/integrations/model.js.map +1 -1
  103. package/dist/integrations/service.js +2 -2
  104. package/dist/integrations/service.js.map +1 -1
  105. package/dist/organizations/access-scopes.d.ts +19 -0
  106. package/dist/organizations/access-scopes.js +89 -0
  107. package/dist/organizations/access-scopes.js.map +1 -0
  108. package/dist/organizations/events.d.ts +74 -32
  109. package/dist/organizations/events.js +6 -0
  110. package/dist/organizations/events.js.map +1 -1
  111. package/dist/organizations/index.d.ts +7 -6
  112. package/dist/organizations/index.js +4 -3
  113. package/dist/organizations/members.d.ts +8 -5
  114. package/dist/organizations/members.js.map +1 -1
  115. package/dist/organizations/ports.d.ts +14 -11
  116. package/dist/organizations/roles.d.ts +6 -10
  117. package/dist/organizations/roles.js +19 -7
  118. package/dist/organizations/roles.js.map +1 -1
  119. package/dist/organizations/service.d.ts +20 -11
  120. package/dist/organizations/service.js +157 -47
  121. package/dist/organizations/service.js.map +1 -1
  122. package/dist/organizations/types.d.ts +1 -10
  123. package/dist/progress/index.d.ts +2 -2
  124. package/dist/progress/ports.d.ts +6 -1
  125. package/dist/progress/service.d.ts +3 -1
  126. package/dist/progress/service.js +6 -2
  127. package/dist/progress/service.js.map +1 -1
  128. package/dist/reporting/admin/index.d.ts +4 -0
  129. package/dist/reporting/admin/index.js +2 -0
  130. package/dist/reporting/admin/model.d.ts +108 -0
  131. package/dist/reporting/admin/ports.d.ts +27 -0
  132. package/dist/reporting/admin/service.d.ts +22 -0
  133. package/dist/reporting/admin/service.js +38 -0
  134. package/dist/reporting/admin/service.js.map +1 -0
  135. package/dist/reporting/member/index.d.ts +5 -0
  136. package/dist/reporting/member/index.js +2 -0
  137. package/dist/reporting/member/model.d.ts +124 -0
  138. package/dist/reporting/member/ports.d.ts +45 -0
  139. package/dist/reporting/member/service.d.ts +73 -0
  140. package/dist/reporting/member/service.js +286 -0
  141. package/dist/reporting/member/service.js.map +1 -0
  142. package/dist/shared/actor.d.ts +9 -0
  143. package/dist/shared/errors.d.ts +7 -1
  144. package/dist/shared/errors.js +10 -1
  145. package/dist/shared/errors.js.map +1 -1
  146. package/dist/shared/id.d.ts +11 -0
  147. package/dist/shared/id.js +12 -1
  148. package/dist/shared/id.js.map +1 -1
  149. package/dist/shared/mailer.d.ts +4 -1
  150. package/dist/shared/mailer.js.map +1 -1
  151. package/dist/shared/network.d.ts +10 -0
  152. package/dist/shared/network.js +211 -0
  153. package/dist/shared/network.js.map +1 -0
  154. package/dist/types/assets.d.ts +2 -2
  155. package/dist/types/automations.d.ts +10 -2
  156. package/dist/types/content.d.ts +2 -2
  157. package/dist/types/discussion.d.ts +11 -3
  158. package/dist/types/email-templates.d.ts +121 -0
  159. package/dist/types/entitlements.d.ts +2 -2
  160. package/dist/types/identity.d.ts +48 -2
  161. package/dist/types/index.d.ts +9 -9
  162. package/dist/types/ports.d.ts +6 -0
  163. package/dist/types/schemas/assets.d.ts +65 -37
  164. package/dist/types/schemas/assets.js +17 -12
  165. package/dist/types/schemas/assets.js.map +1 -1
  166. package/dist/types/schemas/automations.d.ts +18 -1
  167. package/dist/types/schemas/automations.js +15 -6
  168. package/dist/types/schemas/automations.js.map +1 -1
  169. package/dist/types/schemas/content.d.ts +335 -3
  170. package/dist/types/schemas/content.js +139 -3
  171. package/dist/types/schemas/content.js.map +1 -1
  172. package/dist/types/schemas/discussion.d.ts +370 -3
  173. package/dist/types/schemas/discussion.js +199 -4
  174. package/dist/types/schemas/discussion.js.map +1 -1
  175. package/dist/types/schemas/entitlements.d.ts +36 -2
  176. package/dist/types/schemas/entitlements.js +23 -1
  177. package/dist/types/schemas/entitlements.js.map +1 -1
  178. package/dist/types/schemas/identity.d.ts +111 -1
  179. package/dist/types/schemas/identity.js +55 -2
  180. package/dist/types/schemas/identity.js.map +1 -1
  181. package/dist/types/schemas/index.d.ts +8 -8
  182. package/dist/types/schemas/index.js +8 -8
  183. package/dist/types/schemas/organizations.d.ts +8 -0
  184. package/dist/types/schemas/organizations.js +2 -0
  185. package/dist/types/schemas/organizations.js.map +1 -1
  186. package/dist/types/schemas/shared.d.ts +3 -1
  187. package/dist/types/schemas/shared.js +5 -1
  188. package/dist/types/schemas/shared.js.map +1 -1
  189. package/package.json +2 -2
  190. package/dist/reporting/courses/index.d.ts +0 -4
  191. package/dist/reporting/courses/index.js +0 -2
  192. package/dist/reporting/courses/model.d.ts +0 -24
  193. package/dist/reporting/courses/ports.d.ts +0 -13
  194. package/dist/reporting/courses/service.d.ts +0 -19
  195. package/dist/reporting/courses/service.js +0 -20
  196. package/dist/reporting/courses/service.js.map +0 -1
  197. package/dist/reporting/dashboard/index.d.ts +0 -4
  198. package/dist/reporting/dashboard/index.js +0 -2
  199. package/dist/reporting/dashboard/model.d.ts +0 -14
  200. package/dist/reporting/dashboard/ports.d.ts +0 -13
  201. package/dist/reporting/dashboard/service.d.ts +0 -19
  202. package/dist/reporting/dashboard/service.js +0 -20
  203. package/dist/reporting/dashboard/service.js.map +0 -1
  204. package/dist/reporting/learn/index.d.ts +0 -5
  205. package/dist/reporting/learn/index.js +0 -2
  206. package/dist/reporting/learn/model.d.ts +0 -23
  207. package/dist/reporting/learn/ports.d.ts +0 -43
  208. package/dist/reporting/learn/ports.js +0 -1
  209. package/dist/reporting/learn/service.d.ts +0 -49
  210. package/dist/reporting/learn/service.js +0 -116
  211. package/dist/reporting/learn/service.js.map +0 -1
  212. package/dist/reporting/students/index.d.ts +0 -4
  213. package/dist/reporting/students/index.js +0 -2
  214. package/dist/reporting/students/model.d.ts +0 -51
  215. package/dist/reporting/students/model.js +0 -1
  216. package/dist/reporting/students/ports.d.ts +0 -15
  217. package/dist/reporting/students/ports.js +0 -1
  218. package/dist/reporting/students/service.d.ts +0 -20
  219. package/dist/reporting/students/service.js +0 -23
  220. package/dist/reporting/students/service.js.map +0 -1
  221. /package/dist/reporting/{courses → admin}/model.js +0 -0
  222. /package/dist/reporting/{courses → admin}/ports.js +0 -0
  223. /package/dist/reporting/{dashboard → member}/model.js +0 -0
  224. /package/dist/reporting/{dashboard → member}/ports.js +0 -0
  225. /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 { DELIVER_WEBHOOK_ACTION, catalogActions, catalogTriggers } from "./catalog.js";
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 { AutomationKindMismatchError, InvalidTriggerError, ReservedActionError } from "./model.js";
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) if (trigger.startsWith("automation.")) throw new InvalidTriggerError(trigger);
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.filter((a) => a.enabled)) await this.dispatchOne(automation, event);
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
- error: err instanceof Error ? err.message : String(err)
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
- error: message
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
- error: err instanceof Error ? err.message : String(err),
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
- await executeAction(action, d.event, {
171
- mailer: this.mailer,
172
- lookups: this.lookups,
173
- credentials: this.credentials,
174
- webhooks: this.webhooks
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
- assertAuthorableTriggers(input.triggers);
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) assertAuthorableTriggers(input.triggers);
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) this.logger.info("automation updated", {
243
- orgId,
244
- automationId: id
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) this.logger.info("automation deleted", {
262
- orgId,
263
- automationId: id
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
- return this.repo.findById(orgId, id);
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
- return this.runsRepo.list(orgId, automationId, query);
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
- error: message
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
- this.assertSubscribableEvents(events);
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) this.assertSubscribableEvents(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}`,