import { ApplicationUserDto, BaseBlock, BranchDto, CommitDto, DatasourceEnvironments, DataTreeGroup, DataTreeUser, DegradedMode, Difference, ExportedApiV3Dto, IApiV3Dto, Plugin, RepositoryDto, SupersetIntegrationDto } from '..'; import { ApplicationSignatureTreeSigned, Signature } from '../../signing/index.js'; import { ResourceMetadata } from '../metadata/index.js'; import { Dimension, EntityWithBindings6, IPageV2Dto, MultiStepDef, Padding, PageDSL, PageSummary } from '../page/index.js'; import { EntityProfileSettings } from '../profile/index.js'; import { IResourceMetadata } from '../resource/index.js'; export interface Application { id: string; environment: DatasourceEnvironments; name: string; // Older apps have this field, but it's unused icon?: string; color: string; organizationId: string; new: boolean; appIsExample: boolean; isPublic: boolean; userPermissions: string[]; deletedAt: Date | undefined; settings: ApplicationSettings; configuration?: ApplicationConfiguration; } export type GetEditorsForAppIdResponse = ApplicationUserDto[]; interface PropertiesPanelDisplay { controlType: 'text' | 'js-expr' | 'switch'; label: string; defaultValue?: string | number | boolean; placeholder?: string; exampleData?: string; expectedType?: string; } /** @pattern ^[a-zA-Z_][a-zA-Z0-9_]*$ */ type Identifier = string; export interface CustomComponentProperty { path: Identifier; dataType: 'string' | 'number' | 'boolean' | 'any'; propertiesPanelDisplay?: PropertiesPanelDisplay; description?: string; // Is the property settable by other components/triggers isExternallySettable?: boolean; // Is the property readable by other components/triggers/apis isExternallyReadable?: boolean; } // Important: These configs must be self-contained with no references to types that might be modified later // because they are snapshots of a point in time export interface PreviousCustomComponentConfigs { '0.0.20': { id: string; name: string; displayName: string; componentPath: string; statefulProperties: Array<{ path: string; label: string; inputType: 'text' | 'number' | 'boolean' | 'js'; placeholder?: string; }>; eventHandlers: Array<{ label: string; path: string }>; }; '0.0.21': { // @format uuid id: string; name: string; displayName: string; componentPath: string; properties: Array<{ path: string; dataType: 'string' | 'number' | 'boolean' | 'any'; propertiesPanelDisplay?: { controlType: 'text' | 'js-expr' | 'switch'; label: string; defaultValue?: string; placeholder?: string; exampleData?: string; }; description?: string; }>; events: Array<{ label: string; path: string }>; }; } export interface CustomComponentConfig { // @format uuid id: string; // Example: "myComponent" name: string; // Example: "My Component" displayName: string; // Example: "components/myComponent/component.tsx" componentPath: string; properties: CustomComponentProperty[]; events: Array<{ label: string; path: Identifier }>; gridDimensions?: { initialColumns: number; initialRows: number; }; } export enum ComponentEvent { CREATE = 'create', INIT = 'init', LOGIN = 'login', MIGRATE = 'migrate', PULL = 'pull', PUSH = 'push', REGISTER = 'register', UPLOAD = 'upload', DEPLOY = 'deploy' } export type RegisteredComponents = Record; // Defines a Route. Can be thought of as a route "template". type BaseRoute = { id: string; path: string; testParams?: Record; onRouteLoad?: MultiStepDef; } & EntityWithBindings6; export type PageRoute = BaseRoute & { pageId: string }; type PageComponentRoute = PageRoute & { widgetId: string }; type RedirectRoute = BaseRoute; export type RouteDef = PageRoute | PageComponentRoute | RedirectRoute; // Necessary because the fallback route might not be saved until the user first modifies their routes export const DEFAULT_ROUTE_ID = 'ahjjxoy521'; // Necessary for backwards compatibility with pre-multi-page applications export function getDefaultRoute(pageId: string): RouteDef { return { id: DEFAULT_ROUTE_ID, pageId, path: '/' }; } export interface ApplicationConfiguration { // if version is not provided, it's assumed to be 1 version?: 1; routes?: Record; dsl?: { version: 8; // needs to match the current PageDSL version, which is 8 as of 7/10/24 stateVars?: PageDSL['stateVars']; timers?: PageDSL['timers']; events?: PageDSL['events']; apis?: PageDSL['apis']; }; } export interface ApplicationConfigurationUpdate extends ApplicationConfiguration { routes: Record; } export enum ThemeMode { LIGHT = 'LIGHT', DARK = 'DARK', AUTO = 'AUTO' } export type Border = { color?: string; width?: Dimension<'px'>; style?: 'solid'; }; export type PerSideBorder = { left: Border; right: Border; top: Border; bottom: Border; }; export type PerCornerBorderRadius = { topLeft: Dimension<'px'>; topRight: Dimension<'px'>; bottomLeft: Dimension<'px'>; bottomRight: Dimension<'px'>; }; export type UserDefinedPalette = { primaryColor: string; appBackgroundColor: string; }; export interface ColorBlock { default: string; disabled?: string; hover?: string; error?: string; active?: string; activeHover?: string; } interface TextColorBlock { textColor: ColorBlock; } interface BackgroundColorBlock { backgroundColor: ColorBlock; } interface BorderColorBlock { borderColor: ColorBlock; } interface BoxShadowBlock { boxShadow: ColorBlock; } export interface IconColorBlock { iconColor: ColorBlock; } interface SizeBlock { height: Dimension; width: Dimension; } export interface ColoredDimensionStyle { size: Dimension; color?: string; } export interface BorderStyleBlock extends BorderColorBlock, BoxShadowBlock { borderWidth: Dimension; borderStyle: string; borderRadius: Dimension; } export interface StyleBlockWithSize extends StyleBlock, SizeBlock {} export interface TextStyleBlock extends TextColorBlock { fontFamily: string; fontSize: string; fontWeight: number; lineHeight: number | string; textDecoration?: string; fontStyle?: 'normal' | 'italic' | 'inherit'; letterSpacing?: string; textTransform?: 'uppercase' | 'lowercase' | 'capitalize' | 'none' | 'inherit'; } export type CustomTypographies = Record< string, { styles: TextStyleBlock; name: string; key: string; } >; export interface StyleBlock extends TextStyleBlock, BorderStyleBlock, BackgroundColorBlock { padding: Padding; } export const CUSTOM_THEME_TYPOGRAPHY_KEY = 'custom'; export type Typographies = { heading1: TextStyleBlock; heading2: TextStyleBlock; heading3: TextStyleBlock; heading4: TextStyleBlock; heading5: TextStyleBlock; heading6: TextStyleBlock; body1: TextStyleBlock; body2: TextStyleBlock; body3: TextStyleBlock; label: TextStyleBlock; inputLabel: TextStyleBlock; inputPlaceholder: TextStyleBlock; inputText: TextStyleBlock; buttonLabel: TextStyleBlock; link: TextStyleBlock; code: TextStyleBlock; [CUSTOM_THEME_TYPOGRAPHY_KEY]?: CustomTypographies; }; export type NonCustomTypography = Exclude; export type FontFamily = { name: string; key: string; url?: string; type: 'google' | 'custom'; weights?: Array; }; export type UserDefinedTheme = { primaryColor: string; mode: ThemeMode; borderRadius: Dimension<'px'>; typeFace: string; // optional because themes were released without padding padding?: Padding; palette?: { dark?: UserDefinedPalette; light?: UserDefinedPalette; }; typographies?: Typographies; // optional: themes were released without typographies availableFonts?: Record; version?: number; // optional: themes were released without versioning }; export type ApplicationSettings = EntityProfileSettings & VersionedApplicationSettings; export type NonVersionedApplicationSettings = Difference; export type VersionedApplicationSettings = { // CDN prefix for components componentBaseUrl?: string; // Custom component definitions registeredComponents?: RegisteredComponents; // Array of source file paths for custom components // @deprecated files?: string[]; // Added by file upload service sourceFiles?: string[]; bundledFiles?: string[]; // Version of the CLI used to build the custom components // it's using a semver, which can't regress cliVersion?: string; // Timestamp of the most recent upload componentsLastUploaded?: string; theme?: UserDefinedTheme; }; export function toExportedApplicationSettings(applicationSettings: ApplicationSettings): ExportedApplicationSettings { return { componentBaseUrl: applicationSettings.componentBaseUrl, registeredComponents: applicationSettings.registeredComponents, files: applicationSettings.files, sourceFiles: applicationSettings.sourceFiles, bundledFiles: applicationSettings.bundledFiles, cliVersion: applicationSettings.cliVersion, componentsLastUploaded: applicationSettings.componentsLastUploaded, theme: applicationSettings.theme }; } export function toNonVersionedApplicationSettings(applicationSettings?: ApplicationSettings): NonVersionedApplicationSettings | undefined { if (!applicationSettings) { return; } return { profiles: applicationSettings.profiles }; } export type ApplicationSettingsResponse = ApplicationSettings & { prefix?: string; }; export interface FullApplicationV2Dto { application: IApplicationV2Dto; metadata?: ResourceMetadata; plugins?: Plugin[]; integrations?: SupersetIntegrationDto[]; page?: IPageV2Dto | null; // @deprecated this is no longer used and currently not even populated in the responses editors?: ApplicationUserDto[]; global?: Partial; apis?: IApiV3Dto[]; versions?: { current: CommitDto; }; // for backwards compatibility } export type ExportedApplication = Omit< FullApplicationV2Dto['application'], | 'updated' | 'created' | 'creator' | 'deletedAt' | 'deployedAt' | 'deployedCommitId' | 'folderId' | 'isDeployed' | 'isPublic' | 'isEditable' | 'userPermissions' | 'settings' | 'pageSummaryList' > & { settings?: ExportedApplicationSettings; pages: { id: string; isDefault: boolean }[]; }; export type ExportedMultiPageApplication = Pick< FullApplicationV2Dto['application'], 'id' | 'name' | 'organizationId' | 'configuration' | 'devEnvEnabled' | 'templateName' > & { settings?: ExportedApplicationSettings; }; export type ExportedPage = Omit; export type ExportedPageWithApis = Omit & { apis?: ExportedApiV3Dto[]; }; type ExportedApplicationSettings = Omit; export type ExportedMultiPageApplicationDto = Omit & { application: ExportedMultiPageApplication; apis?: ExportedApiV3Dto[]; deployedCommitId?: string | null; pages?: ExportedPageWithApis[]; }; export function isFullApplicationV2Dto(value: unknown): value is FullApplicationV2Dto { return ( isExportedApplicationDto(value) && 'updated' in value.application && 'created' in value.application && 'deletedAt' in value.application && 'folderId' in value.application && 'isDeployed' in value.application && 'isPublic' in value.application && 'isEditable' in value.application && 'userPermissions' in value.application ); } export function isExportedApplicationDto(value: unknown): value is ExportedMultiPageApplicationDto { return Object.prototype.hasOwnProperty.call(value, 'application'); } export interface IApplicationV2Dto { id: string; name: string; organizationId: string; pageSummaryList?: PageSummary[]; isPublic: boolean; isDeployed: boolean; deployedCommitId?: string | null; deployedGitSha?: string | null; isEditable: boolean; canDelete?: boolean; userPermissions: string[]; deletedAt: Date | null; updated: Date; created: Date; deployedAt?: Date | null; folderId: string | null; creator?: { id: string; name: string; email: string; }; settings?: ApplicationSettings; signature?: ApplicationSignatureTreeSigned | null; configuration?: ApplicationConfiguration; repoConnection?: { created: Date; repository: RepositoryDto }; currentBranch?: BranchDto; devEnvEnabled?: boolean; /** The name of the template used to create this application (e.g., 'shadcn-demo-app', 'blank-app'). Null for legacy apps. */ templateName?: string | null; degradedMode?: DegradedMode; removedProfiles?: Record>; } export interface IHomepageApplicationV2Dto extends IResourceMetadata { deployedAt?: Date | null; creator?: { id: string; name: string; email: string; }; } export interface ApplicationV2GlobalDto { groups: DataTreeGroup[]; user: DataTreeUser; versions: { current: CommitDto; }; } export type PostApplicationCreateRequestBody = { name: string; // @format uuid organizationId: string; isPublic?: boolean; // @format uuid folderId?: string; settings?: Partial; configuration?: ApplicationConfiguration; // TODO: remove this once the UI is updated to use the RPC socket codepath signature?: ApplicationSignatureTreeSigned; ipCountry?: string; //ISO-3166-1 alpha-2 codes /** only used for DSL applications */ initialPageDSL?: PageDSL; /** only used for React applications */ createUsingReact?: boolean; /** which template to use when creating a React/code-mode application (e.g. 'shadcn-demo-app', 'blank-app') */ templateName?: string; /** When provided, server generates an AI app name from this prompt */ initialPrompt?: string; }; export interface ApplicationPageClonePayload { applicationId: string; pageId: string; pageName: string; routePath: string; branch?: string; lastSuccessfulWrite: number; signingRequired: boolean; } export interface CreateApplicationPagePayload { // @format uuid, remove this field after FE uses new values pageId?: string; dsl?: PageDSL; // TODO (ahmet) make this required routePath?: string; testParams?: Record; // TODO (ahmet) remove this field after FE uses new values signature?: ApplicationSignatureTreeSigned; lastSuccessfulWrite: number; } export type CreateApplicationPageSocketPayload = CreateApplicationPagePayload & { applicationId: string; signingRequired: boolean; branchName?: string; superblocksSupportUpdateEnabled?: boolean; }; export type ModifyEntitiesPayload = { applicationId: string; appConfiguration: ApplicationConfigurationUpdate; // currently meant to store the current page DSL and changes pages: ModifyEntitiesPayloadPage[]; apis: ModifyEntitiesPayloadApi[]; renames?: ModifyEntityiesPayloadRename[]; branch?: string; lastSuccessfulWrite: number; signingRequired: boolean; }; type ModifyEntitiesPayloadPage = { pageId: string; dsl: PageDSL; }; export type ModifyEntitiesPayloadApi = { apiId: string; pageId?: string; // matches closely to what the client would send dsl: { metadata?: Record; blocks?: BaseBlock[]; trigger: Record; signature?: Signature; }; }; export type ModifyEntityiesPayloadRename = { newName: string; oldName: string; entityId: string; }; export type ModifyEntitiesResponse = { updatedTime: number; updatedAPIs?: string[]; signature?: ApplicationSignatureTreeSigned; }; export enum ApplicationScope { APP = 'APP', PAGE = 'PAGE', GLOBAL = 'GLOBAL' } export type ApplicationHashChangeSource = | 'ai:reject' | 'ai:accept' | 'ai:generate:pre' | 'ai:generate:interim' | 'ai:generate:post' | 'rootUpdate' | 'multiRootUpdate' | 'rootReset' | 'fileChanged' | 'apiUpdate' | 'apiManualUpdate' | 'apiDelete' | 'apiManualDelete' | 'requestedFilesSync' | 'cli:sdk' | 'cli:git-sync' // Brownfield npm private-registry migration (APPS-4195, historical). // Emitted by the now-removed background install/upload path in the // dev-server SDK; retained as a known ApplicationHashChangeSource so // existing audit records still parse. Mapped to action='none' (no commit). | 'cli:private-registry-migration' | 'initialSync' // Atomic V2 -> V3 template migration: the vite dev server performs the // deterministic folder restructure and calls setApplicationHash with this // source + a targetTemplateName. The handler flips application_v2.template_name // and creates the first commit on the new template in the same transaction, // then terminates the SABS live-edit pod so the next pod picks up the new // TEMPLATE_NAME env var. Older deployed commits retain their (legacy/null) // template_name so they keep running on their original template. | 'migrate:templateFlip' // Pre-migration backup: the vite dev server calls setApplicationHash with // this source *before* running the deterministic folder restructure, so the // user has a named SAVEPOINT_MANUAL commit pointing at the pre-v3 tree. The // hash is unchanged (this is a checkpoint of the current live-edit state), // so the handler bypasses the "no commit if hash unchanged" early return. // template_name is inherited from the still-unflipped application_v2 row, so // the backup resolves to the legacy template via effectiveCommitTemplateName. | 'migrate:backup' // Explicit completion checkpoint after v2->v3 migration succeeds and local // scratch artifacts are removed. | 'migrate:complete'; /** * Why setApplicationHash returned no `commitId` even though the call succeeded. * These are the by-design no-checkpoint paths. The dev server, which always * supplies a `source`, uses this to log by-design skips at info instead of * warning on every autosave; for it, absence of both `commitId` and this field * is the only genuine "checkpoint creation failed" signal. (The server-side * `no-source` path also returns a commit-less response without this field, but * the dev server never omits the source, so that case does not arise here.) * * - 'draft-autosave' a DRAFT autosave was written inside an active draft session * (a commit exists, but its id is intentionally withheld) * - 'no-changes' the tree hash matches the last checkpoint, so nothing to commit * - 'no-op-source' the change source never produces a commit (e.g. ai:generate:pre) */ export type CheckpointSkipReason = 'draft-autosave' | 'no-changes' | 'no-op-source'; /** * Returns true if the application's template uses the SDK API structure. * Legacy apps (templateName null/undefined) use YAML API. * * Must stay in sync with the `sdkApi` field in packages/demo-apps/config.json. */ const SDK_API_TEMPLATES = new Set(['app-fullstack']); export function isSdkApiTemplate(templateName: string | null | undefined): boolean { if (!templateName) return false; return SDK_API_TEMPLATES.has(templateName); } /** * The product generation an application belongs to, as reported to analytics and * billing. The values are the customer-facing version labels so downstream * consumers (Snowflake, billing) can filter on them directly. */ export enum ApplicationType { /** Legacy drag-and-drop application (not code mode). */ V1 = '1.0', /** Code-mode application on a non-SDK-API template. */ V2 = '2.0', /** Code-mode application on an SDK API template. */ V3 = '3.0' } /** * Classifies an application as 1.0 / 2.0 / 3.0 from the two fields that already * live on the application row: `dev_env_enabled` separates legacy (1.0) from * code-mode (2.0+), and the SDK API template separates 3.0 from 2.0. A 2.0 to * 3.0 migration is a `template_name` flip on the same application row, so this * classification follows the app across a migration with no extra lookup. * * Both inputs must come from the application ROW. In particular `templateName` * must not be a commit's effective template (see `effectiveCommitTemplateName`): * a commit predating a 2.0 to 3.0 migration resolves to the legacy template and * would misreport a 3.0 app as 2.0. * * Returns `undefined` when the app cannot be classified because a field was not * projected by the caller's query. `undefined` is distinguishable from a real * `null` template (a legacy template, a valid 2.0 value), so an unselected * column yields no answer instead of a confidently wrong one. */ export function getApplicationType(application: { devEnvEnabled?: boolean | null; templateName?: string | null; }): ApplicationType | undefined { // `dev_env_enabled` is NOT NULL DEFAULT false, so a nullish value here means // the column was not selected rather than a legitimate absence. if (application.devEnvEnabled == null) { return undefined; } if (!application.devEnvEnabled) { return ApplicationType.V1; } // `template_name` IS nullable, so only `undefined` means "not projected". if (application.templateName === undefined) { return undefined; } return isSdkApiTemplate(application.templateName) ? ApplicationType.V3 : ApplicationType.V2; } /** * The legacy template name that commits predating the per-commit `template_name` * column are treated as. NULL on `application_commit.template_name` means the * commit was created before the per-commit column existed (or by a legacy * write path), and must be rendered with the old YAML/shadcn code path. */ export const LEGACY_COMMIT_TEMPLATE_NAME = 'shadcn-demo-app'; /** * Resolves the effective template name for a given commit. NULL means "legacy * template" (rendered as `shadcn-demo-app`); anything non-null is the explicit * template the commit was created against. * * Use this helper anywhere `isSdkApiTemplate` is evaluated against a commit * rather than the application row (e.g. when serving preview/deployed versions). */ export function effectiveCommitTemplateName(commitTemplateName: string | null | undefined): string { return commitTemplateName ?? LEGACY_COMMIT_TEMPLATE_NAME; } /** * Convenience: does the commit-scoped template use the SDK API structure? * Equivalent to `isSdkApiTemplate(effectiveCommitTemplateName(commitTemplateName))`. */ export function isSdkApiCommitTemplate(commitTemplateName: string | null | undefined): boolean { return isSdkApiTemplate(effectiveCommitTemplateName(commitTemplateName)); }