Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,48 @@
import { AppSystemProp } from '@activepieces/server-shared'
import {
isNil,
ListTemplatesRequestQuery,
SeekPage,
Template,
} from '@activepieces/shared'
import { paginationHelper } from '../helper/pagination/pagination-utils'
import { system } from '../helper/system/system'
export const communityTemplates = {
get: async (request: ListTemplatesRequestQuery): Promise<SeekPage<Template>> => {
const templateSource = system.get(AppSystemProp.TEMPLATES_SOURCE_URL)
if (isNil(templateSource)) {
return paginationHelper.createPage([], null)
}
const queryString = convertToQueryString(request)
const url = `${templateSource}?${queryString}`
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const templates = await response.json()
return templates
},
}
function convertToQueryString(params: ListTemplatesRequestQuery): string {
const searchParams = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((val) => {
if (!isNil(val)) {
searchParams.append(key, typeof val === 'string' ? val : JSON.stringify(val))
}
})
}
else if (!isNil(value)) {
searchParams.set(key, value.toString())
}
})
return searchParams.toString()
}

View File

@@ -0,0 +1,49 @@
import {
ALL_PRINCIPAL_TYPES,
ApEdition,
SERVICE_KEY_SECURITY_OPENAPI,
TemplateTag,
TemplateType,
} from '@activepieces/shared'
import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
import { Static, Type } from '@sinclair/typebox'
import { system } from '../helper/system/system'
import { communityTemplates } from './community-flow-template.service'
import { templateService } from './template.service'
const edition = system.getEdition()
export const deprecatedFlowTemplateController: FastifyPluginAsyncTypebox = async (app) => {
app.get('/', ListFlowTemplatesParams, async (request) => {
if (edition === ApEdition.CLOUD) {
return templateService().list({ platformId: null, requestQuery: {
...request.query,
type: TemplateType.OFFICIAL,
} })
}
return communityTemplates.get({
...request.query,
type: TemplateType.OFFICIAL,
})
})
}
const ListFlowTemplatesRequestQuery = Type.Object({
pieces: Type.Optional(Type.Array(Type.String())),
tags: Type.Optional(Type.Array(TemplateTag)),
search: Type.Optional(Type.String()),
})
type ListFlowTemplatesRequestQuery = Static<typeof ListFlowTemplatesRequestQuery>
const ListFlowTemplatesParams = {
config: {
allowedPrincipals: ALL_PRINCIPAL_TYPES,
},
schema: {
tags: ['templates'],
description: 'List flow templates. This endpoint is deprecated, use /v1/templates instead.',
deprecated: true,
security: [SERVICE_KEY_SECURITY_OPENAPI],
querystring: ListFlowTemplatesRequestQuery,
},
}

View File

@@ -0,0 +1,210 @@
import {
ActivepiecesError,
ALL_PRINCIPAL_TYPES,
ApEdition,
CreateTemplateRequestBody,
ErrorCode,
FlowVersionTemplate,
isNil,
ListTemplatesRequestQuery,
Principal,
PrincipalType,
SERVICE_KEY_SECURITY_OPENAPI,
TemplateType,
UpdateTemplateRequestBody,
} from '@activepieces/shared'
import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
import { Static, Type } from '@sinclair/typebox'
import { StatusCodes } from 'http-status-codes'
import { platformMustBeOwnedByCurrentUser } from '../ee/authentication/ee-authorization'
import { migrateFlowVersionTemplate } from '../flows/flow-version/migrations'
import { system } from '../helper/system/system'
import { platformService } from '../platform/platform.service'
import { communityTemplates } from './community-flow-template.service'
import { templateService } from './template.service'
const edition = system.getEdition()
export const templateController: FastifyPluginAsyncTypebox = async (app) => {
app.get('/:id', GetParams, async (request) => {
return templateService().getOneOrThrow({ id: request.params.id })
})
app.get('/', ListTemplatesParams, async (request) => {
const platformId = await resolveTemplatesPlatformIdOrThrow(request.principal, request.query.type ?? TemplateType.OFFICIAL)
if (isNil(platformId)) {
if (edition === ApEdition.CLOUD) {
return templateService().list({ platformId: null, requestQuery: request.query })
}
return communityTemplates.get(request.query)
}
return templateService().list({ platformId, requestQuery: request.query })
})
app.post('/', {
...CreateParams,
preValidation: async (request) => {
const migratedFlows = await Promise.all((request.body.flows ?? []).map(async (flow: FlowVersionTemplate) => {
const migratedFlow = await migrateFlowVersionTemplate(flow.trigger, flow.schemaVersion)
return {
...flow,
trigger: migratedFlow.trigger,
schemaVersion: migratedFlow.schemaVersion,
}
}))
request.body.flows = migratedFlows
},
}, async (request, reply) => {
const { type } = request.body
let platformId: string | undefined
switch (type) {
case TemplateType.CUSTOM: {
await platformMustBeOwnedByCurrentUser.call(app, request, reply)
platformId = request.principal.platform.id
}
break
case TemplateType.SHARED:
break
case TemplateType.OFFICIAL: {
throw new ActivepiecesError({
code: ErrorCode.VALIDATION,
params: {
message: 'Official templates are not supported to being created',
},
})
}
}
const result = await templateService().create({ platformId, params: request.body })
return reply.status(StatusCodes.CREATED).send(result)
})
app.post('/:id', UpdateParams, async (request, reply) => {
const result = await templateService().update({ id: request.params.id, params: request.body })
return reply.status(StatusCodes.OK).send(result)
})
app.post('/:id/increment-usage-count', IncrementUsageCountParams, async (request, reply) => {
await templateService().incrementUsageCount({ id: request.params.id })
return reply.status(StatusCodes.OK).send()
})
app.delete('/:id', DeleteParams, async (request, reply) => {
const template = await templateService().getOneOrThrow({ id: request.params.id })
if (template.type === TemplateType.CUSTOM) {
await platformMustBeOwnedByCurrentUser.call(app, request, reply)
}
await templateService().delete({
id: request.params.id,
})
return reply.status(StatusCodes.NO_CONTENT).send()
})
}
async function resolveTemplatesPlatformIdOrThrow(principal: Principal, type: TemplateType): Promise<string | null> {
if (principal.type === PrincipalType.UNKNOWN || principal.type === PrincipalType.WORKER || type === TemplateType.OFFICIAL) {
return null
}
if (type === TemplateType.CUSTOM) {
const platform = await platformService.getOneWithPlanOrThrow(principal.platform.id)
if (!platform.plan.manageTemplatesEnabled) {
throw new ActivepiecesError({
code: ErrorCode.FEATURE_DISABLED,
params: {
message: 'Templates are not enabled for this platform',
},
})
}
return platform.id
}
throw new ActivepiecesError({
code: ErrorCode.VALIDATION,
params: {
message: 'Invalid request, shared templates are not supported to being listed',
},
})
}
const GetIdParams = Type.Object({
id: Type.String(),
})
type GetIdParams = Static<typeof GetIdParams>
const GetParams = {
config: {
allowedPrincipals: ALL_PRINCIPAL_TYPES,
},
schema: {
tags: ['templates'],
description: 'Get a template.',
security: [SERVICE_KEY_SECURITY_OPENAPI],
params: GetIdParams,
},
}
const ListTemplatesParams = {
config: {
allowedPrincipals: ALL_PRINCIPAL_TYPES,
},
schema: {
tags: ['templates'],
description: 'List templates.',
security: [SERVICE_KEY_SECURITY_OPENAPI],
querystring: ListTemplatesRequestQuery,
},
}
const DeleteParams = {
config: {
allowedPrincipals: [PrincipalType.USER, PrincipalType.SERVICE] as const,
},
schema: {
description: 'Delete a template.',
tags: ['templates'],
security: [SERVICE_KEY_SECURITY_OPENAPI],
params: GetIdParams,
},
}
const CreateParams = {
config: {
allowedPrincipals: [PrincipalType.USER, PrincipalType.SERVICE] as const,
},
schema: {
description: 'Create a template.',
tags: ['templates'],
security: [SERVICE_KEY_SECURITY_OPENAPI],
body: CreateTemplateRequestBody,
},
}
const UpdateParams = {
config: {
allowedPrincipals: [PrincipalType.USER, PrincipalType.SERVICE] as const,
},
schema: {
description: 'Update a template.',
tags: ['templates'],
security: [SERVICE_KEY_SECURITY_OPENAPI],
params: GetIdParams,
body: UpdateTemplateRequestBody,
},
}
const IncrementUsageCountParams = {
config: {
allowedPrincipals: [PrincipalType.USER, PrincipalType.SERVICE] as const,
},
schema: {
description: 'Increment usage count of a template.',
tags: ['templates'],
security: [SERVICE_KEY_SECURITY_OPENAPI],
params: GetIdParams,
},
}

View File

@@ -0,0 +1,95 @@
import { Platform, Template } from '@activepieces/shared'
import { EntitySchema } from 'typeorm'
import {
BaseColumnSchemaPart,
} from '../database/database-common'
type TemplateSchema = Template & {
platform: Platform
}
export const TemplateEntity = new EntitySchema<TemplateSchema>({
name: 'template',
columns: {
...BaseColumnSchemaPart,
name: {
type: String,
},
summary: {
type: String,
nullable: false,
},
description: {
type: String,
},
type: {
type: String,
},
platformId: {
type: String,
nullable: true,
},
status: {
type: String,
nullable: false,
},
flows: {
type: 'jsonb',
nullable: false,
},
tags: {
type: 'jsonb',
nullable: false,
},
blogUrl: {
type: String,
nullable: true,
},
metadata: {
type: 'jsonb',
nullable: true,
},
usageCount: {
type: Number,
nullable: false,
},
author: {
type: String,
nullable: false,
},
categories: {
type: String,
array: true,
nullable: false,
},
pieces: {
type: String,
array: true,
},
},
indices: [
{
name: 'idx_template_pieces',
columns: ['pieces'],
unique: false,
},
{
name: 'idx_template_categories',
columns: ['categories'],
unique: false,
},
],
relations: {
platform: {
type: 'many-to-one',
target: 'platform',
cascade: true,
onDelete: 'CASCADE',
nullable: true,
joinColumn: {
name: 'platformId',
foreignKeyConstraintName: 'fk_template_platform_id',
},
},
},
})

View File

@@ -0,0 +1,8 @@
import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox'
import { deprecatedFlowTemplateController } from './deprecated-flow-template.controller'
import { templateController } from './template.controller'
export const templateModule: FastifyPluginAsyncTypebox = async (app) => {
await app.register(templateController, { prefix: '/v1/templates' })
await app.register(deprecatedFlowTemplateController, { prefix: '/v1/flow-templates' })
}

View File

@@ -0,0 +1,174 @@
import { ActivepiecesError, apId, CreateTemplateRequestBody, ErrorCode, flowPieceUtil, FlowVersionTemplate, isNil, ListTemplatesRequestQuery, sanitizeObjectForPostgresql, SeekPage, spreadIfDefined, Template, TemplateStatus, TemplateType, UpdateTemplateRequestBody } from '@activepieces/shared'
import { ArrayContains, ArrayOverlap, Equal, ILike, IsNull } from 'typeorm'
import { repoFactory } from '../core/db/repo-factory'
import { platformTemplateService } from '../ee/template/platform-template.service'
import { paginationHelper } from '../helper/pagination/pagination-utils'
import { TemplateEntity } from './template.entity'
const templateRepo = repoFactory<Template>(TemplateEntity)
export const templateService = () => ({
async getOneOrThrow({ id }: GetParams): Promise<Template> {
const template = await templateRepo().findOneBy({ id })
if (isNil(template)) {
throw new ActivepiecesError({
code: ErrorCode.ENTITY_NOT_FOUND,
params: {
entityType: 'template',
entityId: id,
message: `Template ${id} not found`,
},
})
}
return template
},
async create({ platformId, params }: CreateParams): Promise<Template> {
const { name, summary, description, tags, blogUrl, metadata, author, categories, type } = params
const newTags = tags ?? []
const sanatizedFlows: FlowVersionTemplate[] = params.flows?.map((flow) => sanitizeObjectForPostgresql(flow)) ?? []
const pieces = sanatizedFlows.map((flow) => flowPieceUtil.getUsedPieces(flow.trigger)).flat()
switch (type) {
case TemplateType.OFFICIAL:
case TemplateType.SHARED: {
const newTemplate: NewTemplate = {
id: apId(),
name,
type,
summary,
description,
platformId,
tags: newTags,
blogUrl,
metadata,
author,
usageCount: 0,
categories,
pieces,
flows: sanatizedFlows,
status: TemplateStatus.PUBLISHED,
}
return templateRepo().save(newTemplate)
}
case TemplateType.CUSTOM: {
return platformTemplateService().create({ platformId, name, summary, description, pieces, tags: newTags, blogUrl, metadata, author, categories, flows: sanatizedFlows })
}
}
},
async update({ id, params }: UpdateParams): Promise<Template> {
const { name, summary, description, tags, blogUrl, metadata, categories, status } = params
const template = await templateService().getOneOrThrow({ id })
const newTags = tags ?? []
const sanatizedFlows: FlowVersionTemplate[] = params.flows?.map((flow) => sanitizeObjectForPostgresql(flow)) ?? []
const pieces = sanatizedFlows.map((flow) => flowPieceUtil.getUsedPieces(flow.trigger)).flat()
switch (template.type) {
case TemplateType.OFFICIAL:
case TemplateType.SHARED: {
await templateRepo().update(id, {
...spreadIfDefined('name', name),
...spreadIfDefined('summary', summary),
...spreadIfDefined('description', description),
...spreadIfDefined('tags', tags),
...spreadIfDefined('blogUrl', blogUrl),
...spreadIfDefined('metadata', metadata),
...spreadIfDefined('categories', categories),
...spreadIfDefined('flows', sanatizedFlows),
...spreadIfDefined('pieces', pieces),
...spreadIfDefined('tags', newTags),
...spreadIfDefined('status', status),
})
return templateRepo().findOneByOrFail({ id })
}
case TemplateType.CUSTOM: {
return platformTemplateService().update({ id, params })
}
}
},
async incrementUsageCount({ id }: IncrementUsageCountParams): Promise<void> {
await templateRepo().increment({ id }, 'usageCount', 1)
},
async list({ platformId, requestQuery }: ListParams): Promise<SeekPage<Template>> {
const { pieces, tags, search, type } = requestQuery
const commonFilters: Record<string, unknown> = {}
const typeFilter = type ?? TemplateType.OFFICIAL
if (pieces) {
commonFilters.pieces = ArrayOverlap(pieces)
}
if (tags) {
commonFilters.tags = ArrayContains(tags)
}
if (search) {
commonFilters.name = ILike(`%${search}%`)
commonFilters.description = ILike(`%${search}%`)
}
switch (typeFilter) {
case TemplateType.OFFICIAL:
commonFilters.type = Equal(TemplateType.OFFICIAL)
commonFilters.platformId = IsNull()
break
case TemplateType.CUSTOM:
commonFilters.type = Equal(TemplateType.CUSTOM)
if (isNil(platformId)) {
throw new ActivepiecesError({
code: ErrorCode.VALIDATION,
params: {
message: 'Platform ID is required to list custom templates',
},
})
}
commonFilters.platformId = Equal(platformId)
break
case TemplateType.SHARED:
throw new ActivepiecesError({
code: ErrorCode.VALIDATION,
params: {
message: 'Shared templates are not supported to being listed',
},
})
}
commonFilters.status = Equal(TemplateStatus.PUBLISHED)
const templates = await templateRepo()
.createQueryBuilder('template')
.where(commonFilters)
.getMany()
return paginationHelper.createPage(templates, null)
},
async delete({ id }: DeleteParams): Promise<void> {
await templateRepo().delete({ id })
},
})
type GetParams = {
id: string
}
type CreateParams = {
platformId: string | undefined
params: CreateTemplateRequestBody
}
type NewTemplate = Omit<Template, 'created' | 'updated'>
type ListParams = {
platformId: string | null
requestQuery: ListTemplatesRequestQuery
}
type DeleteParams = {
id: string
}
type UpdateParams = {
id: string
params: UpdateTemplateRequestBody
}
type IncrementUsageCountParams = {
id: string
}