Skip to content

Commit 391a5cd

Browse files
authored
refactor(api): cover the client sources with effect lint (CLI-2841) (#7309)
## TL;DR brings `packages/api/src` and its tests under the effect lint ## whats introduced? - `packages/api/src/**` joins the allow list, and the generated `contracts.ts` stays out - `SupabaseApiConfigError` and `SupabaseApiInputError` move to `Data.TaggedError` with the same message, name, cause, json and printed output, pinned by a new test, so two `apps/cli` call sites whose failures include them now yield them directly - `createApiClient` in `bun.ts` and `node.ts` chains its promise instead of being `async`, still disposing the runtime before it rejects - the src tests move to effect: `FileSystem` and `Path`, schema json codecs, and `Effect.runPromise` bodies instead of `async` callbacks ## ref - closes: CLI-2841
1 parent 58c3596 commit 391a5cd

16 files changed

Lines changed: 2150 additions & 2011 deletions

‎.oxlintrc.effect.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
},
77
"ignorePatterns": [
88
"**",
9+
"!packages/api/src/**",
910
"!packages/config/**",
1011
"!packages/stack/**",
1112
"!apps/cli/src/**",
@@ -16,6 +17,7 @@
1617
"apps/cli/src/shared/feedback/database.types.ts",
1718
"apps/cli/src/command-internal/db-bootstrap/local-db-running.integration.test.ts",
1819
"apps/cli/src/command-internal/db-bootstrap/local-db-running.ts",
20+
"packages/api/src/generated/contracts.ts",
1921
"!apps/cli/tests/helpers/branches-live.ts",
2022
"!apps/cli/tests/helpers/compute.ts",
2123
"!apps/cli/tests/helpers/migration-live.ts",

‎apps/cli/src/command-internal/http-errors.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export function mapHttpError<N, S>(opts: {
157157
if (cause instanceof SupabaseApiInputError || cause instanceof HttpBody.HttpBodyError) {
158158
// Client-side validation/build failures: keep their identity, since
159159
// this generic mapper can't safely reclassify them as response errors.
160-
return yield* Effect.fail(cause);
160+
return yield* cause;
161161
}
162162
if (HttpClientError.isHttpClientError(cause)) {
163163
if (RESPONSE_ERROR_TAGS.has(cause.reason._tag) && cause.response !== undefined) {

‎apps/cli/src/commands/start/start.handler.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1851,7 +1851,7 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) {
18511851
if (Result.isFailure(seedResult)) {
18521852
// No manual `rollbackStart` here — the outer
18531853
// `Effect.onError` below rolls back on this failure too.
1854-
return yield* Effect.fail(seedResult.failure);
1854+
return yield* seedResult.failure;
18551855
}
18561856
}
18571857
}
Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { describe, expect, test } from "vitest";
2+
import { Effect } from "effect";
23
import { createApiClient } from "./bun.ts";
34

45
describe("Bun client lifecycle", () => {
5-
test("serves requests through the Bun entrypoint and rejects operations after disposal", async () => {
6+
test("serves requests through the Bun entrypoint and rejects operations after disposal", () => {
67
const paths: string[] = [];
78
const server = Bun.serve({
89
hostname: "127.0.0.1",
@@ -12,19 +13,27 @@ describe("Bun client lifecycle", () => {
1213
return Response.json([]);
1314
},
1415
});
15-
const client = await createApiClient({
16-
baseUrl: server.url.toString(),
17-
accessToken: "test-token",
18-
});
19-
try {
20-
await expect(client.v1.listAllProjects()).resolves.toEqual([]);
21-
expect(paths).toEqual(["/v1/projects"]);
22-
await client.dispose();
23-
await expect(client.v1.listAllProjects()).rejects.toThrow();
24-
expect(paths).toEqual(["/v1/projects"]);
25-
} finally {
26-
await client.dispose();
27-
await server.stop(true);
28-
}
16+
return Effect.runPromise(
17+
Effect.acquireUseRelease(
18+
Effect.promise(() =>
19+
createApiClient({
20+
baseUrl: server.url.toString(),
21+
accessToken: "test-token",
22+
}),
23+
),
24+
(client) =>
25+
Effect.gen(function* () {
26+
yield* Effect.promise(() => expect(client.v1.listAllProjects()).resolves.toEqual([]));
27+
expect(paths).toEqual(["/v1/projects"]);
28+
yield* Effect.promise(() => client.dispose());
29+
yield* Effect.promise(() => expect(client.v1.listAllProjects()).rejects.toThrow());
30+
expect(paths).toEqual(["/v1/projects"]);
31+
}),
32+
(client) =>
33+
Effect.promise(() => client.dispose()).pipe(
34+
Effect.andThen(Effect.promise(() => server.stop(true))),
35+
),
36+
),
37+
);
2938
});
3039
});

‎packages/api/src/bun.ts‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,15 @@ import { type SupabaseApiClientOptions, type SupabaseApiConfig } from "./interna
77
import { makePromiseClient, type PromiseClient } from "./internal/promise-client.ts";
88

99
/** Creates a Promise API client backed by a managed Bun runtime. */
10-
export async function createApiClient(
10+
export function createApiClient(
1111
config: SupabaseApiConfig = {},
1212
options?: SupabaseApiClientOptions,
1313
): Promise<PromiseSupabaseApiClient> {
1414
const runtime = ManagedRuntime.make(Layer.mergeAll(BunServices.layer, FetchHttpClient.layer));
15-
try {
16-
const effectClient = await runtime.runPromise(makeApiClient(config, options));
17-
return makePromiseClient(runtime, effectClient);
18-
} catch (error) {
19-
await runtime.dispose();
20-
throw error;
21-
}
15+
return runtime
16+
.runPromise(makeApiClient(config, options))
17+
.then((effectClient) => makePromiseClient(runtime, effectClient))
18+
.catch((error: unknown) => runtime.dispose().then(() => Promise.reject(error)));
2219
}
2320

2421
/** Promise API client whose runtime resources are released by `dispose`. */
Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
import { describe, expect, test } from "vitest";
2-
import { ConfigProvider, Effect, Option } from "effect";
2+
import { ConfigProvider, Effect, Layer, Option } from "effect";
33

44
import { apiConfigLayer, DEFAULT_SUPABASE_API_URL } from "./api-config.layer.ts";
55
import { ApiConfig } from "./api-config.service.ts";
66

77
describe("apiConfigLayer", () => {
8-
test("defaults the API URL and reads the access token from config", async () => {
9-
const config = await Effect.runPromise(
8+
test("defaults the API URL and reads the access token from config", () =>
9+
Effect.runPromise(
1010
Effect.gen(function* () {
11-
return yield* ApiConfig;
12-
}).pipe(
13-
Effect.provide(apiConfigLayer),
14-
Effect.provide(
15-
ConfigProvider.layer(
16-
ConfigProvider.fromUnknown({
17-
SUPABASE_ACCESS_TOKEN: "env-token",
18-
}),
11+
const config = yield* Effect.service(ApiConfig).pipe(
12+
Effect.provide(
13+
apiConfigLayer.pipe(
14+
Layer.provide(
15+
ConfigProvider.layer(
16+
ConfigProvider.fromUnknown({
17+
SUPABASE_ACCESS_TOKEN: "env-token",
18+
}),
19+
),
20+
),
21+
),
1922
),
20-
),
21-
),
22-
);
23+
);
2324

24-
expect(config.baseUrl).toBe(DEFAULT_SUPABASE_API_URL);
25-
expect(Option.isSome(config.accessToken)).toBe(true);
26-
});
25+
expect(config.baseUrl).toBe(DEFAULT_SUPABASE_API_URL);
26+
expect(Option.isSome(config.accessToken)).toBe(true);
27+
}),
28+
));
2729
});

0 commit comments

Comments
 (0)