@beechcms/api 0.5.0 → 0.6.0-preview.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 (225) hide show
  1. package/assets/dashboard/BeechLogo.svg +18 -18
  2. package/assets/dashboard/BeechLogoLIght.svg +48 -48
  3. package/assets/dashboard/Searching.svg +1 -0
  4. package/assets/dashboard/assets/chart-BrJFdsGZ.js +39 -0
  5. package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
  6. package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
  7. package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
  8. package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
  9. package/assets/dashboard/beechLogoDark.svg +48 -48
  10. package/assets/dashboard/index.html +18 -18
  11. package/assets/dashboard/noResult.svg +43 -0
  12. package/assets/dashboard/sol.svg +3 -3
  13. package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
  14. package/assets/dashboard/working.svg +1 -0
  15. package/migrations/0000_v040_base.sql +265 -238
  16. package/migrations/0029_automations.sql +14 -14
  17. package/migrations/0030_test_seeds.sql +125 -125
  18. package/package.json +13 -9
  19. package/src/auth/{in-memory-hash-provider.ts → __fixtures__/in-memory-hash-provider.ts} +17 -13
  20. package/src/auth/{static-token-service.ts → __fixtures__/static-token-service.ts} +22 -18
  21. package/src/auth/bcrypt-hash-provider.ts +24 -20
  22. package/src/auth/constants.ts +14 -10
  23. package/src/auth/generate-refresh-token.test.ts +23 -19
  24. package/src/auth/hash-provider.test.ts +55 -46
  25. package/src/auth/jose-token-service.ts +62 -55
  26. package/src/auth/jwt-claims-passthrough.test.ts +87 -0
  27. package/src/auth/login.test.ts +100 -92
  28. package/src/auth/login.ts +78 -74
  29. package/src/auth/refresh.ts +11 -5
  30. package/src/auth/token-service.test.ts +90 -82
  31. package/src/factory.ts +370 -347
  32. package/src/features/automations/__tests__/action-executors.test.ts +255 -268
  33. package/src/features/automations/__tests__/automation-runner.test.ts +196 -192
  34. package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -56
  35. package/src/features/automations/__tests__/automations.handler.test.ts +264 -260
  36. package/src/features/automations/__tests__/automations.repository.test.ts +163 -159
  37. package/src/features/automations/__tests__/automations.schema.test.ts +224 -134
  38. package/src/features/automations/__tests__/context-resolver.test.ts +126 -122
  39. package/src/features/automations/__tests__/cron-runner.test.ts +267 -263
  40. package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -140
  41. package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -306
  42. package/src/features/automations/__tests__/template-grammar.test.ts +308 -304
  43. package/src/features/automations/__tests__/webhook.executor.test.ts +142 -0
  44. package/src/features/automations/__tests__/when-evaluator.test.ts +274 -270
  45. package/src/features/automations/__tests__/when-pushdown.test.ts +281 -277
  46. package/src/features/automations/action-executors/create-entry.executor.ts +27 -23
  47. package/src/features/automations/action-executors/edit-field.executor.ts +26 -22
  48. package/src/features/automations/action-executors/index.ts +37 -33
  49. package/src/features/automations/action-executors/send-mail.executor.ts +53 -42
  50. package/src/features/automations/action-executors/set-variable.executor.ts +159 -146
  51. package/src/features/automations/action-executors/webhook.executor.ts +67 -25
  52. package/src/features/automations/automation-runner.ts +85 -81
  53. package/src/features/automations/automation-runner.utils.ts +47 -43
  54. package/src/features/automations/automations.handler.ts +197 -193
  55. package/src/features/automations/automations.schema.ts +177 -160
  56. package/src/features/automations/context-resolver.ts +152 -148
  57. package/src/features/automations/cron-runner.ts +140 -136
  58. package/src/features/automations/cron-runner.utils.ts +44 -40
  59. package/src/features/automations/filter-translation.ts +46 -42
  60. package/src/features/automations/index.ts +16 -12
  61. package/src/features/automations/template-grammar.ts +245 -241
  62. package/src/features/automations/var-access-resolver.ts +140 -136
  63. package/src/features/automations/when-evaluator.ts +87 -83
  64. package/src/features/automations/when-pushdown.ts +57 -53
  65. package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -0
  66. package/src/features/backrefs/backrefs.handler.ts +168 -0
  67. package/src/features/backrefs/d1-backref.repository.ts +84 -0
  68. package/src/features/backrefs/index.ts +5 -0
  69. package/src/features/content/constants.ts +16 -10
  70. package/src/features/content/handlers/bulk.handler.ts +185 -0
  71. package/src/features/content/handlers/create.ts +129 -165
  72. package/src/features/content/handlers/delete.ts +62 -87
  73. package/src/features/content/handlers/facets.ts +49 -45
  74. package/src/features/content/handlers/get.ts +120 -116
  75. package/src/features/content/handlers/helpers.test.ts +180 -0
  76. package/src/features/content/handlers/helpers.ts +93 -0
  77. package/src/features/content/handlers/list.ts +174 -88
  78. package/src/features/content/handlers/update.ts +174 -219
  79. package/src/features/content/index.ts +26 -20
  80. package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
  81. package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
  82. package/src/features/dashboard-layout/index.ts +5 -0
  83. package/src/features/draft/draft.handler.ts +195 -170
  84. package/src/features/draft/draft.middleware.ts +66 -62
  85. package/src/features/draft/index.ts +5 -1
  86. package/src/features/email/email.provider.ts +42 -38
  87. package/src/features/email/email.service.ts +98 -93
  88. package/src/features/email/email.types.ts +119 -108
  89. package/src/features/email/index.ts +33 -29
  90. package/src/features/email/providers/resend.ts +67 -63
  91. package/src/features/email/providers/smtp.ts +55 -0
  92. package/src/features/email/templates/automation-mail.ts +19 -15
  93. package/src/features/email/templates/password-changed.ts +63 -59
  94. package/src/features/email/templates/password-reset.ts +68 -64
  95. package/src/features/email/templates/shell.ts +96 -92
  96. package/src/features/notifications/index.ts +5 -1
  97. package/src/features/notifications/notifications.handler.ts +105 -101
  98. package/src/features/password-reset/index.ts +19 -15
  99. package/src/features/password-reset/request.ts +92 -82
  100. package/src/features/password-reset/reset.ts +102 -92
  101. package/src/features/rotate-field/index.ts +5 -1
  102. package/src/features/rotate-field/rotate-field.handler.ts +132 -128
  103. package/src/features/rotate-field/rotate-field.schema.ts +17 -13
  104. package/src/features/schema/schema.handler.ts +131 -16
  105. package/src/features/seeds/index.ts +5 -0
  106. package/src/features/seeds/seeds.handler.test.ts +1199 -0
  107. package/src/features/seeds/seeds.handler.ts +558 -0
  108. package/src/features/settings/__tests__/settings.handler.test.ts +555 -0
  109. package/src/features/settings/settings.handler.ts +392 -302
  110. package/src/features/setup/index.ts +288 -91
  111. package/src/features/stats/index.ts +5 -1
  112. package/src/features/stats/stats.handler.ts +420 -430
  113. package/src/features/webhooks/index.ts +63 -0
  114. package/src/index.ts +65 -56
  115. package/src/media-utils.ts +82 -78
  116. package/src/middleware/auth-providers.middleware.test.ts +85 -0
  117. package/src/middleware/auth-providers.middleware.ts +45 -32
  118. package/src/middleware/observability.middleware.test.ts +93 -0
  119. package/src/middleware/observability.middleware.ts +70 -52
  120. package/src/middleware/rate-limit.middleware.ts +45 -41
  121. package/src/middleware/repository.middleware.ts +117 -91
  122. package/src/middleware/seed-registry.middleware.test.ts +97 -0
  123. package/src/middleware/seed-registry.middleware.ts +21 -0
  124. package/src/middleware/storage.middleware.ts +25 -21
  125. package/src/middleware.ts +45 -47
  126. package/src/public/access-policy.ts +27 -23
  127. package/src/public/api-key-middleware.ts +57 -53
  128. package/src/public/cache-utils.ts +38 -34
  129. package/src/public/entry-projection.ts +46 -42
  130. package/src/public/idempotency.ts +23 -19
  131. package/src/public/index.ts +16 -12
  132. package/src/public/problem-details.ts +124 -53
  133. package/src/public/public-add.ts +114 -110
  134. package/src/public/public-edit.ts +163 -159
  135. package/src/public/public-errors.ts +19 -15
  136. package/src/public/public-read.ts +63 -59
  137. package/src/public/public-routes.ts +88 -84
  138. package/src/public/query-builder.test.ts +224 -220
  139. package/src/public/query-builder.ts +156 -152
  140. package/src/public/rate-limit-middleware.ts +34 -30
  141. package/src/public/read-list.ts +54 -50
  142. package/src/public/read-single.ts +48 -44
  143. package/src/public/response-builder.ts +30 -26
  144. package/src/public/sanitize.ts +69 -65
  145. package/src/public/slug-utils.ts +18 -14
  146. package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -26
  147. package/src/rate-limit/cloudflare-rate-limiter.ts +15 -11
  148. package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -33
  149. package/src/rate-limit/in-memory-rate-limiter.ts +17 -13
  150. package/src/rate-limit/no-op-rate-limiter.test.ts +22 -18
  151. package/src/rate-limit/no-op-rate-limiter.ts +11 -7
  152. package/src/search-utils.test.ts +211 -207
  153. package/src/search-utils.ts +213 -209
  154. package/src/search.ts +65 -61
  155. package/src/shared/apply-policies.test.ts +81 -77
  156. package/src/shared/apply-policies.ts +67 -63
  157. package/src/shared/automations.repository.d1.ts +150 -146
  158. package/src/shared/background-notification-service.test.ts +62 -58
  159. package/src/shared/background-notification-service.ts +52 -48
  160. package/src/shared/base.repository.d1.ts +32 -28
  161. package/src/shared/content-utils.test.ts +165 -161
  162. package/src/shared/content-utils.ts +86 -82
  163. package/src/shared/content.repository.d1.test.ts +507 -312
  164. package/src/shared/content.repository.d1.ts +851 -382
  165. package/src/shared/d1-activity-log.repository.test.ts +140 -136
  166. package/src/shared/d1-activity-log.repository.ts +105 -101
  167. package/src/shared/d1-activity-logger.test.ts +86 -82
  168. package/src/shared/d1-activity-logger.ts +67 -63
  169. package/src/shared/d1-analytics.repository.test.ts +78 -74
  170. package/src/shared/d1-analytics.repository.ts +85 -81
  171. package/src/shared/d1-content-scan.repository.test.ts +80 -76
  172. package/src/shared/d1-content-scan.repository.ts +37 -29
  173. package/src/shared/d1-notification.repository.test.ts +128 -124
  174. package/src/shared/d1-notification.repository.ts +118 -114
  175. package/src/shared/d1-password-reset-token.repository.test.ts +81 -77
  176. package/src/shared/d1-password-reset-token.repository.ts +56 -52
  177. package/src/shared/d1-search.repository.test.ts +87 -83
  178. package/src/shared/d1-search.repository.ts +88 -84
  179. package/src/shared/d1-session.repository.test.ts +125 -121
  180. package/src/shared/d1-session.repository.ts +102 -98
  181. package/src/shared/d1-setup-checklist.repository.ts +36 -0
  182. package/src/shared/d1-user.repository.test.ts +151 -147
  183. package/src/shared/d1-user.repository.ts +119 -109
  184. package/src/shared/d1-widget.repository.test.ts +267 -217
  185. package/src/shared/d1-widget.repository.ts +385 -337
  186. package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
  187. package/src/shared/dashboard-layout.repository.d1.ts +58 -0
  188. package/src/shared/demo-data-sql.ts +216 -0
  189. package/src/shared/demo-data.repository.d1.ts +20 -0
  190. package/src/shared/execution-context-scheduler.ts +13 -9
  191. package/src/shared/fixed-clock.ts +25 -21
  192. package/src/shared/fts-sync.ts +8 -4
  193. package/src/shared/idempotency.repository.d1.test.ts +83 -79
  194. package/src/shared/idempotency.repository.d1.ts +60 -56
  195. package/src/shared/in-memory-activity-logger.ts +19 -15
  196. package/src/shared/in-memory-notification-service.ts +19 -15
  197. package/src/shared/in-memory-seed.repository.ts +58 -0
  198. package/src/shared/media.repository.d1.test.ts +107 -103
  199. package/src/shared/media.repository.d1.ts +68 -64
  200. package/src/shared/qstash-notification-service.test.ts +67 -0
  201. package/src/shared/qstash-notification-service.ts +55 -0
  202. package/src/shared/query-utils.ts +141 -137
  203. package/src/shared/request-utils.ts +26 -22
  204. package/src/shared/schema-mutator.d1.test.ts +142 -0
  205. package/src/shared/schema-mutator.d1.ts +64 -0
  206. package/src/shared/seed-layout.repository.d1.test.ts +114 -0
  207. package/src/shared/seed-layout.repository.d1.ts +62 -0
  208. package/src/shared/seed-registry-cache.test.ts +68 -0
  209. package/src/shared/seed-registry-cache.ts +49 -0
  210. package/src/shared/seed.repository.d1.test.ts +143 -0
  211. package/src/shared/seed.repository.d1.ts +98 -0
  212. package/src/shared/sequential-id-generator.ts +33 -22
  213. package/src/shared/site-settings.repository.d1.ts +53 -0
  214. package/src/shared/storage/factory.ts +41 -40
  215. package/src/shared/storage/s3-bucket.ts +195 -163
  216. package/src/shared/storage-utils.ts +40 -36
  217. package/src/shared/system-stats.repository.d1.test.ts +58 -54
  218. package/src/shared/system-stats.repository.d1.ts +48 -44
  219. package/src/types.ts +90 -67
  220. package/src/upload.ts +198 -187
  221. package/src/widget.test.ts +85 -0
  222. package/src/widget.ts +251 -208
  223. package/assets/dashboard/assets/index-B1mUfgiK.css +0 -1
  224. package/assets/dashboard/assets/index-CHxxc_id.js +0 -629
  225. package/src/shared/storage/r2-binding-bucket.ts +0 -81
@@ -1,159 +1,163 @@
1
- import { describe, it, expect, vi } from 'vitest'
2
- import { D1AutomationRepository } from '../../../shared/automations.repository.d1'
3
-
4
- function makeDb() {
5
- const calls: { sql: string; bindings: unknown[] }[] = []
6
- let nextAllResult: unknown[] = []
7
- let nextFirstResult: unknown = null
8
-
9
- const db = {
10
- _calls: calls,
11
- _setAllResult(rows: unknown[]) { nextAllResult = rows },
12
- _setFirstResult(row: unknown) { nextFirstResult = row },
13
- prepare(sql: string) {
14
- let boundValues: unknown[] = []
15
- const stmt = {
16
- bind(...args: unknown[]) { boundValues = args; return stmt },
17
- async run() {
18
- calls.push({ sql, bindings: boundValues })
19
- return { success: true }
20
- },
21
- async all() {
22
- calls.push({ sql, bindings: boundValues })
23
- return { results: nextAllResult }
24
- },
25
- async first() {
26
- calls.push({ sql, bindings: boundValues })
27
- return nextFirstResult
28
- },
29
- }
30
- return stmt
31
- },
32
- }
33
- return db as unknown as D1Database & typeof db
34
- }
35
-
36
- const fixtureRow = {
37
- id: 'abc',
38
- seed_slug: 'posts',
39
- name: 'test',
40
- enabled: 1,
41
- triggers: JSON.stringify([{ event: 'create' }]),
42
- trigger_conditions: null,
43
- actions: JSON.stringify([{ type: 'webhook', url: 'https://example.com' }]),
44
- created_at: 1000,
45
- updated_at: 1000,
46
- }
47
-
48
- describe('D1AutomationRepository.create', () => {
49
- it('serialises actions and trigger_conditions to JSON', async () => {
50
- const db = makeDb()
51
- const repo = new D1AutomationRepository(db)
52
- const whenNode = {
53
- kind: 'group' as const,
54
- op: 'AND' as const,
55
- children: [{ kind: 'predicate' as const, left: { kind: 'ref' as const, key: 'this.status' }, op: 'eq' as const, right: { kind: 'literal' as const, value: 'published' } }],
56
- }
57
- await repo.create({
58
- seed_slug: 'posts',
59
- name: 'test',
60
- triggers: [{ event: 'create' }],
61
- trigger_conditions: whenNode,
62
- actions: [{ type: 'webhook', url: 'https://example.com' }],
63
- })
64
- expect(db._calls).toHaveLength(1)
65
- const { bindings } = db._calls[0]
66
- // triggers is index 3, trigger_conditions is index 4, actions is index 5
67
- expect(typeof bindings[3]).toBe('string')
68
- expect(JSON.parse(bindings[3] as string)).toEqual([{ event: 'create' }])
69
- expect(typeof bindings[4]).toBe('string')
70
- expect(JSON.parse(bindings[4] as string)).toEqual(whenNode)
71
- expect(typeof bindings[5]).toBe('string')
72
- expect(JSON.parse(bindings[5] as string)).toEqual([{ type: 'webhook', url: 'https://example.com' }])
73
- })
74
-
75
- it('stores null for missing trigger_conditions', async () => {
76
- const db = makeDb()
77
- const repo = new D1AutomationRepository(db)
78
- await repo.create({
79
- seed_slug: 'posts',
80
- name: 'test',
81
- triggers: [{ event: 'create' }],
82
- trigger_conditions: null,
83
- actions: [{ type: 'webhook', url: 'https://example.com' }],
84
- })
85
- const { bindings } = db._calls[0]
86
- expect(bindings[4]).toBeNull()
87
- })
88
- })
89
-
90
- describe('D1AutomationRepository.update', () => {
91
- it('builds SET clause only from provided keys', async () => {
92
- const db = makeDb()
93
- const repo = new D1AutomationRepository(db)
94
- await repo.update('abc', { name: 'renamed' })
95
- expect(db._calls).toHaveLength(1)
96
- expect(db._calls[0].sql).toContain('name = ?')
97
- expect(db._calls[0].sql).not.toContain('seed_slug')
98
- })
99
-
100
- it('short-circuits when no fields provided', async () => {
101
- const db = makeDb()
102
- const repo = new D1AutomationRepository(db)
103
- await repo.update('abc', {})
104
- expect(db._calls).toHaveLength(0)
105
- })
106
- })
107
-
108
- describe('D1AutomationRepository.toggle', () => {
109
- it('only writes enabled and updated_at', async () => {
110
- const db = makeDb()
111
- const repo = new D1AutomationRepository(db)
112
- await repo.toggle('abc', false)
113
- expect(db._calls).toHaveLength(1)
114
- const { sql, bindings } = db._calls[0]
115
- expect(sql).toBe('UPDATE automations SET enabled = ?, updated_at = ? WHERE id = ?')
116
- expect(bindings[0]).toBe(0)
117
- expect(bindings[2]).toBe('abc')
118
- })
119
- })
120
-
121
- describe('rowToAutomation JSON round-trip', () => {
122
- it('deserialises trigger_conditions as null when column is null', async () => {
123
- const db = makeDb()
124
- db._setAllResult([fixtureRow])
125
- const repo = new D1AutomationRepository(db)
126
- const rows = await repo.list('posts')
127
- expect(rows[0].trigger_conditions).toBeNull()
128
- })
129
-
130
- it('deserialises trigger_conditions as WhenNode when column contains WhenNode JSON', async () => {
131
- const db = makeDb()
132
- const whenNode = {
133
- kind: 'group',
134
- op: 'AND',
135
- children: [{
136
- kind: 'predicate',
137
- left: { kind: 'ref', key: 'this.status' },
138
- op: 'eq',
139
- right: { kind: 'literal', value: 'published' },
140
- }],
141
- }
142
- const rowWithConditions = {
143
- ...fixtureRow,
144
- trigger_conditions: JSON.stringify(whenNode),
145
- }
146
- db._setAllResult([rowWithConditions])
147
- const repo = new D1AutomationRepository(db)
148
- const rows = await repo.list('posts')
149
- expect(rows[0].trigger_conditions).toEqual(whenNode)
150
- })
151
-
152
- it('deserialises actions as parsed array', async () => {
153
- const db = makeDb()
154
- db._setAllResult([fixtureRow])
155
- const repo = new D1AutomationRepository(db)
156
- const rows = await repo.list('posts')
157
- expect(rows[0].actions).toEqual([{ type: 'webhook', url: 'https://example.com' }])
158
- })
159
- })
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
+ // See LICENSE in the repository root for license terms.
4
+
5
+ import { describe, it, expect, vi } from 'vitest'
6
+ import { D1AutomationRepository } from '../../../shared/automations.repository.d1'
7
+
8
+ function makeDb() {
9
+ const calls: { sql: string; bindings: unknown[] }[] = []
10
+ let nextAllResult: unknown[] = []
11
+ let nextFirstResult: unknown = null
12
+
13
+ const db = {
14
+ _calls: calls,
15
+ _setAllResult(rows: unknown[]) { nextAllResult = rows },
16
+ _setFirstResult(row: unknown) { nextFirstResult = row },
17
+ prepare(sql: string) {
18
+ let boundValues: unknown[] = []
19
+ const stmt = {
20
+ bind(...args: unknown[]) { boundValues = args; return stmt },
21
+ async run() {
22
+ calls.push({ sql, bindings: boundValues })
23
+ return { success: true }
24
+ },
25
+ async all() {
26
+ calls.push({ sql, bindings: boundValues })
27
+ return { results: nextAllResult }
28
+ },
29
+ async first() {
30
+ calls.push({ sql, bindings: boundValues })
31
+ return nextFirstResult
32
+ },
33
+ }
34
+ return stmt
35
+ },
36
+ }
37
+ return db as unknown as D1Database & typeof db
38
+ }
39
+
40
+ const fixtureRow = {
41
+ id: 'abc',
42
+ seed_slug: 'posts',
43
+ name: 'test',
44
+ enabled: 1,
45
+ triggers: JSON.stringify([{ event: 'create' }]),
46
+ trigger_conditions: null,
47
+ actions: JSON.stringify([{ type: 'webhook', url: 'https://example.com' }]),
48
+ created_at: 1000,
49
+ updated_at: 1000,
50
+ }
51
+
52
+ describe('D1AutomationRepository.create', () => {
53
+ it('serialises actions and trigger_conditions to JSON', async () => {
54
+ const db = makeDb()
55
+ const repo = new D1AutomationRepository(db)
56
+ const whenNode = {
57
+ kind: 'group' as const,
58
+ op: 'AND' as const,
59
+ children: [{ kind: 'predicate' as const, left: { kind: 'ref' as const, key: 'this.status' }, op: 'eq' as const, right: { kind: 'literal' as const, value: 'published' } }],
60
+ }
61
+ await repo.create({
62
+ seed_slug: 'posts',
63
+ name: 'test',
64
+ triggers: [{ event: 'create' }],
65
+ trigger_conditions: whenNode,
66
+ actions: [{ type: 'webhook', url: 'https://example.com' }],
67
+ })
68
+ expect(db._calls).toHaveLength(1)
69
+ const { bindings } = db._calls[0]
70
+ // triggers is index 3, trigger_conditions is index 4, actions is index 5
71
+ expect(typeof bindings[3]).toBe('string')
72
+ expect(JSON.parse(bindings[3] as string)).toEqual([{ event: 'create' }])
73
+ expect(typeof bindings[4]).toBe('string')
74
+ expect(JSON.parse(bindings[4] as string)).toEqual(whenNode)
75
+ expect(typeof bindings[5]).toBe('string')
76
+ expect(JSON.parse(bindings[5] as string)).toEqual([{ type: 'webhook', url: 'https://example.com' }])
77
+ })
78
+
79
+ it('stores null for missing trigger_conditions', async () => {
80
+ const db = makeDb()
81
+ const repo = new D1AutomationRepository(db)
82
+ await repo.create({
83
+ seed_slug: 'posts',
84
+ name: 'test',
85
+ triggers: [{ event: 'create' }],
86
+ trigger_conditions: null,
87
+ actions: [{ type: 'webhook', url: 'https://example.com' }],
88
+ })
89
+ const { bindings } = db._calls[0]
90
+ expect(bindings[4]).toBeNull()
91
+ })
92
+ })
93
+
94
+ describe('D1AutomationRepository.update', () => {
95
+ it('builds SET clause only from provided keys', async () => {
96
+ const db = makeDb()
97
+ const repo = new D1AutomationRepository(db)
98
+ await repo.update('abc', { name: 'renamed' })
99
+ expect(db._calls).toHaveLength(1)
100
+ expect(db._calls[0].sql).toContain('name = ?')
101
+ expect(db._calls[0].sql).not.toContain('seed_slug')
102
+ })
103
+
104
+ it('short-circuits when no fields provided', async () => {
105
+ const db = makeDb()
106
+ const repo = new D1AutomationRepository(db)
107
+ await repo.update('abc', {})
108
+ expect(db._calls).toHaveLength(0)
109
+ })
110
+ })
111
+
112
+ describe('D1AutomationRepository.toggle', () => {
113
+ it('only writes enabled and updated_at', async () => {
114
+ const db = makeDb()
115
+ const repo = new D1AutomationRepository(db)
116
+ await repo.toggle('abc', false)
117
+ expect(db._calls).toHaveLength(1)
118
+ const { sql, bindings } = db._calls[0]
119
+ expect(sql).toBe('UPDATE automations SET enabled = ?, updated_at = ? WHERE id = ?')
120
+ expect(bindings[0]).toBe(0)
121
+ expect(bindings[2]).toBe('abc')
122
+ })
123
+ })
124
+
125
+ describe('rowToAutomation JSON round-trip', () => {
126
+ it('deserialises trigger_conditions as null when column is null', async () => {
127
+ const db = makeDb()
128
+ db._setAllResult([fixtureRow])
129
+ const repo = new D1AutomationRepository(db)
130
+ const rows = await repo.list('posts')
131
+ expect(rows[0].trigger_conditions).toBeNull()
132
+ })
133
+
134
+ it('deserialises trigger_conditions as WhenNode when column contains WhenNode JSON', async () => {
135
+ const db = makeDb()
136
+ const whenNode = {
137
+ kind: 'group',
138
+ op: 'AND',
139
+ children: [{
140
+ kind: 'predicate',
141
+ left: { kind: 'ref', key: 'this.status' },
142
+ op: 'eq',
143
+ right: { kind: 'literal', value: 'published' },
144
+ }],
145
+ }
146
+ const rowWithConditions = {
147
+ ...fixtureRow,
148
+ trigger_conditions: JSON.stringify(whenNode),
149
+ }
150
+ db._setAllResult([rowWithConditions])
151
+ const repo = new D1AutomationRepository(db)
152
+ const rows = await repo.list('posts')
153
+ expect(rows[0].trigger_conditions).toEqual(whenNode)
154
+ })
155
+
156
+ it('deserialises actions as parsed array', async () => {
157
+ const db = makeDb()
158
+ db._setAllResult([fixtureRow])
159
+ const repo = new D1AutomationRepository(db)
160
+ const rows = await repo.list('posts')
161
+ expect(rows[0].actions).toEqual([{ type: 'webhook', url: 'https://example.com' }])
162
+ })
163
+ })