Files
simstudioai--sim/apps/sim/lib/api/contracts/custom-blocks.ts
T
wehub-resource-sync d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

179 lines
6.5 KiB
TypeScript

import { z } from 'zod'
import { workflowIdSchema, workspaceIdSchema } from '@/lib/api/contracts/primitives'
import { defineRouteContract } from '@/lib/api/contracts/types'
const inputFieldSchema = z.object({
/** Stable per-field id — preserved so client block configs key sub-blocks on it
* (rename-safe wiring) instead of the display name. Absent on legacy fields. */
id: z.string().optional(),
name: z.string(),
type: z.string(),
description: z.string().optional(),
/** Consumer-facing placeholder hint (curated inputs only). */
placeholder: z.string().optional(),
/** Consumers must fill this input (curated inputs only). */
required: z.boolean().optional(),
})
/**
* The authored per-input data: a placeholder and a required flag, keyed by the
* source Start field's stable `id`. The field's name/type/description are NOT
* stored — they're always derived from the live deployed Start (so they can't go
* stale); an override whose field was removed from the Start is silently ignored.
*/
const inputPlaceholderSchema = z.object({
id: z.string().min(1),
placeholder: z.string().max(200).optional(),
required: z.boolean().optional(),
})
export type CustomBlockInputPlaceholder = z.input<typeof inputPlaceholderSchema>
/** A curated output: a child-workflow block output (blockId + dot-path) exposed under `name`. */
const exposedOutputSchema = z.object({
blockId: z.string().min(1),
path: z.string().min(1),
name: z.string().min(1).max(60),
})
export const customBlockSchema = z.object({
id: z.string(),
organizationId: z.string(),
workflowId: z.string(),
/** Name of the bound source workflow (for display; the source can't be changed). */
workflowName: z.string(),
/** Source workflow's home workspace id — used client-side to gate manage affordances. */
workspaceId: z.string().nullable(),
/** Name of the source workflow's home workspace (display only). */
workspaceName: z.string().nullable(),
type: z.string(),
name: z.string(),
description: z.string(),
/** Uploaded icon image URL, or null for the default icon. */
iconUrl: z.string().nullable(),
enabled: z.boolean(),
inputFields: z.array(inputFieldSchema),
/** Curated outputs exposed to consumers; empty = expose the child's whole result. */
exposedOutputs: z.array(exposedOutputSchema),
})
export type CustomBlock = z.output<typeof customBlockSchema>
export const listCustomBlocksQuerySchema = z.object({
workspaceId: workspaceIdSchema,
})
/**
* Icon URLs are rendered as org-wide `<img>` sources, so only https URLs and
* internal file-serve paths (what the icon upload UI stores) are accepted —
* never data:/blob:/other schemes an admin could smuggle into shared metadata.
* Shared with the copilot deploy_custom_block handler's pass-through branch.
*/
export function isAllowedCustomBlockIconUrl(value: string): boolean {
return value.startsWith('https://') || value.startsWith('/api/files/serve/')
}
const iconUrlSchema = z.string().min(1).max(2048).refine(isAllowedCustomBlockIconUrl, {
message: 'iconUrl must be an https URL or an internal /api/files/serve/ path',
})
export const publishCustomBlockBodySchema = z.object({
workspaceId: workspaceIdSchema,
workflowId: workflowIdSchema,
name: z.string().min(1, 'Name is required').max(60, 'Name must be 60 characters or fewer'),
description: z.string().max(280, 'Description must be 280 characters or fewer').default(''),
/** Uploaded icon image URL (https or internal serve path); omit for the default icon. */
iconUrl: iconUrlSchema.optional(),
/** Per-input placeholder hints keyed by Start field id; the field set itself is always derived from the deployment. */
inputs: z.array(inputPlaceholderSchema).max(50).optional(),
/** Curated outputs; omit/empty to expose the child's whole result. */
exposedOutputs: z.array(exposedOutputSchema).max(50).optional(),
})
export type PublishCustomBlockBody = z.input<typeof publishCustomBlockBodySchema>
export const customBlockIdParamsSchema = z.object({
id: z.string().min(1),
})
export const updateCustomBlockBodySchema = z
.object({
name: z.string().min(1).max(60).optional(),
description: z.string().max(280).optional(),
enabled: z.boolean().optional(),
/** A URL (https or internal serve path) sets/replaces the icon; `null` clears it (default icon). */
iconUrl: iconUrlSchema.nullable().optional(),
inputs: z.array(inputPlaceholderSchema).max(50).optional(),
exposedOutputs: z.array(exposedOutputSchema).max(50).optional(),
})
.refine((v) => Object.keys(v).length > 0, { message: 'At least one field is required' })
export type UpdateCustomBlockBody = z.input<typeof updateCustomBlockBodySchema>
/**
* How many workflows in the org place this block. Live editor state and the
* active deployment snapshot can diverge, so a workflow counts when the block
* appears in either; `deployedUsageCount` counts active deployments only.
*/
export const customBlockUsageCountsSchema = z.object({
usageCount: z.number().int().min(0),
deployedUsageCount: z.number().int().min(0),
})
export type CustomBlockUsageCounts = z.output<typeof customBlockUsageCountsSchema>
export const listCustomBlocksContract = defineRouteContract({
method: 'GET',
path: '/api/custom-blocks',
query: listCustomBlocksQuerySchema,
response: {
mode: 'json',
schema: z.object({
/** Whether this workspace can publish/use custom blocks (feature flag + enterprise plan). */
enabled: z.boolean(),
customBlocks: z.array(customBlockSchema),
}),
},
})
export const publishCustomBlockContract = defineRouteContract({
method: 'POST',
path: '/api/custom-blocks',
body: publishCustomBlockBodySchema,
response: {
mode: 'json',
schema: z.object({ customBlock: customBlockSchema }),
},
})
export const updateCustomBlockContract = defineRouteContract({
method: 'PATCH',
path: '/api/custom-blocks/[id]',
params: customBlockIdParamsSchema,
body: updateCustomBlockBodySchema,
response: {
mode: 'json',
schema: z.object({ success: z.literal(true) }),
},
})
export const deleteCustomBlockContract = defineRouteContract({
method: 'DELETE',
path: '/api/custom-blocks/[id]',
params: customBlockIdParamsSchema,
response: {
mode: 'json',
schema: z.object({ success: z.literal(true) }),
},
})
export const getCustomBlockUsageCountsContract = defineRouteContract({
method: 'GET',
path: '/api/custom-blocks/[id]/usages',
params: customBlockIdParamsSchema,
response: {
mode: 'json',
schema: customBlockUsageCountsSchema,
},
})