@rimelight/cms 0.0.9 → 0.0.10

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/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { bylines, contentSearchIndex, pageDraftLocks, pageDrafts, pageTaxonomyTerms, pageTemplates, pageVersionApprovals, pageVersionComments, pageVersions, pages, siteSettings, taxonomyTerms } from "./schema/index.mjs";
1
+ import { bylines, contentSearchIndex, contentTypes, pageDraftLocks, pageDrafts, pageTaxonomyTerms, pageTemplates, pageVersionApprovals, pageVersionComments, pageVersions, pages, siteSettings, taxonomyTerms } from "./schema/index.mjs";
2
2
  import { rimelightCms } from "./integration.mjs";
3
3
  import { deleteR2File, getR2Bucket, listR2Files, r2, uploadR2File } from "./storage/index.mjs";
4
4
  import { auth0Auth, cfAccessAuth, evaluateAccess, hasPermission, hasRole, mockAuth } from "@rimelight/auth";
5
- import { and, asc, desc, eq, isNotNull, isNull, lt, lte, or, sql } from "drizzle-orm";
5
+ import { and, asc, desc, eq, isNotNull, isNull, like, lt, lte, or, sql } from "drizzle-orm";
6
6
  import { addToast, clearToasts, removeToast, showErrorToast, showSuccessToast, toast, toasts } from "@rimelight/ui/components/toast/store.ts";
7
7
  //#region src/core/types/blocks.ts
8
8
  const MIN_SECTION_HEADING_LEVEL = 2;
@@ -1274,17 +1274,27 @@ const DEFAULT_SITE_SETTINGS = {
1274
1274
  maxDescriptionLength: 160
1275
1275
  }
1276
1276
  };
1277
+ function fallbackSettings(overrides) {
1278
+ return {
1279
+ id: overrides?.id ?? "default",
1280
+ name: overrides?.name ?? DEFAULT_SITE_SETTINGS.name,
1281
+ description: overrides?.description ?? DEFAULT_SITE_SETTINGS.description,
1282
+ url: overrides?.url ?? DEFAULT_SITE_SETTINGS.url,
1283
+ ogImage: overrides?.ogImage ?? DEFAULT_SITE_SETTINGS.ogImage,
1284
+ author: overrides?.author ?? DEFAULT_SITE_SETTINGS.author,
1285
+ email: overrides?.email ?? "",
1286
+ branding: overrides?.branding ?? DEFAULT_SITE_SETTINGS.branding,
1287
+ seo: overrides?.seo ?? DEFAULT_SITE_SETTINGS.seo,
1288
+ createdAt: /* @__PURE__ */ new Date(),
1289
+ updatedAt: /* @__PURE__ */ new Date()
1290
+ };
1291
+ }
1277
1292
  /**
1278
1293
  * Retrieves the dynamic site settings from the database. If no settings are found, seeds the
1279
1294
  * default record and returns it.
1280
1295
  */
1281
1296
  async function getSiteSettings(db, fallbackDefaults) {
1282
- if (!db) return {
1283
- ...DEFAULT_SITE_SETTINGS,
1284
- ...fallbackDefaults,
1285
- createdAt: /* @__PURE__ */ new Date(),
1286
- updatedAt: /* @__PURE__ */ new Date()
1287
- };
1297
+ if (!db) return fallbackSettings(fallbackDefaults);
1288
1298
  try {
1289
1299
  const records = await db.select().from(siteSettings).where(eq(siteSettings.id, "default")).limit(1);
1290
1300
  if (records.length > 0) return records[0];
@@ -1296,12 +1306,7 @@ async function getSiteSettings(db, fallbackDefaults) {
1296
1306
  return (await db.insert(siteSettings).values(initial).returning())[0];
1297
1307
  } catch (error) {
1298
1308
  console.warn("Failed to fetch site settings from DB, using fallback:", error);
1299
- return {
1300
- ...DEFAULT_SITE_SETTINGS,
1301
- ...fallbackDefaults,
1302
- createdAt: /* @__PURE__ */ new Date(),
1303
- updatedAt: /* @__PURE__ */ new Date()
1304
- };
1309
+ return fallbackSettings(fallbackDefaults);
1305
1310
  }
1306
1311
  }
1307
1312
  /**
@@ -1487,6 +1492,186 @@ async function handleCmsCron(_event, _env, _ctx, db) {
1487
1492
  return result;
1488
1493
  }
1489
1494
  //#endregion
1495
+ //#region src/services/taxonomies.ts
1496
+ /**
1497
+ * Retrieves all terms for a given taxonomy. If hierarchical, returns a tree structure with
1498
+ * `children: TaxonomyTerm[]`.
1499
+ */
1500
+ async function getTaxonomyTerms(db, taxonomy, options = {}) {
1501
+ if (!db) return [];
1502
+ const includeCounts = options.includeCounts ?? true;
1503
+ const rawTerms = await db.select().from(taxonomyTerms).where(eq(taxonomyTerms.taxonomy, taxonomy)).orderBy(asc(taxonomyTerms.displayOrder), asc(taxonomyTerms.createdAt));
1504
+ if (!rawTerms || rawTerms.length === 0) return [];
1505
+ const countsMap = /* @__PURE__ */ new Map();
1506
+ if (includeCounts) try {
1507
+ const counts = await db.select({
1508
+ termId: pageTaxonomyTerms.termId,
1509
+ count: sql`count(${pageTaxonomyTerms.pageId})::int`
1510
+ }).from(pageTaxonomyTerms).innerJoin(pages, eq(pages.id, pageTaxonomyTerms.pageId)).where(isNull(pages.deletedAt)).groupBy(pageTaxonomyTerms.termId);
1511
+ for (const row of counts) countsMap.set(row.termId, Number(row.count));
1512
+ } catch {}
1513
+ const termsWithCounts = rawTerms.map((t) => ({
1514
+ id: t.id,
1515
+ taxonomy: t.taxonomy,
1516
+ slug: t.slug,
1517
+ label: t.label,
1518
+ description: t.description,
1519
+ parentId: t.parentId,
1520
+ displayOrder: t.displayOrder,
1521
+ count: countsMap.get(t.id) ?? 0,
1522
+ children: [],
1523
+ createdAt: t.createdAt,
1524
+ updatedAt: t.updatedAt
1525
+ }));
1526
+ if (!termsWithCounts.some((t) => !!t.parentId)) return termsWithCounts;
1527
+ const idMap = /* @__PURE__ */ new Map();
1528
+ const rootTerms = [];
1529
+ termsWithCounts.forEach((term) => {
1530
+ idMap.set(term.id, term);
1531
+ });
1532
+ termsWithCounts.forEach((term) => {
1533
+ if (term.parentId && idMap.has(term.parentId)) {
1534
+ const parent = idMap.get(term.parentId);
1535
+ parent.children = parent.children || [];
1536
+ parent.children.push(term);
1537
+ } else rootTerms.push(term);
1538
+ });
1539
+ return rootTerms;
1540
+ }
1541
+ /**
1542
+ * Retrieves a single term by taxonomy and slug.
1543
+ */
1544
+ async function getTerm(db, taxonomy, slug) {
1545
+ if (!db) return null;
1546
+ const results = await db.select().from(taxonomyTerms).where(and(eq(taxonomyTerms.taxonomy, taxonomy), eq(taxonomyTerms.slug, slug))).limit(1);
1547
+ if (!results[0]) return null;
1548
+ let count = 0;
1549
+ try {
1550
+ const countResult = await db.select({ count: sql`count(${pageTaxonomyTerms.pageId})::int` }).from(pageTaxonomyTerms).innerJoin(pages, eq(pages.id, pageTaxonomyTerms.pageId)).where(and(eq(pageTaxonomyTerms.termId, results[0].id), isNull(pages.deletedAt)));
1551
+ count = Number(countResult[0]?.count ?? 0);
1552
+ } catch {
1553
+ count = 0;
1554
+ }
1555
+ return {
1556
+ ...results[0],
1557
+ count,
1558
+ children: []
1559
+ };
1560
+ }
1561
+ /**
1562
+ * Retrieves all terms assigned to a specific page entry.
1563
+ */
1564
+ async function getEntryTerms(db, pageId, taxonomy) {
1565
+ if (!db || !pageId) return [];
1566
+ const conditions = [eq(pageTaxonomyTerms.pageId, pageId)];
1567
+ if (taxonomy) conditions.push(eq(taxonomyTerms.taxonomy, taxonomy));
1568
+ return await db.select({
1569
+ id: taxonomyTerms.id,
1570
+ taxonomy: taxonomyTerms.taxonomy,
1571
+ slug: taxonomyTerms.slug,
1572
+ label: taxonomyTerms.label,
1573
+ description: taxonomyTerms.description,
1574
+ parentId: taxonomyTerms.parentId,
1575
+ displayOrder: taxonomyTerms.displayOrder
1576
+ }).from(pageTaxonomyTerms).innerJoin(taxonomyTerms, eq(taxonomyTerms.id, pageTaxonomyTerms.termId)).where(and(...conditions)).orderBy(asc(taxonomyTerms.displayOrder));
1577
+ }
1578
+ /**
1579
+ * Retrieves pages tagged with a given taxonomy term.
1580
+ */
1581
+ async function getEntriesByTerm(db, taxonomy, slug, options = {}) {
1582
+ if (!db) return {
1583
+ entries: [],
1584
+ total: 0
1585
+ };
1586
+ const term = await getTerm(db, taxonomy, slug);
1587
+ if (!term) return {
1588
+ entries: [],
1589
+ total: 0
1590
+ };
1591
+ const conditions = [eq(pageTaxonomyTerms.termId, term.id), isNull(pages.deletedAt)];
1592
+ if (options.type) conditions.push(eq(pages.type, options.type));
1593
+ let query = db.select({ page: pages }).from(pageTaxonomyTerms).innerJoin(pages, eq(pages.id, pageTaxonomyTerms.pageId)).where(and(...conditions)).orderBy(asc(pages.createdAt));
1594
+ if (options.limit) query = query.limit(options.limit);
1595
+ if (options.offset) query = query.offset(options.offset);
1596
+ const entries = (await query).map((r) => r.page);
1597
+ return {
1598
+ entries,
1599
+ total: term.count ?? entries.length,
1600
+ term
1601
+ };
1602
+ }
1603
+ /**
1604
+ * Sets/syncs the assigned terms for a page within a specific taxonomy.
1605
+ */
1606
+ async function assignEntryTerms(db, pageId, taxonomy, termIds) {
1607
+ if (!db || !pageId) return;
1608
+ const currentAssigned = await getEntryTerms(db, pageId, taxonomy);
1609
+ const currentTermIds = new Set(currentAssigned.map((t) => t.id));
1610
+ const newTermIds = new Set(termIds);
1611
+ for (const term of currentAssigned) if (!newTermIds.has(term.id)) await db.delete(pageTaxonomyTerms).where(and(eq(pageTaxonomyTerms.pageId, pageId), eq(pageTaxonomyTerms.termId, term.id)));
1612
+ for (const termId of termIds) if (!currentTermIds.has(termId)) await db.insert(pageTaxonomyTerms).values({
1613
+ pageId,
1614
+ termId
1615
+ }).onConflictDoNothing();
1616
+ }
1617
+ /**
1618
+ * Creates a new taxonomy term.
1619
+ */
1620
+ async function createTerm(db, data) {
1621
+ return (await db.insert(taxonomyTerms).values(data).returning())[0];
1622
+ }
1623
+ /**
1624
+ * Updates an existing taxonomy term.
1625
+ */
1626
+ async function updateTerm(db, id, updates) {
1627
+ return (await db.update(taxonomyTerms).set({
1628
+ ...updates,
1629
+ updatedAt: /* @__PURE__ */ new Date()
1630
+ }).where(eq(taxonomyTerms.id, id)).returning())[0];
1631
+ }
1632
+ /**
1633
+ * Deletes a taxonomy term.
1634
+ */
1635
+ async function deleteTerm(db, id) {
1636
+ await db.delete(taxonomyTerms).where(eq(taxonomyTerms.id, id));
1637
+ return { success: true };
1638
+ }
1639
+ //#endregion
1640
+ //#region src/services/search-indexer.ts
1641
+ async function indexPageForSearch(db, page, locales = ["en", "pt"]) {
1642
+ if (!db) return;
1643
+ const parseJson = (val) => typeof val === "string" ? JSON.parse(val) : val;
1644
+ const titleObj = parseJson(page.title) || {};
1645
+ const blocks = (parseJson(page.content) || {}).blocks || [];
1646
+ for (const locale of locales) {
1647
+ const titleText = typeof titleObj === "object" && titleObj !== null ? titleObj[locale] || titleObj.en || Object.values(titleObj)[0] || "" : String(titleObj || "");
1648
+ const contentText = extractTextFromBlocks(blocks, locale);
1649
+ if (!titleText && !contentText) continue;
1650
+ try {
1651
+ const existing = await db.select().from(contentSearchIndex).where(and(eq(contentSearchIndex.pageId, page.id), eq(contentSearchIndex.locale, locale))).limit(1);
1652
+ const hasSearchVector = "searchVector" in contentSearchIndex;
1653
+ const updatePayload = {
1654
+ titleText,
1655
+ contentText
1656
+ };
1657
+ if (hasSearchVector) updatePayload["searchVector"] = sql`to_tsvector('english', ${titleText} || ' ' || ${contentText})`;
1658
+ if (existing.length > 0) await db.update(contentSearchIndex).set(updatePayload).where(eq(contentSearchIndex.id, existing[0].id));
1659
+ else {
1660
+ const insertPayload = {
1661
+ pageId: page.id,
1662
+ locale,
1663
+ titleText,
1664
+ contentText
1665
+ };
1666
+ if (hasSearchVector) insertPayload["searchVector"] = sql`to_tsvector('english', ${titleText} || ' ' || ${contentText})`;
1667
+ await db.insert(contentSearchIndex).values(insertPayload);
1668
+ }
1669
+ } catch (err) {
1670
+ console.error(`[Search Indexer] Failed to index page ${page.id} for locale ${locale}:`, err);
1671
+ }
1672
+ }
1673
+ }
1674
+ //#endregion
1490
1675
  //#region src/mcp/index.ts
1491
1676
  const CMS_MCP_TOOLS = [
1492
1677
  {
@@ -1497,7 +1682,7 @@ const CMS_MCP_TOOLS = [
1497
1682
  properties: {
1498
1683
  type: {
1499
1684
  type: "string",
1500
- description: "Filter by page type (e.g. 'blog', 'docs', 'landing')"
1685
+ description: "Filter by page type (e.g. 'blog', 'doc', 'legal', 'wiki')"
1501
1686
  },
1502
1687
  includeDrafts: {
1503
1688
  type: "boolean",
@@ -1506,13 +1691,17 @@ const CMS_MCP_TOOLS = [
1506
1691
  limit: {
1507
1692
  type: "number",
1508
1693
  description: "Maximum number of pages to return (default 20)"
1694
+ },
1695
+ offset: {
1696
+ type: "number",
1697
+ description: "Offset for pagination (default 0)"
1509
1698
  }
1510
1699
  }
1511
1700
  }
1512
1701
  },
1513
1702
  {
1514
1703
  name: "cms_get_page",
1515
- description: "Get full details, block tree, and properties for a specific CMS page by slug or id.",
1704
+ description: "Get full details, block tree, properties, bylines, and active draft for a page by slug or UUID.",
1516
1705
  inputSchema: {
1517
1706
  type: "object",
1518
1707
  properties: { slugOrId: {
@@ -1522,9 +1711,105 @@ const CMS_MCP_TOOLS = [
1522
1711
  required: ["slugOrId"]
1523
1712
  }
1524
1713
  },
1714
+ {
1715
+ name: "cms_create_page",
1716
+ description: "Create a new CMS page and its initial draft blocks.",
1717
+ inputSchema: {
1718
+ type: "object",
1719
+ properties: {
1720
+ slug: {
1721
+ type: "string",
1722
+ description: "Unique URL slug for the page"
1723
+ },
1724
+ type: {
1725
+ type: "string",
1726
+ description: "Page type (e.g. 'blog', 'doc', 'legal', 'wiki', 'character')"
1727
+ },
1728
+ title: {
1729
+ type: "object",
1730
+ description: "Localized title object, e.g. { en: 'Page Title' }"
1731
+ },
1732
+ description: {
1733
+ type: "object",
1734
+ description: "Localized description object, e.g. { en: 'Page description' }"
1735
+ },
1736
+ templateId: {
1737
+ type: "string",
1738
+ description: "Optional UUID of the template to base the page on"
1739
+ },
1740
+ tags: {
1741
+ type: "array",
1742
+ description: "Array of localized tags or strings"
1743
+ },
1744
+ authorIds: {
1745
+ type: "array",
1746
+ description: "Array of author user IDs"
1747
+ },
1748
+ bylines: {
1749
+ type: "array",
1750
+ description: "Array of structured byline credits [{ bylineId, role, customCredit }]"
1751
+ },
1752
+ blocks: {
1753
+ type: "array",
1754
+ description: "Initial structured CMS blocks array"
1755
+ },
1756
+ properties: {
1757
+ type: "object",
1758
+ description: "Content type specific custom properties"
1759
+ }
1760
+ },
1761
+ required: [
1762
+ "slug",
1763
+ "type",
1764
+ "title"
1765
+ ]
1766
+ }
1767
+ },
1768
+ {
1769
+ name: "cms_update_page",
1770
+ description: "Update an existing CMS page's metadata, properties, bylines, or content blocks.",
1771
+ inputSchema: {
1772
+ type: "object",
1773
+ properties: {
1774
+ id: {
1775
+ type: "string",
1776
+ description: "Page UUID"
1777
+ },
1778
+ slug: {
1779
+ type: "string",
1780
+ description: "New unique slug"
1781
+ },
1782
+ title: {
1783
+ type: "object",
1784
+ description: "Localized title object"
1785
+ },
1786
+ description: {
1787
+ type: "object",
1788
+ description: "Localized description object"
1789
+ },
1790
+ tags: {
1791
+ type: "array",
1792
+ description: "Array of localized tags"
1793
+ },
1794
+ bylines: {
1795
+ type: "array",
1796
+ description: "Array of structured byline credits"
1797
+ },
1798
+ blocks: {
1799
+ type: "array",
1800
+ description: "Updated structured CMS blocks array"
1801
+ },
1802
+ properties: {
1803
+ type: "object",
1804
+ description: "Updated custom properties object"
1805
+ }
1806
+ },
1807
+ required: ["id"]
1808
+ }
1809
+ },
1525
1810
  {
1526
1811
  name: "cms_create_draft",
1527
- description: "Create or update a draft version for a CMS page.",
1812
+ description: "Create or update an in-progress draft version for a CMS page without publishing.",
1528
1813
  inputSchema: {
1529
1814
  type: "object",
1530
1815
  properties: {
@@ -1534,7 +1819,7 @@ const CMS_MCP_TOOLS = [
1534
1819
  },
1535
1820
  userId: {
1536
1821
  type: "string",
1537
- description: "User ID creating the draft"
1822
+ description: "User or Agent ID creating the draft"
1538
1823
  },
1539
1824
  blocks: {
1540
1825
  type: "array",
@@ -1553,11 +1838,581 @@ const CMS_MCP_TOOLS = [
1553
1838
  }
1554
1839
  },
1555
1840
  {
1556
- name: "cms_get_site_settings",
1557
- description: "Retrieve current dynamic site settings including branding and SEO defaults.",
1841
+ name: "cms_publish_page",
1842
+ description: "Publish a page draft, creating a new immutable version snapshot.",
1558
1843
  inputSchema: {
1559
1844
  type: "object",
1560
- properties: {}
1845
+ properties: {
1846
+ pageId: {
1847
+ type: "string",
1848
+ description: "Page UUID"
1849
+ },
1850
+ userId: {
1851
+ type: "string",
1852
+ description: "User or Agent ID publishing the page"
1853
+ },
1854
+ changeSummary: {
1855
+ type: "string",
1856
+ description: "Summary of changes in this version"
1857
+ }
1858
+ },
1859
+ required: ["pageId", "userId"]
1860
+ }
1861
+ },
1862
+ {
1863
+ name: "cms_unpublish_page",
1864
+ description: "Unpublish a live page, reverting it to draft status.",
1865
+ inputSchema: {
1866
+ type: "object",
1867
+ properties: { pageId: {
1868
+ type: "string",
1869
+ description: "Page UUID to unpublish"
1870
+ } },
1871
+ required: ["pageId"]
1872
+ }
1873
+ },
1874
+ {
1875
+ name: "cms_delete_page",
1876
+ description: "Soft delete a CMS page by setting deleted_at.",
1877
+ inputSchema: {
1878
+ type: "object",
1879
+ properties: { pageId: {
1880
+ type: "string",
1881
+ description: "Page UUID to delete"
1882
+ } },
1883
+ required: ["pageId"]
1884
+ }
1885
+ },
1886
+ {
1887
+ name: "cms_list_page_versions",
1888
+ description: "List version history snapshots for a page.",
1889
+ inputSchema: {
1890
+ type: "object",
1891
+ properties: {
1892
+ pageId: {
1893
+ type: "string",
1894
+ description: "Page UUID"
1895
+ },
1896
+ limit: {
1897
+ type: "number",
1898
+ description: "Max versions to return (default 20)"
1899
+ }
1900
+ },
1901
+ required: ["pageId"]
1902
+ }
1903
+ },
1904
+ {
1905
+ name: "cms_get_page_version",
1906
+ description: "Get full snapshot content and block tree for a specific version.",
1907
+ inputSchema: {
1908
+ type: "object",
1909
+ properties: { versionId: {
1910
+ type: "string",
1911
+ description: "Version UUID"
1912
+ } },
1913
+ required: ["versionId"]
1914
+ }
1915
+ },
1916
+ {
1917
+ name: "cms_rollback_page_version",
1918
+ description: "Rollback a page's working draft or live content to a past version snapshot.",
1919
+ inputSchema: {
1920
+ type: "object",
1921
+ properties: {
1922
+ pageId: {
1923
+ type: "string",
1924
+ description: "Page UUID"
1925
+ },
1926
+ versionId: {
1927
+ type: "string",
1928
+ description: "Target Version UUID to rollback to"
1929
+ },
1930
+ userId: {
1931
+ type: "string",
1932
+ description: "User or Agent ID performing rollback"
1933
+ }
1934
+ },
1935
+ required: [
1936
+ "pageId",
1937
+ "versionId",
1938
+ "userId"
1939
+ ]
1940
+ }
1941
+ },
1942
+ {
1943
+ name: "cms_approve_page_version",
1944
+ description: "Record an editorial review approval for a version.",
1945
+ inputSchema: {
1946
+ type: "object",
1947
+ properties: {
1948
+ versionId: {
1949
+ type: "string",
1950
+ description: "Version UUID"
1951
+ },
1952
+ userId: {
1953
+ type: "string",
1954
+ description: "Approver user or agent ID"
1955
+ },
1956
+ userRole: {
1957
+ type: "string",
1958
+ description: "Approver role, e.g. 'Editor', 'Admin'"
1959
+ }
1960
+ },
1961
+ required: [
1962
+ "versionId",
1963
+ "userId",
1964
+ "userRole"
1965
+ ]
1966
+ }
1967
+ },
1968
+ {
1969
+ name: "cms_add_version_comment",
1970
+ description: "Add an editorial or block-level comment to a version.",
1971
+ inputSchema: {
1972
+ type: "object",
1973
+ properties: {
1974
+ versionId: {
1975
+ type: "string",
1976
+ description: "Version UUID"
1977
+ },
1978
+ userId: {
1979
+ type: "string",
1980
+ description: "Author user or agent ID"
1981
+ },
1982
+ userRole: {
1983
+ type: "string",
1984
+ description: "Author role"
1985
+ },
1986
+ content: {
1987
+ type: "string",
1988
+ description: "Comment body text"
1989
+ },
1990
+ blockId: {
1991
+ type: "string",
1992
+ description: "Optional specific block ID target"
1993
+ }
1994
+ },
1995
+ required: [
1996
+ "versionId",
1997
+ "userId",
1998
+ "userRole",
1999
+ "content"
2000
+ ]
2001
+ }
2002
+ },
2003
+ {
2004
+ name: "cms_list_templates",
2005
+ description: "List all page templates with starter blocks and property schemas.",
2006
+ inputSchema: {
2007
+ type: "object",
2008
+ properties: { pageType: {
2009
+ type: "string",
2010
+ description: "Optional filter by page type"
2011
+ } }
2012
+ }
2013
+ },
2014
+ {
2015
+ name: "cms_get_template",
2016
+ description: "Get template definition by slug or UUID.",
2017
+ inputSchema: {
2018
+ type: "object",
2019
+ properties: { slugOrId: {
2020
+ type: "string",
2021
+ description: "Template slug or UUID"
2022
+ } },
2023
+ required: ["slugOrId"]
2024
+ }
2025
+ },
2026
+ {
2027
+ name: "cms_create_template",
2028
+ description: "Create a new page template with default property groups and starter blocks.",
2029
+ inputSchema: {
2030
+ type: "object",
2031
+ properties: {
2032
+ slug: {
2033
+ type: "string",
2034
+ description: "Unique template slug"
2035
+ },
2036
+ title: {
2037
+ type: "object",
2038
+ description: "Localized title, e.g. { en: 'Wiki Template' }"
2039
+ },
2040
+ description: {
2041
+ type: "object",
2042
+ description: "Localized description"
2043
+ },
2044
+ pageType: {
2045
+ type: "string",
2046
+ description: "Page type target"
2047
+ },
2048
+ version: {
2049
+ type: "number",
2050
+ description: "Template version number (default 1)"
2051
+ },
2052
+ allowedRoles: {
2053
+ type: "array",
2054
+ description: "Roles allowed to use this template"
2055
+ },
2056
+ rolePermissions: {
2057
+ type: "object",
2058
+ description: "Granular template role permissions"
2059
+ },
2060
+ approvalRules: {
2061
+ type: "object",
2062
+ description: "Approval workflow configuration"
2063
+ },
2064
+ defaultProperties: {
2065
+ type: "object",
2066
+ description: "Default property schema groups"
2067
+ },
2068
+ initialBlocks: {
2069
+ type: "array",
2070
+ description: "Starter structured CMS blocks"
2071
+ }
2072
+ },
2073
+ required: [
2074
+ "slug",
2075
+ "title",
2076
+ "pageType"
2077
+ ]
2078
+ }
2079
+ },
2080
+ {
2081
+ name: "cms_update_template",
2082
+ description: "Update an existing page template.",
2083
+ inputSchema: {
2084
+ type: "object",
2085
+ properties: {
2086
+ id: {
2087
+ type: "string",
2088
+ description: "Template UUID"
2089
+ },
2090
+ slug: { type: "string" },
2091
+ title: { type: "object" },
2092
+ description: { type: "object" },
2093
+ pageType: { type: "string" },
2094
+ version: { type: "number" },
2095
+ defaultProperties: { type: "object" },
2096
+ initialBlocks: { type: "array" },
2097
+ rolePermissions: { type: "object" },
2098
+ approvalRules: { type: "object" }
2099
+ },
2100
+ required: ["id"]
2101
+ }
2102
+ },
2103
+ {
2104
+ name: "cms_delete_template",
2105
+ description: "Delete a page template by UUID.",
2106
+ inputSchema: {
2107
+ type: "object",
2108
+ properties: { id: {
2109
+ type: "string",
2110
+ description: "Template UUID"
2111
+ } },
2112
+ required: ["id"]
2113
+ }
2114
+ },
2115
+ {
2116
+ name: "cms_list_taxonomies",
2117
+ description: "List taxonomy terms (categories, tags, genres, etc.) with usage counts.",
2118
+ inputSchema: {
2119
+ type: "object",
2120
+ properties: { taxonomy: {
2121
+ type: "string",
2122
+ description: "Taxonomy type, e.g. 'category', 'tag'"
2123
+ } },
2124
+ required: ["taxonomy"]
2125
+ }
2126
+ },
2127
+ {
2128
+ name: "cms_create_taxonomy_term",
2129
+ description: "Create a new taxonomy term (category or tag).",
2130
+ inputSchema: {
2131
+ type: "object",
2132
+ properties: {
2133
+ taxonomy: {
2134
+ type: "string",
2135
+ description: "'category', 'tag', or custom taxonomy name"
2136
+ },
2137
+ slug: {
2138
+ type: "string",
2139
+ description: "Unique slug within taxonomy"
2140
+ },
2141
+ label: {
2142
+ type: "object",
2143
+ description: "Localized label or string, e.g. { en: 'Guides' }"
2144
+ },
2145
+ description: {
2146
+ type: "object",
2147
+ description: "Localized description"
2148
+ },
2149
+ parentId: {
2150
+ type: "string",
2151
+ description: "Optional parent category UUID for hierarchy"
2152
+ },
2153
+ displayOrder: {
2154
+ type: "number",
2155
+ description: "Display sort order"
2156
+ }
2157
+ },
2158
+ required: [
2159
+ "taxonomy",
2160
+ "slug",
2161
+ "label"
2162
+ ]
2163
+ }
2164
+ },
2165
+ {
2166
+ name: "cms_update_taxonomy_term",
2167
+ description: "Update an existing taxonomy term by UUID.",
2168
+ inputSchema: {
2169
+ type: "object",
2170
+ properties: {
2171
+ id: {
2172
+ type: "string",
2173
+ description: "Taxonomy term UUID"
2174
+ },
2175
+ slug: { type: "string" },
2176
+ label: { type: "object" },
2177
+ description: { type: "object" },
2178
+ parentId: { type: "string" },
2179
+ displayOrder: { type: "number" }
2180
+ },
2181
+ required: ["id"]
2182
+ }
2183
+ },
2184
+ {
2185
+ name: "cms_delete_taxonomy_term",
2186
+ description: "Delete a taxonomy term by UUID.",
2187
+ inputSchema: {
2188
+ type: "object",
2189
+ properties: { id: {
2190
+ type: "string",
2191
+ description: "Taxonomy term UUID"
2192
+ } },
2193
+ required: ["id"]
2194
+ }
2195
+ },
2196
+ {
2197
+ name: "cms_assign_page_taxonomy",
2198
+ description: "Assign or unassign taxonomy terms to/from a page.",
2199
+ inputSchema: {
2200
+ type: "object",
2201
+ properties: {
2202
+ pageId: {
2203
+ type: "string",
2204
+ description: "Page UUID"
2205
+ },
2206
+ termIds: {
2207
+ type: "array",
2208
+ description: "Array of taxonomy term UUIDs to assign to the page"
2209
+ }
2210
+ },
2211
+ required: ["pageId", "termIds"]
2212
+ }
2213
+ },
2214
+ {
2215
+ name: "cms_list_bylines",
2216
+ description: "List all registered author and contributor bylines.",
2217
+ inputSchema: {
2218
+ type: "object",
2219
+ properties: {}
2220
+ }
2221
+ },
2222
+ {
2223
+ name: "cms_get_byline",
2224
+ description: "Get a specific byline profile by slug or UUID.",
2225
+ inputSchema: {
2226
+ type: "object",
2227
+ properties: { slugOrId: {
2228
+ type: "string",
2229
+ description: "Byline slug or UUID"
2230
+ } },
2231
+ required: ["slugOrId"]
2232
+ }
2233
+ },
2234
+ {
2235
+ name: "cms_create_byline",
2236
+ description: "Create an author, creator, or studio byline profile.",
2237
+ inputSchema: {
2238
+ type: "object",
2239
+ properties: {
2240
+ name: {
2241
+ type: "string",
2242
+ description: "Display name"
2243
+ },
2244
+ slug: {
2245
+ type: "string",
2246
+ description: "Unique URL slug"
2247
+ },
2248
+ websiteUrl: {
2249
+ type: "string",
2250
+ description: "Author website URL"
2251
+ },
2252
+ bio: {
2253
+ type: "string",
2254
+ description: "Bio or summary text"
2255
+ },
2256
+ avatar: {
2257
+ type: "string",
2258
+ description: "Avatar image URL or asset key"
2259
+ },
2260
+ userId: {
2261
+ type: "string",
2262
+ description: "Linked user ID if applicable"
2263
+ },
2264
+ socials: {
2265
+ type: "object",
2266
+ description: "Social links, e.g. { twitter: '...', github: '...' }"
2267
+ },
2268
+ metadata: {
2269
+ type: "object",
2270
+ description: "Custom author metadata"
2271
+ }
2272
+ },
2273
+ required: ["name", "slug"]
2274
+ }
2275
+ },
2276
+ {
2277
+ name: "cms_update_byline",
2278
+ description: "Update an existing byline profile.",
2279
+ inputSchema: {
2280
+ type: "object",
2281
+ properties: {
2282
+ id: {
2283
+ type: "string",
2284
+ description: "Byline UUID"
2285
+ },
2286
+ name: { type: "string" },
2287
+ slug: { type: "string" },
2288
+ websiteUrl: { type: "string" },
2289
+ bio: { type: "string" },
2290
+ avatar: { type: "string" },
2291
+ userId: { type: "string" },
2292
+ socials: { type: "object" },
2293
+ metadata: { type: "object" }
2294
+ },
2295
+ required: ["id"]
2296
+ }
2297
+ },
2298
+ {
2299
+ name: "cms_delete_byline",
2300
+ description: "Delete an author byline profile by UUID.",
2301
+ inputSchema: {
2302
+ type: "object",
2303
+ properties: { id: {
2304
+ type: "string",
2305
+ description: "Byline UUID"
2306
+ } },
2307
+ required: ["id"]
2308
+ }
2309
+ },
2310
+ {
2311
+ name: "cms_list_content_types",
2312
+ description: "List all dynamic content types, collections, and e-commerce models.",
2313
+ inputSchema: {
2314
+ type: "object",
2315
+ properties: {}
2316
+ }
2317
+ },
2318
+ {
2319
+ name: "cms_get_content_type",
2320
+ description: "Get dynamic content type definition by slug or UUID.",
2321
+ inputSchema: {
2322
+ type: "object",
2323
+ properties: { slugOrId: {
2324
+ type: "string",
2325
+ description: "Content type slug or UUID"
2326
+ } },
2327
+ required: ["slugOrId"]
2328
+ }
2329
+ },
2330
+ {
2331
+ name: "cms_create_content_type",
2332
+ description: "Create a new dynamic collection, content type, or e-commerce schema.",
2333
+ inputSchema: {
2334
+ type: "object",
2335
+ properties: {
2336
+ slug: {
2337
+ type: "string",
2338
+ description: "Unique slug identifier (e.g. 'products', 'characters')"
2339
+ },
2340
+ name: {
2341
+ type: "object",
2342
+ description: "Localized name, e.g. { en: 'Products' }"
2343
+ },
2344
+ description: {
2345
+ type: "object",
2346
+ description: "Localized description"
2347
+ },
2348
+ icon: {
2349
+ type: "string",
2350
+ description: "Icon name (e.g. 'i-lucide-shopping-bag')"
2351
+ },
2352
+ mode: {
2353
+ type: "string",
2354
+ description: "'document' (blocks), 'data' (structured table), or 'singleton'"
2355
+ },
2356
+ fieldSchema: {
2357
+ type: "object",
2358
+ description: "Dynamic property field schemas and validation rules"
2359
+ },
2360
+ ecomSettings: {
2361
+ type: "object",
2362
+ description: "E-commerce settings { enabled, currencyDefault, trackInventory, hasVariants, allowDigitalDownloads, stripeSync }"
2363
+ },
2364
+ initialBlocks: {
2365
+ type: "array",
2366
+ description: "Starter blocks for document mode"
2367
+ },
2368
+ isSystem: {
2369
+ type: "boolean",
2370
+ description: "Whether this is a protected system schema"
2371
+ }
2372
+ },
2373
+ required: ["slug", "name"]
2374
+ }
2375
+ },
2376
+ {
2377
+ name: "cms_update_content_type",
2378
+ description: "Update an existing dynamic content type schema or e-commerce settings.",
2379
+ inputSchema: {
2380
+ type: "object",
2381
+ properties: {
2382
+ id: {
2383
+ type: "string",
2384
+ description: "Content type UUID"
2385
+ },
2386
+ slug: { type: "string" },
2387
+ name: { type: "object" },
2388
+ description: { type: "object" },
2389
+ icon: { type: "string" },
2390
+ mode: { type: "string" },
2391
+ fieldSchema: { type: "object" },
2392
+ ecomSettings: { type: "object" },
2393
+ initialBlocks: { type: "array" }
2394
+ },
2395
+ required: ["id"]
2396
+ }
2397
+ },
2398
+ {
2399
+ name: "cms_delete_content_type",
2400
+ description: "Delete a custom dynamic content type by UUID.",
2401
+ inputSchema: {
2402
+ type: "object",
2403
+ properties: { id: {
2404
+ type: "string",
2405
+ description: "Content type UUID"
2406
+ } },
2407
+ required: ["id"]
2408
+ }
2409
+ },
2410
+ {
2411
+ name: "cms_get_site_settings",
2412
+ description: "Retrieve current dynamic site settings including branding and SEO defaults.",
2413
+ inputSchema: {
2414
+ type: "object",
2415
+ properties: {}
1561
2416
  }
1562
2417
  },
1563
2418
  {
@@ -1574,6 +2429,28 @@ const CMS_MCP_TOOLS = [
1574
2429
  seo: { type: "object" }
1575
2430
  }
1576
2431
  }
2432
+ },
2433
+ {
2434
+ name: "cms_search_content",
2435
+ description: "Search across active CMS pages by keyword.",
2436
+ inputSchema: {
2437
+ type: "object",
2438
+ properties: {
2439
+ query: {
2440
+ type: "string",
2441
+ description: "Search query text"
2442
+ },
2443
+ type: {
2444
+ type: "string",
2445
+ description: "Optional filter by page type"
2446
+ },
2447
+ limit: {
2448
+ type: "number",
2449
+ description: "Max results to return (default 10)"
2450
+ }
2451
+ },
2452
+ required: ["query"]
2453
+ }
1577
2454
  }
1578
2455
  ];
1579
2456
  /**
@@ -1594,14 +2471,498 @@ async function handleCmsMcpRequest(body, db) {
1594
2471
  if (args.type) conditions.push(eq(pages.type, args.type));
1595
2472
  if (!args.includeDrafts) conditions.push(isNotNull(pages.publishedVersionId));
1596
2473
  const queryLimit = typeof args.limit === "number" ? args.limit : 20;
2474
+ const queryOffset = typeof args.offset === "number" ? args.offset : 0;
1597
2475
  const rows = await db.select({
1598
2476
  id: pages.id,
1599
2477
  slug: pages.slug,
1600
2478
  type: pages.type,
1601
2479
  title: pages.title,
2480
+ description: pages.description,
1602
2481
  postedAt: pages.postedAt,
1603
- publishedVersionId: pages.publishedVersionId
1604
- }).from(pages).where(and(...conditions)).orderBy(desc(pages.postedAt)).limit(queryLimit);
2482
+ publishedVersionId: pages.publishedVersionId,
2483
+ createdAt: pages.createdAt,
2484
+ updatedAt: pages.updatedAt
2485
+ }).from(pages).where(and(...conditions)).orderBy(desc(pages.postedAt), desc(pages.createdAt)).limit(queryLimit).offset(queryOffset);
2486
+ return {
2487
+ jsonrpc: "2.0",
2488
+ id,
2489
+ result: { content: [{
2490
+ type: "text",
2491
+ text: JSON.stringify(rows, null, 2)
2492
+ }] }
2493
+ };
2494
+ }
2495
+ case "cms_get_page": {
2496
+ const condition = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(args.slugOrId) ? eq(pages.id, args.slugOrId) : eq(pages.slug, args.slugOrId);
2497
+ const page = (await db.select().from(pages).where(and(isNull(pages.deletedAt), condition)).limit(1))[0] || null;
2498
+ if (!page) return {
2499
+ jsonrpc: "2.0",
2500
+ id,
2501
+ error: {
2502
+ code: -32004,
2503
+ message: `Page '${args.slugOrId}' not found`
2504
+ }
2505
+ };
2506
+ let draft = null;
2507
+ try {
2508
+ draft = (await db.select().from(pageDrafts).where(eq(pageDrafts.pageId, page.id)).limit(1))[0] || null;
2509
+ } catch {}
2510
+ return {
2511
+ jsonrpc: "2.0",
2512
+ id,
2513
+ result: { content: [{
2514
+ type: "text",
2515
+ text: JSON.stringify({
2516
+ page,
2517
+ draft
2518
+ }, null, 2)
2519
+ }] }
2520
+ };
2521
+ }
2522
+ case "cms_create_page": {
2523
+ const titlePayload = typeof args.title === "string" ? { en: args.title } : args.title;
2524
+ const descPayload = typeof args.description === "string" ? { en: args.description } : args.description || null;
2525
+ const blocksPayload = Array.isArray(args.blocks) ? args.blocks : [];
2526
+ const propertiesPayload = args.properties || {};
2527
+ const newPage = (await db.insert(pages).values({
2528
+ slug: args.slug,
2529
+ type: args.type,
2530
+ templateId: args.templateId || null,
2531
+ title: titlePayload,
2532
+ description: descPayload,
2533
+ tags: args.tags || [],
2534
+ authorIds: args.authorIds || [],
2535
+ bylines: args.bylines || [],
2536
+ content: {
2537
+ blocks: blocksPayload,
2538
+ properties: propertiesPayload
2539
+ }
2540
+ }).returning())[0];
2541
+ if (newPage) try {
2542
+ await db.insert(pageDrafts).values({
2543
+ pageId: newPage.id,
2544
+ updatedBy: args.authorIds?.[0] || "agent",
2545
+ content: {
2546
+ blocks: blocksPayload,
2547
+ properties: propertiesPayload
2548
+ }
2549
+ }).onConflictDoNothing();
2550
+ } catch {}
2551
+ return {
2552
+ jsonrpc: "2.0",
2553
+ id,
2554
+ result: { content: [{
2555
+ type: "text",
2556
+ text: JSON.stringify(newPage, null, 2)
2557
+ }] }
2558
+ };
2559
+ }
2560
+ case "cms_update_page": {
2561
+ const updateData = { updatedAt: /* @__PURE__ */ new Date() };
2562
+ if (args.slug) updateData["slug"] = args.slug;
2563
+ if (args.title) updateData["title"] = typeof args.title === "string" ? { en: args.title } : args.title;
2564
+ if (args.description !== void 0) updateData["description"] = typeof args.description === "string" ? { en: args.description } : args.description;
2565
+ if (args.tags) updateData["tags"] = args.tags;
2566
+ if (args.bylines) updateData["bylines"] = args.bylines;
2567
+ if (args.blocks || args.properties) {
2568
+ const existingContent = (await db.select().from(pages).where(eq(pages.id, args.id)).limit(1))[0]?.content || {
2569
+ blocks: [],
2570
+ properties: {}
2571
+ };
2572
+ updateData["content"] = {
2573
+ blocks: args.blocks || existingContent.blocks || [],
2574
+ properties: args.properties || existingContent.properties || {}
2575
+ };
2576
+ }
2577
+ const updated = await db.update(pages).set(updateData).where(eq(pages.id, args.id)).returning();
2578
+ return {
2579
+ jsonrpc: "2.0",
2580
+ id,
2581
+ result: { content: [{
2582
+ type: "text",
2583
+ text: JSON.stringify(updated[0] || null, null, 2)
2584
+ }] }
2585
+ };
2586
+ }
2587
+ case "cms_create_draft": {
2588
+ const contentPayload = {
2589
+ blocks: args.blocks,
2590
+ properties: args.properties || {}
2591
+ };
2592
+ const draft = await db.insert(pageDrafts).values({
2593
+ pageId: args.pageId,
2594
+ updatedBy: args.userId,
2595
+ content: contentPayload
2596
+ }).onConflictDoUpdate({
2597
+ target: [pageDrafts.pageId],
2598
+ set: {
2599
+ content: contentPayload,
2600
+ updatedBy: args.userId,
2601
+ updatedAt: /* @__PURE__ */ new Date()
2602
+ }
2603
+ }).returning();
2604
+ return {
2605
+ jsonrpc: "2.0",
2606
+ id,
2607
+ result: { content: [{
2608
+ type: "text",
2609
+ text: JSON.stringify(draft[0], null, 2)
2610
+ }] }
2611
+ };
2612
+ }
2613
+ case "cms_publish_page": {
2614
+ const page = (await db.select().from(pages).where(eq(pages.id, args.pageId)).limit(1))[0];
2615
+ if (!page) return {
2616
+ jsonrpc: "2.0",
2617
+ id,
2618
+ error: {
2619
+ code: -32004,
2620
+ message: `Page '${args.pageId}' not found`
2621
+ }
2622
+ };
2623
+ const content = (await db.select().from(pageDrafts).where(eq(pageDrafts.pageId, args.pageId)).limit(1))[0]?.content || page.content;
2624
+ const nextVersionNumber = ((await db.select().from(pageVersions).where(eq(pageVersions.pageId, args.pageId)).orderBy(desc(pageVersions.versionNumber)).limit(1))[0]?.versionNumber || 0) + 1;
2625
+ const version = (await db.insert(pageVersions).values({
2626
+ pageId: args.pageId,
2627
+ versionNumber: nextVersionNumber,
2628
+ status: "published",
2629
+ slug: page.slug,
2630
+ type: page.type,
2631
+ title: page.title,
2632
+ description: page.description,
2633
+ tags: page.tags,
2634
+ authorIds: page.authorIds,
2635
+ bylines: page.bylines,
2636
+ content,
2637
+ createdBy: args.userId,
2638
+ approvedBy: [args.userId],
2639
+ approvedAt: /* @__PURE__ */ new Date(),
2640
+ changeSummary: args.changeSummary || "Published via MCP",
2641
+ createdAt: /* @__PURE__ */ new Date()
2642
+ }).returning())[0];
2643
+ await db.update(pages).set({
2644
+ content,
2645
+ publishedVersionId: version.id,
2646
+ postedAt: /* @__PURE__ */ new Date(),
2647
+ updatedAt: /* @__PURE__ */ new Date()
2648
+ }).where(eq(pages.id, args.pageId));
2649
+ await indexPageForSearch(db, {
2650
+ id: args.pageId,
2651
+ title: page.title,
2652
+ content
2653
+ }, ["en", "pt"]);
2654
+ return {
2655
+ jsonrpc: "2.0",
2656
+ id,
2657
+ result: { content: [{
2658
+ type: "text",
2659
+ text: JSON.stringify(version, null, 2)
2660
+ }] }
2661
+ };
2662
+ }
2663
+ case "cms_unpublish_page": {
2664
+ const updated = await db.update(pages).set({
2665
+ postedAt: null,
2666
+ publishedVersionId: null,
2667
+ updatedAt: /* @__PURE__ */ new Date()
2668
+ }).where(eq(pages.id, args.pageId)).returning();
2669
+ return {
2670
+ jsonrpc: "2.0",
2671
+ id,
2672
+ result: { content: [{
2673
+ type: "text",
2674
+ text: JSON.stringify({
2675
+ success: true,
2676
+ page: updated[0] || null
2677
+ }, null, 2)
2678
+ }] }
2679
+ };
2680
+ }
2681
+ case "cms_delete_page": {
2682
+ const deleted = await db.update(pages).set({ deletedAt: /* @__PURE__ */ new Date() }).where(eq(pages.id, args.pageId)).returning();
2683
+ return {
2684
+ jsonrpc: "2.0",
2685
+ id,
2686
+ result: { content: [{
2687
+ type: "text",
2688
+ text: JSON.stringify({
2689
+ success: true,
2690
+ page: deleted[0] || null
2691
+ }, null, 2)
2692
+ }] }
2693
+ };
2694
+ }
2695
+ case "cms_list_page_versions": {
2696
+ const vLimit = typeof args.limit === "number" ? args.limit : 20;
2697
+ const rows = await db.select().from(pageVersions).where(eq(pageVersions.pageId, args.pageId)).orderBy(desc(pageVersions.versionNumber)).limit(vLimit);
2698
+ return {
2699
+ jsonrpc: "2.0",
2700
+ id,
2701
+ result: { content: [{
2702
+ type: "text",
2703
+ text: JSON.stringify(rows, null, 2)
2704
+ }] }
2705
+ };
2706
+ }
2707
+ case "cms_get_page_version": {
2708
+ const row = await db.select().from(pageVersions).where(eq(pageVersions.id, args.versionId)).limit(1);
2709
+ return {
2710
+ jsonrpc: "2.0",
2711
+ id,
2712
+ result: { content: [{
2713
+ type: "text",
2714
+ text: JSON.stringify(row[0] || null, null, 2)
2715
+ }] }
2716
+ };
2717
+ }
2718
+ case "cms_rollback_page_version": {
2719
+ const targetVersion = (await db.select().from(pageVersions).where(eq(pageVersions.id, args.versionId)).limit(1))[0];
2720
+ if (!targetVersion) return {
2721
+ jsonrpc: "2.0",
2722
+ id,
2723
+ error: {
2724
+ code: -32004,
2725
+ message: "Version snapshot not found"
2726
+ }
2727
+ };
2728
+ const draft = await db.insert(pageDrafts).values({
2729
+ pageId: args.pageId,
2730
+ updatedBy: args.userId,
2731
+ content: targetVersion.content
2732
+ }).onConflictDoUpdate({
2733
+ target: [pageDrafts.pageId],
2734
+ set: {
2735
+ content: targetVersion.content,
2736
+ updatedBy: args.userId,
2737
+ updatedAt: /* @__PURE__ */ new Date()
2738
+ }
2739
+ }).returning();
2740
+ return {
2741
+ jsonrpc: "2.0",
2742
+ id,
2743
+ result: { content: [{
2744
+ type: "text",
2745
+ text: JSON.stringify({
2746
+ success: true,
2747
+ rolledBackToVersion: targetVersion.versionNumber,
2748
+ draft: draft[0]
2749
+ }, null, 2)
2750
+ }] }
2751
+ };
2752
+ }
2753
+ case "cms_approve_page_version": {
2754
+ const approval = await db.insert(pageVersionApprovals).values({
2755
+ versionId: args.versionId,
2756
+ userId: args.userId,
2757
+ userRole: args.userRole,
2758
+ approvedAt: /* @__PURE__ */ new Date()
2759
+ }).returning();
2760
+ return {
2761
+ jsonrpc: "2.0",
2762
+ id,
2763
+ result: { content: [{
2764
+ type: "text",
2765
+ text: JSON.stringify(approval[0], null, 2)
2766
+ }] }
2767
+ };
2768
+ }
2769
+ case "cms_add_version_comment": {
2770
+ const comment = await db.insert(pageVersionComments).values({
2771
+ versionId: args.versionId,
2772
+ userId: args.userId,
2773
+ userRole: args.userRole,
2774
+ content: args.content,
2775
+ blockId: args.blockId || null,
2776
+ createdAt: /* @__PURE__ */ new Date()
2777
+ }).returning();
2778
+ return {
2779
+ jsonrpc: "2.0",
2780
+ id,
2781
+ result: { content: [{
2782
+ type: "text",
2783
+ text: JSON.stringify(comment[0], null, 2)
2784
+ }] }
2785
+ };
2786
+ }
2787
+ case "cms_list_templates": {
2788
+ const conditions = [];
2789
+ if (args.pageType) conditions.push(eq(pageTemplates.pageType, args.pageType));
2790
+ const rows = await db.select().from(pageTemplates).where(conditions.length > 0 ? and(...conditions) : void 0).orderBy(desc(pageTemplates.createdAt));
2791
+ return {
2792
+ jsonrpc: "2.0",
2793
+ id,
2794
+ result: { content: [{
2795
+ type: "text",
2796
+ text: JSON.stringify(rows, null, 2)
2797
+ }] }
2798
+ };
2799
+ }
2800
+ case "cms_get_template": {
2801
+ const condition = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(args.slugOrId) ? eq(pageTemplates.id, args.slugOrId) : eq(pageTemplates.slug, args.slugOrId);
2802
+ const row = await db.select().from(pageTemplates).where(condition).limit(1);
2803
+ return {
2804
+ jsonrpc: "2.0",
2805
+ id,
2806
+ result: { content: [{
2807
+ type: "text",
2808
+ text: JSON.stringify(row[0] || null, null, 2)
2809
+ }] }
2810
+ };
2811
+ }
2812
+ case "cms_create_template": {
2813
+ const titlePayload = typeof args.title === "string" ? { en: args.title } : args.title;
2814
+ const descPayload = typeof args.description === "string" ? { en: args.description } : args.description || null;
2815
+ const tmpl = await db.insert(pageTemplates).values({
2816
+ slug: args.slug,
2817
+ title: titlePayload,
2818
+ description: descPayload,
2819
+ pageType: args.pageType,
2820
+ version: args.version || 1,
2821
+ allowedRoles: args.allowedRoles || [],
2822
+ rolePermissions: args.rolePermissions || {
2823
+ whoCanCreate: [],
2824
+ whoCanEdit: [],
2825
+ whoCanReview: [],
2826
+ whoCanView: []
2827
+ },
2828
+ approvalRules: args.approvalRules || {
2829
+ allowSelfApproval: true,
2830
+ minApprovals: 1
2831
+ },
2832
+ defaultProperties: args.defaultProperties || {},
2833
+ initialBlocks: args.initialBlocks || []
2834
+ }).returning();
2835
+ return {
2836
+ jsonrpc: "2.0",
2837
+ id,
2838
+ result: { content: [{
2839
+ type: "text",
2840
+ text: JSON.stringify(tmpl[0], null, 2)
2841
+ }] }
2842
+ };
2843
+ }
2844
+ case "cms_update_template": {
2845
+ const updateData = { updatedAt: /* @__PURE__ */ new Date() };
2846
+ if (args.slug) updateData["slug"] = args.slug;
2847
+ if (args.title) updateData["title"] = typeof args.title === "string" ? { en: args.title } : args.title;
2848
+ if (args.description !== void 0) updateData["description"] = typeof args.description === "string" ? { en: args.description } : args.description;
2849
+ if (args.pageType) updateData["pageType"] = args.pageType;
2850
+ if (args.version) updateData["version"] = args.version;
2851
+ if (args.defaultProperties) updateData["defaultProperties"] = args.defaultProperties;
2852
+ if (args.initialBlocks) updateData["initialBlocks"] = args.initialBlocks;
2853
+ if (args.rolePermissions) updateData["rolePermissions"] = args.rolePermissions;
2854
+ if (args.approvalRules) updateData["approvalRules"] = args.approvalRules;
2855
+ const updated = await db.update(pageTemplates).set(updateData).where(eq(pageTemplates.id, args.id)).returning();
2856
+ return {
2857
+ jsonrpc: "2.0",
2858
+ id,
2859
+ result: { content: [{
2860
+ type: "text",
2861
+ text: JSON.stringify(updated[0] || null, null, 2)
2862
+ }] }
2863
+ };
2864
+ }
2865
+ case "cms_delete_template": {
2866
+ const deleted = await db.delete(pageTemplates).where(eq(pageTemplates.id, args.id)).returning();
2867
+ return {
2868
+ jsonrpc: "2.0",
2869
+ id,
2870
+ result: { content: [{
2871
+ type: "text",
2872
+ text: JSON.stringify({
2873
+ success: true,
2874
+ template: deleted[0] || null
2875
+ }, null, 2)
2876
+ }] }
2877
+ };
2878
+ }
2879
+ case "cms_list_taxonomies": {
2880
+ const terms = await getTaxonomyTerms(db, args.taxonomy);
2881
+ return {
2882
+ jsonrpc: "2.0",
2883
+ id,
2884
+ result: { content: [{
2885
+ type: "text",
2886
+ text: JSON.stringify(terms, null, 2)
2887
+ }] }
2888
+ };
2889
+ }
2890
+ case "cms_create_taxonomy_term": {
2891
+ const labelPayload = typeof args.label === "string" ? { en: args.label } : args.label;
2892
+ const descPayload = typeof args.description === "string" ? { en: args.description } : args.description || null;
2893
+ const term = await db.insert(taxonomyTerms).values({
2894
+ taxonomy: args.taxonomy,
2895
+ slug: args.slug,
2896
+ label: labelPayload,
2897
+ description: descPayload,
2898
+ parentId: args.parentId || null,
2899
+ displayOrder: args.displayOrder || 0
2900
+ }).returning();
2901
+ return {
2902
+ jsonrpc: "2.0",
2903
+ id,
2904
+ result: { content: [{
2905
+ type: "text",
2906
+ text: JSON.stringify(term[0], null, 2)
2907
+ }] }
2908
+ };
2909
+ }
2910
+ case "cms_update_taxonomy_term": {
2911
+ const updateData = { updatedAt: /* @__PURE__ */ new Date() };
2912
+ if (args.slug) updateData["slug"] = args.slug;
2913
+ if (args.label) updateData["label"] = typeof args.label === "string" ? { en: args.label } : args.label;
2914
+ if (args.description !== void 0) updateData["description"] = typeof args.description === "string" ? { en: args.description } : args.description;
2915
+ if (args.parentId !== void 0) updateData["parentId"] = args.parentId || null;
2916
+ if (args.displayOrder !== void 0) updateData["displayOrder"] = args.displayOrder;
2917
+ const updated = await db.update(taxonomyTerms).set(updateData).where(eq(taxonomyTerms.id, args.id)).returning();
2918
+ return {
2919
+ jsonrpc: "2.0",
2920
+ id,
2921
+ result: { content: [{
2922
+ type: "text",
2923
+ text: JSON.stringify(updated[0] || null, null, 2)
2924
+ }] }
2925
+ };
2926
+ }
2927
+ case "cms_delete_taxonomy_term": {
2928
+ const deleted = await db.delete(taxonomyTerms).where(eq(taxonomyTerms.id, args.id)).returning();
2929
+ return {
2930
+ jsonrpc: "2.0",
2931
+ id,
2932
+ result: { content: [{
2933
+ type: "text",
2934
+ text: JSON.stringify({
2935
+ success: true,
2936
+ term: deleted[0] || null
2937
+ }, null, 2)
2938
+ }] }
2939
+ };
2940
+ }
2941
+ case "cms_assign_page_taxonomy": {
2942
+ await db.delete(pageTaxonomyTerms).where(eq(pageTaxonomyTerms.pageId, args.pageId));
2943
+ const termIds = Array.isArray(args.termIds) ? args.termIds : [];
2944
+ const assigned = [];
2945
+ for (const termId of termIds) {
2946
+ const row = await db.insert(pageTaxonomyTerms).values({
2947
+ pageId: args.pageId,
2948
+ termId
2949
+ }).returning();
2950
+ assigned.push(row[0]);
2951
+ }
2952
+ return {
2953
+ jsonrpc: "2.0",
2954
+ id,
2955
+ result: { content: [{
2956
+ type: "text",
2957
+ text: JSON.stringify({
2958
+ success: true,
2959
+ assignedCount: assigned.length
2960
+ }, null, 2)
2961
+ }] }
2962
+ };
2963
+ }
2964
+ case "cms_list_bylines": {
2965
+ const rows = await db.select().from(bylines).where(isNull(bylines.deletedAt)).orderBy(desc(bylines.createdAt));
1605
2966
  return {
1606
2967
  jsonrpc: "2.0",
1607
2968
  id,
@@ -1611,40 +2972,149 @@ async function handleCmsMcpRequest(body, db) {
1611
2972
  }] }
1612
2973
  };
1613
2974
  }
1614
- case "cms_get_page": {
1615
- const page = (await db.select().from(pages).where(and(isNull(pages.deletedAt), eq(pages.slug, args.slugOrId))).limit(1))[0] || null;
2975
+ case "cms_get_byline": {
2976
+ const condition = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(args.slugOrId) ? eq(bylines.id, args.slugOrId) : eq(bylines.slug, args.slugOrId);
2977
+ const row = await db.select().from(bylines).where(and(isNull(bylines.deletedAt), condition)).limit(1);
1616
2978
  return {
1617
2979
  jsonrpc: "2.0",
1618
2980
  id,
1619
2981
  result: { content: [{
1620
2982
  type: "text",
1621
- text: JSON.stringify(page, null, 2)
2983
+ text: JSON.stringify(row[0] || null, null, 2)
1622
2984
  }] }
1623
2985
  };
1624
2986
  }
1625
- case "cms_create_draft": {
1626
- const contentPayload = {
1627
- blocks: args.blocks,
1628
- properties: args.properties || {}
2987
+ case "cms_create_byline": {
2988
+ const row = await db.insert(bylines).values({
2989
+ name: args.name,
2990
+ slug: args.slug,
2991
+ websiteUrl: args.websiteUrl || null,
2992
+ bio: args.bio || null,
2993
+ avatar: args.avatar || null,
2994
+ userId: args.userId || null,
2995
+ socials: args.socials || {},
2996
+ metadata: args.metadata || {}
2997
+ }).returning();
2998
+ return {
2999
+ jsonrpc: "2.0",
3000
+ id,
3001
+ result: { content: [{
3002
+ type: "text",
3003
+ text: JSON.stringify(row[0], null, 2)
3004
+ }] }
1629
3005
  };
1630
- const draft = await db.insert(pageDrafts).values({
1631
- pageId: args.pageId,
1632
- updatedBy: args.userId,
1633
- content: contentPayload
1634
- }).onConflictDoUpdate({
1635
- target: [pageDrafts.pageId],
1636
- set: {
1637
- content: contentPayload,
1638
- updatedBy: args.userId,
1639
- updatedAt: /* @__PURE__ */ new Date()
1640
- }
3006
+ }
3007
+ case "cms_update_byline": {
3008
+ const updateData = { updatedAt: /* @__PURE__ */ new Date() };
3009
+ if (args.name) updateData["name"] = args.name;
3010
+ if (args.slug) updateData["slug"] = args.slug;
3011
+ if (args.websiteUrl !== void 0) updateData["websiteUrl"] = args.websiteUrl;
3012
+ if (args.bio !== void 0) updateData["bio"] = args.bio;
3013
+ if (args.avatar !== void 0) updateData["avatar"] = args.avatar;
3014
+ if (args.userId !== void 0) updateData["userId"] = args.userId;
3015
+ if (args.socials) updateData["socials"] = args.socials;
3016
+ if (args.metadata) updateData["metadata"] = args.metadata;
3017
+ const updated = await db.update(bylines).set(updateData).where(eq(bylines.id, args.id)).returning();
3018
+ return {
3019
+ jsonrpc: "2.0",
3020
+ id,
3021
+ result: { content: [{
3022
+ type: "text",
3023
+ text: JSON.stringify(updated[0] || null, null, 2)
3024
+ }] }
3025
+ };
3026
+ }
3027
+ case "cms_delete_byline": {
3028
+ const deleted = await db.update(bylines).set({ deletedAt: /* @__PURE__ */ new Date() }).where(eq(bylines.id, args.id)).returning();
3029
+ return {
3030
+ jsonrpc: "2.0",
3031
+ id,
3032
+ result: { content: [{
3033
+ type: "text",
3034
+ text: JSON.stringify({
3035
+ success: true,
3036
+ byline: deleted[0] || null
3037
+ }, null, 2)
3038
+ }] }
3039
+ };
3040
+ }
3041
+ case "cms_list_content_types": {
3042
+ const rows = await db.select().from(contentTypes).orderBy(desc(contentTypes.createdAt));
3043
+ return {
3044
+ jsonrpc: "2.0",
3045
+ id,
3046
+ result: { content: [{
3047
+ type: "text",
3048
+ text: JSON.stringify(rows, null, 2)
3049
+ }] }
3050
+ };
3051
+ }
3052
+ case "cms_get_content_type": {
3053
+ const condition = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(args.slugOrId) ? eq(contentTypes.id, args.slugOrId) : eq(contentTypes.slug, args.slugOrId);
3054
+ const row = await db.select().from(contentTypes).where(condition).limit(1);
3055
+ return {
3056
+ jsonrpc: "2.0",
3057
+ id,
3058
+ result: { content: [{
3059
+ type: "text",
3060
+ text: JSON.stringify(row[0] || null, null, 2)
3061
+ }] }
3062
+ };
3063
+ }
3064
+ case "cms_create_content_type": {
3065
+ const namePayload = typeof args.name === "string" ? { en: args.name } : args.name;
3066
+ const descPayload = typeof args.description === "string" ? { en: args.description } : args.description || null;
3067
+ const row = await db.insert(contentTypes).values({
3068
+ slug: args.slug,
3069
+ name: namePayload,
3070
+ description: descPayload,
3071
+ icon: args.icon || "i-lucide-box",
3072
+ mode: args.mode || "document",
3073
+ fieldSchema: args.fieldSchema || {},
3074
+ ecomSettings: args.ecomSettings || null,
3075
+ initialBlocks: args.initialBlocks || [],
3076
+ isSystem: Boolean(args.isSystem)
1641
3077
  }).returning();
1642
3078
  return {
1643
3079
  jsonrpc: "2.0",
1644
3080
  id,
1645
3081
  result: { content: [{
1646
3082
  type: "text",
1647
- text: JSON.stringify(draft[0], null, 2)
3083
+ text: JSON.stringify(row[0], null, 2)
3084
+ }] }
3085
+ };
3086
+ }
3087
+ case "cms_update_content_type": {
3088
+ const updateData = { updatedAt: /* @__PURE__ */ new Date() };
3089
+ if (args.slug) updateData["slug"] = args.slug;
3090
+ if (args.name) updateData["name"] = typeof args.name === "string" ? { en: args.name } : args.name;
3091
+ if (args.description !== void 0) updateData["description"] = typeof args.description === "string" ? { en: args.description } : args.description;
3092
+ if (args.icon) updateData["icon"] = args.icon;
3093
+ if (args.mode) updateData["mode"] = args.mode;
3094
+ if (args.fieldSchema) updateData["fieldSchema"] = args.fieldSchema;
3095
+ if (args.ecomSettings !== void 0) updateData["ecomSettings"] = args.ecomSettings;
3096
+ if (args.initialBlocks) updateData["initialBlocks"] = args.initialBlocks;
3097
+ const updated = await db.update(contentTypes).set(updateData).where(eq(contentTypes.id, args.id)).returning();
3098
+ return {
3099
+ jsonrpc: "2.0",
3100
+ id,
3101
+ result: { content: [{
3102
+ type: "text",
3103
+ text: JSON.stringify(updated[0] || null, null, 2)
3104
+ }] }
3105
+ };
3106
+ }
3107
+ case "cms_delete_content_type": {
3108
+ const deleted = await db.delete(contentTypes).where(eq(contentTypes.id, args.id)).returning();
3109
+ return {
3110
+ jsonrpc: "2.0",
3111
+ id,
3112
+ result: { content: [{
3113
+ type: "text",
3114
+ text: JSON.stringify({
3115
+ success: true,
3116
+ contentType: deleted[0] || null
3117
+ }, null, 2)
1648
3118
  }] }
1649
3119
  };
1650
3120
  }
@@ -1670,6 +3140,27 @@ async function handleCmsMcpRequest(body, db) {
1670
3140
  }] }
1671
3141
  };
1672
3142
  }
3143
+ case "cms_search_content": {
3144
+ const queryLimit = typeof args.limit === "number" ? args.limit : 10;
3145
+ const conditions = [isNull(pages.deletedAt)];
3146
+ if (args.type) conditions.push(eq(pages.type, args.type));
3147
+ const rows = await db.select({
3148
+ id: pages.id,
3149
+ slug: pages.slug,
3150
+ type: pages.type,
3151
+ title: pages.title,
3152
+ description: pages.description,
3153
+ postedAt: pages.postedAt
3154
+ }).from(pages).where(and(...conditions, or(like(pages.slug, `%${args.query}%`), sql`CAST(${pages.title} AS TEXT) ILIKE ${`%${args.query}%`}`))).limit(queryLimit);
3155
+ return {
3156
+ jsonrpc: "2.0",
3157
+ id,
3158
+ result: { content: [{
3159
+ type: "text",
3160
+ text: JSON.stringify(rows, null, 2)
3161
+ }] }
3162
+ };
3163
+ }
1673
3164
  default: return {
1674
3165
  jsonrpc: "2.0",
1675
3166
  id,
@@ -1898,181 +3389,6 @@ function diffPageSnapshots(oldSnapshot, newSnapshot) {
1898
3389
  };
1899
3390
  }
1900
3391
  //#endregion
1901
- //#region src/services/search-indexer.ts
1902
- async function indexPageForSearch(db, page, locales = ["en", "pt"]) {
1903
- if (!db) return;
1904
- const parseJson = (val) => typeof val === "string" ? JSON.parse(val) : val;
1905
- const titleObj = parseJson(page.title) || {};
1906
- const blocks = (parseJson(page.content) || {}).blocks || [];
1907
- for (const locale of locales) {
1908
- const titleText = typeof titleObj === "object" && titleObj !== null ? titleObj[locale] || titleObj.en || Object.values(titleObj)[0] || "" : String(titleObj || "");
1909
- const contentText = extractTextFromBlocks(blocks, locale);
1910
- if (!titleText && !contentText) continue;
1911
- try {
1912
- const existing = await db.select().from(contentSearchIndex).where(and(eq(contentSearchIndex.pageId, page.id), eq(contentSearchIndex.locale, locale))).limit(1);
1913
- if (existing.length > 0) await db.update(contentSearchIndex).set({
1914
- titleText,
1915
- contentText,
1916
- searchVector: sql`to_tsvector('english', ${titleText} || ' ' || ${contentText})`
1917
- }).where(eq(contentSearchIndex.id, existing[0].id));
1918
- else await db.insert(contentSearchIndex).values({
1919
- pageId: page.id,
1920
- locale,
1921
- titleText,
1922
- contentText,
1923
- searchVector: sql`to_tsvector('english', ${titleText} || ' ' || ${contentText})`
1924
- });
1925
- } catch (err) {
1926
- console.error(`[Search Indexer] Failed to index page ${page.id} for locale ${locale}:`, err);
1927
- }
1928
- }
1929
- }
1930
- //#endregion
1931
- //#region src/services/taxonomies.ts
1932
- /**
1933
- * Retrieves all terms for a given taxonomy. If hierarchical, returns a tree structure with
1934
- * `children: TaxonomyTerm[]`.
1935
- */
1936
- async function getTaxonomyTerms(db, taxonomy, options = {}) {
1937
- if (!db) return [];
1938
- const includeCounts = options.includeCounts ?? true;
1939
- const rawTerms = await db.select().from(taxonomyTerms).where(eq(taxonomyTerms.taxonomy, taxonomy)).orderBy(asc(taxonomyTerms.displayOrder), asc(taxonomyTerms.createdAt));
1940
- if (!rawTerms || rawTerms.length === 0) return [];
1941
- const countsMap = /* @__PURE__ */ new Map();
1942
- if (includeCounts) try {
1943
- const counts = await db.select({
1944
- termId: pageTaxonomyTerms.termId,
1945
- count: sql`count(${pageTaxonomyTerms.pageId})::int`
1946
- }).from(pageTaxonomyTerms).innerJoin(pages, eq(pages.id, pageTaxonomyTerms.pageId)).where(isNull(pages.deletedAt)).groupBy(pageTaxonomyTerms.termId);
1947
- for (const row of counts) countsMap.set(row.termId, Number(row.count));
1948
- } catch {}
1949
- const termsWithCounts = rawTerms.map((t) => ({
1950
- id: t.id,
1951
- taxonomy: t.taxonomy,
1952
- slug: t.slug,
1953
- label: t.label,
1954
- description: t.description,
1955
- parentId: t.parentId,
1956
- displayOrder: t.displayOrder,
1957
- count: countsMap.get(t.id) ?? 0,
1958
- children: [],
1959
- createdAt: t.createdAt,
1960
- updatedAt: t.updatedAt
1961
- }));
1962
- if (!termsWithCounts.some((t) => !!t.parentId)) return termsWithCounts;
1963
- const idMap = /* @__PURE__ */ new Map();
1964
- const rootTerms = [];
1965
- termsWithCounts.forEach((term) => {
1966
- idMap.set(term.id, term);
1967
- });
1968
- termsWithCounts.forEach((term) => {
1969
- if (term.parentId && idMap.has(term.parentId)) {
1970
- const parent = idMap.get(term.parentId);
1971
- parent.children = parent.children || [];
1972
- parent.children.push(term);
1973
- } else rootTerms.push(term);
1974
- });
1975
- return rootTerms;
1976
- }
1977
- /**
1978
- * Retrieves a single term by taxonomy and slug.
1979
- */
1980
- async function getTerm(db, taxonomy, slug) {
1981
- if (!db) return null;
1982
- const results = await db.select().from(taxonomyTerms).where(and(eq(taxonomyTerms.taxonomy, taxonomy), eq(taxonomyTerms.slug, slug))).limit(1);
1983
- if (!results[0]) return null;
1984
- let count = 0;
1985
- try {
1986
- const countResult = await db.select({ count: sql`count(${pageTaxonomyTerms.pageId})::int` }).from(pageTaxonomyTerms).innerJoin(pages, eq(pages.id, pageTaxonomyTerms.pageId)).where(and(eq(pageTaxonomyTerms.termId, results[0].id), isNull(pages.deletedAt)));
1987
- count = Number(countResult[0]?.count ?? 0);
1988
- } catch {
1989
- count = 0;
1990
- }
1991
- return {
1992
- ...results[0],
1993
- count,
1994
- children: []
1995
- };
1996
- }
1997
- /**
1998
- * Retrieves all terms assigned to a specific page entry.
1999
- */
2000
- async function getEntryTerms(db, pageId, taxonomy) {
2001
- if (!db || !pageId) return [];
2002
- const conditions = [eq(pageTaxonomyTerms.pageId, pageId)];
2003
- if (taxonomy) conditions.push(eq(taxonomyTerms.taxonomy, taxonomy));
2004
- return await db.select({
2005
- id: taxonomyTerms.id,
2006
- taxonomy: taxonomyTerms.taxonomy,
2007
- slug: taxonomyTerms.slug,
2008
- label: taxonomyTerms.label,
2009
- description: taxonomyTerms.description,
2010
- parentId: taxonomyTerms.parentId,
2011
- displayOrder: taxonomyTerms.displayOrder
2012
- }).from(pageTaxonomyTerms).innerJoin(taxonomyTerms, eq(taxonomyTerms.id, pageTaxonomyTerms.termId)).where(and(...conditions)).orderBy(asc(taxonomyTerms.displayOrder));
2013
- }
2014
- /**
2015
- * Retrieves pages tagged with a given taxonomy term.
2016
- */
2017
- async function getEntriesByTerm(db, taxonomy, slug, options = {}) {
2018
- if (!db) return {
2019
- entries: [],
2020
- total: 0
2021
- };
2022
- const term = await getTerm(db, taxonomy, slug);
2023
- if (!term) return {
2024
- entries: [],
2025
- total: 0
2026
- };
2027
- const conditions = [eq(pageTaxonomyTerms.termId, term.id), isNull(pages.deletedAt)];
2028
- if (options.type) conditions.push(eq(pages.type, options.type));
2029
- let query = db.select({ page: pages }).from(pageTaxonomyTerms).innerJoin(pages, eq(pages.id, pageTaxonomyTerms.pageId)).where(and(...conditions)).orderBy(asc(pages.createdAt));
2030
- if (options.limit) query = query.limit(options.limit);
2031
- if (options.offset) query = query.offset(options.offset);
2032
- const entries = (await query).map((r) => r.page);
2033
- return {
2034
- entries,
2035
- total: term.count ?? entries.length,
2036
- term
2037
- };
2038
- }
2039
- /**
2040
- * Sets/syncs the assigned terms for a page within a specific taxonomy.
2041
- */
2042
- async function assignEntryTerms(db, pageId, taxonomy, termIds) {
2043
- if (!db || !pageId) return;
2044
- const currentAssigned = await getEntryTerms(db, pageId, taxonomy);
2045
- const currentTermIds = new Set(currentAssigned.map((t) => t.id));
2046
- const newTermIds = new Set(termIds);
2047
- for (const term of currentAssigned) if (!newTermIds.has(term.id)) await db.delete(pageTaxonomyTerms).where(and(eq(pageTaxonomyTerms.pageId, pageId), eq(pageTaxonomyTerms.termId, term.id)));
2048
- for (const termId of termIds) if (!currentTermIds.has(termId)) await db.insert(pageTaxonomyTerms).values({
2049
- pageId,
2050
- termId
2051
- }).onConflictDoNothing();
2052
- }
2053
- /**
2054
- * Creates a new taxonomy term.
2055
- */
2056
- async function createTerm(db, data) {
2057
- return (await db.insert(taxonomyTerms).values(data).returning())[0];
2058
- }
2059
- /**
2060
- * Updates an existing taxonomy term.
2061
- */
2062
- async function updateTerm(db, id, updates) {
2063
- return (await db.update(taxonomyTerms).set({
2064
- ...updates,
2065
- updatedAt: /* @__PURE__ */ new Date()
2066
- }).where(eq(taxonomyTerms.id, id)).returning())[0];
2067
- }
2068
- /**
2069
- * Deletes a taxonomy term.
2070
- */
2071
- async function deleteTerm(db, id) {
2072
- await db.delete(taxonomyTerms).where(eq(taxonomyTerms.id, id));
2073
- return { success: true };
2074
- }
2075
- //#endregion
2076
3392
  //#region src/docs/config.ts
2077
3393
  const defineDocsConfig = (config) => config;
2078
3394
  //#endregion
@@ -2379,4 +3695,4 @@ async function renderCorpusMarkdown(db, options = {}) {
2379
3695
  return parts.join("\n");
2380
3696
  }
2381
3697
  //#endregion
2382
- export { ALLOWED_CHILDREN_MAP, BLOG_POST_DEFINITION, BlockRegistry, CARD_DEFINITION, CHARACTER_DEFINITION, CMS_MCP_TOOLS, DEFAULT_SITE_SETTINGS, DOCUMENT_DEFINITION, GROUP_DEFINITION, HERO_DEFINITION, ITEM_DEFINITION, LOCATION_DEFINITION, MAX_SECTION_DEPTH, MAX_SECTION_HEADING_LEVEL, MIN_SECTION_HEADING_LEVEL, OBJECT_DEFINITION, PAGE_MAP, PATCH_NOTE_DEFINITION, POSTGRES_SEARCH_CONFIGS, SERIES_DEFINITION, SKILL_DEFINITION, SPECIES_DEFINITION, addToast, assignEntryTerms, auth0Auth, blocksToMarkdown, bylines, calculateHeadingLevel, canUserCreateFromTemplate, canUserEditBlock, canUserEditFromTemplate, canUserInstantiateTemplate, canUserReviewFromTemplate, canUserViewFromTemplate, cfAccessAuth, clearToasts, cmsAc, cmsStatements, contentSearchIndex, createByline, createPageExcerpt, createTerm, defineDocsConfig, definePageDefinition, deleteByline, deleteR2File, deleteTerm, diffBlocks, diffPageSnapshots, evaluateAccess, extractHeadingsFromBlocks, extractTextFromBlocks, filterBlocksForReader, filterTemplatesForUser, formatDocsUrl, generatePreviewToken, getByline, getBylines, getCmsCollection, getCmsEntry, getDocsSidebar, getDocsVersions, getEntriesByTerm, getEntryTerms, getPageDefinition, getPostgresSearchConfig, getR2Bucket, getSiteSettings, getSurroundItems, getTaxonomyTerms, getTerm, handleCmsCron, handleCmsMcpRequest, hasLockedDescendants, hasPermission, hasRole, indexPageForSearch, inlinesToMarkdown, invalidatePageCache, isValidUUID, listR2Files, mockAuth, pageDraftLocks, pageDrafts, pageTaxonomyTerms, pageTemplates, pageVersionApprovals, pageVersionComments, pageVersions, pages, parseDocsSlug, r2, removeToast, renderCorpusMarkdown, renderPageAsMarkdown, rimelightCms, rimelightCmsLoader, showErrorToast, showSuccessToast, siteSettings, slugifyHeading, taxonomyTerms, toast, toasts, updateByline, updateSiteSettings, updateTerm, uploadR2File, validateBlockAST, validateServerBlockPayload, verifyPreviewToken };
3698
+ export { ALLOWED_CHILDREN_MAP, BLOG_POST_DEFINITION, BlockRegistry, CARD_DEFINITION, CHARACTER_DEFINITION, CMS_MCP_TOOLS, DEFAULT_SITE_SETTINGS, DOCUMENT_DEFINITION, GROUP_DEFINITION, HERO_DEFINITION, ITEM_DEFINITION, LOCATION_DEFINITION, MAX_SECTION_DEPTH, MAX_SECTION_HEADING_LEVEL, MIN_SECTION_HEADING_LEVEL, OBJECT_DEFINITION, PAGE_MAP, PATCH_NOTE_DEFINITION, POSTGRES_SEARCH_CONFIGS, SERIES_DEFINITION, SKILL_DEFINITION, SPECIES_DEFINITION, addToast, assignEntryTerms, auth0Auth, blocksToMarkdown, bylines, calculateHeadingLevel, canUserCreateFromTemplate, canUserEditBlock, canUserEditFromTemplate, canUserInstantiateTemplate, canUserReviewFromTemplate, canUserViewFromTemplate, cfAccessAuth, clearToasts, cmsAc, cmsStatements, contentSearchIndex, contentTypes, createByline, createPageExcerpt, createTerm, defineDocsConfig, definePageDefinition, deleteByline, deleteR2File, deleteTerm, diffBlocks, diffPageSnapshots, evaluateAccess, extractHeadingsFromBlocks, extractTextFromBlocks, filterBlocksForReader, filterTemplatesForUser, formatDocsUrl, generatePreviewToken, getByline, getBylines, getCmsCollection, getCmsEntry, getDocsSidebar, getDocsVersions, getEntriesByTerm, getEntryTerms, getPageDefinition, getPostgresSearchConfig, getR2Bucket, getSiteSettings, getSurroundItems, getTaxonomyTerms, getTerm, handleCmsCron, handleCmsMcpRequest, hasLockedDescendants, hasPermission, hasRole, indexPageForSearch, inlinesToMarkdown, invalidatePageCache, isValidUUID, listR2Files, mockAuth, pageDraftLocks, pageDrafts, pageTaxonomyTerms, pageTemplates, pageVersionApprovals, pageVersionComments, pageVersions, pages, parseDocsSlug, r2, removeToast, renderCorpusMarkdown, renderPageAsMarkdown, rimelightCms, rimelightCmsLoader, showErrorToast, showSuccessToast, siteSettings, slugifyHeading, taxonomyTerms, toast, toasts, updateByline, updateSiteSettings, updateTerm, uploadR2File, validateBlockAST, validateServerBlockPayload, verifyPreviewToken };