From 91a20a89d6b82e82e1c1da0d8d8f144aff126db9 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Wed, 26 Aug 2026 15:28:41 +0330 Subject: [PATCH] Add public AI prompt templates for websites. Manage reusable templates in Super Admin, attach editable per-site prompts with Meshkee API public links that require no auth. Co-authored-by: Cursor --- .../migrations/075_ai_prompt_templates.sql | 32 ++ .../076_ai_prompt_templates_global.sql | 55 +++ .../077_domain_ai_prompt_instances.sql | 73 ++++ prisma/schema.prisma | 32 ++ src/ai-prompts/ai-prompts.controller.ts | 112 +++++++ src/ai-prompts/ai-prompts.module.ts | 11 + src/ai-prompts/ai-prompts.service.ts | 315 ++++++++++++++++++ .../dto/create-ai-prompt-template.dto.ts | 12 + .../dto/create-domain-ai-prompt.dto.ts | 26 ++ .../dto/update-ai-prompt-template.dto.ts | 14 + .../dto/update-domain-ai-prompt.dto.ts | 14 + src/app.module.ts | 2 + 12 files changed, 698 insertions(+) create mode 100644 database/migrations/075_ai_prompt_templates.sql create mode 100644 database/migrations/076_ai_prompt_templates_global.sql create mode 100644 database/migrations/077_domain_ai_prompt_instances.sql create mode 100644 src/ai-prompts/ai-prompts.controller.ts create mode 100644 src/ai-prompts/ai-prompts.module.ts create mode 100644 src/ai-prompts/ai-prompts.service.ts create mode 100644 src/ai-prompts/dto/create-ai-prompt-template.dto.ts create mode 100644 src/ai-prompts/dto/create-domain-ai-prompt.dto.ts create mode 100644 src/ai-prompts/dto/update-ai-prompt-template.dto.ts create mode 100644 src/ai-prompts/dto/update-domain-ai-prompt.dto.ts diff --git a/database/migrations/075_ai_prompt_templates.sql b/database/migrations/075_ai_prompt_templates.sql new file mode 100644 index 0000000..624d700 --- /dev/null +++ b/database/migrations/075_ai_prompt_templates.sql @@ -0,0 +1,32 @@ +-- Super Admin AI prompt templates (per website / domain), with public share ids. + +CREATE TABLE IF NOT EXISTS ai_prompt_templates ( + id BIGSERIAL PRIMARY KEY, + domain_id BIGINT NOT NULL REFERENCES domains (id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + body TEXT NOT NULL, + public_id VARCHAR(32) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT ai_prompt_templates_public_id_unique UNIQUE (public_id) +); + +CREATE INDEX IF NOT EXISTS idx_ai_prompt_templates_domain_id + ON ai_prompt_templates (domain_id); + +CREATE INDEX IF NOT EXISTS idx_ai_prompt_templates_created_at + ON ai_prompt_templates (created_at DESC); + +CREATE OR REPLACE FUNCTION ai_prompt_templates_set_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS ai_prompt_templates_set_updated_at ON ai_prompt_templates; +CREATE TRIGGER ai_prompt_templates_set_updated_at + BEFORE UPDATE ON ai_prompt_templates + FOR EACH ROW + EXECUTE FUNCTION ai_prompt_templates_set_updated_at(); diff --git a/database/migrations/076_ai_prompt_templates_global.sql b/database/migrations/076_ai_prompt_templates_global.sql new file mode 100644 index 0000000..7822fd0 --- /dev/null +++ b/database/migrations/076_ai_prompt_templates_global.sql @@ -0,0 +1,55 @@ +-- Make AI prompt templates global; assign to websites via join + pack public id. + +CREATE TABLE IF NOT EXISTS domain_ai_prompt_templates ( + domain_id BIGINT NOT NULL REFERENCES domains (id) ON DELETE CASCADE, + template_id BIGINT NOT NULL REFERENCES ai_prompt_templates (id) ON DELETE CASCADE, + sort_order INT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (domain_id, template_id) +); + +CREATE INDEX IF NOT EXISTS idx_domain_ai_prompt_templates_template_id + ON domain_ai_prompt_templates (template_id); + +ALTER TABLE domains + ADD COLUMN IF NOT EXISTS ai_prompts_public_id VARCHAR(32); + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'domains_ai_prompts_public_id_unique' + ) THEN + ALTER TABLE domains + ADD CONSTRAINT domains_ai_prompts_public_id_unique UNIQUE (ai_prompts_public_id); + END IF; +END $$; + +-- Preserve any existing per-domain template links (from 075). +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'ai_prompt_templates' AND column_name = 'domain_id' + ) THEN + INSERT INTO domain_ai_prompt_templates (domain_id, template_id, sort_order) + SELECT domain_id, id, 0 + FROM ai_prompt_templates + WHERE domain_id IS NOT NULL + ON CONFLICT DO NOTHING; + + UPDATE domains d + SET ai_prompts_public_id = t.public_id + FROM ( + SELECT DISTINCT ON (domain_id) domain_id, public_id + FROM ai_prompt_templates + WHERE public_id IS NOT NULL + ORDER BY domain_id, id + ) t + WHERE d.id = t.domain_id + AND d.ai_prompts_public_id IS NULL; + END IF; +END $$; + +ALTER TABLE ai_prompt_templates DROP COLUMN IF EXISTS domain_id; +ALTER TABLE ai_prompt_templates DROP COLUMN IF EXISTS public_id; diff --git a/database/migrations/077_domain_ai_prompt_instances.sql b/database/migrations/077_domain_ai_prompt_instances.sql new file mode 100644 index 0000000..46dc17c --- /dev/null +++ b/database/migrations/077_domain_ai_prompt_instances.sql @@ -0,0 +1,73 @@ +-- Per-website AI prompt instances (editable copies) with their own public links. + +CREATE TABLE IF NOT EXISTS domain_ai_prompts ( + id BIGSERIAL PRIMARY KEY, + domain_id BIGINT NOT NULL REFERENCES domains (id) ON DELETE CASCADE, + source_template_id BIGINT REFERENCES ai_prompt_templates (id) ON DELETE SET NULL, + name VARCHAR(255) NOT NULL, + body TEXT NOT NULL, + public_id VARCHAR(32) NOT NULL, + sort_order INT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT domain_ai_prompts_public_id_unique UNIQUE (public_id) +); + +CREATE INDEX IF NOT EXISTS idx_domain_ai_prompts_domain_id + ON domain_ai_prompts (domain_id, sort_order, id); + +CREATE INDEX IF NOT EXISTS idx_domain_ai_prompts_source_template_id + ON domain_ai_prompts (source_template_id); + +CREATE OR REPLACE FUNCTION domain_ai_prompts_set_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS domain_ai_prompts_set_updated_at ON domain_ai_prompts; +CREATE TRIGGER domain_ai_prompts_set_updated_at + BEFORE UPDATE ON domain_ai_prompts + FOR EACH ROW + EXECUTE FUNCTION domain_ai_prompts_set_updated_at(); + +-- Copy existing assignments into editable instances (one public id each). +DO $$ +DECLARE + r RECORD; + pid TEXT; +BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_name = 'domain_ai_prompt_templates' + ) THEN + FOR r IN + SELECT + l.domain_id, + l.template_id, + l.sort_order, + t.name, + t.body + FROM domain_ai_prompt_templates l + JOIN ai_prompt_templates t ON t.id = l.template_id + ORDER BY l.domain_id, l.sort_order, l.template_id + LOOP + LOOP + pid := encode(gen_random_bytes(12), 'hex'); + EXIT WHEN NOT EXISTS ( + SELECT 1 FROM domain_ai_prompts WHERE public_id = pid + ); + END LOOP; + + INSERT INTO domain_ai_prompts ( + domain_id, source_template_id, name, body, public_id, sort_order + ) VALUES ( + r.domain_id, r.template_id, r.name, r.body, pid, r.sort_order + ); + END LOOP; + END IF; +END $$; + +DROP TABLE IF EXISTS domain_ai_prompt_templates; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e188a26..8db1057 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -188,7 +188,9 @@ model Domain { lastDeployStatus String? @map("last_deploy_status") @db.VarChar(32) deploySlug String? @map("deploy_slug") @db.VarChar(64) gitRepoUrl String? @map("git_repo_url") @db.VarChar(512) + aiPromptsPublicId String? @unique(map: "domains_ai_prompts_public_id_unique") @map("ai_prompts_public_id") @db.VarChar(32) business Business @relation(fields: [businessId], references: [id], onDelete: Cascade, onUpdate: NoAction) + aiPrompts DomainAiPrompt[] @@index([businessId], map: "idx_domains_business_id") @@index([host], map: "idx_domains_host") @@ -196,6 +198,36 @@ model Domain { @@map("domains") } +model AiPromptTemplate { + id BigInt @id @default(autoincrement()) + name String @db.VarChar(255) + body String + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) + domainPrompts DomainAiPrompt[] + + @@index([createdAt(sort: Desc)], map: "idx_ai_prompt_templates_created_at") + @@map("ai_prompt_templates") +} + +model DomainAiPrompt { + id BigInt @id @default(autoincrement()) + domainId BigInt @map("domain_id") + sourceTemplateId BigInt? @map("source_template_id") + name String @db.VarChar(255) + body String + publicId String @unique(map: "domain_ai_prompts_public_id_unique") @map("public_id") @db.VarChar(32) + sortOrder Int @default(0) @map("sort_order") + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) + domain Domain @relation(fields: [domainId], references: [id], onDelete: Cascade, onUpdate: NoAction) + sourceTemplate AiPromptTemplate? @relation(fields: [sourceTemplateId], references: [id], onDelete: SetNull, onUpdate: NoAction) + + @@index([domainId, sortOrder, id], map: "idx_domain_ai_prompts_domain_id") + @@index([sourceTemplateId], map: "idx_domain_ai_prompts_source_template_id") + @@map("domain_ai_prompts") +} + model BusinessUser { id BigInt @id @default(autoincrement()) businessId BigInt @map("business_id") diff --git a/src/ai-prompts/ai-prompts.controller.ts b/src/ai-prompts/ai-prompts.controller.ts new file mode 100644 index 0000000..cf522f8 --- /dev/null +++ b/src/ai-prompts/ai-prompts.controller.ts @@ -0,0 +1,112 @@ +import { + Body, + Controller, + Delete, + Get, + Header, + HttpCode, + Param, + Patch, + Post, + UseGuards, +} from '@nestjs/common'; +import { AuthUser } from '../auth/auth.types'; +import { CurrentUser } from '../auth/decorators/current-user.decorator'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { AiPromptsService } from './ai-prompts.service'; +import { CreateAiPromptTemplateDto } from './dto/create-ai-prompt-template.dto'; +import { CreateDomainAiPromptDto } from './dto/create-domain-ai-prompt.dto'; +import { UpdateAiPromptTemplateDto } from './dto/update-ai-prompt-template.dto'; +import { UpdateDomainAiPromptDto } from './dto/update-domain-ai-prompt.dto'; + +@Controller() +export class AiPromptsController { + constructor(private readonly service: AiPromptsService) {} + + @Get('ai-prompt-templates') + @UseGuards(JwtAuthGuard) + list(@CurrentUser() user: AuthUser) { + return this.service.list(user); + } + + @Post('ai-prompt-templates') + @UseGuards(JwtAuthGuard) + create( + @Body() dto: CreateAiPromptTemplateDto, + @CurrentUser() user: AuthUser, + ) { + return this.service.create(dto, user); + } + + @Patch('ai-prompt-templates/:templateId') + @UseGuards(JwtAuthGuard) + update( + @Param('templateId') templateId: string, + @Body() dto: UpdateAiPromptTemplateDto, + @CurrentUser() user: AuthUser, + ) { + return this.service.update(templateId, dto, user); + } + + @Delete('ai-prompt-templates/:templateId') + @HttpCode(200) + @UseGuards(JwtAuthGuard) + remove( + @Param('templateId') templateId: string, + @CurrentUser() user: AuthUser, + ) { + return this.service.remove(templateId, user); + } + + @Get('domains/:domainId/ai-prompts') + @UseGuards(JwtAuthGuard) + listDomainPrompts( + @Param('domainId') domainId: string, + @CurrentUser() user: AuthUser, + ) { + return this.service.listDomainPrompts(domainId, user); + } + + @Post('domains/:domainId/ai-prompts') + @UseGuards(JwtAuthGuard) + createDomainPrompt( + @Param('domainId') domainId: string, + @Body() dto: CreateDomainAiPromptDto, + @CurrentUser() user: AuthUser, + ) { + return this.service.createDomainPrompt(domainId, dto, user); + } + + @Patch('domains/:domainId/ai-prompts/:promptId') + @UseGuards(JwtAuthGuard) + updateDomainPrompt( + @Param('domainId') domainId: string, + @Param('promptId') promptId: string, + @Body() dto: UpdateDomainAiPromptDto, + @CurrentUser() user: AuthUser, + ) { + return this.service.updateDomainPrompt(domainId, promptId, dto, user); + } + + @Delete('domains/:domainId/ai-prompts/:promptId') + @HttpCode(200) + @UseGuards(JwtAuthGuard) + removeDomainPrompt( + @Param('domainId') domainId: string, + @Param('promptId') promptId: string, + @CurrentUser() user: AuthUser, + ) { + return this.service.removeDomainPrompt(domainId, promptId, user); + } + + @Get('public/ai-prompts/:publicId') + getPublic(@Param('publicId') publicId: string) { + return this.service.getPublic(publicId); + } + + @Get('public/ai-prompts/:publicId/raw') + @Header('Content-Type', 'text/plain; charset=utf-8') + getPublicRaw(@Param('publicId') publicId: string) { + return this.service.getPublicText(publicId); + } +} diff --git a/src/ai-prompts/ai-prompts.module.ts b/src/ai-prompts/ai-prompts.module.ts new file mode 100644 index 0000000..e2b3b36 --- /dev/null +++ b/src/ai-prompts/ai-prompts.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { AuthModule } from '../auth/auth.module'; +import { AiPromptsController } from './ai-prompts.controller'; +import { AiPromptsService } from './ai-prompts.service'; + +@Module({ + imports: [AuthModule], + controllers: [AiPromptsController], + providers: [AiPromptsService], +}) +export class AiPromptsModule {} diff --git a/src/ai-prompts/ai-prompts.service.ts b/src/ai-prompts/ai-prompts.service.ts new file mode 100644 index 0000000..c63a13d --- /dev/null +++ b/src/ai-prompts/ai-prompts.service.ts @@ -0,0 +1,315 @@ +import { + BadRequestException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { randomBytes } from 'crypto'; +import { AuthUser } from '../auth/auth.types'; +import { PermissionsService } from '../auth/permissions.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { CreateAiPromptTemplateDto } from './dto/create-ai-prompt-template.dto'; +import { CreateDomainAiPromptDto } from './dto/create-domain-ai-prompt.dto'; +import { UpdateAiPromptTemplateDto } from './dto/update-ai-prompt-template.dto'; +import { UpdateDomainAiPromptDto } from './dto/update-domain-ai-prompt.dto'; + +@Injectable() +export class AiPromptsService { + constructor( + private readonly prisma: PrismaService, + private readonly permissions: PermissionsService, + private readonly config: ConfigService, + ) {} + + private async assertSuperAdmin(actor: AuthUser) { + if (!(await this.permissions.isSuperAdmin(actor.id))) { + throw new ForbiddenException('Super admin access required'); + } + } + + private async generateUniquePromptPublicId(): Promise { + for (let attempt = 0; attempt < 12; attempt += 1) { + const candidate = randomBytes(12).toString('hex'); + const existing = await this.prisma.domainAiPrompt.findUnique({ + where: { publicId: candidate }, + select: { id: true }, + }); + if (!existing) return candidate; + } + throw new BadRequestException('Unable to allocate a public prompt id'); + } + + private publicUrl(publicId: string): string { + const base = + this.config.get('API_PUBLIC_BASE_URL')?.replace(/\/$/, '') || + 'https://api.meshkee.com'; + return `${base}/api/v1/public/ai-prompts/${publicId}`; + } + + private serializeTemplate(row: { + id: bigint; + name: string; + body: string; + createdAt: Date; + updatedAt: Date; + }) { + return { + id: row.id.toString(), + name: row.name, + body: row.body, + createdAt: row.createdAt.toISOString(), + updatedAt: row.updatedAt.toISOString(), + }; + } + + private serializeDomainPrompt(row: { + id: bigint; + domainId: bigint; + sourceTemplateId: bigint | null; + name: string; + body: string; + publicId: string; + sortOrder: number; + createdAt: Date; + updatedAt: Date; + }) { + return { + id: row.id.toString(), + domainId: row.domainId.toString(), + sourceTemplateId: row.sourceTemplateId?.toString() ?? null, + name: row.name, + body: row.body, + publicId: row.publicId, + publicUrl: this.publicUrl(row.publicId), + sortOrder: row.sortOrder, + createdAt: row.createdAt.toISOString(), + updatedAt: row.updatedAt.toISOString(), + }; + } + + async list(actor: AuthUser) { + await this.assertSuperAdmin(actor); + const items = await this.prisma.aiPromptTemplate.findMany({ + orderBy: [{ createdAt: 'desc' }, { id: 'desc' }], + }); + return { items: items.map((row) => this.serializeTemplate(row)) }; + } + + async create(dto: CreateAiPromptTemplateDto, actor: AuthUser) { + await this.assertSuperAdmin(actor); + const created = await this.prisma.aiPromptTemplate.create({ + data: { + name: dto.name.trim(), + body: dto.body.trim(), + }, + }); + return this.serializeTemplate(created); + } + + async update( + templateIdRaw: string, + dto: UpdateAiPromptTemplateDto, + actor: AuthUser, + ) { + await this.assertSuperAdmin(actor); + const id = BigInt(templateIdRaw); + const existing = await this.prisma.aiPromptTemplate.findUnique({ + where: { id }, + select: { id: true }, + }); + if (!existing) throw new NotFoundException('Template not found'); + + const updated = await this.prisma.aiPromptTemplate.update({ + where: { id }, + data: { + ...(dto.name !== undefined ? { name: dto.name.trim() } : {}), + ...(dto.body !== undefined ? { body: dto.body.trim() } : {}), + }, + }); + return this.serializeTemplate(updated); + } + + async remove(templateIdRaw: string, actor: AuthUser) { + await this.assertSuperAdmin(actor); + const id = BigInt(templateIdRaw); + try { + await this.prisma.aiPromptTemplate.delete({ where: { id } }); + } catch { + throw new NotFoundException('Template not found'); + } + return { ok: true }; + } + + async listDomainPrompts(domainIdRaw: string, actor: AuthUser) { + await this.assertSuperAdmin(actor); + const domainId = BigInt(domainIdRaw); + const domain = await this.prisma.domain.findUnique({ + where: { id: domainId }, + select: { + id: true, + host: true, + business: { select: { name: true } }, + }, + }); + if (!domain) throw new NotFoundException('Website not found'); + + const items = await this.prisma.domainAiPrompt.findMany({ + where: { domainId }, + orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }], + }); + + return { + domainId: domain.id.toString(), + host: domain.host, + businessName: domain.business.name, + items: items.map((row) => this.serializeDomainPrompt(row)), + }; + } + + async createDomainPrompt( + domainIdRaw: string, + dto: CreateDomainAiPromptDto, + actor: AuthUser, + ) { + await this.assertSuperAdmin(actor); + const domainId = BigInt(domainIdRaw); + const domain = await this.prisma.domain.findUnique({ + where: { id: domainId }, + select: { id: true }, + }); + if (!domain) throw new NotFoundException('Website not found'); + + let sourceTemplateId: bigint | null = null; + if (dto.sourceTemplateId !== undefined) { + sourceTemplateId = BigInt(dto.sourceTemplateId); + const template = await this.prisma.aiPromptTemplate.findUnique({ + where: { id: sourceTemplateId }, + select: { id: true }, + }); + if (!template) throw new NotFoundException('Template not found'); + } + + const maxSort = await this.prisma.domainAiPrompt.aggregate({ + where: { domainId }, + _max: { sortOrder: true }, + }); + + const created = await this.prisma.domainAiPrompt.create({ + data: { + domainId, + sourceTemplateId, + name: dto.name.trim(), + body: dto.body.trim(), + publicId: await this.generateUniquePromptPublicId(), + sortOrder: (maxSort._max.sortOrder ?? -1) + 1, + }, + }); + return this.serializeDomainPrompt(created); + } + + async updateDomainPrompt( + domainIdRaw: string, + promptIdRaw: string, + dto: UpdateDomainAiPromptDto, + actor: AuthUser, + ) { + await this.assertSuperAdmin(actor); + const domainId = BigInt(domainIdRaw); + const id = BigInt(promptIdRaw); + const existing = await this.prisma.domainAiPrompt.findFirst({ + where: { id, domainId }, + select: { id: true }, + }); + if (!existing) throw new NotFoundException('Prompt not found'); + + const updated = await this.prisma.domainAiPrompt.update({ + where: { id }, + data: { + ...(dto.name !== undefined ? { name: dto.name.trim() } : {}), + ...(dto.body !== undefined ? { body: dto.body.trim() } : {}), + }, + }); + return this.serializeDomainPrompt(updated); + } + + async removeDomainPrompt( + domainIdRaw: string, + promptIdRaw: string, + actor: AuthUser, + ) { + await this.assertSuperAdmin(actor); + const domainId = BigInt(domainIdRaw); + const id = BigInt(promptIdRaw); + const existing = await this.prisma.domainAiPrompt.findFirst({ + where: { id, domainId }, + select: { id: true }, + }); + if (!existing) throw new NotFoundException('Prompt not found'); + await this.prisma.domainAiPrompt.delete({ where: { id } }); + return { ok: true }; + } + + async getPublic(publicIdRaw: string) { + const publicId = publicIdRaw.trim(); + if (!/^[a-f0-9]{16,32}$/i.test(publicId)) { + throw new NotFoundException('Prompt not found'); + } + + const prompt = await this.prisma.domainAiPrompt.findUnique({ + where: { publicId }, + include: { + domain: { + select: { + host: true, + business: { select: { name: true } }, + }, + }, + }, + }); + if (prompt) { + return { + name: prompt.name, + body: prompt.body, + website: prompt.domain.host, + businessName: prompt.domain.business.name, + }; + } + + // Backward compatible: old domain pack public ids. + const domain = await this.prisma.domain.findFirst({ + where: { aiPromptsPublicId: publicId }, + select: { + host: true, + business: { select: { name: true } }, + aiPrompts: { + orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }], + }, + }, + }); + if (!domain) throw new NotFoundException('Prompt not found'); + + return { + website: domain.host, + businessName: domain.business.name, + templates: domain.aiPrompts.map((item) => ({ + name: item.name, + body: item.body, + })), + }; + } + + async getPublicText(publicIdRaw: string): Promise { + const data = await this.getPublic(publicIdRaw); + if ('body' in data && typeof data.body === 'string') { + return data.body; + } + const pack = data as { + templates: Array<{ name: string; body: string }>; + }; + if (!pack.templates?.length) return ''; + return pack.templates + .map((t) => `# ${t.name}\n\n${t.body}`) + .join('\n\n---\n\n'); + } +} diff --git a/src/ai-prompts/dto/create-ai-prompt-template.dto.ts b/src/ai-prompts/dto/create-ai-prompt-template.dto.ts new file mode 100644 index 0000000..1da7293 --- /dev/null +++ b/src/ai-prompts/dto/create-ai-prompt-template.dto.ts @@ -0,0 +1,12 @@ +import { IsString, MaxLength, MinLength } from 'class-validator'; + +export class CreateAiPromptTemplateDto { + @IsString() + @MinLength(1) + @MaxLength(255) + name!: string; + + @IsString() + @MinLength(1) + body!: string; +} diff --git a/src/ai-prompts/dto/create-domain-ai-prompt.dto.ts b/src/ai-prompts/dto/create-domain-ai-prompt.dto.ts new file mode 100644 index 0000000..726bfcb --- /dev/null +++ b/src/ai-prompts/dto/create-domain-ai-prompt.dto.ts @@ -0,0 +1,26 @@ +import { Type } from 'class-transformer'; +import { + IsInt, + IsOptional, + IsPositive, + IsString, + MaxLength, + MinLength, +} from 'class-validator'; + +export class CreateDomainAiPromptDto { + @IsString() + @MinLength(1) + @MaxLength(255) + name!: string; + + @IsString() + @MinLength(1) + body!: string; + + @IsOptional() + @Type(() => Number) + @IsInt() + @IsPositive() + sourceTemplateId?: number; +} diff --git a/src/ai-prompts/dto/update-ai-prompt-template.dto.ts b/src/ai-prompts/dto/update-ai-prompt-template.dto.ts new file mode 100644 index 0000000..b8f6395 --- /dev/null +++ b/src/ai-prompts/dto/update-ai-prompt-template.dto.ts @@ -0,0 +1,14 @@ +import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; + +export class UpdateAiPromptTemplateDto { + @IsOptional() + @IsString() + @MinLength(1) + @MaxLength(255) + name?: string; + + @IsOptional() + @IsString() + @MinLength(1) + body?: string; +} diff --git a/src/ai-prompts/dto/update-domain-ai-prompt.dto.ts b/src/ai-prompts/dto/update-domain-ai-prompt.dto.ts new file mode 100644 index 0000000..d947897 --- /dev/null +++ b/src/ai-prompts/dto/update-domain-ai-prompt.dto.ts @@ -0,0 +1,14 @@ +import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; + +export class UpdateDomainAiPromptDto { + @IsOptional() + @IsString() + @MinLength(1) + @MaxLength(255) + name?: string; + + @IsOptional() + @IsString() + @MinLength(1) + body?: string; +} diff --git a/src/app.module.ts b/src/app.module.ts index 10b6cdb..b89359b 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -39,6 +39,7 @@ import { InternalSslModule } from './internal-ssl/internal-ssl.module'; import { WebsiteDocsModule } from './website-docs/website-docs.module'; import { WebsiteAnalyticsModule } from './website-analytics/website-analytics.module'; import { InvoicesModule } from './invoices/invoices.module'; +import { AiPromptsModule } from './ai-prompts/ai-prompts.module'; import { LegacyMysqlModule } from './legacy-mysql/legacy-mysql.module'; import { PublicSmsModule } from './public-sms/public-sms.module'; import { PaymentsModule } from './payments/payments.module'; @@ -88,6 +89,7 @@ import { SitemapModule } from './sitemap/sitemap.module'; WebsiteAnalyticsModule, WebsiteDocsModule, InvoicesModule, + AiPromptsModule, PublicSmsModule, SitemapModule, ],