Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { FlowsModule } from './flows/flows.module';
import { TriggersModule } from './triggers/triggers.module';
import { ExecutionsModule } from './executions/executions.module';
import { GatewayModule } from './gateway/gateway.module';
import { TemplatesModule } from './templates/templates.module';

@Module({
imports: [
Expand Down Expand Up @@ -40,6 +41,7 @@ import { GatewayModule } from './gateway/gateway.module';
TriggersModule,
ExecutionsModule,
GatewayModule,
TemplatesModule,
],
providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard },
Expand Down
41 changes: 41 additions & 0 deletions apps/api/src/templates/template.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
import type { FlowGraph } from '../flows/entities/flow.entity';

export const TemplateCategory = {
AI: 'ai',
DATA: 'data',
AUTOMATION: 'automation',
MONITORING: 'monitoring',
COMMUNICATION: 'communication',
} as const;
export type TemplateCategory = typeof TemplateCategory[keyof typeof TemplateCategory];

@Entity('templates')
export class Template {
@PrimaryGeneratedColumn('uuid')
id!: string;

@Column()
name!: string;

@Column({ type: 'text' })
description!: string;

@Column({ type: 'varchar' })
category!: TemplateCategory;

@Column({ nullable: true, type: 'varchar' })
icon!: string | null;

@Column({ type: 'jsonb' })
graph!: FlowGraph;

@Column({ default: 0 })
useCount!: number;

@Column({ default: true })
isBuiltin!: boolean;

@CreateDateColumn()
createdAt!: Date;
}
32 changes: 32 additions & 0 deletions apps/api/src/templates/templates.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Controller, Get, Post, Param, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { TemplatesService } from './templates.service';
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
import { CurrentWorkspaceId } from '../common/decorators/current-user.decorator';
import type { TemplateCategory } from './template.entity';

@ApiTags('Templates')
@Controller('templates')
export class TemplatesController {
constructor(private readonly service: TemplatesService) {}

@Get()
findAll(@Query('category') category?: TemplateCategory) {
return this.service.findAll(category);
}

@Get(':id')
findOne(@Param('id') id: string) {
return this.service.findOne(id);
}

@Post(':id/use')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
useTemplate(
@Param('id') id: string,
@CurrentWorkspaceId() workspaceId: string,
) {
return this.service.useTemplate(id, workspaceId);
}
}
13 changes: 13 additions & 0 deletions apps/api/src/templates/templates.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Template } from './template.entity';
import { TemplatesService } from './templates.service';
import { TemplatesController } from './templates.controller';
import { FlowsModule } from '../flows/flows.module';

@Module({
imports: [TypeOrmModule.forFeature([Template]), FlowsModule],
providers: [TemplatesService],
controllers: [TemplatesController],
})
export class TemplatesModule {}
106 changes: 106 additions & 0 deletions apps/api/src/templates/templates.seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { TemplateCategory } from './template.entity';
import type { FlowGraph } from '../flows/entities/flow.entity';

interface SeedTemplate {
name: string;
description: string;
category: TemplateCategory;
icon: string;
graph: FlowGraph;
isBuiltin: boolean;
}

export const BUILTIN_TEMPLATES: SeedTemplate[] = [
{
name: 'Web Scraper → Summarize → Email',
description: 'Scrape a webpage, summarize the content with an LLM, and send the summary by email.',
category: TemplateCategory.AI,
icon: '🌐',
isBuiltin: true,
graph: {
nodes: [
{ id: 'n1', type: 'web-scraper', position: { x: 100, y: 200 }, data: { label: 'Web Scraper', config: { url: '', selector: 'body' } } },
{ id: 'n2', type: 'ai-llm', position: { x: 400, y: 200 }, data: { label: 'Summarize', config: { prompt: 'Summarize this content in 3 bullet points: {{input}}', model: 'gpt-4o-mini' } } },
{ id: 'n3', type: 'email-sender', position: { x: 700, y: 200 }, data: { label: 'Send Email', config: { to: '', subject: 'Daily Summary' } } },
],
edges: [
{ id: 'e1', source: 'n1', target: 'n2' },
{ id: 'e2', source: 'n2', target: 'n3' },
],
},
},
{
name: 'GitHub PR Review',
description: 'Fetch open pull requests from a GitHub repo and generate an AI code review for each.',
category: TemplateCategory.AI,
icon: '🔍',
isBuiltin: true,
graph: {
nodes: [
{ id: 'n1', type: 'api-caller', position: { x: 100, y: 200 }, data: { label: 'Fetch PRs', config: { url: 'https://api.github.com/repos/{{owner}}/{{repo}}/pulls', method: 'GET', headers: { Authorization: 'Bearer {{github_token}}' } } } },
{ id: 'n2', type: 'ai-llm', position: { x: 400, y: 200 }, data: { label: 'Review Code', config: { prompt: 'Review this pull request and identify potential issues: {{input}}', model: 'gpt-4o' } } },
{ id: 'n3', type: 'webhook-sender', position: { x: 700, y: 200 }, data: { label: 'Post Comment', config: { url: '', method: 'POST' } } },
],
edges: [
{ id: 'e1', source: 'n1', target: 'n2' },
{ id: 'e2', source: 'n2', target: 'n3' },
],
},
},
{
name: 'Daily News Digest',
description: 'Fetch news from an RSS feed, filter relevant items, and compile a daily digest.',
category: TemplateCategory.AUTOMATION,
icon: '📰',
isBuiltin: true,
graph: {
nodes: [
{ id: 'n1', type: 'rss-reader', position: { x: 100, y: 200 }, data: { label: 'RSS Feed', config: { url: '', maxItems: 20 } } },
{ id: 'n2', type: 'ai-llm', position: { x: 400, y: 200 }, data: { label: 'Filter & Rank', config: { prompt: 'From these news items, select the 5 most important and explain why: {{input}}', model: 'gpt-4o-mini' } } },
{ id: 'n3', type: 'email-sender', position: { x: 700, y: 200 }, data: { label: 'Send Digest', config: { to: '', subject: 'Daily News Digest' } } },
],
edges: [
{ id: 'e1', source: 'n1', target: 'n2' },
{ id: 'e2', source: 'n2', target: 'n3' },
],
},
},
{
name: 'API Health Monitor',
description: 'Ping a list of API endpoints, check response times, and alert on failures.',
category: TemplateCategory.MONITORING,
icon: '🔔',
isBuiltin: true,
graph: {
nodes: [
{ id: 'n1', type: 'api-caller', position: { x: 100, y: 200 }, data: { label: 'Health Check', config: { url: '', method: 'GET', timeout: 5000 } } },
{ id: 'n2', type: 'condition', position: { x: 400, y: 200 }, data: { label: 'Check Status', config: { condition: '{{status}} !== 200 || {{responseTime}} > 2000' } } },
{ id: 'n3', type: 'webhook-sender', position: { x: 700, y: 100 }, data: { label: 'Alert Slack', config: { url: '', method: 'POST' } } },
{ id: 'n4', type: 'logger', position: { x: 700, y: 300 }, data: { label: 'Log OK', config: { level: 'info' } } },
],
edges: [
{ id: 'e1', source: 'n1', target: 'n2' },
{ id: 'e2', source: 'n2', target: 'n3', label: 'fail' },
{ id: 'e3', source: 'n2', target: 'n4', label: 'pass' },
],
},
},
{
name: 'Data Extractor & CSV Export',
description: 'Extract structured data from text or HTML using an LLM and format it as CSV.',
category: TemplateCategory.DATA,
icon: '📊',
isBuiltin: true,
graph: {
nodes: [
{ id: 'n1', type: 'web-scraper', position: { x: 100, y: 200 }, data: { label: 'Scrape Data', config: { url: '', selector: 'table' } } },
{ id: 'n2', type: 'ai-llm', position: { x: 400, y: 200 }, data: { label: 'Extract Structure', config: { prompt: 'Extract all data from this content and return as JSON array with consistent keys: {{input}}', model: 'gpt-4o-mini' } } },
{ id: 'n3', type: 'transformer', position: { x: 700, y: 200 }, data: { label: 'To CSV', config: { format: 'csv' } } },
],
edges: [
{ id: 'e1', source: 'n1', target: 'n2' },
{ id: 'e2', source: 'n2', target: 'n3' },
],
},
},
];
49 changes: 49 additions & 0 deletions apps/api/src/templates/templates.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Injectable, NotFoundException, OnModuleInit } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Template, TemplateCategory } from './template.entity';
import { FlowsService } from '../flows/flows.service';
import type { Flow } from '../flows/entities/flow.entity';
import { BUILTIN_TEMPLATES } from './templates.seed';

@Injectable()
export class TemplatesService implements OnModuleInit {
constructor(
@InjectRepository(Template)
private readonly repo: Repository<Template>,
private readonly flowsService: FlowsService,
) {}

async onModuleInit(): Promise<void> {
const count = await this.repo.count({ where: { isBuiltin: true } });
if (count === 0) {
await this.repo.save(
BUILTIN_TEMPLATES.map((t) => this.repo.create(t)),
);
}
}

async findAll(category?: TemplateCategory): Promise<Template[]> {
const where = category ? { category } : {};
return this.repo.find({ where, order: { useCount: 'DESC', createdAt: 'DESC' } });
}

async findOne(id: string): Promise<Template> {
const template = await this.repo.findOne({ where: { id } });
if (!template) throw new NotFoundException(`Template ${id} not found`);
return template;
}

async useTemplate(id: string, workspaceId: string): Promise<Flow> {
const template = await this.findOne(id);

const flow = await this.flowsService.create(workspaceId, {
name: `${template.name} (from template)`,
description: template.description,
graph: template.graph,
});

await this.repo.increment({ id }, 'useCount', 1);
return flow;
}
}
Loading