@beechcms/api 0.4.0 → 0.4.2

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 (138) hide show
  1. package/assets/dashboard/assets/index-BKWnlvnV.css +1 -0
  2. package/assets/dashboard/assets/index-C1P9BXnU.js +629 -0
  3. package/assets/dashboard/index.html +2 -2
  4. package/migrations/0000_v040_base.sql +25 -0
  5. package/migrations/0029_automations.sql +14 -0
  6. package/package.json +4 -3
  7. package/src/auth/bcrypt-hash-provider.ts +20 -0
  8. package/src/auth/constants.ts +3 -3
  9. package/src/auth/generate-refresh-token.test.ts +19 -0
  10. package/src/auth/hash-provider.test.ts +46 -0
  11. package/src/auth/in-memory-hash-provider.ts +13 -0
  12. package/src/auth/jose-token-service.ts +55 -0
  13. package/src/auth/login.test.ts +92 -0
  14. package/src/auth/login.ts +15 -32
  15. package/src/auth/refresh.ts +0 -122
  16. package/src/auth/static-token-service.ts +18 -0
  17. package/src/auth/token-service.test.ts +82 -0
  18. package/src/factory.ts +80 -80
  19. package/src/features/automations/__tests__/action-executors.test.ts +268 -0
  20. package/src/features/automations/__tests__/automation-runner.test.ts +192 -0
  21. package/src/features/automations/__tests__/automation-runner.utils.test.ts +56 -0
  22. package/src/features/automations/__tests__/automations.handler.test.ts +260 -0
  23. package/src/features/automations/__tests__/automations.repository.test.ts +159 -0
  24. package/src/features/automations/__tests__/automations.schema.test.ts +134 -0
  25. package/src/features/automations/__tests__/context-resolver.test.ts +122 -0
  26. package/src/features/automations/__tests__/cron-runner.test.ts +263 -0
  27. package/src/features/automations/__tests__/cron-runner.utils.test.ts +140 -0
  28. package/src/features/automations/__tests__/set-variable.executor.test.ts +306 -0
  29. package/src/features/automations/__tests__/template-grammar.test.ts +304 -0
  30. package/src/features/automations/__tests__/when-evaluator.test.ts +270 -0
  31. package/src/features/automations/__tests__/when-pushdown.test.ts +277 -0
  32. package/src/features/automations/action-executors/create-entry.executor.ts +23 -0
  33. package/src/features/automations/action-executors/edit-field.executor.ts +22 -0
  34. package/src/features/automations/action-executors/index.ts +33 -0
  35. package/src/features/automations/action-executors/send-mail.executor.ts +42 -0
  36. package/src/features/automations/action-executors/set-variable.executor.ts +146 -0
  37. package/src/features/automations/action-executors/webhook.executor.ts +25 -0
  38. package/src/features/automations/automation-runner.ts +81 -0
  39. package/src/features/automations/automation-runner.utils.ts +43 -0
  40. package/src/features/automations/automations.handler.ts +193 -0
  41. package/src/features/automations/automations.schema.ts +160 -0
  42. package/src/features/automations/context-resolver.ts +148 -0
  43. package/src/features/automations/cron-runner.ts +136 -0
  44. package/src/features/automations/cron-runner.utils.ts +40 -0
  45. package/src/features/automations/filter-translation.ts +42 -0
  46. package/src/features/automations/index.ts +12 -0
  47. package/src/features/automations/template-grammar.ts +241 -0
  48. package/src/features/automations/var-access-resolver.ts +136 -0
  49. package/src/features/automations/when-evaluator.ts +83 -0
  50. package/src/features/automations/when-pushdown.ts +53 -0
  51. package/src/features/content/handlers/create.ts +22 -10
  52. package/src/features/content/handlers/delete.ts +21 -10
  53. package/src/features/content/handlers/update.ts +21 -9
  54. package/src/features/draft/draft.handler.ts +51 -153
  55. package/src/features/draft/draft.middleware.ts +62 -0
  56. package/src/features/email/email.service.ts +13 -0
  57. package/src/features/email/email.types.ts +10 -0
  58. package/src/features/email/index.ts +2 -1
  59. package/src/features/email/templates/automation-mail.ts +15 -0
  60. package/src/features/notifications/notifications.handler.ts +25 -54
  61. package/src/features/password-reset/request.ts +17 -41
  62. package/src/features/password-reset/reset.ts +18 -54
  63. package/src/features/rotate-field/rotate-field.handler.ts +15 -19
  64. package/src/features/schema/schema.handler.ts +1 -1
  65. package/src/features/settings/settings.handler.ts +64 -176
  66. package/src/features/setup/index.ts +12 -17
  67. package/src/features/stats/stats.handler.ts +110 -138
  68. package/src/index.ts +40 -8
  69. package/src/middleware/auth-providers.middleware.ts +32 -0
  70. package/src/middleware/observability.middleware.ts +52 -0
  71. package/src/middleware/rate-limit.middleware.ts +41 -0
  72. package/src/middleware/repository.middleware.ts +72 -5
  73. package/src/middleware.ts +15 -35
  74. package/src/public/cache-utils.ts +34 -0
  75. package/src/public/entry-projection.ts +42 -0
  76. package/src/public/idempotency.ts +19 -0
  77. package/src/public/problem-details.ts +5 -0
  78. package/src/public/public-add.ts +110 -166
  79. package/src/public/public-edit.ts +4 -3
  80. package/src/public/public-read.ts +59 -216
  81. package/src/public/public-routes.ts +2 -2
  82. package/src/public/query-builder.test.ts +220 -0
  83. package/src/public/rate-limit-middleware.ts +7 -19
  84. package/src/public/read-list.ts +50 -0
  85. package/src/public/read-single.ts +44 -0
  86. package/src/rate-limit/cloudflare-rate-limiter.test.ts +26 -0
  87. package/src/rate-limit/cloudflare-rate-limiter.ts +11 -0
  88. package/src/rate-limit/in-memory-rate-limiter.test.ts +33 -0
  89. package/src/rate-limit/in-memory-rate-limiter.ts +13 -0
  90. package/src/rate-limit/no-op-rate-limiter.test.ts +18 -0
  91. package/src/rate-limit/no-op-rate-limiter.ts +7 -0
  92. package/src/search-utils.test.ts +207 -0
  93. package/src/search-utils.ts +18 -1
  94. package/src/search.ts +24 -35
  95. package/src/shared/apply-policies.test.ts +77 -0
  96. package/src/shared/automations.repository.d1.ts +146 -0
  97. package/src/shared/background-notification-service.test.ts +58 -0
  98. package/src/shared/background-notification-service.ts +48 -0
  99. package/src/shared/content-utils.test.ts +161 -0
  100. package/src/shared/content.repository.d1.test.ts +312 -0
  101. package/src/shared/d1-activity-log.repository.test.ts +136 -0
  102. package/src/shared/d1-activity-log.repository.ts +101 -0
  103. package/src/shared/d1-activity-logger.test.ts +82 -0
  104. package/src/shared/d1-activity-logger.ts +63 -0
  105. package/src/shared/d1-analytics.repository.test.ts +74 -0
  106. package/src/shared/d1-analytics.repository.ts +81 -0
  107. package/src/shared/d1-content-scan.repository.test.ts +76 -0
  108. package/src/shared/d1-content-scan.repository.ts +29 -0
  109. package/src/shared/d1-notification.repository.test.ts +124 -0
  110. package/src/shared/d1-notification.repository.ts +114 -0
  111. package/src/shared/d1-password-reset-token.repository.test.ts +77 -0
  112. package/src/shared/d1-password-reset-token.repository.ts +52 -0
  113. package/src/shared/d1-search.repository.test.ts +83 -0
  114. package/src/shared/d1-search.repository.ts +84 -0
  115. package/src/shared/d1-session.repository.test.ts +121 -0
  116. package/src/shared/d1-session.repository.ts +98 -0
  117. package/src/shared/d1-user.repository.test.ts +147 -0
  118. package/src/shared/d1-user.repository.ts +109 -0
  119. package/src/shared/d1-widget.repository.test.ts +217 -0
  120. package/src/shared/d1-widget.repository.ts +337 -0
  121. package/src/shared/execution-context-scheduler.ts +9 -0
  122. package/src/shared/fixed-clock.ts +21 -0
  123. package/src/shared/idempotency.repository.d1.test.ts +79 -0
  124. package/src/shared/in-memory-activity-logger.ts +15 -0
  125. package/src/shared/in-memory-notification-service.ts +15 -0
  126. package/src/shared/media.repository.d1.test.ts +103 -0
  127. package/src/shared/media.repository.d1.ts +1 -1
  128. package/src/shared/request-utils.ts +22 -0
  129. package/src/shared/sequential-id-generator.ts +22 -0
  130. package/src/shared/storage-utils.ts +3 -3
  131. package/src/shared/system-stats.repository.d1.test.ts +54 -0
  132. package/src/types.ts +24 -3
  133. package/src/upload.ts +17 -9
  134. package/src/widget.ts +112 -253
  135. package/assets/dashboard/assets/index-CewtCjom.css +0 -1
  136. package/assets/dashboard/assets/index-FQ6JhvRH.js +0 -554
  137. package/src/shared/activity-logger.ts +0 -79
  138. package/src/shared/notification-service.ts +0 -56
@@ -0,0 +1,136 @@
1
+ import type { Seed } from '@beechcms/core'
2
+ import type { ParsedKey, InlineCondition } from './template-grammar'
3
+ import { materializeCollection } from './action-executors/set-variable.executor'
4
+
5
+ type VarAccessParsed = Extract<ParsedKey, { kind: 'var_access' }>
6
+
7
+ function toNumeric(v: unknown): number {
8
+ const n = Number(v)
9
+ if (Number.isFinite(n)) return n
10
+ if (typeof v === 'string') {
11
+ const d = Date.parse(v)
12
+ if (!Number.isNaN(d)) return d / 1000
13
+ }
14
+ return NaN
15
+ }
16
+
17
+ function numericCoerce(a: unknown, b: unknown): { left: unknown; right: unknown } {
18
+ const na = toNumeric(a)
19
+ const nb = toNumeric(b)
20
+ const numeric = Number.isFinite(na) || Number.isFinite(nb)
21
+ return { left: numeric ? na : a, right: numeric ? nb : b }
22
+ }
23
+
24
+ function applyCondition(record: Record<string, unknown>, cond: InlineCondition): boolean {
25
+ const raw = record[cond.column]
26
+ const { left, right } = numericCoerce(raw, cond.value)
27
+ switch (cond.op) {
28
+ case '=': return left === right
29
+ case '!=': return left !== right
30
+ case '<': return (left as number) < (right as number)
31
+ case '>': return (left as number) > (right as number)
32
+ case '<=': return (left as number) <= (right as number)
33
+ case '>=': return (left as number) >= (right as number)
34
+ }
35
+ }
36
+
37
+ function applyConditions(
38
+ record: Record<string, unknown>,
39
+ conditions: InlineCondition[],
40
+ ): boolean {
41
+ return conditions.every((c) => applyCondition(record, c))
42
+ }
43
+
44
+ function filterItems(
45
+ items: Array<Record<string, unknown>>,
46
+ conditions: InlineCondition[],
47
+ ): Array<Record<string, unknown>> {
48
+ if (conditions.length === 0) return items
49
+ return items.filter((r) => applyConditions(r, conditions))
50
+ }
51
+
52
+ function getItems(value: unknown): Array<Record<string, unknown>> | null {
53
+ if (!value || typeof value !== 'object') return null
54
+ const items = (value as any)['_items']
55
+ return Array.isArray(items) ? items : null
56
+ }
57
+
58
+ const EMPTY_SEED = { branches: [] } as unknown as Seed
59
+
60
+ export function resolveVarAccess(
61
+ parsed: VarAccessParsed,
62
+ variables: Record<string, unknown>,
63
+ onMissing?: (field: string) => void,
64
+ ): unknown {
65
+ if (!(parsed.name in variables)) {
66
+ onMissing?.(parsed.name)
67
+ return undefined
68
+ }
69
+
70
+ let current: unknown = variables[parsed.name]
71
+ const { steps, conditions } = parsed
72
+
73
+ for (let i = 0; i < steps.length; i++) {
74
+ const step = steps[i]
75
+
76
+ // ── array selector ─────────────────────────────────────────────────────
77
+ if (step.type === 'array') {
78
+ const items = getItems(current) ?? (Array.isArray(current) ? current as Array<Record<string, unknown>> : null)
79
+ if (!items) return undefined
80
+ const idSet = new Set(step.ids)
81
+ const subset = items.filter((r) => idSet.has(String(r['id'] ?? '')))
82
+ // Apply conditions to the subset before building the collection
83
+ const filtered = filterItems(subset, conditions)
84
+ current = materializeCollection(EMPTY_SEED, filtered, null)
85
+ // Conditions consumed — no post-filter needed
86
+ continue
87
+ }
88
+
89
+ // ── nav: firstone / lastone ────────────────────────────────────────────
90
+ if (step.type === 'nav') {
91
+ // Nav moves us from a collection to a single record.
92
+ // Conditions are a guard on that record, NOT a pre-filter on _items.
93
+ const record = (current as any)?.[step.nav] as Record<string, unknown> | null | undefined
94
+ if (!record) { current = null; continue }
95
+ // Apply conditions to the single record (post-filter)
96
+ if (conditions.length > 0 && !applyConditions(record, conditions)) return undefined
97
+ current = record
98
+ continue
99
+ }
100
+
101
+ // ── aggregate ──────────────────────────────────────────────────────────
102
+ if (step.type === 'agg') {
103
+ // Apply conditions to _items before aggregating
104
+ const items = getItems(current)
105
+ if (items !== null) {
106
+ const filtered = filterItems(items, conditions)
107
+ current = materializeCollection(EMPTY_SEED, filtered, null)
108
+ }
109
+ if (step.op === 'count') {
110
+ current = (current as any)?.['count']
111
+ } else if (step.field) {
112
+ const sub = (current as any)?.[step.op]
113
+ current = typeof sub === 'object' && sub !== null ? sub[step.field] : sub
114
+ } else {
115
+ current = (current as any)?.[step.op]
116
+ }
117
+ // Conditions consumed by filtering _items
118
+ return current
119
+ }
120
+
121
+ // ── field access ───────────────────────────────────────────────────────
122
+ if (step.type === 'field') {
123
+ current = (current as any)?.[step.name]
124
+ }
125
+ }
126
+
127
+ // Post-filter on single record (conditions not consumed by agg or array)
128
+ if (conditions.length > 0 && current !== null && current !== undefined) {
129
+ if (typeof current === 'object' && !Array.isArray(current)) {
130
+ if (!applyConditions(current as Record<string, unknown>, conditions)) return undefined
131
+ }
132
+ // For scalar values: conditions cannot be checked — pass through
133
+ }
134
+
135
+ return current
136
+ }
@@ -0,0 +1,83 @@
1
+ import type { WhenNode, WhenPredicate, WhenGroup, WhenOperand } from '@beechcms/core'
2
+ import type { ResolvedContext } from './context-resolver'
3
+ import { parseTemplateKey } from './template-grammar'
4
+
5
+ export function evaluateWhen(
6
+ node: WhenNode | null,
7
+ context: ResolvedContext,
8
+ ): boolean {
9
+ if (!node) return true
10
+ if (node.kind === 'predicate') return evalPredicate(node, context)
11
+ return evalGroup(node, context)
12
+ }
13
+
14
+ function evalGroup(group: WhenGroup, context: ResolvedContext): boolean {
15
+ if (group.children.length === 0) {
16
+ const empty = group.op === 'AND'
17
+ return group.negate ? !empty : empty
18
+ }
19
+
20
+ for (const child of group.children) {
21
+ const r = evaluateWhen(child, context)
22
+ if (group.op === 'AND' && !r) return group.negate ? true : false
23
+ if (group.op === 'OR' && r) return group.negate ? false : true
24
+ }
25
+
26
+ const combined = group.op === 'AND'
27
+ return group.negate ? !combined : combined
28
+ }
29
+
30
+ function resolveOperand(operand: WhenOperand, context: ResolvedContext): unknown {
31
+ if (operand.kind === 'literal') return operand.value
32
+
33
+ // this.field (dot notation from UI) → this:field (colon, parsed as scoped key)
34
+ const key = operand.key.startsWith('this.') ? 'this:' + operand.key.slice(5) : operand.key
35
+ const parsed = parseTemplateKey(key)
36
+ if (!parsed) return null
37
+
38
+ const val = context.lookup(parsed)
39
+ return val ?? null
40
+ }
41
+
42
+ function evalPredicate(pred: WhenPredicate, context: ResolvedContext): boolean {
43
+ const left = resolveOperand(pred.left, context)
44
+ const right = pred.right !== undefined ? resolveOperand(pred.right, context) : undefined
45
+ return compare(pred.op, left, right)
46
+ }
47
+
48
+ function compare(op: string, left: unknown, right: unknown): boolean {
49
+ switch (op) {
50
+ case 'isempty': return left == null || left === ''
51
+ case 'isnotempty': return left != null && left !== ''
52
+
53
+ case 'eq': return coerceEqual(left, right)
54
+ case 'neq': return !coerceEqual(left, right)
55
+
56
+ case 'gt': return Number(left) > Number(right)
57
+ case 'gte': return Number(left) >= Number(right)
58
+ case 'lt': return Number(left) < Number(right)
59
+ case 'lte': return Number(left) <= Number(right)
60
+
61
+ case 'contains': return typeof left === 'string' && typeof right === 'string' && left.includes(right)
62
+ case 'startswith': return typeof left === 'string' && typeof right === 'string' && left.startsWith(right)
63
+ case 'endswith': return typeof left === 'string' && typeof right === 'string' && left.endsWith(right)
64
+
65
+ case 'in': return Array.isArray(right) && right.some((r) => coerceEqual(left, r))
66
+ case 'notin': return !Array.isArray(right) || !right.some((r) => coerceEqual(left, r))
67
+
68
+ case 'matches': {
69
+ if (typeof left !== 'string' || typeof right !== 'string') return false
70
+ try { return new RegExp(right).test(left) } catch { return false }
71
+ }
72
+
73
+ default: return false
74
+ }
75
+ }
76
+
77
+ function coerceEqual(a: unknown, b: unknown): boolean {
78
+ if (a === null && b === null) return true
79
+ if (a === null || b === null) return false
80
+ if (typeof a === 'number' && typeof b === 'string' && !Number.isNaN(Number(b))) return a === Number(b)
81
+ if (typeof b === 'number' && typeof a === 'string' && !Number.isNaN(Number(a))) return Number(a) === b
82
+ return a === b
83
+ }
@@ -0,0 +1,53 @@
1
+ import type { WhenNode, WhenPredicate, TriggerCondition, FilterGroup, Seed } from '@beechcms/core'
2
+ import { conditionToFilterGroup } from './filter-translation'
3
+
4
+ const PUSHDOWN_OPS = new Set<string>(['eq', 'neq', 'contains', 'gt', 'lt', 'isempty', 'isnotempty'])
5
+
6
+ export function extractPushdownFilters(
7
+ node: WhenNode | null,
8
+ seed: Seed,
9
+ ): FilterGroup[] {
10
+ if (!node) return []
11
+
12
+ // Single predicate at root
13
+ if (node.kind === 'predicate') {
14
+ const f = tryPushdown(node, seed)
15
+ return f ? [f] : []
16
+ }
17
+
18
+ // Only extract direct predicate children of the outermost AND group (non-negated)
19
+ if (node.kind === 'group' && node.op === 'AND' && !node.negate) {
20
+ return node.children.flatMap((child) => {
21
+ if (child.kind !== 'predicate') return []
22
+ const f = tryPushdown(child, seed)
23
+ return f ? [f] : []
24
+ })
25
+ }
26
+
27
+ return []
28
+ }
29
+
30
+ function tryPushdown(pred: WhenPredicate, seed: Seed): FilterGroup | null {
31
+ if (pred.left.kind !== 'ref') return null
32
+
33
+ const key = pred.left.key
34
+ if (!key.startsWith('this.')) return null
35
+ const field = key.slice(5)
36
+ if (!field) return null
37
+
38
+ if (!PUSHDOWN_OPS.has(pred.op)) return null
39
+
40
+ if (pred.op === 'isempty' || pred.op === 'isnotempty') {
41
+ return conditionToFilterGroup(
42
+ { field, op: pred.op as TriggerCondition['op'], value: null },
43
+ seed,
44
+ )
45
+ }
46
+
47
+ if (!pred.right || pred.right.kind !== 'literal') return null
48
+
49
+ return conditionToFilterGroup(
50
+ { field, op: pred.op as TriggerCondition['op'], value: pred.right.value },
51
+ seed,
52
+ )
53
+ }
@@ -8,7 +8,6 @@ import {
8
8
  import { applyPrivacy, PrivacyPolicyError } from '../../../shared/apply-policies'
9
9
  import { publicProblem } from '../../../public/problem-details'
10
10
  import { CONTENT_ERRORS } from '../constants'
11
- import { logActivity } from '../../../shared/activity-logger'
12
11
  import { cleanStr } from '../../../shared/query-utils'
13
12
  import { AppEnv } from '../../../types'
14
13
 
@@ -110,7 +109,7 @@ export async function createHandler(context: Context<AppEnv>) {
110
109
  throw error
111
110
  }
112
111
 
113
- const id = crypto.randomUUID()
112
+ const id = context.get('idGenerator').uuid()
114
113
  let finalSlug = entrySlug
115
114
  if (!finalSlug) {
116
115
  const fallbackSource = privacyData[seed.displayNameAlias ?? 'title'] || privacyData.title || privacyData.name || id
@@ -121,17 +120,30 @@ export async function createHandler(context: Context<AppEnv>) {
121
120
  const repository = context.get('repository')
122
121
  await repository.create(seed, id, finalSlug, status, privacyData)
123
122
 
124
- const userId = context.get('jwtPayload')?.sub
123
+ const jwtPayload = context.get('jwtPayload')
125
124
  const title = privacyData.title || privacyData.name || finalSlug
126
-
127
- logActivity(context, {
128
- action: 'create',
129
- entityType: 'content',
130
- entityId: id,
131
- entitySlug: slug,
132
- details: { title }
125
+
126
+ context.get('activityLogger').log({
127
+ action: 'create',
128
+ entityType: 'content',
129
+ entityId: id,
130
+ entitySlug: slug,
131
+ details: { title },
132
+ actor: {
133
+ id: jwtPayload.sub,
134
+ email: jwtPayload.email ?? 'unknown',
135
+ name: jwtPayload.name ?? null,
136
+ },
133
137
  })
134
138
 
139
+ context.get('scheduler').waitUntil(
140
+ context.get('automationRunner').run({
141
+ seedSlug: slug,
142
+ event: 'create',
143
+ entry: { id, slug: finalSlug, status, ...privacyData },
144
+ }),
145
+ )
146
+
135
147
  return context.json({ id }, 201)
136
148
  } catch (error) {
137
149
  if (error instanceof SlugConflictError) {
@@ -4,7 +4,6 @@ import { deleteR2Objects } from '../../../upload'
4
4
  import { extractMediaKeysFromData } from '../../../media-utils'
5
5
  import { publicProblem } from '../../../public/problem-details'
6
6
  import { CONTENT_ERRORS } from '../constants'
7
- import { logActivity } from '../../../shared/activity-logger'
8
7
  import { AppEnv } from '../../../types'
9
8
 
10
9
  export async function deleteHandler(context: Context<AppEnv>) {
@@ -34,18 +33,30 @@ export async function deleteHandler(context: Context<AppEnv>) {
34
33
  // Repository.delete returns the row data for cleanup
35
34
  const { row } = await repository.delete(seed, entryId)
36
35
 
37
- const userId = context.get('jwtPayload')?.sub
36
+ const jwtPayload = context.get('jwtPayload')
38
37
  const title = row.title || row.name || entryId
39
-
40
- logActivity(context, {
41
- action: 'delete',
42
- entityType: 'content',
43
- entityId: entryId,
44
- entitySlug: schemaSlug,
45
- details: { title }
38
+
39
+ context.get('activityLogger').log({
40
+ action: 'delete',
41
+ entityType: 'content',
42
+ entityId: entryId,
43
+ entitySlug: schemaSlug,
44
+ details: { title },
45
+ actor: {
46
+ id: jwtPayload.sub,
47
+ email: jwtPayload.email ?? 'unknown',
48
+ name: jwtPayload.name ?? null,
49
+ },
46
50
  })
47
51
 
48
-
52
+ context.get('scheduler').waitUntil(
53
+ context.get('automationRunner').run({
54
+ seedSlug: schemaSlug,
55
+ event: 'delete',
56
+ entry: { ...row, id: entryId },
57
+ }),
58
+ )
59
+
49
60
  const r2ObjectKeys = extractMediaKeysFromData(seed, row)
50
61
  if (r2ObjectKeys.length > 0) {
51
62
  await deleteR2Objects(context, r2ObjectKeys).catch((error) => {
@@ -10,7 +10,6 @@ import {
10
10
  import { applyPrivacy, PrivacyPolicyError } from '../../../shared/apply-policies'
11
11
  import { publicProblem } from '../../../public/problem-details'
12
12
  import { CONTENT_ERRORS } from '../constants'
13
- import { logActivity } from '../../../shared/activity-logger'
14
13
  import { cleanStr } from '../../../shared/query-utils'
15
14
  import { AppEnv } from '../../../types'
16
15
 
@@ -167,17 +166,30 @@ export async function updateHandler(context: Context<AppEnv>) {
167
166
 
168
167
  await repository.update(seed, id, mergedData, newStatus)
169
168
 
170
- const userId = context.get('jwtPayload')?.sub
169
+ const jwtPayload = context.get('jwtPayload')
171
170
  const title = mergedData.title || mergedData.name || newSlug
172
-
173
- logActivity(context, {
174
- action: 'update',
175
- entityType: 'content',
176
- entityId: id,
177
- entitySlug: slug,
178
- details: { title }
171
+
172
+ context.get('activityLogger').log({
173
+ action: 'update',
174
+ entityType: 'content',
175
+ entityId: id,
176
+ entitySlug: slug,
177
+ details: { title },
178
+ actor: {
179
+ id: jwtPayload.sub,
180
+ email: jwtPayload.email ?? 'unknown',
181
+ name: jwtPayload.name ?? null,
182
+ },
179
183
  })
180
184
 
185
+ context.get('scheduler').waitUntil(
186
+ context.get('automationRunner').run({
187
+ seedSlug: slug,
188
+ event: 'update',
189
+ entry: { ...current, ...mergedData, id, status: newStatus },
190
+ }),
191
+ )
192
+
181
193
  return context.json({ success: true })
182
194
  } catch (error) {
183
195
  if (error instanceof EntryNotFoundError) {