mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-12 13:37:19 +02:00
6244 lines
208 KiB
TypeScript
6244 lines
208 KiB
TypeScript
import * as Provider from "@/Provider";
|
|
import { Unowned } from "@/AdoptPolicy";
|
|
import { InstanceId } from "@/InstanceId";
|
|
import { Branch as PrismaBranch, BranchProvider } from "@/Prisma/Branch";
|
|
import {
|
|
PrismaApiError,
|
|
PrismaClient,
|
|
type DatabaseCreateResult,
|
|
} from "@/Prisma/Client";
|
|
import { Compute as PrismaCompute } from "@/Prisma/Compute";
|
|
import { App as PrismaApp, AppProvider } from "@/Prisma/App";
|
|
import {
|
|
Deployment as PrismaDeployment,
|
|
DeploymentProvider,
|
|
} from "@/Prisma/Deployment";
|
|
import { Connect, ConnectBinding, connectEnvKeys } from "@/Prisma/Connect";
|
|
import {
|
|
Connection as PrismaConnection,
|
|
ConnectionProvider,
|
|
} from "@/Prisma/Connection";
|
|
import {
|
|
CustomDomain as PrismaCustomDomain,
|
|
CustomDomainProvider,
|
|
} from "@/Prisma/CustomDomain";
|
|
import {
|
|
Database as PrismaDatabase,
|
|
DatabaseProvider,
|
|
} from "@/Prisma/Database";
|
|
import {
|
|
EnvironmentVariable as PrismaEnvironmentVariable,
|
|
EnvironmentVariableProvider,
|
|
} from "@/Prisma/EnvironmentVariable";
|
|
import { Project as PrismaProject, ProjectProvider } from "@/Prisma/Project";
|
|
import { Providers as PrismaProviderCollection } from "@/Prisma/Providers";
|
|
import {
|
|
SourceRepository as PrismaSourceRepository,
|
|
SourceRepositoryProvider,
|
|
} from "@/Prisma/SourceRepository";
|
|
import * as Output from "@/Output";
|
|
import type { PrismaManagementClient } from "@/Prisma/Client";
|
|
import type { Database as ApiDatabase } from "@/Prisma/Types";
|
|
import { RuntimeContext } from "@/RuntimeContext";
|
|
import { Self } from "@/Self";
|
|
import { Stack, type StackSpec } from "@/Stack";
|
|
import { inMemoryState } from "@/State/InMemoryState";
|
|
import { Stage } from "@/Stage";
|
|
import { describe, expect, it } from "alchemy-test";
|
|
import {
|
|
type Captured,
|
|
conflict,
|
|
data as envelope,
|
|
dispatchTo,
|
|
failure,
|
|
json,
|
|
makeFakeManagementApi,
|
|
notFound,
|
|
page,
|
|
routesOf,
|
|
unhandled,
|
|
wireConnection,
|
|
wireCreatedDatabase,
|
|
wireDatabase,
|
|
wireProject,
|
|
} from "./fixtures/FakeManagementApi.ts";
|
|
import { testStackContext } from "./fixtures/StackContext.ts";
|
|
import { PlatformServices } from "@/Util/PlatformServices";
|
|
import * as ConfigProvider from "effect/ConfigProvider";
|
|
import * as Effect from "effect/Effect";
|
|
import * as Layer from "effect/Layer";
|
|
import * as Redacted from "effect/Redacted";
|
|
import * as Result from "effect/Result";
|
|
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
import { AlchemyContext } from "@/AlchemyContext";
|
|
|
|
type Call = [operation: string, input?: unknown];
|
|
type Equal<A, B> =
|
|
(<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
|
|
? true
|
|
: false;
|
|
type Expect<T extends true> = T;
|
|
|
|
const createdAt = "2026-01-01T00:00:00Z";
|
|
const updatedAt = "2026-01-01T00:00:01Z";
|
|
|
|
const redactedValue = (
|
|
value: string | Redacted.Redacted<string> | undefined,
|
|
) => {
|
|
if (!Redacted.isRedacted(value)) {
|
|
throw new Error("Expected a redacted value");
|
|
}
|
|
return Redacted.value(value);
|
|
};
|
|
|
|
const expectJsonNotToContain = (value: unknown, ...secrets: string[]) => {
|
|
const json = JSON.stringify(value);
|
|
for (const secret of secrets) {
|
|
expect(json).not.toContain(secret);
|
|
}
|
|
};
|
|
|
|
const resourceRef = (kind: string, id: string, name = id) => ({
|
|
id,
|
|
url: `https://api.prisma.test/v1/${kind}/${id}`,
|
|
name,
|
|
});
|
|
|
|
const databaseConnection = (
|
|
databaseId: string,
|
|
connectionId = `connection-${databaseId}`,
|
|
) => ({
|
|
id: connectionId,
|
|
type: "connection" as const,
|
|
url: `https://api.prisma.test/v1/connections/${connectionId}`,
|
|
name: "default",
|
|
createdAt,
|
|
kind: "postgres" as const,
|
|
endpoints: {
|
|
direct: {
|
|
host: "db.prisma.test",
|
|
port: 5432,
|
|
connectionString: `postgres://user:password@db.prisma.test/${databaseId}`,
|
|
},
|
|
},
|
|
database: resourceRef("databases", databaseId),
|
|
});
|
|
|
|
const makeClient = () => {
|
|
const calls: Call[] = [];
|
|
const client = {
|
|
listProjects: () => {
|
|
calls.push(["listProjects"]);
|
|
return Effect.succeed([]);
|
|
},
|
|
createProject: (input: unknown) => {
|
|
calls.push(["createProject", input]);
|
|
return Effect.succeed({
|
|
id: "project-1",
|
|
type: "project",
|
|
url: "https://api.prisma.test/v1/projects/project-1",
|
|
name: "app",
|
|
createdAt,
|
|
defaultRegion: "us-east-1",
|
|
workspace: resourceRef("workspaces", "workspace-1", "team"),
|
|
database: null,
|
|
});
|
|
},
|
|
listProjectDatabases: (projectId: string, query: unknown) => {
|
|
calls.push(["listProjectDatabases", { projectId, query }]);
|
|
return Effect.succeed([]);
|
|
},
|
|
createDatabase: (input: unknown) => {
|
|
calls.push(["createDatabase", input]);
|
|
return Effect.succeed({
|
|
id: "database-1",
|
|
type: "database",
|
|
url: "https://api.prisma.test/v1/databases/database-1",
|
|
name: "main",
|
|
status: "ready",
|
|
createdAt,
|
|
isDefault: false,
|
|
defaultConnectionId: "connection-1",
|
|
connections: [
|
|
{
|
|
id: "connection-1",
|
|
type: "connection",
|
|
url: "https://api.prisma.test/v1/connections/connection-1",
|
|
name: "default",
|
|
createdAt,
|
|
kind: "postgres",
|
|
endpoints: {
|
|
direct: {
|
|
host: "db.prisma.test",
|
|
port: 5432,
|
|
connectionString: "postgres://direct",
|
|
},
|
|
pooled: {
|
|
host: "pool.prisma.test",
|
|
port: 5432,
|
|
connectionString: "postgres://pooled",
|
|
},
|
|
},
|
|
database: resourceRef("databases", "database-1", "main"),
|
|
},
|
|
],
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
region: { id: "us-east-1", name: "US East" },
|
|
source: { type: "empty" },
|
|
branchId: null,
|
|
});
|
|
},
|
|
listDatabaseConnections: (databaseId: string, query: unknown) => {
|
|
calls.push(["listDatabaseConnections", { databaseId, query }]);
|
|
return Effect.succeed([]);
|
|
},
|
|
createConnection: (input: unknown) => {
|
|
calls.push(["createConnection", input]);
|
|
return Effect.succeed({
|
|
id: "connection-2",
|
|
type: "connection",
|
|
url: "https://api.prisma.test/v1/connections/connection-2",
|
|
name: "api",
|
|
createdAt,
|
|
kind: "postgres",
|
|
endpoints: {
|
|
direct: {
|
|
host: "db.prisma.test",
|
|
port: 5432,
|
|
connectionString: "postgres://api-direct",
|
|
},
|
|
},
|
|
database: resourceRef("databases", "database-1", "main"),
|
|
});
|
|
},
|
|
listBranches: (projectId: string, query: unknown) => {
|
|
calls.push(["listBranches", { projectId, query }]);
|
|
const gitName = (query as { gitName?: string } | undefined)?.gitName;
|
|
return Effect.succeed(
|
|
gitName !== undefined && gitName !== "main"
|
|
? []
|
|
: [
|
|
{
|
|
id: "branch-1",
|
|
type: "branch",
|
|
url: "https://api.prisma.test/v1/branches/branch-1",
|
|
gitName: "main",
|
|
isDefault: true,
|
|
role: "production",
|
|
createdAt,
|
|
updatedAt,
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
},
|
|
],
|
|
);
|
|
},
|
|
getBranch: (id: string) => {
|
|
calls.push(["getBranch", id]);
|
|
return Effect.succeed({
|
|
id,
|
|
type: "branch",
|
|
url: `https://api.prisma.test/v1/branches/${id}`,
|
|
gitName: "main",
|
|
isDefault: true,
|
|
role: "production",
|
|
createdAt,
|
|
updatedAt,
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
});
|
|
},
|
|
createBranch: (
|
|
projectId: string,
|
|
input: { gitName: string; isDefault?: boolean },
|
|
) => {
|
|
calls.push(["createBranch", { projectId, input }]);
|
|
return Effect.succeed({
|
|
id: "branch-2",
|
|
type: "branch",
|
|
url: "https://api.prisma.test/v1/branches/branch-2",
|
|
gitName: input.gitName,
|
|
isDefault: input.isDefault ?? false,
|
|
role: "preview",
|
|
createdAt,
|
|
updatedAt,
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
});
|
|
},
|
|
listApps: (query: unknown) => {
|
|
calls.push(["listApps", query]);
|
|
return Effect.succeed([]);
|
|
},
|
|
createApp: (input: { projectId: string }) => {
|
|
calls.push(["createApp", input]);
|
|
return Effect.succeed({
|
|
id: "service-1",
|
|
type: "app",
|
|
url: "https://api.prisma.test/v1/services/service-1",
|
|
name: "api",
|
|
region: { id: "us-east-1", name: "US East" },
|
|
projectId: "project-1",
|
|
branchId: "branch-1",
|
|
latestDeploymentId: null,
|
|
appEndpointDomain: "service-1.prisma.build",
|
|
createdAt,
|
|
});
|
|
},
|
|
getApp: (id: string) => {
|
|
calls.push(["getApp", id]);
|
|
return Effect.succeed({
|
|
id,
|
|
type: "app",
|
|
url: `https://api.prisma.test/v1/services/${id}`,
|
|
name: "api",
|
|
region: { id: "us-east-1", name: "US East" },
|
|
projectId: "project-1",
|
|
branchId: "branch-1",
|
|
latestDeploymentId: "version-1",
|
|
appEndpointDomain: "service-1.prisma.build",
|
|
createdAt,
|
|
});
|
|
},
|
|
createAppDeployment: (appId: string, input: unknown) => {
|
|
calls.push(["createAppDeployment", { appId, input }]);
|
|
return Effect.succeed({
|
|
id: "version-1",
|
|
type: "deployment",
|
|
url: "https://api.prisma.test/v1/deployments/version-1",
|
|
foundryVersionId: "foundry-1",
|
|
uploadUrl: null,
|
|
});
|
|
},
|
|
listAppDomains: (appId: string) => {
|
|
calls.push(["listAppDomains", appId]);
|
|
return Effect.succeed([]);
|
|
},
|
|
createAppDomain: (appId: string, input: unknown) => {
|
|
calls.push(["createAppDomain", { appId, input }]);
|
|
return Effect.succeed({
|
|
status: 201 as const,
|
|
domain: {
|
|
id: "domain-1",
|
|
type: "custom-domain" as const,
|
|
url: "https://api.prisma.test/v1/domains/domain-1",
|
|
hostname: "api.example.com",
|
|
appId: appId,
|
|
status: "pending_dns" as const,
|
|
foundryStatus: "pending_dns",
|
|
failureReason: null,
|
|
failureCategory: null,
|
|
certExpiresAt: null,
|
|
dnsRecords: [
|
|
{
|
|
type: "CNAME" as const,
|
|
name: "api.example.com",
|
|
value: "service-1.prisma.build",
|
|
ttl: null,
|
|
},
|
|
],
|
|
createdAt,
|
|
updatedAt,
|
|
},
|
|
});
|
|
},
|
|
getDeployment: (id: string) => {
|
|
calls.push(["getDeployment", id]);
|
|
return Effect.succeed({
|
|
id,
|
|
type: "deployment",
|
|
serviceId: "service-1",
|
|
url: `https://api.prisma.test/v1/deployments/${id}`,
|
|
foundryVersionId: "foundry-1",
|
|
status: "new",
|
|
previewDomain: null,
|
|
createdAt,
|
|
});
|
|
},
|
|
listEnvironmentVariables: (query: unknown) => {
|
|
calls.push(["listEnvironmentVariables", query]);
|
|
return Effect.succeed([]);
|
|
},
|
|
createEnvironmentVariable: (input: unknown) => {
|
|
calls.push(["createEnvironmentVariable", input]);
|
|
return Effect.succeed({
|
|
id: "env-1",
|
|
type: "environment-variable",
|
|
url: "https://api.prisma.test/v1/environment-variables/env-1",
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
class: "production",
|
|
key: "TOKEN",
|
|
valueKid: "kid-1",
|
|
isManagedBySystem: false,
|
|
createdAt,
|
|
updatedAt,
|
|
});
|
|
},
|
|
listSourceRepositories: (query: unknown) => {
|
|
calls.push(["listSourceRepositories", query]);
|
|
return Effect.succeed([]);
|
|
},
|
|
createSourceRepository: (input: unknown) => {
|
|
calls.push(["createSourceRepository", input]);
|
|
return Effect.succeed({
|
|
id: "repo-1",
|
|
type: "source-repository",
|
|
url: "https://api.prisma.test/v1/source-repositories/repo-1",
|
|
repoId: 123,
|
|
provider: "github",
|
|
repoFullName: "acme/api",
|
|
defaultBranch: "main",
|
|
isPrivate: true,
|
|
status: "active",
|
|
projectId: "project-1",
|
|
installationId: "installation-1",
|
|
createdAt,
|
|
updatedAt,
|
|
});
|
|
},
|
|
getSourceRepository: (id: string) => {
|
|
calls.push(["getSourceRepository", id]);
|
|
return Effect.succeed({
|
|
id,
|
|
type: "source-repository",
|
|
url: `https://api.prisma.test/v1/source-repositories/${id}`,
|
|
repoId: 123,
|
|
provider: "github",
|
|
repoFullName: "acme/api",
|
|
defaultBranch: "main",
|
|
isPrivate: true,
|
|
status: "active",
|
|
projectId: "project-1",
|
|
installationId: "installation-1",
|
|
createdAt,
|
|
updatedAt,
|
|
});
|
|
},
|
|
} as unknown as PrismaManagementClient;
|
|
return { client, calls };
|
|
};
|
|
|
|
const liveProviderContext = Layer.succeed(AlchemyContext, {
|
|
dotAlchemy: ".alchemy-test",
|
|
dev: false,
|
|
adopt: false,
|
|
});
|
|
|
|
/**
|
|
* Serve the Management API from the same hermetic client-shaped handlers
|
|
* these suites already declare, for the resources that now call distilled
|
|
* operations. `dispatchTo` maps each handler's result onto the wire (see the
|
|
* fixture).
|
|
*/
|
|
const dispatchManagement = (client: any, request: Captured): Response => {
|
|
{
|
|
const segments = request.pathname.split("/").filter((s) => s.length > 0);
|
|
const body = request.bodyJson as any;
|
|
const { call, callVoid, list } = dispatchTo(request);
|
|
|
|
// segments[0] is the "v1" prefix.
|
|
const [head, id, tail] = segments.slice(1) as [
|
|
string | undefined,
|
|
string | undefined,
|
|
string | undefined,
|
|
];
|
|
|
|
if (head === "projects") {
|
|
if (id === undefined) {
|
|
return request.method === "GET"
|
|
? call(client.listProjects, [], list)
|
|
: call(client.createProject, [body]);
|
|
}
|
|
if (tail === "databases") {
|
|
return request.method === "GET"
|
|
? call(client.listProjectDatabases, [id, { limit: 100 }], list)
|
|
: call(client.createProjectDatabase, [id, body]);
|
|
}
|
|
if (tail === "branches") {
|
|
return request.method === "GET"
|
|
? call(
|
|
client.listBranches,
|
|
[id, Object.fromEntries(new URLSearchParams(request.search))],
|
|
list,
|
|
)
|
|
: call(client.createBranch, [id, body]);
|
|
}
|
|
if (request.method === "GET") return call(client.getProject, [id]);
|
|
if (request.method === "PATCH")
|
|
return call(client.updateProject, [id, body]);
|
|
if (request.method === "DELETE") {
|
|
return callVoid(client.deleteProject, [id]);
|
|
}
|
|
}
|
|
|
|
if (head === "databases") {
|
|
if (id === undefined) {
|
|
return request.method === "GET"
|
|
? call(client.listDatabases, [], list)
|
|
: call(client.createDatabase, [body]);
|
|
}
|
|
if (tail === "connections") {
|
|
return request.method === "GET"
|
|
? call(client.listDatabaseConnections, [id, { limit: 100 }], list)
|
|
: call(client.createDatabaseConnection, [id, body]);
|
|
}
|
|
if (request.method === "GET") return call(client.getDatabase, [id]);
|
|
if (request.method === "PATCH")
|
|
return call(client.updateDatabase, [id, body]);
|
|
if (request.method === "DELETE") {
|
|
return callVoid(client.deleteDatabase, [id]);
|
|
}
|
|
}
|
|
|
|
if (head === "connections") {
|
|
if (id === undefined) {
|
|
return request.method === "GET"
|
|
? call(client.listConnections, [], list)
|
|
: call(client.createConnection, [body]);
|
|
}
|
|
if (tail === "rotate") return call(client.rotateConnection, [id]);
|
|
if (request.method === "GET") return call(client.getConnection, [id]);
|
|
if (request.method === "DELETE") {
|
|
return callVoid(client.deleteConnection, [id]);
|
|
}
|
|
}
|
|
|
|
if (head === "environment-variables") {
|
|
if (id === undefined) {
|
|
return request.method === "GET"
|
|
? call(
|
|
client.listEnvironmentVariables,
|
|
[Object.fromEntries(new URLSearchParams(request.search))],
|
|
list,
|
|
)
|
|
: call(client.createEnvironmentVariable, [body]);
|
|
}
|
|
if (request.method === "GET") {
|
|
return call(client.getEnvironmentVariable, [id]);
|
|
}
|
|
if (request.method === "PATCH") {
|
|
return call(client.updateEnvironmentVariable, [id, body]);
|
|
}
|
|
if (request.method === "DELETE") {
|
|
return callVoid(client.deleteEnvironmentVariable, [id]);
|
|
}
|
|
}
|
|
|
|
if (head === "source-repositories") {
|
|
if (id === undefined) {
|
|
return request.method === "GET"
|
|
? call(
|
|
client.listSourceRepositories,
|
|
[Object.fromEntries(new URLSearchParams(request.search))],
|
|
list,
|
|
)
|
|
: call(client.createSourceRepository, [body]);
|
|
}
|
|
if (request.method === "GET") {
|
|
return call(client.getSourceRepository, [id]);
|
|
}
|
|
if (request.method === "DELETE") {
|
|
return callVoid(client.deleteSourceRepository, [id]);
|
|
}
|
|
}
|
|
|
|
if (head === "buckets") {
|
|
if (id === undefined) {
|
|
return request.method === "GET"
|
|
? call(client.listBuckets, [], list)
|
|
: call(client.createBucket, [body]);
|
|
}
|
|
if (tail === "keys") {
|
|
return request.method === "GET"
|
|
? call(client.listBucketKeys, [id, { limit: 100 }], list)
|
|
: call(client.createBucketKey, [id, body]);
|
|
}
|
|
if (request.method === "GET") return call(client.getBucket, [id]);
|
|
if (request.method === "DELETE") {
|
|
return callVoid(client.deleteBucket, [id]);
|
|
}
|
|
}
|
|
|
|
if (head === "services") {
|
|
if (id === undefined) {
|
|
return request.method === "GET"
|
|
? call(
|
|
client.listApps,
|
|
[Object.fromEntries(new URLSearchParams(request.search))],
|
|
list,
|
|
)
|
|
: call(client.createApp, [body]);
|
|
}
|
|
if (tail === "deployments") {
|
|
return request.method === "GET"
|
|
? call(
|
|
client.listAppDeployments,
|
|
[id, Object.fromEntries(new URLSearchParams(request.search))],
|
|
list,
|
|
)
|
|
: call(client.createAppDeployment, [id, body]);
|
|
}
|
|
if (tail === undefined) {
|
|
if (request.method === "GET") return call(client.getApp, [id]);
|
|
if (request.method === "PATCH")
|
|
return call(client.updateApp, [id, body]);
|
|
}
|
|
}
|
|
|
|
if (head === "deployments" && id !== undefined) {
|
|
if (tail === "start") return call(client.startDeployment, [id]);
|
|
if (tail === "stop") {
|
|
return callVoid(client.stopDeployment, [id]);
|
|
}
|
|
if (request.method === "GET") return call(client.getDeployment, [id]);
|
|
if (request.method === "DELETE") {
|
|
return callVoid(client.deleteDeployment, [id]);
|
|
}
|
|
}
|
|
|
|
if (head === "services" && id !== undefined && tail === "promote") {
|
|
return call(client.promoteApp, [id, body]);
|
|
}
|
|
if (head === "services" && id !== undefined && tail === "rollback") {
|
|
return call(client.rollbackApp, [id, body]);
|
|
}
|
|
if (
|
|
head === "services" &&
|
|
id !== undefined &&
|
|
tail === undefined &&
|
|
request.method === "DELETE"
|
|
) {
|
|
return callVoid(client.deleteApp, [id]);
|
|
}
|
|
|
|
if (head === "branches" && id !== undefined) {
|
|
if (request.method === "GET") return call(client.getBranch, [id]);
|
|
if (request.method === "PATCH")
|
|
return call(client.updateBranch, [id, body]);
|
|
if (request.method === "DELETE") {
|
|
return callVoid(client.deleteBranch, [id]);
|
|
}
|
|
}
|
|
|
|
return unhandled(request);
|
|
}
|
|
};
|
|
|
|
const managementApi = (client: any) =>
|
|
makeFakeManagementApi((request) => dispatchManagement(client, request));
|
|
|
|
const providerLayer = (client: PrismaManagementClient) =>
|
|
Layer.mergeAll(
|
|
ProjectProvider(),
|
|
DatabaseProvider(),
|
|
ConnectionProvider(),
|
|
BranchProvider(),
|
|
AppProvider(),
|
|
DeploymentProvider(),
|
|
CustomDomainProvider(),
|
|
EnvironmentVariableProvider(),
|
|
SourceRepositoryProvider(),
|
|
).pipe(
|
|
Layer.provide(Layer.succeed(PrismaClient, client)),
|
|
Layer.provide(liveProviderContext),
|
|
Layer.provide(testStackContext),
|
|
Layer.provide(PlatformServices),
|
|
);
|
|
|
|
const reconcileInput = <Props, Attrs>(
|
|
id: string,
|
|
news: Props,
|
|
output?: Attrs,
|
|
olds?: Props,
|
|
) => ({
|
|
id,
|
|
fqn: id,
|
|
instanceId: "00000000000000000000000000000000",
|
|
news,
|
|
olds,
|
|
output,
|
|
session: undefined as never,
|
|
bindings: [],
|
|
});
|
|
|
|
const deleteInput = (id: string, output: unknown) =>
|
|
({
|
|
id,
|
|
fqn: id,
|
|
instanceId: "00000000000000000000000000000000",
|
|
olds: {} as never,
|
|
output,
|
|
session: undefined as never,
|
|
bindings: [],
|
|
}) as never;
|
|
|
|
const readInput = <Props, Attrs>(id: string, olds: Props, output?: Attrs) =>
|
|
({
|
|
id,
|
|
fqn: id,
|
|
instanceId: "00000000000000000000000000000000",
|
|
olds,
|
|
output,
|
|
}) as never;
|
|
|
|
const diffInput = <Props, Attrs>(olds: Props, news: Props, output?: Attrs) =>
|
|
({
|
|
id: "Resource",
|
|
fqn: "Resource",
|
|
instanceId: "00000000000000000000000000000000",
|
|
olds,
|
|
news,
|
|
oldBindings: [],
|
|
newBindings: [],
|
|
output,
|
|
}) as never;
|
|
|
|
describe(
|
|
"Prisma resource providers",
|
|
{ tags: ["unit", "provider:prisma", "local"] },
|
|
() => {
|
|
it(
|
|
"derives namespaced-safe env keys for connection bindings",
|
|
() => {
|
|
expect(
|
|
connectEnvKeys({
|
|
FQN: "Connection",
|
|
LogicalId: "Connection",
|
|
}).directConnectionString,
|
|
).toBe("PRISMA_CONNECTION_DIRECT_CONNECTION_STRING");
|
|
expect(
|
|
connectEnvKeys({
|
|
FQN: "Api/Connection",
|
|
LogicalId: "Connection",
|
|
}).directConnectionString,
|
|
).toBe("PRISMA_API_CONNECTION_DIRECT_CONNECTION_STRING");
|
|
},
|
|
{ tags: ["provider:prisma:connect"] },
|
|
);
|
|
|
|
it.effect(
|
|
"ConnectBinding resolves bound connection outputs at runtime",
|
|
() => {
|
|
const stored: Record<string, Output.Output> = {};
|
|
let capturedBindingEnv: Record<string, Output.Output> | undefined;
|
|
const runtime = {
|
|
Type: "Prisma.Compute",
|
|
id: "App",
|
|
env: stored,
|
|
set: (id: string, output: Output.Output) =>
|
|
Effect.sync(() => {
|
|
const key = id.replaceAll(/[^a-zA-Z0-9]/g, "_");
|
|
stored[key] = output;
|
|
return key;
|
|
}),
|
|
get: <T>(key: string): Effect.Effect<T> => {
|
|
const output = stored[key];
|
|
if (!output) return Effect.die(`missing runtime binding ${key}`);
|
|
return Output.evaluate(output, {}) as Effect.Effect<T>;
|
|
},
|
|
};
|
|
const host = {
|
|
Type: "Prisma.Compute",
|
|
LogicalId: "App",
|
|
FQN: "App",
|
|
bind: (...args: unknown[]) =>
|
|
args[0] instanceof Array
|
|
? (binding: { env?: Record<string, Output.Output> }) =>
|
|
Effect.sync(() => {
|
|
capturedBindingEnv = binding.env;
|
|
})
|
|
: Effect.void,
|
|
};
|
|
const escapedPooledConnectionString =
|
|
"__ALCHEMY_PRISMA_CONNECTION_VALUE__:prisma://pooled";
|
|
const connection = {
|
|
Type: "Prisma.Connection",
|
|
LogicalId: "Connection",
|
|
FQN: "Api/Connection",
|
|
connectionId: Output.asOutput("connection-1"),
|
|
databaseId: Output.asOutput("database-1"),
|
|
directConnectionString: Output.asOutput(
|
|
Redacted.make("postgres://direct"),
|
|
),
|
|
pooledConnectionString: Output.asOutput(
|
|
Redacted.make(escapedPooledConnectionString),
|
|
),
|
|
accelerateConnectionString: Output.asOutput(undefined),
|
|
host: Output.asOutput("db.example.test"),
|
|
user: Output.asOutput(null),
|
|
password: Output.asOutput(Redacted.make("password")),
|
|
} as PrismaConnection;
|
|
|
|
return Effect.gen(function* () {
|
|
const db = yield* Connect(connection);
|
|
const keys = connectEnvKeys(connection);
|
|
const encodedEnv = yield* Output.evaluate(
|
|
capturedBindingEnv ?? {},
|
|
{},
|
|
) as Effect.Effect<Record<string, unknown>>;
|
|
|
|
expect(Object.keys(stored)).toEqual([]);
|
|
expect(encodedEnv[keys.accelerateConnectionString]).toEqual(
|
|
expect.any(String),
|
|
);
|
|
expect(encodedEnv[keys.user]).toEqual(expect.any(String));
|
|
expect(yield* db.connectionId).toBe("connection-1");
|
|
expect(Redacted.value(yield* db.databaseUrl)).toBe(
|
|
escapedPooledConnectionString,
|
|
);
|
|
expect(Redacted.value((yield* db.directConnectionString)!)).toBe(
|
|
"postgres://direct",
|
|
);
|
|
expect(Redacted.value((yield* db.pooledConnectionString)!)).toBe(
|
|
escapedPooledConnectionString,
|
|
);
|
|
expect(yield* db.accelerateConnectionString).toBeUndefined();
|
|
expect(yield* db.user).toBeNull();
|
|
expect(Redacted.value((yield* db.password)!)).toBe("password");
|
|
expect(Object.keys(stored)).toEqual(
|
|
expect.arrayContaining([
|
|
"PRISMA_API_CONNECTION_CONNECTION_ID",
|
|
"PRISMA_API_CONNECTION_DIRECT_CONNECTION_STRING",
|
|
"PRISMA_API_CONNECTION_POOLED_CONNECTION_STRING",
|
|
"PRISMA_API_CONNECTION_ACCELERATE_CONNECTION_STRING",
|
|
"PRISMA_API_CONNECTION_USER",
|
|
"PRISMA_API_CONNECTION_PASSWORD",
|
|
]),
|
|
);
|
|
}).pipe(
|
|
Effect.provide(ConnectBinding),
|
|
Effect.provide(Layer.succeed(RuntimeContext, runtime)),
|
|
Effect.provide(Layer.succeed(Self, host)),
|
|
Effect.provide(
|
|
Layer.succeed(
|
|
ConfigProvider.ConfigProvider,
|
|
ConfigProvider.fromUnknown({ ALCHEMY_PHASE: "runtime" }),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
{ tags: ["provider:prisma:connect", "provider:prisma:connection"] },
|
|
);
|
|
|
|
it.effect(
|
|
"ConnectBinding does not require the deploy-time host at runtime",
|
|
() => {
|
|
const stored: Record<string, Output.Output> = {};
|
|
const runtime = {
|
|
Type: "Prisma.Compute",
|
|
id: "App",
|
|
env: stored,
|
|
set: (id: string, output: Output.Output) =>
|
|
Effect.sync(() => {
|
|
const key = id.replaceAll(/[^a-zA-Z0-9]/g, "_");
|
|
stored[key] = output;
|
|
return key;
|
|
}),
|
|
get: <T>(key: string): Effect.Effect<T> => {
|
|
const output = stored[key];
|
|
if (!output) return Effect.die(`missing runtime binding ${key}`);
|
|
return Output.evaluate(output, {}) as Effect.Effect<T>;
|
|
},
|
|
};
|
|
const connection = {
|
|
Type: "Prisma.Connection",
|
|
LogicalId: "Connection",
|
|
FQN: "Connection",
|
|
connectionId: Output.asOutput("connection-1"),
|
|
databaseId: Output.asOutput("database-1"),
|
|
directConnectionString: Output.asOutput(
|
|
Redacted.make("postgres://runtime"),
|
|
),
|
|
pooledConnectionString: Output.asOutput(undefined),
|
|
accelerateConnectionString: Output.asOutput(undefined),
|
|
host: Output.asOutput("db.example.test"),
|
|
user: Output.asOutput("api"),
|
|
password: Output.asOutput(Redacted.make("password")),
|
|
} as PrismaConnection;
|
|
|
|
// The deploy-time host dispatch is guarded by `__ALCHEMY_RUNTIME__`,
|
|
// which bundles fold to `true` — simulate that so no Self is needed.
|
|
const wasRuntime = globalThis.__ALCHEMY_RUNTIME__;
|
|
globalThis.__ALCHEMY_RUNTIME__ = true;
|
|
return Effect.gen(function* () {
|
|
const db = yield* Connect(connection);
|
|
|
|
expect(yield* db.connectionId).toBe("connection-1");
|
|
expect(Redacted.value(yield* db.databaseUrl)).toBe(
|
|
"postgres://runtime",
|
|
);
|
|
}).pipe(
|
|
Effect.provide(ConnectBinding),
|
|
Effect.provide(Layer.succeed(RuntimeContext, runtime)),
|
|
Effect.provide(
|
|
Layer.succeed(
|
|
ConfigProvider.ConfigProvider,
|
|
ConfigProvider.fromUnknown({ ALCHEMY_PHASE: "runtime" }),
|
|
),
|
|
),
|
|
Effect.ensuring(
|
|
Effect.sync(() => {
|
|
globalThis.__ALCHEMY_RUNTIME__ = wasRuntime;
|
|
}),
|
|
),
|
|
);
|
|
},
|
|
{ tags: ["provider:prisma:connect", "provider:prisma:connection"] },
|
|
);
|
|
|
|
it.effect(
|
|
"Prisma.Compute records Connection.bind env on platform bindings",
|
|
() => {
|
|
const stack: Omit<StackSpec, "output"> = {
|
|
name: "prisma-compute-binding-test",
|
|
stage: "test",
|
|
resources: {},
|
|
bindings: {},
|
|
actions: {},
|
|
};
|
|
const connection = {
|
|
Type: "Prisma.Connection",
|
|
LogicalId: "Connection",
|
|
FQN: "Api/Connection",
|
|
connectionId: Output.asOutput("connection-1"),
|
|
databaseId: Output.asOutput("database-1"),
|
|
directConnectionString: Output.asOutput(
|
|
Redacted.make("postgres://api"),
|
|
),
|
|
pooledConnectionString: Output.asOutput(
|
|
Redacted.make("prisma+postgres://api"),
|
|
),
|
|
accelerateConnectionString: Output.asOutput(undefined),
|
|
host: Output.asOutput("db.example.test"),
|
|
user: Output.asOutput("api"),
|
|
password: Output.asOutput(Redacted.make("password")),
|
|
} as PrismaConnection;
|
|
|
|
return Effect.gen(function* () {
|
|
const app = yield* PrismaCompute(
|
|
"App",
|
|
{
|
|
project: "project-1",
|
|
appName: "api",
|
|
main: "app.ts",
|
|
},
|
|
Effect.gen(function* () {
|
|
yield* Connect(connection);
|
|
}).pipe(Effect.provide(ConnectBinding)),
|
|
);
|
|
|
|
const keys = connectEnvKeys(connection);
|
|
const binding = stack.bindings[app.FQN]?.[0];
|
|
const env = yield* Output.evaluate(binding?.data.env ?? {}, {});
|
|
|
|
expect(binding?.sid).toBe("Connection");
|
|
expect(Object.keys(env)).toEqual(
|
|
expect.arrayContaining([
|
|
keys.connectionId,
|
|
keys.databaseId,
|
|
keys.directConnectionString,
|
|
keys.pooledConnectionString,
|
|
keys.password,
|
|
]),
|
|
);
|
|
expect(env[keys.connectionId]).toBe("connection-1");
|
|
expect(env[keys.databaseId]).toBe("database-1");
|
|
expect(
|
|
redactedValue(env[keys.directConnectionString] ?? undefined),
|
|
).toBe("postgres://api");
|
|
expect(
|
|
redactedValue(env[keys.pooledConnectionString] ?? undefined),
|
|
).toBe("prisma+postgres://api");
|
|
expect(redactedValue(env[keys.password] ?? undefined)).toBe(
|
|
"password",
|
|
);
|
|
}).pipe(
|
|
Effect.provide(inMemoryState()),
|
|
Effect.provide(
|
|
Layer.succeed(PrismaProviderCollection, {
|
|
kind: "ProviderCollection" as const,
|
|
get: () => undefined,
|
|
providers: {},
|
|
}),
|
|
),
|
|
Effect.provideService(Stack, stack),
|
|
Effect.provideService(Stage, "test"),
|
|
Effect.provide(
|
|
Layer.succeed(
|
|
ConfigProvider.ConfigProvider,
|
|
ConfigProvider.fromUnknown({ ALCHEMY_PHASE: "plan" }),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:compute",
|
|
"provider:prisma:connect",
|
|
"provider:prisma:connection",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"Connection.bind records env for AWS Lambda function hosts",
|
|
() => {
|
|
const stored: Record<string, Output.Output> = {};
|
|
let capturedBindingEnv: Record<string, Output.Output> | undefined;
|
|
const runtime = {
|
|
Type: "AWS.Lambda.Function",
|
|
id: "Api",
|
|
env: stored,
|
|
set: (id: string, output: Output.Output) =>
|
|
Effect.sync(() => {
|
|
const key = id.replaceAll(/[^a-zA-Z0-9]/g, "_");
|
|
stored[key] = output;
|
|
return key;
|
|
}),
|
|
get: <T>(key: string): Effect.Effect<T> => {
|
|
const output = stored[key];
|
|
if (!output) return Effect.die(`missing runtime binding ${key}`);
|
|
return Output.evaluate(output, {}) as Effect.Effect<T>;
|
|
},
|
|
};
|
|
const host = {
|
|
Type: "AWS.Lambda.Function",
|
|
LogicalId: "Api",
|
|
FQN: "Api",
|
|
bind: (...args: unknown[]) =>
|
|
args[0] instanceof Array
|
|
? (binding: { env?: Record<string, Output.Output> }) =>
|
|
Effect.sync(() => {
|
|
capturedBindingEnv = binding.env;
|
|
})
|
|
: Effect.void,
|
|
};
|
|
const connection = {
|
|
Type: "Prisma.Connection",
|
|
LogicalId: "Connection",
|
|
FQN: "Connection",
|
|
connectionId: Output.asOutput("connection-1"),
|
|
databaseId: Output.asOutput("database-1"),
|
|
directConnectionString: Output.asOutput(
|
|
Redacted.make("postgres://api"),
|
|
),
|
|
pooledConnectionString: Output.asOutput(undefined),
|
|
accelerateConnectionString: Output.asOutput(undefined),
|
|
host: Output.asOutput("db.example.test"),
|
|
user: Output.asOutput("api"),
|
|
password: Output.asOutput(Redacted.make("password")),
|
|
} as PrismaConnection;
|
|
|
|
return Effect.gen(function* () {
|
|
const db = yield* Connect(connection);
|
|
const keys = connectEnvKeys(connection);
|
|
const env = yield* Output.evaluate(
|
|
capturedBindingEnv ?? {},
|
|
{},
|
|
) as Effect.Effect<Record<string, unknown>>;
|
|
|
|
expect(Object.keys(env)).toEqual(
|
|
expect.arrayContaining([
|
|
keys.connectionId,
|
|
keys.databaseId,
|
|
keys.directConnectionString,
|
|
keys.password,
|
|
]),
|
|
);
|
|
expect(env[keys.connectionId]).toBe("connection-1");
|
|
expect(env[keys.databaseId]).toBe("database-1");
|
|
expect(
|
|
redactedValue(
|
|
env[keys.directConnectionString] as
|
|
| string
|
|
| Redacted.Redacted<string>
|
|
| undefined,
|
|
),
|
|
).toBe("postgres://api");
|
|
expect(
|
|
redactedValue(
|
|
env[keys.password] as
|
|
| string
|
|
| Redacted.Redacted<string>
|
|
| undefined,
|
|
),
|
|
).toBe("password");
|
|
expect(Redacted.value(yield* db.databaseUrl)).toBe("postgres://api");
|
|
}).pipe(
|
|
Effect.provide(ConnectBinding),
|
|
Effect.provide(inMemoryState()),
|
|
Effect.provide(Layer.succeed(RuntimeContext, runtime)),
|
|
Effect.provide(Layer.succeed(Self, host)),
|
|
Effect.provide(
|
|
Layer.succeed(
|
|
ConfigProvider.ConfigProvider,
|
|
ConfigProvider.fromUnknown({ ALCHEMY_PHASE: "runtime" }),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
{ tags: ["provider:prisma:connect", "provider:prisma:connection"] },
|
|
);
|
|
|
|
it.effect(
|
|
"Connection.bind records native text bindings for Workers",
|
|
() => {
|
|
const workerEnv: Record<string, string> = {};
|
|
let capturedBindings: unknown[] | undefined;
|
|
const runtime = {
|
|
Type: "Cloudflare.Worker",
|
|
id: "Worker",
|
|
env: {},
|
|
set: (id: string) =>
|
|
Effect.succeed(id.replaceAll(/[^a-zA-Z0-9]/g, "_")),
|
|
get: <T>(key: string): Effect.Effect<T> => {
|
|
const value = workerEnv[key];
|
|
if (value === undefined) {
|
|
return Effect.die(`missing worker binding ${key}`);
|
|
}
|
|
return Effect.succeed(value as T);
|
|
},
|
|
};
|
|
const host = {
|
|
Type: "Cloudflare.Worker",
|
|
LogicalId: "Worker",
|
|
FQN: "Worker",
|
|
bind: (...args: unknown[]) =>
|
|
args[0] instanceof Array
|
|
? (binding: {
|
|
bindings?: Output.Output<{
|
|
type: string;
|
|
name: string;
|
|
text: string;
|
|
}>[];
|
|
}) =>
|
|
Effect.sync(() => {
|
|
capturedBindings = binding.bindings;
|
|
})
|
|
: Effect.void,
|
|
};
|
|
const connection = {
|
|
Type: "Prisma.Connection",
|
|
LogicalId: "Connection",
|
|
FQN: "Connection",
|
|
connectionId: Output.asOutput("connection-1"),
|
|
databaseId: Output.asOutput("database-1"),
|
|
directConnectionString: Output.asOutput(
|
|
Redacted.make("postgres://api"),
|
|
),
|
|
pooledConnectionString: Output.asOutput(undefined),
|
|
accelerateConnectionString: Output.asOutput(undefined),
|
|
host: Output.asOutput("db.example.test"),
|
|
user: Output.asOutput("api"),
|
|
password: Output.asOutput(Redacted.make("password")),
|
|
} as PrismaConnection;
|
|
|
|
return Effect.gen(function* () {
|
|
const db = yield* Connect(connection);
|
|
const keys = connectEnvKeys(connection);
|
|
const bindings = (yield* Output.evaluate(
|
|
capturedBindings ?? [],
|
|
{},
|
|
)) as Array<{ type: string; name: string; text: string }>;
|
|
|
|
for (const binding of bindings) {
|
|
workerEnv[binding.name] = binding.text;
|
|
}
|
|
|
|
expect(bindings).toEqual(
|
|
expect.arrayContaining([
|
|
{
|
|
type: "plain_text",
|
|
name: keys.connectionId,
|
|
text: "connection-1",
|
|
},
|
|
{
|
|
type: "plain_text",
|
|
name: keys.databaseId,
|
|
text: "database-1",
|
|
},
|
|
{
|
|
type: "secret_text",
|
|
name: keys.directConnectionString,
|
|
text: "postgres://api",
|
|
},
|
|
{
|
|
type: "secret_text",
|
|
name: keys.password,
|
|
text: "password",
|
|
},
|
|
]),
|
|
);
|
|
expect("connectionString" in db).toBe(false);
|
|
expect(Redacted.value(yield* db.databaseUrl)).toBe("postgres://api");
|
|
expect(Redacted.value((yield* db.password)!)).toBe("password");
|
|
}).pipe(
|
|
Effect.provide(ConnectBinding),
|
|
Effect.provide(inMemoryState()),
|
|
Effect.provide(Layer.succeed(RuntimeContext, runtime)),
|
|
Effect.provide(Layer.succeed(Self, host)),
|
|
Effect.provide(
|
|
Layer.succeed(
|
|
ConfigProvider.ConfigProvider,
|
|
ConfigProvider.fromUnknown({ ALCHEMY_PHASE: "runtime" }),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
{ tags: ["provider:prisma:connect", "provider:prisma:connection"] },
|
|
);
|
|
|
|
it.effect(
|
|
"rejects conflicting App branch inputs",
|
|
() => {
|
|
const { client } = makeClient();
|
|
|
|
return Effect.gen(function* () {
|
|
const serviceProvider = yield* PrismaApp.Provider;
|
|
const error = yield* serviceProvider
|
|
.reconcile(
|
|
reconcileInput("App", {
|
|
project: "project-1",
|
|
displayName: "api",
|
|
branchId: "branch-1",
|
|
branchGitName: "main",
|
|
}),
|
|
)
|
|
.pipe(Effect.flip);
|
|
|
|
expect(error).toBeInstanceOf(Error);
|
|
expect((error as Error).message).toContain(
|
|
"branchId and branchGitName are mutually exclusive",
|
|
);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"rejects conflicting Database branch inputs",
|
|
() => {
|
|
const { client } = makeClient();
|
|
|
|
return Effect.gen(function* () {
|
|
const databaseProvider = yield* PrismaDatabase.Provider;
|
|
const error = yield* databaseProvider
|
|
.reconcile(
|
|
reconcileInput("Database", {
|
|
project: "project-1",
|
|
name: "main",
|
|
branchId: "branch-1",
|
|
branchGitName: "main",
|
|
}),
|
|
)
|
|
.pipe(Effect.flip);
|
|
|
|
expect(error).toBeInstanceOf(Error);
|
|
expect((error as Error).message).toContain(
|
|
"branchId and branchGitName are mutually exclusive",
|
|
);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"classifies Prisma resource diffs as updates or replacements",
|
|
() => {
|
|
const { client } = makeClient();
|
|
|
|
return Effect.gen(function* () {
|
|
const projectProvider = yield* PrismaProject.Provider;
|
|
const databaseProvider = yield* PrismaDatabase.Provider;
|
|
const connectionProvider = yield* PrismaConnection.Provider;
|
|
const branchProvider = yield* PrismaBranch.Provider;
|
|
const serviceProvider = yield* PrismaApp.Provider;
|
|
const versionProvider = yield* PrismaDeployment.Provider;
|
|
const envProvider = yield* PrismaEnvironmentVariable.Provider;
|
|
const repoProvider = yield* PrismaSourceRepository.Provider;
|
|
|
|
expect(
|
|
yield* projectProvider.diff!(
|
|
diffInput(
|
|
{ name: "app", region: "us-east-1", createDatabase: false },
|
|
{ name: "renamed", region: "us-east-1", createDatabase: false },
|
|
),
|
|
),
|
|
).toEqual({ action: "update" });
|
|
expect(
|
|
yield* projectProvider.diff!(
|
|
diffInput(
|
|
{ name: "app", region: "us-east-1", createDatabase: false },
|
|
{ name: "app", region: "us-west-2", createDatabase: false },
|
|
),
|
|
),
|
|
).toBeUndefined();
|
|
expect(
|
|
yield* projectProvider.diff!(
|
|
diffInput(
|
|
{ name: "app", region: "us-east-1", createDatabase: false },
|
|
{
|
|
name: "app",
|
|
region: "us-west-2",
|
|
createDatabase: Output.asOutput(false),
|
|
},
|
|
),
|
|
),
|
|
).toBeUndefined();
|
|
expect(
|
|
yield* projectProvider.diff!(
|
|
diffInput(
|
|
{ name: "app", region: "us-east-1", createDatabase: false },
|
|
{
|
|
name: "app",
|
|
region: "us-west-2",
|
|
createDatabase: false,
|
|
settings: Output.asOutput({}),
|
|
},
|
|
),
|
|
),
|
|
).toBeUndefined();
|
|
expect(
|
|
yield* projectProvider.diff!(
|
|
diffInput(
|
|
{
|
|
name: "app",
|
|
region: "us-east-1",
|
|
createDatabase: false,
|
|
settings: { preview: true, tier: "dev" },
|
|
},
|
|
{
|
|
name: "app",
|
|
region: "us-east-1",
|
|
createDatabase: false,
|
|
settings: { tier: "dev", preview: true },
|
|
},
|
|
),
|
|
),
|
|
).toEqual({ action: "update" });
|
|
|
|
expect(
|
|
yield* databaseProvider.diff!(
|
|
diffInput(
|
|
{ project: "project-1", name: "main", region: "us-east-1" },
|
|
{ project: "project-1", name: "primary", region: "us-east-1" },
|
|
),
|
|
),
|
|
).toEqual({ action: "update" });
|
|
expect(
|
|
yield* databaseProvider.diff!(
|
|
diffInput(
|
|
{
|
|
project: "project-1",
|
|
name: "main",
|
|
region: "us-east-1",
|
|
source: {
|
|
type: "backup",
|
|
databaseId: "database-source",
|
|
backupId: "backup-1",
|
|
},
|
|
},
|
|
{
|
|
project: "project-1",
|
|
name: "main",
|
|
region: "us-east-1",
|
|
source: {
|
|
backupId: "backup-1",
|
|
databaseId: "database-source",
|
|
type: "backup",
|
|
},
|
|
},
|
|
),
|
|
),
|
|
).toBeUndefined();
|
|
expect(
|
|
yield* databaseProvider.diff!(
|
|
diffInput(
|
|
{ project: "project-1", name: "main", region: "us-east-1" },
|
|
{
|
|
project: Output.asOutput("project-1"),
|
|
name: "main",
|
|
region: "us-west-2",
|
|
},
|
|
),
|
|
),
|
|
).toEqual({ action: "replace" });
|
|
expect(
|
|
yield* databaseProvider.diff!(
|
|
diffInput(
|
|
{
|
|
project: "project-1",
|
|
name: "main",
|
|
region: "us-east-1",
|
|
branchId: "branch-1",
|
|
},
|
|
{
|
|
project: "project-1",
|
|
name: "main",
|
|
region: "us-west-2",
|
|
branchId: Output.asOutput("branch-1"),
|
|
},
|
|
),
|
|
),
|
|
).toEqual({ action: "replace" });
|
|
|
|
expect(
|
|
yield* connectionProvider.diff!(
|
|
diffInput(
|
|
{ database: "database-1", name: "api", rotate: false },
|
|
{ database: "database-1", name: "api", rotate: true },
|
|
),
|
|
),
|
|
).toEqual({ action: "update" });
|
|
expect(
|
|
yield* connectionProvider.diff!(
|
|
diffInput(
|
|
{ database: "database-1", name: "api" },
|
|
{ database: "database-1", name: "worker" },
|
|
),
|
|
),
|
|
).toEqual({ action: "replace" });
|
|
expect(
|
|
yield* connectionProvider.diff!(
|
|
diffInput(
|
|
{ database: "database-1", name: "api" },
|
|
{ database: Output.asOutput("database-1"), name: "worker" },
|
|
),
|
|
),
|
|
).toEqual({ action: "replace" });
|
|
expect(
|
|
yield* connectionProvider.diff!(
|
|
diffInput(
|
|
{ database: "database-1", name: "api", rotate: false },
|
|
{
|
|
database: "database-1",
|
|
name: "worker",
|
|
rotate: Output.asOutput(false),
|
|
},
|
|
),
|
|
),
|
|
).toEqual({ action: "replace" });
|
|
|
|
expect(
|
|
yield* branchProvider.diff!(
|
|
diffInput(
|
|
{ project: "project-1", gitName: "main", isDefault: false },
|
|
{ project: "project-1", gitName: "main", isDefault: true },
|
|
),
|
|
),
|
|
).toEqual({ action: "update" });
|
|
expect(
|
|
yield* branchProvider.diff!(
|
|
diffInput(
|
|
{ project: "project-1", gitName: "main", isDefault: true },
|
|
{ project: "project-1", gitName: "main", isDefault: false },
|
|
),
|
|
),
|
|
).toBeUndefined();
|
|
expect(
|
|
yield* branchProvider.diff!(
|
|
diffInput(
|
|
{ project: "project-1", gitName: "main" },
|
|
{ project: "project-1", gitName: "release" },
|
|
),
|
|
),
|
|
).toEqual({ action: "replace" });
|
|
expect(
|
|
yield* branchProvider.diff!(
|
|
diffInput(
|
|
{ project: "project-1", gitName: "main" },
|
|
{ project: Output.asOutput("project-1"), gitName: "release" },
|
|
),
|
|
),
|
|
).toEqual({ action: "replace" });
|
|
expect(
|
|
yield* branchProvider.diff!(
|
|
diffInput(
|
|
{ project: "project-1", gitName: "main", isDefault: false },
|
|
{
|
|
project: "project-1",
|
|
gitName: "release",
|
|
isDefault: Output.asOutput(false),
|
|
},
|
|
),
|
|
),
|
|
).toEqual({ action: "replace" });
|
|
|
|
expect(
|
|
yield* serviceProvider.diff!(
|
|
diffInput(
|
|
{
|
|
project: "project-1",
|
|
displayName: "api",
|
|
regionId: "us-east-1",
|
|
},
|
|
{
|
|
project: "project-1",
|
|
displayName: "web",
|
|
regionId: "us-east-1",
|
|
},
|
|
),
|
|
),
|
|
).toEqual({ action: "update" });
|
|
const appRegionChanges = yield* Effect.forEach(
|
|
[
|
|
serviceProvider.diff!(
|
|
diffInput(
|
|
{
|
|
project: "project-1",
|
|
displayName: "api",
|
|
regionId: "us-east-1",
|
|
},
|
|
{
|
|
project: "project-1",
|
|
displayName: "api",
|
|
regionId: "us-west-2",
|
|
},
|
|
),
|
|
),
|
|
serviceProvider.diff!(
|
|
diffInput(
|
|
{
|
|
project: "project-1",
|
|
displayName: "api",
|
|
regionId: "us-east-1",
|
|
},
|
|
{
|
|
project: Output.asOutput("project-1"),
|
|
displayName: "api",
|
|
regionId: "us-west-2",
|
|
},
|
|
),
|
|
),
|
|
serviceProvider.diff!(
|
|
diffInput(
|
|
{
|
|
project: "project-1",
|
|
displayName: "api",
|
|
regionId: "us-east-1",
|
|
branchId: "branch-1",
|
|
},
|
|
{
|
|
project: "project-1",
|
|
displayName: "api",
|
|
regionId: "us-west-2",
|
|
branchId: Output.asOutput("branch-1"),
|
|
},
|
|
),
|
|
),
|
|
],
|
|
(change) => change.pipe(Effect.result),
|
|
);
|
|
for (const change of appRegionChanges) {
|
|
expect(Result.isFailure(change)).toBe(true);
|
|
if (Result.isFailure(change)) {
|
|
expect(String(change.failure)).toContain(
|
|
"cannot atomically move",
|
|
);
|
|
}
|
|
}
|
|
|
|
expect(
|
|
yield* versionProvider.diff!(
|
|
diffInput(
|
|
{ app: "service-1", start: false },
|
|
{ app: "service-1", start: true },
|
|
),
|
|
),
|
|
).toEqual({ action: "update" });
|
|
expect(
|
|
yield* versionProvider.diff!(
|
|
diffInput(
|
|
{ app: "service-1", portMapping: { http: 3000 } },
|
|
{ app: "service-1", portMapping: { http: 8080 } },
|
|
),
|
|
),
|
|
).toEqual({ action: "replace" });
|
|
expect(
|
|
yield* versionProvider.diff!(
|
|
diffInput(
|
|
{ app: "service-1", portMapping: { http: 3000 } },
|
|
{
|
|
app: Output.asOutput("service-1"),
|
|
portMapping: { http: 8080 },
|
|
},
|
|
),
|
|
),
|
|
).toEqual({ action: "replace" });
|
|
expect(
|
|
yield* versionProvider.diff!(
|
|
diffInput(
|
|
{
|
|
app: "service-1",
|
|
portMapping: { http: 3000 },
|
|
start: false,
|
|
},
|
|
{
|
|
app: "service-1",
|
|
portMapping: { http: 8080 },
|
|
start: Output.asOutput(false),
|
|
},
|
|
),
|
|
),
|
|
).toEqual({ action: "replace" });
|
|
|
|
expect(
|
|
yield* envProvider.diff!(
|
|
diffInput(
|
|
{
|
|
project: "project-1",
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
value: Redacted.make("old"),
|
|
},
|
|
{
|
|
project: "project-1",
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
value: Redacted.make("new"),
|
|
},
|
|
),
|
|
),
|
|
).toEqual({ action: "update" });
|
|
expect(
|
|
yield* envProvider.diff!(
|
|
diffInput(
|
|
{
|
|
project: "project-1",
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
value: Redacted.make("same"),
|
|
},
|
|
{
|
|
project: "project-1",
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
value: Redacted.make("same"),
|
|
},
|
|
),
|
|
),
|
|
).toEqual({ action: "update" });
|
|
expect(
|
|
yield* envProvider.diff!(
|
|
diffInput(
|
|
{
|
|
project: "project-1",
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
value: Redacted.make("secret"),
|
|
},
|
|
{
|
|
project: "project-1",
|
|
class: "preview" as const,
|
|
key: "TOKEN",
|
|
value: Redacted.make("secret"),
|
|
},
|
|
),
|
|
),
|
|
).toEqual({ action: "replace" });
|
|
expect(
|
|
yield* envProvider.diff!(
|
|
diffInput(
|
|
{
|
|
project: "project-1",
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
value: Redacted.make("secret"),
|
|
},
|
|
{
|
|
project: Output.asOutput("project-1"),
|
|
class: "preview" as const,
|
|
key: "TOKEN",
|
|
value: Output.asOutput(Redacted.make("secret")),
|
|
},
|
|
),
|
|
),
|
|
).toEqual({ action: "replace" });
|
|
expect(
|
|
yield* envProvider.diff!(
|
|
diffInput(
|
|
{
|
|
project: "project-1",
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
value: Redacted.make("secret"),
|
|
},
|
|
{
|
|
project: "project-1",
|
|
class: "preview" as const,
|
|
key: Output.asOutput("TOKEN"),
|
|
value: Redacted.make("secret"),
|
|
},
|
|
),
|
|
),
|
|
).toEqual({ action: "replace" });
|
|
|
|
const repositoryChanges = yield* Effect.forEach(
|
|
[
|
|
repoProvider.diff!(
|
|
diffInput(
|
|
{ project: "project-1", providerRepositoryId: 123 },
|
|
{ project: "project-1", providerRepositoryId: 456 },
|
|
),
|
|
),
|
|
repoProvider.diff!(
|
|
diffInput(
|
|
{ project: "project-1", providerRepositoryId: 123 },
|
|
{
|
|
project: Output.asOutput("project-1"),
|
|
providerRepositoryId: 456,
|
|
},
|
|
),
|
|
),
|
|
repoProvider.diff!(
|
|
diffInput(
|
|
{
|
|
project: "project-1",
|
|
providerRepositoryId: 123,
|
|
installationId: "install-1",
|
|
},
|
|
{
|
|
project: "project-1",
|
|
providerRepositoryId: 456,
|
|
installationId: Output.asOutput("install-1"),
|
|
},
|
|
),
|
|
),
|
|
],
|
|
(change) => change.pipe(Effect.result),
|
|
);
|
|
for (const change of repositoryChanges) {
|
|
expect(Result.isFailure(change)).toBe(true);
|
|
if (Result.isFailure(change)) {
|
|
expect(String(change.failure)).toContain(
|
|
"cannot be replaced atomically",
|
|
);
|
|
}
|
|
}
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
Effect.provide(FetchHttpClient.layer),
|
|
Effect.provideService(Stack, {
|
|
name: "prisma-provider-diff-test",
|
|
stage: "test",
|
|
resources: {},
|
|
bindings: {},
|
|
actions: {},
|
|
}),
|
|
Effect.provideService(Stage, "test"),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"reads existing Prisma resources for adoption and refresh",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const database = {
|
|
id: "database-1",
|
|
type: "database" as const,
|
|
url: "https://api.prisma.test/v1/databases/database-1",
|
|
name: "main",
|
|
status: "ready" as const,
|
|
createdAt,
|
|
isDefault: true,
|
|
defaultConnectionId: "connection-1",
|
|
connections: [databaseConnection("database-1", "connection-1")],
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
region: { id: "us-east-1", name: "US East" },
|
|
source: { type: "empty" },
|
|
branchId: null,
|
|
};
|
|
const fake = makeFakeManagementApi((request) => {
|
|
if (request.pathname === "/v1/projects" && request.method === "GET") {
|
|
return page([wireProject()]);
|
|
}
|
|
if (
|
|
request.pathname === "/v1/projects/project-1" &&
|
|
request.method === "GET"
|
|
) {
|
|
return envelope(wireProject());
|
|
}
|
|
if (
|
|
request.pathname === "/v1/projects/project-1/databases" &&
|
|
request.method === "GET"
|
|
) {
|
|
return page([
|
|
wireDatabase({
|
|
isDefault: true,
|
|
defaultConnectionId: "connection-1",
|
|
connections: [wireConnection()],
|
|
}),
|
|
]);
|
|
}
|
|
return dispatchManagement(client, request);
|
|
});
|
|
|
|
const client = {
|
|
listProjectDatabases: (projectId: string, query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listProjectDatabases", { projectId, query }]);
|
|
return [database];
|
|
}),
|
|
listDatabaseConnections: (databaseId: string, query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listDatabaseConnections", { databaseId, query }]);
|
|
return [
|
|
{
|
|
id: "connection-1",
|
|
type: "connection" as const,
|
|
url: "https://api.prisma.test/v1/connections/connection-1",
|
|
name: "api",
|
|
createdAt,
|
|
kind: "postgres" as const,
|
|
endpoints: {
|
|
direct: {
|
|
host: "db.prisma.test",
|
|
port: 5432,
|
|
},
|
|
},
|
|
database: resourceRef("databases", "database-1", "main"),
|
|
},
|
|
];
|
|
}),
|
|
listBranches: (projectId: string, query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listBranches", { projectId, query }]);
|
|
return [
|
|
{
|
|
id: "branch-1",
|
|
type: "branch" as const,
|
|
url: "https://api.prisma.test/v1/branches/branch-1",
|
|
gitName: "main",
|
|
isDefault: true,
|
|
createdAt,
|
|
updatedAt,
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
},
|
|
];
|
|
}),
|
|
listApps: (query: { projectId: string; limit?: number }) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listApps", query]);
|
|
return [
|
|
{
|
|
id: "service-1",
|
|
type: "app" as const,
|
|
url: "https://api.prisma.test/v1/services/service-1",
|
|
name: "api",
|
|
region: { id: "us-east-1", name: "US East" },
|
|
projectId: query.projectId,
|
|
branchId: "branch-1",
|
|
latestDeploymentId: "version-1",
|
|
appEndpointDomain: "api.prisma.build",
|
|
createdAt,
|
|
},
|
|
];
|
|
}),
|
|
listAppDeployments: (appId: string, query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listAppDeployments", { appId, query }]);
|
|
return [
|
|
{
|
|
id: "version-1",
|
|
type: "deployment" as const,
|
|
serviceId: "service-1",
|
|
url: "https://api.prisma.test/v1/deployments/version-1",
|
|
foundryVersionId: "foundry-1",
|
|
createdAt,
|
|
},
|
|
];
|
|
}),
|
|
getDeployment: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getDeployment", id]);
|
|
return {
|
|
id,
|
|
type: "deployment" as const,
|
|
serviceId: "service-1",
|
|
url: `https://api.prisma.test/v1/deployments/${id}`,
|
|
foundryVersionId: "foundry-1",
|
|
status: "running",
|
|
previewDomain: "version-1.preview.prisma.build",
|
|
createdAt,
|
|
};
|
|
}),
|
|
listEnvironmentVariables: (query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listEnvironmentVariables", query]);
|
|
return [
|
|
{
|
|
id: "env-1",
|
|
type: "environment-variable" as const,
|
|
url: "https://api.prisma.test/v1/environment-variables/env-1",
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
valueKid: "kid-1",
|
|
isManagedBySystem: false,
|
|
createdAt,
|
|
updatedAt,
|
|
},
|
|
];
|
|
}),
|
|
listSourceRepositories: (query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listSourceRepositories", query]);
|
|
return [
|
|
{
|
|
id: "repo-1",
|
|
type: "source-repository" as const,
|
|
url: "https://api.prisma.test/v1/source-repositories/repo-1",
|
|
repoId: 123,
|
|
provider: "github" as const,
|
|
repoFullName: "acme/api",
|
|
defaultBranch: "main",
|
|
isPrivate: true,
|
|
status: "active" as const,
|
|
projectId: "project-1",
|
|
installationId: "installation-1",
|
|
createdAt,
|
|
updatedAt,
|
|
},
|
|
];
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const projectProvider = yield* Provider.findProvider(PrismaProject);
|
|
const databaseProvider = yield* Provider.findProvider(PrismaDatabase);
|
|
const connectionProvider =
|
|
yield* Provider.findProvider(PrismaConnection);
|
|
const branchProvider = yield* Provider.findProvider(PrismaBranch);
|
|
const serviceProvider = yield* Provider.findProvider(PrismaApp);
|
|
const versionProvider =
|
|
yield* Provider.findProvider(PrismaDeployment);
|
|
const envProvider = yield* Provider.findProvider(
|
|
PrismaEnvironmentVariable,
|
|
);
|
|
const repoProvider = yield* Provider.findProvider(
|
|
PrismaSourceRepository,
|
|
);
|
|
|
|
const project = yield* projectProvider.read!(
|
|
readInput("Project", { name: "app" }),
|
|
);
|
|
const database = yield* databaseProvider.read!(
|
|
readInput("Database", { project: "project-1", name: "main" }),
|
|
);
|
|
const connection = yield* connectionProvider.read!(
|
|
readInput("Connection", { database: "database-1", name: "api" }),
|
|
);
|
|
const branch = yield* branchProvider.read!(
|
|
readInput("Branch", { project: "project-1", gitName: "main" }),
|
|
);
|
|
const service = yield* serviceProvider.read!(
|
|
readInput("App", {
|
|
project: "project-1",
|
|
displayName: "api",
|
|
}),
|
|
);
|
|
const version = yield* versionProvider.read!(
|
|
readInput(
|
|
"Deployment",
|
|
{ app: "service-1" },
|
|
{
|
|
deploymentId: "version-1",
|
|
appId: "service-1",
|
|
foundryVersionId: "foundry-1",
|
|
status: "new",
|
|
previewDomain: null,
|
|
uploadUrl: null,
|
|
appEndpointDomain: undefined,
|
|
createdAt,
|
|
},
|
|
),
|
|
);
|
|
const env = yield* envProvider.read!(
|
|
readInput("EnvironmentVariable", {
|
|
project: "project-1",
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
value: Redacted.make("secret"),
|
|
}),
|
|
);
|
|
const repo = yield* repoProvider.read!(
|
|
readInput("SourceRepository", {
|
|
project: "project-1",
|
|
providerRepositoryId: 123,
|
|
}),
|
|
);
|
|
|
|
expect(project?.projectId).toBe("project-1");
|
|
expect(project?.databaseId).toBe("database-1");
|
|
expect(database?.databaseId).toBe("database-1");
|
|
expect(connection?.connectionId).toBe("connection-1");
|
|
expect(branch?.branchId).toBe("branch-1");
|
|
expect(service?.appId).toBe("service-1");
|
|
expect(version?.deploymentId).toBe("version-1");
|
|
expect(version?.status).toBe("running");
|
|
expect(env?.environmentVariableId).toBe("env-1");
|
|
expect(env?.valueKid).toBe("kid-1");
|
|
expect(env?.value && Redacted.value(env.value)).toBe("");
|
|
expect(repo?.sourceRepositoryId).toBe("repo-1");
|
|
expect(Unowned.is(project!)).toBe(true);
|
|
expect(Unowned.is(database!)).toBe(true);
|
|
expect(Unowned.is(branch!)).toBe(true);
|
|
expect(Unowned.is(env!)).toBe(true);
|
|
expect(Unowned.is(repo!)).toBe(true);
|
|
expect(routesOf(fake.captured)).toEqual([
|
|
"GET /v1/projects",
|
|
"GET /v1/projects/project-1/databases",
|
|
"GET /v1/projects/project-1/databases",
|
|
"GET /v1/databases/database-1/connections",
|
|
"GET /v1/databases/database-1/connections",
|
|
"GET /v1/databases/database-1/connections",
|
|
"GET /v1/projects/project-1/branches",
|
|
"GET /v1/services",
|
|
"GET /v1/projects/project-1/branches",
|
|
"GET /v1/deployments/version-1",
|
|
"GET /v1/services/service-1/deployments",
|
|
"GET /v1/environment-variables",
|
|
"GET /v1/source-repositories",
|
|
]);
|
|
expect(calls.map(([operation]) => operation)).toEqual([
|
|
"listDatabaseConnections",
|
|
"listDatabaseConnections",
|
|
"listDatabaseConnections",
|
|
"listBranches",
|
|
"listApps",
|
|
"listBranches",
|
|
"getDeployment",
|
|
"listAppDeployments",
|
|
"listEnvironmentVariables",
|
|
"listSourceRepositories",
|
|
]);
|
|
expect(routesOf(fake.captured)).not.toContain("POST /v1/projects");
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(fake.layer),
|
|
Effect.provide(FetchHttpClient.layer),
|
|
Effect.provideService(Stack, {
|
|
name: "prisma-provider-read-test",
|
|
stage: "test",
|
|
resources: {},
|
|
bindings: {},
|
|
actions: {},
|
|
}),
|
|
Effect.provideService(Stage, "test"),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"treats Prisma 404 during delete as already gone",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const notFound = (method: "GET" | "DELETE", path: string) =>
|
|
new PrismaApiError({
|
|
method,
|
|
path,
|
|
status: 404,
|
|
message: "not found",
|
|
});
|
|
const failNotFound = (
|
|
operation: string,
|
|
id: string,
|
|
method: "GET" | "DELETE",
|
|
path: string,
|
|
) =>
|
|
Effect.gen(function* () {
|
|
calls.push([operation, id]);
|
|
return yield* Effect.fail(notFound(method, path));
|
|
});
|
|
|
|
const client = {
|
|
listApps: (query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listApps", query]);
|
|
return [];
|
|
}),
|
|
deleteProject: (id: string) =>
|
|
failNotFound("deleteProject", id, "DELETE", `/v1/projects/${id}`),
|
|
getDatabase: (id: string) =>
|
|
failNotFound("getDatabase", id, "GET", `/v1/databases/${id}`),
|
|
deleteDatabase: (id: string) =>
|
|
failNotFound("deleteDatabase", id, "DELETE", `/v1/databases/${id}`),
|
|
getConnection: (id: string) =>
|
|
failNotFound("getConnection", id, "GET", `/v1/connections/${id}`),
|
|
deleteConnection: (id: string) =>
|
|
failNotFound(
|
|
"deleteConnection",
|
|
id,
|
|
"DELETE",
|
|
`/v1/connections/${id}`,
|
|
),
|
|
getBranch: (id: string) =>
|
|
failNotFound("getBranch", id, "GET", `/v1/branches/${id}`),
|
|
deleteBranch: (id: string) =>
|
|
failNotFound("deleteBranch", id, "DELETE", `/v1/branches/${id}`),
|
|
getEnvironmentVariable: (id: string) =>
|
|
failNotFound(
|
|
"getEnvironmentVariable",
|
|
id,
|
|
"GET",
|
|
`/v1/environment-variables/${id}`,
|
|
),
|
|
deleteEnvironmentVariable: (id: string) =>
|
|
failNotFound(
|
|
"deleteEnvironmentVariable",
|
|
id,
|
|
"DELETE",
|
|
`/v1/environment-variables/${id}`,
|
|
),
|
|
deleteSourceRepository: (id: string) =>
|
|
failNotFound(
|
|
"deleteSourceRepository",
|
|
id,
|
|
"DELETE",
|
|
`/v1/source-repositories/${id}`,
|
|
),
|
|
getSourceRepository: (id: string) =>
|
|
failNotFound(
|
|
"getSourceRepository",
|
|
id,
|
|
"GET",
|
|
`/v1/source-repositories/${id}`,
|
|
),
|
|
listAppDeployments: (appId: string) =>
|
|
failNotFound(
|
|
"listAppDeployments",
|
|
appId,
|
|
"GET",
|
|
`/v1/services/${appId}/deployments`,
|
|
),
|
|
deleteApp: (id: string) =>
|
|
failNotFound("deleteApp", id, "DELETE", `/v1/services/${id}`),
|
|
getApp: (id: string) =>
|
|
failNotFound("getApp", id, "GET", `/v1/services/${id}`),
|
|
getDeployment: (id: string) =>
|
|
failNotFound("getDeployment", id, "GET", `/v1/deployments/${id}`),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const projectProvider = yield* PrismaProject.Provider;
|
|
const databaseProvider = yield* PrismaDatabase.Provider;
|
|
const connectionProvider = yield* PrismaConnection.Provider;
|
|
const branchProvider = yield* PrismaBranch.Provider;
|
|
const serviceProvider = yield* PrismaApp.Provider;
|
|
const versionProvider = yield* PrismaDeployment.Provider;
|
|
const envProvider = yield* PrismaEnvironmentVariable.Provider;
|
|
const repoProvider = yield* PrismaSourceRepository.Provider;
|
|
|
|
yield* projectProvider.delete!(
|
|
deleteInput("Project", { projectId: "project-1" }),
|
|
);
|
|
yield* databaseProvider.delete!(
|
|
deleteInput("Database", { databaseId: "database-1" }),
|
|
);
|
|
yield* connectionProvider.delete!(
|
|
deleteInput("Connection", { connectionId: "connection-1" }),
|
|
);
|
|
yield* branchProvider.delete!(
|
|
deleteInput("Branch", { branchId: "branch-1" }),
|
|
);
|
|
yield* envProvider.delete!(
|
|
deleteInput("EnvironmentVariable", {
|
|
environmentVariableId: "env-1",
|
|
}),
|
|
);
|
|
yield* repoProvider.delete!(
|
|
deleteInput("SourceRepository", { sourceRepositoryId: "repo-1" }),
|
|
);
|
|
yield* serviceProvider.delete!(
|
|
deleteInput("App", { appId: "service-1" }),
|
|
);
|
|
yield* versionProvider.delete!(
|
|
deleteInput("Deployment", { deploymentId: "version-1" }),
|
|
);
|
|
|
|
expect(calls).toEqual([
|
|
// Over the wire, query params arrive as strings.
|
|
["listApps", { limit: "100", projectId: "project-1" }],
|
|
["deleteProject", "project-1"],
|
|
["getDatabase", "database-1"],
|
|
["getConnection", "connection-1"],
|
|
["getBranch", "branch-1"],
|
|
["getEnvironmentVariable", "env-1"],
|
|
["getSourceRepository", "repo-1"],
|
|
["getApp", "service-1"],
|
|
["getDeployment", "version-1"],
|
|
]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"does not delete cloud projects for dev placeholder project IDs",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const client = {
|
|
listProjects: () =>
|
|
Effect.sync(() => {
|
|
calls.push(["listProjects"]);
|
|
return [
|
|
{
|
|
id: "project-cloud",
|
|
type: "project",
|
|
url: "https://api.prisma.test/v1/projects/project-cloud",
|
|
name: "local-project",
|
|
createdAt,
|
|
defaultRegion: "us-east-1",
|
|
workspace: resourceRef("workspaces", "workspace-1", "team"),
|
|
database: null,
|
|
},
|
|
];
|
|
}),
|
|
deleteProject: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteProject", id]);
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const projectProvider = yield* PrismaProject.Provider;
|
|
|
|
yield* projectProvider.delete!(
|
|
deleteInput("Project", {
|
|
projectId: "dev:project:Project",
|
|
projectName: "local-project",
|
|
}),
|
|
);
|
|
|
|
expect(calls).toEqual([]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"reconciles each greenfield Prisma resource through the client",
|
|
() => {
|
|
const { client, calls } = makeClient();
|
|
const fake = makeFakeManagementApi((request) => {
|
|
if (
|
|
request.pathname === "/v1/projects" &&
|
|
request.method === "POST"
|
|
) {
|
|
return envelope(
|
|
{ ...wireProject(), database: null },
|
|
{
|
|
status: 201,
|
|
},
|
|
);
|
|
}
|
|
return dispatchManagement(client, request);
|
|
});
|
|
|
|
return Effect.gen(function* () {
|
|
const projectProvider = yield* PrismaProject.Provider;
|
|
const databaseProvider = yield* PrismaDatabase.Provider;
|
|
const connectionProvider = yield* PrismaConnection.Provider;
|
|
const branchProvider = yield* PrismaBranch.Provider;
|
|
const serviceProvider = yield* PrismaApp.Provider;
|
|
const versionProvider = yield* PrismaDeployment.Provider;
|
|
const envProvider = yield* PrismaEnvironmentVariable.Provider;
|
|
const repoProvider = yield* PrismaSourceRepository.Provider;
|
|
|
|
const project = yield* projectProvider.reconcile(
|
|
reconcileInput("Project", {
|
|
name: "app",
|
|
createDatabase: false,
|
|
region: "us-east-1",
|
|
}),
|
|
);
|
|
const database = yield* databaseProvider.reconcile(
|
|
reconcileInput("Database", {
|
|
project: project.projectId,
|
|
name: "main",
|
|
region: "us-east-1",
|
|
}),
|
|
);
|
|
const connection = yield* connectionProvider.reconcile(
|
|
reconcileInput("Connection", {
|
|
database: database.databaseId,
|
|
name: "api",
|
|
}),
|
|
);
|
|
const branch = yield* branchProvider.reconcile(
|
|
reconcileInput("Branch", {
|
|
project: project.projectId,
|
|
gitName: "preview",
|
|
isDefault: false,
|
|
}),
|
|
);
|
|
const service = yield* serviceProvider.reconcile(
|
|
reconcileInput("App", {
|
|
project: project.projectId,
|
|
displayName: "api",
|
|
regionId: "us-east-1",
|
|
}),
|
|
);
|
|
const version = yield* versionProvider.reconcile(
|
|
reconcileInput("Deployment", {
|
|
app: service.appId,
|
|
portMapping: { http: 3000 },
|
|
skipCodeUpload: true,
|
|
}),
|
|
);
|
|
const env = yield* envProvider.reconcile(
|
|
reconcileInput("EnvironmentVariable", {
|
|
project: project.projectId,
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
value: Redacted.make("secret"),
|
|
}),
|
|
);
|
|
const repo = yield* repoProvider.reconcile(
|
|
reconcileInput("SourceRepository", {
|
|
project: project.projectId,
|
|
providerRepositoryId: 123,
|
|
}),
|
|
);
|
|
|
|
expect(project.projectId).toBe("project-1");
|
|
expect(database.databaseId).toBe("database-1");
|
|
expect(Redacted.value(database.directConnectionString!)).toBe(
|
|
"postgres://direct",
|
|
);
|
|
expect(connection.connectionId).toBe("connection-2");
|
|
expectJsonNotToContain(
|
|
database,
|
|
"postgres://direct",
|
|
"postgres://pooled",
|
|
);
|
|
expectJsonNotToContain(connection, "postgres://api-direct");
|
|
expect(branch.branchId).toBe("branch-2");
|
|
expect(service.appId).toBe("service-1");
|
|
expect(version.deploymentId).toBe("version-1");
|
|
expect(env.environmentVariableId).toBe("env-1");
|
|
expect(Redacted.value(env.value)).toBe("secret");
|
|
expectJsonNotToContain(env, "secret");
|
|
expect(repo.sourceRepositoryId).toBe("repo-1");
|
|
|
|
expect(routesOf(fake.captured)).toEqual([
|
|
"POST /v1/projects",
|
|
"POST /v1/databases",
|
|
"GET /v1/databases/database-1/connections",
|
|
"POST /v1/connections",
|
|
"GET /v1/projects/project-1/branches",
|
|
"POST /v1/projects/project-1/branches",
|
|
"GET /v1/projects/project-1/branches",
|
|
"POST /v1/services",
|
|
"GET /v1/projects/project-1/branches",
|
|
"POST /v1/services/service-1/deployments",
|
|
"GET /v1/deployments/version-1",
|
|
"POST /v1/environment-variables",
|
|
"GET /v1/services",
|
|
"GET /v1/projects/project-1/databases",
|
|
"POST /v1/source-repositories",
|
|
"GET /v1/source-repositories/repo-1",
|
|
"GET /v1/projects/project-1/branches",
|
|
]);
|
|
expect(fake.captured[0]?.bodyJson).toEqual({
|
|
name: "app",
|
|
createDatabase: false,
|
|
region: "us-east-1",
|
|
});
|
|
expect(calls).toEqual([
|
|
[
|
|
"createDatabase",
|
|
// JSON transport drops `undefined` members.
|
|
{
|
|
projectId: "project-1",
|
|
region: "us-east-1",
|
|
name: "main",
|
|
isDefault: false,
|
|
},
|
|
],
|
|
[
|
|
"listDatabaseConnections",
|
|
{ databaseId: "database-1", query: { limit: 100 } },
|
|
],
|
|
[
|
|
"createConnection",
|
|
{ databaseId: "database-1", name: "api-000000000000" },
|
|
],
|
|
["listBranches", { projectId: "project-1", query: {} }],
|
|
[
|
|
"createBranch",
|
|
{
|
|
projectId: "project-1",
|
|
input: { gitName: "preview", isDefault: false },
|
|
},
|
|
],
|
|
[
|
|
"listBranches",
|
|
{ projectId: "project-1", query: { limit: "100" } },
|
|
],
|
|
[
|
|
"createApp",
|
|
// JSON transport drops `undefined` members.
|
|
{
|
|
projectId: "project-1",
|
|
displayName: "api",
|
|
branchId: "branch-1",
|
|
regionId: "us-east-1",
|
|
},
|
|
],
|
|
[
|
|
"listBranches",
|
|
{ projectId: "project-1", query: { limit: "100" } },
|
|
],
|
|
[
|
|
"createAppDeployment",
|
|
{
|
|
appId: "service-1",
|
|
input: {
|
|
portMapping: { http: 3000 },
|
|
skipCodeUpload: true,
|
|
},
|
|
},
|
|
],
|
|
["getDeployment", "version-1"],
|
|
[
|
|
"createEnvironmentVariable",
|
|
{
|
|
projectId: "project-1",
|
|
class: "production",
|
|
key: "TOKEN",
|
|
value: "secret",
|
|
},
|
|
],
|
|
[
|
|
"listApps",
|
|
// Query parameters arrive as strings over the wire.
|
|
{ limit: "100", projectId: "project-1", branchId: "unassigned" },
|
|
],
|
|
[
|
|
"listProjectDatabases",
|
|
{ projectId: "project-1", query: { limit: 100 } },
|
|
],
|
|
[
|
|
"createSourceRepository",
|
|
// JSON transport drops `undefined` members.
|
|
{
|
|
projectId: "project-1",
|
|
provider: "github",
|
|
providerRepositoryId: 123,
|
|
},
|
|
],
|
|
["getSourceRepository", "repo-1"],
|
|
[
|
|
"listBranches",
|
|
{
|
|
projectId: "project-1",
|
|
query: { limit: "100", gitName: "main" },
|
|
},
|
|
],
|
|
]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(fake.layer),
|
|
Effect.provide(FetchHttpClient.layer),
|
|
Effect.provideService(Stack, {
|
|
name: "prisma-provider-test",
|
|
stage: "test",
|
|
resources: {},
|
|
bindings: {},
|
|
actions: {},
|
|
}),
|
|
Effect.provideService(Stage, "test"),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"reconciles a Prisma custom domain through the client",
|
|
() => {
|
|
const { client, calls } = makeClient();
|
|
|
|
return Effect.gen(function* () {
|
|
const domainProvider = yield* PrismaCustomDomain.Provider;
|
|
const domain = yield* domainProvider.reconcile(
|
|
reconcileInput("CustomDomain", {
|
|
app: "service-1",
|
|
hostname: "api.example.com",
|
|
}),
|
|
);
|
|
|
|
expect(domain.customDomainId).toBe("domain-1");
|
|
expect(domain.hostname).toBe("api.example.com");
|
|
expect(domain.appId).toBe("service-1");
|
|
expect(domain.foundryStatus).toBe("pending_dns");
|
|
expect(domain.dnsRecords[0]?.type).toBe("CNAME");
|
|
expect(calls).toEqual([
|
|
["listAppDomains", "service-1"],
|
|
["getApp", "service-1"],
|
|
["getBranch", "branch-1"],
|
|
[
|
|
"createAppDomain",
|
|
{
|
|
appId: "service-1",
|
|
input: { hostname: "api.example.com" },
|
|
},
|
|
],
|
|
]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"refuses to claim an existing custom domain during reconcile",
|
|
() => {
|
|
const { client, calls } = makeClient();
|
|
const existing = {
|
|
id: "domain-existing",
|
|
type: "custom-domain" as const,
|
|
url: "https://api.prisma.test/v1/domains/domain-existing",
|
|
hostname: "api.example.com",
|
|
appId: "service-1",
|
|
status: "active" as const,
|
|
foundryStatus: "ready",
|
|
failureReason: null,
|
|
failureCategory: null,
|
|
certExpiresAt: null,
|
|
dnsRecords: [],
|
|
createdAt,
|
|
updatedAt,
|
|
};
|
|
Object.assign(client, {
|
|
listAppDomains: (appId: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listAppDomains", appId]);
|
|
return [existing];
|
|
}),
|
|
createAppDomain: () =>
|
|
Effect.die("must not claim an existing domain"),
|
|
});
|
|
|
|
return Effect.gen(function* () {
|
|
const domainProvider = yield* PrismaCustomDomain.Provider;
|
|
const error = yield* domainProvider
|
|
.reconcile(
|
|
reconcileInput("CustomDomain", {
|
|
app: "service-1",
|
|
hostname: "api.example.com",
|
|
}),
|
|
)
|
|
.pipe(Effect.flip);
|
|
|
|
expect((error as Error).message).toContain("explicit adoption");
|
|
expect(calls).toEqual([["listAppDomains", "service-1"]]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"routes a 200 custom-domain create race through explicit adoption",
|
|
() => {
|
|
const { client, calls } = makeClient();
|
|
let visible = false;
|
|
const raced = {
|
|
id: "domain-raced",
|
|
type: "custom-domain" as const,
|
|
url: "https://api.prisma.test/v1/domains/domain-raced",
|
|
hostname: "api.example.com",
|
|
appId: "service-1",
|
|
status: "pending_dns" as const,
|
|
foundryStatus: "pending",
|
|
failureReason: null,
|
|
failureCategory: null,
|
|
certExpiresAt: null,
|
|
dnsRecords: [],
|
|
createdAt,
|
|
updatedAt,
|
|
};
|
|
Object.assign(client, {
|
|
listAppDomains: (appId: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listAppDomains", appId]);
|
|
return visible ? [raced] : [];
|
|
}),
|
|
createAppDomain: (appId: string, input: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["createAppDomain", { appId, input }]);
|
|
visible = true;
|
|
return { status: 200 as const, domain: raced };
|
|
}),
|
|
});
|
|
|
|
return Effect.gen(function* () {
|
|
const domainProvider =
|
|
yield* Provider.findProvider(PrismaCustomDomain);
|
|
const error = yield* domainProvider
|
|
.reconcile(
|
|
reconcileInput("CustomDomain", {
|
|
app: "service-1",
|
|
hostname: raced.hostname,
|
|
}),
|
|
)
|
|
.pipe(Effect.flip);
|
|
|
|
expect((error as Error).message).toContain("explicit adoption");
|
|
expect(calls).toContainEqual([
|
|
"createAppDomain",
|
|
{
|
|
appId: "service-1",
|
|
input: { hostname: raced.hostname },
|
|
},
|
|
]);
|
|
|
|
const observed = yield* domainProvider.read!(
|
|
readInput("CustomDomain", {
|
|
app: "service-1",
|
|
hostname: raced.hostname,
|
|
}),
|
|
);
|
|
expect(observed?.customDomainId).toBe(raced.id);
|
|
expect(Unowned.is(observed!)).toBe(true);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"normalizes Prisma custom domain hostnames when matching",
|
|
() => {
|
|
const { client, calls } = makeClient();
|
|
Object.assign(client, {
|
|
listAppDomains: (appId: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listAppDomains", appId]);
|
|
return [
|
|
{
|
|
id: "domain-1",
|
|
type: "custom-domain" as const,
|
|
url: "https://api.prisma.test/v1/domains/domain-1",
|
|
hostname: "api.example.com",
|
|
appId: appId,
|
|
status: "pending_dns" as const,
|
|
foundryStatus: "pending",
|
|
failureReason: null,
|
|
failureCategory: null,
|
|
certExpiresAt: null,
|
|
dnsRecords: [
|
|
{
|
|
type: "CNAME" as const,
|
|
name: "api.example.com",
|
|
value: "service-1.prisma.build",
|
|
ttl: null,
|
|
},
|
|
],
|
|
createdAt,
|
|
updatedAt,
|
|
},
|
|
];
|
|
}),
|
|
});
|
|
|
|
return Effect.gen(function* () {
|
|
const domainProvider =
|
|
yield* Provider.findProvider(PrismaCustomDomain);
|
|
const domain = yield* domainProvider.read!(
|
|
readInput("CustomDomain", {
|
|
app: "service-1",
|
|
hostname: "API.EXAMPLE.COM.",
|
|
}),
|
|
);
|
|
|
|
expect(domain?.customDomainId).toBe("domain-1");
|
|
expect(domain?.hostname).toBe("api.example.com");
|
|
expect(Unowned.is(domain!)).toBe(true);
|
|
expect(calls).toEqual([["listAppDomains", "service-1"]]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"rejects Prisma custom domains on non-default branches",
|
|
() => {
|
|
const { client, calls } = makeClient();
|
|
Object.assign(client, {
|
|
getBranch: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getBranch", id]);
|
|
return {
|
|
id,
|
|
type: "branch" as const,
|
|
url: `https://api.prisma.test/v1/branches/${id}`,
|
|
gitName: "preview",
|
|
isDefault: false,
|
|
createdAt,
|
|
updatedAt,
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
};
|
|
}),
|
|
});
|
|
|
|
return Effect.gen(function* () {
|
|
const domainProvider = yield* PrismaCustomDomain.Provider;
|
|
const error = yield* domainProvider
|
|
.reconcile(
|
|
reconcileInput("CustomDomain", {
|
|
app: "service-1",
|
|
hostname: "api.example.com",
|
|
}),
|
|
)
|
|
.pipe(Effect.flip);
|
|
|
|
expect(error).toBeInstanceOf(Error);
|
|
expect((error as Error).message).toContain(
|
|
"custom domains can only be attached to apps on the default Branch",
|
|
);
|
|
expect(calls).toEqual([
|
|
["listAppDomains", "service-1"],
|
|
["getApp", "service-1"],
|
|
["getBranch", "branch-1"],
|
|
]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"starts a direct deployment only after observing status",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
let status = "new";
|
|
let latestDeploymentId: string | null = null;
|
|
const client = {
|
|
createAppDeployment: (appId: string, input: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["createAppDeployment", { appId, input }]);
|
|
return {
|
|
id: "version-1",
|
|
type: "deployment" as const,
|
|
url: "https://api.prisma.test/v1/deployments/version-1",
|
|
foundryVersionId: "foundry-1",
|
|
uploadUrl: null,
|
|
};
|
|
}),
|
|
getDeployment: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getDeployment", id]);
|
|
return {
|
|
id,
|
|
type: "deployment" as const,
|
|
serviceId: "service-1",
|
|
url: `https://api.prisma.test/v1/deployments/${id}`,
|
|
foundryVersionId: "foundry-1",
|
|
status,
|
|
previewDomain:
|
|
status === "running"
|
|
? "version-1.preview.prisma.build"
|
|
: null,
|
|
createdAt,
|
|
};
|
|
}),
|
|
startDeployment: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["startDeployment", id]);
|
|
status = "running";
|
|
return { previewDomain: "version-1.preview.prisma.build" };
|
|
}),
|
|
getApp: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getApp", id]);
|
|
return {
|
|
id,
|
|
type: "app" as const,
|
|
url: `https://api.prisma.test/v1/services/${id}`,
|
|
name: "api-000000000000",
|
|
region: { id: "us-east-1", name: "US East" },
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
latestDeploymentId,
|
|
appEndpointDomain: "api.prisma.build",
|
|
createdAt,
|
|
};
|
|
}),
|
|
promoteApp: (
|
|
appId: string,
|
|
{ deploymentId }: { deploymentId: string },
|
|
) =>
|
|
Effect.sync(() => {
|
|
calls.push(["promoteApp", { appId, deploymentId }]);
|
|
latestDeploymentId = deploymentId;
|
|
return { appEndpointDomain: "api.prisma.build" };
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const provider = yield* PrismaDeployment.Provider;
|
|
const output = yield* provider.reconcile(
|
|
reconcileInput("Deployment", {
|
|
app: "service-1",
|
|
skipCodeUpload: true,
|
|
start: true,
|
|
promote: true,
|
|
}),
|
|
);
|
|
|
|
expect(output.status).toBe("running");
|
|
expect(output.previewDomain).toBe("version-1.preview.prisma.build");
|
|
expect(output.appEndpointDomain).toBe("api.prisma.build");
|
|
expect(calls).toEqual([
|
|
[
|
|
"createAppDeployment",
|
|
{
|
|
appId: "service-1",
|
|
input: {
|
|
portMapping: undefined,
|
|
skipCodeUpload: true,
|
|
},
|
|
},
|
|
],
|
|
["getDeployment", "version-1"],
|
|
["startDeployment", "version-1"],
|
|
["getDeployment", "version-1"],
|
|
["promoteApp", { appId: "service-1", deploymentId: "version-1" }],
|
|
["getApp", "service-1"],
|
|
]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
Effect.provide(FetchHttpClient.layer),
|
|
Effect.provideService(Stack, {
|
|
name: "prisma-deployment-test",
|
|
stage: "test",
|
|
resources: {},
|
|
bindings: {},
|
|
actions: {},
|
|
}),
|
|
Effect.provideService(Stage, "test"),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"ignores branch env overrides when reconciling project env vars",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const branchVariable = {
|
|
id: "env-branch",
|
|
type: "environment-variable" as const,
|
|
url: "https://api.prisma.test/v1/environment-variables/env-branch",
|
|
projectId: "project-1",
|
|
branchId: "branch-1",
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
valueKid: "kid-branch",
|
|
isManagedBySystem: false,
|
|
createdAt,
|
|
updatedAt,
|
|
};
|
|
const projectVariable = {
|
|
id: "env-project",
|
|
type: "environment-variable" as const,
|
|
url: "https://api.prisma.test/v1/environment-variables/env-project",
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
valueKid: "kid-old",
|
|
isManagedBySystem: false,
|
|
createdAt,
|
|
updatedAt,
|
|
};
|
|
const client = {
|
|
listEnvironmentVariables: (query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listEnvironmentVariables", query]);
|
|
return [
|
|
branchVariable,
|
|
{ ...branchVariable, id: "env-branch-2", branchId: "branch-2" },
|
|
projectVariable,
|
|
];
|
|
}),
|
|
getEnvironmentVariable: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getEnvironmentVariable", id]);
|
|
return projectVariable;
|
|
}),
|
|
updateEnvironmentVariable: (id: string, input: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["updateEnvironmentVariable", { id, input }]);
|
|
return {
|
|
...projectVariable,
|
|
id,
|
|
valueKid: "kid-new",
|
|
updatedAt: "2026-01-01T00:00:02Z",
|
|
};
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const envProvider = yield* Provider.findProvider(
|
|
PrismaEnvironmentVariable,
|
|
);
|
|
const observed = yield* envProvider.read!(
|
|
readInput("EnvironmentVariable", {
|
|
project: "project-1",
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
value: Redacted.make("secret"),
|
|
}),
|
|
);
|
|
expect(Unowned.is(observed!)).toBe(true);
|
|
const env = yield* envProvider.reconcile(
|
|
reconcileInput(
|
|
"EnvironmentVariable",
|
|
{
|
|
project: "project-1",
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
value: Redacted.make("secret"),
|
|
},
|
|
observed,
|
|
),
|
|
);
|
|
|
|
expect(env.environmentVariableId).toBe("env-project");
|
|
expect(env.branchId).toBeNull();
|
|
expect(redactedValue(env.value)).toBe("secret");
|
|
expect(calls).toEqual([
|
|
[
|
|
"listEnvironmentVariables",
|
|
// Query parameters arrive as strings over the wire.
|
|
{
|
|
limit: "100",
|
|
projectId: "project-1",
|
|
class: "production",
|
|
key: "TOKEN",
|
|
},
|
|
],
|
|
["getEnvironmentVariable", "env-project"],
|
|
[
|
|
"updateEnvironmentVariable",
|
|
{ id: "env-project", input: { value: "secret" } },
|
|
],
|
|
]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"refuses to mutate system-managed environment variables",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const systemVariable = {
|
|
id: "env-system",
|
|
type: "environment-variable" as const,
|
|
url: "https://api.prisma.test/v1/environment-variables/env-system",
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
class: "production" as const,
|
|
key: "PRISMA_INTERNAL_URL",
|
|
valueKid: "kid-system",
|
|
isManagedBySystem: true,
|
|
createdAt,
|
|
updatedAt,
|
|
};
|
|
const client = {
|
|
listEnvironmentVariables: (query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listEnvironmentVariables", query]);
|
|
return [systemVariable];
|
|
}),
|
|
getEnvironmentVariable: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getEnvironmentVariable", id]);
|
|
return systemVariable;
|
|
}),
|
|
updateEnvironmentVariable: (id: string, input: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["updateEnvironmentVariable", { id, input }]);
|
|
return systemVariable;
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const envProvider = yield* Provider.findProvider(
|
|
PrismaEnvironmentVariable,
|
|
);
|
|
const observed = yield* envProvider.read!(
|
|
readInput("EnvironmentVariable", {
|
|
project: "project-1",
|
|
class: "production" as const,
|
|
key: "PRISMA_INTERNAL_URL",
|
|
value: Redacted.make("secret"),
|
|
}),
|
|
);
|
|
const error = yield* envProvider
|
|
.reconcile(
|
|
reconcileInput(
|
|
"EnvironmentVariable",
|
|
{
|
|
project: "project-1",
|
|
class: "production" as const,
|
|
key: "PRISMA_INTERNAL_URL",
|
|
value: Redacted.make("secret"),
|
|
},
|
|
observed,
|
|
),
|
|
)
|
|
.pipe(Effect.flip);
|
|
|
|
expect(error).toBeInstanceOf(Error);
|
|
expect((error as Error).message).toContain(
|
|
"is managed by Prisma and cannot be managed by Alchemy",
|
|
);
|
|
expect(calls).toEqual([
|
|
[
|
|
"listEnvironmentVariables",
|
|
// Query parameters arrive as strings over the wire.
|
|
{
|
|
limit: "100",
|
|
projectId: "project-1",
|
|
class: "production",
|
|
key: "PRISMA_INTERNAL_URL",
|
|
},
|
|
],
|
|
["getEnvironmentVariable", "env-system"],
|
|
]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"skips direct delete for system-managed environment variables",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const notes: string[] = [];
|
|
const client = {
|
|
getEnvironmentVariable: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getEnvironmentVariable", id]);
|
|
return {
|
|
id,
|
|
type: "environment-variable" as const,
|
|
url: `https://api.prisma.test/v1/environment-variables/${id}`,
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
class: "production" as const,
|
|
key: "PRISMA_INTERNAL_URL",
|
|
valueKid: "kid-system",
|
|
isManagedBySystem: true,
|
|
createdAt,
|
|
updatedAt,
|
|
};
|
|
}),
|
|
deleteEnvironmentVariable: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteEnvironmentVariable", id]);
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const envProvider = yield* PrismaEnvironmentVariable.Provider;
|
|
yield* envProvider.delete!({
|
|
id: "EnvironmentVariable",
|
|
fqn: "EnvironmentVariable",
|
|
instanceId: "00000000000000000000000000000000",
|
|
olds: {} as never,
|
|
output: {
|
|
environmentVariableId: "env-system",
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
class: "production",
|
|
key: "PRISMA_INTERNAL_URL",
|
|
value: Redacted.make("secret"),
|
|
valueKid: "kid-system",
|
|
isManagedBySystem: true,
|
|
createdAt,
|
|
updatedAt,
|
|
},
|
|
session: {
|
|
note: (message: string) =>
|
|
Effect.sync(() => {
|
|
notes.push(message);
|
|
}),
|
|
} as never,
|
|
bindings: [],
|
|
});
|
|
|
|
expect(calls).toEqual([["getEnvironmentVariable", "env-system"]]);
|
|
expect(notes).toEqual([
|
|
"Skipping direct delete for system-managed Prisma environment variable 'PRISMA_INTERNAL_URL'.",
|
|
]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"checks live environment variable ownership before deleting",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const client = {
|
|
getEnvironmentVariable: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getEnvironmentVariable", id]);
|
|
return {
|
|
id,
|
|
type: "environment-variable" as const,
|
|
url: `https://api.prisma.test/v1/environment-variables/${id}`,
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
class: "production" as const,
|
|
key: "PRISMA_INTERNAL_URL",
|
|
valueKid: "kid-system",
|
|
isManagedBySystem: true,
|
|
createdAt,
|
|
updatedAt,
|
|
};
|
|
}),
|
|
deleteEnvironmentVariable: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteEnvironmentVariable", id]);
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
const session = {
|
|
note: (message: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["note", message]);
|
|
}),
|
|
};
|
|
|
|
return Effect.gen(function* () {
|
|
const envProvider = yield* PrismaEnvironmentVariable.Provider;
|
|
yield* envProvider.delete!({
|
|
id: "EnvironmentVariable",
|
|
fqn: "EnvironmentVariable",
|
|
instanceId: "00000000000000000000000000000000",
|
|
olds: {} as never,
|
|
output: {
|
|
environmentVariableId: "env-system",
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
class: "production",
|
|
key: "PRISMA_INTERNAL_URL",
|
|
value: Redacted.make("secret"),
|
|
valueKid: "kid-user",
|
|
isManagedBySystem: false,
|
|
createdAt,
|
|
updatedAt,
|
|
},
|
|
session: session as never,
|
|
bindings: [],
|
|
});
|
|
|
|
expect(calls).toEqual([
|
|
["getEnvironmentVariable", "env-system"],
|
|
[
|
|
"note",
|
|
"Skipping direct delete for system-managed Prisma environment variable 'PRISMA_INTERNAL_URL'.",
|
|
],
|
|
]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"deletes an environment variable when stale state says system-managed",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const client = {
|
|
getEnvironmentVariable: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getEnvironmentVariable", id]);
|
|
return {
|
|
id,
|
|
type: "environment-variable" as const,
|
|
url: `https://api.prisma.test/v1/environment-variables/${id}`,
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
class: "production" as const,
|
|
key: "PRISMA_INTERNAL_URL",
|
|
valueKid: "kid-user",
|
|
isManagedBySystem: false,
|
|
createdAt,
|
|
updatedAt,
|
|
};
|
|
}),
|
|
deleteEnvironmentVariable: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteEnvironmentVariable", id]);
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const envProvider = yield* PrismaEnvironmentVariable.Provider;
|
|
yield* envProvider.delete!({
|
|
id: "EnvironmentVariable",
|
|
fqn: "EnvironmentVariable",
|
|
instanceId: "00000000000000000000000000000000",
|
|
olds: {} as never,
|
|
output: {
|
|
environmentVariableId: "env-1",
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
class: "production",
|
|
key: "PRISMA_INTERNAL_URL",
|
|
value: Redacted.make("secret"),
|
|
valueKid: "kid-system",
|
|
isManagedBySystem: true,
|
|
createdAt,
|
|
updatedAt,
|
|
},
|
|
session: undefined as never,
|
|
bindings: [],
|
|
});
|
|
|
|
expect(calls).toEqual([
|
|
["getEnvironmentVariable", "env-1"],
|
|
["deleteEnvironmentVariable", "env-1"],
|
|
]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"validates Prisma environment variable writes locally",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const client = {
|
|
listEnvironmentVariables: (query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listEnvironmentVariables", query]);
|
|
return [];
|
|
}),
|
|
createEnvironmentVariable: (input: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["createEnvironmentVariable", input]);
|
|
return {
|
|
id: "env-1",
|
|
type: "environment-variable" as const,
|
|
url: "https://api.prisma.test/v1/environment-variables/env-1",
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
valueKid: "kid-1",
|
|
isManagedBySystem: false,
|
|
createdAt,
|
|
updatedAt,
|
|
};
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const envProvider = yield* PrismaEnvironmentVariable.Provider;
|
|
const invalidKey = yield* envProvider
|
|
.reconcile(
|
|
reconcileInput("EnvironmentVariable", {
|
|
project: "project-1",
|
|
class: "production" as const,
|
|
key: "bad-key",
|
|
value: Redacted.make("secret"),
|
|
}),
|
|
)
|
|
.pipe(Effect.flip);
|
|
const emptyValue = yield* envProvider
|
|
.reconcile(
|
|
reconcileInput("EnvironmentVariable", {
|
|
project: "project-1",
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
value: Redacted.make(""),
|
|
}),
|
|
)
|
|
.pipe(Effect.flip);
|
|
|
|
expect(invalidKey).toBeInstanceOf(Error);
|
|
expect((invalidKey as Error).message).toContain(
|
|
"must match POSIX env-var key shape",
|
|
);
|
|
expect(emptyValue).toBeInstanceOf(Error);
|
|
expect((emptyValue as Error).message).toContain(
|
|
"value must be non-empty",
|
|
);
|
|
expect(calls).toEqual([]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"updates mutable Prisma resources from observed state",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const client = {
|
|
getDatabase: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getDatabase", id]);
|
|
return {
|
|
id,
|
|
type: "database" as const,
|
|
url: `https://api.prisma.test/v1/databases/${id}`,
|
|
name: "main",
|
|
status: "ready" as const,
|
|
createdAt,
|
|
isDefault: false,
|
|
defaultConnectionId: "connection-1",
|
|
connections: [databaseConnection(id, "connection-1")],
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
region: { id: "us-east-1", name: "US East" },
|
|
source: { type: "empty" },
|
|
branchId: null,
|
|
};
|
|
}),
|
|
updateDatabase: (id: string, input: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["updateDatabase", { id, input }]);
|
|
return {
|
|
id,
|
|
type: "database" as const,
|
|
url: `https://api.prisma.test/v1/databases/${id}`,
|
|
name: "primary",
|
|
status: "ready" as const,
|
|
createdAt,
|
|
isDefault: false,
|
|
defaultConnectionId: "connection-1",
|
|
connections: [databaseConnection(id, "connection-1")],
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
region: { id: "us-east-1", name: "US East" },
|
|
source: { type: "empty" },
|
|
branchId: "branch-1",
|
|
};
|
|
}),
|
|
getConnection: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getConnection", id]);
|
|
return {
|
|
id,
|
|
type: "connection" as const,
|
|
url: `https://api.prisma.test/v1/connections/${id}`,
|
|
name: "api-000000000000",
|
|
createdAt,
|
|
kind: "postgres" as const,
|
|
endpoints: {
|
|
direct: {
|
|
host: "db.prisma.test",
|
|
port: 5432,
|
|
connectionString: "postgres://old-direct",
|
|
},
|
|
},
|
|
database: resourceRef("databases", "database-1", "main"),
|
|
};
|
|
}),
|
|
rotateConnection: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["rotateConnection", id]);
|
|
return {
|
|
id,
|
|
type: "connection" as const,
|
|
url: `https://api.prisma.test/v1/connections/${id}`,
|
|
name: "api-000000000000",
|
|
createdAt,
|
|
kind: "postgres" as const,
|
|
endpoints: {
|
|
direct: {
|
|
host: "db.prisma.test",
|
|
port: 5432,
|
|
connectionString:
|
|
"postgres://app:new-password@db.prisma.test/database-1",
|
|
},
|
|
},
|
|
database: resourceRef("databases", "database-1", "main"),
|
|
};
|
|
}),
|
|
getBranch: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getBranch", id]);
|
|
return {
|
|
id,
|
|
type: "branch" as const,
|
|
url: `https://api.prisma.test/v1/branches/${id}`,
|
|
gitName: "main",
|
|
isDefault: false,
|
|
role: "preview" as const,
|
|
createdAt,
|
|
updatedAt,
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
};
|
|
}),
|
|
updateBranch: (id: string, input: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["updateBranch", { id, input }]);
|
|
return {
|
|
id,
|
|
type: "branch" as const,
|
|
url: `https://api.prisma.test/v1/branches/${id}`,
|
|
gitName: "main",
|
|
isDefault: true,
|
|
role: "preview" as const,
|
|
createdAt,
|
|
updatedAt: "2026-01-01T00:00:02Z",
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
};
|
|
}),
|
|
listBranches: (projectId: string, query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listBranches", { projectId, query }]);
|
|
return [
|
|
{
|
|
id: "branch-default",
|
|
type: "branch" as const,
|
|
url: "https://api.prisma.test/v1/branches/branch-default",
|
|
gitName: "production",
|
|
isDefault: true,
|
|
role: "production" as const,
|
|
createdAt,
|
|
updatedAt,
|
|
project: resourceRef("projects", projectId, "app"),
|
|
},
|
|
];
|
|
}),
|
|
getApp: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getApp", id]);
|
|
return {
|
|
id,
|
|
type: "app" as const,
|
|
url: `https://api.prisma.test/v1/services/${id}`,
|
|
name: "api",
|
|
region: { id: "us-east-1", name: "US East" },
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
latestDeploymentId: null,
|
|
appEndpointDomain: "service-1.prisma.build",
|
|
createdAt,
|
|
};
|
|
}),
|
|
updateApp: (id: string, input: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["updateApp", { id, input }]);
|
|
return {
|
|
id,
|
|
type: "app" as const,
|
|
url: `https://api.prisma.test/v1/services/${id}`,
|
|
name: "web",
|
|
region: { id: "us-east-1", name: "US East" },
|
|
projectId: "project-1",
|
|
branchId: "branch-1",
|
|
latestDeploymentId: null,
|
|
appEndpointDomain: "service-1.prisma.build",
|
|
createdAt,
|
|
};
|
|
}),
|
|
getEnvironmentVariable: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getEnvironmentVariable", id]);
|
|
return {
|
|
id,
|
|
type: "environment-variable" as const,
|
|
url: `https://api.prisma.test/v1/environment-variables/${id}`,
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
valueKid: "kid-old",
|
|
isManagedBySystem: false,
|
|
createdAt,
|
|
updatedAt,
|
|
};
|
|
}),
|
|
updateEnvironmentVariable: (id: string, input: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["updateEnvironmentVariable", { id, input }]);
|
|
return {
|
|
id,
|
|
type: "environment-variable" as const,
|
|
url: `https://api.prisma.test/v1/environment-variables/${id}`,
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
valueKid: "kid-new",
|
|
isManagedBySystem: false,
|
|
createdAt,
|
|
updatedAt: "2026-01-01T00:00:02Z",
|
|
};
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const databaseProvider = yield* PrismaDatabase.Provider;
|
|
const connectionProvider = yield* PrismaConnection.Provider;
|
|
const branchProvider = yield* PrismaBranch.Provider;
|
|
const serviceProvider = yield* PrismaApp.Provider;
|
|
const envProvider = yield* PrismaEnvironmentVariable.Provider;
|
|
|
|
const database = yield* databaseProvider.reconcile(
|
|
reconcileInput(
|
|
"Database",
|
|
{
|
|
project: "project-1",
|
|
name: "primary",
|
|
region: "us-east-1",
|
|
branchId: "branch-1",
|
|
},
|
|
{
|
|
databaseId: "database-1",
|
|
databaseName: "main",
|
|
projectId: "project-1",
|
|
status: "ready",
|
|
region: "us-east-1",
|
|
isDefault: false,
|
|
branchId: null,
|
|
defaultConnectionId: "connection-1",
|
|
createdAt,
|
|
directConnectionString: undefined,
|
|
pooledConnectionString: undefined,
|
|
accelerateConnectionString: undefined,
|
|
host: undefined,
|
|
user: undefined,
|
|
password: undefined,
|
|
},
|
|
),
|
|
);
|
|
const connection = yield* connectionProvider.reconcile(
|
|
reconcileInput(
|
|
"Connection",
|
|
{
|
|
database: "database-1",
|
|
name: "api",
|
|
rotate: true,
|
|
},
|
|
{
|
|
connectionId: "connection-1",
|
|
connectionName: "api-000000000000",
|
|
databaseId: "database-1",
|
|
kind: "postgres" as const,
|
|
createdAt,
|
|
directConnectionString: Redacted.make("postgres://old-direct"),
|
|
pooledConnectionString: undefined,
|
|
accelerateConnectionString: undefined,
|
|
host: "db.prisma.test",
|
|
user: undefined,
|
|
password: undefined,
|
|
databaseUrl: Redacted.make("postgres://old-direct"),
|
|
origin: undefined,
|
|
pooledOrigin: undefined,
|
|
},
|
|
{
|
|
database: "database-1",
|
|
name: "api",
|
|
rotate: false,
|
|
},
|
|
),
|
|
);
|
|
const branch = yield* branchProvider.reconcile(
|
|
reconcileInput(
|
|
"Branch",
|
|
{
|
|
project: "project-1",
|
|
gitName: "main",
|
|
isDefault: true,
|
|
},
|
|
{
|
|
branchId: "branch-1",
|
|
gitName: "main",
|
|
projectId: "project-1",
|
|
isDefault: false,
|
|
role: "preview",
|
|
createdAt,
|
|
updatedAt,
|
|
},
|
|
),
|
|
);
|
|
const service = yield* serviceProvider.reconcile(
|
|
reconcileInput(
|
|
"App",
|
|
{
|
|
project: "project-1",
|
|
displayName: "web",
|
|
regionId: "us-east-1",
|
|
branchId: "branch-1",
|
|
},
|
|
{
|
|
appId: "service-1",
|
|
name: "api",
|
|
projectId: "project-1",
|
|
regionId: "us-east-1",
|
|
branchId: null,
|
|
latestDeploymentId: null,
|
|
appEndpointDomain: "service-1.prisma.build",
|
|
createdAt,
|
|
},
|
|
),
|
|
);
|
|
const env = yield* envProvider.reconcile(
|
|
reconcileInput(
|
|
"EnvironmentVariable",
|
|
{
|
|
project: "project-1",
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
value: Redacted.make("new-secret"),
|
|
},
|
|
{
|
|
environmentVariableId: "env-1",
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
value: Redacted.make("old-secret"),
|
|
valueKid: "kid-old",
|
|
isManagedBySystem: false,
|
|
createdAt,
|
|
updatedAt,
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(database.databaseName).toBe("primary");
|
|
expect(database.branchId).toBe("branch-1");
|
|
expect(Redacted.value(connection.directConnectionString!)).toBe(
|
|
"postgres://app:new-password@db.prisma.test/database-1",
|
|
);
|
|
expect(connection.user).toBe("app");
|
|
expect(Redacted.value(connection.password!)).toBe("new-password");
|
|
expectJsonNotToContain(
|
|
connection,
|
|
"postgres://app:new-password@db.prisma.test/database-1",
|
|
"new-password",
|
|
);
|
|
expect(branch.isDefault).toBe(true);
|
|
expect(service.name).toBe("web");
|
|
expect(service.branchId).toBe("branch-1");
|
|
expect(env.valueKid).toBe("kid-new");
|
|
expect(Redacted.value(env.value)).toBe("new-secret");
|
|
expectJsonNotToContain(env, "new-secret");
|
|
expect(calls).toEqual([
|
|
["getDatabase", "database-1"],
|
|
[
|
|
"updateDatabase",
|
|
{
|
|
id: "database-1",
|
|
// JSON transport drops `undefined` members.
|
|
input: { name: "primary", branchId: "branch-1" },
|
|
},
|
|
],
|
|
["getConnection", "connection-1"],
|
|
["rotateConnection", "connection-1"],
|
|
["getBranch", "branch-1"],
|
|
["listBranches", { projectId: "project-1", query: {} }],
|
|
["updateBranch", { id: "branch-1", input: { isDefault: true } }],
|
|
["getApp", "service-1"],
|
|
[
|
|
"updateApp",
|
|
{
|
|
id: "service-1",
|
|
input: {
|
|
displayName: "web",
|
|
branchId: "branch-1",
|
|
branchGitName: undefined,
|
|
},
|
|
},
|
|
],
|
|
["getEnvironmentVariable", "env-1"],
|
|
[
|
|
"updateEnvironmentVariable",
|
|
{ id: "env-1", input: { value: "new-secret" } },
|
|
],
|
|
]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
Effect.provide(FetchHttpClient.layer),
|
|
Effect.provideService(Stack, {
|
|
name: "prisma-provider-update-test",
|
|
stage: "test",
|
|
resources: {},
|
|
bindings: {},
|
|
actions: {},
|
|
}),
|
|
Effect.provideService(Stage, "test"),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"skips branchGitName updates when observed branch ids already match",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const branch = {
|
|
id: "branch-main",
|
|
type: "branch" as const,
|
|
url: "https://api.prisma.test/v1/branches/branch-main",
|
|
gitName: "main",
|
|
isDefault: true,
|
|
createdAt,
|
|
updatedAt,
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
};
|
|
const client = {
|
|
getDatabase: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getDatabase", id]);
|
|
return {
|
|
id,
|
|
type: "database" as const,
|
|
url: `https://api.prisma.test/v1/databases/${id}`,
|
|
name: "main",
|
|
status: "ready" as const,
|
|
createdAt,
|
|
isDefault: false,
|
|
defaultConnectionId: "connection-1",
|
|
connections: [databaseConnection(id, "connection-1")],
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
region: { id: "us-east-1", name: "US East" },
|
|
source: { type: "empty" },
|
|
branchId: "branch-main",
|
|
};
|
|
}),
|
|
getApp: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getApp", id]);
|
|
return {
|
|
id,
|
|
type: "app" as const,
|
|
url: `https://api.prisma.test/v1/services/${id}`,
|
|
name: "api",
|
|
region: { id: "us-east-1", name: "US East" },
|
|
projectId: "project-1",
|
|
branchId: "branch-main",
|
|
latestDeploymentId: null,
|
|
appEndpointDomain: "service-1.prisma.build",
|
|
createdAt,
|
|
};
|
|
}),
|
|
listBranches: (projectId: string, query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listBranches", { projectId, query }]);
|
|
return [branch];
|
|
}),
|
|
updateDatabase: (id: string, input: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["updateDatabase", { id, input }]);
|
|
throw new Error("updateDatabase should not be called");
|
|
}),
|
|
updateApp: (id: string, input: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["updateApp", { id, input }]);
|
|
throw new Error("updateApp should not be called");
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const databaseProvider = yield* PrismaDatabase.Provider;
|
|
const serviceProvider = yield* PrismaApp.Provider;
|
|
|
|
const database = yield* databaseProvider.reconcile(
|
|
reconcileInput(
|
|
"Database",
|
|
{
|
|
project: "project-1",
|
|
name: "main",
|
|
region: "us-east-1",
|
|
branchGitName: "main",
|
|
},
|
|
{
|
|
databaseId: "database-1",
|
|
databaseName: "main",
|
|
projectId: "project-1",
|
|
status: "ready",
|
|
region: "us-east-1",
|
|
isDefault: false,
|
|
branchId: "branch-main",
|
|
defaultConnectionId: "connection-1",
|
|
createdAt,
|
|
directConnectionString: undefined,
|
|
pooledConnectionString: undefined,
|
|
accelerateConnectionString: undefined,
|
|
host: undefined,
|
|
user: undefined,
|
|
password: undefined,
|
|
},
|
|
),
|
|
);
|
|
const service = yield* serviceProvider.reconcile(
|
|
reconcileInput(
|
|
"App",
|
|
{
|
|
project: "project-1",
|
|
displayName: "api",
|
|
regionId: "us-east-1",
|
|
branchGitName: "main",
|
|
},
|
|
{
|
|
appId: "service-1",
|
|
name: "api",
|
|
projectId: "project-1",
|
|
regionId: "us-east-1",
|
|
branchId: "branch-main",
|
|
latestDeploymentId: null,
|
|
appEndpointDomain: "service-1.prisma.build",
|
|
createdAt,
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(database.branchId).toBe("branch-main");
|
|
expect(service.branchId).toBe("branch-main");
|
|
expect(calls).toEqual([
|
|
["getDatabase", "database-1"],
|
|
[
|
|
"listBranches",
|
|
// Query parameters arrive as strings over the wire.
|
|
{
|
|
projectId: "project-1",
|
|
query: { limit: "2", gitName: "main" },
|
|
},
|
|
],
|
|
[
|
|
"listBranches",
|
|
{
|
|
projectId: "project-1",
|
|
query: { limit: "100", gitName: "main" },
|
|
},
|
|
],
|
|
["getApp", "service-1"],
|
|
[
|
|
"listBranches",
|
|
{
|
|
projectId: "project-1",
|
|
query: { limit: "100", gitName: "main" },
|
|
},
|
|
],
|
|
]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
Effect.provide(FetchHttpClient.layer),
|
|
Effect.provideService(Stack, {
|
|
name: "prisma-branch-noop-test",
|
|
stage: "test",
|
|
resources: {},
|
|
bindings: {},
|
|
actions: {},
|
|
}),
|
|
Effect.provideService(Stage, "test"),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"normalizes Management API clone source ids before comparing immutable state",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const database = {
|
|
id: "database-clone",
|
|
type: "database" as const,
|
|
url: "https://api.prisma.test/v1/databases/database-clone",
|
|
name: "clone",
|
|
status: "ready" as const,
|
|
createdAt,
|
|
isDefault: false,
|
|
defaultConnectionId: "connection-clone",
|
|
connections: [
|
|
databaseConnection("database-clone", "connection-clone"),
|
|
],
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
region: { id: "us-east-1", name: "US East" },
|
|
source: { type: "database" as const, databaseId: "source" },
|
|
branchId: null,
|
|
};
|
|
const client = {
|
|
getDatabase: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getDatabase", id]);
|
|
return database;
|
|
}),
|
|
updateDatabase: () =>
|
|
Effect.die("normalized clone source must not trigger an update"),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const provider = yield* PrismaDatabase.Provider;
|
|
const result = yield* provider.reconcile(
|
|
reconcileInput(
|
|
"Database",
|
|
{
|
|
project: "project-1",
|
|
name: "clone",
|
|
region: "us-east-1",
|
|
source: { type: "database" as const, databaseId: "db_source" },
|
|
},
|
|
{
|
|
databaseId: "database-clone",
|
|
databaseName: "clone",
|
|
projectId: "project-1",
|
|
status: "ready" as const,
|
|
region: "us-east-1",
|
|
isDefault: false,
|
|
branchId: null,
|
|
defaultConnectionId: "connection-clone",
|
|
createdAt,
|
|
directConnectionString: undefined,
|
|
pooledConnectionString: undefined,
|
|
accelerateConnectionString: undefined,
|
|
host: undefined,
|
|
user: undefined,
|
|
password: undefined,
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(result.databaseId).toBe("database-clone");
|
|
expect(calls).toEqual([["getDatabase", "database-clone"]]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"refuses convergence when the observed source shape is unrecognized",
|
|
() => {
|
|
const database = {
|
|
id: "database-exotic",
|
|
type: "database" as const,
|
|
url: "https://api.prisma.test/v1/databases/database-exotic",
|
|
name: "exotic",
|
|
status: "ready" as const,
|
|
createdAt,
|
|
isDefault: false,
|
|
defaultConnectionId: "connection-exotic",
|
|
connections: [
|
|
databaseConnection("database-exotic", "connection-exotic"),
|
|
],
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
region: { id: "us-east-1", name: "US East" },
|
|
source: { type: "replica", replicaOf: "db_source" },
|
|
branchId: null,
|
|
};
|
|
const client = {
|
|
getDatabase: () => Effect.succeed(database),
|
|
updateDatabase: () =>
|
|
Effect.die("an unrecognized source must refuse before updating"),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const provider = yield* PrismaDatabase.Provider;
|
|
const error = yield* provider
|
|
.reconcile(
|
|
reconcileInput(
|
|
"Database",
|
|
{
|
|
project: "project-1",
|
|
name: "exotic",
|
|
region: "us-east-1",
|
|
},
|
|
{
|
|
databaseId: "database-exotic",
|
|
databaseName: "exotic",
|
|
projectId: "project-1",
|
|
status: "ready" as const,
|
|
region: "us-east-1",
|
|
isDefault: false,
|
|
branchId: null,
|
|
defaultConnectionId: "connection-exotic",
|
|
createdAt,
|
|
directConnectionString: undefined,
|
|
pooledConnectionString: undefined,
|
|
accelerateConnectionString: undefined,
|
|
host: undefined,
|
|
user: undefined,
|
|
password: undefined,
|
|
},
|
|
),
|
|
)
|
|
.pipe(Effect.flip);
|
|
|
|
expect((error as Error).message).toContain("immutable source");
|
|
expect((error as Error).message).toContain('"replica"');
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"leaves the observed branch alone when branch props are omitted",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const database = {
|
|
id: "database-1",
|
|
type: "database" as const,
|
|
url: "https://api.prisma.test/v1/databases/database-1",
|
|
name: "main",
|
|
status: "ready" as const,
|
|
createdAt,
|
|
isDefault: false,
|
|
defaultConnectionId: "connection-1",
|
|
connections: [],
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
region: { id: "us-east-1", name: "US East" },
|
|
source: { type: "empty" as const },
|
|
branchId: "branch-1",
|
|
};
|
|
const client = {
|
|
getDatabase: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getDatabase", id]);
|
|
return database;
|
|
}),
|
|
updateDatabase: () =>
|
|
Effect.die(
|
|
"every database belongs to a Branch; omitted branch props must not detach",
|
|
),
|
|
rotateConnection: () =>
|
|
Effect.die(
|
|
"persisted credentials must prevent an unrelated rotation",
|
|
),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const provider = yield* PrismaDatabase.Provider;
|
|
const result = yield* provider.reconcile(
|
|
reconcileInput(
|
|
"Database",
|
|
{
|
|
project: "project-1",
|
|
name: "main",
|
|
region: "us-east-1",
|
|
},
|
|
{
|
|
databaseId: "database-1",
|
|
databaseName: "main",
|
|
projectId: "project-1",
|
|
status: "ready" as const,
|
|
region: "us-east-1",
|
|
isDefault: false,
|
|
branchId: "branch-1",
|
|
defaultConnectionId: "connection-1",
|
|
createdAt,
|
|
directConnectionString: Redacted.make("postgres://persisted"),
|
|
pooledConnectionString: undefined,
|
|
accelerateConnectionString: undefined,
|
|
host: "db.prisma.test",
|
|
user: "user",
|
|
password: undefined,
|
|
},
|
|
{
|
|
project: "project-1",
|
|
name: "main",
|
|
region: "us-east-1",
|
|
branchId: "branch-1",
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(result.branchId).toBe("branch-1");
|
|
expect(calls).toEqual([["getDatabase", "database-1"]]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"forces Project and Database reconcile when adoption rotation is enabled",
|
|
() => {
|
|
const { client } = makeClient();
|
|
|
|
return Effect.gen(function* () {
|
|
const projectProvider = yield* PrismaProject.Provider;
|
|
const databaseProvider = yield* PrismaDatabase.Provider;
|
|
|
|
expect(
|
|
yield* projectProvider.diff!(
|
|
diffInput(
|
|
{
|
|
name: "app",
|
|
createDatabase: false,
|
|
rotateCredentialsOnAdopt: false,
|
|
},
|
|
{
|
|
name: "app",
|
|
createDatabase: false,
|
|
rotateCredentialsOnAdopt: true,
|
|
},
|
|
),
|
|
),
|
|
).toEqual({ action: "update" });
|
|
expect(
|
|
yield* databaseProvider.diff!(
|
|
diffInput(
|
|
{
|
|
project: "project-1",
|
|
name: "main",
|
|
region: "us-east-1",
|
|
rotateCredentialsOnAdopt: false,
|
|
},
|
|
{
|
|
project: "project-1",
|
|
name: "main",
|
|
region: "us-east-1",
|
|
rotateCredentialsOnAdopt: true,
|
|
},
|
|
),
|
|
),
|
|
).toEqual({ action: "update" });
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"rejects non-atomic explicit-name database branch creation",
|
|
() => {
|
|
let created = false;
|
|
const client = {
|
|
createDatabase: () =>
|
|
Effect.sync(() => {
|
|
created = true;
|
|
throw new Error("must fail before create");
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const provider = yield* PrismaDatabase.Provider;
|
|
const error = yield* provider
|
|
.reconcile(
|
|
reconcileInput("Database", {
|
|
project: "project-1",
|
|
name: "main",
|
|
region: "us-east-1",
|
|
branchGitName: "main",
|
|
}),
|
|
)
|
|
.pipe(Effect.flip);
|
|
|
|
expect((error as Error).message).toContain(
|
|
"cannot be distinguished from a foreign database",
|
|
);
|
|
expect(created).toBe(false);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"recovers credentials after a generated database create conflict",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
let attemptedName: string | undefined;
|
|
const recoveredDatabase = () => ({
|
|
id: "database-recovered",
|
|
type: "database" as const,
|
|
url: "https://api.prisma.test/v1/databases/database-recovered",
|
|
name: attemptedName!,
|
|
status: "ready" as const,
|
|
createdAt,
|
|
isDefault: false,
|
|
defaultConnectionId: "connection-recovered",
|
|
connections: [],
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
region: { id: "us-east-1", name: "US East" },
|
|
source: { type: "empty" as const },
|
|
branchId: "branch-main",
|
|
});
|
|
const client = {
|
|
listProjectDatabases: (projectId: string, query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listProjectDatabases", { projectId, query }]);
|
|
return attemptedName === undefined ? [] : [recoveredDatabase()];
|
|
}),
|
|
createDatabase: (input: { name: string }) =>
|
|
Effect.gen(function* () {
|
|
calls.push(["createDatabase", input]);
|
|
attemptedName = input.name;
|
|
return yield* Effect.fail(
|
|
new PrismaApiError({
|
|
method: "POST",
|
|
path: "/v1/databases",
|
|
status: 409,
|
|
message: "already exists",
|
|
}),
|
|
);
|
|
}),
|
|
listBranches: () =>
|
|
Effect.succeed([
|
|
{
|
|
id: "branch-main",
|
|
gitName: "main",
|
|
},
|
|
]),
|
|
rotateConnection: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["rotateConnection", id]);
|
|
return databaseConnection("database-recovered", id);
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const provider = yield* PrismaDatabase.Provider;
|
|
const recovered = yield* provider.reconcile(
|
|
reconcileInput("Database", {
|
|
project: "project-1",
|
|
region: "us-east-1",
|
|
branchGitName: "main",
|
|
}),
|
|
);
|
|
|
|
expect(attemptedName).toContain("-Database-test-");
|
|
expect(redactedValue(recovered.directConnectionString)).toContain(
|
|
"database-recovered",
|
|
);
|
|
expect(calls).toContainEqual([
|
|
"rotateConnection",
|
|
"connection-recovered",
|
|
]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
Effect.provideService(Stack, {
|
|
name: "prisma-database-recovery-test",
|
|
stage: "test",
|
|
resources: {},
|
|
bindings: {},
|
|
actions: {},
|
|
}),
|
|
Effect.provideService(Stage, "test"),
|
|
Effect.provideService(InstanceId, "00000000000000000000000000000000"),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"recovers default credentials after a generated project create conflict",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
let attemptedName: string | undefined;
|
|
const database = wireDatabase({
|
|
id: "database-default",
|
|
name: "default",
|
|
isDefault: true,
|
|
defaultConnectionId: "connection-default",
|
|
projectId: "project-recovered",
|
|
projectName: "recovered",
|
|
});
|
|
// The 409 below is served as a real wire response; distilled's status
|
|
// matcher is what turns it into the `Conflict` tag the provider catches.
|
|
const fake = makeFakeManagementApi((request) => {
|
|
if (request.pathname === "/v1/projects" && request.method === "GET") {
|
|
return page(
|
|
attemptedName === undefined
|
|
? []
|
|
: [
|
|
wireProject({
|
|
id: "project-recovered",
|
|
name: attemptedName,
|
|
}),
|
|
],
|
|
);
|
|
}
|
|
if (
|
|
request.pathname === "/v1/projects" &&
|
|
request.method === "POST"
|
|
) {
|
|
attemptedName = (request.bodyJson as { name: string }).name;
|
|
return conflict("already exists");
|
|
}
|
|
if (
|
|
request.pathname === "/v1/projects/project-recovered/databases" &&
|
|
request.method === "GET"
|
|
) {
|
|
return page([database]);
|
|
}
|
|
return dispatchManagement(client, request);
|
|
});
|
|
const client = {
|
|
rotateConnection: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["rotateConnection", id]);
|
|
return databaseConnection("database-default", id);
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const provider = yield* PrismaProject.Provider;
|
|
const recovered = yield* provider.reconcile(
|
|
reconcileInput("Project", {
|
|
createDatabase: true,
|
|
region: "us-east-1",
|
|
}),
|
|
);
|
|
|
|
expect(attemptedName).toContain("-Project-test-");
|
|
expect(redactedValue(recovered.directConnectionString)).toContain(
|
|
"database-default",
|
|
);
|
|
expect(calls).toContainEqual([
|
|
"rotateConnection",
|
|
"connection-default",
|
|
]);
|
|
// The create really was rejected with 409 over the wire and recovery
|
|
// ran off the follow-up list.
|
|
expect(routesOf(fake.captured)).toEqual([
|
|
"GET /v1/projects",
|
|
"POST /v1/projects",
|
|
"GET /v1/projects",
|
|
"GET /v1/projects/project-recovered/databases",
|
|
"POST /v1/connections/connection-default/rotate",
|
|
]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(fake.layer),
|
|
Effect.provideService(Stack, {
|
|
name: "prisma-project-recovery-test",
|
|
stage: "test",
|
|
resources: {},
|
|
bindings: {},
|
|
actions: {},
|
|
}),
|
|
Effect.provideService(Stage, "test"),
|
|
Effect.provideService(InstanceId, "00000000000000000000000000000000"),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"rejects a createDatabase false response that contains a default database",
|
|
() => {
|
|
const fake = makeFakeManagementApi((request) => {
|
|
if (
|
|
request.pathname === "/v1/projects" &&
|
|
request.method === "POST"
|
|
) {
|
|
return envelope(
|
|
{
|
|
...wireProject(),
|
|
database: wireCreatedDatabase({
|
|
id: "database-unexpected",
|
|
name: "default",
|
|
isDefault: true,
|
|
}),
|
|
},
|
|
{ status: 201 },
|
|
);
|
|
}
|
|
return unhandled(request);
|
|
});
|
|
|
|
return Effect.gen(function* () {
|
|
const provider = yield* PrismaProject.Provider;
|
|
const error = yield* provider
|
|
.reconcile(
|
|
reconcileInput("Project", {
|
|
name: "app",
|
|
createDatabase: false,
|
|
region: "us-east-1",
|
|
}),
|
|
)
|
|
.pipe(Effect.flip);
|
|
|
|
expect((error as Error).message).toContain(
|
|
"created unexpected default database",
|
|
);
|
|
expect(routesOf(fake.captured)).toEqual(["POST /v1/projects"]);
|
|
expect(fake.captured[0]?.bodyJson).toEqual({
|
|
name: "app",
|
|
createDatabase: false,
|
|
region: "us-east-1",
|
|
});
|
|
}).pipe(
|
|
Effect.provide(providerLayer({} as PrismaManagementClient)),
|
|
Effect.provide(fake.layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"ensures a default database on an existing Prisma project",
|
|
() => {
|
|
let createdDefault: unknown;
|
|
const defaultDatabase = () =>
|
|
wireDatabase({
|
|
isDefault: true,
|
|
defaultConnectionId: "connection-1",
|
|
connections: [
|
|
wireConnection({ directConnectionString: "postgres://direct" }),
|
|
],
|
|
});
|
|
const fake = makeFakeManagementApi((request) => {
|
|
if (request.pathname === "/v1/projects" && request.method === "GET") {
|
|
return page([wireProject()]);
|
|
}
|
|
if (
|
|
request.pathname === "/v1/projects/project-1" &&
|
|
request.method === "GET"
|
|
) {
|
|
return envelope(wireProject());
|
|
}
|
|
if (
|
|
request.pathname === "/v1/projects/project-1/databases" &&
|
|
request.method === "GET"
|
|
) {
|
|
return page([
|
|
wireDatabase({ id: "database-reporting", name: "reporting" }),
|
|
...(createdDefault === undefined ? [] : [createdDefault]),
|
|
]);
|
|
}
|
|
if (
|
|
request.pathname === "/v1/projects/project-1/databases" &&
|
|
request.method === "POST"
|
|
) {
|
|
createdDefault = defaultDatabase();
|
|
return envelope(
|
|
wireCreatedDatabase({
|
|
isDefault: true,
|
|
defaultConnectionId: "connection-1",
|
|
connections: [
|
|
wireConnection({
|
|
directConnectionString: "postgres://direct",
|
|
}),
|
|
],
|
|
}),
|
|
{ status: 201 },
|
|
);
|
|
}
|
|
return unhandled(request);
|
|
});
|
|
|
|
return Effect.gen(function* () {
|
|
const projectProvider = yield* Provider.findProvider(PrismaProject);
|
|
const observed = yield* projectProvider.read!(
|
|
readInput("Project", { name: "app", region: "us-east-1" }),
|
|
);
|
|
expect(Unowned.is(observed!)).toBe(true);
|
|
const project = yield* projectProvider.reconcile(
|
|
reconcileInput(
|
|
"Project",
|
|
{ name: "app", region: "us-east-1" },
|
|
observed,
|
|
),
|
|
);
|
|
|
|
expect(project.projectId).toBe("project-1");
|
|
expect(project.databaseId).toBe("database-1");
|
|
expect(Redacted.value(project.directConnectionString!)).toBe(
|
|
"postgres://direct",
|
|
);
|
|
expectJsonNotToContain(project, "postgres://direct");
|
|
expect(routesOf(fake.captured)).toEqual([
|
|
"GET /v1/projects",
|
|
"GET /v1/projects/project-1/databases",
|
|
"GET /v1/projects/project-1",
|
|
"GET /v1/projects/project-1/databases",
|
|
"POST /v1/projects/project-1/databases",
|
|
"GET /v1/projects/project-1",
|
|
"GET /v1/projects/project-1/databases",
|
|
]);
|
|
expect(
|
|
fake.captured.find((request) => request.method === "POST")
|
|
?.bodyJson,
|
|
).toEqual({ region: "us-east-1", isDefault: true });
|
|
}).pipe(
|
|
Effect.provide(
|
|
ProjectProvider().pipe(
|
|
Layer.provide(
|
|
Layer.succeed(PrismaClient, {} as PrismaManagementClient),
|
|
),
|
|
Layer.provide(liveProviderContext),
|
|
),
|
|
),
|
|
Effect.provide(fake.layer),
|
|
Effect.provide(FetchHttpClient.layer),
|
|
Effect.provideService(Stack, {
|
|
name: "prisma-project-ensure-test",
|
|
stage: "test",
|
|
resources: {},
|
|
bindings: {},
|
|
actions: {},
|
|
}),
|
|
Effect.provideService(Stage, "test"),
|
|
);
|
|
},
|
|
{ tags: ["provider:prisma:project"] },
|
|
);
|
|
|
|
it.effect(
|
|
"clears previously managed project settings",
|
|
() => {
|
|
const fake = makeFakeManagementApi((request) => {
|
|
if (request.pathname === "/v1/projects/project-1") {
|
|
if (request.method === "GET" || request.method === "PATCH") {
|
|
return envelope(wireProject());
|
|
}
|
|
}
|
|
if (
|
|
request.pathname === "/v1/projects/project-1/databases" &&
|
|
request.method === "GET"
|
|
) {
|
|
return page([]);
|
|
}
|
|
return unhandled(request);
|
|
});
|
|
|
|
return Effect.gen(function* () {
|
|
const projectProvider = yield* PrismaProject.Provider;
|
|
const project = yield* projectProvider.reconcile(
|
|
reconcileInput(
|
|
"Project",
|
|
{ name: "app", createDatabase: false },
|
|
{
|
|
projectId: "project-1",
|
|
projectName: "app",
|
|
workspaceId: "workspace-1",
|
|
createdAt,
|
|
defaultRegion: "us-east-1",
|
|
databaseId: undefined,
|
|
defaultConnectionId: undefined,
|
|
directConnectionString: undefined,
|
|
pooledConnectionString: undefined,
|
|
accelerateConnectionString: undefined,
|
|
host: undefined,
|
|
user: undefined,
|
|
password: undefined,
|
|
},
|
|
{
|
|
name: "app",
|
|
createDatabase: false,
|
|
settings: { preview: true },
|
|
},
|
|
),
|
|
);
|
|
|
|
expect(project.projectId).toBe("project-1");
|
|
expect(routesOf(fake.captured)).toEqual([
|
|
"GET /v1/projects/project-1",
|
|
"GET /v1/projects/project-1/databases",
|
|
"PATCH /v1/projects/project-1",
|
|
]);
|
|
expect(fake.captured.at(-1)?.bodyJson).toEqual({
|
|
name: "app",
|
|
settings: {},
|
|
});
|
|
}).pipe(
|
|
Effect.provide(
|
|
ProjectProvider().pipe(
|
|
Layer.provide(
|
|
Layer.succeed(PrismaClient, {} as PrismaManagementClient),
|
|
),
|
|
Layer.provide(liveProviderContext),
|
|
),
|
|
),
|
|
Effect.provide(fake.layer),
|
|
Effect.provide(FetchHttpClient.layer),
|
|
Effect.provideService(Stack, {
|
|
name: "prisma-project-settings-test",
|
|
stage: "test",
|
|
resources: {},
|
|
bindings: {},
|
|
actions: {},
|
|
}),
|
|
Effect.provideService(Stage, "test"),
|
|
);
|
|
},
|
|
{ tags: ["provider:prisma:project"] },
|
|
);
|
|
|
|
it.effect(
|
|
"renames a project without clearing unmanaged settings",
|
|
() => {
|
|
const fake = makeFakeManagementApi((request) => {
|
|
if (
|
|
request.pathname === "/v1/projects/project-1" &&
|
|
request.method === "GET"
|
|
) {
|
|
return envelope(wireProject());
|
|
}
|
|
if (
|
|
request.pathname === "/v1/projects/project-1" &&
|
|
request.method === "PATCH"
|
|
) {
|
|
return envelope(wireProject({ name: "renamed" }));
|
|
}
|
|
if (
|
|
request.pathname === "/v1/projects/project-1/databases" &&
|
|
request.method === "GET"
|
|
) {
|
|
return page([]);
|
|
}
|
|
return unhandled(request);
|
|
});
|
|
|
|
return Effect.gen(function* () {
|
|
const projectProvider = yield* PrismaProject.Provider;
|
|
yield* projectProvider.reconcile(
|
|
reconcileInput(
|
|
"Project",
|
|
{ name: "renamed", createDatabase: false },
|
|
{
|
|
projectId: "project-1",
|
|
projectName: "app",
|
|
workspaceId: "workspace-1",
|
|
createdAt,
|
|
defaultRegion: "us-east-1",
|
|
databaseId: undefined,
|
|
defaultConnectionId: undefined,
|
|
directConnectionString: undefined,
|
|
pooledConnectionString: undefined,
|
|
accelerateConnectionString: undefined,
|
|
host: undefined,
|
|
user: undefined,
|
|
password: undefined,
|
|
},
|
|
{ name: "app", createDatabase: false },
|
|
),
|
|
);
|
|
|
|
expect(routesOf(fake.captured)).toEqual([
|
|
"GET /v1/projects/project-1",
|
|
"GET /v1/projects/project-1/databases",
|
|
"PATCH /v1/projects/project-1",
|
|
]);
|
|
expect(fake.captured.at(-1)?.bodyJson).toEqual({ name: "renamed" });
|
|
}).pipe(
|
|
Effect.provide(
|
|
ProjectProvider().pipe(
|
|
Layer.provide(
|
|
Layer.succeed(PrismaClient, {} as PrismaManagementClient),
|
|
),
|
|
Layer.provide(liveProviderContext),
|
|
),
|
|
),
|
|
Effect.provide(fake.layer),
|
|
Effect.provide(FetchHttpClient.layer),
|
|
Effect.provideService(Stack, {
|
|
name: "prisma-project-rename-test",
|
|
stage: "test",
|
|
resources: {},
|
|
bindings: {},
|
|
actions: {},
|
|
}),
|
|
Effect.provideService(Stage, "test"),
|
|
);
|
|
},
|
|
{ tags: ["provider:prisma:project"] },
|
|
);
|
|
|
|
it.effect(
|
|
"deletes Prisma resources through their management APIs",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const status = new Map([["version-1", "running"]]);
|
|
const client = {
|
|
deleteProject: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteProject", id]);
|
|
}),
|
|
deleteDatabase: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteDatabase", id]);
|
|
}),
|
|
getDatabase: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getDatabase", id]);
|
|
return { id, isDefault: false };
|
|
}),
|
|
deleteConnection: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteConnection", id]);
|
|
}),
|
|
getConnection: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getConnection", id]);
|
|
return {
|
|
id,
|
|
name: "api-000000000000",
|
|
database: { id: "database-1" },
|
|
};
|
|
}),
|
|
deleteBranch: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteBranch", id]);
|
|
}),
|
|
getBranch: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getBranch", id]);
|
|
return {
|
|
id,
|
|
gitName: "preview",
|
|
isDefault: false,
|
|
role: "preview" as const,
|
|
project: { id: "project-1" },
|
|
};
|
|
}),
|
|
listApps: (query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listApps", query]);
|
|
return [];
|
|
}),
|
|
listAppDeployments: (appId: string, query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listAppDeployments", { appId, query }]);
|
|
return [
|
|
{
|
|
id: "version-1",
|
|
type: "deployment" as const,
|
|
serviceId: "service-1",
|
|
url: "https://api.prisma.test/v1/deployments/version-1",
|
|
foundryVersionId: "foundry-1",
|
|
createdAt,
|
|
},
|
|
];
|
|
}),
|
|
getDeployment: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getDeployment", id]);
|
|
return {
|
|
id,
|
|
type: "deployment" as const,
|
|
serviceId: "service-1",
|
|
url: `https://api.prisma.test/v1/deployments/${id}`,
|
|
foundryVersionId: "foundry-1",
|
|
status: status.get(id) ?? "stopped",
|
|
previewDomain: null,
|
|
createdAt,
|
|
};
|
|
}),
|
|
stopDeployment: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["stopDeployment", id]);
|
|
status.set(id, "stopped");
|
|
}),
|
|
deleteDeployment: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteDeployment", id]);
|
|
}),
|
|
deleteApp: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteApp", id]);
|
|
}),
|
|
getApp: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getApp", id]);
|
|
return {
|
|
id,
|
|
name: "api",
|
|
projectId: "project-1",
|
|
region: { id: "us-east-1" },
|
|
branchId: "branch-1",
|
|
};
|
|
}),
|
|
deleteCustomDomain: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteCustomDomain", id]);
|
|
}),
|
|
getCustomDomain: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getCustomDomain", id]);
|
|
return { id, appId: "service-1", hostname: "api.example.com" };
|
|
}),
|
|
getEnvironmentVariable: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getEnvironmentVariable", id]);
|
|
return {
|
|
id,
|
|
type: "environment-variable" as const,
|
|
url: `https://api.prisma.test/v1/environment-variables/${id}`,
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
valueKid: "kid-1",
|
|
isManagedBySystem: false,
|
|
createdAt,
|
|
updatedAt,
|
|
};
|
|
}),
|
|
deleteEnvironmentVariable: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteEnvironmentVariable", id]);
|
|
}),
|
|
deleteSourceRepository: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteSourceRepository", id]);
|
|
}),
|
|
getSourceRepository: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getSourceRepository", id]);
|
|
return {
|
|
id,
|
|
projectId: "project-1",
|
|
repoId: 123,
|
|
provider: "github" as const,
|
|
installationId: "installation-1",
|
|
};
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const projectProvider = yield* PrismaProject.Provider;
|
|
const databaseProvider = yield* PrismaDatabase.Provider;
|
|
const connectionProvider = yield* PrismaConnection.Provider;
|
|
const branchProvider = yield* PrismaBranch.Provider;
|
|
const serviceProvider = yield* PrismaApp.Provider;
|
|
const versionProvider = yield* PrismaDeployment.Provider;
|
|
const domainProvider = yield* PrismaCustomDomain.Provider;
|
|
const envProvider = yield* PrismaEnvironmentVariable.Provider;
|
|
const repoProvider = yield* PrismaSourceRepository.Provider;
|
|
|
|
yield* versionProvider.delete(
|
|
deleteInput("Deployment", {
|
|
deploymentId: "version-1",
|
|
appId: "service-1",
|
|
}),
|
|
);
|
|
yield* serviceProvider.delete(
|
|
deleteInput("App", {
|
|
appId: "service-1",
|
|
projectId: "project-1",
|
|
regionId: "us-east-1",
|
|
name: "api",
|
|
branchId: "branch-1",
|
|
}),
|
|
);
|
|
yield* repoProvider.delete(
|
|
deleteInput("SourceRepository", {
|
|
sourceRepositoryId: "repo-1",
|
|
projectId: "project-1",
|
|
repoId: 123,
|
|
provider: "github",
|
|
installationId: "installation-1",
|
|
}),
|
|
);
|
|
yield* domainProvider.delete(
|
|
deleteInput("CustomDomain", {
|
|
customDomainId: "domain-1",
|
|
appId: "service-1",
|
|
hostname: "api.example.com",
|
|
}),
|
|
);
|
|
yield* envProvider.delete(
|
|
deleteInput("EnvironmentVariable", {
|
|
environmentVariableId: "env-1",
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
class: "production",
|
|
key: "TOKEN",
|
|
}),
|
|
);
|
|
yield* branchProvider.delete(
|
|
deleteInput("Branch", {
|
|
branchId: "branch-1",
|
|
projectId: "project-1",
|
|
gitName: "preview",
|
|
}),
|
|
);
|
|
yield* connectionProvider.delete(
|
|
deleteInput("Connection", {
|
|
connectionId: "connection-1",
|
|
databaseId: "database-1",
|
|
connectionName: "api-000000000000",
|
|
}),
|
|
);
|
|
yield* databaseProvider.delete(
|
|
deleteInput("Database", { databaseId: "database-1" }),
|
|
);
|
|
yield* projectProvider.delete(
|
|
deleteInput("Project", { projectId: "project-1" }),
|
|
);
|
|
|
|
expect(calls).toEqual([
|
|
["getDeployment", "version-1"],
|
|
// Over the wire, an empty query arrives as an empty object.
|
|
["listAppDeployments", { appId: "service-1", query: {} }],
|
|
["getDeployment", "version-1"],
|
|
["stopDeployment", "version-1"],
|
|
["getDeployment", "version-1"],
|
|
["deleteDeployment", "version-1"],
|
|
["getApp", "service-1"],
|
|
["deleteApp", "service-1"],
|
|
["getSourceRepository", "repo-1"],
|
|
["deleteSourceRepository", "repo-1"],
|
|
["getCustomDomain", "domain-1"],
|
|
["deleteCustomDomain", "domain-1"],
|
|
["getEnvironmentVariable", "env-1"],
|
|
["deleteEnvironmentVariable", "env-1"],
|
|
["getBranch", "branch-1"],
|
|
["deleteBranch", "branch-1"],
|
|
["getConnection", "connection-1"],
|
|
["deleteConnection", "connection-1"],
|
|
["getDatabase", "database-1"],
|
|
["deleteDatabase", "database-1"],
|
|
// Over the wire, query params arrive as strings.
|
|
["listApps", { limit: "100", projectId: "project-1" }],
|
|
["deleteProject", "project-1"],
|
|
]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
Effect.provide(FetchHttpClient.layer),
|
|
Effect.provideService(Stack, {
|
|
name: "prisma-provider-delete-test",
|
|
stage: "test",
|
|
resources: {},
|
|
bindings: {},
|
|
actions: {},
|
|
}),
|
|
Effect.provideService(Stage, "test"),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"rejects direct delete for a default Prisma database",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const client = {
|
|
getDatabase: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getDatabase", id]);
|
|
return { id, isDefault: true };
|
|
}),
|
|
deleteDatabase: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteDatabase", id]);
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const databaseProvider = yield* PrismaDatabase.Provider;
|
|
const result = yield* databaseProvider
|
|
.delete({
|
|
id: "Postgres",
|
|
fqn: "Postgres",
|
|
instanceId: "00000000000000000000000000000000",
|
|
olds: {} as never,
|
|
output: {
|
|
databaseId: "database-1",
|
|
projectId: "project-1",
|
|
isDefault: true,
|
|
},
|
|
session: undefined,
|
|
bindings: [],
|
|
} as never)
|
|
.pipe(Effect.result);
|
|
|
|
expect(Result.isFailure(result)).toBe(true);
|
|
if (Result.isFailure(result)) {
|
|
expect(String(result.failure)).toContain(
|
|
"Cannot delete default Prisma database",
|
|
);
|
|
}
|
|
expect(calls).toEqual([["getDatabase", "database-1"]]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"checks live database default state before deleting",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const client = {
|
|
getDatabase: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getDatabase", id]);
|
|
return { id, isDefault: true };
|
|
}),
|
|
deleteDatabase: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteDatabase", id]);
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const databaseProvider = yield* PrismaDatabase.Provider;
|
|
const result = yield* databaseProvider
|
|
.delete({
|
|
id: "Postgres",
|
|
fqn: "Postgres",
|
|
instanceId: "00000000000000000000000000000000",
|
|
olds: {} as never,
|
|
output: {
|
|
databaseId: "database-1",
|
|
projectId: "project-1",
|
|
isDefault: false,
|
|
},
|
|
session: undefined,
|
|
bindings: [],
|
|
} as never)
|
|
.pipe(Effect.result);
|
|
|
|
expect(Result.isFailure(result)).toBe(true);
|
|
expect(calls).toEqual([["getDatabase", "database-1"]]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"rejects default Prisma database delete without a session",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const client = {
|
|
getDatabase: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getDatabase", id]);
|
|
return { id, isDefault: true };
|
|
}),
|
|
deleteDatabase: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteDatabase", id]);
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const databaseProvider = yield* PrismaDatabase.Provider;
|
|
const result = yield* databaseProvider
|
|
.delete({
|
|
id: "Postgres",
|
|
fqn: "Postgres",
|
|
instanceId: "00000000000000000000000000000000",
|
|
olds: {} as never,
|
|
output: {
|
|
databaseId: "database-1",
|
|
projectId: "project-1",
|
|
isDefault: true,
|
|
},
|
|
session: undefined,
|
|
bindings: [],
|
|
} as never)
|
|
.pipe(Effect.result);
|
|
|
|
expect(Result.isFailure(result)).toBe(true);
|
|
expect(calls).toEqual([["getDatabase", "database-1"]]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"deletes a database when stale state says it is default",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const client = {
|
|
getDatabase: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getDatabase", id]);
|
|
return { id, isDefault: false };
|
|
}),
|
|
deleteDatabase: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteDatabase", id]);
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const databaseProvider = yield* PrismaDatabase.Provider;
|
|
yield* databaseProvider.delete({
|
|
id: "Postgres",
|
|
fqn: "Postgres",
|
|
instanceId: "00000000000000000000000000000000",
|
|
olds: {} as never,
|
|
output: {
|
|
databaseId: "database-1",
|
|
projectId: "project-1",
|
|
isDefault: true,
|
|
},
|
|
session: undefined,
|
|
bindings: [],
|
|
} as never);
|
|
|
|
expect(calls).toEqual([
|
|
["getDatabase", "database-1"],
|
|
["deleteDatabase", "database-1"],
|
|
]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"rejects direct delete for a default Prisma branch",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const client = {
|
|
getBranch: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getBranch", id]);
|
|
return {
|
|
id,
|
|
gitName: "preview",
|
|
isDefault: true,
|
|
role: "preview" as const,
|
|
project: { id: "project-1" },
|
|
};
|
|
}),
|
|
deleteBranch: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteBranch", id]);
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const branchProvider = yield* PrismaBranch.Provider;
|
|
const result = yield* branchProvider
|
|
.delete({
|
|
id: "MainBranch",
|
|
fqn: "MainBranch",
|
|
instanceId: "00000000000000000000000000000000",
|
|
olds: {} as never,
|
|
output: {
|
|
branchId: "branch-1",
|
|
projectId: "project-1",
|
|
gitName: "preview",
|
|
isDefault: true,
|
|
role: "preview",
|
|
},
|
|
session: undefined,
|
|
bindings: [],
|
|
} as never)
|
|
.pipe(Effect.result);
|
|
|
|
expect(Result.isFailure(result)).toBe(true);
|
|
if (Result.isFailure(result)) {
|
|
expect(String(result.failure)).toContain(
|
|
"Cannot safely delete default Prisma branch",
|
|
);
|
|
}
|
|
expect(calls).toEqual([["getBranch", "branch-1"]]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"checks live branch default state before deleting",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const client = {
|
|
getBranch: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getBranch", id]);
|
|
return {
|
|
id,
|
|
gitName: "preview",
|
|
isDefault: true,
|
|
role: "preview" as const,
|
|
project: { id: "project-1" },
|
|
};
|
|
}),
|
|
deleteBranch: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteBranch", id]);
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const branchProvider = yield* PrismaBranch.Provider;
|
|
const result = yield* branchProvider
|
|
.delete({
|
|
id: "MainBranch",
|
|
fqn: "MainBranch",
|
|
instanceId: "00000000000000000000000000000000",
|
|
olds: {} as never,
|
|
output: {
|
|
branchId: "branch-1",
|
|
projectId: "project-1",
|
|
gitName: "preview",
|
|
isDefault: false,
|
|
role: "preview",
|
|
},
|
|
session: undefined,
|
|
bindings: [],
|
|
} as never)
|
|
.pipe(Effect.result);
|
|
|
|
expect(Result.isFailure(result)).toBe(true);
|
|
expect(calls).toEqual([["getBranch", "branch-1"]]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"rejects default Prisma branch delete without a session",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const client = {
|
|
getBranch: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getBranch", id]);
|
|
return {
|
|
id,
|
|
gitName: "preview",
|
|
isDefault: true,
|
|
role: "preview" as const,
|
|
project: { id: "project-1" },
|
|
};
|
|
}),
|
|
deleteBranch: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteBranch", id]);
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const branchProvider = yield* PrismaBranch.Provider;
|
|
const result = yield* branchProvider
|
|
.delete({
|
|
id: "MainBranch",
|
|
fqn: "MainBranch",
|
|
instanceId: "00000000000000000000000000000000",
|
|
olds: {} as never,
|
|
output: {
|
|
branchId: "branch-1",
|
|
projectId: "project-1",
|
|
gitName: "preview",
|
|
isDefault: true,
|
|
role: "preview",
|
|
},
|
|
session: undefined,
|
|
bindings: [],
|
|
} as never)
|
|
.pipe(Effect.result);
|
|
|
|
expect(Result.isFailure(result)).toBe(true);
|
|
expect(calls).toEqual([["getBranch", "branch-1"]]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"deletes a branch when stale state says it is default",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const client = {
|
|
getBranch: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["getBranch", id]);
|
|
return {
|
|
id,
|
|
gitName: "preview",
|
|
isDefault: false,
|
|
role: "preview" as const,
|
|
project: { id: "project-1" },
|
|
};
|
|
}),
|
|
deleteBranch: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["deleteBranch", id]);
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const branchProvider = yield* PrismaBranch.Provider;
|
|
yield* branchProvider.delete({
|
|
id: "MainBranch",
|
|
fqn: "MainBranch",
|
|
instanceId: "00000000000000000000000000000000",
|
|
olds: {} as never,
|
|
output: {
|
|
branchId: "branch-1",
|
|
projectId: "project-1",
|
|
gitName: "preview",
|
|
isDefault: true,
|
|
role: "preview",
|
|
},
|
|
session: undefined,
|
|
bindings: [],
|
|
} as never);
|
|
|
|
expect(calls).toEqual([
|
|
["getBranch", "branch-1"],
|
|
["deleteBranch", "branch-1"],
|
|
]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(managementApi(client).layer),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
|
|
it.effect(
|
|
"refuses to adopt explicit identities after create races",
|
|
() => {
|
|
const calls: Call[] = [];
|
|
const visible = new Set<string>();
|
|
const apiConflict = (path: string) =>
|
|
new PrismaApiError({
|
|
method: "POST",
|
|
path,
|
|
status: 409,
|
|
message: "already exists",
|
|
});
|
|
const project = {
|
|
id: "project-1",
|
|
type: "project" as const,
|
|
url: "https://api.prisma.test/v1/projects/project-1",
|
|
name: "app",
|
|
createdAt,
|
|
defaultRegion: "us-east-1",
|
|
workspace: resourceRef("workspaces", "workspace-1", "team"),
|
|
};
|
|
const database = {
|
|
id: "database-1",
|
|
type: "database" as const,
|
|
url: "https://api.prisma.test/v1/databases/database-1",
|
|
name: "main",
|
|
status: "ready" as const,
|
|
createdAt,
|
|
isDefault: false,
|
|
defaultConnectionId: "connection-1",
|
|
connections: [],
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
region: { id: "us-east-1", name: "US East" },
|
|
source: { type: "empty" },
|
|
branchId: null,
|
|
};
|
|
const connection = {
|
|
id: "connection-1",
|
|
type: "connection" as const,
|
|
url: "https://api.prisma.test/v1/connections/connection-1",
|
|
name: "api-000000000000",
|
|
createdAt,
|
|
kind: "postgres" as const,
|
|
endpoints: {
|
|
direct: { host: "db.prisma.test", port: 5432 },
|
|
},
|
|
database: resourceRef("databases", "database-1", "main"),
|
|
};
|
|
const branch = {
|
|
id: "branch-1",
|
|
type: "branch" as const,
|
|
url: "https://api.prisma.test/v1/branches/branch-1",
|
|
gitName: "preview",
|
|
isDefault: false,
|
|
role: "preview" as const,
|
|
createdAt,
|
|
updatedAt,
|
|
project: resourceRef("projects", "project-1", "app"),
|
|
};
|
|
const defaultBranch = {
|
|
...branch,
|
|
id: "branch-main",
|
|
url: "https://api.prisma.test/v1/branches/branch-main",
|
|
gitName: "main",
|
|
isDefault: true,
|
|
role: "production" as const,
|
|
};
|
|
const service = {
|
|
id: "service-1",
|
|
type: "app" as const,
|
|
url: "https://api.prisma.test/v1/services/service-1",
|
|
name: "api",
|
|
region: { id: "us-east-1", name: "US East" },
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
latestDeploymentId: null,
|
|
appEndpointDomain: "service-1.prisma.build",
|
|
createdAt,
|
|
};
|
|
const variable = {
|
|
id: "env-1",
|
|
type: "environment-variable" as const,
|
|
url: "https://api.prisma.test/v1/environment-variables/env-1",
|
|
projectId: "project-1",
|
|
branchId: null,
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
valueKid: "kid-1",
|
|
isManagedBySystem: false,
|
|
createdAt,
|
|
updatedAt,
|
|
};
|
|
const repo = {
|
|
id: "repo-1",
|
|
type: "source-repository" as const,
|
|
url: "https://api.prisma.test/v1/source-repositories/repo-1",
|
|
repoId: 123,
|
|
provider: "github" as const,
|
|
repoFullName: "acme/api",
|
|
defaultBranch: "main",
|
|
isPrivate: true,
|
|
status: "active" as const,
|
|
projectId: "project-1",
|
|
installationId: "installation-1",
|
|
createdAt,
|
|
updatedAt,
|
|
};
|
|
|
|
const fake = makeFakeManagementApi((request) => {
|
|
if (request.pathname === "/v1/projects" && request.method === "GET") {
|
|
return page(visible.has("project") ? [wireProject()] : []);
|
|
}
|
|
if (
|
|
request.pathname === "/v1/projects" &&
|
|
request.method === "POST"
|
|
) {
|
|
visible.add("project");
|
|
return conflict("already exists");
|
|
}
|
|
return dispatchManagement(client, request);
|
|
});
|
|
|
|
const client = {
|
|
listProjectDatabases: (projectId: string, query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listProjectDatabases", { projectId, query }]);
|
|
return visible.has("database") ? [database] : [];
|
|
}),
|
|
createDatabase: (input: unknown) =>
|
|
Effect.gen(function* () {
|
|
calls.push(["createDatabase", input]);
|
|
visible.add("database");
|
|
return yield* Effect.fail(apiConflict("/v1/databases"));
|
|
}),
|
|
listDatabaseConnections: (databaseId: string, query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listDatabaseConnections", { databaseId, query }]);
|
|
return visible.has("connection") ? [connection] : [];
|
|
}),
|
|
createConnection: (input: unknown) =>
|
|
Effect.gen(function* () {
|
|
calls.push(["createConnection", input]);
|
|
visible.add("connection");
|
|
return yield* Effect.fail(apiConflict("/v1/connections"));
|
|
}),
|
|
rotateConnection: (id: string) =>
|
|
Effect.sync(() => {
|
|
calls.push(["rotateConnection", id]);
|
|
return {
|
|
...connection,
|
|
endpoints: {
|
|
direct: {
|
|
host: "db.prisma.test",
|
|
port: 5432,
|
|
connectionString:
|
|
"postgres://app:rotated-password@db.prisma.test/database-1",
|
|
},
|
|
},
|
|
};
|
|
}),
|
|
listBranches: (projectId: string, query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listBranches", { projectId, query }]);
|
|
const gitName = (query as { gitName?: string } | undefined)
|
|
?.gitName;
|
|
if (gitName === "preview") {
|
|
return visible.has("branch") ? [branch] : [];
|
|
}
|
|
return [
|
|
defaultBranch,
|
|
...(visible.has("branch") ? [branch] : []),
|
|
];
|
|
}),
|
|
createBranch: (projectId: string, input: unknown) =>
|
|
Effect.gen(function* () {
|
|
calls.push(["createBranch", { projectId, input }]);
|
|
visible.add("branch");
|
|
return yield* Effect.fail(
|
|
apiConflict(`/v1/projects/${projectId}/branches`),
|
|
);
|
|
}),
|
|
listApps: (query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listApps", query]);
|
|
return visible.has("service") ? [service] : [];
|
|
}),
|
|
createApp: (input: { projectId: string }) =>
|
|
Effect.gen(function* () {
|
|
calls.push(["createApp", input]);
|
|
visible.add("service");
|
|
return yield* Effect.fail(apiConflict(`/v1/services`));
|
|
}),
|
|
listEnvironmentVariables: (query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listEnvironmentVariables", query]);
|
|
return visible.has("env") ? [variable] : [];
|
|
}),
|
|
createEnvironmentVariable: (input: unknown) =>
|
|
Effect.gen(function* () {
|
|
calls.push(["createEnvironmentVariable", input]);
|
|
visible.add("env");
|
|
return yield* Effect.fail(
|
|
apiConflict("/v1/environment-variables"),
|
|
);
|
|
}),
|
|
updateEnvironmentVariable: (id: string, input: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["updateEnvironmentVariable", { id, input }]);
|
|
return variable;
|
|
}),
|
|
listSourceRepositories: (query: unknown) =>
|
|
Effect.sync(() => {
|
|
calls.push(["listSourceRepositories", query]);
|
|
return visible.has("repo") ? [repo] : [];
|
|
}),
|
|
createSourceRepository: (input: unknown) =>
|
|
Effect.gen(function* () {
|
|
calls.push(["createSourceRepository", input]);
|
|
visible.add("repo");
|
|
return yield* Effect.fail(apiConflict("/v1/source-repositories"));
|
|
}),
|
|
} as unknown as PrismaManagementClient;
|
|
|
|
return Effect.gen(function* () {
|
|
const projectProvider = yield* PrismaProject.Provider;
|
|
const databaseProvider = yield* PrismaDatabase.Provider;
|
|
const connectionProvider = yield* PrismaConnection.Provider;
|
|
const branchProvider = yield* PrismaBranch.Provider;
|
|
const serviceProvider = yield* PrismaApp.Provider;
|
|
const envProvider = yield* PrismaEnvironmentVariable.Provider;
|
|
const repoProvider = yield* PrismaSourceRepository.Provider;
|
|
|
|
const projectError = yield* projectProvider
|
|
.reconcile(
|
|
reconcileInput("Project", { name: "app", createDatabase: false }),
|
|
)
|
|
.pipe(Effect.flip);
|
|
const databaseError = yield* databaseProvider
|
|
.reconcile(
|
|
reconcileInput("Database", {
|
|
project: "project-1",
|
|
name: "main",
|
|
}),
|
|
)
|
|
.pipe(Effect.flip);
|
|
const connectionOut = yield* connectionProvider.reconcile(
|
|
reconcileInput("Connection", {
|
|
database: "database-1",
|
|
name: "api",
|
|
rotate: true,
|
|
}),
|
|
);
|
|
const branchError = yield* branchProvider
|
|
.reconcile(
|
|
reconcileInput("Branch", {
|
|
project: "project-1",
|
|
gitName: "preview",
|
|
}),
|
|
)
|
|
.pipe(Effect.flip);
|
|
const serviceError = yield* serviceProvider
|
|
.reconcile(
|
|
reconcileInput("App", {
|
|
project: "project-1",
|
|
displayName: "api",
|
|
}),
|
|
)
|
|
.pipe(Effect.flip);
|
|
const envError = yield* envProvider
|
|
.reconcile(
|
|
reconcileInput("EnvironmentVariable", {
|
|
project: "project-1",
|
|
class: "production" as const,
|
|
key: "TOKEN",
|
|
value: Redacted.make("secret"),
|
|
}),
|
|
)
|
|
.pipe(Effect.flip);
|
|
const repoError = yield* repoProvider
|
|
.reconcile(
|
|
reconcileInput("SourceRepository", {
|
|
project: "project-1",
|
|
providerRepositoryId: 123,
|
|
}),
|
|
)
|
|
.pipe(Effect.flip);
|
|
|
|
expect((projectError as Error).message).toContain(
|
|
"appeared after the adoption check",
|
|
);
|
|
expect((databaseError as Error).message).toContain(
|
|
"appeared after the adoption check",
|
|
);
|
|
expect(connectionOut.connectionId).toBe("connection-1");
|
|
expect(Redacted.value(connectionOut.directConnectionString!)).toBe(
|
|
"postgres://app:rotated-password@db.prisma.test/database-1",
|
|
);
|
|
expect(Redacted.value(connectionOut.password!)).toBe(
|
|
"rotated-password",
|
|
);
|
|
expect((branchError as Error).message).toContain(
|
|
"appeared after the adoption check",
|
|
);
|
|
expect((serviceError as Error).message).toContain(
|
|
"not owned by this App resource",
|
|
);
|
|
expect((envError as Error).message).toContain(
|
|
"appeared after the adoption check",
|
|
);
|
|
expect((repoError as Error).message).toContain(
|
|
"appeared after the adoption check",
|
|
);
|
|
expect(
|
|
fake.captured
|
|
.filter((request) => request.method === "POST")
|
|
.map((request) => request.bodyJson),
|
|
).toEqual([
|
|
{ createDatabase: false, name: "app" },
|
|
{
|
|
projectId: "project-1",
|
|
region: "us-east-1",
|
|
name: "main",
|
|
isDefault: false,
|
|
},
|
|
{ databaseId: "database-1", name: "api-000000000000" },
|
|
undefined,
|
|
{ gitName: "preview" },
|
|
{
|
|
displayName: "api",
|
|
branchId: "branch-main",
|
|
projectId: "project-1",
|
|
},
|
|
{
|
|
projectId: "project-1",
|
|
class: "production",
|
|
key: "TOKEN",
|
|
value: "secret",
|
|
},
|
|
{
|
|
projectId: "project-1",
|
|
provider: "github",
|
|
providerRepositoryId: 123,
|
|
},
|
|
]);
|
|
expect(calls.filter(([name]) => name.startsWith("create"))).toEqual([
|
|
[
|
|
"createDatabase",
|
|
{
|
|
projectId: "project-1",
|
|
name: "main",
|
|
region: "us-east-1",
|
|
isDefault: false,
|
|
source: undefined,
|
|
branchId: undefined,
|
|
branchGitName: undefined,
|
|
},
|
|
],
|
|
[
|
|
"createConnection",
|
|
{ databaseId: "database-1", name: "api-000000000000" },
|
|
],
|
|
[
|
|
"createBranch",
|
|
{
|
|
projectId: "project-1",
|
|
input: { gitName: "preview", isDefault: undefined },
|
|
},
|
|
],
|
|
[
|
|
"createApp",
|
|
// JSON transport drops `undefined` members.
|
|
{
|
|
projectId: "project-1",
|
|
displayName: "api",
|
|
branchId: "branch-main",
|
|
},
|
|
],
|
|
[
|
|
"createEnvironmentVariable",
|
|
{
|
|
projectId: "project-1",
|
|
class: "production",
|
|
key: "TOKEN",
|
|
value: "secret",
|
|
},
|
|
],
|
|
[
|
|
"createSourceRepository",
|
|
{
|
|
projectId: "project-1",
|
|
provider: "github",
|
|
providerRepositoryId: 123,
|
|
installationId: undefined,
|
|
},
|
|
],
|
|
]);
|
|
expect(calls).toContainEqual(["rotateConnection", "connection-1"]);
|
|
}).pipe(
|
|
Effect.provide(providerLayer(client)),
|
|
Effect.provide(fake.layer),
|
|
Effect.provide(FetchHttpClient.layer),
|
|
Effect.provideService(Stack, {
|
|
name: "prisma-provider-conflict-test",
|
|
stage: "test",
|
|
resources: {},
|
|
bindings: {},
|
|
actions: {},
|
|
}),
|
|
Effect.provideService(Stage, "test"),
|
|
);
|
|
},
|
|
{
|
|
tags: [
|
|
"provider:prisma:app",
|
|
"provider:prisma:branch",
|
|
"provider:prisma:connection",
|
|
"provider:prisma:customdomain",
|
|
"provider:prisma:database",
|
|
"provider:prisma:deployment",
|
|
"provider:prisma:environmentvariable",
|
|
"provider:prisma:project",
|
|
"provider:prisma:sourcerepository",
|
|
],
|
|
},
|
|
);
|
|
},
|
|
);
|