@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.
- package/assets/dashboard/BeechLogo.svg +18 -18
- package/assets/dashboard/BeechLogoLIght.svg +48 -48
- package/assets/dashboard/assets/chart-BrJFdsGZ.js +39 -0
- package/assets/dashboard/assets/index-CIaoMxuB.css +1 -0
- package/assets/dashboard/assets/index-wTKgygsg.js +636 -0
- package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +1 -0
- package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +1 -0
- package/assets/dashboard/beechLogoDark.svg +48 -48
- package/assets/dashboard/index.html +18 -18
- package/assets/dashboard/noResult.svg +42 -42
- package/assets/dashboard/sol.svg +3 -3
- package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
- package/migrations/0000_v040_base.sql +265 -265
- package/migrations/0029_automations.sql +14 -14
- package/migrations/0030_test_seeds.sql +125 -125
- package/package.json +2 -2
- package/src/auth/__fixtures__/in-memory-hash-provider.ts +17 -17
- package/src/auth/__fixtures__/static-token-service.ts +22 -22
- package/src/auth/bcrypt-hash-provider.ts +24 -24
- package/src/auth/constants.ts +14 -14
- package/src/auth/generate-refresh-token.test.ts +23 -23
- package/src/auth/hash-provider.test.ts +55 -55
- package/src/auth/jose-token-service.ts +62 -62
- package/src/auth/jwt-claims-passthrough.test.ts +87 -87
- package/src/auth/login.test.ts +100 -100
- package/src/auth/login.ts +78 -78
- package/src/auth/refresh.ts +11 -11
- package/src/auth/token-service.test.ts +90 -90
- package/src/factory.ts +370 -368
- package/src/features/automations/__tests__/action-executors.test.ts +255 -255
- package/src/features/automations/__tests__/automation-runner.test.ts +196 -196
- package/src/features/automations/__tests__/automation-runner.utils.test.ts +60 -60
- package/src/features/automations/__tests__/automations.handler.test.ts +264 -264
- package/src/features/automations/__tests__/automations.repository.test.ts +163 -163
- package/src/features/automations/__tests__/automations.schema.test.ts +224 -224
- package/src/features/automations/__tests__/context-resolver.test.ts +126 -126
- package/src/features/automations/__tests__/cron-runner.test.ts +267 -267
- package/src/features/automations/__tests__/cron-runner.utils.test.ts +144 -144
- package/src/features/automations/__tests__/set-variable.executor.test.ts +383 -383
- package/src/features/automations/__tests__/template-grammar.test.ts +308 -308
- package/src/features/automations/__tests__/webhook.executor.test.ts +142 -142
- package/src/features/automations/__tests__/when-evaluator.test.ts +274 -274
- package/src/features/automations/__tests__/when-pushdown.test.ts +281 -281
- package/src/features/automations/action-executors/create-entry.executor.ts +27 -27
- package/src/features/automations/action-executors/edit-field.executor.ts +26 -26
- package/src/features/automations/action-executors/index.ts +37 -37
- package/src/features/automations/action-executors/send-mail.executor.ts +53 -53
- package/src/features/automations/action-executors/set-variable.executor.ts +159 -159
- package/src/features/automations/action-executors/webhook.executor.ts +67 -67
- package/src/features/automations/automation-runner.ts +85 -85
- package/src/features/automations/automation-runner.utils.ts +47 -47
- package/src/features/automations/automations.handler.ts +197 -197
- package/src/features/automations/automations.schema.ts +177 -177
- package/src/features/automations/context-resolver.ts +152 -152
- package/src/features/automations/cron-runner.ts +140 -140
- package/src/features/automations/cron-runner.utils.ts +44 -44
- package/src/features/automations/filter-translation.ts +46 -46
- package/src/features/automations/index.ts +16 -16
- package/src/features/automations/template-grammar.ts +245 -245
- package/src/features/automations/var-access-resolver.ts +140 -140
- package/src/features/automations/when-evaluator.ts +87 -87
- package/src/features/automations/when-pushdown.ts +57 -57
- package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -359
- package/src/features/backrefs/backrefs.handler.ts +168 -168
- package/src/features/backrefs/d1-backref.repository.ts +84 -84
- package/src/features/backrefs/index.ts +5 -5
- package/src/features/content/constants.ts +16 -16
- package/src/features/content/handlers/bulk.handler.ts +185 -185
- package/src/features/content/handlers/create.ts +129 -129
- package/src/features/content/handlers/delete.ts +62 -62
- package/src/features/content/handlers/facets.ts +49 -49
- package/src/features/content/handlers/get.ts +120 -120
- package/src/features/content/handlers/helpers.test.ts +180 -180
- package/src/features/content/handlers/helpers.ts +93 -93
- package/src/features/content/handlers/list.ts +174 -174
- package/src/features/content/handlers/update.ts +174 -174
- package/src/features/content/index.ts +26 -26
- package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +388 -0
- package/src/features/dashboard-layout/dashboard-layout.handler.ts +206 -0
- package/src/features/dashboard-layout/index.ts +5 -0
- package/src/features/draft/draft.handler.ts +195 -195
- package/src/features/draft/draft.middleware.ts +66 -66
- package/src/features/draft/index.ts +5 -5
- package/src/features/email/email.provider.ts +42 -42
- package/src/features/email/email.service.ts +98 -98
- package/src/features/email/email.types.ts +119 -119
- package/src/features/email/index.ts +33 -33
- package/src/features/email/providers/resend.ts +67 -67
- package/src/features/email/providers/smtp.ts +55 -55
- package/src/features/email/templates/automation-mail.ts +19 -19
- package/src/features/email/templates/password-changed.ts +63 -63
- package/src/features/email/templates/password-reset.ts +68 -68
- package/src/features/email/templates/shell.ts +96 -96
- package/src/features/notifications/index.ts +5 -5
- package/src/features/notifications/notifications.handler.ts +105 -105
- package/src/features/password-reset/index.ts +19 -19
- package/src/features/password-reset/request.ts +92 -92
- package/src/features/password-reset/reset.ts +102 -102
- package/src/features/rotate-field/index.ts +5 -5
- package/src/features/rotate-field/rotate-field.handler.ts +132 -132
- package/src/features/rotate-field/rotate-field.schema.ts +17 -17
- package/src/features/schema/schema.handler.ts +131 -131
- package/src/features/seeds/index.ts +5 -5
- package/src/features/seeds/seeds.handler.test.ts +1199 -632
- package/src/features/seeds/seeds.handler.ts +558 -693
- package/src/features/settings/__tests__/settings.handler.test.ts +555 -555
- package/src/features/settings/settings.handler.ts +392 -392
- package/src/features/setup/index.ts +288 -204
- package/src/features/stats/index.ts +5 -5
- package/src/features/stats/stats.handler.ts +420 -420
- package/src/features/webhooks/index.ts +63 -63
- package/src/index.ts +65 -65
- package/src/media-utils.ts +82 -82
- package/src/middleware/auth-providers.middleware.test.ts +85 -0
- package/src/middleware/auth-providers.middleware.ts +45 -45
- package/src/middleware/observability.middleware.test.ts +93 -0
- package/src/middleware/observability.middleware.ts +70 -70
- package/src/middleware/rate-limit.middleware.ts +45 -45
- package/src/middleware/repository.middleware.ts +117 -114
- package/src/middleware/seed-registry.middleware.test.ts +97 -97
- package/src/middleware/seed-registry.middleware.ts +21 -21
- package/src/middleware/storage.middleware.ts +25 -25
- package/src/middleware.ts +45 -45
- package/src/public/access-policy.ts +27 -27
- package/src/public/api-key-middleware.ts +57 -57
- package/src/public/cache-utils.ts +38 -38
- package/src/public/entry-projection.ts +46 -46
- package/src/public/idempotency.ts +23 -23
- package/src/public/index.ts +16 -16
- package/src/public/problem-details.ts +124 -124
- package/src/public/public-add.ts +114 -114
- package/src/public/public-edit.ts +163 -163
- package/src/public/public-errors.ts +19 -19
- package/src/public/public-read.ts +63 -63
- package/src/public/public-routes.ts +88 -88
- package/src/public/query-builder.test.ts +224 -224
- package/src/public/query-builder.ts +156 -156
- package/src/public/rate-limit-middleware.ts +34 -34
- package/src/public/read-list.ts +54 -54
- package/src/public/read-single.ts +48 -48
- package/src/public/response-builder.ts +30 -30
- package/src/public/sanitize.ts +69 -69
- package/src/public/slug-utils.ts +18 -18
- package/src/rate-limit/cloudflare-rate-limiter.test.ts +30 -30
- package/src/rate-limit/cloudflare-rate-limiter.ts +15 -15
- package/src/rate-limit/in-memory-rate-limiter.test.ts +37 -37
- package/src/rate-limit/in-memory-rate-limiter.ts +17 -17
- package/src/rate-limit/no-op-rate-limiter.test.ts +22 -22
- package/src/rate-limit/no-op-rate-limiter.ts +11 -11
- package/src/search-utils.test.ts +211 -211
- package/src/search-utils.ts +213 -213
- package/src/search.ts +65 -65
- package/src/shared/apply-policies.test.ts +81 -81
- package/src/shared/apply-policies.ts +67 -67
- package/src/shared/automations.repository.d1.ts +150 -150
- package/src/shared/background-notification-service.test.ts +62 -62
- package/src/shared/background-notification-service.ts +52 -52
- package/src/shared/base.repository.d1.ts +32 -32
- package/src/shared/content-utils.test.ts +165 -165
- package/src/shared/content-utils.ts +86 -86
- package/src/shared/content.repository.d1.test.ts +507 -458
- package/src/shared/content.repository.d1.ts +851 -851
- package/src/shared/d1-activity-log.repository.test.ts +140 -140
- package/src/shared/d1-activity-log.repository.ts +105 -105
- package/src/shared/d1-activity-logger.test.ts +86 -86
- package/src/shared/d1-activity-logger.ts +67 -67
- package/src/shared/d1-analytics.repository.test.ts +78 -78
- package/src/shared/d1-analytics.repository.ts +85 -85
- package/src/shared/d1-content-scan.repository.test.ts +80 -80
- package/src/shared/d1-content-scan.repository.ts +37 -37
- package/src/shared/d1-notification.repository.test.ts +128 -128
- package/src/shared/d1-notification.repository.ts +118 -118
- package/src/shared/d1-password-reset-token.repository.test.ts +81 -81
- package/src/shared/d1-password-reset-token.repository.ts +56 -56
- package/src/shared/d1-search.repository.test.ts +87 -87
- package/src/shared/d1-search.repository.ts +88 -88
- package/src/shared/d1-session.repository.test.ts +125 -125
- package/src/shared/d1-session.repository.ts +102 -102
- package/src/shared/d1-setup-checklist.repository.ts +36 -36
- package/src/shared/d1-user.repository.test.ts +151 -151
- package/src/shared/d1-user.repository.ts +119 -119
- package/src/shared/d1-widget.repository.test.ts +267 -221
- package/src/shared/d1-widget.repository.ts +385 -341
- package/src/shared/dashboard-layout.repository.d1.test.ts +135 -0
- package/src/shared/dashboard-layout.repository.d1.ts +58 -0
- package/src/shared/demo-data-sql.ts +216 -54
- package/src/shared/demo-data.repository.d1.ts +20 -20
- package/src/shared/execution-context-scheduler.ts +13 -13
- package/src/shared/fixed-clock.ts +25 -25
- package/src/shared/fts-sync.ts +8 -8
- package/src/shared/idempotency.repository.d1.test.ts +83 -83
- package/src/shared/idempotency.repository.d1.ts +60 -60
- package/src/shared/in-memory-activity-logger.ts +19 -19
- package/src/shared/in-memory-notification-service.ts +19 -19
- package/src/shared/in-memory-seed.repository.ts +58 -51
- package/src/shared/media.repository.d1.test.ts +107 -107
- package/src/shared/media.repository.d1.ts +68 -68
- package/src/shared/qstash-notification-service.test.ts +67 -67
- package/src/shared/qstash-notification-service.ts +55 -55
- package/src/shared/query-utils.ts +141 -141
- package/src/shared/request-utils.ts +26 -26
- package/src/shared/schema-mutator.d1.test.ts +142 -0
- package/src/shared/schema-mutator.d1.ts +64 -64
- package/src/shared/seed-layout.repository.d1.test.ts +114 -114
- package/src/shared/seed-layout.repository.d1.ts +62 -62
- package/src/shared/seed-registry-cache.test.ts +68 -68
- package/src/shared/seed-registry-cache.ts +49 -49
- package/src/shared/seed.repository.d1.test.ts +143 -143
- package/src/shared/seed.repository.d1.ts +98 -98
- package/src/shared/sequential-id-generator.ts +33 -33
- package/src/shared/site-settings.repository.d1.ts +53 -53
- package/src/shared/storage/factory.ts +41 -41
- package/src/shared/storage/s3-bucket.ts +195 -195
- package/src/shared/storage-utils.ts +40 -40
- package/src/shared/system-stats.repository.d1.test.ts +58 -58
- package/src/shared/system-stats.repository.d1.ts +48 -48
- package/src/types.ts +90 -87
- package/src/upload.ts +198 -198
- package/src/widget.test.ts +85 -0
- package/src/widget.ts +251 -212
- package/assets/dashboard/assets/index-BSjk0Sx9.js +0 -634
- 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
|