@rimelight/cms 0.0.2 → 0.0.3
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/LICENSE +21 -0
- package/package.json +16 -14
- package/src/admin/api/media-file.ts +39 -0
- package/src/admin/api/media.ts +199 -0
- package/src/admin/layouts/CMSDashboardLayout.astro +178 -0
- package/src/admin/layouts/DefaultParentLayout.astro +50 -0
- package/src/admin/pages/assets.astro +424 -0
- package/src/admin/pages/byline-schema.astro +62 -0
- package/src/admin/pages/bylines.astro +16 -0
- package/src/admin/pages/categories.astro +21 -0
- package/src/admin/pages/content-types.astro +55 -0
- package/src/admin/pages/dashboard.astro +61 -0
- package/src/admin/pages/pages-edit.astro +595 -0
- package/src/admin/pages/pages-index.astro +146 -0
- package/src/admin/pages/pages-new.astro +130 -0
- package/src/admin/pages/pages-preview.astro +111 -0
- package/src/admin/pages/pages-review.astro +236 -0
- package/src/admin/pages/settings.astro +203 -0
- package/src/admin/pages/tags.astro +21 -0
- package/src/admin/pages/templates-edit.astro +234 -0
- package/src/admin/pages/templates-index.astro +95 -0
- package/src/admin/pages/templates-new.astro +193 -0
- package/src/admin/pages/users.astro +74 -0
- package/src/admin/pages/versions.astro +74 -0
- package/src/astro/BlockRenderer.astro +44 -102
- package/src/astro/PageRenderer.astro +9 -2
- package/src/astro/blocks/CalloutBlock.astro +7 -7
- package/src/astro/blocks/CardBlock.astro +5 -5
- package/src/astro/blocks/CodeBlock.astro +2 -2
- package/src/astro/blocks/FileTreeBlock.astro +7 -7
- package/src/astro/blocks/ImageBlock.astro +2 -2
- package/src/astro/blocks/OrderedListBlock.astro +2 -2
- package/src/astro/blocks/ParagraphBlock.astro +3 -3
- package/src/astro/blocks/SectionBlock.astro +6 -6
- package/src/astro/blocks/StepItemBlock.astro +3 -3
- package/src/astro/blocks/StepsBlock.astro +1 -1
- package/src/astro/blocks/TableBlock.astro +6 -6
- package/src/astro/blocks/TabsBlock.astro +9 -9
- package/src/astro/blocks/UnorderedListBlock.astro +2 -2
- package/src/astro/blocks/index.ts +35 -0
- package/src/client/components/RimelightBlock.tsx +19 -19
- package/src/client/components/RimelightBlockPicker.tsx +9 -13
- package/src/client/components/RimelightBylinesManager.tsx +438 -0
- package/src/client/components/RimelightEditor.tsx +3 -3
- package/src/client/components/RimelightPreview.tsx +37 -45
- package/src/client/components/RimelightPropertiesPanel.tsx +368 -26
- package/src/client/components/RimelightTaxonomyManager.tsx +369 -0
- package/src/client/components/RimelightTaxonomyMasterDetail.tsx +412 -0
- package/src/client/components/RimelightTemplateEditor.tsx +6 -8
- package/src/client/components/editors/CalloutEditor.tsx +2 -4
- package/src/client/components/editors/CardsEditor.tsx +2 -4
- package/src/client/components/editors/CodeEditor.tsx +1 -3
- package/src/client/components/editors/FileTreeEditor.tsx +2 -4
- package/src/client/components/editors/ImageEditor.tsx +3 -3
- package/src/client/components/editors/ParagraphEditor.tsx +2 -2
- package/src/client/components/editors/SceneEditor.tsx +6 -12
- package/src/client/components/editors/ScriptEditor.tsx +8 -16
- package/src/client/components/editors/SectionEditor.tsx +4 -4
- package/src/client/components/editors/StepItemEditor.tsx +2 -2
- package/src/client/components/editors/StepsEditor.tsx +2 -4
- package/src/client/components/editors/TabItemEditor.tsx +3 -3
- package/src/client/components/editors/TableEditor.tsx +5 -5
- package/src/client/components/editors/TabsEditor.tsx +2 -4
- package/src/client/components/index.ts +3 -0
- package/src/client/loader.ts +3 -0
- package/src/core/diff.ts +145 -0
- package/src/core/excerpt.ts +159 -0
- package/src/core/preview.ts +87 -0
- package/src/core/registry.ts +0 -86
- package/src/core/types/blocks.ts +6 -1
- package/src/core/types/bylines.ts +14 -0
- package/src/core/types/media.ts +13 -0
- package/src/core/types/taxonomies.ts +20 -0
- package/src/cron/scheduled.ts +57 -0
- package/src/docs/agent.ts +116 -0
- package/src/docs/config.ts +25 -0
- package/src/docs/index.ts +5 -0
- package/src/docs/routing.ts +104 -0
- package/src/docs/sidebar.ts +251 -0
- package/src/docs/toc.ts +47 -0
- package/src/env.d.ts +13 -0
- package/src/index.ts +40 -8
- package/src/integration.ts +196 -0
- package/src/loader/live.ts +254 -0
- package/src/markdown/index.ts +0 -1
- package/src/mcp/index.ts +228 -0
- package/src/schema/bylines.ts +23 -0
- package/src/schema/index.ts +3 -4
- package/src/schema/page_versions.ts +2 -0
- package/src/schema/pages.ts +2 -0
- package/src/schema/site_settings.ts +41 -0
- package/src/schema/taxonomies.ts +49 -0
- package/src/services/bylines.ts +74 -0
- package/src/services/search-indexer.ts +61 -0
- package/src/services/site-settings.ts +108 -0
- package/src/services/taxonomies.ts +276 -0
- package/src/storage/index.ts +253 -0
- package/src/astro/blocks/ApiDocumentationBlock.astro +0 -350
- package/src/astro/blocks/DialogueBlock.astro +0 -23
- package/src/astro/blocks/LivePreviewBlock.astro +0 -27
- package/src/astro/blocks/SceneBlock.astro +0 -65
- package/src/astro/blocks/ScriptBlock.astro +0 -59
- package/src/core/template-sync.ts +0 -75
- package/src/markdown/parser.ts +0 -347
- package/src/migration-tools.ts +0 -334
- package/src/migration.ts +0 -155
- package/src/schema/component_dependencies.ts +0 -21
- package/src/schema/component_metadata.ts +0 -31
- package/src/schema/component_metadata_cache.ts +0 -15
- package/src/schema/component_versions.ts +0 -19
- package/src/search/sync.ts +0 -32
- package/src/services/component-metadata.ts +0 -134
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { and, desc, eq, isNotNull, isNull, lte, sql, type SQL } from "drizzle-orm"
|
|
2
|
+
import { pages } from "../schema/pages.ts"
|
|
3
|
+
import { createPageExcerpt } from "../core/excerpt.ts"
|
|
4
|
+
import type { BaseBlock } from "../core/types/blocks.ts"
|
|
5
|
+
import type { Localized, PageType } from "../core/types/pages.ts"
|
|
6
|
+
|
|
7
|
+
import { taxonomyTerms, pageTaxonomyTerms } from "../schema/taxonomies.ts"
|
|
8
|
+
|
|
9
|
+
export interface CmsLoaderOptions {
|
|
10
|
+
type?: PageType | undefined
|
|
11
|
+
db?: any
|
|
12
|
+
locale?: string | undefined
|
|
13
|
+
includeDrafts?: boolean | undefined
|
|
14
|
+
includeScheduled?: boolean | undefined
|
|
15
|
+
taxonomy?:
|
|
16
|
+
| {
|
|
17
|
+
category?: string | undefined
|
|
18
|
+
tag?: string | undefined
|
|
19
|
+
[key: string]: string | undefined
|
|
20
|
+
}
|
|
21
|
+
| undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CmsPageData {
|
|
25
|
+
id: string
|
|
26
|
+
slug: string
|
|
27
|
+
type: PageType
|
|
28
|
+
title: Localized
|
|
29
|
+
resolvedTitle: string
|
|
30
|
+
description?: Localized
|
|
31
|
+
resolvedDescription: string
|
|
32
|
+
excerpt: string
|
|
33
|
+
tags: Localized[]
|
|
34
|
+
authorIds: string[]
|
|
35
|
+
blocks: BaseBlock[]
|
|
36
|
+
properties: Record<string, any>
|
|
37
|
+
publishedVersionId: string | null
|
|
38
|
+
postedAt: Date | null
|
|
39
|
+
createdAt: Date
|
|
40
|
+
updatedAt: Date | null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CmsEntry {
|
|
44
|
+
id: string
|
|
45
|
+
data: CmsPageData
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface CacheHint {
|
|
49
|
+
tags?: string[]
|
|
50
|
+
lastModified?: Date
|
|
51
|
+
maxAge?: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveLocalized(val: unknown, locale: string = "en"): string {
|
|
55
|
+
if (!val) return ""
|
|
56
|
+
if (typeof val === "string") return val
|
|
57
|
+
if (typeof val === "object" && val !== null) {
|
|
58
|
+
const rec = val as Record<string, string>
|
|
59
|
+
return rec[locale] || rec.en || Object.values(rec)[0] || ""
|
|
60
|
+
}
|
|
61
|
+
return typeof val === "number" ? String(val) : ""
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function transformPageToEntry(rawPage: any, locale: string = "en"): CmsEntry {
|
|
65
|
+
let contentObj = rawPage.content
|
|
66
|
+
if (typeof contentObj === "string") {
|
|
67
|
+
try {
|
|
68
|
+
contentObj = JSON.parse(contentObj)
|
|
69
|
+
} catch {
|
|
70
|
+
contentObj = { blocks: [], properties: {} }
|
|
71
|
+
}
|
|
72
|
+
} else if (!contentObj || typeof contentObj !== "object") {
|
|
73
|
+
contentObj = { blocks: [], properties: {} }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const blocks: BaseBlock[] = Array.isArray(contentObj.blocks) ? contentObj.blocks : []
|
|
77
|
+
const properties: Record<string, any> = contentObj.properties || {}
|
|
78
|
+
|
|
79
|
+
const resolvedTitle = resolveLocalized(rawPage.title, locale)
|
|
80
|
+
const resolvedDescription = resolveLocalized(rawPage.description, locale)
|
|
81
|
+
const excerpt = createPageExcerpt(rawPage, { locale, maxLength: 160 })
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
id: rawPage.slug || rawPage.id,
|
|
85
|
+
data: {
|
|
86
|
+
id: rawPage.id,
|
|
87
|
+
slug: rawPage.slug,
|
|
88
|
+
type: rawPage.type,
|
|
89
|
+
title: rawPage.title,
|
|
90
|
+
resolvedTitle,
|
|
91
|
+
description: rawPage.description,
|
|
92
|
+
resolvedDescription,
|
|
93
|
+
excerpt,
|
|
94
|
+
tags: rawPage.tags || [],
|
|
95
|
+
authorIds: rawPage.authorIds || [],
|
|
96
|
+
blocks,
|
|
97
|
+
properties,
|
|
98
|
+
publishedVersionId: rawPage.publishedVersionId,
|
|
99
|
+
postedAt: rawPage.postedAt ? new Date(rawPage.postedAt) : null,
|
|
100
|
+
createdAt: new Date(rawPage.createdAt),
|
|
101
|
+
updatedAt: rawPage.updatedAt ? new Date(rawPage.updatedAt) : null
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Direct querying helper for CMS collections.
|
|
108
|
+
*/
|
|
109
|
+
export async function getCmsCollection(
|
|
110
|
+
db: any,
|
|
111
|
+
options: CmsLoaderOptions & {
|
|
112
|
+
limit?: number | undefined
|
|
113
|
+
offset?: number | undefined
|
|
114
|
+
where?: SQL | undefined
|
|
115
|
+
} = {}
|
|
116
|
+
): Promise<{ entries: CmsEntry[]; cacheHint: CacheHint }> {
|
|
117
|
+
const {
|
|
118
|
+
type,
|
|
119
|
+
locale = "en",
|
|
120
|
+
includeDrafts = false,
|
|
121
|
+
includeScheduled = false,
|
|
122
|
+
limit,
|
|
123
|
+
offset,
|
|
124
|
+
where: customWhere
|
|
125
|
+
} = options
|
|
126
|
+
|
|
127
|
+
if (!db) {
|
|
128
|
+
return { entries: [], cacheHint: { tags: ["cms"] } }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const conditions: SQL[] = [isNull(pages.deletedAt)]
|
|
132
|
+
|
|
133
|
+
if (type) {
|
|
134
|
+
// eslint-disable-next-line typescript/no-unsafe-type-assertion
|
|
135
|
+
conditions.push(eq(pages.type, type as PageType))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!includeDrafts) {
|
|
139
|
+
conditions.push(isNotNull(pages.publishedVersionId))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!includeScheduled) {
|
|
143
|
+
conditions.push(lte(pages.postedAt, new Date()))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (customWhere) {
|
|
147
|
+
conditions.push(customWhere)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (options.taxonomy) {
|
|
151
|
+
for (const [taxName, termSlug] of Object.entries(options.taxonomy)) {
|
|
152
|
+
if (termSlug) {
|
|
153
|
+
conditions.push(
|
|
154
|
+
sql`${pages.id} IN (
|
|
155
|
+
SELECT ${pageTaxonomyTerms.pageId} FROM ${pageTaxonomyTerms}
|
|
156
|
+
INNER JOIN ${taxonomyTerms} ON ${taxonomyTerms.id} = ${pageTaxonomyTerms.termId}
|
|
157
|
+
WHERE ${taxonomyTerms.taxonomy} = ${taxName} AND ${taxonomyTerms.slug} = ${termSlug}
|
|
158
|
+
)`
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let query = db
|
|
165
|
+
.select()
|
|
166
|
+
.from(pages)
|
|
167
|
+
.where(and(...conditions))
|
|
168
|
+
.orderBy(desc(pages.postedAt))
|
|
169
|
+
|
|
170
|
+
if (typeof limit === "number") {
|
|
171
|
+
query = query.limit(limit)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (typeof offset === "number") {
|
|
175
|
+
query = query.offset(offset)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const rows = await query
|
|
179
|
+
const entries = rows.map((r: any) => transformPageToEntry(r, locale))
|
|
180
|
+
|
|
181
|
+
let lastModified = new Date(0)
|
|
182
|
+
for (const entry of entries) {
|
|
183
|
+
if (entry.data.updatedAt && entry.data.updatedAt > lastModified) {
|
|
184
|
+
lastModified = entry.data.updatedAt
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const tags = ["cms"]
|
|
189
|
+
if (type) tags.push(`cms:${type}`)
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
entries,
|
|
193
|
+
cacheHint: {
|
|
194
|
+
tags,
|
|
195
|
+
lastModified: lastModified.getTime() > 0 ? lastModified : new Date()
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Direct querying helper for a single CMS entry by slug or id.
|
|
202
|
+
*/
|
|
203
|
+
export async function getCmsEntry(
|
|
204
|
+
db: any,
|
|
205
|
+
slugOrId: string,
|
|
206
|
+
options: CmsLoaderOptions = {}
|
|
207
|
+
): Promise<{ entry: CmsEntry | null; cacheHint: CacheHint }> {
|
|
208
|
+
const { entries, cacheHint } = await getCmsCollection(db, {
|
|
209
|
+
...options,
|
|
210
|
+
limit: 1,
|
|
211
|
+
where: eq(pages.slug, slugOrId)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const entry = entries[0] || null
|
|
215
|
+
if (entry) {
|
|
216
|
+
cacheHint.tags?.push(`page:${entry.data.slug}`)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return { entry, cacheHint }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Astro Live Loader for CMS collections. Use inside src/live.config.ts with defineLiveCollection({
|
|
224
|
+
* loader: rimelightCmsLoader({ type: 'blog' }) })
|
|
225
|
+
*/
|
|
226
|
+
export function rimelightCmsLoader(loaderOptions: CmsLoaderOptions = {}) {
|
|
227
|
+
return {
|
|
228
|
+
name: "rimelight-cms-loader",
|
|
229
|
+
loadCollection: async (context: {
|
|
230
|
+
filter?: { limit?: number; offset?: number }
|
|
231
|
+
collection?: string
|
|
232
|
+
}) => {
|
|
233
|
+
const db = loaderOptions.db || (globalThis as any).db
|
|
234
|
+
const limit = typeof context.filter?.limit === "number" ? context.filter.limit : undefined
|
|
235
|
+
const offset = typeof context.filter?.offset === "number" ? context.filter.offset : undefined
|
|
236
|
+
|
|
237
|
+
return getCmsCollection(db, {
|
|
238
|
+
...loaderOptions,
|
|
239
|
+
limit,
|
|
240
|
+
offset
|
|
241
|
+
})
|
|
242
|
+
},
|
|
243
|
+
loadEntry: async (context: { filter: { id: string }; collection?: string }) => {
|
|
244
|
+
const db = loaderOptions.db || (globalThis as any).db
|
|
245
|
+
const { entry, cacheHint } = await getCmsEntry(db, context.filter.id, loaderOptions)
|
|
246
|
+
if (!entry) return undefined
|
|
247
|
+
return {
|
|
248
|
+
id: entry.id,
|
|
249
|
+
data: entry.data,
|
|
250
|
+
cacheHint
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
package/src/markdown/index.ts
CHANGED
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { and, desc, eq, isNotNull, isNull } from "drizzle-orm"
|
|
2
|
+
import { pages } from "../schema/pages.ts"
|
|
3
|
+
import { pageDrafts } from "../schema/page_drafts.ts"
|
|
4
|
+
import { getSiteSettings, updateSiteSettings } from "../services/site-settings.ts"
|
|
5
|
+
import type { PageType } from "../core/types/pages.ts"
|
|
6
|
+
import type { BaseBlock } from "../core/types/blocks.ts"
|
|
7
|
+
|
|
8
|
+
export const CMS_MCP_TOOLS = [
|
|
9
|
+
{
|
|
10
|
+
name: "cms_list_pages",
|
|
11
|
+
description: "List CMS pages with optional filters for type, draft status, and pagination.",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
type: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "Filter by page type (e.g. 'blog', 'docs', 'landing')"
|
|
18
|
+
},
|
|
19
|
+
includeDrafts: { type: "boolean", description: "Whether to include unpublished drafts" },
|
|
20
|
+
limit: { type: "number", description: "Maximum number of pages to return (default 20)" }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "cms_get_page",
|
|
26
|
+
description:
|
|
27
|
+
"Get full details, block tree, and properties for a specific CMS page by slug or id.",
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
slugOrId: { type: "string", description: "Page slug or UUID" }
|
|
32
|
+
},
|
|
33
|
+
required: ["slugOrId"]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "cms_create_draft",
|
|
38
|
+
description: "Create or update a draft version for a CMS page.",
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
pageId: { type: "string", description: "Page UUID" },
|
|
43
|
+
userId: { type: "string", description: "User ID creating the draft" },
|
|
44
|
+
blocks: { type: "array", description: "Array of structured CMS blocks" },
|
|
45
|
+
properties: { type: "object", description: "Page properties object" }
|
|
46
|
+
},
|
|
47
|
+
required: ["pageId", "userId", "blocks"]
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "cms_get_site_settings",
|
|
52
|
+
description: "Retrieve current dynamic site settings including branding and SEO defaults.",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "cms_update_site_settings",
|
|
60
|
+
description: "Update dynamic site settings (name, description, branding, SEO).",
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: {
|
|
64
|
+
name: { type: "string" },
|
|
65
|
+
description: { type: "string" },
|
|
66
|
+
author: { type: "string" },
|
|
67
|
+
email: { type: "string" },
|
|
68
|
+
branding: { type: "object" },
|
|
69
|
+
seo: { type: "object" }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Handles Model Context Protocol (MCP) JSON-RPC requests for CMS tools.
|
|
77
|
+
*/
|
|
78
|
+
export async function handleCmsMcpRequest(
|
|
79
|
+
body: {
|
|
80
|
+
jsonrpc?: string
|
|
81
|
+
id?: string | number
|
|
82
|
+
method: string
|
|
83
|
+
params?: any
|
|
84
|
+
},
|
|
85
|
+
db: any
|
|
86
|
+
): Promise<any> {
|
|
87
|
+
const { id = 1, method, params = {} } = body
|
|
88
|
+
|
|
89
|
+
if (method === "tools/list") {
|
|
90
|
+
return {
|
|
91
|
+
jsonrpc: "2.0",
|
|
92
|
+
id,
|
|
93
|
+
result: {
|
|
94
|
+
tools: CMS_MCP_TOOLS
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (method === "tools/call") {
|
|
100
|
+
const { name, arguments: args = {} } = params
|
|
101
|
+
|
|
102
|
+
switch (name) {
|
|
103
|
+
case "cms_list_pages": {
|
|
104
|
+
const conditions = [isNull(pages.deletedAt)]
|
|
105
|
+
if (args.type) {
|
|
106
|
+
// eslint-disable-next-line typescript/no-unsafe-type-assertion
|
|
107
|
+
conditions.push(eq(pages.type, args.type as PageType))
|
|
108
|
+
}
|
|
109
|
+
if (!args.includeDrafts) {
|
|
110
|
+
conditions.push(isNotNull(pages.publishedVersionId))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const queryLimit = typeof args.limit === "number" ? args.limit : 20
|
|
114
|
+
const rows = await db
|
|
115
|
+
.select({
|
|
116
|
+
id: pages.id,
|
|
117
|
+
slug: pages.slug,
|
|
118
|
+
type: pages.type,
|
|
119
|
+
title: pages.title,
|
|
120
|
+
postedAt: pages.postedAt,
|
|
121
|
+
publishedVersionId: pages.publishedVersionId
|
|
122
|
+
})
|
|
123
|
+
.from(pages)
|
|
124
|
+
.where(and(...conditions))
|
|
125
|
+
.orderBy(desc(pages.postedAt))
|
|
126
|
+
.limit(queryLimit)
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
jsonrpc: "2.0",
|
|
130
|
+
id,
|
|
131
|
+
result: {
|
|
132
|
+
content: [{ type: "text", text: JSON.stringify(rows, null, 2) }]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case "cms_get_page": {
|
|
138
|
+
const row = await db
|
|
139
|
+
.select()
|
|
140
|
+
.from(pages)
|
|
141
|
+
.where(and(isNull(pages.deletedAt), eq(pages.slug, args.slugOrId)))
|
|
142
|
+
.limit(1)
|
|
143
|
+
|
|
144
|
+
const page = row[0] || null
|
|
145
|
+
return {
|
|
146
|
+
jsonrpc: "2.0",
|
|
147
|
+
id,
|
|
148
|
+
result: {
|
|
149
|
+
content: [{ type: "text", text: JSON.stringify(page, null, 2) }]
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
case "cms_create_draft": {
|
|
155
|
+
const contentPayload = {
|
|
156
|
+
blocks: args.blocks as BaseBlock[],
|
|
157
|
+
properties: args.properties || {}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const draft = await db
|
|
161
|
+
.insert(pageDrafts)
|
|
162
|
+
.values({
|
|
163
|
+
pageId: args.pageId,
|
|
164
|
+
updatedBy: args.userId,
|
|
165
|
+
content: contentPayload
|
|
166
|
+
})
|
|
167
|
+
.onConflictDoUpdate({
|
|
168
|
+
target: [pageDrafts.pageId],
|
|
169
|
+
set: {
|
|
170
|
+
content: contentPayload,
|
|
171
|
+
updatedBy: args.userId,
|
|
172
|
+
updatedAt: new Date()
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
.returning()
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
jsonrpc: "2.0",
|
|
179
|
+
id,
|
|
180
|
+
result: {
|
|
181
|
+
content: [{ type: "text", text: JSON.stringify(draft[0], null, 2) }]
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
case "cms_get_site_settings": {
|
|
187
|
+
const settings = await getSiteSettings(db)
|
|
188
|
+
return {
|
|
189
|
+
jsonrpc: "2.0",
|
|
190
|
+
id,
|
|
191
|
+
result: {
|
|
192
|
+
content: [{ type: "text", text: JSON.stringify(settings, null, 2) }]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
case "cms_update_site_settings": {
|
|
198
|
+
const updated = await updateSiteSettings(db, args)
|
|
199
|
+
return {
|
|
200
|
+
jsonrpc: "2.0",
|
|
201
|
+
id,
|
|
202
|
+
result: {
|
|
203
|
+
content: [{ type: "text", text: JSON.stringify(updated, null, 2) }]
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
default:
|
|
209
|
+
return {
|
|
210
|
+
jsonrpc: "2.0",
|
|
211
|
+
id,
|
|
212
|
+
error: {
|
|
213
|
+
code: -32601,
|
|
214
|
+
message: `Method or tool '${name}' not found`
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
jsonrpc: "2.0",
|
|
222
|
+
id,
|
|
223
|
+
error: {
|
|
224
|
+
code: -32601,
|
|
225
|
+
message: `Unknown method: ${method}`
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { pgTable, uuid, text, jsonb, timestamp, uniqueIndex } from "drizzle-orm/pg-core"
|
|
2
|
+
|
|
3
|
+
export const bylines = pgTable(
|
|
4
|
+
"bylines",
|
|
5
|
+
{
|
|
6
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
7
|
+
name: text("name").notNull(),
|
|
8
|
+
slug: text("slug").notNull(),
|
|
9
|
+
websiteUrl: text("website_url"),
|
|
10
|
+
bio: text("bio"),
|
|
11
|
+
avatar: text("avatar"),
|
|
12
|
+
userId: text("user_id"),
|
|
13
|
+
socials: jsonb("socials").$type<Record<string, string>>().default({}),
|
|
14
|
+
metadata: jsonb("metadata").$type<Record<string, any>>().default({}),
|
|
15
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
16
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
17
|
+
deletedAt: timestamp("deleted_at")
|
|
18
|
+
},
|
|
19
|
+
(table) => [uniqueIndex("bylines_slug_idx").on(table.slug)]
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
export type Byline = typeof bylines.$inferSelect
|
|
23
|
+
export type NewByline = typeof bylines.$inferInsert
|
package/src/schema/index.ts
CHANGED
|
@@ -6,7 +6,6 @@ export * from "./page_version_comments.ts"
|
|
|
6
6
|
export * from "./page_version_approvals.ts"
|
|
7
7
|
export * from "./page_templates.ts"
|
|
8
8
|
export * from "./search.ts"
|
|
9
|
-
export * from "./
|
|
10
|
-
export * from "./
|
|
11
|
-
export * from "./
|
|
12
|
-
export * from "./component_dependencies.ts"
|
|
9
|
+
export * from "./site_settings.ts"
|
|
10
|
+
export * from "./taxonomies.ts"
|
|
11
|
+
export * from "./bylines.ts"
|
|
@@ -2,6 +2,7 @@ import { integer, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "dri
|
|
|
2
2
|
import type { BaseBlock } from "../core/types/blocks"
|
|
3
3
|
import type { Localized, PageType } from "../core/types/pages"
|
|
4
4
|
import type { PageVersionStatus } from "../core/types/versioning"
|
|
5
|
+
import type { BylineCredit } from "../core/types/bylines"
|
|
5
6
|
|
|
6
7
|
export const pageVersions = pgTable(
|
|
7
8
|
"page_versions",
|
|
@@ -16,6 +17,7 @@ export const pageVersions = pgTable(
|
|
|
16
17
|
description: jsonb("description").$type<Localized>(),
|
|
17
18
|
tags: jsonb("tags").$type<Localized[]>().default([]).notNull(),
|
|
18
19
|
authorIds: jsonb("author_ids").$type<string[]>().default([]),
|
|
20
|
+
bylines: jsonb("bylines").$type<BylineCredit[]>().default([]),
|
|
19
21
|
content: jsonb("content")
|
|
20
22
|
.$type<{
|
|
21
23
|
blocks: BaseBlock[]
|
package/src/schema/pages.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { sql } from "drizzle-orm"
|
|
|
2
2
|
import { integer, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core"
|
|
3
3
|
import type { BaseBlock } from "../core/types/blocks"
|
|
4
4
|
import type { Localized, PageType } from "../core/types/pages"
|
|
5
|
+
import type { BylineCredit } from "../core/types/bylines"
|
|
5
6
|
import { pageVersions } from "./page_versions.ts"
|
|
6
7
|
import { pageTemplates } from "./page_templates.ts"
|
|
7
8
|
|
|
@@ -17,6 +18,7 @@ export const pages = pgTable(
|
|
|
17
18
|
description: jsonb("description").$type<Localized>(),
|
|
18
19
|
tags: jsonb("tags").$type<Localized[]>().default([]).notNull(),
|
|
19
20
|
authorIds: jsonb("author_ids").$type<string[]>().default([]),
|
|
21
|
+
bylines: jsonb("bylines").$type<BylineCredit[]>().default([]),
|
|
20
22
|
/**
|
|
21
23
|
* Denormalized read-cache of the active published version content (NULL if unpublished draft)
|
|
22
24
|
*/
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core"
|
|
2
|
+
|
|
3
|
+
export interface SiteSettingsBranding {
|
|
4
|
+
logo: {
|
|
5
|
+
alt: string
|
|
6
|
+
}
|
|
7
|
+
favicon: {
|
|
8
|
+
svg: string
|
|
9
|
+
}
|
|
10
|
+
colors: {
|
|
11
|
+
themeColor: string
|
|
12
|
+
backgroundColor: string
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SiteSettingsSeo {
|
|
17
|
+
titleTemplate: string
|
|
18
|
+
ogImageFallback: string
|
|
19
|
+
maxDescriptionLength: number
|
|
20
|
+
authorHandle?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const siteSettings = pgTable("site_settings", {
|
|
24
|
+
id: text("id").primaryKey().default("default"),
|
|
25
|
+
name: text("name").notNull(),
|
|
26
|
+
description: text("description").notNull(),
|
|
27
|
+
url: text("url").notNull(),
|
|
28
|
+
ogImage: text("og_image").notNull(),
|
|
29
|
+
author: text("author").notNull(),
|
|
30
|
+
email: text("email").notNull().default(""),
|
|
31
|
+
branding: jsonb("branding").$type<SiteSettingsBranding>().notNull(),
|
|
32
|
+
seo: jsonb("seo").$type<SiteSettingsSeo>().notNull(),
|
|
33
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
34
|
+
updatedAt: timestamp("updated_at", { withTimezone: true })
|
|
35
|
+
.defaultNow()
|
|
36
|
+
.$onUpdate(() => new Date())
|
|
37
|
+
.notNull()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export type SiteSettings = typeof siteSettings.$inferSelect
|
|
41
|
+
export type InsertSiteSettings = typeof siteSettings.$inferInsert
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
foreignKey,
|
|
3
|
+
integer,
|
|
4
|
+
jsonb,
|
|
5
|
+
pgTable,
|
|
6
|
+
text,
|
|
7
|
+
timestamp,
|
|
8
|
+
uniqueIndex,
|
|
9
|
+
uuid
|
|
10
|
+
} from "drizzle-orm/pg-core"
|
|
11
|
+
import { pages } from "./pages.ts"
|
|
12
|
+
import type { Localized } from "../core/types/pages"
|
|
13
|
+
|
|
14
|
+
export const taxonomyTerms = pgTable(
|
|
15
|
+
"taxonomy_terms",
|
|
16
|
+
{
|
|
17
|
+
id: uuid("id").defaultRandom().notNull().primaryKey(),
|
|
18
|
+
taxonomy: text("taxonomy").notNull(), // "category" | "tag" | custom (e.g. "genre")
|
|
19
|
+
slug: text("slug").notNull(),
|
|
20
|
+
label: jsonb("label").$type<Localized | string>().notNull(),
|
|
21
|
+
description: jsonb("description").$type<Localized | string>(),
|
|
22
|
+
parentId: uuid("parent_id"),
|
|
23
|
+
displayOrder: integer("display_order").default(0).notNull(),
|
|
24
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
25
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(() => new Date())
|
|
26
|
+
},
|
|
27
|
+
(table) => [
|
|
28
|
+
uniqueIndex("taxonomy_terms_tax_slug_idx").on(table.taxonomy, table.slug),
|
|
29
|
+
foreignKey({
|
|
30
|
+
columns: [table.parentId],
|
|
31
|
+
foreignColumns: [table.id],
|
|
32
|
+
name: "taxonomy_terms_parent_id_fk"
|
|
33
|
+
}).onDelete("cascade")
|
|
34
|
+
]
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
export const pageTaxonomyTerms = pgTable(
|
|
38
|
+
"page_taxonomy_terms",
|
|
39
|
+
{
|
|
40
|
+
pageId: uuid("page_id")
|
|
41
|
+
.notNull()
|
|
42
|
+
.references(() => pages.id, { onDelete: "cascade" }),
|
|
43
|
+
termId: uuid("term_id")
|
|
44
|
+
.notNull()
|
|
45
|
+
.references(() => taxonomyTerms.id, { onDelete: "cascade" }),
|
|
46
|
+
assignedAt: timestamp("assigned_at", { withTimezone: true }).defaultNow().notNull()
|
|
47
|
+
},
|
|
48
|
+
(table) => [uniqueIndex("page_taxonomy_terms_page_term_idx").on(table.pageId, table.termId)]
|
|
49
|
+
)
|