Skip to content

Commit

Permalink
implement getBindingsProxy utility (#4523)
Browse files Browse the repository at this point in the history
* implement getBindingsProxy utility

* add new get-bindings-proxy fixture to test the `getBindingsProxy` utility

---------

Co-authored-by: Pete Bacon Darwin <pete@bacondarwin.com>
  • Loading branch information
dario-piotrowicz and petebacondarwin committed Jan 19, 2024
1 parent b79e93a commit 9f96f28
Show file tree
Hide file tree
Showing 26 changed files with 736 additions and 36 deletions.
42 changes: 42 additions & 0 deletions .changeset/dull-jobs-deliver.md
@@ -0,0 +1,42 @@
---
"wrangler": minor
---

Add new `getBindingsProxy` utility to the wrangler package

The new utility is part of wrangler's JS API (it is not part of the wrangler CLI) and its use is to provide proxy objects to bindings, such objects can be used in Node.js code as if they were actual bindings

The utility reads the `wrangler.toml` file present in the current working directory in order to discern what bindings should be available (a `wrangler.json` file can be used too, as well as config files with custom paths).

## Example

Assuming that in the current working directory there is a `wrangler.toml` file with the following
content:

```
[[kv_namespaces]]
binding = "MY_KV"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

The utility could be used in a nodejs script in the following way:

```js
import { getBindingsProxy } from "wrangler";

// we use the utility to get the bindings proxies
const { bindings, dispose } = await getBindingsProxy();

// we get access to the KV binding proxy
const myKv = bindings.MY_KV;
// we can then use the proxy in the same exact way we'd use the
// KV binding in the workerd runtime, without any API discrepancies
const kvValue = await myKv.get("my-kv-key");

console.log(`
KV Value = ${kvValue}
`);

// we need to dispose of the underlying child process in order for this nodejs script to properly terminate
await dispose();
```
7 changes: 7 additions & 0 deletions fixtures/get-bindings-proxy/custom-toml/path/test-toml
@@ -0,0 +1,7 @@
name = "get-bindings-proxy-fixture"
main = "src/index.ts"
compatibility_date = "2023-11-21"

[vars]
MY_VAR = "my-var-value-from-a-custom-toml"
MY_JSON_VAR = { test = true, customToml = true }
15 changes: 15 additions & 0 deletions fixtures/get-bindings-proxy/package.json
@@ -0,0 +1,15 @@
{
"name": "get-bindings-proxy-fixture",
"private": true,
"description": "A test for the getBindingsProxy utility",
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"type:tests": "tsc --noEmit"
},
"devDependencies": {
"@cloudflare/workers-tsconfig": "workspace:*",
"@cloudflare/workers-types": "^4.20221111.1",
"wrangler": "workspace:*"
}
}
200 changes: 200 additions & 0 deletions fixtures/get-bindings-proxy/tests/index.test.ts
@@ -0,0 +1,200 @@
import { describe, expect, it, beforeAll, afterAll } from "vitest";
import {
type UnstableDevWorker,
unstable_dev,
getBindingsProxy,
} from "wrangler";
import {
R2Bucket,
type KVNamespace,
Fetcher,
D1Database,
DurableObjectNamespace,
} from "@cloudflare/workers-types";
import { readdir, rm } from "fs/promises";
import path from "path";

type Bindings = {
MY_VAR: string;
MY_JSON_VAR: Object;
MY_SERVICE_A: Fetcher;
MY_SERVICE_B: Fetcher;
MY_KV: KVNamespace;
MY_DO_A: DurableObjectNamespace;
MY_DO_B: DurableObjectNamespace;
MY_BUCKET: R2Bucket;
MY_D1: D1Database;
};

const wranglerTomlFilePath = path.join(__dirname, "..", "wrangler.toml");

describe("getBindingsProxy", () => {
let devWorkers: UnstableDevWorker[];

beforeAll(async () => {
devWorkers = await startWorkers();

await rm(path.join(__dirname, "..", ".wrangler"), {
force: true,
recursive: true,
});
});

afterAll(async () => {
await Promise.allSettled(devWorkers.map((i) => i.stop()));
});

it("correctly obtains var bindings", async () => {
const { bindings, dispose } = await getBindingsProxy<Bindings>({
configPath: wranglerTomlFilePath,
});
const { MY_VAR, MY_JSON_VAR } = bindings;
expect(MY_VAR).toEqual("my-var-value");
expect(MY_JSON_VAR).toEqual({
test: true,
});
await dispose();
});

it("correctly reads a toml from a custom path", async () => {
const { bindings, dispose } = await getBindingsProxy<Bindings>({
configPath: path.join(
__dirname,
"..",
"custom-toml",
"path",
"test-toml"
),
});
const { MY_VAR, MY_JSON_VAR } = bindings;
expect(MY_VAR).toEqual("my-var-value-from-a-custom-toml");
expect(MY_JSON_VAR).toEqual({
test: true,
customToml: true,
});
await dispose();
});

it("correctly reads a json config file", async () => {
const { bindings, dispose } = await getBindingsProxy<Bindings>({
configPath: path.join(__dirname, "..", "wrangler.json"),
});
const { MY_VAR, MY_JSON_VAR } = bindings;
expect(MY_VAR).toEqual("my-var-value-from-a-json-config-file");
expect(MY_JSON_VAR).toEqual({
test: true,
fromJson: true,
});
await dispose();
});

it("provides service bindings to external local workers", async () => {
const { bindings, dispose } = await getBindingsProxy<Bindings>({
configPath: wranglerTomlFilePath,
});
const { MY_SERVICE_A, MY_SERVICE_B } = bindings;
await testServiceBinding(MY_SERVICE_A, "Hello World from hello-worker-a");
await testServiceBinding(MY_SERVICE_B, "Hello World from hello-worker-b");
await dispose();
});

it("correctly obtains functioning KV bindings", async () => {
const { bindings, dispose } = await getBindingsProxy<Bindings>({
configPath: wranglerTomlFilePath,
});
const { MY_KV } = bindings;
let numOfKeys = (await MY_KV.list()).keys.length;
expect(numOfKeys).toBe(0);
await MY_KV.put("my-key", "my-value");
numOfKeys = (await MY_KV.list()).keys.length;
expect(numOfKeys).toBe(1);
const value = await MY_KV.get("my-key");
expect(value).toBe("my-value");
await dispose();
});

it("correctly obtains functioning DO bindings (provided by external local workers)", async () => {
const { bindings, dispose } = await getBindingsProxy<Bindings>({
configPath: wranglerTomlFilePath,
});
const { MY_DO_A, MY_DO_B } = bindings;
await testDoBinding(MY_DO_A, "Hello from DurableObject A");
await testDoBinding(MY_DO_B, "Hello from DurableObject B");
await dispose();
});

it("correctly obtains functioning R2 bindings", async () => {
const { bindings, dispose } = await getBindingsProxy<Bindings>({
configPath: wranglerTomlFilePath,
});
const { MY_BUCKET } = bindings;
let numOfObjects = (await MY_BUCKET.list()).objects.length;
expect(numOfObjects).toBe(0);
await MY_BUCKET.put("my-object", "my-value");
numOfObjects = (await MY_BUCKET.list()).objects.length;
expect(numOfObjects).toBe(1);
const value = await MY_BUCKET.get("my-object");
expect(await value?.text()).toBe("my-value");
await dispose();
});

it("correctly obtains functioning D1 bindings", async () => {
const { bindings, dispose } = await getBindingsProxy<Bindings>({
configPath: wranglerTomlFilePath,
});
const { MY_D1 } = bindings;
await MY_D1.exec(
`CREATE TABLE IF NOT EXISTS users ( id integer PRIMARY KEY AUTOINCREMENT, name text NOT NULL )`
);
const stmt = MY_D1.prepare("insert into users (name) values (?1)");
await MY_D1.batch([
stmt.bind("userA"),
stmt.bind("userB"),
stmt.bind("userC"),
]);
const { results } = await MY_D1.prepare(
"SELECT name FROM users LIMIT 5"
).all();
expect(results).toEqual([
{ name: "userA" },
{ name: "userB" },
{ name: "userC" },
]);
await dispose();
});
});

/**
* Starts all the workers present in the `workers` directory using `unstable_dev`
*
* @returns the workers' UnstableDevWorker instances
*/
async function startWorkers(): Promise<UnstableDevWorker[]> {
const workersDirPath = path.join(__dirname, "..", "workers");
const workers = await readdir(workersDirPath);
return await Promise.all(
workers.map((workerName) => {
const workerPath = path.join(workersDirPath, workerName);
return unstable_dev(path.join(workerPath, "index.ts"), {
config: path.join(workerPath, "wrangler.toml"),
});
})
);
}

async function testServiceBinding(binding: Fetcher, expectedResponse: string) {
const resp = await binding.fetch("http://0.0.0.0");
const respText = await resp.text();
expect(respText).toBe(expectedResponse);
}

async function testDoBinding(
binding: DurableObjectNamespace,
expectedResponse: string
) {
const durableObjectId = binding.idFromName("__my-do__");
const doStub = binding.get(durableObjectId);
const doResp = await doStub.fetch("http://0.0.0.0");
const doRespText = await doResp.text();
expect(doRespText).toBe(expectedResponse);
}
16 changes: 16 additions & 0 deletions fixtures/get-bindings-proxy/tsconfig.json
@@ -0,0 +1,16 @@
{
"include": [
"workers/hello-worker-a",
"module-worker-b",
"service-worker-a",
"module-worker-c",
"module-worker-d",
"pages-functions-app"
],
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"lib": ["ES2020"],
"types": ["@cloudflare/workers-types"]
}
}
10 changes: 10 additions & 0 deletions fixtures/get-bindings-proxy/vitest.config.ts
@@ -0,0 +1,10 @@
import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
testTimeout: 25_000,
hookTimeout: 25_000,
teardownTimeout: 25_000,
useAtomics: true,
},
});
11 changes: 11 additions & 0 deletions fixtures/get-bindings-proxy/workers/do-worker-a/index.ts
@@ -0,0 +1,11 @@
export default {
async fetch(): Promise<Response> {
return new Response("Hello World from do-worker-a");
},
};

export class DurableObjectClass {
async fetch() {
return new Response("Hello from DurableObject A");
}
}
5 changes: 5 additions & 0 deletions fixtures/get-bindings-proxy/workers/do-worker-a/wrangler.toml
@@ -0,0 +1,5 @@
name = "do-worker-a"

[[durable_objects.bindings]]
name = "MY_DO"
class_name = "DurableObjectClass"
11 changes: 11 additions & 0 deletions fixtures/get-bindings-proxy/workers/do-worker-b/index.ts
@@ -0,0 +1,11 @@
export default {
async fetch(): Promise<Response> {
return new Response("Hello World from do-worker-a");
},
};

export class DurableObjectClass {
async fetch() {
return new Response("Hello from DurableObject B");
}
}
5 changes: 5 additions & 0 deletions fixtures/get-bindings-proxy/workers/do-worker-b/wrangler.toml
@@ -0,0 +1,5 @@
name = "do-worker-b"

[[durable_objects.bindings]]
name = "MY_DO"
class_name = "DurableObjectClass"
5 changes: 5 additions & 0 deletions fixtures/get-bindings-proxy/workers/hello-worker-a/index.ts
@@ -0,0 +1,5 @@
export default {
fetch() {
return new Response("Hello World from hello-worker-a");
},
};
@@ -0,0 +1 @@
name = "hello-worker-a"
5 changes: 5 additions & 0 deletions fixtures/get-bindings-proxy/workers/hello-worker-b/index.ts
@@ -0,0 +1,5 @@
export default {
fetch() {
return new Response("Hello World from hello-worker-b");
},
};
@@ -0,0 +1 @@
name = "hello-worker-b"
11 changes: 11 additions & 0 deletions fixtures/get-bindings-proxy/wrangler.json
@@ -0,0 +1,11 @@
{
"name": "get-bindings-proxy-fixture",
"main": "src/index.ts",
"vars": {
"MY_VAR": "my-var-value-from-a-json-config-file",
"MY_JSON_VAR": {
"test": true,
"fromJson": true
}
}
}
31 changes: 31 additions & 0 deletions fixtures/get-bindings-proxy/wrangler.toml
@@ -0,0 +1,31 @@
name = "get-bindings-proxy-fixture"
main = "src/index.ts"
compatibility_date = "2023-11-21"

services = [
{ binding = "MY_SERVICE_A", service = "hello-worker-a" },
{ binding = "MY_SERVICE_B", service = "hello-worker-b" }
]

[vars]
MY_VAR = "my-var-value"
MY_JSON_VAR = { test = true }

[[kv_namespaces]]
binding = "MY_KV"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-bucket"

[durable_objects]
bindings = [
{ name = "MY_DO_A", script_name = "do-worker-a", class_name = "DurableObjectClass" },
{ name = "MY_DO_B", script_name = "do-worker-b", class_name = "DurableObjectClass" }
]

[[d1_databases]]
binding = "MY_D1"
database_name = "test-db"
database_id = "000000000-0000-0000-0000-000000000000"
1 change: 1 addition & 0 deletions packages/wrangler/src/api/index.ts
Expand Up @@ -10,3 +10,4 @@ export {
deleteMTlsCertificate,
} from "./mtls-certificate";
export * from "./startDevWorker";
export * from "./integrations";

0 comments on commit 9f96f28

Please sign in to comment.