| 1 | import * as storage from "@distilled.cloud/gcp/storage_v1"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import type { Bucket } from "./Bucket.ts"; |
| 4 | import { grantOnBucket } from "./ObjectHttp.ts"; |
| 5 | import { makeObjectMedia } from "./ObjectMedia.ts"; |
| 6 | import type { ReadBucketClient } from "./ReadBucket.ts"; |
| 7 | import 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 | */ |
| 16 | export 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. */ |
| 67 | export 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 | }); |