@beechcms/api 0.4.3 → 0.6.0-preview.1
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/Searching.svg +1 -0
- package/assets/dashboard/assets/index-BSjk0Sx9.js +634 -0
- package/assets/dashboard/assets/index-kRCYVN2B.css +1 -0
- package/assets/dashboard/index.html +2 -2
- package/assets/dashboard/noResult.svg +43 -0
- package/assets/dashboard/working.svg +1 -0
- package/migrations/0000_v040_base.sql +27 -0
- package/migrations/0030_test_seeds.sql +125 -0
- package/package.json +14 -9
- package/src/auth/{in-memory-hash-provider.ts → __fixtures__/in-memory-hash-provider.ts} +4 -0
- package/src/auth/{static-token-service.ts → __fixtures__/static-token-service.ts} +4 -0
- package/src/auth/bcrypt-hash-provider.ts +6 -2
- package/src/auth/constants.ts +4 -0
- package/src/auth/generate-refresh-token.test.ts +8 -4
- package/src/auth/hash-provider.test.ts +14 -5
- package/src/auth/jose-token-service.ts +7 -0
- package/src/auth/jwt-claims-passthrough.test.ts +87 -0
- package/src/auth/login.test.ts +9 -1
- package/src/auth/login.ts +5 -1
- package/src/auth/refresh.ts +7 -1
- package/src/auth/token-service.test.ts +9 -1
- package/src/factory.ts +37 -16
- package/src/features/automations/__tests__/action-executors.test.ts +59 -72
- package/src/features/automations/__tests__/automation-runner.test.ts +4 -0
- package/src/features/automations/__tests__/automation-runner.utils.test.ts +4 -0
- package/src/features/automations/__tests__/automations.handler.test.ts +7 -3
- package/src/features/automations/__tests__/automations.repository.test.ts +4 -0
- package/src/features/automations/__tests__/automations.schema.test.ts +92 -2
- package/src/features/automations/__tests__/context-resolver.test.ts +4 -0
- package/src/features/automations/__tests__/cron-runner.test.ts +11 -7
- package/src/features/automations/__tests__/cron-runner.utils.test.ts +4 -0
- package/src/features/automations/__tests__/set-variable.executor.test.ts +77 -0
- package/src/features/automations/__tests__/template-grammar.test.ts +4 -0
- package/src/features/automations/__tests__/webhook.executor.test.ts +142 -0
- package/src/features/automations/__tests__/when-evaluator.test.ts +4 -0
- package/src/features/automations/__tests__/when-pushdown.test.ts +5 -1
- package/src/features/automations/action-executors/create-entry.executor.ts +4 -0
- package/src/features/automations/action-executors/edit-field.executor.ts +4 -0
- package/src/features/automations/action-executors/index.ts +5 -1
- package/src/features/automations/action-executors/send-mail.executor.ts +13 -2
- package/src/features/automations/action-executors/set-variable.executor.ts +15 -2
- package/src/features/automations/action-executors/webhook.executor.ts +55 -13
- package/src/features/automations/automation-runner.ts +4 -0
- package/src/features/automations/automation-runner.utils.ts +4 -0
- package/src/features/automations/automations.handler.ts +4 -0
- package/src/features/automations/automations.schema.ts +19 -2
- package/src/features/automations/context-resolver.ts +4 -0
- package/src/features/automations/cron-runner.ts +4 -0
- package/src/features/automations/cron-runner.utils.ts +4 -0
- package/src/features/automations/filter-translation.ts +4 -0
- package/src/features/automations/index.ts +4 -0
- package/src/features/automations/template-grammar.ts +4 -0
- package/src/features/automations/var-access-resolver.ts +4 -0
- package/src/features/automations/when-evaluator.ts +4 -0
- package/src/features/automations/when-pushdown.ts +4 -0
- package/src/features/backrefs/__tests__/backrefs.handler.test.ts +359 -0
- package/src/features/backrefs/backrefs.handler.ts +168 -0
- package/src/features/backrefs/d1-backref.repository.ts +84 -0
- package/src/features/backrefs/index.ts +5 -0
- package/src/features/content/constants.ts +6 -0
- package/src/features/content/handlers/bulk.handler.ts +185 -0
- package/src/features/content/handlers/create.ts +19 -55
- package/src/features/content/handlers/delete.ts +12 -37
- package/src/features/content/handlers/facets.ts +4 -0
- package/src/features/content/handlers/get.ts +4 -0
- package/src/features/content/handlers/helpers.test.ts +180 -0
- package/src/features/content/handlers/helpers.ts +93 -0
- package/src/features/content/handlers/list.ts +105 -19
- package/src/features/content/handlers/update.ts +19 -64
- package/src/features/content/index.ts +6 -0
- package/src/features/draft/draft.handler.ts +33 -8
- package/src/features/draft/draft.middleware.ts +4 -0
- package/src/features/draft/index.ts +4 -0
- package/src/features/email/email.provider.ts +4 -0
- package/src/features/email/email.service.ts +50 -45
- package/src/features/email/email.types.ts +12 -1
- package/src/features/email/index.ts +4 -0
- package/src/features/email/providers/resend.ts +4 -0
- package/src/features/email/providers/smtp.ts +55 -0
- package/src/features/email/templates/automation-mail.ts +4 -0
- package/src/features/email/templates/password-changed.ts +4 -0
- package/src/features/email/templates/password-reset.ts +4 -0
- package/src/features/email/templates/shell.ts +4 -0
- package/src/features/notifications/index.ts +4 -0
- package/src/features/notifications/notifications.handler.ts +4 -0
- package/src/features/password-reset/index.ts +5 -1
- package/src/features/password-reset/request.ts +12 -2
- package/src/features/password-reset/reset.ts +12 -2
- package/src/features/rotate-field/index.ts +4 -0
- package/src/features/rotate-field/rotate-field.handler.ts +4 -0
- package/src/features/rotate-field/rotate-field.schema.ts +4 -0
- package/src/features/schema/schema.handler.ts +118 -3
- package/src/features/seeds/index.ts +5 -0
- package/src/features/seeds/seeds.handler.test.ts +632 -0
- package/src/features/seeds/seeds.handler.ts +693 -0
- package/src/features/settings/__tests__/settings.handler.test.ts +555 -0
- package/src/features/settings/settings.handler.ts +98 -8
- package/src/features/setup/index.ts +124 -11
- package/src/features/stats/index.ts +4 -0
- package/src/features/stats/stats.handler.ts +11 -21
- package/src/features/webhooks/index.ts +63 -0
- package/src/index.ts +28 -19
- package/src/media-utils.ts +4 -0
- package/src/middleware/auth-providers.middleware.ts +13 -0
- package/src/middleware/observability.middleware.ts +21 -3
- package/src/middleware/rate-limit.middleware.ts +4 -0
- package/src/middleware/repository.middleware.ts +25 -2
- package/src/middleware/seed-registry.middleware.test.ts +97 -0
- package/src/middleware/seed-registry.middleware.ts +21 -0
- package/src/middleware/storage.middleware.ts +4 -0
- package/src/middleware.ts +5 -7
- package/src/public/access-policy.ts +4 -0
- package/src/public/api-key-middleware.ts +4 -0
- package/src/public/cache-utils.ts +38 -34
- package/src/public/entry-projection.ts +46 -42
- package/src/public/idempotency.ts +23 -19
- package/src/public/index.ts +4 -0
- package/src/public/problem-details.ts +71 -0
- package/src/public/public-add.ts +4 -0
- package/src/public/public-edit.ts +4 -0
- package/src/public/public-errors.ts +4 -0
- package/src/public/public-read.ts +4 -0
- package/src/public/public-routes.ts +4 -0
- package/src/public/query-builder.test.ts +4 -0
- package/src/public/query-builder.ts +4 -0
- package/src/public/rate-limit-middleware.ts +4 -0
- package/src/public/read-list.ts +54 -50
- package/src/public/read-single.ts +48 -44
- package/src/public/response-builder.ts +4 -0
- package/src/public/sanitize.ts +4 -0
- package/src/public/slug-utils.ts +4 -0
- package/src/rate-limit/cloudflare-rate-limiter.test.ts +4 -0
- package/src/rate-limit/cloudflare-rate-limiter.ts +4 -0
- package/src/rate-limit/in-memory-rate-limiter.test.ts +4 -0
- package/src/rate-limit/in-memory-rate-limiter.ts +4 -0
- package/src/rate-limit/no-op-rate-limiter.test.ts +4 -0
- package/src/rate-limit/no-op-rate-limiter.ts +4 -0
- package/src/search-utils.test.ts +4 -0
- package/src/search-utils.ts +5 -1
- package/src/search.ts +5 -1
- package/src/shared/apply-policies.test.ts +4 -0
- package/src/shared/apply-policies.ts +4 -0
- package/src/shared/automations.repository.d1.ts +4 -0
- package/src/shared/background-notification-service.test.ts +4 -0
- package/src/shared/background-notification-service.ts +4 -0
- package/src/shared/base.repository.d1.ts +4 -0
- package/src/shared/content-utils.test.ts +4 -0
- package/src/shared/content-utils.ts +4 -0
- package/src/shared/content.repository.d1.test.ts +147 -1
- package/src/shared/content.repository.d1.ts +535 -66
- package/src/shared/d1-activity-log.repository.test.ts +4 -0
- package/src/shared/d1-activity-log.repository.ts +4 -0
- package/src/shared/d1-activity-logger.test.ts +4 -0
- package/src/shared/d1-activity-logger.ts +4 -0
- package/src/shared/d1-analytics.repository.test.ts +4 -0
- package/src/shared/d1-analytics.repository.ts +4 -0
- package/src/shared/d1-content-scan.repository.test.ts +9 -5
- package/src/shared/d1-content-scan.repository.ts +15 -7
- package/src/shared/d1-notification.repository.test.ts +4 -0
- package/src/shared/d1-notification.repository.ts +4 -0
- package/src/shared/d1-password-reset-token.repository.test.ts +4 -0
- package/src/shared/d1-password-reset-token.repository.ts +4 -0
- package/src/shared/d1-search.repository.test.ts +4 -0
- package/src/shared/d1-search.repository.ts +4 -0
- package/src/shared/d1-session.repository.test.ts +4 -0
- package/src/shared/d1-session.repository.ts +4 -0
- package/src/shared/d1-setup-checklist.repository.ts +36 -0
- package/src/shared/d1-user.repository.test.ts +8 -4
- package/src/shared/d1-user.repository.ts +15 -5
- package/src/shared/d1-widget.repository.test.ts +4 -0
- package/src/shared/d1-widget.repository.ts +4 -0
- package/src/shared/demo-data-sql.ts +54 -0
- package/src/shared/demo-data.repository.d1.ts +20 -0
- package/src/shared/execution-context-scheduler.ts +4 -0
- package/src/shared/fixed-clock.ts +4 -0
- package/src/shared/fts-sync.ts +4 -0
- package/src/shared/idempotency.repository.d1.test.ts +4 -0
- package/src/shared/idempotency.repository.d1.ts +4 -0
- package/src/shared/in-memory-activity-logger.ts +4 -0
- package/src/shared/in-memory-notification-service.ts +4 -0
- package/src/shared/in-memory-seed.repository.ts +51 -0
- package/src/shared/media.repository.d1.test.ts +4 -0
- package/src/shared/media.repository.d1.ts +4 -0
- package/src/shared/qstash-notification-service.test.ts +67 -0
- package/src/shared/qstash-notification-service.ts +55 -0
- package/src/shared/query-utils.ts +4 -0
- package/src/shared/request-utils.ts +4 -0
- package/src/shared/schema-mutator.d1.ts +64 -0
- package/src/shared/seed-layout.repository.d1.test.ts +114 -0
- package/src/shared/seed-layout.repository.d1.ts +62 -0
- package/src/shared/seed-registry-cache.test.ts +68 -0
- package/src/shared/seed-registry-cache.ts +49 -0
- package/src/shared/seed.repository.d1.test.ts +143 -0
- package/src/shared/seed.repository.d1.ts +98 -0
- package/src/shared/sequential-id-generator.ts +11 -0
- package/src/shared/site-settings.repository.d1.ts +53 -0
- package/src/shared/storage/factory.ts +16 -15
- package/src/shared/storage/s3-bucket.ts +34 -2
- package/src/shared/storage-utils.ts +4 -0
- package/src/shared/system-stats.repository.d1.test.ts +4 -0
- package/src/shared/system-stats.repository.d1.ts +5 -1
- package/src/types.ts +23 -3
- package/src/upload.ts +134 -123
- package/src/widget.ts +6 -2
- package/assets/dashboard/assets/index-BKWnlvnV.css +0 -1
- package/assets/dashboard/assets/index-BMkd1Irh.js +0 -629
- package/src/shared/storage/r2-binding-bucket.ts +0 -81
package/src/public/read-list.ts
CHANGED
|
@@ -1,50 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
data,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 type { Seed, ContentRepository } from '@beechcms/core'
|
|
6
|
+
import { cleanStr } from '../shared/query-utils'
|
|
7
|
+
import { toFlatPublicEntry } from './entry-projection'
|
|
8
|
+
import { buildPublicListMeta } from './response-builder'
|
|
9
|
+
import { parsePublicFilter, parsePublicPagination, parseLatestCount, toEngineFilters } from './query-builder'
|
|
10
|
+
|
|
11
|
+
type ReadListInput = {
|
|
12
|
+
seed: Seed
|
|
13
|
+
seedSlug: string
|
|
14
|
+
repository: ContentRepository
|
|
15
|
+
query: Record<string, string | undefined>
|
|
16
|
+
publishedOnly: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function readListEntries(input: ReadListInput) {
|
|
20
|
+
const { seed, seedSlug, repository, query, publishedOnly } = input
|
|
21
|
+
|
|
22
|
+
const parsedFilter = parsePublicFilter(query.filter)
|
|
23
|
+
const allMode = cleanStr(query.all)?.toLowerCase() === 'true'
|
|
24
|
+
const latestMode = cleanStr(query.latest) !== null
|
|
25
|
+
const latestCount = latestMode ? parseLatestCount(query.latest ?? '') : null
|
|
26
|
+
const pagination = allMode ? { page: 1, limit: 100 } : parsePublicPagination(query)
|
|
27
|
+
const offset = (pagination.page - 1) * pagination.limit
|
|
28
|
+
const search = cleanStr(query.search) ?? ''
|
|
29
|
+
const engineFilters = toEngineFilters(seed, parsedFilter)
|
|
30
|
+
const sortBy = cleanStr(query.orderBy) ?? 'created_at'
|
|
31
|
+
const sortDir = (cleanStr(query.orderDir) ?? 'desc').toLowerCase() === 'asc' ? 'ASC' : 'DESC'
|
|
32
|
+
|
|
33
|
+
const { items, total } = await repository.findMany(seed, {
|
|
34
|
+
filters: engineFilters,
|
|
35
|
+
search: search || undefined,
|
|
36
|
+
status: publishedOnly ? 'published' : null,
|
|
37
|
+
pagination: {
|
|
38
|
+
limit: latestMode ? (latestCount ?? 10) : pagination.limit,
|
|
39
|
+
offset: latestMode ? 0 : offset,
|
|
40
|
+
},
|
|
41
|
+
orderBy: latestMode ? { column: 'created_at', dir: 'DESC' } : { column: sortBy, dir: sortDir },
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const data = items.map(item => toFlatPublicEntry(item, seed, query.fields))
|
|
45
|
+
|
|
46
|
+
if (latestMode) {
|
|
47
|
+
return { data, meta: { total, returned: data.length, seed: seedSlug } }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
data,
|
|
52
|
+
meta: buildPublicListMeta({ total, page: pagination.page, limit: pagination.limit, returned: data.length, seed: seedSlug }),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -1,44 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
ok:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
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 { EntryNotFoundError } from '@beechcms/core'
|
|
6
|
+
import type { Seed, ContentRepository } from '@beechcms/core'
|
|
7
|
+
import { toFlatPublicEntry } from './entry-projection'
|
|
8
|
+
import { buildPublicSingleMeta } from './response-builder'
|
|
9
|
+
|
|
10
|
+
type ReadSingleInput = {
|
|
11
|
+
seed: Seed
|
|
12
|
+
seedSlug: string
|
|
13
|
+
repository: ContentRepository
|
|
14
|
+
id: string | null
|
|
15
|
+
slug: string | null
|
|
16
|
+
publishedOnly: boolean
|
|
17
|
+
fieldsParam?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ReadSingleResult =
|
|
21
|
+
| { ok: true; data: Record<string, unknown>; meta: { seed: string } }
|
|
22
|
+
| { ok: false; detail: string }
|
|
23
|
+
|
|
24
|
+
export async function readSingleEntry(input: ReadSingleInput): Promise<ReadSingleResult> {
|
|
25
|
+
const { seed, seedSlug, repository, id, slug, publishedOnly, fieldsParam } = input
|
|
26
|
+
const label = id ?? slug!
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const entry = id
|
|
30
|
+
? await repository.findById(seed, id)
|
|
31
|
+
: await repository.findBySlug(seed, slug!)
|
|
32
|
+
|
|
33
|
+
if (publishedOnly && entry.status !== 'published') {
|
|
34
|
+
return { ok: false, detail: `Entry '${label}' not found or not published.` }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
data: toFlatPublicEntry(entry, seed, fieldsParam),
|
|
40
|
+
meta: buildPublicSingleMeta(seedSlug),
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error instanceof EntryNotFoundError) {
|
|
44
|
+
return { ok: false, detail: `Entry '${label}' not found for content type '${seedSlug}'.` }
|
|
45
|
+
}
|
|
46
|
+
throw error
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/public/sanitize.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
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
|
+
|
|
1
5
|
import { validateAndSanitizeSeedPayload } from '@beechcms/core'
|
|
2
6
|
import type { Seed, ValidationDetail } from '@beechcms/core'
|
|
3
7
|
|
package/src/public/slug-utils.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
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
|
+
|
|
1
5
|
import { describe, it, expect, vi } from 'vitest'
|
|
2
6
|
import { CloudflareRateLimiter } from './cloudflare-rate-limiter'
|
|
3
7
|
|
|
@@ -1,3 +1,7 @@
|
|
|
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
|
+
|
|
1
5
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
6
|
import type { IRateLimiter, RateLimitResult } from '@beechcms/core'
|
|
3
7
|
|
|
@@ -1,3 +1,7 @@
|
|
|
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
|
+
|
|
1
5
|
import { describe, it, expect } from 'vitest'
|
|
2
6
|
import { InMemoryRateLimiter } from './in-memory-rate-limiter'
|
|
3
7
|
|
|
@@ -1,3 +1,7 @@
|
|
|
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
|
+
|
|
1
5
|
import type { IRateLimiter, RateLimitResult } from '@beechcms/core'
|
|
2
6
|
|
|
3
7
|
export class InMemoryRateLimiter implements IRateLimiter {
|
|
@@ -1,3 +1,7 @@
|
|
|
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
|
+
|
|
1
5
|
import { describe, it, expect } from 'vitest'
|
|
2
6
|
import { NoOpRateLimiter } from './no-op-rate-limiter'
|
|
3
7
|
|
|
@@ -1,3 +1,7 @@
|
|
|
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
|
+
|
|
1
5
|
import type { IRateLimiter, RateLimitResult } from '@beechcms/core'
|
|
2
6
|
|
|
3
7
|
export class NoOpRateLimiter implements IRateLimiter {
|
package/src/search-utils.test.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
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
|
+
|
|
1
5
|
import { describe, it, expect } from 'vitest'
|
|
2
6
|
import { encodeCursor, decodeCursor, buildFtsQuery, mapFtsRow } from './search-utils'
|
|
3
7
|
import type { Seed } from '@beechcms/core'
|
package/src/search-utils.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
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
|
+
|
|
1
5
|
// apps/api/src/search-utils.ts
|
|
2
6
|
// Pure functions — zero Hono dependencies, importable from Vitest.
|
|
3
7
|
// v0.4.0: FTS is per-seed (fts_{slug}), joined with content_{slug} for metadata.
|
|
@@ -53,7 +57,7 @@ export function decodeCursor(cursor: string): { rank: number; entryId: string }
|
|
|
53
57
|
const sep = decoded.lastIndexOf(":")
|
|
54
58
|
if (sep === -1) return null
|
|
55
59
|
return {
|
|
56
|
-
rank: parseFloat(decoded.slice(0, sep)),
|
|
60
|
+
rank: Number.parseFloat(decoded.slice(0, sep)),
|
|
57
61
|
entryId: decoded.slice(sep + 1),
|
|
58
62
|
}
|
|
59
63
|
} catch {
|
package/src/search.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
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
|
+
|
|
1
5
|
// apps/api/src/search.ts
|
|
2
6
|
|
|
3
7
|
import { Hono } from "hono"
|
|
@@ -18,7 +22,7 @@ searchRouter.get("/", async (c) => {
|
|
|
18
22
|
const queryText = c.req.query("q")?.trim() ?? ""
|
|
19
23
|
const schemaSlug = c.req.query("schema_slug") ?? null
|
|
20
24
|
const status = c.req.query("status") ?? null
|
|
21
|
-
const rawLimit = parseInt(c.req.query("limit") ?? "20", 10)
|
|
25
|
+
const rawLimit = Number.parseInt(c.req.query("limit") ?? "20", 10)
|
|
22
26
|
const limit = Math.min(Math.max(rawLimit, 1), 50)
|
|
23
27
|
const cursor = c.req.query("cursor") ?? null
|
|
24
28
|
|
|
@@ -1,3 +1,7 @@
|
|
|
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
|
+
|
|
1
5
|
import { describe, it, expect } from 'vitest'
|
|
2
6
|
import { applyPrivacy, applyVisibility, PrivacyPolicyError } from './apply-policies'
|
|
3
7
|
import type { Seed } from '@beechcms/core'
|
|
@@ -1,3 +1,7 @@
|
|
|
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
|
+
|
|
1
5
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
6
|
import { resolvePolicies, sha256hex } from '@beechcms/core'
|
|
3
7
|
import type { Seed } from '@beechcms/core'
|
|
@@ -1,3 +1,7 @@
|
|
|
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
|
+
|
|
1
5
|
import { describe, it, expect, vi } from 'vitest'
|
|
2
6
|
import { BackgroundNotificationService } from './background-notification-service'
|
|
3
7
|
import type { INotificationRepository, NotificationRecord } from '@beechcms/core'
|
|
@@ -1,3 +1,7 @@
|
|
|
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
|
+
|
|
1
5
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
6
|
import { RepositoryError } from '@beechcms/core'
|
|
3
7
|
|
|
@@ -1,3 +1,7 @@
|
|
|
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
|
+
|
|
1
5
|
import { describe, it, expect, vi } from 'vitest'
|
|
2
6
|
import { rowToApiData, rowToEntry, buildInsertBindings, buildUpdateBindings, hasDraft } from './content-utils'
|
|
3
7
|
import type { Seed } from '@beechcms/core'
|
|
@@ -1,3 +1,7 @@
|
|
|
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
|
+
|
|
1
5
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
6
|
import { deserializeFromDb, serializeForDb } from '@beechcms/core'
|
|
3
7
|
import type { Seed } from '@beechcms/core'
|
|
@@ -1,6 +1,10 @@
|
|
|
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
|
+
|
|
1
5
|
import { describe, it, expect, vi } from 'vitest'
|
|
2
6
|
import { D1ContentRepository } from './content.repository.d1'
|
|
3
|
-
import { EntryNotFoundError, SlugConflictError } from '@beechcms/core'
|
|
7
|
+
import { EntryNotFoundError, SlugConflictError, RelationTargetNotFoundError } from '@beechcms/core'
|
|
4
8
|
import type { Seed } from '@beechcms/core'
|
|
5
9
|
|
|
6
10
|
const SEED = {
|
|
@@ -20,6 +24,17 @@ const NO_DRAFT_SEED = {
|
|
|
20
24
|
branches: [{ id: 'br_01', alias: 'name', type: 'text' }],
|
|
21
25
|
} as unknown as Seed
|
|
22
26
|
|
|
27
|
+
// Seed with a multi-relation branch
|
|
28
|
+
const M2M_SEED = {
|
|
29
|
+
slug: 'articles',
|
|
30
|
+
displayNameAlias: 'title',
|
|
31
|
+
allowDrafts: true,
|
|
32
|
+
branches: [
|
|
33
|
+
{ id: 'br_01', alias: 'title', type: 'text' },
|
|
34
|
+
{ id: 'br_02', alias: 'tags', type: 'relation', multiple: true, targetSeed: 'tag' },
|
|
35
|
+
],
|
|
36
|
+
} as unknown as Seed
|
|
37
|
+
|
|
23
38
|
function makeMockDb(opts: {
|
|
24
39
|
firstResult?: unknown
|
|
25
40
|
allResults?: unknown[]
|
|
@@ -309,4 +324,135 @@ describe('D1ContentRepository', () => {
|
|
|
309
324
|
expect(prepareMock).toHaveBeenCalledWith(expect.stringContaining('content_posts_drafts'))
|
|
310
325
|
})
|
|
311
326
|
})
|
|
327
|
+
|
|
328
|
+
// ─── many-to-many (Sprint 5) ──────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
describe('many-to-many relations', () => {
|
|
331
|
+
it('create: multi-relation values go to junction table, not main INSERT', async () => {
|
|
332
|
+
const { db, prepareMock, batchMock } = makeMockDb({ firstResult: null })
|
|
333
|
+
batchMock.mockResolvedValue([{ success: true, meta: { changes: 1 } }])
|
|
334
|
+
|
|
335
|
+
await new D1ContentRepository(db).create(
|
|
336
|
+
M2M_SEED, 'e1', 'art-1', 'draft',
|
|
337
|
+
{ title: 'Hello', tags: ['tag-1', 'tag-2'] },
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
const sqls = prepareMock.mock.calls.map((c: any[]) => c[0] as string)
|
|
341
|
+
// Main INSERT must not contain 'tags'
|
|
342
|
+
const insertSql = sqls.find(s => s.includes('INSERT INTO content_articles '))
|
|
343
|
+
expect(insertSql).toBeDefined()
|
|
344
|
+
expect(insertSql).not.toContain('tags')
|
|
345
|
+
// Junction INSERTs must exist
|
|
346
|
+
const junctionInserts = sqls.filter(s => s.includes('rel_articles_tags'))
|
|
347
|
+
expect(junctionInserts.length).toBeGreaterThan(0)
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('create: uses batch when multi-relation values present', async () => {
|
|
351
|
+
const { db, batchMock } = makeMockDb({ firstResult: null })
|
|
352
|
+
batchMock.mockResolvedValue([{ success: true }])
|
|
353
|
+
|
|
354
|
+
await new D1ContentRepository(db).create(
|
|
355
|
+
M2M_SEED, 'e1', 'art-1', 'draft',
|
|
356
|
+
{ title: 'Hello', tags: ['tag-1'] },
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
expect(batchMock).toHaveBeenCalled()
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('update: replaces junction rows (DELETE + INSERT) in batch', async () => {
|
|
363
|
+
const { db, prepareMock, batchMock } = makeMockDb({ runChanges: 1 })
|
|
364
|
+
batchMock.mockResolvedValue([
|
|
365
|
+
{ meta: { changes: 1 } },
|
|
366
|
+
{ success: true },
|
|
367
|
+
{ success: true },
|
|
368
|
+
])
|
|
369
|
+
|
|
370
|
+
await new D1ContentRepository(db).update(
|
|
371
|
+
M2M_SEED, 'e1',
|
|
372
|
+
{ tags: ['tag-3', 'tag-4'] },
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
const sqls = prepareMock.mock.calls.map((c: any[]) => c[0] as string)
|
|
376
|
+
expect(sqls.some(s => s.includes('DELETE FROM rel_articles_tags WHERE parent_id'))).toBe(true)
|
|
377
|
+
expect(sqls.some(s => s.includes('INSERT INTO rel_articles_tags'))).toBe(true)
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it('update: empty array deletes all junction rows', async () => {
|
|
381
|
+
const { db, prepareMock, batchMock } = makeMockDb({ runChanges: 1 })
|
|
382
|
+
batchMock.mockResolvedValue([{ meta: { changes: 1 } }, { success: true }])
|
|
383
|
+
|
|
384
|
+
await new D1ContentRepository(db).update(M2M_SEED, 'e1', { tags: [] })
|
|
385
|
+
|
|
386
|
+
const sqls = prepareMock.mock.calls.map((c: any[]) => c[0] as string)
|
|
387
|
+
expect(sqls.some(s => s.includes('DELETE FROM rel_articles_tags WHERE parent_id'))).toBe(true)
|
|
388
|
+
// No INSERT because array is empty
|
|
389
|
+
expect(sqls.filter(s => s.includes('INSERT INTO rel_articles_tags'))).toHaveLength(0)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('findById: attaches multi-relation ids from junction table', async () => {
|
|
393
|
+
const row = { id: 'e1', slug: 'art-1', status: 'published', title: 'Hello', created_at: 0, updated_at: 0 }
|
|
394
|
+
const { db, firstMock, batchMock } = makeMockDb({ firstResult: row })
|
|
395
|
+
// First call: main SELECT; batch: junction query
|
|
396
|
+
batchMock.mockResolvedValueOnce([{ results: [{ target_id: 'tag-1' }, { target_id: 'tag-2' }] }])
|
|
397
|
+
|
|
398
|
+
const result = await new D1ContentRepository(db).findById(M2M_SEED, 'e1')
|
|
399
|
+
expect(result.tags).toEqual(['tag-1', 'tag-2'])
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('findById: attaches empty array when no junction rows', async () => {
|
|
403
|
+
const row = { id: 'e1', slug: 'art-1', status: 'published', title: 'Hello', created_at: 0, updated_at: 0 }
|
|
404
|
+
const { db, batchMock } = makeMockDb({ firstResult: row })
|
|
405
|
+
batchMock.mockResolvedValueOnce([{ results: [] }])
|
|
406
|
+
|
|
407
|
+
const result = await new D1ContentRepository(db).findById(M2M_SEED, 'e1')
|
|
408
|
+
expect(result.tags).toEqual([])
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('findMany: attaches multi-relation ids per entry', async () => {
|
|
412
|
+
const rows = [
|
|
413
|
+
{ id: 'e1', slug: 'art-1', status: 'published', title: 'A', created_at: 0, updated_at: 0 },
|
|
414
|
+
{ id: 'e2', slug: 'art-2', status: 'published', title: 'B', created_at: 0, updated_at: 0 },
|
|
415
|
+
]
|
|
416
|
+
const { db, batchMock } = makeMockDb()
|
|
417
|
+
// First batch: main select + count; second batch: junction query
|
|
418
|
+
batchMock
|
|
419
|
+
.mockResolvedValueOnce([{ results: rows }, { results: [{ total: 2 }] }])
|
|
420
|
+
.mockResolvedValueOnce([{
|
|
421
|
+
results: [
|
|
422
|
+
{ parent_id: 'e1', target_id: 'tag-1' },
|
|
423
|
+
{ parent_id: 'e1', target_id: 'tag-2' },
|
|
424
|
+
{ parent_id: 'e2', target_id: 'tag-3' },
|
|
425
|
+
],
|
|
426
|
+
}])
|
|
427
|
+
|
|
428
|
+
const result = await new D1ContentRepository(db).findMany(M2M_SEED, {})
|
|
429
|
+
expect(result.items[0].tags).toEqual(['tag-1', 'tag-2'])
|
|
430
|
+
expect(result.items[1].tags).toEqual(['tag-3'])
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
it('saveDraft: multi-relation values go to junction draft table', async () => {
|
|
434
|
+
const { db, prepareMock, batchMock } = makeMockDb()
|
|
435
|
+
batchMock.mockResolvedValue([{ success: true }])
|
|
436
|
+
|
|
437
|
+
await new D1ContentRepository(db).saveDraft(M2M_SEED, 'e1', { tags: ['tag-1', 'tag-2'] })
|
|
438
|
+
|
|
439
|
+
const sqls = prepareMock.mock.calls.map((c: any[]) => c[0] as string)
|
|
440
|
+
expect(sqls.some(s => s.includes('rel_articles_tags_drafts'))).toBe(true)
|
|
441
|
+
expect(batchMock).toHaveBeenCalled()
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
it('publishDraft: throws RelationTargetNotFoundError when draft multi-rel target missing', async () => {
|
|
445
|
+
const draftRow = { entry_id: 'e1', title: 'Hello' }
|
|
446
|
+
const { db, firstMock, allMock, batchMock } = makeMockDb()
|
|
447
|
+
firstMock
|
|
448
|
+
.mockResolvedValueOnce(draftRow) // getDraft row
|
|
449
|
+
.mockResolvedValueOnce(null) // target existence check → not found
|
|
450
|
+
allMock.mockResolvedValueOnce({ results: [{ target_id: 'missing-tag' }] })
|
|
451
|
+
batchMock.mockResolvedValue([{}])
|
|
452
|
+
|
|
453
|
+
await expect(
|
|
454
|
+
new D1ContentRepository(db).publishDraft(M2M_SEED, 'e1'),
|
|
455
|
+
).rejects.toBeInstanceOf(RelationTargetNotFoundError)
|
|
456
|
+
})
|
|
457
|
+
})
|
|
312
458
|
})
|