From af35e2abe9aeadba7f10ac1dd4ee606cc95484a0 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 1 Jul 2025 11:51:21 +0200 Subject: [PATCH 1/5] fix: schema generation for unsupported field type --- packages/sdk/src/schema/schema.ts | 2 +- packages/sdk/src/ts-schema-generator.ts | 53 +- tests/e2e/cal.com/cal-com.test.ts | 11 +- tests/e2e/formbricks/formbricks.test.ts | 12 + tests/e2e/formbricks/schema.zmodel | 1101 +++++++++++++++++++++++ 5 files changed, 1148 insertions(+), 31 deletions(-) create mode 100644 tests/e2e/formbricks/formbricks.test.ts create mode 100644 tests/e2e/formbricks/schema.zmodel diff --git a/packages/sdk/src/schema/schema.ts b/packages/sdk/src/schema/schema.ts index d6242e68..8ef02fb6 100644 --- a/packages/sdk/src/schema/schema.ts +++ b/packages/sdk/src/schema/schema.ts @@ -59,7 +59,7 @@ export type FieldDef = { unique?: boolean; updatedAt?: boolean; attributes?: AttributeApplication[]; - default?: MappedBuiltinType | Expression; + default?: MappedBuiltinType | Expression | unknown[]; relation?: RelationInfo; foreignKeyFor?: string[]; computed?: boolean; diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index 55111bd1..1bf9bbe8 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -284,12 +284,7 @@ export class TsSchemaGenerator { } private createDataModelFieldObject(field: DataModelField) { - const objectFields = [ - ts.factory.createPropertyAssignment( - 'type', - ts.factory.createStringLiteral(field.type.type ?? field.type.reference!.$refText), - ), - ]; + const objectFields = [ts.factory.createPropertyAssignment('type', this.generateFieldTypeLiteral(field))]; if (isIdField(field)) { objectFields.push(ts.factory.createPropertyAssignment('id', ts.factory.createTrue())); @@ -325,7 +320,7 @@ export class TsSchemaGenerator { const defaultValue = this.getMappedDefault(field); if (defaultValue !== undefined) { - if (typeof defaultValue === 'object') { + if (typeof defaultValue === 'object' && !Array.isArray(defaultValue)) { if ('call' in defaultValue) { objectFields.push( ts.factory.createPropertyAssignment( @@ -371,18 +366,20 @@ export class TsSchemaGenerator { throw new Error(`Unsupported default value type for field ${field.name}`); } } else { - objectFields.push( - ts.factory.createPropertyAssignment( - 'default', - typeof defaultValue === 'string' - ? ts.factory.createStringLiteral(defaultValue) - : typeof defaultValue === 'number' - ? ts.factory.createNumericLiteral(defaultValue) - : defaultValue === true - ? ts.factory.createTrue() - : ts.factory.createFalse(), - ), - ); + if (Array.isArray(defaultValue)) { + objectFields.push( + ts.factory.createPropertyAssignment( + 'default', + ts.factory.createArrayLiteralExpression( + defaultValue.map((item) => this.createLiteralNode(item as any)), + ), + ), + ); + } else { + objectFields.push( + ts.factory.createPropertyAssignment('default', this.createLiteralNode(defaultValue)), + ); + } } } @@ -440,7 +437,7 @@ export class TsSchemaGenerator { private getMappedDefault( field: DataModelField, - ): string | number | boolean | { call: string; args: any[] } | { authMember: string[] } | undefined { + ): string | number | boolean | unknown[] | { call: string; args: any[] } | { authMember: string[] } | undefined { const defaultAttr = getAttribute(field, '@default'); if (!defaultAttr) { return undefined; @@ -456,6 +453,8 @@ export class TsSchemaGenerator { : ['Int', 'Float', 'Decimal', 'BigInt'].includes(field.type.type!) ? Number(lit) : lit; + } else if (isArrayExpr(defaultValue)) { + return defaultValue.items.map((item) => this.getLiteral(item)); } else if (isReferenceExpr(defaultValue) && isEnumField(defaultValue.target.ref)) { return defaultValue.target.ref.name; } else if (isInvocationExpr(defaultValue)) { @@ -681,9 +680,17 @@ export class TsSchemaGenerator { return ts.factory.createObjectLiteralExpression(properties, true); } - private generateFieldTypeLiteral(field: DataModelField): ts.Expression { - invariant(field.type.type || field.type.reference, 'Field type must be a primitive or reference'); - return ts.factory.createStringLiteral(field.type.type ?? field.type.reference!.$refText); + private generateFieldTypeLiteral(field: DataModelField | ProcedureParam): ts.Expression { + invariant( + field.type.type || field.type.reference || field.type.unsupported, + 'Field type must be a primitive, reference, or Unsupported', + ); + + return field.type.type + ? ts.factory.createStringLiteral(field.type.type) + : field.type.reference + ? ts.factory.createStringLiteral(field.type.reference.$refText) + : ts.factory.createStringLiteral('unknown'); } private createEnumObject(e: Enum) { diff --git a/tests/e2e/cal.com/cal-com.test.ts b/tests/e2e/cal.com/cal-com.test.ts index 46a07ca6..84290d5f 100644 --- a/tests/e2e/cal.com/cal-com.test.ts +++ b/tests/e2e/cal.com/cal-com.test.ts @@ -1,15 +1,12 @@ import { generateTsSchema } from '@zenstackhq/testtools'; -import { describe, it } from 'vitest'; +import { describe, expect, it } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; describe('Cal.com e2e tests', () => { it('has a working schema', async () => { - const generated = await generateTsSchema( - fs.readFileSync(path.join(__dirname, 'schema.zmodel'), 'utf8'), - 'postgresql', - 'cal-com', - ); - console.log(generated); + await expect( + generateTsSchema(fs.readFileSync(path.join(__dirname, 'schema.zmodel'), 'utf8'), 'postgresql', 'cal-com'), + ).resolves.toBeTruthy(); }); }); diff --git a/tests/e2e/formbricks/formbricks.test.ts b/tests/e2e/formbricks/formbricks.test.ts new file mode 100644 index 00000000..1e16f6dd --- /dev/null +++ b/tests/e2e/formbricks/formbricks.test.ts @@ -0,0 +1,12 @@ +import { generateTsSchema } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; + +describe('Formbricks e2e tests', () => { + it('has a working schema', async () => { + await expect( + generateTsSchema(fs.readFileSync(path.join(__dirname, 'schema.zmodel'), 'utf8'), 'postgresql', 'cal-com'), + ).resolves.toBeTruthy(); + }); +}); diff --git a/tests/e2e/formbricks/schema.zmodel b/tests/e2e/formbricks/schema.zmodel new file mode 100644 index 00000000..7a7c19f3 --- /dev/null +++ b/tests/e2e/formbricks/schema.zmodel @@ -0,0 +1,1101 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + extensions = [pgvector(map: "vector")] +} + +generator client { + provider = "prisma-client-js" + previewFeatures = ["postgresqlExtensions"] +} + +generator json { + provider = "prisma-json-types-generator" +} + +enum PipelineTriggers { + responseCreated + responseUpdated + responseFinished +} + +enum WebhookSource { + user + zapier + make + n8n + activepieces +} + +/// Represents a webhook endpoint for receiving survey-related events. +/// Webhooks can be configured to receive notifications about response creation, updates, and completion. +/// +/// @property id - Unique identifier for the webhook +/// @property name - Optional display name for the webhook +/// @property url - The endpoint URL where events will be sent +/// @property source - Origin of the webhook (user, zapier, make, etc.) +/// @property environment - Associated environment +/// @property triggers - Types of events that trigger this webhook +/// @property surveyIds - List of surveys this webhook is monitoring +model Webhook { + id String @id @default(cuid()) + name String? + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at") + url String + source WebhookSource @default(user) + environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade) + environmentId String + triggers PipelineTriggers[] + surveyIds String[] + + @@index([environmentId]) +} + +/// Represents an attribute value associated with a contact. +/// Used to store custom properties and characteristics of contacts. +/// +/// @property id - Unique identifier for the attribute +/// @property attributeKey - Reference to the attribute definition +/// @property contact - The contact this attribute belongs to +/// @property value - The actual value of the attribute +model ContactAttribute { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + attributeKey ContactAttributeKey @relation(fields: [attributeKeyId], references: [id], onDelete: Cascade) + attributeKeyId String + contact Contact @relation(fields: [contactId], references: [id], onDelete: Cascade) + contactId String + value String + + @@unique([contactId, attributeKeyId]) + @@index([attributeKeyId, value]) +} + +enum ContactAttributeType { + default + custom +} + +/// Defines the possible attributes that can be assigned to contacts. +/// Acts as a schema for contact attributes within an environment. +/// +/// @property id - Unique identifier for the attribute key +/// @property isUnique - Whether the attribute must have unique values across contacts +/// @property key - The attribute identifier used in the system +/// @property name - Display name for the attribute +/// @property type - Whether this is a default or custom attribute +/// @property environment - The environment this attribute belongs to +model ContactAttributeKey { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + isUnique Boolean @default(false) + key String + name String? + description String? + type ContactAttributeType @default(custom) + environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade) + environmentId String + attributes ContactAttribute[] + attributeFilters SurveyAttributeFilter[] + + @@unique([key, environmentId]) + @@index([environmentId, createdAt]) +} + +/// Represents a person or user who can receive and respond to surveys. +/// Contacts are environment-specific and can have multiple attributes and responses. +/// +/// @property id - Unique identifier for the contact +/// @property userId - Optional external user identifier +/// @property environment - The environment this contact belongs to +/// @property responses - Survey responses from this contact +/// @property attributes - Custom attributes associated with this contact +/// @property displays - Record of surveys shown to this contact +model Contact { + id String @id @default(cuid()) + userId String? + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade) + environmentId String + responses Response[] + attributes ContactAttribute[] + displays Display[] + + @@index([environmentId]) +} + +/// Stores a user's response to a survey, including their answers and metadata. +/// Each response is linked to a specific survey and optionally to a contact. +/// +/// @property id - Unique identifier for the response +/// @property finished - Whether the survey was completed +/// @property survey - The associated survey +/// @property contact - The optional contact who provided the response +/// @property data - JSON object containing the actual response data +/// @property variables - Custom variables used in the response +/// @property ttc - Time to completion metrics +/// @property meta - Additional metadata about the response +model Response { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at") + finished Boolean @default(false) + survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade) + surveyId String + contact Contact? @relation(fields: [contactId], references: [id], onDelete: Cascade) + contactId String? + endingId String? + notes ResponseNote[] + /// [ResponseData] + data Json @default("{}") + /// [ResponseVariables] + variables Json @default("{}") + /// [ResponseTtc] + ttc Json @default("{}") + /// [ResponseMeta] + meta Json @default("{}") + tags TagsOnResponses[] + /// [ResponseContactAttributes] + contactAttributes Json? + // singleUseId, used to prevent multiple responses + singleUseId String? + language String? + documents Document[] + displayId String? @unique + display Display? @relation(fields: [displayId], references: [id]) + + @@unique([surveyId, singleUseId]) + @@index([createdAt]) + @@index([surveyId, createdAt]) // to determine monthly response count + @@index([contactId, createdAt]) // to determine monthly identified users (persons) + @@index([surveyId]) +} + +/// Represents notes or comments added to survey responses by team members. +/// Used for internal communication and response analysis. +/// +/// @property id - Unique identifier for the note +/// @property response - The associated survey response +/// @property user - The team member who created the note +/// @property text - Content of the note +/// @property isResolved - Whether the note has been marked as resolved +/// @property isEdited - Whether the note has been modified +model ResponseNote { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + response Response @relation(fields: [responseId], references: [id], onDelete: Cascade) + responseId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String + text String + isResolved Boolean @default(false) + isEdited Boolean @default(false) + + @@index([responseId]) +} + +/// Represents a label that can be applied to survey responses. +/// Used for categorizing and organizing responses within an environment. +/// +/// @property id - Unique identifier for the tag +/// @property name - Display name of the tag +/// @property responses - Survey responses tagged with this label +/// @property environment - The environment this tag belongs to +model Tag { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + name String + responses TagsOnResponses[] + environmentId String + environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade) + + @@unique([environmentId, name]) + @@index([environmentId]) +} + +/// Junction table linking tags to responses. +/// Enables many-to-many relationship between tags and responses. +/// +/// @property response - The tagged response +/// @property tag - The tag applied to the response +model TagsOnResponses { + responseId String + response Response @relation(fields: [responseId], references: [id], onDelete: Cascade) + tagId String + tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) + + @@id([responseId, tagId]) + @@index([responseId]) +} + +enum SurveyStatus { + draft + scheduled + inProgress + paused + completed +} + +enum DisplayStatus { + seen + responded +} + +/// Records when a survey is shown to a user. +/// Tracks survey display history and response status. +/// +/// @property id - Unique identifier for the display event +/// @property survey - The survey that was displayed +/// @property contact - The contact who saw the survey +/// @property status - Whether the survey was just seen or responded to +/// @property response - The associated response if one exists +model Display { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade) + surveyId String + contact Contact? @relation(fields: [contactId], references: [id], onDelete: Cascade) + contactId String? + responseId String? @unique //deprecated + status DisplayStatus? + response Response? + + @@index([surveyId]) + @@index([contactId, createdAt]) +} + +/// Links surveys to specific trigger actions. +/// Defines which user actions should cause a survey to be displayed. +/// This is the connection table between Surveys and ActionClasses that determines +/// when and under what conditions a survey should be triggered. +/// +/// @property id - Unique identifier for the trigger +/// @property survey - The survey to be triggered +/// @property actionClass - The action that triggers the survey +/// @property createdAt - When the trigger was created +/// @property updatedAt - When the trigger was last modified +model SurveyTrigger { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade) + surveyId String + actionClass ActionClass @relation(fields: [actionClassId], references: [id], onDelete: Cascade) + actionClassId String + + @@unique([surveyId, actionClassId]) + @@index([surveyId]) +} + +enum SurveyAttributeFilterCondition { + equals + notEquals +} + +/// Defines targeting rules for surveys based on contact attributes. +/// Used to show surveys only to contacts matching specific criteria. +/// +/// @property id - Unique identifier for the filter +/// @property attributeKey - The contact attribute to filter on +/// @property survey - The survey being filtered +/// @property condition - The comparison operator to use +/// @property value - The value to compare against +model SurveyAttributeFilter { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + attributeKey ContactAttributeKey @relation(fields: [attributeKeyId], references: [id], onDelete: Cascade) + attributeKeyId String + survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade) + surveyId String + condition SurveyAttributeFilterCondition + value String + + @@unique([surveyId, attributeKeyId]) + @@index([surveyId]) + @@index([attributeKeyId]) +} + +enum SurveyType { + link + web + website + app +} + +enum displayOptions { + displayOnce + displayMultiple + displaySome + respondMultiple +} + +/// Represents a complete survey configuration including questions, styling, and display rules. +/// Core model for the survey functionality in Formbricks. +/// +/// @property id - Unique identifier for the survey +/// @property name - Display name of the survey +/// @property type - Survey delivery method (link, web, website, app) +/// @property status - Current state of the survey (draft, active, completed, etc) +/// @property environment - The environment this survey belongs to +/// @property questions - JSON array containing survey questions configuration +/// @property displayOption - Rules for how often the survey can be shown +/// @property triggers - Actions that can trigger this survey +/// @property attributeFilters - Rules for targeting specific contacts +model Survey { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + name String + redirectUrl String? + type SurveyType @default(web) + environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade) + environmentId String + creator User? @relation(fields: [createdBy], references: [id]) + createdBy String? + status SurveyStatus @default(draft) + /// [SurveyWelcomeCard] + welcomeCard Json @default("{\"enabled\": false}") + /// [SurveyQuestions] + questions Json @default("[]") + /// [SurveyEnding] + endings Json[] @default([]) + thankYouCard Json? //deprecated + /// [SurveyHiddenFields] + hiddenFields Json @default("{\"enabled\": false}") + /// [SurveyVariables] + variables Json @default("[]") + responses Response[] + displayOption displayOptions @default(displayOnce) + recontactDays Int? + displayLimit Int? + triggers SurveyTrigger[] + /// [SurveyInlineTriggers] + inlineTriggers Json? + attributeFilters SurveyAttributeFilter[] + displays Display[] + autoClose Int? + autoComplete Int? + delay Int @default(0) + runOnDate DateTime? + closeOnDate DateTime? + /// [SurveyClosedMessage] + surveyClosedMessage Json? + segmentId String? + segment Segment? @relation(fields: [segmentId], references: [id]) + + /// [SurveyProjectOverwrites] + projectOverwrites Json? + + /// [SurveyStyling] + styling Json? + + /// [SurveySingleUse] + singleUse Json? @default("{\"enabled\": false, \"isEncrypted\": true}") + + /// [SurveyVerifyEmail] + verifyEmail Json? // deprecated + isVerifyEmailEnabled Boolean @default(false) + isSingleResponsePerEmailEnabled Boolean @default(false) + isBackButtonHidden Boolean @default(false) + pin String? + resultShareKey String? @unique + displayPercentage Decimal? + languages SurveyLanguage[] + showLanguageSwitch Boolean? + documents Document[] + followUps SurveyFollowUp[] + /// [SurveyRecaptcha] + recaptcha Json? @default("{\"enabled\": false, \"threshold\":0.1}") + + @@index([environmentId, updatedAt]) + @@index([segmentId]) +} + +/// Defines follow-up actions for survey responses. +/// Enables automated actions based on specific survey response conditions. +/// +/// @property id - Unique identifier for the follow-up +/// @property survey - The associated survey +/// @property name - Display name for the follow-up +/// @property trigger - Conditions that activate the follow-up +/// @property action - Actions to take when triggered +model SurveyFollowUp { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade) + surveyId String + name String + /// [SurveyFollowUpTrigger] + trigger Json + /// [SurveyFollowUpAction] + action Json +} + +enum ActionType { + code + noCode +} + +/// Represents a user action that can trigger surveys. +/// Used to define points in the user journey where surveys can be shown. +/// +/// @property id - Unique identifier for the action +/// @property name - Display name of the action +/// @property type - Whether this is a code or no-code action +/// @property key - Unique identifier used in code implementation +/// @property noCodeConfig - Configuration for no-code setup +/// @property environment - The environment this action belongs to +model ActionClass { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + name String + description String? + type ActionType + key String? + /// [ActionClassNoCodeConfig] + noCodeConfig Json? + environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade) + environmentId String + surveyTriggers SurveyTrigger[] + + @@unique([key, environmentId]) + @@unique([name, environmentId]) + @@index([environmentId, createdAt]) +} + +enum EnvironmentType { + production + development +} + +enum IntegrationType { + googleSheets + notion + airtable + slack +} + +/// Represents third-party service integrations. +/// Enables data flow between Formbricks and external services. +/// +/// @property id - Unique identifier for the integration +/// @property type - The service being integrated (Google Sheets, Notion, etc.) +/// @property environment - The environment this integration belongs to +/// @property config - Service-specific configuration details +model Integration { + id String @id @default(cuid()) + type IntegrationType + environmentId String + /// [IntegrationConfig] + config Json + environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade) + + @@unique([type, environmentId]) + @@index([environmentId]) +} + +enum DataMigrationStatus { + pending + applied + failed +} + +/// Tracks database schema migrations. +/// Used to manage and track the state of data structure changes. +/// +/// @property id - Unique identifier for the migration +/// @property name - Name of the migration +/// @property status - Current state of the migration (pending, applied, failed) +/// @property startedAt - When the migration began +/// @property finishedAt - When the migration completed +model DataMigration { + id String @id @default(cuid()) + startedAt DateTime @default(now()) @map(name: "started_at") + finishedAt DateTime? @map(name: "finished_at") + name String @unique + status DataMigrationStatus +} + +/// Represents either a production or development environment within a project. +/// Each project has exactly two environments, serving as the main reference point +/// for most Formbricks resources including surveys and actions. +/// +/// @property id - Unique identifier for the environment +/// @property type - Either 'production' or 'development' +/// @property project - Reference to parent project +/// @property widgetSetupCompleted - Tracks initial widget setup status +/// @property surveys - Collection of surveys in this environment +/// @property contacts - Collection of contacts/users tracked +/// @property actionClasses - Defined actions that can trigger surveys +/// @property attributeKeys - Custom attributes configuration +model Environment { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + type EnvironmentType + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + projectId String + widgetSetupCompleted Boolean @default(false) + appSetupCompleted Boolean @default(false) + surveys Survey[] + contacts Contact[] + actionClasses ActionClass[] + attributeKeys ContactAttributeKey[] + webhooks Webhook[] + tags Tag[] + segments Segment[] + integration Integration[] + documents Document[] + insights Insight[] + ApiKeyEnvironment ApiKeyEnvironment[] + + @@index([projectId]) +} + +enum WidgetPlacement { + bottomLeft + bottomRight + topLeft + topRight + center +} + +/// Main grouping mechanism for resources in Formbricks. +/// Each organization can have multiple projects to separate different applications or products. +/// +/// @property id - Unique identifier for the project +/// @property name - Display name of the project +/// @property organization - Reference to parent organization +/// @property environments - Development and production environments +/// @property styling - Project-wide styling configuration +/// @property config - Project-specific configuration +/// @property recontactDays - Default recontact delay for surveys +/// @property placement - Default widget placement for in-app surveys +model Project { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + name String + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + organizationId String + environments Environment[] + brandColor String? // deprecated; use styling.brandColor instead + highlightBorderColor String? // deprecated + /// [Styling] + styling Json @default("{\"allowStyleOverwrite\":true}") + /// [ProjectConfig] + config Json @default("{}") + recontactDays Int @default(7) + linkSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in link surveys + inAppSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in in-app surveys + placement WidgetPlacement @default(bottomRight) + clickOutsideClose Boolean @default(true) + darkOverlay Boolean @default(false) + languages Language[] + /// [Logo] + logo Json? + projectTeams ProjectTeam[] + + @@unique([organizationId, name]) + @@index([organizationId]) +} + +/// Represents the top-level organizational hierarchy in Formbricks. +/// Self-hosting instances typically have a single organization, while Formbricks Cloud +/// supports multiple organizations with multi-tenancy. +/// +/// @property id - Unique identifier for the organization +/// @property name - Display name of the organization +/// @property memberships - User memberships within the organization +/// @property projects - Collection of projects owned by the organization +/// @property billing - JSON field containing billing information +/// @property whitelabel - Whitelabel configuration for the organization +/// @property isAIEnabled - Controls access to AI-powered features +model Organization { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + name String + memberships Membership[] + projects Project[] + /// [OrganizationBilling] + billing Json + /// [OrganizationWhitelabel] + whitelabel Json @default("{}") + invites Invite[] + isAIEnabled Boolean @default(false) + teams Team[] + apiKeys ApiKey[] +} + +enum OrganizationRole { + owner + manager + member + billing +} + +enum MembershipRole { + owner + admin + editor + developer + viewer +} + +/// Links users to organizations with specific roles. +/// Manages organization membership and permissions. +/// Core model for managing user access within organizations. +/// +/// @property organization - The organization the user belongs to +/// @property user - The member user +/// @property accepted - Whether the user has accepted the membership +/// @property role - User's role within the organization (owner, manager, member, billing) +model Membership { + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + organizationId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String + accepted Boolean @default(false) + deprecatedRole MembershipRole? //deprecated + role OrganizationRole @default(member) + + @@id([userId, organizationId]) + @@index([userId]) + @@index([organizationId]) +} + +/// Represents pending invitations to join an organization. +/// Used to manage the process of adding new users to an organization. +/// Once accepted, invites are converted into memberships. +/// +/// @property id - Unique identifier for the invite +/// @property email - Email address of the invited user +/// @property name - Optional display name for the invited user +/// @property organization - The organization sending the invite +/// @property creator - The user who created the invite +/// @property acceptor - The user who accepted the invite (if accepted) +/// @property expiresAt - When the invite becomes invalid +/// @property role - Intended role for the invited user +/// @property teamIds - Teams the user will be added to upon acceptance +model Invite { + id String @id @default(uuid()) + email String + name String? + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + organizationId String + creator User @relation("inviteCreatedBy", fields: [creatorId], references: [id]) + creatorId String + acceptor User? @relation("inviteAcceptedBy", fields: [acceptorId], references: [id], onDelete: Cascade) + acceptorId String? + createdAt DateTime @default(now()) + expiresAt DateTime + deprecatedRole MembershipRole? //deprecated + role OrganizationRole @default(member) + teamIds String[] @default([]) + + @@index([email, organizationId]) + @@index([organizationId]) +} + +/// Represents enhanced API authentication keys with organization-level ownership. +/// Used for authenticating API requests to Formbricks with more granular permissions. +/// +/// @property id - Unique identifier for the API key +/// @property label - Optional descriptive name for the key +/// @property hashedKey - Securely stored API key +/// @property organization - The organization this key belongs to +/// @property createdBy - User ID who created this key +/// @property lastUsedAt - Timestamp of last usage +/// @property apiKeyEnvironments - Environments this key has access to +model ApiKey { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + createdBy String? + lastUsedAt DateTime? + label String + hashedKey String @unique + organizationId String + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + apiKeyEnvironments ApiKeyEnvironment[] + /// [OrganizationAccess] + organizationAccess Json @default("{}") + + @@index([organizationId]) +} + +/// Defines permission levels for API keys. +/// Controls what operations an API key can perform. +enum ApiKeyPermission { + read + write + manage +} + +/// Links API keys to environments with specific permissions. +/// Enables granular access control for API keys across environments. +/// +/// @property id - Unique identifier for the environment access entry +/// @property apiKey - The associated API key +/// @property environment - The environment being accessed +/// @property permission - Level of access granted +model ApiKeyEnvironment { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + apiKeyId String + apiKey ApiKey @relation(fields: [apiKeyId], references: [id], onDelete: Cascade) + environmentId String + environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade) + permission ApiKeyPermission + + @@unique([apiKeyId, environmentId]) + @@index([environmentId]) +} + +enum IdentityProvider { + email + github + google + azuread + openid + saml +} + +/// Stores third-party authentication account information. +/// Enables OAuth and other external authentication methods. +/// +/// @property id - Unique identifier for the account +/// @property user - The Formbricks user who owns this account +/// @property provider - The authentication provider (GitHub, Google, etc.) +/// @property providerAccountId - User ID from the provider +/// @property access_token - OAuth access token +/// @property refresh_token - OAuth refresh token +model Account { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String + type String + provider String + providerAccountId String + access_token String? @db.Text + refresh_token String? @db.Text + expires_at Int? + ext_expires_in Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + + @@unique([provider, providerAccountId]) + @@index([userId]) +} + +enum Role { + project_manager + engineer + founder + marketing_specialist + other +} + +enum Objective { + increase_conversion + improve_user_retention + increase_user_adoption + sharpen_marketing_messaging + support_sales + other +} + +enum Intention { + survey_user_segments + survey_at_specific_point_in_user_journey + enrich_customer_profiles + collect_all_user_feedback_on_one_platform + other +} + +/// Represents a user in the Formbricks system. +/// Central model for user authentication and profile management. +/// +/// @property id - Unique identifier for the user +/// @property name - Display name of the user +/// @property email - User's email address +/// @property role - User's professional role +/// @property objective - User's main goal with Formbricks +/// @property twoFactorEnabled - Whether 2FA is active +/// @property memberships - Organizations the user belongs to +/// @property notificationSettings - User's notification preferences +model User { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + name String + email String @unique + emailVerified DateTime? @map(name: "email_verified") + + imageUrl String? + twoFactorSecret String? + twoFactorEnabled Boolean @default(false) + backupCodes String? + password String? + identityProvider IdentityProvider @default(email) + identityProviderAccountId String? + memberships Membership[] + accounts Account[] + responseNotes ResponseNote[] + groupId String? + invitesCreated Invite[] @relation("inviteCreatedBy") + invitesAccepted Invite[] @relation("inviteAcceptedBy") + role Role? + objective Objective? + /// [UserNotificationSettings] + notificationSettings Json @default("{}") + /// [Locale] + locale String @default("en-US") + surveys Survey[] + teamUsers TeamUser[] + lastLoginAt DateTime? + isActive Boolean @default(true) + + @@index([email]) +} + +/// Maps a short URL to its full destination. +/// Used for creating memorable, shortened URLs for surveys. +/// +/// @property id - Short identifier/slug for the URL +/// @property url - The full destination URL +model ShortUrl { + id String @id // generate nanoId in service + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + url String @unique +} + +/// Defines a segment of contacts based on attributes. +/// Used for targeting surveys to specific user groups. +/// +/// @property id - Unique identifier for the segment +/// @property title - Display name of the segment +/// @property filters - Rules defining the segment +/// @property isPrivate - Whether the segment is private +/// @property environment - The environment this segment belongs to +model Segment { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + title String + description String? + isPrivate Boolean @default(true) + /// [SegmentFilter] + filters Json @default("[]") + environmentId String + environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade) + surveys Survey[] + + @@unique([environmentId, title]) + @@index([environmentId]) +} + +/// Represents a supported language in the system. +/// Used for multilingual survey support. +/// +/// @property id - Unique identifier for the language +/// @property code - Language code (e.g., 'en-US') +/// @property alias - Optional friendly name +/// @property project - The project this language is enabled for +model Language { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + code String + alias String? + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + projectId String + surveyLanguages SurveyLanguage[] + + @@unique([projectId, code]) +} + +/// Links surveys to their supported languages. +/// Manages which languages are available for each survey. +/// +/// @property language - The supported language +/// @property survey - The associated survey +/// @property default - Whether this is the default language +/// @property enabled - Whether this language is currently active +model SurveyLanguage { + language Language @relation(fields: [languageId], references: [id], onDelete: Cascade) + languageId String + surveyId String + survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade) + default Boolean @default(false) + enabled Boolean @default(true) + + @@id([languageId, surveyId]) + @@index([surveyId]) + @@index([languageId]) +} + +enum InsightCategory { + featureRequest + complaint + praise + other +} + +/// Stores analyzed insights from survey responses. +/// Used for tracking patterns and extracting meaningful information. +/// +/// @property id - Unique identifier for the insight +/// @property category - Type of insight (feature request, complaint, etc.) +/// @property title - Summary of the insight +/// @property description - Detailed explanation +/// @property vector - Embedding vector for similarity search +model Insight { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + environmentId String + environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade) + category InsightCategory + title String + description String + vector Unsupported("vector(512)")? + documentInsights DocumentInsight[] +} + +/// Links insights to source documents. +/// Enables tracing insights back to original responses. +/// +/// @property document - The source document +/// @property insight - The derived insight +model DocumentInsight { + documentId String + document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) + insightId String + insight Insight @relation(fields: [insightId], references: [id], onDelete: Cascade) + + @@id([documentId, insightId]) + @@index([insightId]) +} + +enum Sentiment { + positive + negative + neutral +} + +/// Represents a processed text document from survey responses. +/// Used for analysis and insight generation. +/// +/// @property id - Unique identifier for the document +/// @property survey - The associated survey +/// @property response - The source response +/// @property sentiment - Analyzed sentiment (positive, negative, neutral) +/// @property text - The document content +/// @property vector - Embedding vector for similarity search +model Document { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + environmentId String + environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade) + surveyId String? + survey Survey? @relation(fields: [surveyId], references: [id], onDelete: Cascade) + responseId String? + response Response? @relation(fields: [responseId], references: [id], onDelete: Cascade) + questionId String? + sentiment Sentiment + isSpam Boolean + text String + vector Unsupported("vector(512)")? + documentInsights DocumentInsight[] + + @@unique([responseId, questionId]) + @@index([createdAt]) +} + +/// Represents a team within an organization. +/// Enables group-based access control and collaboration. +/// +/// @property id - Unique identifier for the team +/// @property name - Display name of the team +/// @property organization - The parent organization +/// @property teamUsers - Users who are part of this team +/// @property projectTeams - Projects this team has access to +model Team { + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + name String + organizationId String + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + teamUsers TeamUser[] + projectTeams ProjectTeam[] + + @@unique([organizationId, name]) +} + +enum TeamUserRole { + admin + contributor +} + +/// Links users to teams with specific roles. +/// Manages team membership and permissions. +/// +/// @property team - The associated team +/// @property user - The team member +/// @property role - User's role within the team +model TeamUser { + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + teamId String + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + role TeamUserRole + + @@id([teamId, userId]) + @@index([userId]) +} + +enum ProjectTeamPermission { + read + readWrite + manage +} + +/// Defines team access to specific projects. +/// Manages project-level permissions for teams. +/// +/// @property project - The accessed project +/// @property team - The team receiving access +/// @property permission - Level of access granted +model ProjectTeam { + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + projectId String + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + teamId String + team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + permission ProjectTeamPermission @default(read) + + @@id([projectId, teamId]) + @@index([teamId]) +} From 20cb34b7755488d7440b780043a9b578d68c089e Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 1 Jul 2025 11:53:26 +0200 Subject: [PATCH 2/5] fix build --- packages/sdk/src/ts-schema-generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index 1bf9bbe8..34b354cf 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -680,7 +680,7 @@ export class TsSchemaGenerator { return ts.factory.createObjectLiteralExpression(properties, true); } - private generateFieldTypeLiteral(field: DataModelField | ProcedureParam): ts.Expression { + private generateFieldTypeLiteral(field: DataModelField): ts.Expression { invariant( field.type.type || field.type.reference || field.type.unsupported, 'Field type must be a primitive, reference, or Unsupported', From 4a0c40f24c5eb2c95fc7e0f42ab140062e4509fb Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 1 Jul 2025 15:11:06 +0200 Subject: [PATCH 3/5] update --- packages/cli/test/db.test.ts | 5 ++--- packages/cli/test/generate.test.ts | 13 ++++++------- packages/cli/test/init.test.ts | 9 +++------ packages/cli/test/migrate.test.ts | 19 +++++++++---------- packages/cli/test/utils.ts | 6 +++++- 5 files changed, 25 insertions(+), 27 deletions(-) diff --git a/packages/cli/test/db.test.ts b/packages/cli/test/db.test.ts index 9657489c..162d09d6 100644 --- a/packages/cli/test/db.test.ts +++ b/packages/cli/test/db.test.ts @@ -1,8 +1,7 @@ -import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; -import { createProject } from './utils'; +import { createProject, runCli } from './utils'; const model = ` model User { @@ -13,7 +12,7 @@ model User { describe('CLI db commands test', () => { it('should generate a database with db push', () => { const workDir = createProject(model); - execSync('node node_modules/@zenstackhq/cli/bin/cli db push'); + runCli('db push', workDir); expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true); }); }); diff --git a/packages/cli/test/generate.test.ts b/packages/cli/test/generate.test.ts index a204383a..4372b869 100644 --- a/packages/cli/test/generate.test.ts +++ b/packages/cli/test/generate.test.ts @@ -1,8 +1,7 @@ -import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; -import { createProject } from './utils'; +import { createProject, runCli } from './utils'; const model = ` model User { @@ -13,33 +12,33 @@ model User { describe('CLI generate command test', () => { it('should generate a TypeScript schema', () => { const workDir = createProject(model); - execSync('node node_modules/@zenstackhq/cli/bin/cli generate'); + runCli('generate', workDir); expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true); expect(fs.existsSync(path.join(workDir, 'zenstack/schema.prisma'))).toBe(false); }); it('should respect custom output directory', () => { const workDir = createProject(model); - execSync('node node_modules/@zenstackhq/cli/bin/cli generate --output ./zen'); + runCli('generate --output ./zen', workDir); expect(fs.existsSync(path.join(workDir, 'zen/schema.ts'))).toBe(true); }); it('should respect custom schema location', () => { const workDir = createProject(model); fs.renameSync(path.join(workDir, 'zenstack/schema.zmodel'), path.join(workDir, 'zenstack/foo.zmodel')); - execSync('node node_modules/@zenstackhq/cli/bin/cli generate --schema ./zenstack/foo.zmodel'); + runCli('generate --schema ./zenstack/foo.zmodel', workDir); expect(fs.existsSync(path.join(workDir, 'zenstack/schema.ts'))).toBe(true); }); it('should respect save prisma schema option', () => { const workDir = createProject(model); - execSync('node node_modules/@zenstackhq/cli/bin/cli generate --save-prisma-schema'); + runCli('generate --save-prisma-schema', workDir); expect(fs.existsSync(path.join(workDir, 'zenstack/schema.prisma'))).toBe(true); }); it('should respect save prisma schema custom path option', () => { const workDir = createProject(model); - execSync('node node_modules/@zenstackhq/cli/bin/cli generate --save-prisma-schema "../prisma/schema.prisma"'); + runCli('generate --save-prisma-schema "../prisma/schema.prisma"', workDir); expect(fs.existsSync(path.join(workDir, 'prisma/schema.prisma'))).toBe(true); }); }); diff --git a/packages/cli/test/init.test.ts b/packages/cli/test/init.test.ts index c1406c00..947db005 100644 --- a/packages/cli/test/init.test.ts +++ b/packages/cli/test/init.test.ts @@ -1,16 +1,13 @@ -import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import tmp from 'tmp'; import { describe, expect, it } from 'vitest'; +import { runCli } from './utils'; describe('Cli init command tests', () => { it('should create a new project', () => { const { name: workDir } = tmp.dirSync({ unsafeCleanup: true }); - process.chdir(workDir); - execSync('npm init -y'); - const cli = path.join(__dirname, '../dist/index.js'); - execSync(`node ${cli} init`); - expect(fs.existsSync('zenstack/schema.zmodel')).toBe(true); + runCli('init', workDir); + expect(fs.existsSync(path.join(workDir, 'zenstack/schema.zmodel'))).toBe(true); }); }); diff --git a/packages/cli/test/migrate.test.ts b/packages/cli/test/migrate.test.ts index be260074..85c2a928 100644 --- a/packages/cli/test/migrate.test.ts +++ b/packages/cli/test/migrate.test.ts @@ -1,8 +1,7 @@ -import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; -import { createProject } from './utils'; +import { createProject, runCli } from './utils'; const model = ` model User { @@ -13,30 +12,30 @@ model User { describe('CLI migrate commands test', () => { it('should generate a database with migrate dev', () => { const workDir = createProject(model); - execSync('node node_modules/@zenstackhq/cli/bin/cli migrate dev --name init'); + runCli('migrate dev --name init', workDir); expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true); expect(fs.existsSync(path.join(workDir, 'zenstack/migrations'))).toBe(true); }); it('should reset the database with migrate reset', () => { const workDir = createProject(model); - execSync('node node_modules/@zenstackhq/cli/bin/cli db push'); + runCli('db push', workDir); expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true); - execSync('node node_modules/@zenstackhq/cli/bin/cli migrate reset --force'); + runCli('migrate reset --force', workDir); expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true); }); it('should reset the database with migrate deploy', () => { const workDir = createProject(model); - execSync('node node_modules/@zenstackhq/cli/bin/cli migrate dev --name init'); + runCli('migrate dev --name init', workDir); fs.rmSync(path.join(workDir, 'zenstack/dev.db')); - execSync('node node_modules/@zenstackhq/cli/bin/cli migrate deploy'); + runCli('migrate deploy', workDir); expect(fs.existsSync(path.join(workDir, 'zenstack/dev.db'))).toBe(true); }); it('supports migrate status', () => { - createProject(model); - execSync('node node_modules/@zenstackhq/cli/bin/cli migrate dev --name init'); - execSync('node node_modules/@zenstackhq/cli/bin/cli migrate status'); + const workDir = createProject(model); + runCli('migrate dev --name init', workDir); + runCli('migrate status', workDir); }); }); diff --git a/packages/cli/test/utils.ts b/packages/cli/test/utils.ts index 88d7d73e..a2b8c130 100644 --- a/packages/cli/test/utils.ts +++ b/packages/cli/test/utils.ts @@ -1,4 +1,5 @@ import { createTestProject } from '@zenstackhq/testtools'; +import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; @@ -13,6 +14,9 @@ export function createProject(zmodel: string, addPrelude = true) { fs.mkdirSync(path.join(workDir, 'zenstack'), { recursive: true }); const schemaPath = path.join(workDir, 'zenstack/schema.zmodel'); fs.writeFileSync(schemaPath, addPrelude ? `${ZMODEL_PRELUDE}\n\n${zmodel}` : zmodel); - process.chdir(workDir); return workDir; } + +export function runCli(command: string, cwd: string) { + execSync(`node ${__dirname}/../bin/cli ${command}`, { cwd }); +} From a38a06cb0a488402de1cf938fa277ec5d4cb8eb9 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 1 Jul 2025 15:29:16 +0200 Subject: [PATCH 4/5] more tests --- packages/sdk/src/ts-schema-generator.ts | 39 +- tests/e2e/trigger.dev/schema.zmodel | 2084 +++++++++++++++++++++ tests/e2e/trigger.dev/trigger-dev.test.ts | 12 + 3 files changed, 2118 insertions(+), 17 deletions(-) create mode 100644 tests/e2e/trigger.dev/schema.zmodel create mode 100644 tests/e2e/trigger.dev/trigger-dev.test.ts diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index 34b354cf..c1cc020b 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -318,7 +318,7 @@ export class TsSchemaGenerator { ); } - const defaultValue = this.getMappedDefault(field); + const defaultValue = this.getFieldMappedDefault(field); if (defaultValue !== undefined) { if (typeof defaultValue === 'object' && !Array.isArray(defaultValue)) { if ('call' in defaultValue) { @@ -435,39 +435,44 @@ export class TsSchemaGenerator { } } - private getMappedDefault( + private getFieldMappedDefault( field: DataModelField, ): string | number | boolean | unknown[] | { call: string; args: any[] } | { authMember: string[] } | undefined { const defaultAttr = getAttribute(field, '@default'); if (!defaultAttr) { return undefined; } - const defaultValue = defaultAttr.args[0]?.value; invariant(defaultValue, 'Expected a default value'); + return this.getMappedValue(defaultValue, field.type); + } - if (isLiteralExpr(defaultValue)) { - const lit = (defaultValue as LiteralExpr).value; - return field.type.type === 'Boolean' + private getMappedValue( + expr: Expression, + fieldType: DataModelFieldType, + ): string | number | boolean | unknown[] | { call: string; args: any[] } | { authMember: string[] } | undefined { + if (isLiteralExpr(expr)) { + const lit = (expr as LiteralExpr).value; + return fieldType.type === 'Boolean' ? (lit as boolean) - : ['Int', 'Float', 'Decimal', 'BigInt'].includes(field.type.type!) + : ['Int', 'Float', 'Decimal', 'BigInt'].includes(fieldType.type!) ? Number(lit) : lit; - } else if (isArrayExpr(defaultValue)) { - return defaultValue.items.map((item) => this.getLiteral(item)); - } else if (isReferenceExpr(defaultValue) && isEnumField(defaultValue.target.ref)) { - return defaultValue.target.ref.name; - } else if (isInvocationExpr(defaultValue)) { + } else if (isArrayExpr(expr)) { + return expr.items.map((item) => this.getMappedValue(item, fieldType)); + } else if (isReferenceExpr(expr) && isEnumField(expr.target.ref)) { + return expr.target.ref.name; + } else if (isInvocationExpr(expr)) { return { - call: defaultValue.function.$refText, - args: defaultValue.args.map((arg) => this.getLiteral(arg.value)), + call: expr.function.$refText, + args: expr.args.map((arg) => this.getLiteral(arg.value)), }; - } else if (this.isAuthMemberAccess(defaultValue)) { + } else if (this.isAuthMemberAccess(expr)) { return { - authMember: this.getMemberAccessChain(defaultValue), + authMember: this.getMemberAccessChain(expr), }; } else { - throw new Error(`Unsupported default value type for field ${field.name}`); + throw new Error(`Unsupported default value type for ${expr.$type}`); } } diff --git a/tests/e2e/trigger.dev/schema.zmodel b/tests/e2e/trigger.dev/schema.zmodel new file mode 100644 index 00000000..0975ebb5 --- /dev/null +++ b/tests/e2e/trigger.dev/schema.zmodel @@ -0,0 +1,2084 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + directUrl = env("DIRECT_URL") +} + +generator client { + provider = "prisma-client-js" + binaryTargets = ["native", "debian-openssl-1.1.x"] + previewFeatures = ["tracing", "metrics"] +} + +model User { + id String @id @default(cuid()) + email String @unique + + authenticationMethod AuthenticationMethod + authenticationProfile Json? + authenticationExtraParams Json? + authIdentifier String? @unique + + displayName String? + name String? + avatarUrl String? + + admin Boolean @default(false) + + /// Preferences for the dashboard + dashboardPreferences Json? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + /// @deprecated + isOnCloudWaitlist Boolean @default(false) + /// @deprecated + featureCloud Boolean @default(false) + /// @deprecated + isOnHostedRepoWaitlist Boolean @default(false) + + marketingEmails Boolean @default(true) + confirmedBasicDetails Boolean @default(false) + + referralSource String? + + orgMemberships OrgMember[] + sentInvites OrgMemberInvite[] + + invitationCode InvitationCode? @relation(fields: [invitationCodeId], references: [id]) + invitationCodeId String? + personalAccessTokens PersonalAccessToken[] + deployments WorkerDeployment[] +} + +// @deprecated This model is no longer used as the Cloud is out of private beta +// Leaving it here for now for historical reasons +model InvitationCode { + id String @id @default(cuid()) + code String @unique + + users User[] + + createdAt DateTime @default(now()) +} + +enum AuthenticationMethod { + GITHUB + MAGIC_LINK +} + +/// Used to generate PersonalAccessTokens, they're one-time use +model AuthorizationCode { + id String @id @default(cuid()) + + code String @unique + + personalAccessToken PersonalAccessToken? @relation(fields: [personalAccessTokenId], references: [id], onDelete: Cascade, onUpdate: Cascade) + personalAccessTokenId String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// Used by User's to perform API actions +model PersonalAccessToken { + id String @id @default(cuid()) + + /// If generated by the CLI this will be "cli", otherwise user-provided + name String + + /// This is the token encrypted using the ENCRYPTION_KEY + encryptedToken Json + + /// This is shown in the UI, with ******** + obfuscatedToken String + + /// This is used to find the token in the database + hashedToken String @unique + + user User @relation(fields: [userId], references: [id]) + userId String + + revokedAt DateTime? + lastAccessedAt DateTime? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + authorizationCodes AuthorizationCode[] +} + +model Organization { + id String @id @default(cuid()) + slug String @unique + title String + + maximumExecutionTimePerRunInMs Int @default(900000) // 15 minutes + maximumConcurrencyLimit Int @default(10) + /// This is deprecated and will be removed in the future + maximumSchedulesLimit Int @default(5) + + maximumDevQueueSize Int? + maximumDeployedQueueSize Int? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + companySize String? + + avatar Json? + + runsEnabled Boolean @default(true) + + v3Enabled Boolean @default(false) + + /// @deprecated + v2Enabled Boolean @default(false) + /// @deprecated + v2MarqsEnabled Boolean @default(false) + /// @deprecated + hasRequestedV3 Boolean @default(false) + + environments RuntimeEnvironment[] + + apiRateLimiterConfig Json? + realtimeRateLimiterConfig Json? + + projects Project[] + members OrgMember[] + invites OrgMemberInvite[] + organizationIntegrations OrganizationIntegration[] + workerGroups WorkerInstanceGroup[] + workerInstances WorkerInstance[] + executionSnapshots TaskRunExecutionSnapshot[] +} + +model OrgMember { + id String @id @default(cuid()) + + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) + organizationId String + + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) + userId String + + role OrgMemberRole @default(MEMBER) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + environments RuntimeEnvironment[] + + @@unique([organizationId, userId]) +} + +enum OrgMemberRole { + ADMIN + MEMBER +} + +model OrgMemberInvite { + id String @id @default(cuid()) + token String @unique @default(cuid()) + email String + role OrgMemberRole @default(MEMBER) + + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) + organizationId String + + inviter User @relation(fields: [inviterId], references: [id], onDelete: Cascade, onUpdate: Cascade) + inviterId String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([organizationId, email]) +} + +model RuntimeEnvironment { + id String @id @default(cuid()) + slug String + apiKey String @unique + + /// @deprecated was for v2 + pkApiKey String @unique + + type RuntimeEnvironmentType @default(DEVELOPMENT) + + // Preview branches + /// If true, this environment has branches and is treated differently in the dashboard/API + isBranchableEnvironment Boolean @default(false) + branchName String? + parentEnvironment RuntimeEnvironment? @relation("parentEnvironment", fields: [parentEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + parentEnvironmentId String? + childEnvironments RuntimeEnvironment[] @relation("parentEnvironment") + + // This is GitMeta type + git Json? + + /// When set API calls will fail + archivedAt DateTime? + + ///A memorable code for the environment + shortcode String + + maximumConcurrencyLimit Int @default(5) + paused Boolean @default(false) + + autoEnableInternalSources Boolean @default(true) + + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) + organizationId String + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + //when the org member is deleted, it will keep the environment but set it to null + orgMember OrgMember? @relation(fields: [orgMemberId], references: [id], onDelete: SetNull, onUpdate: Cascade) + orgMemberId String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tunnelId String? + + backgroundWorkers BackgroundWorker[] + backgroundWorkerTasks BackgroundWorkerTask[] + taskRuns TaskRun[] + taskQueues TaskQueue[] + batchTaskRuns BatchTaskRun[] + environmentVariableValues EnvironmentVariableValue[] + checkpoints Checkpoint[] + workerDeployments WorkerDeployment[] + workerDeploymentPromotions WorkerDeploymentPromotion[] + taskRunAttempts TaskRunAttempt[] + CheckpointRestoreEvent CheckpointRestoreEvent[] + taskScheduleInstances TaskScheduleInstance[] + alerts ProjectAlert[] + + sessions RuntimeEnvironmentSession[] + currentSession RuntimeEnvironmentSession? @relation("currentSession", fields: [currentSessionId], references: [id], onDelete: SetNull, onUpdate: Cascade) + currentSessionId String? + taskRunNumberCounter TaskRunNumberCounter[] + taskRunCheckpoints TaskRunCheckpoint[] + waitpoints Waitpoint[] + workerInstances WorkerInstance[] + executionSnapshots TaskRunExecutionSnapshot[] + waitpointTags WaitpointTag[] + + @@unique([projectId, slug, orgMemberId]) + @@unique([projectId, shortcode]) + @@index([parentEnvironmentId]) + @@index([projectId]) +} + +enum RuntimeEnvironmentType { + PRODUCTION + STAGING + DEVELOPMENT + PREVIEW +} + +model Project { + id String @id @default(cuid()) + slug String @unique + name String + + externalRef String @unique + + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) + organizationId String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + + version ProjectVersion @default(V2) + engine RunEngineVersion @default(V1) + + builderProjectId String? + + workerGroups WorkerInstanceGroup[] + workers WorkerInstance[] + + defaultWorkerGroup WorkerInstanceGroup? @relation("ProjectDefaultWorkerGroup", fields: [defaultWorkerGroupId], references: [id]) + defaultWorkerGroupId String? + + environments RuntimeEnvironment[] + backgroundWorkers BackgroundWorker[] + backgroundWorkerTasks BackgroundWorkerTask[] + taskRuns TaskRun[] + runTags TaskRunTag[] + taskQueues TaskQueue[] + environmentVariables EnvironmentVariable[] + checkpoints Checkpoint[] + WorkerDeployment WorkerDeployment[] + CheckpointRestoreEvent CheckpointRestoreEvent[] + taskSchedules TaskSchedule[] + alertChannels ProjectAlertChannel[] + alerts ProjectAlert[] + alertStorages ProjectAlertStorage[] + bulkActionGroups BulkActionGroup[] + BackgroundWorkerFile BackgroundWorkerFile[] + waitpoints Waitpoint[] + taskRunWaitpoints TaskRunWaitpoint[] + taskRunCheckpoints TaskRunCheckpoint[] + executionSnapshots TaskRunExecutionSnapshot[] + waitpointTags WaitpointTag[] +} + +enum ProjectVersion { + V2 + V3 +} + +model SecretReference { + id String @id @default(cuid()) + key String @unique + provider SecretStoreProvider @default(DATABASE) + + environmentVariableValues EnvironmentVariableValue[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + OrganizationIntegration OrganizationIntegration[] +} + +enum SecretStoreProvider { + DATABASE + AWS_PARAM_STORE +} + +model SecretStore { + key String @unique + value Json + version String @default("1") + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([key(ops: raw("text_pattern_ops"))], type: BTree) +} + +model DataMigration { + id String @id @default(cuid()) + name String @unique + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + completedAt DateTime? +} + +// ==================================================== +// v3 Models +// ==================================================== +model BackgroundWorker { + id String @id @default(cuid()) + + friendlyId String @unique + + engine RunEngineVersion @default(V1) + + contentHash String + sdkVersion String @default("unknown") + cliVersion String @default("unknown") + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + runtimeEnvironment RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + runtimeEnvironmentId String + + version String + metadata Json + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + tasks BackgroundWorkerTask[] + attempts TaskRunAttempt[] + lockedRuns TaskRun[] + files BackgroundWorkerFile[] + queues TaskQueue[] + + deployment WorkerDeployment? + + workerGroup WorkerInstanceGroup? @relation(fields: [workerGroupId], references: [id], onDelete: SetNull, onUpdate: Cascade) + workerGroupId String? + + supportsLazyAttempts Boolean @default(false) + + @@unique([projectId, runtimeEnvironmentId, version]) + @@index([runtimeEnvironmentId]) + // Get the latest worker for a given environment + @@index([runtimeEnvironmentId, createdAt(sort: Desc)]) +} + +model BackgroundWorkerFile { + id String @id @default(cuid()) + + friendlyId String @unique + + filePath String + contentHash String + contents Bytes + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + backgroundWorkers BackgroundWorker[] + + tasks BackgroundWorkerTask[] + + createdAt DateTime @default(now()) + + @@unique([projectId, contentHash]) +} + +model BackgroundWorkerTask { + id String @id @default(cuid()) + slug String + + description String? + + friendlyId String @unique + + filePath String + exportName String? + + worker BackgroundWorker @relation(fields: [workerId], references: [id], onDelete: Cascade, onUpdate: Cascade) + workerId String + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + file BackgroundWorkerFile? @relation(fields: [fileId], references: [id], onDelete: Cascade, onUpdate: Cascade) + fileId String? + + runtimeEnvironment RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + runtimeEnvironmentId String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + attempts TaskRunAttempt[] + runs TaskRun[] + + queueConfig Json? + retryConfig Json? + machineConfig Json? + + queueId String? + queue TaskQueue? @relation(fields: [queueId], references: [id], onDelete: SetNull, onUpdate: Cascade) + + maxDurationInSeconds Int? + + triggerSource TaskTriggerSource @default(STANDARD) + + @@unique([workerId, slug]) + // Quick lookup of task identifiers + @@index([projectId, slug]) + @@index([runtimeEnvironmentId, projectId]) +} + +enum TaskTriggerSource { + STANDARD + SCHEDULED +} + +model TaskRun { + id String @id @default(cuid()) + + number Int @default(0) + friendlyId String @unique + + engine RunEngineVersion @default(V1) + + status TaskRunStatus @default(PENDING) + statusReason String? + + idempotencyKey String? + idempotencyKeyExpiresAt DateTime? + taskIdentifier String + + isTest Boolean @default(false) + + payload String + payloadType String @default("application/json") + context Json? + traceContext Json? + + traceId String + spanId String + + runtimeEnvironment RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + runtimeEnvironmentId String + + environmentType RuntimeEnvironmentType? + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + organizationId String? + + // The specific queue this run is in + queue String + // The queueId is set when the run is locked to a specific queue + lockedQueueId String? + + /// The main queue that this run is part of + workerQueue String @default("main") @map("masterQueue") + + /// @deprecated + secondaryMasterQueue String? + + /// From engine v2+ this will be defined after a run has been dequeued (starting at 1) + attemptNumber Int? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + attempts TaskRunAttempt[] @relation("attempts") + tags TaskRunTag[] + + /// Denormized column that holds the raw tags + runTags String[] + + /// Denormalized version of the background worker task + taskVersion String? + sdkVersion String? + cliVersion String? + + checkpoints Checkpoint[] + + /// startedAt marks the point at which a run is dequeued from MarQS + startedAt DateTime? + /// executedAt is set when the first attempt is about to execute + executedAt DateTime? + completedAt DateTime? + machinePreset String? + + usageDurationMs Int @default(0) + costInCents Float @default(0) + baseCostInCents Float @default(0) + + lockedAt DateTime? + lockedBy BackgroundWorkerTask? @relation(fields: [lockedById], references: [id]) + lockedById String? + + lockedToVersion BackgroundWorker? @relation(fields: [lockedToVersionId], references: [id]) + lockedToVersionId String? + + /// The "priority" of the run. This is just a negative offset in ms for the queue timestamp + /// E.g. a value of 60_000 would put the run into the queue 60s ago. + priorityMs Int @default(0) + + concurrencyKey String? + + delayUntil DateTime? + queuedAt DateTime? + ttl String? + expiredAt DateTime? + maxAttempts Int? + + /// optional token that can be used to authenticate the task run + oneTimeUseToken String? + + ///When this run is finished, the waitpoint will be marked as completed + associatedWaitpoint Waitpoint? @relation("CompletingRun") + + ///If there are any blocked waitpoints, the run won't be executed + blockedByWaitpoints TaskRunWaitpoint[] + + /// All waitpoints that blocked this run at some point, used for display purposes + connectedWaitpoints Waitpoint[] @relation("WaitpointRunConnections") + + /// Where the logs are stored + taskEventStore String @default("taskEvent") + + queueTimestamp DateTime? + + batchItems BatchTaskRunItem[] + dependency TaskRunDependency? + CheckpointRestoreEvent CheckpointRestoreEvent[] + executionSnapshots TaskRunExecutionSnapshot[] + + alerts ProjectAlert[] + + scheduleInstanceId String? + scheduleId String? + + sourceBulkActionItems BulkActionItem[] @relation("SourceActionItemRun") + destinationBulkActionItems BulkActionItem[] @relation("DestinationActionItemRun") + + logsDeletedAt DateTime? + + /// This represents the original task that that was triggered outside of a Trigger.dev task + rootTaskRun TaskRun? @relation("TaskRootRun", fields: [rootTaskRunId], references: [id], onDelete: SetNull, onUpdate: NoAction) + rootTaskRunId String? + + /// The root run will have a list of all the descendant runs, children, grand children, etc. + descendantRuns TaskRun[] @relation("TaskRootRun") + + /// The immediate parent run of this task run + parentTaskRun TaskRun? @relation("TaskParentRun", fields: [parentTaskRunId], references: [id], onDelete: SetNull, onUpdate: NoAction) + parentTaskRunId String? + + /// The immediate child runs of this task run + childRuns TaskRun[] @relation("TaskParentRun") + + /// The immediate parent attempt of this task run + parentTaskRunAttempt TaskRunAttempt? @relation("TaskParentRunAttempt", fields: [parentTaskRunAttemptId], references: [id], onDelete: SetNull, onUpdate: NoAction) + parentTaskRunAttemptId String? + + /// The batch run that this task run is a part of + batch BatchTaskRun? @relation(fields: [batchId], references: [id], onDelete: SetNull, onUpdate: NoAction) + batchId String? + + /// whether or not the task run was created because of a triggerAndWait for batchTriggerAndWait + resumeParentOnCompletion Boolean @default(false) + + /// The depth of this task run in the task run hierarchy + depth Int @default(0) + + /// The span ID of the "trigger" span in the parent task run + parentSpanId String? + + /// Holds the state of the run chain for deadlock detection + runChainState Json? + + /// seed run metadata + seedMetadata String? + seedMetadataType String @default("application/json") + + /// Run metadata + metadata String? + metadataType String @default("application/json") + metadataVersion Int @default(1) + + /// Run output + output String? + outputType String @default("application/json") + + /// Run error + error Json? + + maxDurationInSeconds Int? + + @@unique([oneTimeUseToken]) + @@unique([runtimeEnvironmentId, taskIdentifier, idempotencyKey]) + // Finding child runs + @@index([parentTaskRunId]) + // Finding ancestor runs + @@index([rootTaskRunId]) + //Schedules + @@index([scheduleId]) + // Run page inspector + @@index([spanId]) + @@index([parentSpanId]) + // Schedule list page + @@index([scheduleId, createdAt(sort: Desc)]) + // Finding runs in a batch + @@index([runTags(ops: ArrayOps)], type: Gin) + @@index([runtimeEnvironmentId, batchId]) + // This will include the createdAt index to help speed up the run list page + @@index([runtimeEnvironmentId, id(sort: Desc)]) + @@index([runtimeEnvironmentId, createdAt(sort: Desc)]) + @@index([createdAt], type: Brin) + @@index([status, runtimeEnvironmentId, createdAt, id(sort: Desc)]) +} + +enum TaskRunStatus { + /// + /// NON-FINAL STATUSES + /// + + /// Task has been scheduled to run in the future + DELAYED + /// Task is waiting to be executed by a worker + PENDING + + /// The run is pending a version update because it cannot execute without additional information (task, queue, etc.). Replaces WAITING_FOR_DEPLOY + PENDING_VERSION + + /// Task hasn't been deployed yet but is waiting to be executed. Deprecated in favor of PENDING_VERSION + WAITING_FOR_DEPLOY + + /// Task is currently being executed by a worker + EXECUTING + + /// Task has been paused by the system, and will be resumed by the system + WAITING_TO_RESUME + + /// Task has failed and is waiting to be retried + RETRYING_AFTER_FAILURE + + /// Task has been paused by the user, and can be resumed by the user + PAUSED + + /// + /// FINAL STATUSES + /// + + /// Task has been canceled by the user + CANCELED + + /// Task was interrupted during execution, mostly this happens in development environments + INTERRUPTED + + /// Task has been completed successfully + COMPLETED_SUCCESSFULLY + + /// Task has been completed with errors + COMPLETED_WITH_ERRORS + + /// Task has failed to complete, due to an error in the system + SYSTEM_FAILURE + + /// Task has crashed and won't be retried, most likely the worker ran out of resources, e.g. memory or storage + CRASHED + + /// Task reached the ttl without being executed + EXPIRED + + /// Task has been timed out when using maxDuration + TIMED_OUT +} + +enum RunEngineVersion { + /// The original version that uses marqs v1 and Graphile + V1 + V2 +} + +/// Used by the RunEngine during TaskRun execution +/// It has the required information to transactionally progress a run through states, +/// and prevent side effects like heartbeats failing a run that has progressed. +/// It is optimised for performance and is designed to be cleared at some point, +/// so there are no cascading relationships to other models. +model TaskRunExecutionSnapshot { + id String @id @default(cuid()) + + /// This should always be 2+ (V1 didn't use the run engine or snapshots) + engine RunEngineVersion @default(V2) + + /// The execution status + executionStatus TaskRunExecutionStatus + /// For debugging + description String + + /// We store invalid snapshots as a record of the run state when we tried to move + isValid Boolean @default(true) + error String? + + /// The previous snapshot ID + previousSnapshotId String? + + /// Run + runId String + run TaskRun @relation(fields: [runId], references: [id]) + runStatus TaskRunStatus + + // Batch + batchId String? + batch BatchTaskRun? @relation(fields: [batchId], references: [id]) + + /// This is the current run attempt number. Users can define how many attempts they want for a run. + attemptNumber Int? + + /// Environment + environmentId String + environment RuntimeEnvironment @relation(fields: [environmentId], references: [id]) + environmentType RuntimeEnvironmentType + + projectId String + project Project @relation(fields: [projectId], references: [id]) + + organizationId String + organization Organization @relation(fields: [organizationId], references: [id]) + + /// Waitpoints that have been completed for this execution + completedWaitpoints Waitpoint[] @relation("completedWaitpoints") + + /// An array of waitpoint IDs in the correct order, used for batches + completedWaitpointOrder String[] + + /// Checkpoint + checkpointId String? + checkpoint TaskRunCheckpoint? @relation(fields: [checkpointId], references: [id]) + + /// Worker + workerId String? + worker WorkerInstance? @relation(fields: [workerId], references: [id]) + + runnerId String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + lastHeartbeatAt DateTime? + + /// Metadata used by various systems in the run engine + metadata Json? + + /// Used to get the latest valid snapshot quickly + @@index([runId, isValid, createdAt(sort: Desc)]) +} + +enum TaskRunExecutionStatus { + /// Run has been created + RUN_CREATED + /// Run is in the RunQueue + QUEUED + /// Run is in the RunQueue, and is also executing. This happens when a run is continued cannot reacquire concurrency + QUEUED_EXECUTING + /// Run has been pulled from the queue, but isn't executing yet + PENDING_EXECUTING + /// Run is executing on a worker + EXECUTING + /// Run is executing on a worker but is waiting for waitpoints to complete + EXECUTING_WITH_WAITPOINTS + /// Run has been suspended and may be waiting for waitpoints to complete before resuming + SUSPENDED + /// Run has been scheduled for cancellation + PENDING_CANCEL + /// Run is finished (success of failure) + FINISHED +} + +model TaskRunCheckpoint { + id String @id @default(cuid()) + + friendlyId String @unique + + type TaskRunCheckpointType + location String + imageRef String? + reason String? + metadata String? + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + runtimeEnvironment RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + runtimeEnvironmentId String + + executionSnapshot TaskRunExecutionSnapshot[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +enum TaskRunCheckpointType { + DOCKER + KUBERNETES +} + +/// A Waitpoint blocks a run from continuing until it's completed +/// If there's a waitpoint blocking a run, it shouldn't be in the queue +model Waitpoint { + id String @id @default(cuid()) + + friendlyId String @unique + + type WaitpointType + status WaitpointStatus @default(PENDING) + + completedAt DateTime? + + /// If it's an Event type waitpoint, this is the event. It can also be provided for the DATETIME type + idempotencyKey String + /// If this is true then we can show it in the dashboard/return it from the SDK + userProvidedIdempotencyKey Boolean + + /// If there's a user provided idempotency key, this is the time it expires at + idempotencyKeyExpiresAt DateTime? + + /// If an idempotencyKey is no longer active, we store it here and generate a new one for the idempotencyKey field. + /// Clearing an idempotencyKey is useful for debounce or cancelling child runs. + /// This is a workaround because Prisma doesn't support partial indexes. + inactiveIdempotencyKey String? + + /// If it's a RUN type waitpoint, this is the associated run + completedByTaskRunId String? @unique + completedByTaskRun TaskRun? @relation("CompletingRun", fields: [completedByTaskRunId], references: [id], onDelete: SetNull) + + /// If it's a DATETIME type waitpoint, this is the date. + /// If it's a MANUAL waitpoint, this can be set as the `timeout`. + completedAfter DateTime? + + /// If it's a BATCH type waitpoint, this is the associated batch + completedByBatchId String? + completedByBatch BatchTaskRun? @relation(fields: [completedByBatchId], references: [id], onDelete: SetNull) + + /// The runs this waitpoint is blocking + blockingTaskRuns TaskRunWaitpoint[] + + /// All runs that have ever been blocked by this waitpoint, used for display purposes + connectedRuns TaskRun[] @relation("WaitpointRunConnections") + + /// When a waitpoint is complete + completedExecutionSnapshots TaskRunExecutionSnapshot[] @relation("completedWaitpoints") + + /// When completed, an output can be stored here + output String? + outputType String @default("application/json") + outputIsError Boolean @default(false) + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + environmentId String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + /// Denormized column that holds the raw tags + /// Denormalized column that holds the raw tags + tags String[] + + /// Quickly find an idempotent waitpoint + @@unique([environmentId, idempotencyKey]) + /// Quickly find a batch waitpoint + @@index([completedByBatchId]) + /// Used on the Waitpoint dashboard pages + /// Time period filtering + @@index([environmentId, type, createdAt(sort: Desc)]) + /// Status filtering + @@index([environmentId, type, status]) +} + +enum WaitpointType { + RUN + DATETIME + MANUAL + BATCH +} + +enum WaitpointStatus { + PENDING + COMPLETED +} + +model TaskRunWaitpoint { + id String @id @default(cuid()) + + taskRun TaskRun @relation(fields: [taskRunId], references: [id]) + taskRunId String + + waitpoint Waitpoint @relation(fields: [waitpointId], references: [id]) + waitpointId String + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + /// This span id is completed when the waitpoint is completed. This is used with cached runs (idempotent) + spanIdToComplete String? + + //associated batch + batchId String? + batch BatchTaskRun? @relation(fields: [batchId], references: [id]) + //if there's an associated batch and this isn't set it's for the entire batch + //if it is set, it's a specific run in the batch + batchIndex Int? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + /// There are two constraints, the one below and also one that Prisma doesn't support + /// The second one implemented in SQL only prevents a TaskRun + Waitpoint with a null batchIndex + @@unique([taskRunId, waitpointId, batchIndex]) + @@index([taskRunId]) + @@index([waitpointId]) +} + +model WaitpointTag { + id String @id @default(cuid()) + name String + + environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + environmentId String + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + createdAt DateTime @default(now()) + + @@unique([environmentId, name]) +} + +model FeatureFlag { + id String @id @default(cuid()) + + key String @unique + value Json? +} + +model WorkerInstance { + id String @id @default(cuid()) + + /// For example "worker-1" + name String + + /// If managed, it will default to the name, e.g. "worker-1" + /// If unmanged, it will be prefixed with the deployment ID e.g. "deploy-123-worker-1" + resourceIdentifier String + + metadata Json? + + workerGroup WorkerInstanceGroup @relation(fields: [workerGroupId], references: [id]) + workerGroupId String + + TaskRunExecutionSnapshot TaskRunExecutionSnapshot[] + + organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) + organizationId String? + + project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String? + + environment RuntimeEnvironment? @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + environmentId String? + + deployment WorkerDeployment? @relation(fields: [deploymentId], references: [id], onDelete: SetNull, onUpdate: Cascade) + deploymentId String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + lastDequeueAt DateTime? + lastHeartbeatAt DateTime? + + @@unique([workerGroupId, resourceIdentifier]) +} + +enum WorkerInstanceGroupType { + MANAGED + UNMANAGED +} + +model WorkerInstanceGroup { + id String @id @default(cuid()) + type WorkerInstanceGroupType + + /// For example "us-east-1" + name String + + /// If managed, it will default to the name, e.g. "us-east-1" + /// If unmanged, it will be prefixed with the project ID e.g. "project_1-us-east-1" + masterQueue String @unique + + description String? + hidden Boolean @default(false) + + token WorkerGroupToken @relation(fields: [tokenId], references: [id], onDelete: Cascade, onUpdate: Cascade) + tokenId String @unique + + workers WorkerInstance[] + backgroundWorkers BackgroundWorker[] + + defaultForProjects Project[] @relation("ProjectDefaultWorkerGroup") + + organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) + organizationId String? + + project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model WorkerGroupToken { + id String @id @default(cuid()) + + tokenHash String @unique + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + workerGroup WorkerInstanceGroup? +} + +model TaskRunTag { + id String @id @default(cuid()) + name String + + friendlyId String @unique + + runs TaskRun[] + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + createdAt DateTime @default(now()) + + @@unique([projectId, name]) + //Makes run filtering by tag faster + @@index([name, id]) +} + +/// This is used for triggerAndWait and batchTriggerAndWait. The taskRun is the child task, it points at a parent attempt or a batch +model TaskRunDependency { + id String @id @default(cuid()) + + /// The child run + taskRun TaskRun @relation(fields: [taskRunId], references: [id], onDelete: Cascade, onUpdate: Cascade) + taskRunId String @unique + + checkpointEvent CheckpointRestoreEvent? @relation(fields: [checkpointEventId], references: [id], onDelete: Cascade, onUpdate: Cascade) + checkpointEventId String? @unique + + /// An attempt that is dependent on this task run. + dependentAttempt TaskRunAttempt? @relation(fields: [dependentAttemptId], references: [id]) + dependentAttemptId String? + + /// A batch run that is dependent on this task run + dependentBatchRun BatchTaskRun? @relation("dependentBatchRun", fields: [dependentBatchRunId], references: [id]) + dependentBatchRunId String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + resumedAt DateTime? + + @@index([dependentAttemptId]) + @@index([dependentBatchRunId]) +} + +/// deprecated, we hadn't included the project id in the unique constraint +model TaskRunCounter { + taskIdentifier String @id + lastNumber Int @default(0) +} + +/// Used for the TaskRun number (e.g. #1,421) +model TaskRunNumberCounter { + id String @id @default(cuid()) + + taskIdentifier String + environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + environmentId String + + lastNumber Int @default(0) + + @@unique([taskIdentifier, environmentId]) +} + +/// This is not used from engine v2+, attempts use the TaskRunExecutionSnapshot and TaskRun +model TaskRunAttempt { + id String @id @default(cuid()) + number Int @default(0) + + friendlyId String @unique + + taskRun TaskRun @relation("attempts", fields: [taskRunId], references: [id], onDelete: Cascade, onUpdate: Cascade) + taskRunId String + + backgroundWorker BackgroundWorker @relation(fields: [backgroundWorkerId], references: [id], onDelete: Cascade, onUpdate: Cascade) + backgroundWorkerId String + + backgroundWorkerTask BackgroundWorkerTask @relation(fields: [backgroundWorkerTaskId], references: [id], onDelete: Cascade, onUpdate: Cascade) + backgroundWorkerTaskId String + + runtimeEnvironment RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + runtimeEnvironmentId String + + queue TaskQueue @relation(fields: [queueId], references: [id], onDelete: Cascade, onUpdate: Cascade) + queueId String + + status TaskRunAttemptStatus @default(PENDING) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + startedAt DateTime? + completedAt DateTime? + + usageDurationMs Int @default(0) + + error Json? + output String? + outputType String @default("application/json") + + dependencies TaskRunDependency[] + batchDependencies BatchTaskRun[] + + checkpoints Checkpoint[] + batchTaskRunItems BatchTaskRunItem[] + CheckpointRestoreEvent CheckpointRestoreEvent[] + alerts ProjectAlert[] + childRuns TaskRun[] @relation("TaskParentRunAttempt") + + @@unique([taskRunId, number]) + @@index([taskRunId]) +} + +enum TaskRunAttemptStatus { + /// NON-FINAL + PENDING + EXECUTING + PAUSED + /// FINAL + FAILED + CANCELED + COMPLETED +} + +/// This is the unified otel span/log model that will eventually be replaced by clickhouse +model TaskEvent { + id String @id @default(cuid()) + + /// This matches the span name for a trace event, or the log body for a log event + message String + + traceId String + spanId String + parentId String? + tracestate String? + + isError Boolean @default(false) + isPartial Boolean @default(false) + isCancelled Boolean @default(false) + /// deprecated: don't use this, moving this to properties, this now uses TaskEventKind.LOG + isDebug Boolean @default(false) + + serviceName String + serviceNamespace String + + level TaskEventLevel @default(TRACE) + kind TaskEventKind @default(INTERNAL) + status TaskEventStatus @default(UNSET) + + links Json? + events Json? + + /// This is the time the event started in nanoseconds since the epoch + startTime BigInt + + /// This is the duration of the event in nanoseconds + duration BigInt @default(0) + + attemptId String? + attemptNumber Int? + + environmentId String + environmentType RuntimeEnvironmentType + + organizationId String + + projectId String + projectRef String + + runId String + runIsTest Boolean @default(false) + + idempotencyKey String? + + taskSlug String + taskPath String? + taskExportName String? + + workerId String? + workerVersion String? + + queueId String? + queueName String? + + batchId String? + + /// This represents all the span attributes available, like http.status_code, and special attributes like $style.icon, $output, $metadata.payload.userId, as it's used for searching and filtering + properties Json + + /// This represents all span attributes in the $metadata namespace, like $metadata.payload + metadata Json? + + /// This represents all span attributes in the $style namespace, like $style + style Json? + + /// This represents all span attributes in the $output namespace, like $output + output Json? + + /// This represents the mimetype of the output, such as application/json or application/super+json + outputType String? + + payload Json? + payloadType String? + + createdAt DateTime @default(now()) + + // This represents the amount of "usage time" the event took, e.g. the CPU time + usageDurationMs Int @default(0) + usageCostInCents Float @default(0) + + machinePreset String? + machinePresetCpu Float? + machinePresetMemory Float? + machinePresetCentsPerMs Float? + + /// Used on the run page + @@index([traceId]) + /// Used when looking up span events to complete when a run completes + @@index([spanId]) + // Used for getting all logs for a run + @@index([runId]) +} + +enum TaskEventLevel { + TRACE + DEBUG + LOG + INFO + WARN + ERROR +} + +enum TaskEventKind { + UNSPECIFIED + INTERNAL + SERVER + CLIENT + PRODUCER + CONSUMER + UNRECOGNIZED + LOG +} + +enum TaskEventStatus { + UNSET + OK + ERROR + UNRECOGNIZED +} + +model TaskQueue { + id String @id @default(cuid()) + + friendlyId String @unique + + name String + type TaskQueueType @default(VIRTUAL) + + version TaskQueueVersion @default(V1) + orderableName String? + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + runtimeEnvironment RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + runtimeEnvironmentId String + + concurrencyLimit Int? + rateLimit Json? + + paused Boolean @default(false) + + /// If true, when a run is paused and waiting for waitpoints to be completed, the run will release the concurrency capacity. + releaseConcurrencyOnWaitpoint Boolean @default(false) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + attempts TaskRunAttempt[] + tasks BackgroundWorkerTask[] + workers BackgroundWorker[] + + @@unique([runtimeEnvironmentId, name]) +} + +enum TaskQueueType { + VIRTUAL + NAMED +} + +enum TaskQueueVersion { + V1 + V2 +} + +model BatchTaskRun { + id String @id @default(cuid()) + friendlyId String @unique + idempotencyKey String? + idempotencyKeyExpiresAt DateTime? + runtimeEnvironment RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + status BatchTaskRunStatus @default(PENDING) + runtimeEnvironmentId String + /// This only includes new runs, not idempotent runs. + runs TaskRun[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // new columns + /// Friendly IDs + runIds String[] @default([]) + runCount Int @default(0) + payload String? + payloadType String @default("application/json") + options Json? + batchVersion String @default("v1") + + //engine v2 + /// Snapshots that reference this batch + executionSnapshots TaskRunExecutionSnapshot[] + /// Specific run blockers, + runsBlocked TaskRunWaitpoint[] + /// Waitpoints that are blocked by this batch. + /// When a Batch is created it blocks execution of the associated parent run (for andWait) + waitpoints Waitpoint[] + + // This is for v3 batches + /// sealed is set to true once no more items can be added to the batch + sealed Boolean @default(false) + sealedAt DateTime? + /// this is the expected number of items in the batch + expectedCount Int @default(0) + /// this is the completed number of items in the batch. once this reaches expectedCount, and the batch is sealed, the batch is considered completed + completedCount Int @default(0) + completedAt DateTime? + resumedAt DateTime? + + /// this is used to be able to "seal" this BatchTaskRun when all of the runs have been triggered asynchronously, and using the "parallel" processing strategy + processingJobsCount Int @default(0) + processingJobsExpectedCount Int @default(0) + + /// optional token that can be used to authenticate the task run + oneTimeUseToken String? + + ///all the below properties are engine v1 only + items BatchTaskRunItem[] + taskIdentifier String? + checkpointEvent CheckpointRestoreEvent? @relation(fields: [checkpointEventId], references: [id], onDelete: Cascade, onUpdate: Cascade) + checkpointEventId String? @unique + dependentTaskAttempt TaskRunAttempt? @relation(fields: [dependentTaskAttemptId], references: [id], onDelete: Cascade, onUpdate: Cascade) + dependentTaskAttemptId String? + runDependencies TaskRunDependency[] @relation("dependentBatchRun") + + @@unique([oneTimeUseToken]) + ///this is used for all engine versions + @@unique([runtimeEnvironmentId, idempotencyKey]) + @@index([dependentTaskAttemptId]) +} + +enum BatchTaskRunStatus { + PENDING + COMPLETED + ABORTED +} + +///Used in engine V1 only +model BatchTaskRunItem { + id String @id @default(cuid()) + + status BatchTaskRunItemStatus @default(PENDING) + + batchTaskRun BatchTaskRun @relation(fields: [batchTaskRunId], references: [id], onDelete: Cascade, onUpdate: Cascade) + batchTaskRunId String + + taskRun TaskRun @relation(fields: [taskRunId], references: [id], onDelete: Cascade, onUpdate: Cascade) + taskRunId String + + taskRunAttempt TaskRunAttempt? @relation(fields: [taskRunAttemptId], references: [id], onDelete: SetNull, onUpdate: Cascade) + taskRunAttemptId String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + completedAt DateTime? + + @@unique([batchTaskRunId, taskRunId]) + @@index([taskRunAttemptId], map: "idx_batchtaskrunitem_taskrunattempt") + @@index([taskRunId], map: "idx_batchtaskrunitem_taskrun") +} + +enum BatchTaskRunItemStatus { + PENDING + FAILED + CANCELED + COMPLETED +} + +model EnvironmentVariable { + id String @id @default(cuid()) + friendlyId String @unique + key String + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + values EnvironmentVariableValue[] + + @@unique([projectId, key]) +} + +model EnvironmentVariableValue { + id String @id @default(cuid()) + valueReference SecretReference? @relation(fields: [valueReferenceId], references: [id], onDelete: SetNull, onUpdate: Cascade) + valueReferenceId String? + variable EnvironmentVariable @relation(fields: [variableId], references: [id], onDelete: Cascade, onUpdate: Cascade) + variableId String + environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + environmentId String + + /// If true, the value is secret and cannot be revealed + isSecret Boolean @default(false) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([variableId, environmentId]) +} + +model Checkpoint { + id String @id @default(cuid()) + + friendlyId String @unique + + type CheckpointType + location String + imageRef String + reason String? + metadata String? + + events CheckpointRestoreEvent[] + + run TaskRun @relation(fields: [runId], references: [id], onDelete: Cascade, onUpdate: Cascade) + runId String + + attempt TaskRunAttempt @relation(fields: [attemptId], references: [id], onDelete: Cascade, onUpdate: Cascade) + attemptId String + attemptNumber Int? + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + runtimeEnvironment RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + runtimeEnvironmentId String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([attemptId]) + @@index([runId]) +} + +enum CheckpointType { + DOCKER + KUBERNETES +} + +model CheckpointRestoreEvent { + id String @id @default(cuid()) + + type CheckpointRestoreEventType + reason String? + metadata String? + + checkpoint Checkpoint @relation(fields: [checkpointId], references: [id], onDelete: Cascade, onUpdate: Cascade) + checkpointId String + + run TaskRun @relation(fields: [runId], references: [id], onDelete: Cascade, onUpdate: Cascade) + runId String + + attempt TaskRunAttempt @relation(fields: [attemptId], references: [id], onDelete: Cascade, onUpdate: Cascade) + attemptId String + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + runtimeEnvironment RuntimeEnvironment @relation(fields: [runtimeEnvironmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + runtimeEnvironmentId String + + taskRunDependency TaskRunDependency? + batchTaskRunDependency BatchTaskRun? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([checkpointId]) + @@index([runId]) +} + +enum CheckpointRestoreEventType { + CHECKPOINT + RESTORE +} + +enum WorkerDeploymentType { + MANAGED + UNMANAGED + V1 +} + +model WorkerDeployment { + id String @id @default(cuid()) + + contentHash String + friendlyId String @unique + shortCode String + version String + + imageReference String? + imagePlatform String @default("linux/amd64") + + externalBuildData Json? + + status WorkerDeploymentStatus @default(PENDING) + type WorkerDeploymentType @default(V1) + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + environmentId String + + worker BackgroundWorker? @relation(fields: [workerId], references: [id], onDelete: Cascade, onUpdate: Cascade) + workerId String? @unique + + triggeredBy User? @relation(fields: [triggeredById], references: [id], onDelete: SetNull, onUpdate: Cascade) + triggeredById String? + + builtAt DateTime? + deployedAt DateTime? + + failedAt DateTime? + errorData Json? + + // This is GitMeta type + git Json? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + promotions WorkerDeploymentPromotion[] + alerts ProjectAlert[] + workerInstance WorkerInstance[] + + @@unique([projectId, shortCode]) + @@unique([environmentId, version]) +} + +enum WorkerDeploymentStatus { + PENDING + /// This is the status when the image is being built + BUILDING + /// This is the status when the image is built and we are waiting for the indexing to finish + DEPLOYING + /// This is the status when the image is built and indexed, meaning we have everything we need to deploy + DEPLOYED + FAILED + CANCELED + /// This is the status when the image is built and indexing does not finish in time + TIMED_OUT +} + +model WorkerDeploymentPromotion { + id String @id @default(cuid()) + + /// This is the promotion label, e.g. "current" + label String + + deployment WorkerDeployment @relation(fields: [deploymentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + deploymentId String + + environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + environmentId String + + // Only one promotion per environment can be active at a time + @@unique([environmentId, label]) +} + +///Schedules can be attached to tasks to trigger them on a schedule +model TaskSchedule { + id String @id @default(cuid()) + + type ScheduleType @default(IMPERATIVE) + + ///users see this as `id`. They start with schedule_ + friendlyId String @unique + ///a reference to a task (not a foreign key because it's across versions) + taskIdentifier String + + ///can be provided and we won't create another with the same key + deduplicationKey String @default(cuid()) + userProvidedDeduplicationKey Boolean @default(false) + + ///the CRON pattern + generatorExpression String + generatorDescription String @default("") + generatorType ScheduleGeneratorType @default(CRON) + + /// These are IANA format string, or the default "UTC". E.g. "America/New_York" + timezone String @default("UTC") + + ///Can be provided by the user then accessed inside a run + externalId String? + + ///Instances of the schedule that are active + instances TaskScheduleInstance[] + + lastRunTriggeredAt DateTime? + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + active Boolean @default(true) + + @@unique([projectId, deduplicationKey]) + /// Dashboard list view + @@index([projectId]) + @@index([projectId, createdAt(sort: Desc)]) +} + +enum ScheduleType { + /// defined on your task using the `cron` property + DECLARATIVE + /// explicit calls to the SDK are used to create, or using the dashboard + IMPERATIVE +} + +enum ScheduleGeneratorType { + CRON +} + +///An instance links a schedule with an environment +model TaskScheduleInstance { + id String @id @default(cuid()) + + taskSchedule TaskSchedule @relation(fields: [taskScheduleId], references: [id], onDelete: Cascade, onUpdate: Cascade) + taskScheduleId String + + environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + environmentId String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + active Boolean @default(true) + + lastScheduledTimestamp DateTime? + nextScheduledTimestamp DateTime? + + //you can only have a schedule attached to each environment once + @@unique([taskScheduleId, environmentId]) +} + +model RuntimeEnvironmentSession { + id String @id @default(cuid()) + + ipAddress String + + environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + environmentId String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + disconnectedAt DateTime? + + currentEnvironments RuntimeEnvironment[] @relation("currentSession") +} + +model ProjectAlertChannel { + id String @id @default(cuid()) + + friendlyId String @unique + + ///can be provided and we won't create another with the same key + deduplicationKey String @default(cuid()) + userProvidedDeduplicationKey Boolean @default(false) + + integration OrganizationIntegration? @relation(fields: [integrationId], references: [id], onDelete: SetNull, onUpdate: Cascade) + integrationId String? + + enabled Boolean @default(true) + + type ProjectAlertChannelType + name String + properties Json + alertTypes ProjectAlertType[] + environmentTypes RuntimeEnvironmentType[] @default([STAGING, PRODUCTION]) + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + alerts ProjectAlert[] + alertStorages ProjectAlertStorage[] + + @@unique([projectId, deduplicationKey]) +} + +enum ProjectAlertChannelType { + EMAIL + SLACK + WEBHOOK +} + +model ProjectAlert { + id String @id @default(cuid()) + friendlyId String @unique + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + environmentId String + + channel ProjectAlertChannel @relation(fields: [channelId], references: [id], onDelete: Cascade, onUpdate: Cascade) + channelId String + + status ProjectAlertStatus @default(PENDING) + + type ProjectAlertType + + taskRunAttempt TaskRunAttempt? @relation(fields: [taskRunAttemptId], references: [id], onDelete: Cascade, onUpdate: Cascade) + taskRunAttemptId String? + + taskRun TaskRun? @relation(fields: [taskRunId], references: [id], onDelete: Cascade, onUpdate: Cascade) + taskRunId String? + + workerDeployment WorkerDeployment? @relation(fields: [workerDeploymentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + workerDeploymentId String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +enum ProjectAlertType { + TASK_RUN + /// deprecated, we don't send new alerts for this type + TASK_RUN_ATTEMPT + DEPLOYMENT_FAILURE + DEPLOYMENT_SUCCESS +} + +enum ProjectAlertStatus { + PENDING + SENT + FAILED +} + +model ProjectAlertStorage { + id String @id @default(cuid()) + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + alertChannel ProjectAlertChannel @relation(fields: [alertChannelId], references: [id], onDelete: Cascade, onUpdate: Cascade) + alertChannelId String + + alertType ProjectAlertType + + storageId String + storageData Json + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model OrganizationIntegration { + id String @id @default(cuid()) + + friendlyId String @unique + + service IntegrationService + + integrationData Json + + tokenReference SecretReference @relation(fields: [tokenReferenceId], references: [id], onDelete: Cascade, onUpdate: Cascade) + tokenReferenceId String + + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade) + organizationId String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + alertChannels ProjectAlertChannel[] +} + +enum IntegrationService { + SLACK +} + +/// Bulk actions, like canceling and replaying runs +model BulkActionGroup { + id String @id @default(cuid()) + + friendlyId String @unique + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) + projectId String + + type BulkActionType + items BulkActionItem[] + + /// When the group is created it's pending. After we've processed all the items it's completed. This does not mean the associated runs are completed. + status BulkActionStatus @default(PENDING) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +enum BulkActionType { + /// Cancels existing runs. This populates the destination runs. + CANCEL + /// Replays existing runs. The original runs go as source runs, and the new runs go as destination runs. + REPLAY +} + +enum BulkActionStatus { + PENDING + COMPLETED +} + +model BulkActionItem { + id String @id @default(cuid()) + + friendlyId String @unique + + group BulkActionGroup @relation(fields: [groupId], references: [id], onDelete: Cascade, onUpdate: Cascade) + groupId String + + type BulkActionType + + /// When the item is created it's pending. After we've processed the item it's completed. This does not mean the associated runs are completed. + status BulkActionItemStatus @default(PENDING) + + /// The run that is the source of the action, e.g. when replaying this is the original run + sourceRun TaskRun @relation("SourceActionItemRun", fields: [sourceRunId], references: [id], onDelete: Cascade, onUpdate: Cascade) + sourceRunId String + + /// The run that's a result of the action, this will be set when the run has been created + destinationRun TaskRun? @relation("DestinationActionItemRun", fields: [destinationRunId], references: [id], onDelete: Cascade, onUpdate: Cascade) + destinationRunId String? + + error String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +enum BulkActionItemStatus { + PENDING + COMPLETED + FAILED +} + +model RealtimeStreamChunk { + id String @id @default(cuid()) + + key String + value String + + sequence Int + + runId String + + createdAt DateTime @default(now()) + + @@index([runId]) + @@index([createdAt]) +} + +/// This is the unified otel span/log model that will eventually be replaced by clickhouse +model TaskEventPartitioned { + id String @default(cuid()) + /// This matches the span name for a trace event, or the log body for a log event + message String + + traceId String + spanId String + parentId String? + tracestate String? + + isError Boolean @default(false) + isPartial Boolean @default(false) + isCancelled Boolean @default(false) + + serviceName String + serviceNamespace String + + level TaskEventLevel @default(TRACE) + kind TaskEventKind @default(INTERNAL) + status TaskEventStatus @default(UNSET) + + links Json? + events Json? + + /// This is the time the event started in nanoseconds since the epoch + startTime BigInt + + /// This is the duration of the event in nanoseconds + duration BigInt @default(0) + + attemptId String? + attemptNumber Int? + + environmentId String + environmentType RuntimeEnvironmentType + + organizationId String + + projectId String + projectRef String + + runId String + runIsTest Boolean @default(false) + + idempotencyKey String? + + taskSlug String + taskPath String? + taskExportName String? + + workerId String? + workerVersion String? + + queueId String? + queueName String? + + batchId String? + + /// This represents all the span attributes available, like http.status_code, and special attributes like $style.icon, $output, $metadata.payload.userId, as it's used for searching and filtering + properties Json + + /// This represents all span attributes in the $metadata namespace, like $metadata.payload + metadata Json? + + /// This represents all span attributes in the $style namespace, like $style + style Json? + + /// This represents all span attributes in the $output namespace, like $output + output Json? + + /// This represents the mimetype of the output, such as application/json or application/super+json + outputType String? + + payload Json? + payloadType String? + + createdAt DateTime @default(now()) + + // This represents the amount of "usage time" the event took, e.g. the CPU time + usageDurationMs Int @default(0) + usageCostInCents Float @default(0) + + machinePreset String? + machinePresetCpu Float? + machinePresetMemory Float? + machinePresetCentsPerMs Float? + + @@id([id, createdAt]) + /// Used on the run page + @@index([traceId]) + /// Used when looking up span events to complete when a run completes + @@index([spanId]) + // Used for getting all logs for a run + @@index([runId]) +} \ No newline at end of file diff --git a/tests/e2e/trigger.dev/trigger-dev.test.ts b/tests/e2e/trigger.dev/trigger-dev.test.ts new file mode 100644 index 00000000..6c5b9e32 --- /dev/null +++ b/tests/e2e/trigger.dev/trigger-dev.test.ts @@ -0,0 +1,12 @@ +import { generateTsSchema } from '@zenstackhq/testtools'; +import fs from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; + +describe('Trigger.dev e2e tests', () => { + it('has a working schema', async () => { + await expect( + generateTsSchema(fs.readFileSync(path.join(__dirname, 'schema.zmodel'), 'utf8'), 'postgresql', 'cal-com'), + ).resolves.toBeTruthy(); + }); +}); From 3995d6d85665f652a5899d7992d7c7e7a33d071a Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 1 Jul 2025 15:41:44 +0200 Subject: [PATCH 5/5] update --- packages/cli/test/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/utils.ts b/packages/cli/test/utils.ts index a2b8c130..2fafb207 100644 --- a/packages/cli/test/utils.ts +++ b/packages/cli/test/utils.ts @@ -18,5 +18,6 @@ export function createProject(zmodel: string, addPrelude = true) { } export function runCli(command: string, cwd: string) { - execSync(`node ${__dirname}/../bin/cli ${command}`, { cwd }); + const cli = path.join(__dirname, '../dist/index.js'); + execSync(`node ${cli} ${command}`, { cwd }); }