@stacksjs/cms 0.72.8 → 0.72.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.d.ts CHANGED
@@ -6,6 +6,7 @@ import * as posts from './posts/index';
6
6
  import * as tags from './taggables/index';
7
7
  export type { BlockDefinition, BlockError, PageBlock, ValidateBlocksResult } from './blocks/types';
8
8
  export type { MenuTreeItem } from './menus/index';
9
+ export type { EditablePageDocument } from './pages/document';
9
10
  export type { SavedPageDocument, SavePageDocumentInput } from './pages/document';
10
11
  export type { PublishedPage } from './public/resolve';
11
12
  export declare const cms: CmsNamespace;
@@ -35,7 +36,7 @@ export {
35
36
  export { defaultBlocks, registerDefaultBlocks } from './blocks/defaults';
36
37
  export { allBlocks, defineBlock, getBlock, parseStoredBlocks, registerBlocks, validateBlocks } from './blocks/registry';
37
38
  export { fetchMenuTree } from './menus/index';
38
- export { createPageDocument, PageDocumentError, updatePageDocument } from './pages/document';
39
+ export { createPageDocument, fetchPageDocument, PageDocumentError, updatePageDocument } from './pages/document';
39
40
  export { publishDuePages } from './publish/index';
40
41
  export { cmsNotFoundFallback, cmsPageFallback } from './public/fallback';
41
42
  export { renderCmsPage } from './public/render';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import*as authors from"./authors";import*as postCategories from"./categorizables";import*as comments from"./commentables";import*as pages from"./pages";import*as posts from"./posts";import*as tags from"./taggables";export const cms={posts,postCategories,tags,comments,authors,pages};export default cms;export{authors,postCategories as categorizable,comments,pages,posts,tags};export{defaultBlocks,registerDefaultBlocks}from"./blocks/defaults";export{allBlocks,defineBlock,getBlock,parseStoredBlocks,registerBlocks,validateBlocks}from"./blocks/registry";export{fetchMenuTree}from"./menus";export{createPageDocument,PageDocumentError,updatePageDocument}from"./pages/document";export{publishDuePages}from"./publish";export{cmsNotFoundFallback,cmsPageFallback}from"./public/fallback";export{renderCmsPage}from"./public/render";export{normalizePath,resolvePublishedPage}from"./public/resolve";export{sanitizeRichText}from"./public/sanitize";export{recordSlugChangeRedirects,resolveRedirect}from"./redirects";export{fetchRevisions,restoreRevision,storeRevision}from"./revisions";
1
+ import*as authors from"./authors";import*as postCategories from"./categorizables";import*as comments from"./commentables";import*as pages from"./pages";import*as posts from"./posts";import*as tags from"./taggables";export const cms={posts,postCategories,tags,comments,authors,pages};export default cms;export{authors,postCategories as categorizable,comments,pages,posts,tags};export{defaultBlocks,registerDefaultBlocks}from"./blocks/defaults";export{allBlocks,defineBlock,getBlock,parseStoredBlocks,registerBlocks,validateBlocks}from"./blocks/registry";export{fetchMenuTree}from"./menus";export{createPageDocument,fetchPageDocument,PageDocumentError,updatePageDocument}from"./pages/document";export{publishDuePages}from"./publish";export{cmsNotFoundFallback,cmsPageFallback}from"./public/fallback";export{renderCmsPage}from"./public/render";export{normalizePath,resolvePublishedPage}from"./public/resolve";export{sanitizeRichText}from"./public/sanitize";export{recordSlugChangeRedirects,resolveRedirect}from"./redirects";export{fetchRevisions,restoreRevision,storeRevision}from"./revisions";
@@ -12,6 +12,18 @@ export declare function createPageDocument(siteId: number, input: SavePageDocume
12
12
  * rewrites descendant paths, and records 301s for every path that changed.
13
13
  */
14
14
  export declare function updatePageDocument(siteId: number, pageId: number, input: SavePageDocumentInput): Promise<SavedPageDocument>;
15
+ /**
16
+ * Load one page for editing, scoped to its site.
17
+ *
18
+ * `pages.fetchById` returns the raw row, which leaves every caller to parse
19
+ * `blocks` themselves and - more to the point - to remember the site check. An
20
+ * editor that looks a page up by id alone will happily open another tenant's
21
+ * page, so `siteId` is required and the lookup is scoped by it.
22
+ *
23
+ * Returns null when the page does not exist ON THAT SITE, which is the same
24
+ * answer as "not found" and deliberately indistinguishable from it.
25
+ */
26
+ export declare function fetchPageDocument(siteId: number, pageId: number): Promise<EditablePageDocument | null>;
15
27
  export declare interface SavePageDocumentInput {
16
28
  title: string
17
29
  slug?: string
@@ -29,6 +41,22 @@ export declare interface SavedPageDocument {
29
41
  path: string
30
42
  blocks: PageBlock[]
31
43
  }
44
+ /** A page as an editor needs it: meta, parsed blocks, and where it lives. */
45
+ export declare interface EditablePageDocument {
46
+ id: number
47
+ siteId: number
48
+ title: string
49
+ slug: string
50
+ path: string
51
+ parentId: number | null
52
+ template: string | null
53
+ metaDescription: string | null
54
+ status: string
55
+ scheduledAt: string | null
56
+ publishedAt: string | null
57
+ updatedAt: string | null
58
+ blocks: PageBlock[]
59
+ }
32
60
  declare class PageDocumentError extends Error {
33
61
  readonly status: number;
34
62
  readonly details?: unknown;
@@ -1 +1 @@
1
- import{formatDate}from"@stacksjs/orm";import{slugify}from"ts-slug";import{validateBlocks}from"../blocks/registry";import{getDb}from"../database";import{recordSlugChangeRedirects}from"../redirects";import{storeRevision}from"../revisions";class PageDocumentError extends Error{details;status=422;constructor(message,details){super(message);this.details=details;this.name="PageDocumentError"}}async function parentPath(db,siteId,parentId){if(!parentId)return"";const parent=await db.selectFrom("pages").where("id","=",parentId).where("site_id","=",siteId).select(["path"]).executeTakeFirst();if(!parent?.path)throw new PageDocumentError(`Parent page ${parentId} not found on this site`);return parent.path==="/"?"":parent.path}async function uniquePathFor(db,siteId,basePath,requestedSlug,excludePageId){const cleaned=slugify(requestedSlug);if(!cleaned)throw new PageDocumentError("Slug resolves to an empty string");let candidate=cleaned;for(let attempt=2;attempt<100;attempt++){const path=`${basePath}/${candidate}`;let query=db.selectFrom("pages").where("site_id","=",siteId).where("path","=",path).select(["id"]);if(excludePageId)query=query.where("id","!=",excludePageId);if(!await query.executeTakeFirst())return{slug:candidate,path};candidate=`${cleaned}-${attempt}`}throw new PageDocumentError(`Could not find a free path for "${cleaned}"`)}export async function createPageDocument(siteId,input){const db=await getDb(),validated=await validateBlocks(input.blocks??[]);if(!validated.ok)throw new PageDocumentError("Invalid page blocks",validated.errors);const isRoot=input.slug==="/",base=isRoot?"":await parentPath(db,siteId,input.parentId),{slug,path}=isRoot?{slug:"/",path:"/"}:await uniquePathFor(db,siteId,base,input.slug||input.title),now=formatDate(new Date),written=await db.insertInto("pages").values({site_id:siteId,author_id:input.authorId??null,parent_id:isRoot?null:input.parentId??null,title:input.title,slug,path,template:input.template??"default",blocks:JSON.stringify(validated.blocks),meta_description:input.metaDescription??null,status:input.status??"draft",scheduled_at:input.scheduledAt??null,published_at:input.status==="published"?now:null,views:0,conversions:0,created_at:now,updated_at:now}).returningAll().executeTakeFirst(),id=Number(written?.id);if(!id){const row=await db.selectFrom("pages").where("site_id","=",siteId).where("path","=",path).select(["id"]).executeTakeFirst();if(!row)throw Error("Failed to create page");return{id:Number(row.id),path,blocks:validated.blocks}}return{id,path,blocks:validated.blocks}}export async function updatePageDocument(siteId,pageId,input){const db=await getDb(),current=await db.selectFrom("pages").where("id","=",pageId).where("site_id","=",siteId).select(["id","site_id","parent_id","title","slug","path","blocks","meta_description"]).executeTakeFirst();if(!current)throw new PageDocumentError(`Page ${pageId} not found on this site`);const validated=await validateBlocks(input.blocks??[]);if(!validated.ok)throw new PageDocumentError("Invalid page blocks",validated.errors);await storeRevision(pageId,{title:current.title,blocks:current.blocks,metaDescription:current.meta_description},{authorId:input.authorId,note:input.note??null});const isRoot=(input.slug??current.slug)==="/",nextParentId=input.parentId!==void 0?input.parentId:current.parent_id,base=isRoot?"":await parentPath(db,siteId,nextParentId),requestedSlug=input.slug||current.slug||input.title,{slug,path}=isRoot?{slug:"/",path:"/"}:await uniquePathFor(db,siteId,base,requestedSlug,pageId),now=formatDate(new Date);await db.updateTable("pages").set({title:input.title,slug,path,parent_id:isRoot?null:nextParentId??null,...input.template!==void 0&&{template:input.template},blocks:JSON.stringify(validated.blocks),meta_description:input.metaDescription??null,...input.status!==void 0&&{status:input.status},...input.scheduledAt!==void 0&&{scheduled_at:input.scheduledAt},...input.status==="published"&&{published_at:now},updated_at:now}).where("id","=",pageId).execute();if(current.path&&current.path!==path){const moves=[{fromPath:current.path,toPath:path}],descendants=await db.selectFrom("pages").where("site_id","=",siteId).where("path","like",`${current.path}/%`).select(["id","path"]).execute();for(const descendant of descendants){const nextPath=path+descendant.path.slice(current.path.length);await db.updateTable("pages").set({path:nextPath,updated_at:now}).where("id","=",descendant.id).execute();moves.push({fromPath:descendant.path,toPath:nextPath})}await recordSlugChangeRedirects(siteId,moves)}return{id:pageId,path,blocks:validated.blocks}}export{PageDocumentError};
1
+ import{formatDate}from"@stacksjs/orm";import{slugify}from"ts-slug";import{parseStoredBlocks,validateBlocks}from"../blocks/registry";import{getDb}from"../database";import{recordSlugChangeRedirects}from"../redirects";import{storeRevision}from"../revisions";class PageDocumentError extends Error{details;status=422;constructor(message,details){super(message);this.details=details;this.name="PageDocumentError"}}async function parentPath(db,siteId,parentId){if(!parentId)return"";const parent=await db.selectFrom("pages").where("id","=",parentId).where("site_id","=",siteId).select(["path"]).executeTakeFirst();if(!parent?.path)throw new PageDocumentError(`Parent page ${parentId} not found on this site`);return parent.path==="/"?"":parent.path}async function uniquePathFor(db,siteId,basePath,requestedSlug,excludePageId){const cleaned=slugify(requestedSlug);if(!cleaned)throw new PageDocumentError("Slug resolves to an empty string");let candidate=cleaned;for(let attempt=2;attempt<100;attempt++){const path=`${basePath}/${candidate}`;let query=db.selectFrom("pages").where("site_id","=",siteId).where("path","=",path).select(["id"]);if(excludePageId)query=query.where("id","!=",excludePageId);if(!await query.executeTakeFirst())return{slug:candidate,path};candidate=`${cleaned}-${attempt}`}throw new PageDocumentError(`Could not find a free path for "${cleaned}"`)}export async function createPageDocument(siteId,input){const db=await getDb(),validated=await validateBlocks(input.blocks??[]);if(!validated.ok)throw new PageDocumentError("Invalid page blocks",validated.errors);const isRoot=input.slug==="/",base=isRoot?"":await parentPath(db,siteId,input.parentId),{slug,path}=isRoot?{slug:"/",path:"/"}:await uniquePathFor(db,siteId,base,input.slug||input.title),now=formatDate(new Date),written=await db.insertInto("pages").values({site_id:siteId,author_id:input.authorId??null,parent_id:isRoot?null:input.parentId??null,title:input.title,slug,path,template:input.template??"default",blocks:JSON.stringify(validated.blocks),meta_description:input.metaDescription??null,status:input.status??"draft",scheduled_at:input.scheduledAt??null,published_at:input.status==="published"?now:null,views:0,conversions:0,created_at:now,updated_at:now}).returningAll().executeTakeFirst(),id=Number(written?.id);if(!id){const row=await db.selectFrom("pages").where("site_id","=",siteId).where("path","=",path).select(["id"]).executeTakeFirst();if(!row)throw Error("Failed to create page");return{id:Number(row.id),path,blocks:validated.blocks}}return{id,path,blocks:validated.blocks}}export async function updatePageDocument(siteId,pageId,input){const db=await getDb(),current=await db.selectFrom("pages").where("id","=",pageId).where("site_id","=",siteId).select(["id","site_id","parent_id","title","slug","path","blocks","meta_description"]).executeTakeFirst();if(!current)throw new PageDocumentError(`Page ${pageId} not found on this site`);const validated=await validateBlocks(input.blocks??[]);if(!validated.ok)throw new PageDocumentError("Invalid page blocks",validated.errors);await storeRevision(pageId,{title:current.title,blocks:current.blocks,metaDescription:current.meta_description},{authorId:input.authorId,note:input.note??null});const isRoot=(input.slug??current.slug)==="/",nextParentId=input.parentId!==void 0?input.parentId:current.parent_id,base=isRoot?"":await parentPath(db,siteId,nextParentId),requestedSlug=input.slug||current.slug||input.title,{slug,path}=isRoot?{slug:"/",path:"/"}:await uniquePathFor(db,siteId,base,requestedSlug,pageId),now=formatDate(new Date);await db.updateTable("pages").set({title:input.title,slug,path,parent_id:isRoot?null:nextParentId??null,...input.template!==void 0&&{template:input.template},blocks:JSON.stringify(validated.blocks),meta_description:input.metaDescription??null,...input.status!==void 0&&{status:input.status},...input.scheduledAt!==void 0&&{scheduled_at:input.scheduledAt},...input.status==="published"&&{published_at:now},updated_at:now}).where("id","=",pageId).execute();if(current.path&&current.path!==path){const moves=[{fromPath:current.path,toPath:path}],descendants=await db.selectFrom("pages").where("site_id","=",siteId).where("path","like",`${current.path}/%`).select(["id","path"]).execute();for(const descendant of descendants){const nextPath=path+descendant.path.slice(current.path.length);await db.updateTable("pages").set({path:nextPath,updated_at:now}).where("id","=",descendant.id).execute();moves.push({fromPath:descendant.path,toPath:nextPath})}await recordSlugChangeRedirects(siteId,moves)}return{id:pageId,path,blocks:validated.blocks}}export{PageDocumentError};export async function fetchPageDocument(siteId,pageId){const row=await(await getDb()).selectFrom("pages").where("id","=",pageId).where("site_id","=",siteId).selectAll().executeTakeFirst();if(!row)return null;return{id:Number(row.id),siteId:Number(row.site_id),title:String(row.title??""),slug:String(row.slug??""),path:String(row.path??""),parentId:row.parent_id===null||row.parent_id===void 0?null:Number(row.parent_id),template:row.template===null||row.template===void 0?null:String(row.template),metaDescription:row.meta_description===null||row.meta_description===void 0?null:String(row.meta_description),status:String(row.status??"draft"),scheduledAt:row.scheduled_at===null||row.scheduled_at===void 0?null:String(row.scheduled_at),publishedAt:row.published_at===null||row.published_at===void 0?null:String(row.published_at),updatedAt:row.updated_at===null||row.updated_at===void 0?null:String(row.updated_at),blocks:parseStoredBlocks(row.blocks)}}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/cms",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.8",
5
+ "version": "0.72.10",
6
6
  "description": "Stacks cms utilities.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -58,8 +58,8 @@
58
58
  "prepublishOnly": "bun run build"
59
59
  },
60
60
  "dependencies": {
61
- "@stacksjs/sites": "0.72.8",
62
- "@stacksjs/validation": "0.72.8",
61
+ "@stacksjs/sites": "0.72.10",
62
+ "@stacksjs/validation": "0.72.10",
63
63
  "ts-slug": "^0.1.0",
64
64
  "ts-spreadsheets": "^0.2.0"
65
65
  },