@beechcms/api 0.6.0-preview.1 → 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 (222) hide show
  1. package/assets/dashboard/BeechLogo.svg +18 -18
  2. package/assets/dashboard/BeechLogoLIght.svg +48 -48
  3. package/assets/dashboard/assets/chart-BrJFdsGZ.js +39 -0
  4. package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
  5. package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
  6. package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
  7. package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
  8. package/assets/dashboard/beechLogoDark.svg +48 -48
  9. package/assets/dashboard/index.html +18 -18
  10. package/assets/dashboard/noResult.svg +42 -42
  11. package/assets/dashboard/sol.svg +3 -3
  12. package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
  13. package/migrations/0000_v040_base.sql +265 -265
  14. package/migrations/0029_automations.sql +14 -14
  15. package/migrations/0030_test_seeds.sql +125 -125
  16. package/package.json +2 -2
  17. package/src/auth/__fixtures__/in-memory-hash-provider.ts +17 -17
  18. package/src/auth/__fixtures__/static-token-service.ts +22 -22
  19. package/src/auth/bcrypt-hash-provider.ts +24 -24
  20. package/src/auth/constants.ts +14 -14
  21. package/src/auth/generate-refresh-token.test.ts +23 -23
  22. package/src/auth/hash-provider.test.ts +55 -55
  23. package/src/auth/jose-token-service.ts +62 -62
  24. package/src/auth/jwt-claims-passthrough.test.ts +87 -87
  25. package/src/auth/login.test.ts +100 -100
  26. package/src/auth/login.ts +78 -78
  27. package/src/auth/refresh.ts +11 -11
  28. package/src/auth/token-service.test.ts +90 -90
  29. package/src/factory.ts +370 -368
  30. package/src/features/automations/__tests__/action-executors.test.ts +255 -255
  31. package/src/features/automations/__tests__/automation-runner.test.ts +196 -196
  32. package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -60
  33. package/src/features/automations/__tests__/automations.handler.test.ts +264 -264
  34. package/src/features/automations/__tests__/automations.repository.test.ts +163 -163
  35. package/src/features/automations/__tests__/automations.schema.test.ts +224 -224
  36. package/src/features/automations/__tests__/context-resolver.test.ts +126 -126
  37. package/src/features/automations/__tests__/cron-runner.test.ts +267 -267
  38. package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -144
  39. package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -383
  40. package/src/features/automations/__tests__/template-grammar.test.ts +308 -308
  41. package/src/features/automations/__tests__/webhook.executor.test.ts +142 -142
  42. package/src/features/automations/__tests__/when-evaluator.test.ts +274 -274
  43. package/src/features/automations/__tests__/when-pushdown.test.ts +281 -281
  44. package/src/features/automations/action-executors/create-entry.executor.ts +27 -27
  45. package/src/features/automations/action-executors/edit-field.executor.ts +26 -26
  46. package/src/features/automations/action-executors/index.ts +37 -37
  47. package/src/features/automations/action-executors/send-mail.executor.ts +53 -53
  48. package/src/features/automations/action-executors/set-variable.executor.ts +159 -159
  49. package/src/features/automations/action-executors/webhook.executor.ts +67 -67
  50. package/src/features/automations/automation-runner.ts +85 -85
  51. package/src/features/automations/automation-runner.utils.ts +47 -47
  52. package/src/features/automations/automations.handler.ts +197 -197
  53. package/src/features/automations/automations.schema.ts +177 -177
  54. package/src/features/automations/context-resolver.ts +152 -152
  55. package/src/features/automations/cron-runner.ts +140 -140
  56. package/src/features/automations/cron-runner.utils.ts +44 -44
  57. package/src/features/automations/filter-translation.ts +46 -46
  58. package/src/features/automations/index.ts +16 -16
  59. package/src/features/automations/template-grammar.ts +245 -245
  60. package/src/features/automations/var-access-resolver.ts +140 -140
  61. package/src/features/automations/when-evaluator.ts +87 -87
  62. package/src/features/automations/when-pushdown.ts +57 -57
  63. package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -359
  64. package/src/features/backrefs/backrefs.handler.ts +168 -168
  65. package/src/features/backrefs/d1-backref.repository.ts +84 -84
  66. package/src/features/backrefs/index.ts +5 -5
  67. package/src/features/content/constants.ts +16 -16
  68. package/src/features/content/handlers/bulk.handler.ts +185 -185
  69. package/src/features/content/handlers/create.ts +129 -129
  70. package/src/features/content/handlers/delete.ts +62 -62
  71. package/src/features/content/handlers/facets.ts +49 -49
  72. package/src/features/content/handlers/get.ts +120 -120
  73. package/src/features/content/handlers/helpers.test.ts +180 -180
  74. package/src/features/content/handlers/helpers.ts +93 -93
  75. package/src/features/content/handlers/list.ts +174 -174
  76. package/src/features/content/handlers/update.ts +174 -174
  77. package/src/features/content/index.ts +26 -26
  78. package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
  79. package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
  80. package/src/features/dashboard-layout/index.ts +5 -0
  81. package/src/features/draft/draft.handler.ts +195 -195
  82. package/src/features/draft/draft.middleware.ts +66 -66
  83. package/src/features/draft/index.ts +5 -5
  84. package/src/features/email/email.provider.ts +42 -42
  85. package/src/features/email/email.service.ts +98 -98
  86. package/src/features/email/email.types.ts +119 -119
  87. package/src/features/email/index.ts +33 -33
  88. package/src/features/email/providers/resend.ts +67 -67
  89. package/src/features/email/providers/smtp.ts +55 -55
  90. package/src/features/email/templates/automation-mail.ts +19 -19
  91. package/src/features/email/templates/password-changed.ts +63 -63
  92. package/src/features/email/templates/password-reset.ts +68 -68
  93. package/src/features/email/templates/shell.ts +96 -96
  94. package/src/features/notifications/index.ts +5 -5
  95. package/src/features/notifications/notifications.handler.ts +105 -105
  96. package/src/features/password-reset/index.ts +19 -19
  97. package/src/features/password-reset/request.ts +92 -92
  98. package/src/features/password-reset/reset.ts +102 -102
  99. package/src/features/rotate-field/index.ts +5 -5
  100. package/src/features/rotate-field/rotate-field.handler.ts +132 -132
  101. package/src/features/rotate-field/rotate-field.schema.ts +17 -17
  102. package/src/features/schema/schema.handler.ts +131 -131
  103. package/src/features/seeds/index.ts +5 -5
  104. package/src/features/seeds/seeds.handler.test.ts +1199 -632
  105. package/src/features/seeds/seeds.handler.ts +558 -693
  106. package/src/features/settings/__tests__/settings.handler.test.ts +555 -555
  107. package/src/features/settings/settings.handler.ts +392 -392
  108. package/src/features/setup/index.ts +288 -204
  109. package/src/features/stats/index.ts +5 -5
  110. package/src/features/stats/stats.handler.ts +420 -420
  111. package/src/features/webhooks/index.ts +63 -63
  112. package/src/index.ts +65 -65
  113. package/src/media-utils.ts +82 -82
  114. package/src/middleware/auth-providers.middleware.test.ts +85 -0
  115. package/src/middleware/auth-providers.middleware.ts +45 -45
  116. package/src/middleware/observability.middleware.test.ts +93 -0
  117. package/src/middleware/observability.middleware.ts +70 -70
  118. package/src/middleware/rate-limit.middleware.ts +45 -45
  119. package/src/middleware/repository.middleware.ts +117 -114
  120. package/src/middleware/seed-registry.middleware.test.ts +97 -97
  121. package/src/middleware/seed-registry.middleware.ts +21 -21
  122. package/src/middleware/storage.middleware.ts +25 -25
  123. package/src/middleware.ts +45 -45
  124. package/src/public/access-policy.ts +27 -27
  125. package/src/public/api-key-middleware.ts +57 -57
  126. package/src/public/cache-utils.ts +38 -38
  127. package/src/public/entry-projection.ts +46 -46
  128. package/src/public/idempotency.ts +23 -23
  129. package/src/public/index.ts +16 -16
  130. package/src/public/problem-details.ts +124 -124
  131. package/src/public/public-add.ts +114 -114
  132. package/src/public/public-edit.ts +163 -163
  133. package/src/public/public-errors.ts +19 -19
  134. package/src/public/public-read.ts +63 -63
  135. package/src/public/public-routes.ts +88 -88
  136. package/src/public/query-builder.test.ts +224 -224
  137. package/src/public/query-builder.ts +156 -156
  138. package/src/public/rate-limit-middleware.ts +34 -34
  139. package/src/public/read-list.ts +54 -54
  140. package/src/public/read-single.ts +48 -48
  141. package/src/public/response-builder.ts +30 -30
  142. package/src/public/sanitize.ts +69 -69
  143. package/src/public/slug-utils.ts +18 -18
  144. package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -30
  145. package/src/rate-limit/cloudflare-rate-limiter.ts +15 -15
  146. package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -37
  147. package/src/rate-limit/in-memory-rate-limiter.ts +17 -17
  148. package/src/rate-limit/no-op-rate-limiter.test.ts +22 -22
  149. package/src/rate-limit/no-op-rate-limiter.ts +11 -11
  150. package/src/search-utils.test.ts +211 -211
  151. package/src/search-utils.ts +213 -213
  152. package/src/search.ts +65 -65
  153. package/src/shared/apply-policies.test.ts +81 -81
  154. package/src/shared/apply-policies.ts +67 -67
  155. package/src/shared/automations.repository.d1.ts +150 -150
  156. package/src/shared/background-notification-service.test.ts +62 -62
  157. package/src/shared/background-notification-service.ts +52 -52
  158. package/src/shared/base.repository.d1.ts +32 -32
  159. package/src/shared/content-utils.test.ts +165 -165
  160. package/src/shared/content-utils.ts +86 -86
  161. package/src/shared/content.repository.d1.test.ts +507 -458
  162. package/src/shared/content.repository.d1.ts +851 -851
  163. package/src/shared/d1-activity-log.repository.test.ts +140 -140
  164. package/src/shared/d1-activity-log.repository.ts +105 -105
  165. package/src/shared/d1-activity-logger.test.ts +86 -86
  166. package/src/shared/d1-activity-logger.ts +67 -67
  167. package/src/shared/d1-analytics.repository.test.ts +78 -78
  168. package/src/shared/d1-analytics.repository.ts +85 -85
  169. package/src/shared/d1-content-scan.repository.test.ts +80 -80
  170. package/src/shared/d1-content-scan.repository.ts +37 -37
  171. package/src/shared/d1-notification.repository.test.ts +128 -128
  172. package/src/shared/d1-notification.repository.ts +118 -118
  173. package/src/shared/d1-password-reset-token.repository.test.ts +81 -81
  174. package/src/shared/d1-password-reset-token.repository.ts +56 -56
  175. package/src/shared/d1-search.repository.test.ts +87 -87
  176. package/src/shared/d1-search.repository.ts +88 -88
  177. package/src/shared/d1-session.repository.test.ts +125 -125
  178. package/src/shared/d1-session.repository.ts +102 -102
  179. package/src/shared/d1-setup-checklist.repository.ts +36 -36
  180. package/src/shared/d1-user.repository.test.ts +151 -151
  181. package/src/shared/d1-user.repository.ts +119 -119
  182. package/src/shared/d1-widget.repository.test.ts +267 -221
  183. package/src/shared/d1-widget.repository.ts +385 -341
  184. package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
  185. package/src/shared/dashboard-layout.repository.d1.ts +58 -0
  186. package/src/shared/demo-data-sql.ts +216 -54
  187. package/src/shared/demo-data.repository.d1.ts +20 -20
  188. package/src/shared/execution-context-scheduler.ts +13 -13
  189. package/src/shared/fixed-clock.ts +25 -25
  190. package/src/shared/fts-sync.ts +8 -8
  191. package/src/shared/idempotency.repository.d1.test.ts +83 -83
  192. package/src/shared/idempotency.repository.d1.ts +60 -60
  193. package/src/shared/in-memory-activity-logger.ts +19 -19
  194. package/src/shared/in-memory-notification-service.ts +19 -19
  195. package/src/shared/in-memory-seed.repository.ts +58 -51
  196. package/src/shared/media.repository.d1.test.ts +107 -107
  197. package/src/shared/media.repository.d1.ts +68 -68
  198. package/src/shared/qstash-notification-service.test.ts +67 -67
  199. package/src/shared/qstash-notification-service.ts +55 -55
  200. package/src/shared/query-utils.ts +141 -141
  201. package/src/shared/request-utils.ts +26 -26
  202. package/src/shared/schema-mutator.d1.test.ts +142 -0
  203. package/src/shared/schema-mutator.d1.ts +64 -64
  204. package/src/shared/seed-layout.repository.d1.test.ts +114 -114
  205. package/src/shared/seed-layout.repository.d1.ts +62 -62
  206. package/src/shared/seed-registry-cache.test.ts +68 -68
  207. package/src/shared/seed-registry-cache.ts +49 -49
  208. package/src/shared/seed.repository.d1.test.ts +143 -143
  209. package/src/shared/seed.repository.d1.ts +98 -98
  210. package/src/shared/sequential-id-generator.ts +33 -33
  211. package/src/shared/site-settings.repository.d1.ts +53 -53
  212. package/src/shared/storage/factory.ts +41 -41
  213. package/src/shared/storage/s3-bucket.ts +195 -195
  214. package/src/shared/storage-utils.ts +40 -40
  215. package/src/shared/system-stats.repository.d1.test.ts +58 -58
  216. package/src/shared/system-stats.repository.d1.ts +48 -48
  217. package/src/types.ts +90 -87
  218. package/src/upload.ts +198 -198
  219. package/src/widget.test.ts +85 -0
  220. package/src/widget.ts +251 -212
  221. package/assets/dashboard/assets/index-BSjk0Sx9.js +0 -634
  222. package/assets/dashboard/assets/index-kRCYVN2B.css +0 -1
@@ -1,168 +1,168 @@
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
- /// <reference types="@cloudflare/workers-types" />
6
- import { Hono } from 'hono'
7
- import { resolvePolicies } from '@beechcms/core'
8
- import { publicProblem } from '../../public/problem-details'
9
- import type { AppEnv } from '../../types'
10
- import { D1BackrefRepository, type BackrefItem } from './d1-backref.repository'
11
-
12
- const PREVIEW_LIMIT = 3
13
- const DEFAULT_PAGE_LIMIT = 20
14
- const MAX_PAGE_LIMIT = 100
15
-
16
- export const backrefsApp = new Hono<AppEnv>()
17
-
18
- // ---------------------------------------------------------------------------
19
- // GET /content/:targetSlug/:targetId/backrefs
20
- // ---------------------------------------------------------------------------
21
-
22
- backrefsApp.get('/:targetSlug/:targetId/backrefs', async (c) => {
23
- const targetSlug = c.req.param('targetSlug')
24
- const targetId = c.req.param('targetId')
25
- const groupParam = c.req.query('group') // '<sourceSlug>:<branchAlias>'
26
- const pageParam = c.req.query('page')
27
- const limitParam = c.req.query('limit')
28
-
29
- const getSeed = c.get('getSeed')
30
- const backrefMap = c.get('backrefMap')
31
- const backrefRepository = new D1BackrefRepository(c.env.DB)
32
-
33
- // 1. Resolve target seed
34
- const targetSeed = getSeed(targetSlug)
35
- if (!targetSeed) {
36
- return publicProblem(c, {
37
- type: 'not-found',
38
- title: 'Not Found',
39
- status: 404,
40
- detail: `Seed '${targetSlug}' does not exist`,
41
- })
42
- }
43
-
44
- // 2. Verify targetId exists
45
- const exists = await backrefRepository.entryExists(targetSlug, targetId)
46
- if (!exists) {
47
- return publicProblem(c, {
48
- type: 'not-found',
49
- title: 'Not Found',
50
- status: 404,
51
- detail: `Entry '${targetId}' not found in '${targetSlug}'`,
52
- })
53
- }
54
-
55
- // 3. Get sources for this target
56
- const sources = backrefMap.get(targetSlug)
57
- if (!sources || sources.length === 0) {
58
- return c.json({ groups: [] })
59
- }
60
-
61
- // 4. Pagination
62
- const page = Math.max(1, Number.parseInt(pageParam ?? '1', 10) || 1)
63
- const rawLimit = Number.parseInt(limitParam ?? String(DEFAULT_PAGE_LIMIT), 10) || DEFAULT_PAGE_LIMIT
64
- const limit = Math.min(Math.max(1, rawLimit), MAX_PAGE_LIMIT)
65
- const offset = (page - 1) * limit
66
-
67
- // 5. Paginated single-group request
68
- if (groupParam) {
69
- const colonIdx = groupParam.indexOf(':')
70
- if (colonIdx === -1) {
71
- return publicProblem(c, {
72
- type: 'bad-request',
73
- title: 'Bad Request',
74
- status: 400,
75
- detail: `'group' must be '<sourceSlug>:<branchAlias>'`,
76
- })
77
- }
78
- const sourceSlug = groupParam.slice(0, colonIdx)
79
- const branchAlias = groupParam.slice(colonIdx + 1)
80
-
81
- const source = sources.find(
82
- s => s.sourceSlug === sourceSlug && s.branchAlias === branchAlias
83
- )
84
- if (!source) {
85
- return publicProblem(c, {
86
- type: 'not-found',
87
- title: 'Not Found',
88
- status: 404,
89
- detail: `Back-ref group '${groupParam}' not found for target '${targetSlug}'`,
90
- })
91
- }
92
-
93
- const sourceSeed = getSeed(sourceSlug)
94
- if (!sourceSeed) {
95
- return c.json({ groups: [] })
96
- }
97
-
98
- const { items, total } = await backrefRepository.queryGroup(source, sourceSeed, targetId, limit, offset)
99
- const filteredItems = filterVisibility(items, sourceSeed)
100
-
101
- return c.json({
102
- groups: [{
103
- sourceSlug: source.sourceSlug,
104
- sourceLabel: sourceSeed.labelPlural ?? sourceSeed.label,
105
- branchAlias: source.branchAlias,
106
- branchLabel: source.branchLabel,
107
- relationship: source.relationship,
108
- restricts: source.restricts,
109
- total,
110
- items: filteredItems,
111
- }],
112
- })
113
- }
114
-
115
- // 6. Preview mode — all groups, PREVIEW_LIMIT each
116
- const groupResults = await Promise.all(
117
- sources.map(async (source) => {
118
- const sourceSeed = getSeed(source.sourceSlug)
119
- if (!sourceSeed) return null
120
-
121
- const { items, total } = await backrefRepository.queryGroup(source, sourceSeed, targetId, PREVIEW_LIMIT, 0)
122
- const filteredItems = filterVisibility(items, sourceSeed)
123
-
124
- return {
125
- sourceSlug: source.sourceSlug,
126
- sourceLabel: sourceSeed.labelPlural ?? sourceSeed.label,
127
- branchAlias: source.branchAlias,
128
- branchLabel: source.branchLabel,
129
- relationship: source.relationship,
130
- restricts: source.restricts,
131
- total,
132
- items: filteredItems,
133
- }
134
- })
135
- )
136
-
137
- const groups = groupResults.filter(
138
- (g): g is NonNullable<typeof g> => g !== null && g.total > 0
139
- )
140
-
141
- return c.json({ groups })
142
- })
143
-
144
- // ---------------------------------------------------------------------------
145
- // Internal helpers
146
- // ---------------------------------------------------------------------------
147
-
148
- function filterVisibility(
149
- items: BackrefItem[],
150
- sourceSeed: import('@beechcms/core').Seed
151
- ): BackrefItem[] {
152
- const displayBranch = sourceSeed.branches.find(
153
- b => b.alias === sourceSeed.displayNameAlias
154
- )
155
- if (!displayBranch) return items
156
-
157
- const { visibility } = resolvePolicies(displayBranch)
158
- if (visibility === 'hidden') {
159
- return items.map(item => ({ ...item, displayName: null }))
160
- }
161
- if (visibility === 'masked') {
162
- return items.map(item => ({
163
- ...item,
164
- displayName: item.displayName ? '••••••••' : null,
165
- }))
166
- }
167
- return items
168
- }
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
+ /// <reference types="@cloudflare/workers-types" />
6
+ import { Hono } from 'hono'
7
+ import { resolvePolicies } from '@beechcms/core'
8
+ import { publicProblem } from '../../public/problem-details'
9
+ import type { AppEnv } from '../../types'
10
+ import { D1BackrefRepository, type BackrefItem } from './d1-backref.repository'
11
+
12
+ const PREVIEW_LIMIT = 3
13
+ const DEFAULT_PAGE_LIMIT = 20
14
+ const MAX_PAGE_LIMIT = 100
15
+
16
+ export const backrefsApp = new Hono<AppEnv>()
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // GET /content/:targetSlug/:targetId/backrefs
20
+ // ---------------------------------------------------------------------------
21
+
22
+ backrefsApp.get('/:targetSlug/:targetId/backrefs', async (c) => {
23
+ const targetSlug = c.req.param('targetSlug')
24
+ const targetId = c.req.param('targetId')
25
+ const groupParam = c.req.query('group') // '<sourceSlug>:<branchAlias>'
26
+ const pageParam = c.req.query('page')
27
+ const limitParam = c.req.query('limit')
28
+
29
+ const getSeed = c.get('getSeed')
30
+ const backrefMap = c.get('backrefMap')
31
+ const backrefRepository = new D1BackrefRepository(c.env.DB)
32
+
33
+ // 1. Resolve target seed
34
+ const targetSeed = getSeed(targetSlug)
35
+ if (!targetSeed) {
36
+ return publicProblem(c, {
37
+ type: 'not-found',
38
+ title: 'Not Found',
39
+ status: 404,
40
+ detail: `Seed '${targetSlug}' does not exist`,
41
+ })
42
+ }
43
+
44
+ // 2. Verify targetId exists
45
+ const exists = await backrefRepository.entryExists(targetSlug, targetId)
46
+ if (!exists) {
47
+ return publicProblem(c, {
48
+ type: 'not-found',
49
+ title: 'Not Found',
50
+ status: 404,
51
+ detail: `Entry '${targetId}' not found in '${targetSlug}'`,
52
+ })
53
+ }
54
+
55
+ // 3. Get sources for this target
56
+ const sources = backrefMap.get(targetSlug)
57
+ if (!sources || sources.length === 0) {
58
+ return c.json({ groups: [] })
59
+ }
60
+
61
+ // 4. Pagination
62
+ const page = Math.max(1, Number.parseInt(pageParam ?? '1', 10) || 1)
63
+ const rawLimit = Number.parseInt(limitParam ?? String(DEFAULT_PAGE_LIMIT), 10) || DEFAULT_PAGE_LIMIT
64
+ const limit = Math.min(Math.max(1, rawLimit), MAX_PAGE_LIMIT)
65
+ const offset = (page - 1) * limit
66
+
67
+ // 5. Paginated single-group request
68
+ if (groupParam) {
69
+ const colonIdx = groupParam.indexOf(':')
70
+ if (colonIdx === -1) {
71
+ return publicProblem(c, {
72
+ type: 'bad-request',
73
+ title: 'Bad Request',
74
+ status: 400,
75
+ detail: `'group' must be '<sourceSlug>:<branchAlias>'`,
76
+ })
77
+ }
78
+ const sourceSlug = groupParam.slice(0, colonIdx)
79
+ const branchAlias = groupParam.slice(colonIdx + 1)
80
+
81
+ const source = sources.find(
82
+ s => s.sourceSlug === sourceSlug && s.branchAlias === branchAlias
83
+ )
84
+ if (!source) {
85
+ return publicProblem(c, {
86
+ type: 'not-found',
87
+ title: 'Not Found',
88
+ status: 404,
89
+ detail: `Back-ref group '${groupParam}' not found for target '${targetSlug}'`,
90
+ })
91
+ }
92
+
93
+ const sourceSeed = getSeed(sourceSlug)
94
+ if (!sourceSeed) {
95
+ return c.json({ groups: [] })
96
+ }
97
+
98
+ const { items, total } = await backrefRepository.queryGroup(source, sourceSeed, targetId, limit, offset)
99
+ const filteredItems = filterVisibility(items, sourceSeed)
100
+
101
+ return c.json({
102
+ groups: [{
103
+ sourceSlug: source.sourceSlug,
104
+ sourceLabel: sourceSeed.labelPlural ?? sourceSeed.label,
105
+ branchAlias: source.branchAlias,
106
+ branchLabel: source.branchLabel,
107
+ relationship: source.relationship,
108
+ restricts: source.restricts,
109
+ total,
110
+ items: filteredItems,
111
+ }],
112
+ })
113
+ }
114
+
115
+ // 6. Preview mode — all groups, PREVIEW_LIMIT each
116
+ const groupResults = await Promise.all(
117
+ sources.map(async (source) => {
118
+ const sourceSeed = getSeed(source.sourceSlug)
119
+ if (!sourceSeed) return null
120
+
121
+ const { items, total } = await backrefRepository.queryGroup(source, sourceSeed, targetId, PREVIEW_LIMIT, 0)
122
+ const filteredItems = filterVisibility(items, sourceSeed)
123
+
124
+ return {
125
+ sourceSlug: source.sourceSlug,
126
+ sourceLabel: sourceSeed.labelPlural ?? sourceSeed.label,
127
+ branchAlias: source.branchAlias,
128
+ branchLabel: source.branchLabel,
129
+ relationship: source.relationship,
130
+ restricts: source.restricts,
131
+ total,
132
+ items: filteredItems,
133
+ }
134
+ })
135
+ )
136
+
137
+ const groups = groupResults.filter(
138
+ (g): g is NonNullable<typeof g> => g !== null && g.total > 0
139
+ )
140
+
141
+ return c.json({ groups })
142
+ })
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Internal helpers
146
+ // ---------------------------------------------------------------------------
147
+
148
+ function filterVisibility(
149
+ items: BackrefItem[],
150
+ sourceSeed: import('@beechcms/core').Seed
151
+ ): BackrefItem[] {
152
+ const displayBranch = sourceSeed.branches.find(
153
+ b => b.alias === sourceSeed.displayNameAlias
154
+ )
155
+ if (!displayBranch) return items
156
+
157
+ const { visibility } = resolvePolicies(displayBranch)
158
+ if (visibility === 'hidden') {
159
+ return items.map(item => ({ ...item, displayName: null }))
160
+ }
161
+ if (visibility === 'masked') {
162
+ return items.map(item => ({
163
+ ...item,
164
+ displayName: item.displayName ? '••••••••' : null,
165
+ }))
166
+ }
167
+ return items
168
+ }
@@ -1,84 +1,84 @@
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
- /// <reference types="@cloudflare/workers-types" />
6
- import type { BackrefSource, Seed } from '@beechcms/core'
7
-
8
- export interface BackrefItem {
9
- id: string
10
- displayName: string | null
11
- status: string
12
- updated_at: number | null
13
- }
14
-
15
- export interface QueryGroupResult {
16
- items: BackrefItem[]
17
- total: number
18
- }
19
-
20
- export class D1BackrefRepository {
21
- constructor(private readonly db: D1Database) {}
22
-
23
- async entryExists(slug: string, id: string): Promise<boolean> {
24
- const row = await this.db
25
- .prepare(`SELECT id FROM content_${slug} WHERE id = ? LIMIT 1`)
26
- .bind(id)
27
- .first<{ id: string }>()
28
- return row !== null
29
- }
30
-
31
- async queryGroup(
32
- source: BackrefSource,
33
- sourceSeed: Seed,
34
- targetId: string,
35
- limit: number,
36
- offset: number,
37
- ): Promise<QueryGroupResult> {
38
- const displayCol = sourceSeed.displayNameAlias
39
-
40
- if (source.relationship === 'single') {
41
- const [rowsResult, countResult] = await Promise.all([
42
- this.db
43
- .prepare(
44
- `SELECT id, status, updated_at, ${displayCol} AS displayName
45
- FROM content_${source.sourceSlug}
46
- WHERE ${source.branchAlias} = ?
47
- ORDER BY updated_at DESC
48
- LIMIT ? OFFSET ?`,
49
- )
50
- .bind(targetId, limit, offset)
51
- .all<BackrefItem>(),
52
- this.db
53
- .prepare(
54
- `SELECT COUNT(*) AS total FROM content_${source.sourceSlug} WHERE ${source.branchAlias} = ?`,
55
- )
56
- .bind(targetId)
57
- .first<{ total: number }>(),
58
- ])
59
- return { items: rowsResult.results ?? [], total: countResult?.total ?? 0 }
60
- }
61
-
62
- const joinTable = `rel_${source.sourceSlug}_${source.branchAlias}`
63
- const [rowsResult, countResult] = await Promise.all([
64
- this.db
65
- .prepare(
66
- `SELECT c.id, c.status, c.updated_at, c.${displayCol} AS displayName
67
- FROM content_${source.sourceSlug} c
68
- JOIN ${joinTable} r ON r.parent_id = c.id
69
- WHERE r.target_id = ?
70
- ORDER BY c.updated_at DESC
71
- LIMIT ? OFFSET ?`,
72
- )
73
- .bind(targetId, limit, offset)
74
- .all<BackrefItem>(),
75
- this.db
76
- .prepare(
77
- `SELECT COUNT(DISTINCT parent_id) AS total FROM ${joinTable} WHERE target_id = ?`,
78
- )
79
- .bind(targetId)
80
- .first<{ total: number }>(),
81
- ])
82
- return { items: rowsResult.results ?? [], total: countResult?.total ?? 0 }
83
- }
84
- }
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
+ /// <reference types="@cloudflare/workers-types" />
6
+ import type { BackrefSource, Seed } from '@beechcms/core'
7
+
8
+ export interface BackrefItem {
9
+ id: string
10
+ displayName: string | null
11
+ status: string
12
+ updated_at: number | null
13
+ }
14
+
15
+ export interface QueryGroupResult {
16
+ items: BackrefItem[]
17
+ total: number
18
+ }
19
+
20
+ export class D1BackrefRepository {
21
+ constructor(private readonly db: D1Database) {}
22
+
23
+ async entryExists(slug: string, id: string): Promise<boolean> {
24
+ const row = await this.db
25
+ .prepare(`SELECT id FROM content_${slug} WHERE id = ? LIMIT 1`)
26
+ .bind(id)
27
+ .first<{ id: string }>()
28
+ return row !== null
29
+ }
30
+
31
+ async queryGroup(
32
+ source: BackrefSource,
33
+ sourceSeed: Seed,
34
+ targetId: string,
35
+ limit: number,
36
+ offset: number,
37
+ ): Promise<QueryGroupResult> {
38
+ const displayCol = sourceSeed.displayNameAlias
39
+
40
+ if (source.relationship === 'single') {
41
+ const [rowsResult, countResult] = await Promise.all([
42
+ this.db
43
+ .prepare(
44
+ `SELECT id, status, updated_at, ${displayCol} AS displayName
45
+ FROM content_${source.sourceSlug}
46
+ WHERE ${source.branchAlias} = ?
47
+ ORDER BY updated_at DESC
48
+ LIMIT ? OFFSET ?`,
49
+ )
50
+ .bind(targetId, limit, offset)
51
+ .all<BackrefItem>(),
52
+ this.db
53
+ .prepare(
54
+ `SELECT COUNT(*) AS total FROM content_${source.sourceSlug} WHERE ${source.branchAlias} = ?`,
55
+ )
56
+ .bind(targetId)
57
+ .first<{ total: number }>(),
58
+ ])
59
+ return { items: rowsResult.results ?? [], total: countResult?.total ?? 0 }
60
+ }
61
+
62
+ const joinTable = `rel_${source.sourceSlug}_${source.branchAlias}`
63
+ const [rowsResult, countResult] = await Promise.all([
64
+ this.db
65
+ .prepare(
66
+ `SELECT c.id, c.status, c.updated_at, c.${displayCol} AS displayName
67
+ FROM content_${source.sourceSlug} c
68
+ JOIN ${joinTable} r ON r.parent_id = c.id
69
+ WHERE r.target_id = ?
70
+ ORDER BY c.updated_at DESC
71
+ LIMIT ? OFFSET ?`,
72
+ )
73
+ .bind(targetId, limit, offset)
74
+ .all<BackrefItem>(),
75
+ this.db
76
+ .prepare(
77
+ `SELECT COUNT(DISTINCT parent_id) AS total FROM ${joinTable} WHERE target_id = ?`,
78
+ )
79
+ .bind(targetId)
80
+ .first<{ total: number }>(),
81
+ ])
82
+ return { items: rowsResult.results ?? [], total: countResult?.total ?? 0 }
83
+ }
84
+ }
@@ -1,5 +1,5 @@
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
- export { backrefsApp } from './backrefs.handler'
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
+ export { backrefsApp } from './backrefs.handler'
@@ -1,16 +1,16 @@
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
- export const CONTENT_ERRORS = {
6
- INVALID_SLUG: 'Invalid slug',
7
- INVALID_SLUG_OR_ID: 'Invalid slug or id',
8
- INVALID_JSON_BODY: 'Invalid JSON body',
9
- NOT_FOUND: 'Not found',
10
- SEED_NOT_FOUND: 'Seed not found',
11
- DATABASE_ERROR: 'Database error',
12
- SLUG_CONFLICT: 'Slug already exists for this schema',
13
- SENSITIVE_FIELD_EDIT: 'Cannot edit sensitive fields',
14
- BULK_SIZE_EXCEEDED: 'Cannot edit more than 500 entries at once',
15
- FIELD_NOT_BULK_EDITABLE: 'Field cannot be bulk-edited',
16
- } as const
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
+ export const CONTENT_ERRORS = {
6
+ INVALID_SLUG: 'Invalid slug',
7
+ INVALID_SLUG_OR_ID: 'Invalid slug or id',
8
+ INVALID_JSON_BODY: 'Invalid JSON body',
9
+ NOT_FOUND: 'Not found',
10
+ SEED_NOT_FOUND: 'Seed not found',
11
+ DATABASE_ERROR: 'Database error',
12
+ SLUG_CONFLICT: 'Slug already exists for this schema',
13
+ SENSITIVE_FIELD_EDIT: 'Cannot edit sensitive fields',
14
+ BULK_SIZE_EXCEEDED: 'Cannot edit more than 500 entries at once',
15
+ FIELD_NOT_BULK_EDITABLE: 'Field cannot be bulk-edited',
16
+ } as const