Skip to content

Commit acc6c1d

Browse files
committed
fix: simplify strict project binding resolution
1 parent 386acd8 commit acc6c1d

5 files changed

Lines changed: 71 additions & 95 deletions

File tree

packages/cli/src/controllers/app.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2232,6 +2232,7 @@ async function resolveProjectContext(
22322232
explicitProject: string | undefined,
22332233
options?: {
22342234
branch?: ResolvedDeployBranch;
2235+
commandName?: string;
22352236
},
22362237
): Promise<ResolvedAppProjectContext> {
22372238
const authState = await requireAuthenticatedAuthState(context);
@@ -3237,12 +3238,12 @@ function localResolutionPinStaleError(): CliError {
32373238
domain: "project",
32383239
summary: "Local project binding is stale",
32393240
why: `The target recorded in ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} is no longer available in the selected workspace.`,
3240-
fix: `Delete ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} and re-run to re-bootstrap.`,
3241+
fix: `Delete ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH}, then choose a Project explicitly.`,
32413242
meta: {
32423243
pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH,
32433244
},
32443245
exitCode: 1,
3245-
nextSteps: ["prisma-cli app deploy"],
3246+
nextSteps: ["prisma-cli project list", "prisma-cli project link <id-or-name>", "prisma-cli app deploy --project <id-or-name>"],
32463247
});
32473248
}
32483249

packages/cli/src/controllers/project.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,10 +1175,3 @@ function repoConnectionFixForStatus(status: number): string {
11751175

11761176
return "Re-run with --trace for the underlying API response details.";
11771177
}
1178-
1179-
function toProjectSummary(project: ProjectCandidate) {
1180-
return {
1181-
id: project.id,
1182-
name: project.name,
1183-
};
1184-
}

packages/cli/src/lib/project/resolution.ts

Lines changed: 66 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,8 @@ export interface ProjectCandidate extends ProjectSummary {
1919
workspace: AuthWorkspace;
2020
}
2121

22-
export interface ResolvedProjectTarget {
23-
workspace: AuthWorkspace;
24-
project: ProjectSummary;
25-
resolution: ProjectResolution;
26-
}
22+
export type ResolvedProjectTarget = BoundProjectShowResult;
23+
type BoundProjectSource = Exclude<ProjectSource, "unbound">;
2724

2825
export type InferredTargetNameSource = "package-name" | "directory-name";
2926

@@ -43,51 +40,10 @@ export interface ResolveProjectOptions {
4340

4441
export async function resolveProjectTarget(options: ResolveProjectOptions): Promise<ResolvedProjectTarget> {
4542
const projects = await options.listProjects();
43+
const target = await resolveBoundProjectTarget(options, projects, { allowEnvProjectId: true });
4644

47-
if (options.explicitProject) {
48-
return resolvedTarget(options.workspace, resolveExplicitProject(options.explicitProject, projects, options.workspace), "explicit", {
49-
targetName: options.explicitProject,
50-
targetNameSource: "explicit",
51-
});
52-
}
53-
54-
if (options.envProjectId) {
55-
const project = projects.find((candidate) => candidate.id === options.envProjectId);
56-
if (!project) {
57-
throw projectNotFoundError(options.envProjectId, options.workspace);
58-
}
59-
return resolvedTarget(options.workspace, project, "env", {
60-
targetName: options.envProjectId,
61-
targetNameSource: "env",
62-
});
63-
}
64-
65-
const localPin = await readLocalResolutionPin(options.context.runtime.cwd);
66-
if (localPin.kind === "invalid") {
67-
throw localStateStaleError();
68-
}
69-
if (localPin.kind === "present") {
70-
if (localPin.pin.workspaceId !== options.workspace.id) {
71-
throw localStateStaleError();
72-
}
73-
74-
const project = projects.find((candidate) => candidate.id === localPin.pin.projectId);
75-
if (!project) {
76-
throw localStateStaleError();
77-
}
78-
79-
return resolvedTarget(options.workspace, project, "local-pin", {
80-
targetName: project.name,
81-
targetNameSource: "local-pin",
82-
});
83-
}
84-
85-
const platformMapping = await resolveDurablePlatformMapping();
86-
if (platformMapping && platformMapping.workspace.id === options.workspace.id) {
87-
return resolvedTarget(options.workspace, platformMapping, "platform-mapping", {
88-
targetName: platformMapping.name,
89-
targetNameSource: "platform-mapping",
90-
});
45+
if (target) {
46+
return target;
9147
}
9248

9349
throw await projectSetupRequiredError({
@@ -99,40 +55,10 @@ export async function resolveProjectTarget(options: ResolveProjectOptions): Prom
9955

10056
export async function inspectProjectBinding(options: ResolveProjectOptions): Promise<ProjectShowResult> {
10157
const projects = await options.listProjects();
58+
const target = await resolveBoundProjectTarget(options, projects, { allowEnvProjectId: false });
10259

103-
if (options.explicitProject) {
104-
return resolvedTarget(options.workspace, resolveExplicitProject(options.explicitProject, projects, options.workspace), "explicit", {
105-
targetName: options.explicitProject,
106-
targetNameSource: "explicit",
107-
});
108-
}
109-
110-
const localPin = await readLocalResolutionPin(options.context.runtime.cwd);
111-
if (localPin.kind === "invalid") {
112-
throw localStateStaleError();
113-
}
114-
if (localPin.kind === "present") {
115-
if (localPin.pin.workspaceId !== options.workspace.id) {
116-
throw localStateStaleError();
117-
}
118-
119-
const project = projects.find((candidate) => candidate.id === localPin.pin.projectId);
120-
if (!project) {
121-
throw localStateStaleError();
122-
}
123-
124-
return resolvedTarget(options.workspace, project, "local-pin", {
125-
targetName: project.name,
126-
targetNameSource: "local-pin",
127-
});
128-
}
129-
130-
const platformMapping = await resolveDurablePlatformMapping();
131-
if (platformMapping && platformMapping.workspace.id === options.workspace.id) {
132-
return resolvedTarget(options.workspace, platformMapping, "platform-mapping", {
133-
targetName: platformMapping.name,
134-
targetNameSource: "platform-mapping",
135-
});
60+
if (target) {
61+
return target;
13662
}
13763

13864
return {
@@ -233,7 +159,7 @@ export async function projectSetupRequiredError(options: {
233159
summary: "Choose a Project before running this command",
234160
why: `This directory is not linked to a Prisma Project, and ${commandLabel} will not choose one from package or directory names.`,
235161
fix: "Link the directory to an existing Project, or pass --project <id-or-name> for this command.",
236-
meta: suggestion,
162+
meta: { ...suggestion },
237163
exitCode: 1,
238164
nextSteps: ["prisma-cli project list", ...suggestion.recoveryCommands],
239165
});
@@ -307,10 +233,66 @@ export async function resolveDurablePlatformMapping(): Promise<ProjectCandidate
307233
return null;
308234
}
309235

236+
async function resolveBoundProjectTarget(
237+
options: ResolveProjectOptions,
238+
projects: ProjectCandidate[],
239+
settings: {
240+
allowEnvProjectId: boolean;
241+
},
242+
): Promise<BoundProjectShowResult | null> {
243+
if (options.explicitProject) {
244+
return resolvedTarget(options.workspace, resolveExplicitProject(options.explicitProject, projects, options.workspace), "explicit", {
245+
targetName: options.explicitProject,
246+
targetNameSource: "explicit",
247+
});
248+
}
249+
250+
if (settings.allowEnvProjectId && options.envProjectId) {
251+
const project = projects.find((candidate) => candidate.id === options.envProjectId);
252+
if (!project) {
253+
throw projectNotFoundError(options.envProjectId, options.workspace);
254+
}
255+
return resolvedTarget(options.workspace, project, "env", {
256+
targetName: options.envProjectId,
257+
targetNameSource: "env",
258+
});
259+
}
260+
261+
const localPin = await readLocalResolutionPin(options.context.runtime.cwd);
262+
if (localPin.kind === "invalid") {
263+
throw localStateStaleError();
264+
}
265+
if (localPin.kind === "present") {
266+
if (localPin.pin.workspaceId !== options.workspace.id) {
267+
throw localStateStaleError();
268+
}
269+
270+
const project = projects.find((candidate) => candidate.id === localPin.pin.projectId);
271+
if (!project) {
272+
throw localStateStaleError();
273+
}
274+
275+
return resolvedTarget(options.workspace, project, "local-pin", {
276+
targetName: project.name,
277+
targetNameSource: "local-pin",
278+
});
279+
}
280+
281+
const platformMapping = await resolveDurablePlatformMapping();
282+
if (platformMapping && platformMapping.workspace.id === options.workspace.id) {
283+
return resolvedTarget(options.workspace, platformMapping, "platform-mapping", {
284+
targetName: platformMapping.name,
285+
targetNameSource: "platform-mapping",
286+
});
287+
}
288+
289+
return null;
290+
}
291+
310292
function resolvedTarget(
311293
workspace: AuthWorkspace,
312294
project: ProjectCandidate,
313-
projectSource: ProjectSource,
295+
projectSource: BoundProjectSource,
314296
resolutionDetails?: Omit<ProjectResolution, "projectSource">,
315297
): BoundProjectShowResult {
316298
return {

packages/cli/src/types/project.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,5 @@ export interface GitRepositoryConnection {
8686
}
8787

8888
export interface ProjectRepositoryConnectionResult extends BoundProjectShowResult {
89-
repositoryConnection: GitRepositoryConnection | null;
89+
repositoryConnection: GitRepositoryConnection;
9090
}

packages/cli/tests/app-controller.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1753,7 +1753,7 @@ describe("app controller", () => {
17531753
meta: {
17541754
pinPath: ".prisma/local.json",
17551755
},
1756-
fix: "Delete .prisma/local.json and re-run to re-bootstrap.",
1756+
fix: "Delete .prisma/local.json, then choose a Project explicitly.",
17571757
});
17581758
expect(listApps).not.toHaveBeenCalled();
17591759
expect(deployApp).not.toHaveBeenCalled();

0 commit comments

Comments
 (0)