feat(sprint-5.5): add NestJS API, BlockMetaBar, block components + fix Vercel build
- Add vercel.json to build only apps/web (fix Vercel build failure) - NestJS API: BlocksModule, BlocksController, BlocksService with Prisma 7 - PostgreSQL migration: Block model (path, version, isInPreview) - BlockMetaBar component: inline version edit, API fetch with offline fallback - New block components: CeoBlock, ContactFormsBlock, FooterBlock, NewsBlock, ReviewsBlock - PreviewClient: fetch isInPreview from API, block visibility toggle - Pages updated: hero, doctors, ceo, contact-forms, contact, news, reviews - docker-compose: PostgreSQL on port 5434 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -24,8 +24,11 @@
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@prisma/adapter-pg": "^7.5.0",
|
||||
"@prisma/client": "^7.5.0",
|
||||
"@types/pg": "^8.20.0",
|
||||
"dotenv": "^17.3.1",
|
||||
"pg": "^8.20.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
@@ -52,9 +55,13 @@
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "ts-node prisma/seed.ts"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
|
||||
@@ -7,6 +7,7 @@ export default defineConfig({
|
||||
schema: "prisma/schema.prisma",
|
||||
migrations: {
|
||||
path: "prisma/migrations",
|
||||
seed: "tsx prisma/seed.ts",
|
||||
},
|
||||
datasource: {
|
||||
url: process.env["DATABASE_URL"],
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Role" AS ENUM ('viewer', 'editor');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ComponentStatus" AS ENUM ('draft', 'review', 'approved');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"passwordHash" TEXT NOT NULL,
|
||||
"role" "Role" NOT NULL DEFAULT 'viewer',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ExperimentalComponent" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"baseComponent" TEXT NOT NULL,
|
||||
"attributes" JSONB NOT NULL,
|
||||
"status" "ComponentStatus" NOT NULL DEFAULT 'draft',
|
||||
"authorId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ExperimentalComponent_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Block" (
|
||||
"id" TEXT NOT NULL,
|
||||
"path" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"version" TEXT NOT NULL,
|
||||
"isInPreview" BOOLEAN NOT NULL DEFAULT false,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Block_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Block_path_key" ON "Block"("path");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ExperimentalComponent" ADD CONSTRAINT "ExperimentalComponent_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -1,6 +1,5 @@
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../generated/prisma"
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
@@ -40,3 +39,13 @@ model ExperimentalComponent {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Block {
|
||||
id String @id @default(uuid())
|
||||
path String @unique
|
||||
name String
|
||||
version String
|
||||
isInPreview Boolean @default(false)
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'dotenv/config';
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
const BLOCKS = [
|
||||
{ path: '/components/navigation', name: 'Шапка / Навигация', version: 'v1.0', isInPreview: true },
|
||||
{ path: '/blocks/hero', name: 'Hero-баннер', version: 'v1.1', isInPreview: true },
|
||||
{ path: '/blocks/ceo', name: 'Вводный текст (CEO-блок)', version: 'v0.1', isInPreview: false },
|
||||
{ path: '/blocks/doctors', name: 'Наши врачи', version: 'v1.1', isInPreview: true },
|
||||
{ path: '/blocks/reviews', name: 'Отзывы', version: 'v0.1', isInPreview: false },
|
||||
{ path: '/blocks/contact-forms', name: 'Формы записи', version: 'v0.1', isInPreview: false },
|
||||
{ path: '/blocks/news', name: 'Новости', version: 'v0.1', isInPreview: false },
|
||||
{ path: '/blocks/contact', name: 'Подвал / Контакт', version: 'v0.1', isInPreview: false },
|
||||
];
|
||||
|
||||
async function main() {
|
||||
for (const block of BLOCKS) {
|
||||
await prisma.block.upsert({
|
||||
where: { path: block.path },
|
||||
update: {},
|
||||
create: block,
|
||||
});
|
||||
}
|
||||
console.log(`Seeded ${BLOCKS.length} blocks`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { BlocksModule } from './blocks/blocks.module';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
imports: [BlocksModule],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Controller, Get, Patch, Query, Body } from '@nestjs/common';
|
||||
import { BlocksService } from './blocks.service';
|
||||
|
||||
@Controller('blocks')
|
||||
export class BlocksController {
|
||||
constructor(private blocks: BlocksService) {}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.blocks.findAll();
|
||||
}
|
||||
|
||||
@Get('by-path')
|
||||
findByPath(@Query('path') path: string) {
|
||||
return this.blocks.findByPath(path);
|
||||
}
|
||||
|
||||
@Patch('by-path')
|
||||
update(
|
||||
@Query('path') path: string,
|
||||
@Body() body: { version?: string; isInPreview?: boolean },
|
||||
) {
|
||||
return this.blocks.update(path, body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BlocksController } from './blocks.controller';
|
||||
import { BlocksService } from './blocks.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
@Module({
|
||||
controllers: [BlocksController],
|
||||
providers: [BlocksService, PrismaService],
|
||||
})
|
||||
export class BlocksModule {}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class BlocksService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
findAll() {
|
||||
return this.prisma.block.findMany({ orderBy: { createdAt: 'asc' } });
|
||||
}
|
||||
|
||||
async findByPath(path: string) {
|
||||
const existing = await this.prisma.block.findUnique({ where: { path } });
|
||||
if (existing) return existing;
|
||||
return this.prisma.block.create({
|
||||
data: { path, name: path, version: 'v0.1', isInPreview: false },
|
||||
});
|
||||
}
|
||||
|
||||
update(path: string, data: { version?: string; isInPreview?: boolean }) {
|
||||
return this.prisma.block.update({ where: { path }, data });
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'dotenv/config';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.enableCors({ origin: 'http://localhost:3001' });
|
||||
await app.listen(process.env.PORT ?? 3000);
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||
constructor() {
|
||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||||
super({ adapter });
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.$disconnect();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user