modern-cms 1.0.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.
- package/README.md +41 -0
- package/bin/lib.js +97 -0
- package/bin/modern-cms.js +197 -0
- package/package.json +37 -0
- package/template/.env.example +48 -0
- package/template/README.md +210 -0
- package/template/apps/api/Dockerfile +26 -0
- package/template/apps/api/package.json +45 -0
- package/template/apps/api/prisma/backfill-search-text.ts +31 -0
- package/template/apps/api/prisma/migrations/20260701034530_init/migration.sql +86 -0
- package/template/apps/api/prisma/migrations/20260701081942_add_publish_at_and_revisions/migration.sql +19 -0
- package/template/apps/api/prisma/migrations/20260701085011_add_form_submissions/migration.sql +13 -0
- package/template/apps/api/prisma/migrations/20260702084049_add_global_sections_and_saved_blocks/migration.sql +24 -0
- package/template/apps/api/prisma/migrations/20260702085609_add_page_locale_and_translations/migration.sql +6 -0
- package/template/apps/api/prisma/migrations/20260702091914_add_collections/migration.sql +38 -0
- package/template/apps/api/prisma/migrations/20260702120000_platform_hardening/migration.sql +15 -0
- package/template/apps/api/prisma/migrations/20260705031230_add_collections_redirects_media_meta/migration.sql +17 -0
- package/template/apps/api/prisma/migrations/20260705033241_add_audit_log/migration.sql +18 -0
- package/template/apps/api/prisma/migrations/20260706031301_standard_cms_platform_pass/migration.sql +48 -0
- package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
- package/template/apps/api/prisma/reset-admin.ts +19 -0
- package/template/apps/api/prisma/schema.prisma +251 -0
- package/template/apps/api/prisma/seed-homepage.ts +752 -0
- package/template/apps/api/prisma/seed-our-charities.ts +231 -0
- package/template/apps/api/prisma/seed-templates.ts +154 -0
- package/template/apps/api/prisma/seed.ts +191 -0
- package/template/apps/api/src/env.ts +61 -0
- package/template/apps/api/src/index.ts +66 -0
- package/template/apps/api/src/lib/revalidate.ts +28 -0
- package/template/apps/api/src/lib/storage/azure.ts +25 -0
- package/template/apps/api/src/lib/storage/cloudinary.ts +42 -0
- package/template/apps/api/src/lib/storage/index.ts +25 -0
- package/template/apps/api/src/lib/storage/s3.ts +39 -0
- package/template/apps/api/src/lib/storage/types.ts +8 -0
- package/template/apps/api/src/middleware/auditLog.ts +32 -0
- package/template/apps/api/src/middleware/auth.ts +53 -0
- package/template/apps/api/src/middleware/rateLimit.ts +31 -0
- package/template/apps/api/src/middleware/securityHeaders.ts +10 -0
- package/template/apps/api/src/prisma.ts +3 -0
- package/template/apps/api/src/routes/audit.routes.ts +26 -0
- package/template/apps/api/src/routes/auth.routes.ts +46 -0
- package/template/apps/api/src/routes/backup.routes.ts +216 -0
- package/template/apps/api/src/routes/blocks.routes.ts +38 -0
- package/template/apps/api/src/routes/collections.routes.ts +185 -0
- package/template/apps/api/src/routes/forms.routes.ts +114 -0
- package/template/apps/api/src/routes/globalSections.routes.ts +43 -0
- package/template/apps/api/src/routes/media.routes.ts +77 -0
- package/template/apps/api/src/routes/pages.routes.ts +319 -0
- package/template/apps/api/src/routes/redirects.routes.ts +52 -0
- package/template/apps/api/src/routes/search.routes.ts +83 -0
- package/template/apps/api/src/routes/templates.routes.ts +41 -0
- package/template/apps/api/src/routes/theme.routes.ts +75 -0
- package/template/apps/api/src/routes/users.routes.ts +39 -0
- package/template/apps/api/tsconfig.json +11 -0
- package/template/apps/web/Dockerfile +26 -0
- package/template/apps/web/app/[[...slug]]/page.tsx +254 -0
- package/template/apps/web/app/admin/(protected)/audit/page.tsx +80 -0
- package/template/apps/web/app/admin/(protected)/collections/item/[itemId]/page.tsx +375 -0
- package/template/apps/web/app/admin/(protected)/collections/page.tsx +224 -0
- package/template/apps/web/app/admin/(protected)/forms/page.tsx +88 -0
- package/template/apps/web/app/admin/(protected)/layout.tsx +5 -0
- package/template/apps/web/app/admin/(protected)/media/page.tsx +138 -0
- package/template/apps/web/app/admin/(protected)/page.tsx +186 -0
- package/template/apps/web/app/admin/(protected)/pages/[id]/page.tsx +560 -0
- package/template/apps/web/app/admin/(protected)/settings/backup/page.tsx +97 -0
- package/template/apps/web/app/admin/(protected)/settings/global-sections/page.tsx +333 -0
- package/template/apps/web/app/admin/(protected)/settings/navigation/page.tsx +192 -0
- package/template/apps/web/app/admin/(protected)/settings/redirects/page.tsx +108 -0
- package/template/apps/web/app/admin/(protected)/settings/theme/page.tsx +239 -0
- package/template/apps/web/app/admin/(protected)/users/page.tsx +94 -0
- package/template/apps/web/app/admin/login/page.tsx +76 -0
- package/template/apps/web/app/api/revalidate/route.ts +34 -0
- package/template/apps/web/app/globals.css +120 -0
- package/template/apps/web/app/layout.tsx +39 -0
- package/template/apps/web/app/preview/[token]/page.tsx +70 -0
- package/template/apps/web/app/robots.ts +11 -0
- package/template/apps/web/app/sitemap.ts +27 -0
- package/template/apps/web/components/admin/AdminShell.tsx +104 -0
- package/template/apps/web/components/builder/BuilderCanvasNode.tsx +546 -0
- package/template/apps/web/components/builder/BuilderDeviceContext.tsx +13 -0
- package/template/apps/web/components/builder/Canvas.tsx +76 -0
- package/template/apps/web/components/builder/CanvasOverlayContext.tsx +22 -0
- package/template/apps/web/components/builder/ColorPickerField.tsx +47 -0
- package/template/apps/web/components/builder/DroppableChildren.tsx +50 -0
- package/template/apps/web/components/builder/FieldRenderer.tsx +277 -0
- package/template/apps/web/components/builder/IconRenderer.tsx +7 -0
- package/template/apps/web/components/builder/Inspector.tsx +956 -0
- package/template/apps/web/components/builder/JsonEditor.tsx +71 -0
- package/template/apps/web/components/builder/KeyValueListEditor.tsx +76 -0
- package/template/apps/web/components/builder/MediaPickerModal.tsx +95 -0
- package/template/apps/web/components/builder/PageSettingsModal.tsx +363 -0
- package/template/apps/web/components/builder/Palette.tsx +106 -0
- package/template/apps/web/components/builder/RichTextEditor.tsx +140 -0
- package/template/apps/web/components/renderer/CmsImage.tsx +46 -0
- package/template/apps/web/components/renderer/NodeBackgroundLayers.tsx +35 -0
- package/template/apps/web/components/renderer/PublicPageView.tsx +56 -0
- package/template/apps/web/components/renderer/Renderer.tsx +146 -0
- package/template/apps/web/components/renderer/RendererContext.tsx +20 -0
- package/template/apps/web/components/renderer/registry.tsx +87 -0
- package/template/apps/web/components/renderer/sections/Accordion.tsx +39 -0
- package/template/apps/web/components/renderer/sections/BeforeAfter.tsx +75 -0
- package/template/apps/web/components/renderer/sections/Button.tsx +42 -0
- package/template/apps/web/components/renderer/sections/ButtonGroup.tsx +22 -0
- package/template/apps/web/components/renderer/sections/CardGrid.tsx +66 -0
- package/template/apps/web/components/renderer/sections/CollectionList.tsx +84 -0
- package/template/apps/web/components/renderer/sections/Column.tsx +26 -0
- package/template/apps/web/components/renderer/sections/ContactForm.tsx +96 -0
- package/template/apps/web/components/renderer/sections/Container.tsx +44 -0
- package/template/apps/web/components/renderer/sections/ContentLinks.tsx +38 -0
- package/template/apps/web/components/renderer/sections/Countdown.tsx +69 -0
- package/template/apps/web/components/renderer/sections/Cta.tsx +15 -0
- package/template/apps/web/components/renderer/sections/Divider.tsx +26 -0
- package/template/apps/web/components/renderer/sections/EmbedBlock.tsx +43 -0
- package/template/apps/web/components/renderer/sections/Footer.tsx +117 -0
- package/template/apps/web/components/renderer/sections/Gallery.tsx +27 -0
- package/template/apps/web/components/renderer/sections/GoToTop.tsx +54 -0
- package/template/apps/web/components/renderer/sections/Grid.tsx +54 -0
- package/template/apps/web/components/renderer/sections/GridCell.tsx +33 -0
- package/template/apps/web/components/renderer/sections/Header.tsx +132 -0
- package/template/apps/web/components/renderer/sections/Hero.tsx +73 -0
- package/template/apps/web/components/renderer/sections/ImageBackgroundSection.tsx +20 -0
- package/template/apps/web/components/renderer/sections/ImageBlock.tsx +55 -0
- package/template/apps/web/components/renderer/sections/LanguageSwitcher.tsx +51 -0
- package/template/apps/web/components/renderer/sections/Logo.tsx +24 -0
- package/template/apps/web/components/renderer/sections/LogoCloud.tsx +67 -0
- package/template/apps/web/components/renderer/sections/MapEmbed.tsx +24 -0
- package/template/apps/web/components/renderer/sections/Marquee.tsx +45 -0
- package/template/apps/web/components/renderer/sections/NavLinks.tsx +69 -0
- package/template/apps/web/components/renderer/sections/NavZone.tsx +19 -0
- package/template/apps/web/components/renderer/sections/PricingTable.tsx +65 -0
- package/template/apps/web/components/renderer/sections/RichText.tsx +7 -0
- package/template/apps/web/components/renderer/sections/SiteSearch.tsx +67 -0
- package/template/apps/web/components/renderer/sections/Slider.tsx +81 -0
- package/template/apps/web/components/renderer/sections/Spacer.tsx +8 -0
- package/template/apps/web/components/renderer/sections/Stats.tsx +69 -0
- package/template/apps/web/components/renderer/sections/Tabs.tsx +59 -0
- package/template/apps/web/components/renderer/sections/TeamGrid.tsx +49 -0
- package/template/apps/web/components/renderer/sections/Testimonial.tsx +79 -0
- package/template/apps/web/components/renderer/sections/ThreeBackground.tsx +43 -0
- package/template/apps/web/components/renderer/sections/Timeline.tsx +29 -0
- package/template/apps/web/components/renderer/sections/VideoBackgroundSection.tsx +20 -0
- package/template/apps/web/components/renderer/sections/VideoEmbed.tsx +70 -0
- package/template/apps/web/components/renderer/sections/types.ts +7 -0
- package/template/apps/web/components/seo/JsonLd.tsx +111 -0
- package/template/apps/web/components/theme/AnalyticsScripts.tsx +68 -0
- package/template/apps/web/components/theme/DarkModeToggle.tsx +39 -0
- package/template/apps/web/components/theme/SiteDataContext.tsx +39 -0
- package/template/apps/web/components/theme/ThemeProvider.tsx +58 -0
- package/template/apps/web/eslint.config.mjs +16 -0
- package/template/apps/web/lib/api.ts +45 -0
- package/template/apps/web/lib/responsiveCss.ts +42 -0
- package/template/apps/web/lib/sanitizeHtml.ts +16 -0
- package/template/apps/web/lib/serverApi.ts +25 -0
- package/template/apps/web/lib/style.ts +73 -0
- package/template/apps/web/lib/threeBackgroundScene.ts +298 -0
- package/template/apps/web/lib/useCurrentUser.ts +35 -0
- package/template/apps/web/lib/useEntranceAnimation.ts +245 -0
- package/template/apps/web/next-env.d.ts +6 -0
- package/template/apps/web/next.config.mjs +32 -0
- package/template/apps/web/package.json +46 -0
- package/template/apps/web/postcss.config.mjs +6 -0
- package/template/apps/web/proxy.ts +23 -0
- package/template/apps/web/tailwind.config.ts +24 -0
- package/template/apps/web/tsconfig.json +41 -0
- package/template/docker-compose.yml +60 -0
- package/template/package.json +29 -0
- package/template/packages/shared/package.json +21 -0
- package/template/packages/shared/src/components.ts +1626 -0
- package/template/packages/shared/src/index.ts +9 -0
- package/template/packages/shared/src/schema/collections.ts +70 -0
- package/template/packages/shared/src/schema/fields.ts +26 -0
- package/template/packages/shared/src/schema/media.ts +13 -0
- package/template/packages/shared/src/schema/pageNode.ts +255 -0
- package/template/packages/shared/src/schema/templates.ts +20 -0
- package/template/packages/shared/src/schema/theme.ts +69 -0
- package/template/packages/shared/src/schema/user.ts +25 -0
- package/template/packages/shared/src/tree.test.ts +65 -0
- package/template/packages/shared/src/tree.ts +303 -0
- package/template/packages/shared/tsconfig.json +8 -0
- package/template/packages/shared/vitest.config.ts +8 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pgcms/api",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx watch src/index.ts",
|
|
8
|
+
"build": "tsc -p tsconfig.json",
|
|
9
|
+
"start": "node dist/index.js",
|
|
10
|
+
"db:migrate": "prisma migrate dev",
|
|
11
|
+
"db:generate": "prisma generate",
|
|
12
|
+
"db:seed": "tsx prisma/seed.ts",
|
|
13
|
+
"db:seed:demo-charities": "tsx prisma/seed-our-charities.ts",
|
|
14
|
+
"db:seed:templates": "tsx prisma/seed-templates.ts"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@aws-sdk/client-s3": "^3.665.0",
|
|
18
|
+
"@aws-sdk/s3-request-presigner": "^3.665.0",
|
|
19
|
+
"@azure/storage-blob": "^12.33.0",
|
|
20
|
+
"@pgcms/shared": "*",
|
|
21
|
+
"@prisma/client": "^5.20.0",
|
|
22
|
+
"bcryptjs": "^2.4.3",
|
|
23
|
+
"cloudinary": "^2.10.0",
|
|
24
|
+
"cookie-parser": "^1.4.6",
|
|
25
|
+
"cors": "^2.8.5",
|
|
26
|
+
"dotenv": "^16.4.5",
|
|
27
|
+
"express": "^4.21.0",
|
|
28
|
+
"jsonwebtoken": "^9.0.2",
|
|
29
|
+
"multer": "^2.0.0",
|
|
30
|
+
"nanoid": "^5.0.7",
|
|
31
|
+
"zod": "^3.23.8"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/bcryptjs": "^2.4.6",
|
|
35
|
+
"@types/cookie-parser": "^1.4.7",
|
|
36
|
+
"@types/cors": "^2.8.17",
|
|
37
|
+
"@types/express": "^4.17.21",
|
|
38
|
+
"@types/jsonwebtoken": "^9.0.7",
|
|
39
|
+
"@types/multer": "^1.4.12",
|
|
40
|
+
"@types/node": "^24.13.2",
|
|
41
|
+
"prisma": "^5.20.0",
|
|
42
|
+
"tsx": "^4.22.4",
|
|
43
|
+
"typescript": "^5.9.3"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// One-off: populates the new `searchText` column (added alongside the standard-CMS
|
|
2
|
+
// platform pass) for every Page/CollectionItem row that predates it. Going forward,
|
|
3
|
+
// pages.routes.ts/collections.routes.ts keep it in sync on every save — this only
|
|
4
|
+
// needs to run once, against existing data.
|
|
5
|
+
import { PrismaClient } from "@prisma/client";
|
|
6
|
+
import { extractSearchText, type PageNode } from "@pgcms/shared";
|
|
7
|
+
|
|
8
|
+
const prisma = new PrismaClient();
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
const pages = await prisma.page.findMany({ where: { searchText: "" } });
|
|
12
|
+
for (const page of pages) {
|
|
13
|
+
const searchText = extractSearchText(page.content as unknown as PageNode);
|
|
14
|
+
await prisma.page.update({ where: { id: page.id }, data: { searchText } });
|
|
15
|
+
}
|
|
16
|
+
console.log(`Backfilled searchText for ${pages.length} page(s).`);
|
|
17
|
+
|
|
18
|
+
const items = await prisma.collectionItem.findMany({ where: { searchText: "" } });
|
|
19
|
+
for (const item of items) {
|
|
20
|
+
const searchText = extractSearchText(item.content as unknown as PageNode);
|
|
21
|
+
await prisma.collectionItem.update({ where: { id: item.id }, data: { searchText } });
|
|
22
|
+
}
|
|
23
|
+
console.log(`Backfilled searchText for ${items.length} collection item(s).`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
main()
|
|
27
|
+
.catch((err) => {
|
|
28
|
+
console.error(err);
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
})
|
|
31
|
+
.finally(() => prisma.$disconnect());
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
-- CreateEnum
|
|
2
|
+
CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'EDITOR');
|
|
3
|
+
|
|
4
|
+
-- CreateEnum
|
|
5
|
+
CREATE TYPE "PageStatus" AS ENUM ('DRAFT', 'PUBLISHED');
|
|
6
|
+
|
|
7
|
+
-- CreateTable
|
|
8
|
+
CREATE TABLE "User" (
|
|
9
|
+
"id" TEXT NOT NULL,
|
|
10
|
+
"email" TEXT NOT NULL,
|
|
11
|
+
"passwordHash" TEXT NOT NULL,
|
|
12
|
+
"role" "UserRole" NOT NULL DEFAULT 'EDITOR',
|
|
13
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
14
|
+
|
|
15
|
+
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
-- CreateTable
|
|
19
|
+
CREATE TABLE "Page" (
|
|
20
|
+
"id" TEXT NOT NULL,
|
|
21
|
+
"slug" TEXT NOT NULL,
|
|
22
|
+
"title" TEXT NOT NULL,
|
|
23
|
+
"status" "PageStatus" NOT NULL DEFAULT 'DRAFT',
|
|
24
|
+
"content" JSONB NOT NULL,
|
|
25
|
+
"seoTitle" TEXT,
|
|
26
|
+
"seoDescription" TEXT,
|
|
27
|
+
"ogImage" TEXT,
|
|
28
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
29
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
30
|
+
|
|
31
|
+
CONSTRAINT "Page_pkey" PRIMARY KEY ("id")
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
-- CreateTable
|
|
35
|
+
CREATE TABLE "MediaAsset" (
|
|
36
|
+
"id" TEXT NOT NULL,
|
|
37
|
+
"key" TEXT NOT NULL,
|
|
38
|
+
"url" TEXT NOT NULL,
|
|
39
|
+
"mimeType" TEXT NOT NULL,
|
|
40
|
+
"width" INTEGER,
|
|
41
|
+
"height" INTEGER,
|
|
42
|
+
"size" INTEGER NOT NULL,
|
|
43
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
44
|
+
|
|
45
|
+
CONSTRAINT "MediaAsset_pkey" PRIMARY KEY ("id")
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
-- CreateTable
|
|
49
|
+
CREATE TABLE "SiteTheme" (
|
|
50
|
+
"id" TEXT NOT NULL,
|
|
51
|
+
"primaryColor" TEXT NOT NULL DEFAULT '#2563eb',
|
|
52
|
+
"secondaryColor" TEXT NOT NULL DEFAULT '#0f172a',
|
|
53
|
+
"backgroundColor" TEXT NOT NULL DEFAULT '#ffffff',
|
|
54
|
+
"textColor" TEXT NOT NULL DEFAULT '#111827',
|
|
55
|
+
"headingFont" TEXT NOT NULL DEFAULT 'Poppins',
|
|
56
|
+
"bodyFont" TEXT NOT NULL DEFAULT 'Inter',
|
|
57
|
+
"logoUrl" TEXT,
|
|
58
|
+
"faviconUrl" TEXT,
|
|
59
|
+
"siteName" TEXT NOT NULL DEFAULT 'My Site',
|
|
60
|
+
"customCss" TEXT,
|
|
61
|
+
|
|
62
|
+
CONSTRAINT "SiteTheme_pkey" PRIMARY KEY ("id")
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
-- CreateTable
|
|
66
|
+
CREATE TABLE "NavItem" (
|
|
67
|
+
"id" TEXT NOT NULL,
|
|
68
|
+
"label" TEXT NOT NULL,
|
|
69
|
+
"href" TEXT NOT NULL,
|
|
70
|
+
"order" INTEGER NOT NULL DEFAULT 0,
|
|
71
|
+
"parentId" TEXT,
|
|
72
|
+
|
|
73
|
+
CONSTRAINT "NavItem_pkey" PRIMARY KEY ("id")
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
-- CreateIndex
|
|
77
|
+
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
|
78
|
+
|
|
79
|
+
-- CreateIndex
|
|
80
|
+
CREATE UNIQUE INDEX "Page_slug_key" ON "Page"("slug");
|
|
81
|
+
|
|
82
|
+
-- CreateIndex
|
|
83
|
+
CREATE UNIQUE INDEX "MediaAsset_key_key" ON "MediaAsset"("key");
|
|
84
|
+
|
|
85
|
+
-- AddForeignKey
|
|
86
|
+
ALTER TABLE "NavItem" ADD CONSTRAINT "NavItem_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "NavItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- AlterTable
|
|
2
|
+
ALTER TABLE "Page" ADD COLUMN "publishAt" TIMESTAMP(3);
|
|
3
|
+
|
|
4
|
+
-- CreateTable
|
|
5
|
+
CREATE TABLE "PageRevision" (
|
|
6
|
+
"id" TEXT NOT NULL,
|
|
7
|
+
"pageId" TEXT NOT NULL,
|
|
8
|
+
"title" TEXT NOT NULL,
|
|
9
|
+
"content" JSONB NOT NULL,
|
|
10
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
11
|
+
|
|
12
|
+
CONSTRAINT "PageRevision_pkey" PRIMARY KEY ("id")
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
-- CreateIndex
|
|
16
|
+
CREATE INDEX "PageRevision_pageId_createdAt_idx" ON "PageRevision"("pageId", "createdAt");
|
|
17
|
+
|
|
18
|
+
-- AddForeignKey
|
|
19
|
+
ALTER TABLE "PageRevision" ADD CONSTRAINT "PageRevision_pageId_fkey" FOREIGN KEY ("pageId") REFERENCES "Page"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- CreateTable
|
|
2
|
+
CREATE TABLE "FormSubmission" (
|
|
3
|
+
"id" TEXT NOT NULL,
|
|
4
|
+
"formName" TEXT NOT NULL,
|
|
5
|
+
"pageSlug" TEXT NOT NULL,
|
|
6
|
+
"data" JSONB NOT NULL,
|
|
7
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
8
|
+
|
|
9
|
+
CONSTRAINT "FormSubmission_pkey" PRIMARY KEY ("id")
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
-- CreateIndex
|
|
13
|
+
CREATE INDEX "FormSubmission_formName_createdAt_idx" ON "FormSubmission"("formName", "createdAt");
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- CreateTable
|
|
2
|
+
CREATE TABLE "GlobalSection" (
|
|
3
|
+
"id" TEXT NOT NULL,
|
|
4
|
+
"kind" TEXT NOT NULL,
|
|
5
|
+
"enabled" BOOLEAN NOT NULL DEFAULT false,
|
|
6
|
+
"content" JSONB NOT NULL,
|
|
7
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
8
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
9
|
+
|
|
10
|
+
CONSTRAINT "GlobalSection_pkey" PRIMARY KEY ("id")
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
-- CreateTable
|
|
14
|
+
CREATE TABLE "SavedBlock" (
|
|
15
|
+
"id" TEXT NOT NULL,
|
|
16
|
+
"name" TEXT NOT NULL,
|
|
17
|
+
"content" JSONB NOT NULL,
|
|
18
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
19
|
+
|
|
20
|
+
CONSTRAINT "SavedBlock_pkey" PRIMARY KEY ("id")
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
-- CreateIndex
|
|
24
|
+
CREATE UNIQUE INDEX "GlobalSection_kind_key" ON "GlobalSection"("kind");
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- AlterTable
|
|
2
|
+
ALTER TABLE "Page" ADD COLUMN "locale" TEXT NOT NULL DEFAULT 'en',
|
|
3
|
+
ADD COLUMN "translationOfId" TEXT;
|
|
4
|
+
|
|
5
|
+
-- AddForeignKey
|
|
6
|
+
ALTER TABLE "Page" ADD CONSTRAINT "Page_translationOfId_fkey" FOREIGN KEY ("translationOfId") REFERENCES "Page"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
-- CreateTable
|
|
2
|
+
CREATE TABLE "Collection" (
|
|
3
|
+
"id" TEXT NOT NULL,
|
|
4
|
+
"name" TEXT NOT NULL,
|
|
5
|
+
"slug" TEXT NOT NULL,
|
|
6
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
7
|
+
|
|
8
|
+
CONSTRAINT "Collection_pkey" PRIMARY KEY ("id")
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
-- CreateTable
|
|
12
|
+
CREATE TABLE "CollectionItem" (
|
|
13
|
+
"id" TEXT NOT NULL,
|
|
14
|
+
"collectionId" TEXT NOT NULL,
|
|
15
|
+
"slug" TEXT NOT NULL,
|
|
16
|
+
"title" TEXT NOT NULL,
|
|
17
|
+
"excerpt" TEXT,
|
|
18
|
+
"coverImage" TEXT,
|
|
19
|
+
"content" JSONB NOT NULL,
|
|
20
|
+
"status" "PageStatus" NOT NULL DEFAULT 'DRAFT',
|
|
21
|
+
"publishedAt" TIMESTAMP(3),
|
|
22
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
23
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
24
|
+
|
|
25
|
+
CONSTRAINT "CollectionItem_pkey" PRIMARY KEY ("id")
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
-- CreateIndex
|
|
29
|
+
CREATE UNIQUE INDEX "Collection_slug_key" ON "Collection"("slug");
|
|
30
|
+
|
|
31
|
+
-- CreateIndex
|
|
32
|
+
CREATE INDEX "CollectionItem_collectionId_status_publishedAt_idx" ON "CollectionItem"("collectionId", "status", "publishedAt");
|
|
33
|
+
|
|
34
|
+
-- CreateIndex
|
|
35
|
+
CREATE UNIQUE INDEX "CollectionItem_collectionId_slug_key" ON "CollectionItem"("collectionId", "slug");
|
|
36
|
+
|
|
37
|
+
-- AddForeignKey
|
|
38
|
+
ALTER TABLE "CollectionItem" ADD CONSTRAINT "CollectionItem_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "Collection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- AlterTable
|
|
2
|
+
ALTER TABLE "Page" ADD COLUMN "noIndex" BOOLEAN NOT NULL DEFAULT false;
|
|
3
|
+
ALTER TABLE "Page" ADD COLUMN "previewToken" TEXT;
|
|
4
|
+
|
|
5
|
+
-- Backfill preview tokens for existing pages
|
|
6
|
+
UPDATE "Page" SET "previewToken" = 'prev_' || "id" WHERE "previewToken" IS NULL;
|
|
7
|
+
|
|
8
|
+
ALTER TABLE "Page" ALTER COLUMN "previewToken" SET NOT NULL;
|
|
9
|
+
CREATE UNIQUE INDEX "Page_previewToken_key" ON "Page"("previewToken");
|
|
10
|
+
|
|
11
|
+
-- AlterTable
|
|
12
|
+
ALTER TABLE "CollectionItem" ADD COLUMN "seoTitle" TEXT;
|
|
13
|
+
ALTER TABLE "CollectionItem" ADD COLUMN "seoDescription" TEXT;
|
|
14
|
+
ALTER TABLE "CollectionItem" ADD COLUMN "ogImage" TEXT;
|
|
15
|
+
ALTER TABLE "CollectionItem" ADD COLUMN "noIndex" BOOLEAN NOT NULL DEFAULT false;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- AlterTable
|
|
2
|
+
ALTER TABLE "MediaAsset" ADD COLUMN "alt" TEXT,
|
|
3
|
+
ADD COLUMN "filename" TEXT NOT NULL DEFAULT '';
|
|
4
|
+
|
|
5
|
+
-- CreateTable
|
|
6
|
+
CREATE TABLE "Redirect" (
|
|
7
|
+
"id" TEXT NOT NULL,
|
|
8
|
+
"fromPath" TEXT NOT NULL,
|
|
9
|
+
"toPath" TEXT NOT NULL,
|
|
10
|
+
"permanent" BOOLEAN NOT NULL DEFAULT true,
|
|
11
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
12
|
+
|
|
13
|
+
CONSTRAINT "Redirect_pkey" PRIMARY KEY ("id")
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
-- CreateIndex
|
|
17
|
+
CREATE UNIQUE INDEX "Redirect_fromPath_key" ON "Redirect"("fromPath");
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- CreateTable
|
|
2
|
+
CREATE TABLE "AuditLog" (
|
|
3
|
+
"id" TEXT NOT NULL,
|
|
4
|
+
"userId" TEXT,
|
|
5
|
+
"userEmail" TEXT,
|
|
6
|
+
"action" TEXT NOT NULL,
|
|
7
|
+
"entityPath" TEXT NOT NULL,
|
|
8
|
+
"status" INTEGER NOT NULL,
|
|
9
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
10
|
+
|
|
11
|
+
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
-- CreateIndex
|
|
15
|
+
CREATE INDEX "AuditLog_createdAt_idx" ON "AuditLog"("createdAt");
|
|
16
|
+
|
|
17
|
+
-- CreateIndex
|
|
18
|
+
CREATE INDEX "AuditLog_userEmail_createdAt_idx" ON "AuditLog"("userEmail", "createdAt");
|
package/template/apps/api/prisma/migrations/20260706031301_standard_cms_platform_pass/migration.sql
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
-- AlterEnum
|
|
2
|
+
ALTER TYPE "UserRole" ADD VALUE 'AUTHOR';
|
|
3
|
+
|
|
4
|
+
-- AlterTable
|
|
5
|
+
ALTER TABLE "CollectionItem" ADD COLUMN "createdById" TEXT,
|
|
6
|
+
ADD COLUMN "locale" TEXT NOT NULL DEFAULT 'en',
|
|
7
|
+
ADD COLUMN "searchText" TEXT NOT NULL DEFAULT '',
|
|
8
|
+
ADD COLUMN "translationOfId" TEXT;
|
|
9
|
+
|
|
10
|
+
-- AlterTable
|
|
11
|
+
ALTER TABLE "Page" ADD COLUMN "createdById" TEXT,
|
|
12
|
+
ADD COLUMN "searchText" TEXT NOT NULL DEFAULT '';
|
|
13
|
+
|
|
14
|
+
-- AlterTable
|
|
15
|
+
ALTER TABLE "SavedBlock" ADD COLUMN "createdById" TEXT;
|
|
16
|
+
|
|
17
|
+
-- AlterTable
|
|
18
|
+
ALTER TABLE "SiteTheme" ADD COLUMN "analyticsCustomScript" TEXT,
|
|
19
|
+
ADD COLUMN "analyticsId" TEXT,
|
|
20
|
+
ADD COLUMN "analyticsProvider" TEXT NOT NULL DEFAULT 'none',
|
|
21
|
+
ADD COLUMN "backgroundColorDark" TEXT,
|
|
22
|
+
ADD COLUMN "darkModeEnabled" BOOLEAN NOT NULL DEFAULT false,
|
|
23
|
+
ADD COLUMN "primaryColorDark" TEXT,
|
|
24
|
+
ADD COLUMN "textColorDark" TEXT;
|
|
25
|
+
|
|
26
|
+
-- CreateTable
|
|
27
|
+
CREATE TABLE "PageTemplate" (
|
|
28
|
+
"id" TEXT NOT NULL,
|
|
29
|
+
"name" TEXT NOT NULL,
|
|
30
|
+
"category" TEXT NOT NULL DEFAULT 'General',
|
|
31
|
+
"thumbnail" TEXT,
|
|
32
|
+
"content" JSONB NOT NULL,
|
|
33
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
34
|
+
|
|
35
|
+
CONSTRAINT "PageTemplate_pkey" PRIMARY KEY ("id")
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
-- AddForeignKey
|
|
39
|
+
ALTER TABLE "Page" ADD CONSTRAINT "Page_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
40
|
+
|
|
41
|
+
-- AddForeignKey
|
|
42
|
+
ALTER TABLE "CollectionItem" ADD CONSTRAINT "CollectionItem_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
43
|
+
|
|
44
|
+
-- AddForeignKey
|
|
45
|
+
ALTER TABLE "CollectionItem" ADD CONSTRAINT "CollectionItem_translationOfId_fkey" FOREIGN KEY ("translationOfId") REFERENCES "CollectionItem"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
46
|
+
|
|
47
|
+
-- AddForeignKey
|
|
48
|
+
ALTER TABLE "SavedBlock" ADD CONSTRAINT "SavedBlock_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { PrismaClient } from "@prisma/client";
|
|
3
|
+
|
|
4
|
+
const prisma = new PrismaClient();
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
const email = "admin@example.com";
|
|
8
|
+
await prisma.user.deleteMany({ where: { email } });
|
|
9
|
+
console.log("Deleted existing admin user");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
main()
|
|
13
|
+
.catch((err) => {
|
|
14
|
+
console.error(err);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
})
|
|
17
|
+
.finally(async () => {
|
|
18
|
+
await prisma.$disconnect();
|
|
19
|
+
});
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
generator client {
|
|
2
|
+
provider = "prisma-client-js"
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
datasource db {
|
|
6
|
+
provider = "postgresql"
|
|
7
|
+
url = env("DATABASE_URL")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
enum UserRole {
|
|
11
|
+
ADMIN
|
|
12
|
+
EDITOR
|
|
13
|
+
AUTHOR
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
enum PageStatus {
|
|
17
|
+
DRAFT
|
|
18
|
+
PUBLISHED
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
model User {
|
|
22
|
+
id String @id @default(cuid())
|
|
23
|
+
email String @unique
|
|
24
|
+
passwordHash String
|
|
25
|
+
role UserRole @default(EDITOR)
|
|
26
|
+
createdAt DateTime @default(now())
|
|
27
|
+
pages Page[] @relation("PageAuthor")
|
|
28
|
+
collectionItems CollectionItem[] @relation("CollectionItemAuthor")
|
|
29
|
+
savedBlocks SavedBlock[] @relation("SavedBlockAuthor")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
model Page {
|
|
33
|
+
id String @id @default(cuid())
|
|
34
|
+
slug String @unique
|
|
35
|
+
title String
|
|
36
|
+
status PageStatus @default(DRAFT)
|
|
37
|
+
content Json
|
|
38
|
+
seoTitle String?
|
|
39
|
+
seoDescription String?
|
|
40
|
+
ogImage String?
|
|
41
|
+
// BCP-47 language tag for this page's content ("en", "es", "ar"...). Slugs stay
|
|
42
|
+
// globally unique — localized pages get their own slug (e.g. "es/servicios") and are
|
|
43
|
+
// linked to their source page via translationOfId so a language switcher can list
|
|
44
|
+
// the whole translation group.
|
|
45
|
+
locale String @default("en")
|
|
46
|
+
translationOfId String?
|
|
47
|
+
translationOf Page? @relation("PageTranslations", fields: [translationOfId], references: [id], onDelete: SetNull)
|
|
48
|
+
translations Page[] @relation("PageTranslations")
|
|
49
|
+
// When set alongside status=PUBLISHED, the page only actually becomes publicly
|
|
50
|
+
// visible once this moment passes — checked lazily at request time rather than via
|
|
51
|
+
// a background job, since "is it visible" is just "status=PUBLISHED AND (no
|
|
52
|
+
// publishAt OR publishAt <= now)".
|
|
53
|
+
publishAt DateTime?
|
|
54
|
+
noIndex Boolean @default(false)
|
|
55
|
+
previewToken String @unique @default(cuid())
|
|
56
|
+
// Denormalized plain-text extraction of `content` (see extractSearchText in
|
|
57
|
+
// packages/shared), kept in sync on every save so Postgres full-text search doesn't
|
|
58
|
+
// need to parse the JSON tree at query time.
|
|
59
|
+
searchText String @default("")
|
|
60
|
+
createdById String?
|
|
61
|
+
createdBy User? @relation("PageAuthor", fields: [createdById], references: [id], onDelete: SetNull)
|
|
62
|
+
createdAt DateTime @default(now())
|
|
63
|
+
updatedAt DateTime @updatedAt
|
|
64
|
+
revisions PageRevision[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A lightweight snapshot taken on every Save, so an editor can look back at and
|
|
68
|
+
// restore an earlier version of a page. Capped and pruned in application code rather
|
|
69
|
+
// than modeled here, to keep the schema simple.
|
|
70
|
+
model PageRevision {
|
|
71
|
+
id String @id @default(cuid())
|
|
72
|
+
pageId String
|
|
73
|
+
page Page @relation(fields: [pageId], references: [id], onDelete: Cascade)
|
|
74
|
+
title String
|
|
75
|
+
content Json
|
|
76
|
+
createdAt DateTime @default(now())
|
|
77
|
+
|
|
78
|
+
@@index([pageId, createdAt])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
model MediaAsset {
|
|
82
|
+
id String @id @default(cuid())
|
|
83
|
+
key String @unique
|
|
84
|
+
url String
|
|
85
|
+
mimeType String
|
|
86
|
+
width Int?
|
|
87
|
+
height Int?
|
|
88
|
+
size Int
|
|
89
|
+
// Original filename kept for search; alt text for accessibility wherever the asset
|
|
90
|
+
// is used without an explicit per-usage alt.
|
|
91
|
+
filename String @default("")
|
|
92
|
+
alt String?
|
|
93
|
+
createdAt DateTime @default(now())
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Old URL → new URL mappings, consulted when a public path doesn't match any page or
|
|
97
|
+
// collection entry — keeps inbound links alive across restructures.
|
|
98
|
+
model Redirect {
|
|
99
|
+
id String @id @default(cuid())
|
|
100
|
+
fromPath String @unique
|
|
101
|
+
toPath String
|
|
102
|
+
permanent Boolean @default(true)
|
|
103
|
+
createdAt DateTime @default(now())
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
model SiteTheme {
|
|
107
|
+
id String @id @default(cuid())
|
|
108
|
+
primaryColor String @default("#2563eb")
|
|
109
|
+
secondaryColor String @default("#0f172a")
|
|
110
|
+
backgroundColor String @default("#ffffff")
|
|
111
|
+
textColor String @default("#111827")
|
|
112
|
+
headingFont String @default("Poppins")
|
|
113
|
+
bodyFont String @default("Inter")
|
|
114
|
+
logoUrl String?
|
|
115
|
+
faviconUrl String?
|
|
116
|
+
siteName String @default("My Site")
|
|
117
|
+
customCss String?
|
|
118
|
+
// Dark-mode token overrides — used only when darkModeEnabled; left blank fields fall
|
|
119
|
+
// back to sensible computed defaults (see ThemeStyleTag) rather than the light colors.
|
|
120
|
+
darkModeEnabled Boolean @default(false)
|
|
121
|
+
primaryColorDark String?
|
|
122
|
+
backgroundColorDark String?
|
|
123
|
+
textColorDark String?
|
|
124
|
+
// "none" | "ga4" | "plausible" | "custom" — kept as a plain string (not a Prisma enum)
|
|
125
|
+
// since it's validated at the Zod boundary and an enum migration is heavier for a
|
|
126
|
+
// field this low-stakes.
|
|
127
|
+
analyticsProvider String @default("none")
|
|
128
|
+
analyticsId String?
|
|
129
|
+
analyticsCustomScript String?
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
model NavItem {
|
|
133
|
+
id String @id @default(cuid())
|
|
134
|
+
label String
|
|
135
|
+
href String
|
|
136
|
+
order Int @default(0)
|
|
137
|
+
parentId String?
|
|
138
|
+
parent NavItem? @relation("NavItemChildren", fields: [parentId], references: [id], onDelete: Cascade)
|
|
139
|
+
children NavItem[] @relation("NavItemChildren")
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// A dynamic content type — Blog, News, Events — whose items share a URL prefix
|
|
143
|
+
// (`/{collection.slug}/{item.slug}`) and are listed by the "Collection List" block.
|
|
144
|
+
model Collection {
|
|
145
|
+
id String @id @default(cuid())
|
|
146
|
+
name String
|
|
147
|
+
slug String @unique
|
|
148
|
+
items CollectionItem[]
|
|
149
|
+
createdAt DateTime @default(now())
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// One entry in a collection (a post, an event...). `content` is a PageNode tree edited
|
|
153
|
+
// with the same visual builder as pages, so items get the full component library.
|
|
154
|
+
model CollectionItem {
|
|
155
|
+
id String @id @default(cuid())
|
|
156
|
+
collectionId String
|
|
157
|
+
collection Collection @relation(fields: [collectionId], references: [id], onDelete: Cascade)
|
|
158
|
+
slug String
|
|
159
|
+
title String
|
|
160
|
+
excerpt String?
|
|
161
|
+
coverImage String?
|
|
162
|
+
content Json
|
|
163
|
+
status PageStatus @default(DRAFT)
|
|
164
|
+
publishedAt DateTime?
|
|
165
|
+
seoTitle String?
|
|
166
|
+
seoDescription String?
|
|
167
|
+
ogImage String?
|
|
168
|
+
noIndex Boolean @default(false)
|
|
169
|
+
searchText String @default("")
|
|
170
|
+
createdById String?
|
|
171
|
+
createdBy User? @relation("CollectionItemAuthor", fields: [createdById], references: [id], onDelete: SetNull)
|
|
172
|
+
// Mirrors Page's locale/translation pattern exactly, but scoped to items within the
|
|
173
|
+
// same collection rather than site-wide, since a "translation of" only makes sense
|
|
174
|
+
// among items sharing a collection.
|
|
175
|
+
locale String @default("en")
|
|
176
|
+
translationOfId String?
|
|
177
|
+
translationOf CollectionItem? @relation("CollectionItemTranslations", fields: [translationOfId], references: [id], onDelete: SetNull)
|
|
178
|
+
translations CollectionItem[] @relation("CollectionItemTranslations")
|
|
179
|
+
createdAt DateTime @default(now())
|
|
180
|
+
updatedAt DateTime @updatedAt
|
|
181
|
+
|
|
182
|
+
@@unique([collectionId, slug])
|
|
183
|
+
@@index([collectionId, status, publishedAt])
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Site-level sections rendered on every page: the global header above each page's own
|
|
187
|
+
// content and the global footer below it. `kind` is "header" or "footer"; `content` is
|
|
188
|
+
// a PageNode root tree (so a global header can be several stacked sections — utility
|
|
189
|
+
// bar + header, say). Disabled rows are kept so switching off doesn't lose the layout.
|
|
190
|
+
model GlobalSection {
|
|
191
|
+
id String @id @default(cuid())
|
|
192
|
+
kind String @unique
|
|
193
|
+
enabled Boolean @default(false)
|
|
194
|
+
content Json
|
|
195
|
+
createdAt DateTime @default(now())
|
|
196
|
+
updatedAt DateTime @updatedAt
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// A section (or whole subtree) an editor saved for reuse. Inserting a block deep-clones
|
|
200
|
+
// its content with fresh node ids, so instances are independent copies (not synced).
|
|
201
|
+
model SavedBlock {
|
|
202
|
+
id String @id @default(cuid())
|
|
203
|
+
name String
|
|
204
|
+
content Json
|
|
205
|
+
createdById String?
|
|
206
|
+
createdBy User? @relation("SavedBlockAuthor", fields: [createdById], references: [id], onDelete: SetNull)
|
|
207
|
+
createdAt DateTime @default(now())
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// A starter PageNode tree an editor can pick when creating a new page instead of
|
|
211
|
+
// starting blank. `content` is a full root PageNode, same shape as Page.content, so
|
|
212
|
+
// applying a template is just cloning it with fresh node ids (cloneWithNewIds).
|
|
213
|
+
model PageTemplate {
|
|
214
|
+
id String @id @default(cuid())
|
|
215
|
+
name String
|
|
216
|
+
category String @default("General")
|
|
217
|
+
thumbnail String?
|
|
218
|
+
content Json
|
|
219
|
+
createdAt DateTime @default(now())
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Who changed what, when — one row per successful mutating API call, captured by
|
|
223
|
+
// middleware rather than per-route calls so nothing is forgotten as routes are added.
|
|
224
|
+
// `action` is "METHOD /path" and `entityPath` the concrete URL hit.
|
|
225
|
+
model AuditLog {
|
|
226
|
+
id String @id @default(cuid())
|
|
227
|
+
userId String?
|
|
228
|
+
userEmail String?
|
|
229
|
+
action String
|
|
230
|
+
entityPath String
|
|
231
|
+
status Int
|
|
232
|
+
createdAt DateTime @default(now())
|
|
233
|
+
|
|
234
|
+
@@index([createdAt])
|
|
235
|
+
@@index([userEmail, createdAt])
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// One row per submission of any "Contact Form" block on the site. `formName` lets
|
|
239
|
+
// multiple distinct forms (e.g. "Contact", "Newsletter") share one table while still
|
|
240
|
+
// being filterable in the admin submissions view. `data` holds whatever fields that
|
|
241
|
+
// particular form instance defined — kept schemaless since form fields are authored
|
|
242
|
+
// per-component, not modeled globally.
|
|
243
|
+
model FormSubmission {
|
|
244
|
+
id String @id @default(cuid())
|
|
245
|
+
formName String
|
|
246
|
+
pageSlug String
|
|
247
|
+
data Json
|
|
248
|
+
createdAt DateTime @default(now())
|
|
249
|
+
|
|
250
|
+
@@index([formName, createdAt])
|
|
251
|
+
}
|