Skip to content

Commit b2e805c

Browse files
authored
Merge 208f81d into 4992e56
2 parents 4992e56 + 208f81d commit b2e805c

File tree

8 files changed

+278
-195
lines changed

8 files changed

+278
-195
lines changed

alchemy/src/cloudflare/bucket-object.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { R2PutOptions } from "@cloudflare/workers-types/experimental/index.ts";
21
import type { Context } from "../context.ts";
32
import { Resource } from "../resource.ts";
43
import { createCloudflareApi, type CloudflareApiOptions } from "./api.ts";

alchemy/src/cloudflare/bucket.ts

Lines changed: 39 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import type { R2PutOptions } from "@cloudflare/workers-types/experimental/index.ts";
2-
import * as mf from "miniflare";
32
import { isDeepStrictEqual } from "node:util";
43
import type { Context } from "../context.ts";
54
import { Resource, ResourceKind } from "../resource.ts";
65
import { Scope } from "../scope.ts";
7-
import { streamToBuffer } from "../serde.ts";
86
import { isRetryableError } from "../state/r2-rest-state-store.ts";
97
import { withExponentialBackoff } from "../util/retry.ts";
108
import { CloudflareApiError, handleApiError } from "./api-error.ts";
@@ -22,7 +20,10 @@ import {
2220
type R2BucketCustomDomainOptions,
2321
} from "./bucket-custom-domain.ts";
2422
import { deleteMiniflareBinding } from "./miniflare/delete.ts";
25-
import { getDefaultPersistPath } from "./miniflare/paths.ts";
23+
import {
24+
makeAsyncProxy,
25+
makeAsyncProxyForBinding,
26+
} from "./miniflare/node-binding.ts";
2627

2728
export type R2BucketJurisdiction = "default" | "eu" | "fedramp";
2829

@@ -306,23 +307,7 @@ export type R2Objects = {
306307
}
307308
);
308309

309-
export type R2Bucket = _R2Bucket & {
310-
head(key: string): Promise<R2ObjectMetadata | null>;
311-
get(key: string): Promise<R2ObjectContent | null>;
312-
put(
313-
key: string,
314-
value:
315-
| ReadableStream
316-
| ArrayBuffer
317-
| ArrayBufferView
318-
| string
319-
| null
320-
| Blob,
321-
options?: Pick<R2PutOptions, "httpMetadata">,
322-
): Promise<PutR2ObjectResponse>;
323-
delete(key: string): Promise<Response>;
324-
list(options?: R2ListOptions): Promise<R2Objects>;
325-
};
310+
export type R2Bucket = _R2Bucket & globalThis.R2Bucket;
326311

327312
/**
328313
* Output returned after R2 Bucket creation/update
@@ -461,9 +446,6 @@ export async function R2Bucket(
461446
id: string,
462447
props: BucketProps = {},
463448
): Promise<R2Bucket> {
464-
const scope = Scope.current;
465-
const isLocal = scope.local && props.dev?.remote !== true;
466-
const api = await createCloudflareApi(props);
467449
const bucket = await _R2Bucket(id, {
468450
...props,
469451
dev: {
@@ -472,144 +454,31 @@ export async function R2Bucket(
472454
},
473455
});
474456

475-
let _miniflare: mf.Miniflare | undefined;
476-
const miniflare = () => {
477-
if (_miniflare) {
478-
return _miniflare;
479-
}
480-
_miniflare = new mf.Miniflare({
481-
script: "",
482-
modules: true,
483-
defaultPersistRoot: getDefaultPersistPath(scope.rootDir),
484-
r2Buckets: [bucket.dev.id],
485-
log: process.env.DEBUG ? new mf.Log(mf.LogLevel.DEBUG) : undefined,
486-
});
487-
scope.onCleanup(async () => _miniflare?.dispose());
488-
return _miniflare;
489-
};
490-
const localBucket = () => miniflare().getR2Bucket(bucket.dev.id);
491-
492-
return {
493-
...bucket,
494-
head: async (key: string) => {
495-
if (isLocal) {
496-
const result = await (await localBucket()).head(key);
497-
if (result) {
498-
return {
499-
key: result.key,
500-
etag: result.etag,
501-
uploaded: result.uploaded,
502-
size: result.size,
503-
httpMetadata: result.httpMetadata,
504-
} as R2ObjectMetadata;
505-
}
506-
return null;
507-
}
508-
return headObject(api, {
509-
bucketName: bucket.name,
510-
key,
511-
});
512-
},
513-
get: async (key: string) => {
514-
if (isLocal) {
515-
const result = await (await localBucket()).get(key);
516-
if (result) {
517-
// cast because workers vs node built-ins
518-
return result as unknown as R2ObjectContent;
519-
}
520-
return null;
521-
}
522-
const response = await getObject(api, {
523-
bucketName: bucket.name,
524-
key,
525-
});
526-
if (response.ok) {
527-
return parseR2Object(key, response);
528-
} else if (response.status === 404) {
529-
return null;
530-
} else {
531-
throw await handleApiError(response, "get", "object", key);
532-
}
533-
},
534-
list: async (options?: R2ListOptions): Promise<R2Objects> => {
535-
if (isLocal) {
536-
return (await localBucket()).list(options);
537-
}
538-
return listObjects(api, bucket.name, {
539-
...options,
540-
jurisdiction: bucket.jurisdiction,
541-
});
542-
},
543-
put: async (
544-
key: string,
545-
value: PutObjectObject,
546-
options?: Pick<R2PutOptions, "httpMetadata">,
547-
): Promise<PutR2ObjectResponse> => {
548-
if (isLocal) {
549-
return await (await localBucket()).put(
550-
key,
551-
typeof value === "string"
552-
? value
553-
: Buffer.isBuffer(value) ||
554-
value instanceof Uint8Array ||
555-
value instanceof ArrayBuffer
556-
? new Uint8Array(value)
557-
: value instanceof Blob
558-
? new Uint8Array(await value.arrayBuffer())
559-
: value instanceof ReadableStream
560-
? new Uint8Array(await streamToBuffer(value))
561-
: value,
562-
options,
563-
);
564-
}
565-
const response = await putObject(api, {
566-
bucketName: bucket.name,
567-
key: key,
568-
object: value,
569-
options: options,
570-
});
571-
const body = (await response.json()) as {
572-
result: {
573-
key: string;
574-
etag: string;
575-
uploaded: string;
576-
version: string;
577-
size: string;
578-
};
579-
};
580-
return {
581-
key: body.result.key,
582-
etag: body.result.etag,
583-
uploaded: new Date(body.result.uploaded),
584-
version: body.result.version,
585-
size: Number(body.result.size),
586-
};
587-
},
588-
delete: async (key: string) => {
589-
if (isLocal) {
590-
await (await localBucket()).delete(key);
591-
}
592-
return deleteObject(api, {
593-
bucketName: bucket.name,
594-
key: key,
595-
});
457+
return makeAsyncProxyForBinding({
458+
apiOptions: props,
459+
name: id,
460+
binding: bucket as Omit<R2Bucket, keyof globalThis.R2Bucket>,
461+
properties: {
462+
createMultipartUpload: true,
463+
delete: true,
464+
get: true,
465+
head: true,
466+
list: true,
467+
put: true,
468+
resumeMultipartUpload: (promise) => (key: string, uploadId: string) =>
469+
makeAsyncProxy(
470+
{ key, uploadId },
471+
promise.then((bucket) => bucket.resumeMultipartUpload(key, uploadId)),
472+
{
473+
uploadPart: true,
474+
abort: true,
475+
complete: true,
476+
},
477+
),
596478
},
597-
};
479+
});
598480
}
599481

600-
const parseR2Object = (key: string, response: Response): R2ObjectContent => ({
601-
etag: response.headers.get("ETag")!,
602-
uploaded: parseDate(response.headers),
603-
key,
604-
size: Number(response.headers.get("Content-Length")),
605-
httpMetadata: mapHeadersToHttpMetadata(response.headers),
606-
arrayBuffer: () => response.arrayBuffer(),
607-
bytes: () => response.bytes(),
608-
text: () => response.text(),
609-
json: () => response.json(),
610-
blob: () => response.blob(),
611-
});
612-
613482
const parseDate = (headers: Headers) =>
614483
new Date(headers.get("Last-Modified") ?? headers.get("Date")!);
615484

@@ -1143,7 +1012,9 @@ export async function putBucketLifecycleRules(
11431012
api.put(
11441013
`/accounts/${api.accountId}/r2/buckets/${bucketName}/lifecycle`,
11451014
rulesBody,
1146-
{ headers: withJurisdiction(props) },
1015+
{
1016+
headers: withJurisdiction(props),
1017+
},
11471018
),
11481019
);
11491020
}
@@ -1158,7 +1029,9 @@ export async function getBucketLifecycleRules(
11581029
): Promise<R2BucketLifecycleRule[]> {
11591030
const res = await api.get(
11601031
`/accounts/${api.accountId}/r2/buckets/${bucketName}/lifecycle`,
1161-
{ headers: withJurisdiction(props) },
1032+
{
1033+
headers: withJurisdiction(props),
1034+
},
11621035
);
11631036
const json: any = await res.json();
11641037
if (!json?.success) {
@@ -1196,7 +1069,9 @@ export async function putBucketLockRules(
11961069
api.put(
11971070
`/accounts/${api.accountId}/r2/buckets/${bucketName}/lock`,
11981071
rulesBody,
1199-
{ headers: withJurisdiction(props) },
1072+
{
1073+
headers: withJurisdiction(props),
1074+
},
12001075
),
12011076
);
12021077
}
@@ -1211,7 +1086,9 @@ export async function getBucketLockRules(
12111086
): Promise<R2BucketLockRule[]> {
12121087
const res = await api.get(
12131088
`/accounts/${api.accountId}/r2/buckets/${bucketName}/lock`,
1214-
{ headers: withJurisdiction(props) },
1089+
{
1090+
headers: withJurisdiction(props),
1091+
},
12151092
);
12161093
const json: any = await res.json();
12171094
if (!json?.success) {

alchemy/src/cloudflare/d1-database.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import { cloneD1Database } from "./d1-clone.ts";
1313
import { applyLocalD1Migrations } from "./d1-local-migrations.ts";
1414
import { applyMigrations, listMigrationsFiles } from "./d1-migrations.ts";
1515
import { deleteMiniflareBinding } from "./miniflare/delete.ts";
16+
import {
17+
makeAsyncProxy,
18+
makeAsyncProxyForBinding,
19+
} from "./miniflare/node-binding.ts";
1620

1721
const DEFAULT_MIGRATIONS_TABLE = "d1_migrations";
1822

@@ -171,7 +175,7 @@ export type D1Database = Pick<
171175
* The jurisdiction of the database
172176
*/
173177
jurisdiction: D1DatabaseJurisdiction;
174-
};
178+
} & globalThis.D1Database;
175179

176180
/**
177181
* Creates and manages Cloudflare D1 Databases.
@@ -257,7 +261,7 @@ export async function D1Database(
257261
? await listMigrationsFiles(props.migrationsDir)
258262
: [];
259263

260-
return _D1Database(id, {
264+
const database = await _D1Database(id, {
261265
...props,
262266
migrationsFiles,
263267
dev: {
@@ -267,6 +271,57 @@ export async function D1Database(
267271
force: Scope.current.local,
268272
},
269273
});
274+
275+
function makePreparedStatementProxy(
276+
promise: Promise<D1PreparedStatement>,
277+
): D1PreparedStatement {
278+
return makeAsyncProxy({}, promise, {
279+
bind:
280+
(promise) =>
281+
(...args) =>
282+
makePreparedStatementProxy(
283+
promise.then((statement) => statement.bind(...args)),
284+
),
285+
first: true,
286+
run: true,
287+
all: true,
288+
raw: true,
289+
});
290+
}
291+
292+
return makeAsyncProxyForBinding({
293+
apiOptions: props,
294+
name: id,
295+
binding: database,
296+
properties: {
297+
prepare: (promise) => (query) =>
298+
makePreparedStatementProxy(
299+
promise.then((database) => database.prepare(query)),
300+
),
301+
batch: true,
302+
exec: true,
303+
withSession: (promise) => (constraintOrBookmark) =>
304+
makeAsyncProxy(
305+
{},
306+
promise.then((database) =>
307+
database.withSession(constraintOrBookmark),
308+
),
309+
{
310+
prepare: (session) => (query) =>
311+
makePreparedStatementProxy(
312+
session.then((session) => session.prepare(query)),
313+
),
314+
batch: true,
315+
getBookmark: () => () => {
316+
throw new Error(
317+
"D1DatabaseSession.getBookmark is not implemented",
318+
);
319+
},
320+
},
321+
),
322+
dump: true,
323+
},
324+
});
270325
}
271326

272327
const _D1Database = Resource(
@@ -275,7 +330,7 @@ const _D1Database = Resource(
275330
this: Context<D1Database>,
276331
id: string,
277332
props: D1DatabaseProps,
278-
): Promise<D1Database> {
333+
): Promise<Omit<D1Database, keyof globalThis.D1Database>> {
279334
const databaseName =
280335
props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
281336
const jurisdiction = props.jurisdiction ?? "default";

0 commit comments

Comments
 (0)