@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.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +16 -14
  3. package/src/admin/api/media-file.ts +39 -0
  4. package/src/admin/api/media.ts +199 -0
  5. package/src/admin/layouts/CMSDashboardLayout.astro +178 -0
  6. package/src/admin/layouts/DefaultParentLayout.astro +50 -0
  7. package/src/admin/pages/assets.astro +424 -0
  8. package/src/admin/pages/byline-schema.astro +62 -0
  9. package/src/admin/pages/bylines.astro +16 -0
  10. package/src/admin/pages/categories.astro +21 -0
  11. package/src/admin/pages/content-types.astro +55 -0
  12. package/src/admin/pages/dashboard.astro +61 -0
  13. package/src/admin/pages/pages-edit.astro +595 -0
  14. package/src/admin/pages/pages-index.astro +146 -0
  15. package/src/admin/pages/pages-new.astro +130 -0
  16. package/src/admin/pages/pages-preview.astro +111 -0
  17. package/src/admin/pages/pages-review.astro +236 -0
  18. package/src/admin/pages/settings.astro +203 -0
  19. package/src/admin/pages/tags.astro +21 -0
  20. package/src/admin/pages/templates-edit.astro +234 -0
  21. package/src/admin/pages/templates-index.astro +95 -0
  22. package/src/admin/pages/templates-new.astro +193 -0
  23. package/src/admin/pages/users.astro +74 -0
  24. package/src/admin/pages/versions.astro +74 -0
  25. package/src/astro/BlockRenderer.astro +44 -102
  26. package/src/astro/PageRenderer.astro +9 -2
  27. package/src/astro/blocks/CalloutBlock.astro +7 -7
  28. package/src/astro/blocks/CardBlock.astro +5 -5
  29. package/src/astro/blocks/CodeBlock.astro +2 -2
  30. package/src/astro/blocks/FileTreeBlock.astro +7 -7
  31. package/src/astro/blocks/ImageBlock.astro +2 -2
  32. package/src/astro/blocks/OrderedListBlock.astro +2 -2
  33. package/src/astro/blocks/ParagraphBlock.astro +3 -3
  34. package/src/astro/blocks/SectionBlock.astro +6 -6
  35. package/src/astro/blocks/StepItemBlock.astro +3 -3
  36. package/src/astro/blocks/StepsBlock.astro +1 -1
  37. package/src/astro/blocks/TableBlock.astro +6 -6
  38. package/src/astro/blocks/TabsBlock.astro +9 -9
  39. package/src/astro/blocks/UnorderedListBlock.astro +2 -2
  40. package/src/astro/blocks/index.ts +35 -0
  41. package/src/client/components/RimelightBlock.tsx +19 -19
  42. package/src/client/components/RimelightBlockPicker.tsx +9 -13
  43. package/src/client/components/RimelightBylinesManager.tsx +438 -0
  44. package/src/client/components/RimelightEditor.tsx +3 -3
  45. package/src/client/components/RimelightPreview.tsx +37 -45
  46. package/src/client/components/RimelightPropertiesPanel.tsx +368 -26
  47. package/src/client/components/RimelightTaxonomyManager.tsx +369 -0
  48. package/src/client/components/RimelightTaxonomyMasterDetail.tsx +412 -0
  49. package/src/client/components/RimelightTemplateEditor.tsx +6 -8
  50. package/src/client/components/editors/CalloutEditor.tsx +2 -4
  51. package/src/client/components/editors/CardsEditor.tsx +2 -4
  52. package/src/client/components/editors/CodeEditor.tsx +1 -3
  53. package/src/client/components/editors/FileTreeEditor.tsx +2 -4
  54. package/src/client/components/editors/ImageEditor.tsx +3 -3
  55. package/src/client/components/editors/ParagraphEditor.tsx +2 -2
  56. package/src/client/components/editors/SceneEditor.tsx +6 -12
  57. package/src/client/components/editors/ScriptEditor.tsx +8 -16
  58. package/src/client/components/editors/SectionEditor.tsx +4 -4
  59. package/src/client/components/editors/StepItemEditor.tsx +2 -2
  60. package/src/client/components/editors/StepsEditor.tsx +2 -4
  61. package/src/client/components/editors/TabItemEditor.tsx +3 -3
  62. package/src/client/components/editors/TableEditor.tsx +5 -5
  63. package/src/client/components/editors/TabsEditor.tsx +2 -4
  64. package/src/client/components/index.ts +3 -0
  65. package/src/client/loader.ts +3 -0
  66. package/src/core/diff.ts +145 -0
  67. package/src/core/excerpt.ts +159 -0
  68. package/src/core/preview.ts +87 -0
  69. package/src/core/registry.ts +0 -86
  70. package/src/core/types/blocks.ts +6 -1
  71. package/src/core/types/bylines.ts +14 -0
  72. package/src/core/types/media.ts +13 -0
  73. package/src/core/types/taxonomies.ts +20 -0
  74. package/src/cron/scheduled.ts +57 -0
  75. package/src/docs/agent.ts +116 -0
  76. package/src/docs/config.ts +25 -0
  77. package/src/docs/index.ts +5 -0
  78. package/src/docs/routing.ts +104 -0
  79. package/src/docs/sidebar.ts +251 -0
  80. package/src/docs/toc.ts +47 -0
  81. package/src/env.d.ts +13 -0
  82. package/src/index.ts +40 -8
  83. package/src/integration.ts +196 -0
  84. package/src/loader/live.ts +254 -0
  85. package/src/markdown/index.ts +0 -1
  86. package/src/mcp/index.ts +228 -0
  87. package/src/schema/bylines.ts +23 -0
  88. package/src/schema/index.ts +3 -4
  89. package/src/schema/page_versions.ts +2 -0
  90. package/src/schema/pages.ts +2 -0
  91. package/src/schema/site_settings.ts +41 -0
  92. package/src/schema/taxonomies.ts +49 -0
  93. package/src/services/bylines.ts +74 -0
  94. package/src/services/search-indexer.ts +61 -0
  95. package/src/services/site-settings.ts +108 -0
  96. package/src/services/taxonomies.ts +276 -0
  97. package/src/storage/index.ts +253 -0
  98. package/src/astro/blocks/ApiDocumentationBlock.astro +0 -350
  99. package/src/astro/blocks/DialogueBlock.astro +0 -23
  100. package/src/astro/blocks/LivePreviewBlock.astro +0 -27
  101. package/src/astro/blocks/SceneBlock.astro +0 -65
  102. package/src/astro/blocks/ScriptBlock.astro +0 -59
  103. package/src/core/template-sync.ts +0 -75
  104. package/src/markdown/parser.ts +0 -347
  105. package/src/migration-tools.ts +0 -334
  106. package/src/migration.ts +0 -155
  107. package/src/schema/component_dependencies.ts +0 -21
  108. package/src/schema/component_metadata.ts +0 -31
  109. package/src/schema/component_metadata_cache.ts +0 -15
  110. package/src/schema/component_versions.ts +0 -19
  111. package/src/search/sync.ts +0 -32
  112. package/src/services/component-metadata.ts +0 -134
@@ -0,0 +1,74 @@
1
+ import { eq, and, isNull, or } from "drizzle-orm"
2
+ import { bylines, type Byline, type NewByline } from "../schema/bylines.ts"
3
+
4
+ /**
5
+ * Returns all active (non-deleted) bylines.
6
+ */
7
+ export async function getBylines(db: any): Promise<Byline[]> {
8
+ if (!db) return []
9
+ return db.select().from(bylines).where(isNull(bylines.deletedAt)).orderBy(bylines.name)
10
+ }
11
+
12
+ /**
13
+ * Retrieves a single byline by ID or slug.
14
+ */
15
+ export async function getByline(db: any, idOrSlug: string): Promise<Byline | null> {
16
+ if (!db || !idOrSlug) return null
17
+ const rows = await db
18
+ .select()
19
+ .from(bylines)
20
+ .where(
21
+ and(
22
+ isNull(bylines.deletedAt),
23
+ or(eq(bylines.id, idOrSlug as any), eq(bylines.slug, idOrSlug))
24
+ )
25
+ )
26
+ .limit(1)
27
+
28
+ return rows[0] || null
29
+ }
30
+
31
+ /**
32
+ * Creates a new byline.
33
+ */
34
+ export async function createByline(db: any, data: NewByline): Promise<Byline> {
35
+ const [created] = await db
36
+ .insert(bylines)
37
+ .values({
38
+ ...data,
39
+ updatedAt: new Date()
40
+ })
41
+ .returning()
42
+ return created
43
+ }
44
+
45
+ /**
46
+ * Updates an existing byline.
47
+ */
48
+ export async function updateByline(
49
+ db: any,
50
+ id: string,
51
+ data: Partial<NewByline>
52
+ ): Promise<Byline | null> {
53
+ const [updated] = await db
54
+ .update(bylines)
55
+ .set({
56
+ ...data,
57
+ updatedAt: new Date()
58
+ })
59
+ .where(and(eq(bylines.id, id as any), isNull(bylines.deletedAt)))
60
+ .returning()
61
+ return updated || null
62
+ }
63
+
64
+ /**
65
+ * Soft deletes a byline.
66
+ */
67
+ export async function deleteByline(db: any, id: string): Promise<boolean> {
68
+ const [deleted] = await db
69
+ .update(bylines)
70
+ .set({ deletedAt: new Date(), updatedAt: new Date() })
71
+ .where(and(eq(bylines.id, id as any), isNull(bylines.deletedAt)))
72
+ .returning()
73
+ return !!deleted
74
+ }
@@ -0,0 +1,61 @@
1
+ import { extractTextFromBlocks } from "../core/excerpt"
2
+ import { contentSearchIndex } from "../schema/search"
3
+ import { eq, and, sql } from "drizzle-orm"
4
+
5
+ export async function indexPageForSearch(
6
+ db: any,
7
+ page: {
8
+ id: string
9
+ title: any
10
+ content: any
11
+ },
12
+ locales: string[] = ["en", "pt"]
13
+ ) {
14
+ if (!db) return
15
+
16
+ const parseJson = (val: any) => (typeof val === "string" ? JSON.parse(val) : val)
17
+ const titleObj = parseJson(page.title) || {}
18
+ const contentObj = parseJson(page.content) || {}
19
+ const blocks = contentObj.blocks || []
20
+
21
+ for (const locale of locales) {
22
+ const titleText =
23
+ typeof titleObj === "object" && titleObj !== null
24
+ ? titleObj[locale] || titleObj.en || Object.values(titleObj)[0] || ""
25
+ : String(titleObj || "")
26
+
27
+ const contentText = extractTextFromBlocks(blocks, locale)
28
+
29
+ if (!titleText && !contentText) continue
30
+
31
+ // Upsert search index record
32
+ try {
33
+ const existing = await db
34
+ .select()
35
+ .from(contentSearchIndex)
36
+ .where(and(eq(contentSearchIndex.pageId, page.id), eq(contentSearchIndex.locale, locale)))
37
+ .limit(1)
38
+
39
+ if (existing.length > 0) {
40
+ await db
41
+ .update(contentSearchIndex)
42
+ .set({
43
+ titleText,
44
+ contentText,
45
+ searchVector: sql`to_tsvector('english', ${titleText} || ' ' || ${contentText})`
46
+ })
47
+ .where(eq(contentSearchIndex.id, existing[0].id))
48
+ } else {
49
+ await db.insert(contentSearchIndex).values({
50
+ pageId: page.id,
51
+ locale,
52
+ titleText,
53
+ contentText,
54
+ searchVector: sql`to_tsvector('english', ${titleText} || ' ' || ${contentText})`
55
+ })
56
+ }
57
+ } catch (err) {
58
+ console.error(`[Search Indexer] Failed to index page ${page.id} for locale ${locale}:`, err)
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,108 @@
1
+ import { eq } from "drizzle-orm"
2
+ import {
3
+ siteSettings,
4
+ type InsertSiteSettings,
5
+ type SiteSettings
6
+ } from "../schema/site_settings.ts"
7
+
8
+ export const DEFAULT_SITE_SETTINGS: InsertSiteSettings = {
9
+ id: "default",
10
+ name: "Rimelight",
11
+ description: "Built with Rimelight CMS",
12
+ url: "https://rimelight.com",
13
+ ogImage: "https://cdn.rimelight.com/Images/og-placeholder.webp",
14
+ author: "Rimelight",
15
+ email: "",
16
+ branding: {
17
+ logo: {
18
+ alt: "Rimelight"
19
+ },
20
+ favicon: {
21
+ svg: "/favicon.svg"
22
+ },
23
+ colors: {
24
+ themeColor: "#ffffff",
25
+ backgroundColor: "#ffffff"
26
+ }
27
+ },
28
+ seo: {
29
+ titleTemplate: "%s | Rimelight",
30
+ ogImageFallback: "https://cdn.rimelight.com/Images/og-placeholder.webp",
31
+ maxDescriptionLength: 160
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Retrieves the dynamic site settings from the database. If no settings are found, seeds the
37
+ * default record and returns it.
38
+ */
39
+ export async function getSiteSettings(
40
+ db: any,
41
+ fallbackDefaults?: Partial<InsertSiteSettings>
42
+ ): Promise<SiteSettings> {
43
+ if (!db) {
44
+ // eslint-disable-next-line typescript/no-unsafe-type-assertion
45
+ return {
46
+ ...DEFAULT_SITE_SETTINGS,
47
+ ...fallbackDefaults,
48
+ createdAt: new Date(),
49
+ updatedAt: new Date()
50
+ } as SiteSettings
51
+ }
52
+
53
+ try {
54
+ const records = await db
55
+ .select()
56
+ .from(siteSettings)
57
+ .where(eq(siteSettings.id, "default"))
58
+ .limit(1)
59
+
60
+ if (records.length > 0) {
61
+ return records[0]
62
+ }
63
+
64
+ const initial: InsertSiteSettings = {
65
+ ...DEFAULT_SITE_SETTINGS,
66
+ ...fallbackDefaults,
67
+ id: "default"
68
+ }
69
+
70
+ const inserted = await db.insert(siteSettings).values(initial).returning()
71
+ return inserted[0]
72
+ } catch (error) {
73
+ console.warn("Failed to fetch site settings from DB, using fallback:", error)
74
+ // eslint-disable-next-line typescript/no-unsafe-type-assertion
75
+ return {
76
+ ...DEFAULT_SITE_SETTINGS,
77
+ ...fallbackDefaults,
78
+ createdAt: new Date(),
79
+ updatedAt: new Date()
80
+ } as SiteSettings
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Updates dynamic site settings in the database.
86
+ */
87
+ export async function updateSiteSettings(
88
+ db: any,
89
+ updates: Partial<InsertSiteSettings>,
90
+ id: string = "default"
91
+ ): Promise<SiteSettings> {
92
+ const current = await getSiteSettings(db)
93
+
94
+ const updatedValues = {
95
+ ...updates,
96
+ branding: updates.branding ? { ...current.branding, ...updates.branding } : current.branding,
97
+ seo: updates.seo ? { ...current.seo, ...updates.seo } : current.seo,
98
+ updatedAt: new Date()
99
+ }
100
+
101
+ const result = await db
102
+ .update(siteSettings)
103
+ .set(updatedValues)
104
+ .where(eq(siteSettings.id, id))
105
+ .returning()
106
+
107
+ return result[0]
108
+ }
@@ -0,0 +1,276 @@
1
+ import { taxonomyTerms, pageTaxonomyTerms } from "../schema/taxonomies"
2
+ import { pages } from "../schema/pages"
3
+ import type { TaxonomyTerm, TaxonomyQueryOptions } from "../core/types/taxonomies"
4
+ import { eq, and, isNull, asc, sql } from "drizzle-orm"
5
+
6
+ /**
7
+ * Retrieves all terms for a given taxonomy. If hierarchical, returns a tree structure with
8
+ * `children: TaxonomyTerm[]`.
9
+ */
10
+ export async function getTaxonomyTerms(
11
+ db: any,
12
+ taxonomy: string,
13
+ options: TaxonomyQueryOptions = {}
14
+ ): Promise<TaxonomyTerm[]> {
15
+ if (!db) return []
16
+
17
+ const includeCounts = options.includeCounts ?? true
18
+
19
+ const rawTerms = await db
20
+ .select()
21
+ .from(taxonomyTerms)
22
+ .where(eq(taxonomyTerms.taxonomy, taxonomy))
23
+ .orderBy(asc(taxonomyTerms.displayOrder), asc(taxonomyTerms.createdAt))
24
+
25
+ if (!rawTerms || rawTerms.length === 0) return []
26
+
27
+ // Count active published posts per term if requested
28
+ const countsMap = new Map<string, number>()
29
+ if (includeCounts) {
30
+ try {
31
+ const counts = await db
32
+ .select({
33
+ termId: pageTaxonomyTerms.termId,
34
+ count: sql<number>`count(${pageTaxonomyTerms.pageId})::int`
35
+ })
36
+ .from(pageTaxonomyTerms)
37
+ .innerJoin(pages, eq(pages.id, pageTaxonomyTerms.pageId))
38
+ .where(isNull(pages.deletedAt))
39
+ .groupBy(pageTaxonomyTerms.termId)
40
+
41
+ for (const row of counts) {
42
+ countsMap.set(row.termId, Number(row.count))
43
+ }
44
+ } catch {
45
+ // Fallback if joined counts query fails
46
+ }
47
+ }
48
+
49
+ const termsWithCounts: TaxonomyTerm[] = rawTerms.map((t: any) => ({
50
+ id: t.id,
51
+ taxonomy: t.taxonomy,
52
+ slug: t.slug,
53
+ label: t.label,
54
+ description: t.description,
55
+ parentId: t.parentId,
56
+ displayOrder: t.displayOrder,
57
+ count: countsMap.get(t.id) ?? 0,
58
+ children: [],
59
+ createdAt: t.createdAt,
60
+ updatedAt: t.updatedAt
61
+ }))
62
+
63
+ // Determine if this taxonomy is hierarchical (any term has parentId)
64
+ const isHierarchical = termsWithCounts.some((t) => !!t.parentId)
65
+ if (!isHierarchical) {
66
+ return termsWithCounts
67
+ }
68
+
69
+ // Build tree
70
+ const idMap = new Map<string, TaxonomyTerm>()
71
+ const rootTerms: TaxonomyTerm[] = []
72
+
73
+ termsWithCounts.forEach((term) => {
74
+ idMap.set(term.id, term)
75
+ })
76
+
77
+ termsWithCounts.forEach((term) => {
78
+ if (term.parentId && idMap.has(term.parentId)) {
79
+ const parent = idMap.get(term.parentId)!
80
+ parent.children = parent.children || []
81
+ parent.children.push(term)
82
+ } else {
83
+ rootTerms.push(term)
84
+ }
85
+ })
86
+
87
+ return rootTerms
88
+ }
89
+
90
+ /**
91
+ * Retrieves a single term by taxonomy and slug.
92
+ */
93
+ export async function getTerm(
94
+ db: any,
95
+ taxonomy: string,
96
+ slug: string
97
+ ): Promise<TaxonomyTerm | null> {
98
+ if (!db) return null
99
+
100
+ const results = await db
101
+ .select()
102
+ .from(taxonomyTerms)
103
+ .where(and(eq(taxonomyTerms.taxonomy, taxonomy), eq(taxonomyTerms.slug, slug)))
104
+ .limit(1)
105
+
106
+ if (!results[0]) return null
107
+
108
+ // Get post count
109
+ let count = 0
110
+ try {
111
+ const countResult = await db
112
+ .select({ count: sql<number>`count(${pageTaxonomyTerms.pageId})::int` })
113
+ .from(pageTaxonomyTerms)
114
+ .innerJoin(pages, eq(pages.id, pageTaxonomyTerms.pageId))
115
+ .where(and(eq(pageTaxonomyTerms.termId, results[0].id), isNull(pages.deletedAt)))
116
+ count = Number(countResult[0]?.count ?? 0)
117
+ } catch {
118
+ count = 0
119
+ }
120
+
121
+ return {
122
+ ...results[0],
123
+ count,
124
+ children: []
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Retrieves all terms assigned to a specific page entry.
130
+ */
131
+ export async function getEntryTerms(
132
+ db: any,
133
+ pageId: string,
134
+ taxonomy?: string
135
+ ): Promise<TaxonomyTerm[]> {
136
+ if (!db || !pageId) return []
137
+
138
+ const conditions = [eq(pageTaxonomyTerms.pageId, pageId)]
139
+ if (taxonomy) {
140
+ conditions.push(eq(taxonomyTerms.taxonomy, taxonomy))
141
+ }
142
+
143
+ const results = await db
144
+ .select({
145
+ id: taxonomyTerms.id,
146
+ taxonomy: taxonomyTerms.taxonomy,
147
+ slug: taxonomyTerms.slug,
148
+ label: taxonomyTerms.label,
149
+ description: taxonomyTerms.description,
150
+ parentId: taxonomyTerms.parentId,
151
+ displayOrder: taxonomyTerms.displayOrder
152
+ })
153
+ .from(pageTaxonomyTerms)
154
+ .innerJoin(taxonomyTerms, eq(taxonomyTerms.id, pageTaxonomyTerms.termId))
155
+ .where(and(...conditions))
156
+ .orderBy(asc(taxonomyTerms.displayOrder))
157
+
158
+ return results
159
+ }
160
+
161
+ /**
162
+ * Retrieves pages tagged with a given taxonomy term.
163
+ */
164
+ export async function getEntriesByTerm(
165
+ db: any,
166
+ taxonomy: string,
167
+ slug: string,
168
+ options: { limit?: number; offset?: number; type?: string } = {}
169
+ ) {
170
+ if (!db) return { entries: [], total: 0 }
171
+
172
+ const term = await getTerm(db, taxonomy, slug)
173
+ if (!term) return { entries: [], total: 0 }
174
+
175
+ const conditions: any[] = [eq(pageTaxonomyTerms.termId, term.id), isNull(pages.deletedAt)]
176
+
177
+ if (options.type) {
178
+ conditions.push(eq(pages.type, options.type))
179
+ }
180
+
181
+ let query = db
182
+ .select({ page: pages })
183
+ .from(pageTaxonomyTerms)
184
+ .innerJoin(pages, eq(pages.id, pageTaxonomyTerms.pageId))
185
+ .where(and(...conditions))
186
+ .orderBy(asc(pages.createdAt))
187
+
188
+ if (options.limit) query = query.limit(options.limit)
189
+ if (options.offset) query = query.offset(options.offset)
190
+
191
+ const rows = await query
192
+ const entries = rows.map((r: any) => r.page)
193
+
194
+ return { entries, total: term.count ?? entries.length, term }
195
+ }
196
+
197
+ /**
198
+ * Sets/syncs the assigned terms for a page within a specific taxonomy.
199
+ */
200
+ export async function assignEntryTerms(
201
+ db: any,
202
+ pageId: string,
203
+ taxonomy: string,
204
+ termIds: string[]
205
+ ) {
206
+ if (!db || !pageId) return
207
+
208
+ // Find all current terms in this taxonomy assigned to the page
209
+ const currentAssigned = await getEntryTerms(db, pageId, taxonomy)
210
+ const currentTermIds = new Set(currentAssigned.map((t) => t.id))
211
+ const newTermIds = new Set(termIds)
212
+
213
+ // Remove unassigned
214
+ for (const term of currentAssigned) {
215
+ if (!newTermIds.has(term.id)) {
216
+ await db
217
+ .delete(pageTaxonomyTerms)
218
+ .where(and(eq(pageTaxonomyTerms.pageId, pageId), eq(pageTaxonomyTerms.termId, term.id)))
219
+ }
220
+ }
221
+
222
+ // Insert newly assigned
223
+ for (const termId of termIds) {
224
+ if (!currentTermIds.has(termId)) {
225
+ await db.insert(pageTaxonomyTerms).values({ pageId, termId }).onConflictDoNothing()
226
+ }
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Creates a new taxonomy term.
232
+ */
233
+ export async function createTerm(
234
+ db: any,
235
+ data: {
236
+ taxonomy: string
237
+ slug: string
238
+ label: any
239
+ description?: any
240
+ parentId?: string | null
241
+ displayOrder?: number
242
+ }
243
+ ) {
244
+ const result = await db.insert(taxonomyTerms).values(data).returning()
245
+ return result[0]
246
+ }
247
+
248
+ /**
249
+ * Updates an existing taxonomy term.
250
+ */
251
+ export async function updateTerm(
252
+ db: any,
253
+ id: string,
254
+ updates: Partial<{
255
+ slug: string
256
+ label: any
257
+ description: any
258
+ parentId: string | null
259
+ displayOrder: number
260
+ }>
261
+ ) {
262
+ const result = await db
263
+ .update(taxonomyTerms)
264
+ .set({ ...updates, updatedAt: new Date() })
265
+ .where(eq(taxonomyTerms.id, id))
266
+ .returning()
267
+ return result[0]
268
+ }
269
+
270
+ /**
271
+ * Deletes a taxonomy term.
272
+ */
273
+ export async function deleteTerm(db: any, id: string) {
274
+ await db.delete(taxonomyTerms).where(eq(taxonomyTerms.id, id))
275
+ return { success: true }
276
+ }