1import * as storage from "@distilled.cloud/gcp/storage_v1";
2import * as Effect from "effect/Effect";
3import type { Bucket } from "./Bucket.ts";
4import { grantOnBucket } from "./ObjectHttp.ts";
5import { makeObjectMedia } from "./ObjectMedia.ts";
6import type { ReadBucketClient } from "./ReadBucket.ts";
7import type { WriteBucketClient } from "./WriteBucket.ts";
8
9/**
10 * Shared HTTP scaffolding for the Storage Read/Write/ReadWrite bindings:
11 * resolves the distilled operations and media endpoints once at Layer
12 * construction and grants `role` on the bound bucket at deploy time.
13 *
14 * NOT exported from `index.ts`.
15 */
16export const makeStorageBucketHelpers = Effect.gen(function* () {
17 const media = yield* makeObjectMedia;
18 const getObject = yield* storage.getObjects;
19 const listObjects = yield* storage.listObjects;
20 const deleteObject = yield* storage.deleteObjects;
21
22 const makeRead = (bucketName: Effect.Effect<string>): ReadBucketClient => ({
23 head: (object) =>
24 bucketName.pipe(
25 Effect.flatMap((bucket) => getObject({ bucket, object })),
26 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
27 ),
28 get: (object, options) =>
29 bucketName.pipe(
30 Effect.flatMap((bucket) =>
31 media.download({ bucket, object, generation: options?.generation }),
32 ),
33 Effect.catchTag("GCP.Storage.ObjectNotFound", () =>
34 Effect.succeed(undefined),
35 ),
36 ),
37 list: (options) =>
38 bucketName.pipe(
39 Effect.flatMap((bucket) => listObjects({ bucket, ...options })),
40 Effect.map((page) => ({
41 objects: page.items ?? [],
42 prefixes: page.prefixes ?? [],
43 nextPageToken: page.nextPageToken,
44 })),
45 ),
46 });
47
48 const makeWrite = (bucketName: Effect.Effect<string>): WriteBucketClient => ({
49 put: (name, body, options) =>
50 bucketName.pipe(
51 Effect.flatMap((bucket) =>
52 media.upload(bucket, { ...options, name, body }),
53 ),
54 ),
55 delete: (object) =>
56 bucketName.pipe(
57 Effect.flatMap((bucket) => deleteObject({ bucket, object })),
58 Effect.asVoid,
59 Effect.catchTag("NotFound", () => Effect.void),
60 ),
61 });
62
63 return { makeRead, makeWrite };
64});
65
66/** Build a bucket binding that grants `role` and returns `makeClient`'s client. */
67export const makeStorageBucketBinding = <Client>(options: {
68 tag: string;
69 role: string;
70 makeClient: (
71 helpers: Effect.Success<typeof makeStorageBucketHelpers>,
72 bucketName: Effect.Effect<string>,
73 ) => Client;
74}) =>
75 Effect.gen(function* () {
76 const helpers = yield* makeStorageBucketHelpers;
77 return Effect.fn(function* (bucket: Bucket) {
78 yield* grantOnBucket(options.tag, bucket, options.role);
79 const bucketName = yield* bucket.bucketName;
80 return options.makeClient(helpers, bucketName);
81 });
82 });