@actuate-media/cms-core 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/__tests__/api/collections-discovery.test.js +47 -0
  2. package/dist/__tests__/api/collections-discovery.test.js.map +1 -1
  3. package/dist/__tests__/api/page-sections-routes.test.d.ts +2 -0
  4. package/dist/__tests__/api/page-sections-routes.test.d.ts.map +1 -0
  5. package/dist/__tests__/api/page-sections-routes.test.js +271 -0
  6. package/dist/__tests__/api/page-sections-routes.test.js.map +1 -0
  7. package/dist/__tests__/api/stats-collection-filter.test.d.ts +2 -0
  8. package/dist/__tests__/api/stats-collection-filter.test.d.ts.map +1 -0
  9. package/dist/__tests__/api/stats-collection-filter.test.js +190 -0
  10. package/dist/__tests__/api/stats-collection-filter.test.js.map +1 -0
  11. package/dist/api/handlers.d.ts.map +1 -1
  12. package/dist/api/handlers.js +415 -5
  13. package/dist/api/handlers.js.map +1 -1
  14. package/dist/config/types.d.ts +8 -0
  15. package/dist/config/types.d.ts.map +1 -1
  16. package/dist/sections/__tests__/sections.test.d.ts +2 -0
  17. package/dist/sections/__tests__/sections.test.d.ts.map +1 -0
  18. package/dist/sections/__tests__/sections.test.js +215 -0
  19. package/dist/sections/__tests__/sections.test.js.map +1 -0
  20. package/dist/sections/helpers.d.ts +62 -0
  21. package/dist/sections/helpers.d.ts.map +1 -0
  22. package/dist/sections/helpers.js +202 -0
  23. package/dist/sections/helpers.js.map +1 -0
  24. package/dist/sections/index.d.ts +13 -0
  25. package/dist/sections/index.d.ts.map +1 -0
  26. package/dist/sections/index.js +12 -0
  27. package/dist/sections/index.js.map +1 -0
  28. package/dist/sections/registry.d.ts +27 -0
  29. package/dist/sections/registry.d.ts.map +1 -0
  30. package/dist/sections/registry.js +349 -0
  31. package/dist/sections/registry.js.map +1 -0
  32. package/dist/sections/types.d.ts +127 -0
  33. package/dist/sections/types.d.ts.map +1 -0
  34. package/dist/sections/types.js +21 -0
  35. package/dist/sections/types.js.map +1 -0
  36. package/dist/sections/validate.d.ts +10 -0
  37. package/dist/sections/validate.d.ts.map +1 -0
  38. package/dist/sections/validate.js +92 -0
  39. package/dist/sections/validate.js.map +1 -0
  40. package/package.json +6 -1
@@ -33,6 +33,7 @@ import { createSSEPresenceAdapter } from '../presence/index.js';
33
33
  import { BUILT_IN_TEMPLATES } from '../page-builder/templates.js';
34
34
  import { validateTree } from '../page-builder/validate.js';
35
35
  import { auditAccessibility, fixAccessibility } from '../page-builder/a11y-fix.js';
36
+ import { SECTION_TYPE_LIST, getSectionType, getSectionTypesForScope, buildSection, coerceSections, coercePostHeader, defaultPostHeader, addSection as addSectionToList, removeSection as removeSectionFromList, reorderSections as reorderSectionList, setSectionVisibility, updateSectionContent as updateSectionContentInList, updateSectionSettings as updateSectionSettingsInList, updateSectionMeta as updateSectionMetaInList, validateSectionContent, } from '../sections/index.js';
36
37
  import { getClientIp, isResolvedIp } from '../security/client-ip.js';
37
38
  import { safeFetch, SsrfBlockedError } from '../security/safe-fetch.js';
38
39
  import { redactSecrets } from '../security/redact.js';
@@ -708,6 +709,41 @@ class ModelNotAvailableError extends Error {
708
709
  this.model = model;
709
710
  }
710
711
  }
712
+ /**
713
+ * Resolve a collection definition from the runtime config, tolerating both
714
+ * the array and the record (keyed-by-slug) shapes consumers use.
715
+ */
716
+ function findCollectionDef(slug) {
717
+ const collections = getActuateConfig()?.collections;
718
+ if (Array.isArray(collections)) {
719
+ return collections.find((c) => c?.slug === slug);
720
+ }
721
+ if (collections && typeof collections === 'object') {
722
+ const rec = collections;
723
+ return rec[slug] ?? Object.values(rec).find((c) => c?.slug === slug);
724
+ }
725
+ return undefined;
726
+ }
727
+ /**
728
+ * True when `collection` declares a `sections` JSON field — the storage
729
+ * contract the flat Page Editor + section endpoints rely on. Guards against
730
+ * silently dropping section writes on a collection that never declared the
731
+ * field (the field-access layer strips undeclared keys).
732
+ */
733
+ function collectionSupportsSections(slug) {
734
+ const def = findCollectionDef(slug);
735
+ if (!def)
736
+ return false;
737
+ const fields = def.fields;
738
+ if (Array.isArray(fields)) {
739
+ return fields.some((f) => f?.name === 'sections' && f?.type === 'json');
740
+ }
741
+ if (fields && typeof fields === 'object') {
742
+ const f = fields.sections;
743
+ return f?.type === 'json';
744
+ }
745
+ return false;
746
+ }
711
747
  export function registerCMSRoutes(router) {
712
748
  const rawDb = () => getDB();
713
749
  const db = () => {
@@ -811,6 +847,14 @@ export function registerCMSRoutes(router) {
811
847
  type: def?.type ?? 'page',
812
848
  urlPrefix: def?.urlPrefix ?? slug,
813
849
  hidden: def?.admin?.hidden === true,
850
+ // Surface the admin metadata the Posts area needs to render
851
+ // type tabs / cards (description, icon, accent color, group).
852
+ // None are sensitive — they all already live on the public
853
+ // collection definition the admin client receives via props.
854
+ description: def?.admin?.description ?? null,
855
+ icon: def?.admin?.icon ?? null,
856
+ color: def?.admin?.color ?? null,
857
+ group: def?.admin?.group ?? null,
814
858
  fieldCount: fieldSummary.length,
815
859
  fields: fieldSummary,
816
860
  };
@@ -1705,6 +1749,337 @@ export function registerCMSRoutes(router) {
1705
1749
  }
1706
1750
  });
1707
1751
  // ---------------------------------------------------------------------------
1752
+ // Page section routes (flat Page Editor model)
1753
+ //
1754
+ // These operate on a page document's `data.sections` JSON array — the same
1755
+ // model the visual Page Editor reads/writes — so an AI agent driving the
1756
+ // CMS from Cursor/VS Code via MCP produces real, editable, publishable
1757
+ // sections rather than disposable markup. All mutations go through the
1758
+ // audited `updateDocument` path (versions, hooks, cache invalidation,
1759
+ // live-preview push) and reuse collection scope checks.
1760
+ //
1761
+ // Discovery + validation are deterministic (no LLM) and only need auth.
1762
+ // ---------------------------------------------------------------------------
1763
+ // GET /page-sections/types — discover registered section types + schemas.
1764
+ // Optional `?scope=page|post` filters to types usable on that surface so an
1765
+ // agent only builds sections that have a working renderer in that context.
1766
+ router.get('/page-sections/types', async (request) => {
1767
+ try {
1768
+ const auth = await requireAuth(request);
1769
+ if (auth.error)
1770
+ return auth.error;
1771
+ const scope = new URL(request.url).searchParams.get('scope');
1772
+ const data = scope === 'page' || scope === 'post'
1773
+ ? getSectionTypesForScope(scope)
1774
+ : SECTION_TYPE_LIST;
1775
+ return json({ data });
1776
+ }
1777
+ catch (err) {
1778
+ return internalError(err, 'list section types');
1779
+ }
1780
+ });
1781
+ // POST /page-sections/validate — validate section content against its
1782
+ // type schema. Body: { sectionType, content, settings? }.
1783
+ router.post('/page-sections/validate', async (request) => {
1784
+ try {
1785
+ const auth = await requireAuth(request);
1786
+ if (auth.error)
1787
+ return auth.error;
1788
+ const body = (await request.json().catch(() => null));
1789
+ const sectionType = typeof body?.sectionType === 'string' ? body.sectionType : '';
1790
+ if (!sectionType)
1791
+ return errorResponse('sectionType is required', 400);
1792
+ const content = body?.content && typeof body.content === 'object'
1793
+ ? body.content
1794
+ : {};
1795
+ const settings = body?.settings && typeof body.settings === 'object'
1796
+ ? body.settings
1797
+ : undefined;
1798
+ const result = validateSectionContent(sectionType, content, settings);
1799
+ return json({ data: result });
1800
+ }
1801
+ catch (err) {
1802
+ return internalError(err, 'validate section');
1803
+ }
1804
+ });
1805
+ // Shared: load a page document's sections, or a Response on error.
1806
+ const loadPageSections = async (collection, id, ctx) => {
1807
+ if (!collectionSupportsSections(collection)) {
1808
+ return errorResponse(`Collection "${collection}" does not declare a "sections" JSON field, so it cannot store page sections.`, 400);
1809
+ }
1810
+ const doc = await getDocument(collection, id, ctx);
1811
+ if (!doc)
1812
+ return errorResponse('Page not found', 404);
1813
+ const data = doc.data ?? {};
1814
+ return { sections: coerceSections(data.sections) };
1815
+ };
1816
+ // Shared: persist a mutated section array via the audited update path.
1817
+ const persistPageSections = async (collection, id, sections, ctx, userId) => {
1818
+ const doc = await updateDocument(collection, id, { sections }, ctx);
1819
+ await logEvent({
1820
+ event: 'document_updated',
1821
+ userId,
1822
+ details: { collection, documentId: id, action: 'page_sections_updated' },
1823
+ });
1824
+ notifyPreviewUpdate(collection, id, {
1825
+ values: doc.data,
1826
+ status: doc.status,
1827
+ updatedAt: doc.updatedAt?.toISOString?.(),
1828
+ });
1829
+ const data = doc.data ?? {};
1830
+ return coerceSections(data.sections);
1831
+ };
1832
+ // GET /page-sections/:collection/:id — list a page's sections.
1833
+ router.get('/page-sections/:collection/:id', async (request, params) => {
1834
+ try {
1835
+ const auth = await requireAuth(request);
1836
+ if (auth.error)
1837
+ return auth.error;
1838
+ const scopeErr = requireCollectionScope(auth.session, params.collection, 'read');
1839
+ if (scopeErr)
1840
+ return scopeErr;
1841
+ const ctx = buildActionContext(auth.session, db());
1842
+ const loaded = await loadPageSections(params.collection, params.id, ctx);
1843
+ if (loaded instanceof Response)
1844
+ return loaded;
1845
+ return json({ data: loaded.sections });
1846
+ }
1847
+ catch (err) {
1848
+ return internalError(err, 'list page sections');
1849
+ }
1850
+ });
1851
+ // POST /page-sections/:collection/:id — add a section.
1852
+ // Body: { sectionType, name?, internalLabel?, visible?, content?, settings?, position? }.
1853
+ router.post('/page-sections/:collection/:id', async (request, params) => {
1854
+ try {
1855
+ const auth = await requireAuth(request);
1856
+ if (auth.error)
1857
+ return auth.error;
1858
+ const scopeErr = requireCollectionScope(auth.session, params.collection, 'update');
1859
+ if (scopeErr)
1860
+ return scopeErr;
1861
+ const body = (await request.json().catch(() => null));
1862
+ const sectionType = typeof body?.sectionType === 'string' ? body.sectionType : '';
1863
+ if (!getSectionType(sectionType)) {
1864
+ return errorResponse(`Unknown section type "${sectionType}". Call GET /page-sections/types for valid ids.`, 400);
1865
+ }
1866
+ const ctx = buildActionContext(auth.session, db());
1867
+ const loaded = await loadPageSections(params.collection, params.id, ctx);
1868
+ if (loaded instanceof Response)
1869
+ return loaded;
1870
+ const section = buildSection(sectionType, {
1871
+ name: typeof body?.name === 'string' ? body.name : undefined,
1872
+ internalLabel: typeof body?.internalLabel === 'string' ? body.internalLabel : undefined,
1873
+ visible: body?.visible !== false,
1874
+ content: body?.content && typeof body.content === 'object'
1875
+ ? body.content
1876
+ : undefined,
1877
+ settings: body?.settings && typeof body.settings === 'object'
1878
+ ? body.settings
1879
+ : undefined,
1880
+ });
1881
+ const position = typeof body?.position === 'number' ? body.position : undefined;
1882
+ const next = addSectionToList(loaded.sections, section, position);
1883
+ const saved = await persistPageSections(params.collection, params.id, next, ctx, auth.session.userId);
1884
+ const validation = validateSectionContent(section.sectionType, section.content, section.settings);
1885
+ return json({ data: { section, sections: saved, validation } }, 201);
1886
+ }
1887
+ catch (err) {
1888
+ return internalError(err, 'add page section');
1889
+ }
1890
+ });
1891
+ // POST /page-sections/:collection/:id/reorder — reorder by id list.
1892
+ // Registered BEFORE the :sectionId routes so "reorder" isn't captured as a
1893
+ // section id (the router matches in registration order).
1894
+ router.post('/page-sections/:collection/:id/reorder', async (request, params) => {
1895
+ try {
1896
+ const auth = await requireAuth(request);
1897
+ if (auth.error)
1898
+ return auth.error;
1899
+ const scopeErr = requireCollectionScope(auth.session, params.collection, 'update');
1900
+ if (scopeErr)
1901
+ return scopeErr;
1902
+ const body = (await request.json().catch(() => null));
1903
+ const orderedIds = Array.isArray(body?.orderedIds)
1904
+ ? body.orderedIds.filter((x) => typeof x === 'string')
1905
+ : null;
1906
+ if (!orderedIds)
1907
+ return errorResponse('orderedIds (string[]) is required', 400);
1908
+ const ctx = buildActionContext(auth.session, db());
1909
+ const loaded = await loadPageSections(params.collection, params.id, ctx);
1910
+ if (loaded instanceof Response)
1911
+ return loaded;
1912
+ const next = reorderSectionList(loaded.sections, orderedIds);
1913
+ const saved = await persistPageSections(params.collection, params.id, next, ctx, auth.session.userId);
1914
+ return json({ data: saved });
1915
+ }
1916
+ catch (err) {
1917
+ return internalError(err, 'reorder page sections');
1918
+ }
1919
+ });
1920
+ // PATCH /page-sections/:collection/:id/:sectionId — update a section.
1921
+ // Body: { content?, settings?, name?, internalLabel?, visible? }.
1922
+ router.patch('/page-sections/:collection/:id/:sectionId', async (request, params) => {
1923
+ try {
1924
+ const auth = await requireAuth(request);
1925
+ if (auth.error)
1926
+ return auth.error;
1927
+ const scopeErr = requireCollectionScope(auth.session, params.collection, 'update');
1928
+ if (scopeErr)
1929
+ return scopeErr;
1930
+ const body = (await request.json().catch(() => null));
1931
+ const ctx = buildActionContext(auth.session, db());
1932
+ const loaded = await loadPageSections(params.collection, params.id, ctx);
1933
+ if (loaded instanceof Response)
1934
+ return loaded;
1935
+ const sectionId = params.sectionId;
1936
+ if (!loaded.sections.some((s) => s.id === sectionId)) {
1937
+ return errorResponse(`Section "${sectionId}" not found on this page.`, 404);
1938
+ }
1939
+ let next = loaded.sections;
1940
+ if (body?.content && typeof body.content === 'object') {
1941
+ next = updateSectionContentInList(next, sectionId, body.content);
1942
+ }
1943
+ if (body?.settings && typeof body.settings === 'object') {
1944
+ next = updateSectionSettingsInList(next, sectionId, body.settings);
1945
+ }
1946
+ const meta = {};
1947
+ if (typeof body?.name === 'string')
1948
+ meta.name = body.name;
1949
+ if (typeof body?.internalLabel === 'string')
1950
+ meta.internalLabel = body.internalLabel;
1951
+ if (Object.keys(meta).length > 0)
1952
+ next = updateSectionMetaInList(next, sectionId, meta);
1953
+ if (typeof body?.visible === 'boolean') {
1954
+ next = setSectionVisibility(next, sectionId, body.visible);
1955
+ }
1956
+ const saved = await persistPageSections(params.collection, params.id, next, ctx, auth.session.userId);
1957
+ const updated = saved.find((s) => s.id === sectionId);
1958
+ const validation = updated
1959
+ ? validateSectionContent(updated.sectionType, updated.content, updated.settings)
1960
+ : undefined;
1961
+ return json({ data: { section: updated, sections: saved, validation } });
1962
+ }
1963
+ catch (err) {
1964
+ return internalError(err, 'update page section');
1965
+ }
1966
+ });
1967
+ // DELETE /page-sections/:collection/:id/:sectionId — remove a section.
1968
+ router.delete('/page-sections/:collection/:id/:sectionId', async (request, params) => {
1969
+ try {
1970
+ const auth = await requireAuth(request);
1971
+ if (auth.error)
1972
+ return auth.error;
1973
+ const scopeErr = requireCollectionScope(auth.session, params.collection, 'update');
1974
+ if (scopeErr)
1975
+ return scopeErr;
1976
+ const ctx = buildActionContext(auth.session, db());
1977
+ const loaded = await loadPageSections(params.collection, params.id, ctx);
1978
+ if (loaded instanceof Response)
1979
+ return loaded;
1980
+ const sectionId = params.sectionId;
1981
+ if (!loaded.sections.some((s) => s.id === sectionId)) {
1982
+ return errorResponse(`Section "${sectionId}" not found on this page.`, 404);
1983
+ }
1984
+ const next = removeSectionFromList(loaded.sections, sectionId);
1985
+ const saved = await persistPageSections(params.collection, params.id, next, ctx, auth.session.userId);
1986
+ return json({ data: { sections: saved } });
1987
+ }
1988
+ catch (err) {
1989
+ return internalError(err, 'remove page section');
1990
+ }
1991
+ });
1992
+ // ---------------------------------------------------------------------------
1993
+ // Post template routes
1994
+ //
1995
+ // A post type's template is a single document in the hidden
1996
+ // `post-templates` collection, matched by `slug` == the post-type slug. It
1997
+ // stores the design-driven header layout (`data.header`) and the default
1998
+ // section list (`data.sections`) new posts inherit. Lets an external AI
1999
+ // agent (Cursor/VS Code) read a type's template and build it from
2000
+ // scope=post section types discovered via `/page-sections/types?scope=post`.
2001
+ // ---------------------------------------------------------------------------
2002
+ const POST_TEMPLATES_COLLECTION = 'post-templates';
2003
+ const findPostTemplateDoc = async (postType, ctx) => {
2004
+ const res = await listDocuments({ collection: POST_TEMPLATES_COLLECTION, pageSize: 200 }, ctx);
2005
+ const docs = res.docs ?? [];
2006
+ return (docs.find((d) => (d.slug ?? d.data?.slug) === postType) ?? null);
2007
+ };
2008
+ const templateResponse = (postType, doc) => {
2009
+ if (!doc) {
2010
+ return { postType, docId: null, header: defaultPostHeader(), sections: [] };
2011
+ }
2012
+ const data = doc.data ?? {};
2013
+ return {
2014
+ postType,
2015
+ docId: doc.id,
2016
+ header: coercePostHeader(data.header),
2017
+ sections: coerceSections(data.sections),
2018
+ };
2019
+ };
2020
+ // GET /post-templates/:type — read a post type's template (header +
2021
+ // default sections). Returns an unsaved default when none exists yet.
2022
+ router.get('/post-templates/:type', async (request, params) => {
2023
+ try {
2024
+ const auth = await requireAuth(request);
2025
+ if (auth.error)
2026
+ return auth.error;
2027
+ const scopeErr = requireCollectionScope(auth.session, POST_TEMPLATES_COLLECTION, 'read');
2028
+ if (scopeErr)
2029
+ return scopeErr;
2030
+ const ctx = buildActionContext(auth.session, db());
2031
+ const doc = await findPostTemplateDoc(params.type, ctx);
2032
+ return json({ data: templateResponse(params.type, doc) });
2033
+ }
2034
+ catch (err) {
2035
+ return internalError(err, 'get post template');
2036
+ }
2037
+ });
2038
+ // PUT /post-templates/:type — create or replace a post type's template.
2039
+ // Body: { header?, sections? }. Header is coerced to a full PostHeaderConfig
2040
+ // and sections are coerced (unknown section types dropped) so the stored
2041
+ // template always renders.
2042
+ router.put('/post-templates/:type', async (request, params) => {
2043
+ try {
2044
+ const auth = await requireAuth(request);
2045
+ if (auth.error)
2046
+ return auth.error;
2047
+ const scopeErr = requireCollectionScope(auth.session, POST_TEMPLATES_COLLECTION, 'update');
2048
+ if (scopeErr)
2049
+ return scopeErr;
2050
+ const body = (await request.json().catch(() => null));
2051
+ const header = coercePostHeader(body?.header);
2052
+ const sections = coerceSections(body?.sections);
2053
+ const ctx = buildActionContext(auth.session, db());
2054
+ const existing = await findPostTemplateDoc(params.type, ctx);
2055
+ const data = {
2056
+ title: `${params.type} template`,
2057
+ slug: params.type,
2058
+ postType: params.type,
2059
+ header,
2060
+ sections,
2061
+ status: 'PUBLISHED',
2062
+ };
2063
+ const doc = existing
2064
+ ? await updateDocument(POST_TEMPLATES_COLLECTION, existing.id, data, ctx)
2065
+ : await createDocument(POST_TEMPLATES_COLLECTION, data, ctx);
2066
+ await logEvent({
2067
+ event: existing ? 'document_updated' : 'document_created',
2068
+ userId: auth.session.userId,
2069
+ details: {
2070
+ collection: POST_TEMPLATES_COLLECTION,
2071
+ documentId: doc.id,
2072
+ action: 'post_template_saved',
2073
+ postType: params.type,
2074
+ },
2075
+ });
2076
+ return json({ data: templateResponse(params.type, doc) });
2077
+ }
2078
+ catch (err) {
2079
+ return internalError(err, 'save post template');
2080
+ }
2081
+ });
2082
+ // ---------------------------------------------------------------------------
1708
2083
  // Media routes
1709
2084
  // ---------------------------------------------------------------------------
1710
2085
  router.get('/media', async (request) => {
@@ -2578,6 +2953,36 @@ export function registerCMSRoutes(router) {
2578
2953
  score += 25;
2579
2954
  return score;
2580
2955
  }
2956
+ /**
2957
+ * Collection slugs that are written by the benchmark suite and a few
2958
+ * other internal/test fixtures. Documents in these collections are
2959
+ * never editor-facing content and should never appear in the
2960
+ * dashboard's stats, counts, or recent-activity feed.
2961
+ *
2962
+ * This list is the fallback when the consumer ships collection
2963
+ * definitions only to the admin client (e.g. `apps/dev`) and
2964
+ * `getActuateConfig()?.collections` is therefore empty on the
2965
+ * server. Production consumers normally pass `collections` to
2966
+ * `handleActuateAPI({ config })`, in which case the registered
2967
+ * slugs become the allowlist and this denylist is unused.
2968
+ */
2969
+ const INTERNAL_COLLECTION_SLUGS = ['benchmarks', 'bench'];
2970
+ /**
2971
+ * Build the Prisma `where` clause for "user-facing content".
2972
+ *
2973
+ * - If the CMS config registered collections, restrict to those
2974
+ * slugs — anything else is orphan/test data.
2975
+ * - Otherwise fall back to excluding the internal denylist.
2976
+ *
2977
+ * Always excludes soft-deleted rows. Callers can spread additional
2978
+ * filters on top (`status: 'PUBLISHED'`, etc.).
2979
+ */
2980
+ function userContentWhere(extra = {}) {
2981
+ const cfg = getActuateConfig();
2982
+ const registered = Object.keys(cfg?.collections ?? {});
2983
+ const collectionFilter = registered.length > 0 ? { in: registered } : { notIn: [...INTERNAL_COLLECTION_SLUGS] };
2984
+ return { deletedAt: null, collection: collectionFilter, ...extra };
2985
+ }
2581
2986
  router.get('/stats', async (request) => {
2582
2987
  try {
2583
2988
  if (!isSecretMissing()) {
@@ -2592,8 +2997,13 @@ export function registerCMSRoutes(router) {
2592
2997
  catch {
2593
2998
  return json({ data: EMPTY_STATS });
2594
2999
  }
3000
+ // `userContentWhere()` filters out benchmark / orphan / internal
3001
+ // collections so the dashboard surfaces editor content only. See
3002
+ // the helper above for the allowlist-vs-denylist behaviour.
3003
+ const contentWhere = userContentWhere();
3004
+ const publishedContentWhere = userContentWhere({ status: 'PUBLISHED' });
2595
3005
  const [docResult, mediaResult, userResult, recentResult, collGroupResult, statusGroupResult, formResult, seoDocsResult, webhookTotalResult, webhookActiveResult,] = await Promise.allSettled([
2596
- safeCount(d.document, { deletedAt: null }),
3006
+ safeCount(d.document, contentWhere),
2597
3007
  safeCount(d.media),
2598
3008
  safeCount(d.user),
2599
3009
  safeFindMany(d.document, {
@@ -2603,7 +3013,7 @@ export function registerCMSRoutes(router) {
2603
3013
  // -> Prisma Postgres path that saves ~80 ms (one full network
2604
3014
  // RTT) on every dashboard load.
2605
3015
  relationLoadStrategy: 'join',
2606
- where: { deletedAt: null },
3016
+ where: contentWhere,
2607
3017
  orderBy: { updatedAt: 'desc' },
2608
3018
  take: 20,
2609
3019
  select: {
@@ -2616,11 +3026,11 @@ export function registerCMSRoutes(router) {
2616
3026
  createdBy: { select: { name: true, email: true } },
2617
3027
  },
2618
3028
  }),
2619
- safeGroupBy(d.document, ['collection'], { deletedAt: null }),
2620
- safeGroupBy(d.document, ['status'], { deletedAt: null }),
3029
+ safeGroupBy(d.document, ['collection'], contentWhere),
3030
+ safeGroupBy(d.document, ['status'], contentWhere),
2621
3031
  safeCount(d.formSubmission),
2622
3032
  safeFindMany(d.document, {
2623
- where: { deletedAt: null, status: 'PUBLISHED' },
3033
+ where: publishedContentWhere,
2624
3034
  select: { data: true },
2625
3035
  take: 200,
2626
3036
  }),