@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.
- package/assets/dashboard/assets/index-BKWnlvnV.css +1 -0
- package/assets/dashboard/assets/index-C1P9BXnU.js +629 -0
- package/assets/dashboard/index.html +2 -2
- package/migrations/0000_v040_base.sql +25 -0
- package/migrations/0029_automations.sql +14 -0
- package/package.json +4 -3
- package/src/auth/bcrypt-hash-provider.ts +20 -0
- package/src/auth/constants.ts +3 -3
- package/src/auth/generate-refresh-token.test.ts +19 -0
- package/src/auth/hash-provider.test.ts +46 -0
- package/src/auth/in-memory-hash-provider.ts +13 -0
- package/src/auth/jose-token-service.ts +55 -0
- package/src/auth/login.test.ts +92 -0
- package/src/auth/login.ts +15 -32
- package/src/auth/refresh.ts +0 -122
- package/src/auth/static-token-service.ts +18 -0
- package/src/auth/token-service.test.ts +82 -0
- package/src/factory.ts +80 -80
- package/src/features/automations/__tests__/action-executors.test.ts +268 -0
- package/src/features/automations/__tests__/automation-runner.test.ts +192 -0
- package/src/features/automations/__tests__/automation-runner.utils.test.ts +56 -0
- package/src/features/automations/__tests__/automations.handler.test.ts +260 -0
- package/src/features/automations/__tests__/automations.repository.test.ts +159 -0
- package/src/features/automations/__tests__/automations.schema.test.ts +134 -0
- package/src/features/automations/__tests__/context-resolver.test.ts +122 -0
- package/src/features/automations/__tests__/cron-runner.test.ts +263 -0
- package/src/features/automations/__tests__/cron-runner.utils.test.ts +140 -0
- package/src/features/automations/__tests__/set-variable.executor.test.ts +306 -0
- package/src/features/automations/__tests__/template-grammar.test.ts +304 -0
- package/src/features/automations/__tests__/when-evaluator.test.ts +270 -0
- package/src/features/automations/__tests__/when-pushdown.test.ts +277 -0
- package/src/features/automations/action-executors/create-entry.executor.ts +23 -0
- package/src/features/automations/action-executors/edit-field.executor.ts +22 -0
- package/src/features/automations/action-executors/index.ts +33 -0
- package/src/features/automations/action-executors/send-mail.executor.ts +42 -0
- package/src/features/automations/action-executors/set-variable.executor.ts +146 -0
- package/src/features/automations/action-executors/webhook.executor.ts +25 -0
- package/src/features/automations/automation-runner.ts +81 -0
- package/src/features/automations/automation-runner.utils.ts +43 -0
- package/src/features/automations/automations.handler.ts +193 -0
- package/src/features/automations/automations.schema.ts +160 -0
- package/src/features/automations/context-resolver.ts +148 -0
- package/src/features/automations/cron-runner.ts +136 -0
- package/src/features/automations/cron-runner.utils.ts +40 -0
- package/src/features/automations/filter-translation.ts +42 -0
- package/src/features/automations/index.ts +12 -0
- package/src/features/automations/template-grammar.ts +241 -0
- package/src/features/automations/var-access-resolver.ts +136 -0
- package/src/features/automations/when-evaluator.ts +83 -0
- package/src/features/automations/when-pushdown.ts +53 -0
- package/src/features/content/handlers/create.ts +22 -10
- package/src/features/content/handlers/delete.ts +21 -10
- package/src/features/content/handlers/update.ts +21 -9
- package/src/features/draft/draft.handler.ts +51 -153
- package/src/features/draft/draft.middleware.ts +62 -0
- package/src/features/email/email.service.ts +13 -0
- package/src/features/email/email.types.ts +10 -0
- package/src/features/email/index.ts +2 -1
- package/src/features/email/templates/automation-mail.ts +15 -0
- package/src/features/notifications/notifications.handler.ts +25 -54
- package/src/features/password-reset/request.ts +17 -41
- package/src/features/password-reset/reset.ts +18 -54
- package/src/features/rotate-field/rotate-field.handler.ts +15 -19
- package/src/features/schema/schema.handler.ts +1 -1
- package/src/features/settings/settings.handler.ts +64 -176
- package/src/features/setup/index.ts +12 -17
- package/src/features/stats/stats.handler.ts +110 -138
- package/src/index.ts +40 -8
- package/src/middleware/auth-providers.middleware.ts +32 -0
- package/src/middleware/observability.middleware.ts +52 -0
- package/src/middleware/rate-limit.middleware.ts +41 -0
- package/src/middleware/repository.middleware.ts +72 -5
- package/src/middleware.ts +15 -35
- package/src/public/cache-utils.ts +34 -0
- package/src/public/entry-projection.ts +42 -0
- package/src/public/idempotency.ts +19 -0
- package/src/public/problem-details.ts +5 -0
- package/src/public/public-add.ts +110 -166
- package/src/public/public-edit.ts +4 -3
- package/src/public/public-read.ts +59 -216
- package/src/public/public-routes.ts +2 -2
- package/src/public/query-builder.test.ts +220 -0
- package/src/public/rate-limit-middleware.ts +7 -19
- package/src/public/read-list.ts +50 -0
- package/src/public/read-single.ts +44 -0
- package/src/rate-limit/cloudflare-rate-limiter.test.ts +26 -0
- package/src/rate-limit/cloudflare-rate-limiter.ts +11 -0
- package/src/rate-limit/in-memory-rate-limiter.test.ts +33 -0
- package/src/rate-limit/in-memory-rate-limiter.ts +13 -0
- package/src/rate-limit/no-op-rate-limiter.test.ts +18 -0
- package/src/rate-limit/no-op-rate-limiter.ts +7 -0
- package/src/search-utils.test.ts +207 -0
- package/src/search-utils.ts +18 -1
- package/src/search.ts +24 -35
- package/src/shared/apply-policies.test.ts +77 -0
- package/src/shared/automations.repository.d1.ts +146 -0
- package/src/shared/background-notification-service.test.ts +58 -0
- package/src/shared/background-notification-service.ts +48 -0
- package/src/shared/content-utils.test.ts +161 -0
- package/src/shared/content.repository.d1.test.ts +312 -0
- package/src/shared/d1-activity-log.repository.test.ts +136 -0
- package/src/shared/d1-activity-log.repository.ts +101 -0
- package/src/shared/d1-activity-logger.test.ts +82 -0
- package/src/shared/d1-activity-logger.ts +63 -0
- package/src/shared/d1-analytics.repository.test.ts +74 -0
- package/src/shared/d1-analytics.repository.ts +81 -0
- package/src/shared/d1-content-scan.repository.test.ts +76 -0
- package/src/shared/d1-content-scan.repository.ts +29 -0
- package/src/shared/d1-notification.repository.test.ts +124 -0
- package/src/shared/d1-notification.repository.ts +114 -0
- package/src/shared/d1-password-reset-token.repository.test.ts +77 -0
- package/src/shared/d1-password-reset-token.repository.ts +52 -0
- package/src/shared/d1-search.repository.test.ts +83 -0
- package/src/shared/d1-search.repository.ts +84 -0
- package/src/shared/d1-session.repository.test.ts +121 -0
- package/src/shared/d1-session.repository.ts +98 -0
- package/src/shared/d1-user.repository.test.ts +147 -0
- package/src/shared/d1-user.repository.ts +109 -0
- package/src/shared/d1-widget.repository.test.ts +217 -0
- package/src/shared/d1-widget.repository.ts +337 -0
- package/src/shared/execution-context-scheduler.ts +9 -0
- package/src/shared/fixed-clock.ts +21 -0
- package/src/shared/idempotency.repository.d1.test.ts +79 -0
- package/src/shared/in-memory-activity-logger.ts +15 -0
- package/src/shared/in-memory-notification-service.ts +15 -0
- package/src/shared/media.repository.d1.test.ts +103 -0
- package/src/shared/media.repository.d1.ts +1 -1
- package/src/shared/request-utils.ts +22 -0
- package/src/shared/sequential-id-generator.ts +22 -0
- package/src/shared/storage-utils.ts +3 -3
- package/src/shared/system-stats.repository.d1.test.ts +54 -0
- package/src/types.ts +24 -3
- package/src/upload.ts +17 -9
- package/src/widget.ts +112 -253
- package/assets/dashboard/assets/index-CewtCjom.css +0 -1
- package/assets/dashboard/assets/index-FQ6JhvRH.js +0 -554
- package/src/shared/activity-logger.ts +0 -79
- 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 =
|
|
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
|
|
123
|
+
const jwtPayload = context.get('jwtPayload')
|
|
125
124
|
const title = privacyData.title || privacyData.name || finalSlug
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
36
|
+
const jwtPayload = context.get('jwtPayload')
|
|
38
37
|
const title = row.title || row.name || entryId
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
169
|
+
const jwtPayload = context.get('jwtPayload')
|
|
171
170
|
const title = mergedData.title || mergedData.name || newSlug
|
|
172
|
-
|
|
173
|
-
|
|
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) {
|