d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
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
159 lines
5.3 KiB
TypeScript
159 lines
5.3 KiB
TypeScript
import { db } from '@sim/db'
|
|
import { webhook, workflow, workflowDeploymentVersion } from '@sim/db/schema'
|
|
import { and, eq, isNull, or } from 'drizzle-orm'
|
|
import {
|
|
SIM_EVENT_TYPES,
|
|
SIM_RULE_DEFAULTS,
|
|
SIM_TRIGGER_PROVIDER,
|
|
type SimEventType,
|
|
} from '@/lib/workspace-events/constants'
|
|
import type { SimSubscription, SimSubscriptionConfig } from '@/lib/workspace-events/types'
|
|
|
|
/**
|
|
* Fetches active Sim-trigger subscriptions for one workspace.
|
|
*
|
|
* Workspace-scoped on purpose: execution completion is the hottest event in
|
|
* the platform, so this must never degrade into a global provider scan. The
|
|
* deployment-version join enforces that subscribers are deployed and the
|
|
* webhook row belongs to the active deployment version.
|
|
*/
|
|
export async function fetchSimTriggerSubscriptions(
|
|
workspaceId: string
|
|
): Promise<SimSubscription[]> {
|
|
const rows = await db
|
|
.select({ webhook, workflow })
|
|
.from(webhook)
|
|
.innerJoin(workflow, eq(webhook.workflowId, workflow.id))
|
|
.leftJoin(
|
|
workflowDeploymentVersion,
|
|
and(
|
|
eq(workflowDeploymentVersion.workflowId, workflow.id),
|
|
eq(workflowDeploymentVersion.isActive, true)
|
|
)
|
|
)
|
|
.where(
|
|
and(
|
|
eq(webhook.provider, SIM_TRIGGER_PROVIDER),
|
|
eq(webhook.isActive, true),
|
|
isNull(webhook.archivedAt),
|
|
eq(workflow.workspaceId, workspaceId),
|
|
eq(workflow.isDeployed, true),
|
|
isNull(workflow.archivedAt),
|
|
or(
|
|
eq(webhook.deploymentVersionId, workflowDeploymentVersion.id),
|
|
and(isNull(workflowDeploymentVersion.id), isNull(webhook.deploymentVersionId))
|
|
)
|
|
)
|
|
)
|
|
|
|
return rows
|
|
}
|
|
|
|
function parseStringArray(value: unknown): string[] {
|
|
if (Array.isArray(value)) {
|
|
return value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)
|
|
}
|
|
if (typeof value === 'string' && value.length > 0) {
|
|
return value
|
|
.split(',')
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean)
|
|
}
|
|
return []
|
|
}
|
|
|
|
/**
|
|
* Per-field bounds ported from the legacy notifications contract. Rule SQL
|
|
* runs on the execution-completion hot path, so windows and counts must stay
|
|
* inside the designed envelope regardless of what the free-text subblocks
|
|
* contain. Integer fields are rounded — counts feed SQL LIMIT, which rejects
|
|
* fractional values. The credit bounds are the legacy dollar bounds
|
|
* ($0.01-$1000) at 200 credits per dollar; credits stay fractional like the
|
|
* legacy dollar threshold.
|
|
*/
|
|
const SIM_RULE_BOUNDS = {
|
|
consecutiveFailures: { min: 1, max: 100, integer: true },
|
|
failureRatePercent: { min: 1, max: 100, integer: true },
|
|
windowHours: { min: 1, max: 168, integer: true },
|
|
durationThresholdMs: { min: 1000, max: 3_600_000, integer: true },
|
|
latencySpikePercent: { min: 10, max: 1000, integer: true },
|
|
costThresholdCredits: { min: 2, max: 200_000 },
|
|
errorCountThreshold: { min: 1, max: 1000, integer: true },
|
|
inactivityHours: { min: 1, max: 168, integer: true },
|
|
} as const
|
|
|
|
function parseBoundedNumber(
|
|
value: unknown,
|
|
fallback: number,
|
|
bounds: { min: number; max: number; integer?: boolean }
|
|
): number {
|
|
const parsed = typeof value === 'number' ? value : Number(value)
|
|
if (!Number.isFinite(parsed) || parsed <= 0) return fallback
|
|
const clamped = Math.min(Math.max(parsed, bounds.min), bounds.max)
|
|
return bounds.integer ? Math.round(clamped) : clamped
|
|
}
|
|
|
|
/**
|
|
* Parses a webhook row's providerConfig into a typed subscription config.
|
|
* Returns null when the config has no recognizable event type.
|
|
*/
|
|
export function parseSubscriptionConfig(providerConfig: unknown): SimSubscriptionConfig | null {
|
|
const config =
|
|
providerConfig && typeof providerConfig === 'object' && !Array.isArray(providerConfig)
|
|
? (providerConfig as Record<string, unknown>)
|
|
: {}
|
|
|
|
const eventType = config.eventType
|
|
if (
|
|
typeof eventType !== 'string' ||
|
|
!(SIM_EVENT_TYPES as readonly string[]).includes(eventType)
|
|
) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
eventType: eventType as SimEventType,
|
|
workflowIds: parseStringArray(config.workflowIds),
|
|
consecutiveFailures: parseBoundedNumber(
|
|
config.consecutiveFailures,
|
|
SIM_RULE_DEFAULTS.consecutiveFailures,
|
|
SIM_RULE_BOUNDS.consecutiveFailures
|
|
),
|
|
failureRatePercent: parseBoundedNumber(
|
|
config.failureRatePercent,
|
|
SIM_RULE_DEFAULTS.failureRatePercent,
|
|
SIM_RULE_BOUNDS.failureRatePercent
|
|
),
|
|
windowHours: parseBoundedNumber(
|
|
config.windowHours,
|
|
SIM_RULE_DEFAULTS.windowHours,
|
|
SIM_RULE_BOUNDS.windowHours
|
|
),
|
|
durationThresholdMs: parseBoundedNumber(
|
|
config.durationThresholdMs,
|
|
SIM_RULE_DEFAULTS.durationThresholdMs,
|
|
SIM_RULE_BOUNDS.durationThresholdMs
|
|
),
|
|
latencySpikePercent: parseBoundedNumber(
|
|
config.latencySpikePercent,
|
|
SIM_RULE_DEFAULTS.latencySpikePercent,
|
|
SIM_RULE_BOUNDS.latencySpikePercent
|
|
),
|
|
costThresholdCredits: parseBoundedNumber(
|
|
config.costThresholdCredits,
|
|
SIM_RULE_DEFAULTS.costThresholdCredits,
|
|
SIM_RULE_BOUNDS.costThresholdCredits
|
|
),
|
|
errorCountThreshold: parseBoundedNumber(
|
|
config.errorCountThreshold,
|
|
SIM_RULE_DEFAULTS.errorCountThreshold,
|
|
SIM_RULE_BOUNDS.errorCountThreshold
|
|
),
|
|
inactivityHours: parseBoundedNumber(
|
|
config.inactivityHours,
|
|
SIM_RULE_DEFAULTS.inactivityHours,
|
|
SIM_RULE_BOUNDS.inactivityHours
|
|
),
|
|
}
|
|
}
|