@beechcms/api 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/dashboard/assets/index-B1mUfgiK.css +1 -0
- package/assets/dashboard/assets/index-CHxxc_id.js +629 -0
- package/assets/dashboard/index.html +2 -2
- package/migrations/0030_test_seeds.sql +125 -0
- package/package.json +4 -3
- package/src/public/cache-utils.ts +34 -34
- package/src/public/entry-projection.ts +42 -42
- package/src/public/idempotency.ts +19 -19
- package/src/public/read-list.ts +50 -50
- package/src/public/read-single.ts +44 -44
- package/src/search-utils.ts +1 -1
- package/src/search.ts +1 -1
- package/assets/dashboard/assets/index-BKWnlvnV.css +0 -1
- package/assets/dashboard/assets/index-BMkd1Irh.js +0 -629
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
<!-- Poppins: pesi normali + italic. Inter: fallback con piena copertura degli stili. -->
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&family=Poppins:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet" />
|
|
11
11
|
<title>dashboard</title>
|
|
12
|
-
<script type="module" crossorigin src="/admin/assets/index-
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/admin/assets/index-
|
|
12
|
+
<script type="module" crossorigin src="/admin/assets/index-CHxxc_id.js"></script>
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/admin/assets/index-B1mUfgiK.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="root"></div>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
-- content_products
|
|
2
|
+
CREATE TABLE IF NOT EXISTS content_products (
|
|
3
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
4
|
+
slug TEXT NOT NULL UNIQUE,
|
|
5
|
+
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'review', 'published', 'archived')),
|
|
6
|
+
name TEXT NOT NULL,
|
|
7
|
+
description TEXT,
|
|
8
|
+
price REAL,
|
|
9
|
+
inventory REAL,
|
|
10
|
+
rating REAL,
|
|
11
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
12
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
CREATE INDEX IF NOT EXISTS idx_products_status ON content_products(status);
|
|
16
|
+
CREATE INDEX IF NOT EXISTS idx_products_created_at ON content_products(created_at);
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_products_name ON content_products(name);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_products_price ON content_products(price);
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_products_inventory ON content_products(inventory);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_products_rating ON content_products(rating);
|
|
21
|
+
|
|
22
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS fts_products USING fts5(
|
|
23
|
+
entry_id UNINDEXED,
|
|
24
|
+
name,
|
|
25
|
+
description,
|
|
26
|
+
tokenize = 'unicode61'
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE TRIGGER IF NOT EXISTS fts_products_insert
|
|
30
|
+
AFTER INSERT ON content_products BEGIN
|
|
31
|
+
INSERT INTO fts_products(entry_id, name, description) VALUES (new.id, new.name, new.description);
|
|
32
|
+
END;
|
|
33
|
+
|
|
34
|
+
CREATE TRIGGER IF NOT EXISTS fts_products_update
|
|
35
|
+
AFTER UPDATE OF name, description ON content_products BEGIN
|
|
36
|
+
DELETE FROM fts_products WHERE entry_id = old.id;
|
|
37
|
+
INSERT INTO fts_products(entry_id, name, description) VALUES (new.id, new.name, new.description);
|
|
38
|
+
END;
|
|
39
|
+
|
|
40
|
+
CREATE TRIGGER IF NOT EXISTS fts_products_delete
|
|
41
|
+
AFTER DELETE ON content_products BEGIN
|
|
42
|
+
DELETE FROM fts_products WHERE entry_id = old.id;
|
|
43
|
+
END;
|
|
44
|
+
|
|
45
|
+
-- content_reviews
|
|
46
|
+
CREATE TABLE IF NOT EXISTS content_reviews (
|
|
47
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
48
|
+
slug TEXT NOT NULL UNIQUE,
|
|
49
|
+
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'review', 'published', 'archived')),
|
|
50
|
+
title TEXT NOT NULL,
|
|
51
|
+
body TEXT,
|
|
52
|
+
score REAL,
|
|
53
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
54
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_reviews_status ON content_reviews(status);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_reviews_created_at ON content_reviews(created_at);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_reviews_title ON content_reviews(title);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_reviews_body ON content_reviews(body);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_reviews_score ON content_reviews(score);
|
|
62
|
+
|
|
63
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS fts_reviews USING fts5(
|
|
64
|
+
entry_id UNINDEXED,
|
|
65
|
+
title,
|
|
66
|
+
body,
|
|
67
|
+
tokenize = 'unicode61'
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE TRIGGER IF NOT EXISTS fts_reviews_insert
|
|
71
|
+
AFTER INSERT ON content_reviews BEGIN
|
|
72
|
+
INSERT INTO fts_reviews(entry_id, title, body) VALUES (new.id, new.title, new.body);
|
|
73
|
+
END;
|
|
74
|
+
|
|
75
|
+
CREATE TRIGGER IF NOT EXISTS fts_reviews_update
|
|
76
|
+
AFTER UPDATE OF title, body ON content_reviews BEGIN
|
|
77
|
+
DELETE FROM fts_reviews WHERE entry_id = old.id;
|
|
78
|
+
INSERT INTO fts_reviews(entry_id, title, body) VALUES (new.id, new.title, new.body);
|
|
79
|
+
END;
|
|
80
|
+
|
|
81
|
+
CREATE TRIGGER IF NOT EXISTS fts_reviews_delete
|
|
82
|
+
AFTER DELETE ON content_reviews BEGIN
|
|
83
|
+
DELETE FROM fts_reviews WHERE entry_id = old.id;
|
|
84
|
+
END;
|
|
85
|
+
|
|
86
|
+
-- content_tasks
|
|
87
|
+
CREATE TABLE IF NOT EXISTS content_tasks (
|
|
88
|
+
id TEXT NOT NULL PRIMARY KEY,
|
|
89
|
+
slug TEXT NOT NULL UNIQUE,
|
|
90
|
+
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'review', 'published', 'archived')),
|
|
91
|
+
title TEXT NOT NULL,
|
|
92
|
+
taskStatus TEXT,
|
|
93
|
+
completion REAL,
|
|
94
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
95
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON content_tasks(status);
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON content_tasks(created_at);
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_title ON content_tasks(title);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_taskStatus ON content_tasks(taskStatus);
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_completion ON content_tasks(completion);
|
|
103
|
+
|
|
104
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS fts_tasks USING fts5(
|
|
105
|
+
entry_id UNINDEXED,
|
|
106
|
+
title,
|
|
107
|
+
taskStatus,
|
|
108
|
+
tokenize = 'unicode61'
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
CREATE TRIGGER IF NOT EXISTS fts_tasks_insert
|
|
112
|
+
AFTER INSERT ON content_tasks BEGIN
|
|
113
|
+
INSERT INTO fts_tasks(entry_id, title, taskStatus) VALUES (new.id, new.title, new.taskStatus);
|
|
114
|
+
END;
|
|
115
|
+
|
|
116
|
+
CREATE TRIGGER IF NOT EXISTS fts_tasks_update
|
|
117
|
+
AFTER UPDATE OF title, taskStatus ON content_tasks BEGIN
|
|
118
|
+
DELETE FROM fts_tasks WHERE entry_id = old.id;
|
|
119
|
+
INSERT INTO fts_tasks(entry_id, title, taskStatus) VALUES (new.id, new.title, new.taskStatus);
|
|
120
|
+
END;
|
|
121
|
+
|
|
122
|
+
CREATE TRIGGER IF NOT EXISTS fts_tasks_delete
|
|
123
|
+
AFTER DELETE ON content_tasks BEGIN
|
|
124
|
+
DELETE FROM fts_tasks WHERE entry_id = old.id;
|
|
125
|
+
END;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beechcms/api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/factory.ts"
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
"src/",
|
|
10
10
|
"migrations/0000_v040_base.sql",
|
|
11
11
|
"migrations/0029_automations.sql",
|
|
12
|
+
"migrations/0030_test_seeds.sql",
|
|
12
13
|
"assets/"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
16
|
"dev": "wrangler dev --port 8789",
|
|
16
17
|
"build": "tsc -p tsconfig.build.json --noEmit",
|
|
17
|
-
"db:migrate:local": "wrangler d1 execute beech-db --local --file=./migrations/0000_v040_base.sql && wrangler d1 execute beech-db --local --file=./migrations/0029_automations.sql",
|
|
18
|
+
"db:migrate:local": "wrangler d1 execute beech-db --local --file=./migrations/0000_v040_base.sql && wrangler d1 execute beech-db --local --file=./migrations/0029_automations.sql && wrangler d1 execute beech-db --local --file=./migrations/0030_test_seeds.sql",
|
|
18
19
|
"db:reset:local": "node -e \"require('fs').rmSync('.wrangler/state', {recursive:true,force:true})\" && npm run db:migrate:local && cd ../.. && node bin/cli.mjs seed:load --local && cd apps/api && wrangler d1 execute beech-db --local --file=./migrations/0028_v040_seed_data.sql",
|
|
19
20
|
"deploy": "wrangler deploy --minify",
|
|
20
21
|
"cf-typegen": "wrangler types --env-interface CloudflareBindings",
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
25
26
|
"@aws-sdk/client-s3": "^3.995.0",
|
|
26
|
-
"@beechcms/core": "^0.
|
|
27
|
+
"@beechcms/core": "^0.5.0",
|
|
27
28
|
"bcryptjs": "^2.4.3",
|
|
28
29
|
"hono": "^4.11.9",
|
|
29
30
|
"jose": "^6.1.3"
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import type { Context } from 'hono'
|
|
2
|
-
|
|
3
|
-
type EdgeCache = {
|
|
4
|
-
cache: Cache
|
|
5
|
-
executionCtx: { waitUntil: (p: Promise<unknown>) => void }
|
|
6
|
-
} | null
|
|
7
|
-
|
|
8
|
-
export function resolveEdgeCache(c: Context): EdgeCache {
|
|
9
|
-
try {
|
|
10
|
-
const cache = caches.default
|
|
11
|
-
let executionCtx: any
|
|
12
|
-
try {
|
|
13
|
-
executionCtx = c.executionCtx
|
|
14
|
-
} catch {
|
|
15
|
-
return null
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (!executionCtx?.waitUntil) return null
|
|
19
|
-
return { cache, executionCtx: executionCtx as { waitUntil: (p: Promise<unknown>) => void } }
|
|
20
|
-
} catch {
|
|
21
|
-
return null
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function withCachedResponse(edgeCache: EdgeCache, cacheKey: Request, response: Response): Response {
|
|
26
|
-
if (!edgeCache) return response
|
|
27
|
-
const cloned = response.clone()
|
|
28
|
-
const headers = new Headers(cloned.headers)
|
|
29
|
-
headers.set('Cache-Control', 'public, max-age=60')
|
|
30
|
-
edgeCache.executionCtx.waitUntil(
|
|
31
|
-
edgeCache.cache.put(cacheKey, new Response(cloned.body, { status: cloned.status, headers }))
|
|
32
|
-
)
|
|
33
|
-
return response
|
|
34
|
-
}
|
|
1
|
+
import type { Context } from 'hono'
|
|
2
|
+
|
|
3
|
+
type EdgeCache = {
|
|
4
|
+
cache: Cache
|
|
5
|
+
executionCtx: { waitUntil: (p: Promise<unknown>) => void }
|
|
6
|
+
} | null
|
|
7
|
+
|
|
8
|
+
export function resolveEdgeCache(c: Context): EdgeCache {
|
|
9
|
+
try {
|
|
10
|
+
const cache = caches.default
|
|
11
|
+
let executionCtx: any
|
|
12
|
+
try {
|
|
13
|
+
executionCtx = c.executionCtx
|
|
14
|
+
} catch {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!executionCtx?.waitUntil) return null
|
|
19
|
+
return { cache, executionCtx: executionCtx as { waitUntil: (p: Promise<unknown>) => void } }
|
|
20
|
+
} catch {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function withCachedResponse(edgeCache: EdgeCache, cacheKey: Request, response: Response): Response {
|
|
26
|
+
if (!edgeCache) return response
|
|
27
|
+
const cloned = response.clone()
|
|
28
|
+
const headers = new Headers(cloned.headers)
|
|
29
|
+
headers.set('Cache-Control', 'public, max-age=60')
|
|
30
|
+
edgeCache.executionCtx.waitUntil(
|
|
31
|
+
edgeCache.cache.put(cacheKey, new Response(cloned.body, { status: cloned.status, headers }))
|
|
32
|
+
)
|
|
33
|
+
return response
|
|
34
|
+
}
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import { resolvePolicies } from '@beechcms/core'
|
|
2
|
-
import type { Seed } from '@beechcms/core'
|
|
3
|
-
|
|
4
|
-
const SYSTEM_FIELDS = ['id', 'slug', 'status', 'created_at', 'updated_at']
|
|
5
|
-
const IDENTITY_FIELDS = ['id', 'slug']
|
|
6
|
-
|
|
7
|
-
function applyPublicPolicies(data: Record<string, unknown>, seed: Seed): Record<string, unknown> {
|
|
8
|
-
const result: Record<string, unknown> = {}
|
|
9
|
-
|
|
10
|
-
for (const key of SYSTEM_FIELDS) {
|
|
11
|
-
if (key in data) result[key] = data[key]
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
for (const branch of seed.branches) {
|
|
15
|
-
const value = data[branch.alias]
|
|
16
|
-
const { public: isPublic, visibility } = resolvePolicies(branch)
|
|
17
|
-
if (!isPublic) continue
|
|
18
|
-
if (visibility === 'hidden') continue
|
|
19
|
-
result[branch.alias] = visibility === 'masked' && typeof value === 'string' && value.length > 0
|
|
20
|
-
? '••••••••'
|
|
21
|
-
: value
|
|
22
|
-
}
|
|
23
|
-
return result
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function toFlatPublicEntry(data: Record<string, unknown>, seed: Seed, fieldsParam?: string): Record<string, unknown> {
|
|
27
|
-
const projected = applyPublicPolicies(data, seed)
|
|
28
|
-
const requestedFields = (fieldsParam ?? '').split(',').map(f => f.trim()).filter(Boolean)
|
|
29
|
-
|
|
30
|
-
if (requestedFields.length === 0) return projected
|
|
31
|
-
|
|
32
|
-
const filtered: Record<string, unknown> = {}
|
|
33
|
-
for (const field of requestedFields) {
|
|
34
|
-
if (field in projected) filtered[field] = projected[field]
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
for (const key of IDENTITY_FIELDS) {
|
|
38
|
-
if (key in projected && !filtered[key]) filtered[key] = projected[key]
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return filtered
|
|
42
|
-
}
|
|
1
|
+
import { resolvePolicies } from '@beechcms/core'
|
|
2
|
+
import type { Seed } from '@beechcms/core'
|
|
3
|
+
|
|
4
|
+
const SYSTEM_FIELDS = ['id', 'slug', 'status', 'created_at', 'updated_at']
|
|
5
|
+
const IDENTITY_FIELDS = ['id', 'slug']
|
|
6
|
+
|
|
7
|
+
function applyPublicPolicies(data: Record<string, unknown>, seed: Seed): Record<string, unknown> {
|
|
8
|
+
const result: Record<string, unknown> = {}
|
|
9
|
+
|
|
10
|
+
for (const key of SYSTEM_FIELDS) {
|
|
11
|
+
if (key in data) result[key] = data[key]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
for (const branch of seed.branches) {
|
|
15
|
+
const value = data[branch.alias]
|
|
16
|
+
const { public: isPublic, visibility } = resolvePolicies(branch)
|
|
17
|
+
if (!isPublic) continue
|
|
18
|
+
if (visibility === 'hidden') continue
|
|
19
|
+
result[branch.alias] = visibility === 'masked' && typeof value === 'string' && value.length > 0
|
|
20
|
+
? '••••••••'
|
|
21
|
+
: value
|
|
22
|
+
}
|
|
23
|
+
return result
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function toFlatPublicEntry(data: Record<string, unknown>, seed: Seed, fieldsParam?: string): Record<string, unknown> {
|
|
27
|
+
const projected = applyPublicPolicies(data, seed)
|
|
28
|
+
const requestedFields = (fieldsParam ?? '').split(',').map(f => f.trim()).filter(Boolean)
|
|
29
|
+
|
|
30
|
+
if (requestedFields.length === 0) return projected
|
|
31
|
+
|
|
32
|
+
const filtered: Record<string, unknown> = {}
|
|
33
|
+
for (const field of requestedFields) {
|
|
34
|
+
if (field in projected) filtered[field] = projected[field]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const key of IDENTITY_FIELDS) {
|
|
38
|
+
if (key in projected && !filtered[key]) filtered[key] = projected[key]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return filtered
|
|
42
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { sha256hex } from '@beechcms/core'
|
|
2
|
-
|
|
3
|
-
export function parseIdempotencyKey(rawValue: string | undefined): string | null {
|
|
4
|
-
if (!rawValue) return null
|
|
5
|
-
const key = rawValue.trim()
|
|
6
|
-
if (!key || key.length > 128) return null
|
|
7
|
-
return key
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
type FingerprintInput = {
|
|
11
|
-
seedSlug: string
|
|
12
|
-
statusValue: unknown
|
|
13
|
-
slug: string | null
|
|
14
|
-
data: Record<string, unknown>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function buildRequestFingerprint(input: FingerprintInput): Promise<string> {
|
|
18
|
-
return sha256hex(JSON.stringify({ seedSlug: input.seedSlug, statusValue: input.statusValue, slug: input.slug, data: input.data }))
|
|
19
|
-
}
|
|
1
|
+
import { sha256hex } from '@beechcms/core'
|
|
2
|
+
|
|
3
|
+
export function parseIdempotencyKey(rawValue: string | undefined): string | null {
|
|
4
|
+
if (!rawValue) return null
|
|
5
|
+
const key = rawValue.trim()
|
|
6
|
+
if (!key || key.length > 128) return null
|
|
7
|
+
return key
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type FingerprintInput = {
|
|
11
|
+
seedSlug: string
|
|
12
|
+
statusValue: unknown
|
|
13
|
+
slug: string | null
|
|
14
|
+
data: Record<string, unknown>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildRequestFingerprint(input: FingerprintInput): Promise<string> {
|
|
18
|
+
return sha256hex(JSON.stringify({ seedSlug: input.seedSlug, statusValue: input.statusValue, slug: input.slug, data: input.data }))
|
|
19
|
+
}
|
package/src/public/read-list.ts
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
import type { Seed, ContentRepository } from '@beechcms/core'
|
|
2
|
-
import { cleanStr } from '../shared/query-utils'
|
|
3
|
-
import { toFlatPublicEntry } from './entry-projection'
|
|
4
|
-
import { buildPublicListMeta } from './response-builder'
|
|
5
|
-
import { parsePublicFilter, parsePublicPagination, parseLatestCount, toEngineFilters } from './query-builder'
|
|
6
|
-
|
|
7
|
-
type ReadListInput = {
|
|
8
|
-
seed: Seed
|
|
9
|
-
seedSlug: string
|
|
10
|
-
repository: ContentRepository
|
|
11
|
-
query: Record<string, string | undefined>
|
|
12
|
-
publishedOnly: boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export async function readListEntries(input: ReadListInput) {
|
|
16
|
-
const { seed, seedSlug, repository, query, publishedOnly } = input
|
|
17
|
-
|
|
18
|
-
const parsedFilter = parsePublicFilter(query.filter)
|
|
19
|
-
const allMode = cleanStr(query.all)?.toLowerCase() === 'true'
|
|
20
|
-
const latestMode = cleanStr(query.latest) !== null
|
|
21
|
-
const latestCount = latestMode ? parseLatestCount(query.latest ?? '') : null
|
|
22
|
-
const pagination = allMode ? { page: 1, limit: 100 } : parsePublicPagination(query)
|
|
23
|
-
const offset = (pagination.page - 1) * pagination.limit
|
|
24
|
-
const search = cleanStr(query.search) ?? ''
|
|
25
|
-
const engineFilters = toEngineFilters(seed, parsedFilter)
|
|
26
|
-
const sortBy = cleanStr(query.orderBy) ?? 'created_at'
|
|
27
|
-
const sortDir = (cleanStr(query.orderDir) ?? 'desc').toLowerCase() === 'asc' ? 'ASC' : 'DESC'
|
|
28
|
-
|
|
29
|
-
const { items, total } = await repository.findMany(seed, {
|
|
30
|
-
filters: engineFilters,
|
|
31
|
-
search: search || undefined,
|
|
32
|
-
status: publishedOnly ? 'published' : null,
|
|
33
|
-
pagination: {
|
|
34
|
-
limit: latestMode ? (latestCount ?? 10) : pagination.limit,
|
|
35
|
-
offset: latestMode ? 0 : offset,
|
|
36
|
-
},
|
|
37
|
-
orderBy: latestMode ? { column: 'created_at', dir: 'DESC' } : { column: sortBy, dir: sortDir },
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
const data = items.map(item => toFlatPublicEntry(item, seed, query.fields))
|
|
41
|
-
|
|
42
|
-
if (latestMode) {
|
|
43
|
-
return { data, meta: { total, returned: data.length, seed: seedSlug } }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
data,
|
|
48
|
-
meta: buildPublicListMeta({ total, page: pagination.page, limit: pagination.limit, returned: data.length, seed: seedSlug }),
|
|
49
|
-
}
|
|
50
|
-
}
|
|
1
|
+
import type { Seed, ContentRepository } from '@beechcms/core'
|
|
2
|
+
import { cleanStr } from '../shared/query-utils'
|
|
3
|
+
import { toFlatPublicEntry } from './entry-projection'
|
|
4
|
+
import { buildPublicListMeta } from './response-builder'
|
|
5
|
+
import { parsePublicFilter, parsePublicPagination, parseLatestCount, toEngineFilters } from './query-builder'
|
|
6
|
+
|
|
7
|
+
type ReadListInput = {
|
|
8
|
+
seed: Seed
|
|
9
|
+
seedSlug: string
|
|
10
|
+
repository: ContentRepository
|
|
11
|
+
query: Record<string, string | undefined>
|
|
12
|
+
publishedOnly: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function readListEntries(input: ReadListInput) {
|
|
16
|
+
const { seed, seedSlug, repository, query, publishedOnly } = input
|
|
17
|
+
|
|
18
|
+
const parsedFilter = parsePublicFilter(query.filter)
|
|
19
|
+
const allMode = cleanStr(query.all)?.toLowerCase() === 'true'
|
|
20
|
+
const latestMode = cleanStr(query.latest) !== null
|
|
21
|
+
const latestCount = latestMode ? parseLatestCount(query.latest ?? '') : null
|
|
22
|
+
const pagination = allMode ? { page: 1, limit: 100 } : parsePublicPagination(query)
|
|
23
|
+
const offset = (pagination.page - 1) * pagination.limit
|
|
24
|
+
const search = cleanStr(query.search) ?? ''
|
|
25
|
+
const engineFilters = toEngineFilters(seed, parsedFilter)
|
|
26
|
+
const sortBy = cleanStr(query.orderBy) ?? 'created_at'
|
|
27
|
+
const sortDir = (cleanStr(query.orderDir) ?? 'desc').toLowerCase() === 'asc' ? 'ASC' : 'DESC'
|
|
28
|
+
|
|
29
|
+
const { items, total } = await repository.findMany(seed, {
|
|
30
|
+
filters: engineFilters,
|
|
31
|
+
search: search || undefined,
|
|
32
|
+
status: publishedOnly ? 'published' : null,
|
|
33
|
+
pagination: {
|
|
34
|
+
limit: latestMode ? (latestCount ?? 10) : pagination.limit,
|
|
35
|
+
offset: latestMode ? 0 : offset,
|
|
36
|
+
},
|
|
37
|
+
orderBy: latestMode ? { column: 'created_at', dir: 'DESC' } : { column: sortBy, dir: sortDir },
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const data = items.map(item => toFlatPublicEntry(item, seed, query.fields))
|
|
41
|
+
|
|
42
|
+
if (latestMode) {
|
|
43
|
+
return { data, meta: { total, returned: data.length, seed: seedSlug } }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
data,
|
|
48
|
+
meta: buildPublicListMeta({ total, page: pagination.page, limit: pagination.limit, returned: data.length, seed: seedSlug }),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
import { EntryNotFoundError } from '@beechcms/core'
|
|
2
|
-
import type { Seed, ContentRepository } from '@beechcms/core'
|
|
3
|
-
import { toFlatPublicEntry } from './entry-projection'
|
|
4
|
-
import { buildPublicSingleMeta } from './response-builder'
|
|
5
|
-
|
|
6
|
-
type ReadSingleInput = {
|
|
7
|
-
seed: Seed
|
|
8
|
-
seedSlug: string
|
|
9
|
-
repository: ContentRepository
|
|
10
|
-
id: string | null
|
|
11
|
-
slug: string | null
|
|
12
|
-
publishedOnly: boolean
|
|
13
|
-
fieldsParam?: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type ReadSingleResult =
|
|
17
|
-
| { ok: true; data: Record<string, unknown>; meta: { seed: string } }
|
|
18
|
-
| { ok: false; detail: string }
|
|
19
|
-
|
|
20
|
-
export async function readSingleEntry(input: ReadSingleInput): Promise<ReadSingleResult> {
|
|
21
|
-
const { seed, seedSlug, repository, id, slug, publishedOnly, fieldsParam } = input
|
|
22
|
-
const label = id ?? slug!
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
const entry = id
|
|
26
|
-
? await repository.findById(seed, id)
|
|
27
|
-
: await repository.findBySlug(seed, slug!)
|
|
28
|
-
|
|
29
|
-
if (publishedOnly && entry.status !== 'published') {
|
|
30
|
-
return { ok: false, detail: `Entry '${label}' not found or not published.` }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
ok: true,
|
|
35
|
-
data: toFlatPublicEntry(entry, seed, fieldsParam),
|
|
36
|
-
meta: buildPublicSingleMeta(seedSlug),
|
|
37
|
-
}
|
|
38
|
-
} catch (error) {
|
|
39
|
-
if (error instanceof EntryNotFoundError) {
|
|
40
|
-
return { ok: false, detail: `Entry '${label}' not found for content type '${seedSlug}'.` }
|
|
41
|
-
}
|
|
42
|
-
throw error
|
|
43
|
-
}
|
|
44
|
-
}
|
|
1
|
+
import { EntryNotFoundError } from '@beechcms/core'
|
|
2
|
+
import type { Seed, ContentRepository } from '@beechcms/core'
|
|
3
|
+
import { toFlatPublicEntry } from './entry-projection'
|
|
4
|
+
import { buildPublicSingleMeta } from './response-builder'
|
|
5
|
+
|
|
6
|
+
type ReadSingleInput = {
|
|
7
|
+
seed: Seed
|
|
8
|
+
seedSlug: string
|
|
9
|
+
repository: ContentRepository
|
|
10
|
+
id: string | null
|
|
11
|
+
slug: string | null
|
|
12
|
+
publishedOnly: boolean
|
|
13
|
+
fieldsParam?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ReadSingleResult =
|
|
17
|
+
| { ok: true; data: Record<string, unknown>; meta: { seed: string } }
|
|
18
|
+
| { ok: false; detail: string }
|
|
19
|
+
|
|
20
|
+
export async function readSingleEntry(input: ReadSingleInput): Promise<ReadSingleResult> {
|
|
21
|
+
const { seed, seedSlug, repository, id, slug, publishedOnly, fieldsParam } = input
|
|
22
|
+
const label = id ?? slug!
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const entry = id
|
|
26
|
+
? await repository.findById(seed, id)
|
|
27
|
+
: await repository.findBySlug(seed, slug!)
|
|
28
|
+
|
|
29
|
+
if (publishedOnly && entry.status !== 'published') {
|
|
30
|
+
return { ok: false, detail: `Entry '${label}' not found or not published.` }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
ok: true,
|
|
35
|
+
data: toFlatPublicEntry(entry, seed, fieldsParam),
|
|
36
|
+
meta: buildPublicSingleMeta(seedSlug),
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error instanceof EntryNotFoundError) {
|
|
40
|
+
return { ok: false, detail: `Entry '${label}' not found for content type '${seedSlug}'.` }
|
|
41
|
+
}
|
|
42
|
+
throw error
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/search-utils.ts
CHANGED
|
@@ -53,7 +53,7 @@ export function decodeCursor(cursor: string): { rank: number; entryId: string }
|
|
|
53
53
|
const sep = decoded.lastIndexOf(":")
|
|
54
54
|
if (sep === -1) return null
|
|
55
55
|
return {
|
|
56
|
-
rank: parseFloat(decoded.slice(0, sep)),
|
|
56
|
+
rank: Number.parseFloat(decoded.slice(0, sep)),
|
|
57
57
|
entryId: decoded.slice(sep + 1),
|
|
58
58
|
}
|
|
59
59
|
} catch {
|
package/src/search.ts
CHANGED
|
@@ -18,7 +18,7 @@ searchRouter.get("/", async (c) => {
|
|
|
18
18
|
const queryText = c.req.query("q")?.trim() ?? ""
|
|
19
19
|
const schemaSlug = c.req.query("schema_slug") ?? null
|
|
20
20
|
const status = c.req.query("status") ?? null
|
|
21
|
-
const rawLimit = parseInt(c.req.query("limit") ?? "20", 10)
|
|
21
|
+
const rawLimit = Number.parseInt(c.req.query("limit") ?? "20", 10)
|
|
22
22
|
const limit = Math.min(Math.max(rawLimit, 1), 50)
|
|
23
23
|
const cursor = c.req.query("cursor") ?? null
|
|
24
24
|
|