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:
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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' })
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user