@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.
@@ -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-BMkd1Irh.js"></script>
13
- <link rel="stylesheet" crossorigin href="/admin/assets/index-BKWnlvnV.css">
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.4.3",
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.4.3",
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
+ }
@@ -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
+ }
@@ -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