npm.io
0.43.0 • Published 1 month ago

@prisma/compute-sdk

Licence
Apache-2.0
Version
0.43.0
Deps
9
Size
898 kB
Vulns
0
Weekly
0

@prisma/compute-sdk

TypeScript SDK for deploying and managing applications on Prisma Compute.

Installation

npm install @prisma/compute-sdk @prisma/management-api-sdk

@prisma/management-api-sdk is a peer dependency that provides the authenticated API client.

Prerequisites

You need an authenticated ManagementApiClient from @prisma/management-api-sdk. There are two ways to create one:

Using a service token
import { createManagementApiClient } from "@prisma/management-api-sdk";

const apiClient = createManagementApiClient({
  token: process.env.PRISMA_API_TOKEN,
});
Using OAuth
import { createManagementApiSdk } from "@prisma/management-api-sdk";

const sdk = createManagementApiSdk({
  clientId: "your-client-id",
  redirectUri: "http://localhost:3000/callback",
  tokenStorage: yourTokenStorageImpl, // implements TokenStorage interface
});

// sdk.client is a ManagementApiClient with automatic token refresh
const apiClient = sdk.client;

See the @prisma/management-api-sdk documentation for full details on authentication setup.

Quick start

import { ComputeClient, PreBuilt, Ok } from "@prisma/compute-sdk";
import { createManagementApiClient } from "@prisma/management-api-sdk";

const apiClient = createManagementApiClient({
  token: process.env.PRISMA_API_TOKEN,
});

const compute = new ComputeClient(apiClient);

// Deploy a pre-built application
const result = await compute.deploy({
  strategy: new PreBuilt({
    appPath: "./dist",
    entrypoint: "index.js",
  }),
  projectId: "your-project-id",
  appName: "my-app",
  region: "us-east-1",
});

if (result.isOk()) {
  console.log(`Deployed to ${result.value.deploymentEndpointDomain}`);
} else {
  console.error(`Deploy failed: ${result.error.message}`);
}

API reference

ComputeClient

The main entry point for all operations. Created from a ManagementApiClient:

import { ComputeClient } from "@prisma/compute-sdk";

const compute = new ComputeClient(apiClient);

All methods return Promise<Result<T, E>> — a discriminated union that is either Ok with a value or Err with a typed error. Each method declares only the error variants it can actually produce. See Error handling for details.


deploy(options): Promise<Result<DeployResult, DeployError>>

Builds, uploads, and deploys an application, then promotes it to the app's live endpoint.

const result = await compute.deploy({
  // Required: how to produce the deployable artifact
  strategy: new PreBuilt({ appPath: "./dist", entrypoint: "index.js" }),

  // Target (provide appId OR projectId + appName + region)
  projectId: "proj_abc",
  appName: "my-app",
  region: "us-east-1",
  // OR:
  appId: "app_xyz",

  // Optional
  // Stored through the environment-variable API before the deployment is created.
  // Production apps use project vars; preview-branch apps use branch overrides.
  envVars: { DATABASE_URL: "postgresql://..." },
  portMapping: { http: 3000 },
  timeoutSeconds: 120, // max time to wait for "running" status
  pollIntervalMs: 1000, // how often to check status
  skipPromote: false, // deploy without promoting to the app's live endpoint
  destroyOldDeployment: false, // destroy (rather than just stop) the previously promoted deployment
  signal: abortController.signal,
  progress: {
    /* DeployProgress callbacks */
  },
  interaction: {
    /* DeployInteraction callbacks */
  },
});

if (result.isOk()) {
  const { deploymentEndpointDomain, deploymentId, appId, resolvedConfig } =
    result.value;
}

skipPromote and destroyOldDeployment are mutually exclusive — combining them returns an InvalidOptionsError, since there is no previously-active deployment to destroy when promotion is skipped.

Returns DeployResult:

Field Type Description
projectId string Project ID
appId string App ID
appName string App display name
region string Region identifier
deploymentId string Created deployment ID
deploymentEndpointDomain string Live URL of the new deployment
appEndpointDomain string | null The app's live URL, or null when skipPromote was set
promoted boolean Whether the new deployment was promoted
previousDeploymentId string | null The previously active deployment, if any
previousDeploymentAction "stopped" | "destroyed" | "still-active" | null What happened to the previous deployment
resolvedConfig ResolvedConfig Final resolved configuration

updateEnv(options): Promise<Result<UpdateEnvResult, UpdateEnvError>>

Updates project environment variables and/or port mapping, then creates a new deployment that reuses the code from the most recent deployment. Preview-branch apps update preview branch overrides.

const result = await compute.updateEnv({
  appId: "app_xyz",
  envVars: { DATABASE_URL: "postgresql://new-url..." },
  portMapping: { http: 8080 },
});

To remove an environment variable, set its value to null:

const result = await compute.updateEnv({
  appId: "app_xyz",
  envVars: {
    DATABASE_URL: "postgresql://new-url...", // set or update
    OLD_SECRET: null, // remove
  },
});

The app must have at least one existing deployment. If not, this returns a NoExistingDeploymentError.


destroyDeployment(options): Promise<Result<DestroyDeploymentResult, DestroyDeploymentError>>

Stops (if running) and deletes a single deployment.

const result = await compute.destroyDeployment({
  deploymentId: "dep_abc",
  // OR provide appId + interaction.selectDeployment for interactive selection
});

Returns DestroyDeploymentResult:

Field Type Description
deploymentId string The destroyed deployment ID
previousStatus string Status before destruction
stopped boolean Whether a stop was required
deleted boolean Whether deletion succeeded

destroyApp(options): Promise<Result<DestroyAppResult, DestroyAppError>>

Stops all running deployments, deletes all deployments, and optionally deletes the app itself.

const result = await compute.destroyApp({
  appId: "app_xyz",
  keepApp: false, // set to true to keep the app record
});

If some deployments fail to stop or delete, returns a DestroyAggregateError with details on which succeeded and which failed.


promote(options): Promise<Result<PromoteResult, PromoteError>>

Promotes an existing deployment to the app's live endpoint, starting it first if it isn't already running. Unlike deploy, this does not build, upload, or create a new deployment — it operates on one that already exists.

const result = await compute.promote({
  appId: "app_xyz",
  deploymentId: "dep_abc",
  // OR provide interaction.selectDeployment for interactive selection
});

Returns PromoteResult:

Field Type Description
appId string App ID
deploymentId string The promoted deployment ID
appEndpointDomain string The app's live URL after promotion
deploymentStarted boolean Whether the deployment had to be started first

createProject(options): Promise<Result<CreateProjectResult, ApiRequestError>>

Creates a new project, optionally provisioning a database alongside it.

const result = await compute.createProject({
  name: "my-project",
  createDatabase: true,
  region: "us-east-1",
});

if (result.isOk()) {
  console.log(`Project: ${result.value.id}`);
  if (result.value.database) {
    console.log(`Database: ${result.value.database.connectionString}`);
  }
}

Returns CreateProjectResult (ProjectInfo & { database?: DatabaseInfo }):

Field Type Description
id string Project ID
name string Project name
defaultRegion string | undefined Default region, if set
database DatabaseInfo | undefined Provisioned database, present when createDatabase is true

listProjects(options?): Promise<Result<ProjectInfo[], ApiRequestError>>
const result = await compute.listProjects();
if (result.isOk()) {
  for (const project of result.value) {
    console.log(`${project.name} (${project.id})`);
  }
}

listApps(options): Promise<Result<AppInfo[], ApiRequestError>>
const result = await compute.listApps({ projectId: "proj_abc" });

createApp(options): Promise<Result<AppInfo, ApiRequestError>>
const result = await compute.createApp({
  projectId: "proj_abc",
  appName: "my-new-app",
  region: "eu-west-3",
});

showApp(options): Promise<Result<AppDetail, ApiRequestError>>
const result = await compute.showApp({ appId: "app_xyz" });
if (result.isOk()) {
  console.log(`Latest deployment: ${result.value.latestDeploymentId}`);
}

deleteApp(options): Promise<Result<void, ApiRequestError>>

Deletes an app record. The app should have no deployments (use destroyApp to clean up deployments first).

await compute.deleteApp({ appId: "app_xyz" });

listDeployments(options): Promise<Result<DeploymentInfo[], ApiRequestError>>
const result = await compute.listDeployments({ appId: "app_xyz" });

showDeployment(options): Promise<Result<DeploymentDetail, ApiRequestError>>
const result = await compute.showDeployment({ deploymentId: "dep_abc" });
if (result.isOk()) {
  console.log(`Status: ${result.value.status}`);
  console.log(`URL: https://${result.value.previewDomain}`);
}

startDeployment(options): Promise<Result<void, ApiRequestError>>
await compute.startDeployment({ deploymentId: "dep_abc" });

stopDeployment(options): Promise<Result<void, ApiRequestError>>
await compute.stopDeployment({ deploymentId: "dep_abc" });

deleteDeployment(options): Promise<Result<void, ApiRequestError>>
await compute.deleteDeployment({ deploymentId: "dep_abc" });

Repository snapshot detection

Use detectComputeRepository when a consumer has repository metadata but no local checkout, such as a GitHub import flow. The SDK discovers workspace packages, excludes non-deployable packages, distinguishes a real root app from a root script that delegates to a workspace app, and infers editable build settings for every deploy target.

import { detectComputeRepository } from "@prisma/compute-sdk/config";

const detected = await detectComputeRepository({
  repositoryName: "platform",
  filePaths: repositoryTree.map((entry) => entry.path),
  readFile: async (path) => repositoryFiles.get(path) ?? null,
});

The result contains all inferred apps plus warnings for incomplete or ambiguous repository metadata. readFile is called only for package manifests and workspace metadata, so the consumer controls how remote contents are fetched.

Use detectComputeApp for the lower-level case where the caller intentionally detects one known app root.

Node.js consumers inspecting a local app directory can use the filesystem adapter instead of building a snapshot:

import { detectComputeAppFromDirectory } from "@prisma/compute-sdk/config/directory";

const detected = await detectComputeAppFromDirectory({
  appPath: process.cwd(),
});

Build strategies

A build strategy produces a deployable artifact (a directory with an entrypoint file). The SDK ships with two built-in general-purpose strategies (plus framework-specific ones used internally by AutoBuild):

PreBuilt

Use when your application is already built (e.g., output of tsc, esbuild, or any other bundler):

import { PreBuilt } from "@prisma/compute-sdk";

const strategy = new PreBuilt({
  appPath: "./dist", // absolute or relative path to the build output
  entrypoint: "index.js", // relative to appPath
});

PreBuilt validates that the entrypoint exists and is a relative path that doesn't escape the application directory. It performs no copying or transformation.

BunBuild

Use when you want the SDK to bundle your application using Bun:

import { BunBuild } from "@prisma/compute-sdk";

const strategy = new BunBuild({
  appPath: "./my-app", // path to your application source
  entrypoint: "src/index.ts", // optional: resolved from package.json "main" if omitted
});

BunBuild runs bun build with --target bun --sourcemap=external, manages a temporary output directory, and cleans it up after the archive is created. Requires Bun to be installed on the machine.

Custom strategies

Implement the BuildStrategy interface to use any build tool:

import type { BuildStrategy, BuildArtifact } from "@prisma/compute-sdk";

class MyCustomBuild implements BuildStrategy {
  async execute(): Promise<BuildArtifact> {
    // Run your build process...
    return {
      directory: "/path/to/output", // absolute path to the built files
      entrypoint: "index.js", // relative to directory, posix separators
      cleanup: async () => {
        // optional: called after archiving
        // clean up temp files
      },
    };
  }
}

Error handling

All ComputeClient methods return Result<T, E> from the better-result library instead of throwing exceptions. This gives you exhaustive, type-safe error handling.

Checking results
const result = await compute.deploy({
  /* ... */
});

// Pattern 1: isOk / isErr
if (result.isOk()) {
  console.log(result.value.deploymentEndpointDomain);
} else {
  console.error(result.error.message);
}

// Pattern 2: match
result.match({
  Ok: (value) => console.log(value.deploymentEndpointDomain),
  Err: (error) => console.error(error.message),
});
Error types

Every error extends TaggedError and has a _tag discriminant for pattern matching:

Error class _tag Description
AuthenticationError "AuthenticationError" API returned HTTP 401
ApiError "ApiError" API returned a non-401 error
MissingArgumentError "MissingArgumentError" A required argument was not provided
InvalidOptionsError "InvalidOptionsError" A conflicting or invalid combination of options was provided
BuildError "BuildError" Build strategy failed
ArtifactError "ArtifactError" Archive creation or upload failed
TimeoutError "TimeoutError" Deployment didn't reach target status in time
DeploymentFailedError "DeploymentFailedError" Deployment transitioned to "failed" status
NoExistingDeploymentError "NoExistingDeploymentError" updateEnv called on an app with no prior deployments
NoDeploymentsFoundError "NoDeploymentsFoundError" App has no deployments to select from
CancelledError "CancelledError" Operation cancelled via AbortSignal
DestroyAggregateError "DestroyAggregateError" Some deployments failed during destroyApp
Matching specific errors
import { matchError, ApiError, AuthenticationError } from "@prisma/compute-sdk";

const result = await compute.deploy({
  /* ... */
});

if (result.isErr()) {
  matchError(result.error, {
    AuthenticationError: (e) => {
      console.error("Not authenticated. Check your token.");
    },
    ApiError: (e) => {
      console.error(`API error (${e.statusCode}): ${e.message}`);
      if (e.hint) console.error(`Hint: ${e.hint}`);
    },
    BuildError: (e) => {
      console.error(`Build failed: ${e.message}`);
    },
    TimeoutError: (e) => {
      console.error(`Timed out after ${Math.round(e.elapsedMs / 1000)}s`);
    },
    _: (e) => {
      console.error(`Unexpected error: ${e.message}`);
    },
  });
}
Error type unions

The SDK exports narrowed error unions for each operation:

  • DeployError — errors from deploy(): CancelledError | MissingArgumentError | InvalidOptionsError | AuthenticationError | ApiError | BuildError | ArtifactError | TimeoutError | DeploymentFailedError
  • UpdateEnvError — errors from updateEnv(): CancelledError | MissingArgumentError | InvalidOptionsError | AuthenticationError | ApiError | NoExistingDeploymentError | TimeoutError | DeploymentFailedError
  • DestroyDeploymentError — errors from destroyDeployment(): CancelledError | MissingArgumentError | NoDeploymentsFoundError | AuthenticationError | ApiError | TimeoutError | DeploymentFailedError
  • DestroyAppError — errors from destroyApp(): CancelledError | AuthenticationError | ApiError | DestroyAggregateError
  • PromoteError — errors from promote(): AuthenticationError | ApiError | MissingArgumentError | NoDeploymentsFoundError | TimeoutError | DeploymentFailedError | CancelledError
  • ApiRequestError — errors from single-request methods (createProject, listProjects, listApps, createApp, startDeployment, stopDeployment, deleteDeployment, etc.): CancelledError | AuthenticationError | ApiError

Progress and interaction callbacks

Long-running operations accept progress and interaction callbacks for UI integration.

Deploy progress

deploy() promotes the new deployment and stops (optionally destroys) the previous one by default, so DeployProgress also reports promotion and old-deployment cleanup progress alongside the build/upload/start steps:

await compute.deploy({
  strategy,
  projectId: "proj_abc",
  appName: "my-app",
  region: "us-east-1",
  progress: {
    onBuildStart() {
      console.log("Building...");
    },
    onBuildComplete(artifact) {
      console.log(`Built to ${artifact.directory}`);
    },
    onArchiveCreating() {
      console.log("Creating archive...");
    },
    onArchiveReady(sizeBytes) {
      console.log(`Archive: ${(sizeBytes / 1024).toFixed(1)} KB`);
    },
    onDeploymentCreated(deploymentId) {
      console.log(`Deployment: ${deploymentId}`);
    },
    onUploadStart() {
      console.log("Uploading...");
    },
    onUploadComplete() {
      console.log("Upload complete.");
    },
    onStartRequested() {
      console.log("Starting...");
    },
    onStatusChange(status) {
      console.log(`Status: ${status}`);
    },
    onRunning(deploymentUrl) {
      console.log(`Live at ${deploymentUrl}`);
    },
    onPromoteStart() {
      console.log("Promoting...");
    },
    onPromoted(appEndpointDomain) {
      console.log(`Promoted: ${appEndpointDomain}`);
    },
    onOldDeploymentStopping(deploymentId) {
      console.log(`Stopping previous deployment ${deploymentId}...`);
    },
    onOldDeploymentStopped(deploymentId) {
      console.log(`Stopped previous deployment ${deploymentId}.`);
    },
  },
});
Deploy interaction

When projectId, appId/appName, or region aren't provided, the SDK calls interaction callbacks so the caller can resolve them (e.g., by prompting the user):

await compute.deploy({
  strategy,
  interaction: {
    async selectProject(projects) {
      // projects: ProjectInfo[] — return the chosen project ID
      return projects[0].id;
    },
    async selectApp(apps) {
      // apps: AppInfo[] — return an app ID, or null to create a new one
      return null;
    },
    async provideAppName() {
      // return a name for the new app
      return "my-new-app";
    },
    async selectRegion(regions) {
      // regions: RegionInfo[] — used to resolve a region when one wasn't supplied
      return "us-east-1";
    },
  },
});
Destroy progress
await compute.destroyApp({
  appId: "app_xyz",
  progress: {
    onStoppingDeployments(deploymentIds) {
      /* ... */
    },
    onDeploymentStopped(deploymentId) {
      /* ... */
    },
    onAllDeploymentsStopped() {
      /* ... */
    },
    onDeletingDeployments(deploymentIds) {
      /* ... */
    },
    onDeploymentDeleted(deploymentId) {
      /* ... */
    },
    onAllDeploymentsDeleted() {
      /* ... */
    },
    onAppDeleted(appId) {
      /* ... */
    },
  },
});

Cancellation

All operations support cancellation via the standard AbortSignal:

const controller = new AbortController();

// Cancel after 30 seconds
setTimeout(() => controller.abort(), 30_000);

const result = await compute.deploy({
  strategy,
  appId: "app_xyz",
  signal: controller.signal,
});

if (result.isErr() && result.error._tag === "CancelledError") {
  console.log("Deployment was cancelled.");
}

Domain types

import type {
  ProjectInfo, // { id, name, defaultRegion? }
  AppInfo, // { id, name, region, projectId, createdAt? }
  AppDetail, // AppInfo & { latestDeploymentId?, appEndpointDomain? }
  DeploymentInfo, // { id, status, createdAt, previewDomain? }
  DeploymentDetail, // DeploymentInfo & { envVars?, foundryVersionId? }
  RegionInfo, // { id, displayName }
  ResolvedConfig, // { projectId, appId, appName, region, portMapping? }
  PortMapping, // { http?: number | null }
} from "@prisma/compute-sdk";
Available regions
import { REGIONS, KNOWN_REGION_IDS } from "@prisma/compute-sdk";

// KNOWN_REGION_IDS: readonly ["us-east-1", "us-west-1", "eu-west-3", "eu-central-1", "ap-northeast-1", "ap-southeast-1"]
// REGIONS: RegionInfo[] — same IDs with displayName

Full example

import { ComputeClient, PreBuilt, matchError } from "@prisma/compute-sdk";
import { createManagementApiClient } from "@prisma/management-api-sdk";

async function main() {
  const apiClient = createManagementApiClient({
    token: process.env.PRISMA_API_TOKEN!,
  });

  const compute = new ComputeClient(apiClient);

  // Deploy
  const deployResult = await compute.deploy({
    strategy: new PreBuilt({
      appPath: "./dist",
      entrypoint: "server.js",
    }),
    projectId: process.env.PROJECT_ID!,
    appName: "my-api",
    region: "us-east-1",
    envVars: {
      DATABASE_URL: process.env.DATABASE_URL!,
      NODE_ENV: "production",
    },
    portMapping: { http: 3000 },
    progress: {
      onBuildStart: () => console.log("Preparing artifact..."),
      onUploadStart: () => console.log("Uploading..."),
      onStartRequested: () => console.log("Starting..."),
      onStatusChange: (s) => console.log(`  Status: ${s}`),
      onRunning: (url) => console.log(`Deployed: ${url}`),
    },
  });

  if (deployResult.isErr()) {
    matchError(deployResult.error, {
      AuthenticationError: () => {
        console.error("Invalid token. Set PRISMA_API_TOKEN.");
        process.exit(1);
      },
      BuildError: (e) => {
        console.error(`Build failed: ${e.message}`);
        process.exit(1);
      },
      TimeoutError: (e) => {
        console.error(`Deploy timed out (${Math.round(e.elapsedMs / 1000)}s).`);
        console.error(`Deployment ${e.deploymentId} may still be starting.`);
        process.exit(1);
      },
      _: (e) => {
        console.error(`Error: ${e.message}`);
        process.exit(1);
      },
    });
    return;
  }

  const { appId, deploymentId, deploymentEndpointDomain } = deployResult.value;
  console.log(`\nApp:        ${appId}`);
  console.log(`Deployment: ${deploymentId}`);
  console.log(`URL:        ${deploymentEndpointDomain}`);

  // List deployments
  const deploymentsResult = await compute.listDeployments({ appId });
  if (deploymentsResult.isOk()) {
    console.log(`\nDeployments (${deploymentsResult.value.length}):`);
    for (const d of deploymentsResult.value) {
      console.log(`  ${d.id} — ${d.status} (${d.createdAt})`);
    }
  }
}

main();

Requirements

  • Node.js >= 18.0.0
  • @prisma/management-api-sdk ^1.44.0

License

Apache-2.0