mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-27 21:01:03 +08:00
https://www.loom.com/share/cc379c5372244a169f3ae1d2cc91eae5?sid=ec5bc438-56d8-4cca-9bbc-6cf6c6d313ad
<!-- ELLIPSIS_HIDDEN -->
----
> [!IMPORTANT]
> Introduce email draft functionality with AI assistance, including
creation, editing, and sending capabilities, and update APIs and UI
components accordingly.
>
> - **Features**:
> - Add email draft functionality: create, list, edit, preview, and send
drafts.
> - Integrate AI-assisted draft generation with `emailDraftAdapter` in
`email-draft-adapter.ts`.
> - Update `send-email` and `render-email` APIs to support drafts.
> - **Backend**:
> - Add `EmailDraft` model in `schema.prisma`.
> - Implement draft CRUD operations in `email-drafts/route.tsx` and
`email-drafts/[id]/route.tsx`.
> - Update `render-email/route.tsx` and `send-email/route.tsx` to handle
draft inputs.
> - **Frontend**:
> - Add email draft UI in `email-drafts/page-client.tsx` and
`email-drafts/[draftId]/page-client.tsx`.
> - Implement theme selection with `EmailThemeSelector` in
`email-theme-selector.tsx`.
> - Update sidebar navigation in `sidebar-layout.tsx` to include drafts.
> - **Tests**:
> - Add E2E tests for draft lifecycle and sending in
`email-drafts.test.ts` and `send-email.test.ts`.
> - **Misc**:
> - Update `admin-interface.ts` and `server-interface.ts` to support
draft operations.
> - Add `XOR` type utility in `types.tsx` for exclusive option handling.
>
> <sup>This description was created by </sup>[<img alt="Ellipsis"
src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup>
for c7ebb00ac5. You can
[customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this
summary. It will automatically update as commits are pushed.</sup>
----
<!-- ELLIPSIS_HIDDEN -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Email Drafts: create, list, edit, preview, theme-select, AI-assisted
draft generation, send (marks draft as sent) and dashboard UI for
drafting + recipient selection.
* **Improvements**
* Send/render APIs accept html, template, or draft inputs and an
all-users option; per-recipient delivery/reporting, unified theme
selector, expanded chat context for drafts, clearer schema validation
errors, breadcrumb updates.
* **Tests**
* E2E coverage for draft lifecycle, draft-based send, all-users flow,
and updated schema-validation snapshots.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
906 lines
26 KiB
Plaintext
906 lines
26 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
previewFeatures = ["driverAdapters", "relationJoins"]
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("STACK_DATABASE_CONNECTION_STRING")
|
|
directUrl = env("STACK_DIRECT_DATABASE_CONNECTION_STRING")
|
|
}
|
|
|
|
model Project {
|
|
// Note that the project with ID `internal` is handled as a special case. All other project IDs are UUIDs.
|
|
id String @id
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
displayName String
|
|
description String @default("")
|
|
isProductionMode Boolean
|
|
ownerTeamId String? @db.Uuid
|
|
logoUrl String?
|
|
fullLogoUrl String?
|
|
|
|
projectConfigOverride Json?
|
|
stripeAccountId String?
|
|
|
|
apiKeySets ApiKeySet[]
|
|
projectUsers ProjectUser[]
|
|
provisionedProject ProvisionedProject?
|
|
tenancies Tenancy[]
|
|
environmentConfigOverrides EnvironmentConfigOverride[]
|
|
}
|
|
|
|
model Tenancy {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
projectId String
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
|
|
branchId String
|
|
|
|
// If organizationId is NULL, hasNoOrganization must be TRUE. If organizationId is not NULL, hasNoOrganization must be NULL.
|
|
organizationId String? @db.Uuid
|
|
hasNoOrganization BooleanTrue?
|
|
|
|
@@unique([projectId, branchId, organizationId])
|
|
@@unique([projectId, branchId, hasNoOrganization])
|
|
}
|
|
|
|
model EnvironmentConfigOverride {
|
|
projectId String
|
|
branchId String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
config Json
|
|
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([projectId, branchId])
|
|
}
|
|
|
|
model Team {
|
|
tenancyId String @db.Uuid
|
|
teamId String @default(uuid()) @db.Uuid
|
|
|
|
// Team IDs must be unique across all organizations (but not necessarily across all branches).
|
|
// To model this in the DB, we add two columns that are always equal to tenancy.projectId and tenancy.branchId.
|
|
mirroredProjectId String
|
|
mirroredBranchId String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
displayName String
|
|
clientMetadata Json?
|
|
clientReadOnlyMetadata Json?
|
|
serverMetadata Json?
|
|
profileImageUrl String?
|
|
|
|
teamMembers TeamMember[]
|
|
projectApiKey ProjectApiKey[]
|
|
|
|
@@id([tenancyId, teamId])
|
|
@@unique([mirroredProjectId, mirroredBranchId, teamId])
|
|
}
|
|
|
|
// This is used for fields that are boolean but only the true value is part of a unique constraint.
|
|
// For example if you want to allow only one selected team per user, you can make an optional field with this type and add a unique constraint.
|
|
// Only the true value is considered for the unique constraint, the null value is not.
|
|
enum BooleanTrue {
|
|
TRUE
|
|
}
|
|
|
|
model TeamMember {
|
|
tenancyId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
teamId String @db.Uuid
|
|
|
|
// This will override the displayName of the user in this team.
|
|
displayName String?
|
|
profileImageUrl String?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
team Team @relation(fields: [tenancyId, teamId], references: [tenancyId, teamId], onDelete: Cascade)
|
|
isSelected BooleanTrue?
|
|
teamMemberDirectPermissions TeamMemberDirectPermission[]
|
|
|
|
@@id([tenancyId, projectUserId, teamId])
|
|
@@unique([tenancyId, projectUserId, isSelected])
|
|
}
|
|
|
|
model ProjectUserDirectPermission {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
permissionId String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@unique([tenancyId, projectUserId, permissionId])
|
|
}
|
|
|
|
model TeamMemberDirectPermission {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
teamId String @db.Uuid
|
|
permissionId String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
teamMember TeamMember @relation(fields: [tenancyId, projectUserId, teamId], references: [tenancyId, projectUserId, teamId], onDelete: Cascade)
|
|
|
|
@@unique([tenancyId, projectUserId, teamId, permissionId])
|
|
}
|
|
|
|
model ProjectUser {
|
|
tenancyId String @db.Uuid
|
|
projectUserId String @default(uuid()) @db.Uuid
|
|
|
|
// User IDs must be unique across all organizations (but not necessarily across all branches).
|
|
// To model this in the DB, we add two columns that are always equal to tenancy.projectId and tenancy.branchId.
|
|
mirroredProjectId String
|
|
mirroredBranchId String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
displayName String?
|
|
serverMetadata Json?
|
|
clientReadOnlyMetadata Json?
|
|
clientMetadata Json?
|
|
profileImageUrl String?
|
|
requiresTotpMfa Boolean @default(false)
|
|
totpSecret Bytes?
|
|
isAnonymous Boolean @default(false)
|
|
|
|
projectUserOAuthAccounts ProjectUserOAuthAccount[]
|
|
teamMembers TeamMember[]
|
|
contactChannels ContactChannel[]
|
|
authMethods AuthMethod[]
|
|
|
|
// some backlinks for the unique constraints on some auth methods
|
|
passwordAuthMethod PasswordAuthMethod[]
|
|
passkeyAuthMethod PasskeyAuthMethod[]
|
|
otpAuthMethod OtpAuthMethod[]
|
|
oauthAuthMethod OAuthAuthMethod[]
|
|
SentEmail SentEmail[]
|
|
projectApiKey ProjectApiKey[]
|
|
directPermissions ProjectUserDirectPermission[]
|
|
Project Project? @relation(fields: [projectId], references: [id])
|
|
projectId String?
|
|
userNotificationPreference UserNotificationPreference[]
|
|
|
|
@@id([tenancyId, projectUserId])
|
|
@@unique([mirroredProjectId, mirroredBranchId, projectUserId])
|
|
// indices for sorting and filtering
|
|
@@index([tenancyId, displayName(sort: Asc)], name: "ProjectUser_displayName_asc")
|
|
@@index([tenancyId, displayName(sort: Desc)], name: "ProjectUser_displayName_desc")
|
|
@@index([tenancyId, createdAt(sort: Asc)], name: "ProjectUser_createdAt_asc")
|
|
@@index([tenancyId, createdAt(sort: Desc)], name: "ProjectUser_createdAt_desc")
|
|
}
|
|
|
|
// This should be renamed to "OAuthAccount" as it is not always bound to a user
|
|
// When ever a user goes through the OAuth flow and gets an account ID from the OAuth provider, we store that here.
|
|
model ProjectUserOAuthAccount {
|
|
id String @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
projectUserId String? @db.Uuid
|
|
configOAuthProviderId String
|
|
providerAccountId String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// This is used for the user to distinguish between multiple accounts from the same provider.
|
|
// we might want to add more user info here later
|
|
email String?
|
|
|
|
// Before the OAuth account is connected to a user (for example, in the link oauth process), the projectUser is null.
|
|
projectUser ProjectUser? @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
oauthTokens OAuthToken[]
|
|
oauthAccessToken OAuthAccessToken[]
|
|
|
|
// if allowSignIn is true, oauthAuthMethod must be set
|
|
oauthAuthMethod OAuthAuthMethod?
|
|
allowConnectedAccounts Boolean @default(true)
|
|
allowSignIn Boolean @default(true)
|
|
|
|
@@id([tenancyId, id])
|
|
@@unique([tenancyId, configOAuthProviderId, projectUserId, providerAccountId])
|
|
@@index([tenancyId, projectUserId])
|
|
}
|
|
|
|
enum ContactChannelType {
|
|
EMAIL
|
|
// PHONE
|
|
}
|
|
|
|
model ContactChannel {
|
|
tenancyId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
id String @default(uuid()) @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
type ContactChannelType
|
|
isPrimary BooleanTrue?
|
|
usedForAuth BooleanTrue?
|
|
isVerified Boolean
|
|
value String
|
|
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, projectUserId, id])
|
|
// each user has at most one primary contact channel of each type
|
|
@@unique([tenancyId, projectUserId, type, isPrimary])
|
|
// value must be unique per user per type
|
|
@@unique([tenancyId, projectUserId, type, value])
|
|
// only one contact channel per project with the same value and type can be used for auth
|
|
@@unique([tenancyId, type, value, usedForAuth])
|
|
}
|
|
|
|
model AuthMethod {
|
|
tenancyId String @db.Uuid
|
|
id String @default(uuid()) @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// exactly one of the xyzAuthMethods should be set
|
|
otpAuthMethod OtpAuthMethod?
|
|
passwordAuthMethod PasswordAuthMethod?
|
|
passkeyAuthMethod PasskeyAuthMethod?
|
|
oauthAuthMethod OAuthAuthMethod?
|
|
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, id])
|
|
@@index([tenancyId, projectUserId])
|
|
}
|
|
|
|
model OtpAuthMethod {
|
|
tenancyId String @db.Uuid
|
|
authMethodId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
authMethod AuthMethod @relation(fields: [tenancyId, authMethodId], references: [tenancyId, id], onDelete: Cascade)
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, authMethodId])
|
|
// a user can only have one OTP auth method
|
|
@@unique([tenancyId, projectUserId])
|
|
}
|
|
|
|
model PasswordAuthMethod {
|
|
tenancyId String @db.Uuid
|
|
authMethodId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
passwordHash String
|
|
|
|
authMethod AuthMethod @relation(fields: [tenancyId, authMethodId], references: [tenancyId, id], onDelete: Cascade)
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, authMethodId])
|
|
// a user can only have one password auth method
|
|
@@unique([tenancyId, projectUserId])
|
|
}
|
|
|
|
model PasskeyAuthMethod {
|
|
tenancyId String @db.Uuid
|
|
authMethodId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
credentialId String
|
|
publicKey String
|
|
userHandle String
|
|
transports String[]
|
|
credentialDeviceType String
|
|
counter Int
|
|
|
|
authMethod AuthMethod @relation(fields: [tenancyId, authMethodId], references: [tenancyId, id], onDelete: Cascade)
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, authMethodId])
|
|
// a user can only have one password auth method
|
|
@@unique([tenancyId, projectUserId])
|
|
}
|
|
|
|
// This connects to projectUserOauthAccount, which might be shared between auth method and connected account.
|
|
model OAuthAuthMethod {
|
|
tenancyId String @db.Uuid
|
|
authMethodId String @db.Uuid
|
|
configOAuthProviderId String
|
|
providerAccountId String
|
|
projectUserId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
authMethod AuthMethod @relation(fields: [tenancyId, authMethodId], references: [tenancyId, id], onDelete: Cascade)
|
|
oauthAccount ProjectUserOAuthAccount @relation(fields: [tenancyId, configOAuthProviderId, projectUserId, providerAccountId], references: [tenancyId, configOAuthProviderId, projectUserId, providerAccountId])
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, authMethodId])
|
|
@@unique([tenancyId, configOAuthProviderId, providerAccountId])
|
|
@@unique([tenancyId, projectUserId, configOAuthProviderId])
|
|
@@unique([tenancyId, configOAuthProviderId, projectUserId, providerAccountId])
|
|
}
|
|
|
|
enum StandardOAuthProviderType {
|
|
GITHUB
|
|
FACEBOOK
|
|
GOOGLE
|
|
MICROSOFT
|
|
SPOTIFY
|
|
DISCORD
|
|
GITLAB
|
|
BITBUCKET
|
|
LINKEDIN
|
|
APPLE
|
|
X
|
|
TWITCH
|
|
}
|
|
|
|
model OAuthToken {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
|
|
tenancyId String @db.Uuid
|
|
oauthAccountId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
projectUserOAuthAccount ProjectUserOAuthAccount @relation(fields: [tenancyId, oauthAccountId], references: [tenancyId, id], onDelete: Cascade)
|
|
|
|
refreshToken String
|
|
scopes String[]
|
|
isValid Boolean @default(true)
|
|
}
|
|
|
|
model OAuthAccessToken {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
|
|
tenancyId String @db.Uuid
|
|
oauthAccountId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
projectUserOAuthAccount ProjectUserOAuthAccount @relation(fields: [tenancyId, oauthAccountId], references: [tenancyId, id], onDelete: Cascade)
|
|
|
|
accessToken String
|
|
scopes String[]
|
|
expiresAt DateTime
|
|
isValid Boolean @default(true)
|
|
}
|
|
|
|
model OAuthOuterInfo {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
|
|
info Json
|
|
innerState String @unique
|
|
expiresAt DateTime
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model ProjectUserRefreshToken {
|
|
id String @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
refreshToken String @unique
|
|
expiresAt DateTime?
|
|
isImpersonation Boolean @default(false)
|
|
|
|
@@id([tenancyId, id])
|
|
}
|
|
|
|
model ProjectUserAuthorizationCode {
|
|
tenancyId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
authorizationCode String @unique
|
|
redirectUri String
|
|
expiresAt DateTime
|
|
|
|
codeChallenge String
|
|
codeChallengeMethod String
|
|
|
|
newUser Boolean
|
|
afterCallbackRedirectUrl String?
|
|
|
|
@@id([tenancyId, authorizationCode])
|
|
}
|
|
|
|
model VerificationCode {
|
|
projectId String
|
|
branchId String
|
|
id String @default(uuid()) @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
type VerificationCodeType
|
|
code String
|
|
expiresAt DateTime
|
|
usedAt DateTime?
|
|
redirectUrl String?
|
|
method Json @default("null")
|
|
data Json
|
|
attemptCount Int @default(0)
|
|
|
|
@@id([projectId, branchId, id])
|
|
@@unique([projectId, branchId, code])
|
|
@@index([data(ops: JsonbPathOps)], type: Gin)
|
|
}
|
|
|
|
enum VerificationCodeType {
|
|
ONE_TIME_PASSWORD
|
|
PASSWORD_RESET
|
|
CONTACT_CHANNEL_VERIFICATION
|
|
TEAM_INVITATION
|
|
MFA_ATTEMPT
|
|
PASSKEY_REGISTRATION_CHALLENGE
|
|
PASSKEY_AUTHENTICATION_CHALLENGE
|
|
INTEGRATION_PROJECT_TRANSFER
|
|
PURCHASE_URL
|
|
}
|
|
|
|
//#region API keys
|
|
// Internal API keys
|
|
model ApiKeySet {
|
|
projectId String
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
id String @default(uuid()) @db.Uuid
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
description String
|
|
expiresAt DateTime
|
|
manuallyRevokedAt DateTime?
|
|
publishableClientKey String? @unique
|
|
secretServerKey String? @unique
|
|
superSecretAdminKey String? @unique
|
|
|
|
@@id([projectId, id])
|
|
}
|
|
|
|
//#endregion
|
|
|
|
model ProjectApiKey {
|
|
tenancyId String @db.Uuid
|
|
|
|
id String @default(uuid()) @db.Uuid
|
|
secretApiKey String @unique
|
|
|
|
// Validity and revocation
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
expiresAt DateTime?
|
|
manuallyRevokedAt DateTime?
|
|
description String
|
|
isPublic Boolean
|
|
|
|
// exactly one of [teamId] or [projectUserId] must be set
|
|
teamId String? @db.Uuid
|
|
projectUserId String? @db.Uuid
|
|
|
|
projectUser ProjectUser? @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
team Team? @relation(fields: [tenancyId, teamId], references: [tenancyId, teamId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, id])
|
|
}
|
|
|
|
enum EmailTemplateType {
|
|
EMAIL_VERIFICATION
|
|
PASSWORD_RESET
|
|
MAGIC_LINK
|
|
TEAM_INVITATION
|
|
SIGN_IN_INVITATION
|
|
}
|
|
|
|
model EmailTemplate {
|
|
projectId String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
content Json
|
|
type EmailTemplateType
|
|
subject String
|
|
|
|
@@id([projectId, type])
|
|
}
|
|
|
|
//#region IdP
|
|
model IdPAccountToCdfcResultMapping {
|
|
idpId String
|
|
id String
|
|
|
|
idpAccountId String @unique @db.Uuid
|
|
cdfcResult Json
|
|
|
|
@@id([idpId, id])
|
|
}
|
|
|
|
model ProjectWrapperCodes {
|
|
idpId String
|
|
id String @default(uuid()) @db.Uuid
|
|
|
|
interactionUid String
|
|
authorizationCode String @unique
|
|
|
|
cdfcResult Json
|
|
|
|
@@id([idpId, id])
|
|
}
|
|
|
|
model IdPAdapterData {
|
|
idpId String
|
|
model String
|
|
id String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
payload Json
|
|
expiresAt DateTime
|
|
|
|
@@id([idpId, model, id])
|
|
@@index([payload(ops: JsonbPathOps)], type: Gin)
|
|
@@index([expiresAt])
|
|
}
|
|
|
|
//#endregion
|
|
|
|
model ProvisionedProject {
|
|
projectId String @id
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
clientId String
|
|
}
|
|
|
|
//#region Events
|
|
|
|
model Event {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// if isWide == false, then eventEndedAt is always equal to eventStartedAt
|
|
isWide Boolean
|
|
eventStartedAt DateTime
|
|
eventEndedAt DateTime
|
|
|
|
// TODO: add event_type, and at least one of either system_event_type or event_type is always set
|
|
systemEventTypeIds String[]
|
|
data Json
|
|
|
|
// ============================== BEGIN END USER PROPERTIES ==============================
|
|
// Below are properties describing the end user that caused this event to be logged
|
|
// This is different from a request IP. See: apps/backend/src/lib/end-users.tsx
|
|
|
|
// Note that the IP may have been spoofed, unless isEndUserIpInfoGuessTrusted is true
|
|
endUserIpInfoGuessId String? @db.Uuid
|
|
endUserIpInfoGuess EventIpInfo? @relation("EventIpInfo", fields: [endUserIpInfoGuessId], references: [id])
|
|
// If true, then endUserIpInfoGuess is not spoofed (might still be behind VPNs/proxies). If false, then the values may be spoofed.
|
|
isEndUserIpInfoGuessTrusted Boolean @default(false)
|
|
// =============================== END END USER PROPERTIES ===============================
|
|
|
|
@@index([data(ops: JsonbPathOps)], type: Gin)
|
|
}
|
|
|
|
// An IP address that was seen in an event. Use the location fields instead of refetching the location from the ip, as the real-world geoip data may have changed since the event was logged.
|
|
model EventIpInfo {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
|
|
ip String
|
|
|
|
countryCode String?
|
|
regionCode String?
|
|
cityName String?
|
|
latitude Float?
|
|
longitude Float?
|
|
tzIdentifier String?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
events Event[] @relation("EventIpInfo")
|
|
}
|
|
|
|
//#endregion
|
|
|
|
model SentEmail {
|
|
tenancyId String @db.Uuid
|
|
|
|
id String @default(uuid()) @db.Uuid
|
|
|
|
userId String? @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
senderConfig Json
|
|
to String[]
|
|
subject String
|
|
html String?
|
|
text String?
|
|
|
|
error Json?
|
|
user ProjectUser? @relation(fields: [tenancyId, userId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, id])
|
|
}
|
|
|
|
model EmailDraft {
|
|
tenancyId String @db.Uuid
|
|
|
|
id String @default(uuid()) @db.Uuid
|
|
|
|
displayName String
|
|
themeMode DraftThemeMode @default(PROJECT_DEFAULT)
|
|
themeId String?
|
|
tsxSource String
|
|
sentAt DateTime?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@id([tenancyId, id])
|
|
}
|
|
|
|
enum DraftThemeMode {
|
|
PROJECT_DEFAULT
|
|
NONE
|
|
CUSTOM
|
|
}
|
|
|
|
model CliAuthAttempt {
|
|
tenancyId String @db.Uuid
|
|
|
|
id String @default(uuid()) @db.Uuid
|
|
pollingCode String @unique
|
|
loginCode String @unique
|
|
refreshToken String?
|
|
expiresAt DateTime
|
|
usedAt DateTime?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@id([tenancyId, id])
|
|
}
|
|
|
|
model UserNotificationPreference {
|
|
id String @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
notificationCategoryId String @db.Uuid
|
|
|
|
enabled Boolean
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, id])
|
|
@@unique([tenancyId, projectUserId, notificationCategoryId])
|
|
}
|
|
|
|
model ThreadMessage {
|
|
id String @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
threadId String @db.Uuid
|
|
|
|
content Json
|
|
createdAt DateTime @default(now())
|
|
|
|
@@id([tenancyId, id])
|
|
}
|
|
|
|
enum CustomerType {
|
|
USER
|
|
TEAM
|
|
CUSTOM
|
|
}
|
|
|
|
enum SubscriptionStatus {
|
|
active
|
|
trialing
|
|
canceled
|
|
paused
|
|
incomplete
|
|
incomplete_expired
|
|
past_due
|
|
unpaid
|
|
}
|
|
|
|
enum SubscriptionCreationSource {
|
|
PURCHASE_PAGE
|
|
TEST_MODE
|
|
}
|
|
|
|
model Subscription {
|
|
id String @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
customerId String
|
|
customerType CustomerType
|
|
offerId String?
|
|
offer Json
|
|
quantity Int @default(1)
|
|
|
|
stripeSubscriptionId String?
|
|
status SubscriptionStatus
|
|
currentPeriodEnd DateTime
|
|
currentPeriodStart DateTime
|
|
cancelAtPeriodEnd Boolean
|
|
|
|
creationSource SubscriptionCreationSource
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@id([tenancyId, id])
|
|
@@unique([tenancyId, stripeSubscriptionId])
|
|
}
|
|
|
|
model ItemQuantityChange {
|
|
id String @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
customerId String
|
|
itemId String
|
|
quantity Int
|
|
description String?
|
|
expiresAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@id([tenancyId, id])
|
|
@@index([tenancyId, customerId, expiresAt])
|
|
}
|
|
|
|
model DataVaultEntry {
|
|
id String @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
storeId String
|
|
hashedKey String
|
|
encrypted Json // Contains { edkBase64, ciphertextBase64 } from encryptWithKms()
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@id([tenancyId, id])
|
|
@@unique([tenancyId, storeId, hashedKey])
|
|
@@index([tenancyId, storeId])
|
|
}
|
|
|
|
model WorkflowTriggerToken {
|
|
tenancyId String @db.Uuid
|
|
id String @default(uuid()) @db.Uuid
|
|
|
|
tokenHash String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
expiresAt DateTime
|
|
|
|
@@id([tenancyId, id])
|
|
@@unique([tenancyId, tokenHash])
|
|
}
|
|
|
|
model WorkflowTrigger {
|
|
tenancyId String @db.Uuid
|
|
id String @default(uuid()) @db.Uuid
|
|
executionId String @db.Uuid
|
|
|
|
triggerData Json
|
|
|
|
// the following fields determine the state of the trigger:
|
|
// - scheduledAt && !compiledWorkflowId && !output && !error: the trigger is scheduled to be executed
|
|
// - !scheduledAt && !compiledWorkflowId: the trigger was scheduled, but its workflow subsequently deleted. The trigger never ran
|
|
// - !scheduledAt && compiledWorkflowId && !output && !error: the trigger is currently executing
|
|
// - !scheduledAt && compiledWorkflowId && output && !error: the trigger has successfully completed execution
|
|
// - !scheduledAt && compiledWorkflowId && !output && error: the trigger has failed execution
|
|
// All other combinations are invalid.
|
|
scheduledAt DateTime?
|
|
output Json?
|
|
error Json?
|
|
compiledWorkflowId String? @db.Uuid
|
|
compiledWorkflow CompiledWorkflow? @relation(fields: [tenancyId, compiledWorkflowId], references: [tenancyId, id])
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
execution WorkflowExecution @relation(fields: [tenancyId, executionId], references: [tenancyId, id])
|
|
|
|
@@id([tenancyId, id])
|
|
}
|
|
|
|
model WorkflowExecution {
|
|
tenancyId String @db.Uuid
|
|
id String @default(uuid()) @db.Uuid
|
|
|
|
workflowId String
|
|
|
|
triggerIds String[]
|
|
triggers WorkflowTrigger[]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@id([tenancyId, id])
|
|
}
|
|
|
|
model CurrentlyCompilingWorkflow {
|
|
tenancyId String @db.Uuid
|
|
workflowId String
|
|
compilationVersion Int
|
|
sourceHash String
|
|
|
|
startedCompilingAt DateTime @default(now())
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@id([tenancyId, workflowId, compilationVersion, sourceHash])
|
|
}
|
|
|
|
model CompiledWorkflow {
|
|
tenancyId String @db.Uuid
|
|
id String @default(uuid()) @db.Uuid
|
|
workflowId String // note: The workflow with this ID may have been edited or deleted in the meantime, so there may be multiple CompiledWorkflows with the same workflowId
|
|
compilationVersion Int
|
|
sourceHash String
|
|
|
|
// exactly one of [compiledCode, compileError] must be set
|
|
compiledCode String?
|
|
compileError String?
|
|
|
|
compiledAt DateTime @default(now())
|
|
registeredTriggers String[]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
workflowTriggers WorkflowTrigger[]
|
|
|
|
@@id([tenancyId, id])
|
|
@@unique([tenancyId, workflowId, compilationVersion, sourceHash])
|
|
}
|