From 670d06f6cc641c6dc28b01e0ee597286f4f100dc Mon Sep 17 00:00:00 2001 From: Sam Martin Date: Tue, 23 May 2023 22:27:57 +0200 Subject: [PATCH 1/6] Infer chained response types in Pipeline --- pkg/pipeline.ts | 67 +++++++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/pkg/pipeline.ts b/pkg/pipeline.ts index 2f365ab4..7f675d26 100644 --- a/pkg/pipeline.ts +++ b/pkg/pipeline.ts @@ -152,7 +152,10 @@ import { ZMScoreCommand } from "./commands/zmscore.ts"; import { HRandFieldCommand } from "./commands/hrandfield.ts"; import { ZDiffStoreCommand } from "./commands/zdiffstore.ts"; -type Chain = (command: Command) => Pipeline; +// Given a tuple of commands, returns a tuple of the response data of each command +type InferResponseData = { + [K in keyof T]: T[K] extends Command ? TData : unknown; +}; /** * Upstash REST API supports command pipelining to send multiple commands in @@ -182,7 +185,7 @@ type Chain = (command: Command) => Pipeline; * const res = await p.set("key","value").get("key").exec() * ``` * - * It's not possible to infer correct types with a dynamic pipeline, so you can + * Return types are inferred if all commands are chained, but you can still * override the response type manually: * ```ts * redis.pipeline() @@ -192,9 +195,9 @@ type Chain = (command: Command) => Pipeline; * * ``` */ -export class Pipeline { +export class Pipeline[] = []> { private client: Requester; - private commands: Command[]; + private commands: TCommands; private commandOptions?: CommandOptions; private multiExec: boolean; constructor(opts: { @@ -204,7 +207,7 @@ export class Pipeline { }) { this.client = opts.client; - this.commands = []; + this.commands = ([] as unknown) as TCommands; // the TCommands generic in the class definition is only used for carrying through chained command types and should never be explicitly set when instantiating the class this.commandOptions = opts.commandOptions; this.multiExec = opts.multiExec ?? false; } @@ -214,13 +217,16 @@ export class Pipeline { * * Returns an array with the results of all pipelined commands. * - * You can define a return type manually to make working in typescript easier + * If all commands are statically chained from start to finish, types are inferred. You can still define a return type manually if necessary though: * ```ts - * redis.pipeline().get("key").exec<[{ greeting: string }]>() + * const p = redis.pipeline() + * p.get("key") + * const result = p.exec<[{ greeting: string }]>() * ``` */ exec = async < - TCommandResults extends unknown[] = unknown[], + TCommandResults extends unknown[] = [] extends TCommands ? unknown[] + : InferResponseData, >(): Promise => { if (this.commands.length === 0) { throw new Error("Pipeline is empty"); @@ -245,12 +251,14 @@ export class Pipeline { }; /** - * Pushes a command into the pipelien and returns a chainable instance of the + * Pushes a command into the pipeline and returns a chainable instance of the * pipeline */ - private chain(command: Command): this { + private chain( + command: Command, + ): Pipeline<[...TCommands, Command]> { this.commands.push(command); - return this; + return this as any; // TS thinks we're returning Pipeline<[]> here, because we're not creating a new instance of the class, hence the cast } /** @@ -274,14 +282,18 @@ export class Pipeline { destinationKey: string, sourceKey: string, ...sourceKeys: string[] - ): Pipeline; - (op: "not", destinationKey: string, sourceKey: string): Pipeline; + ): Pipeline<[...TCommands, BitOpCommand]>; + ( + op: "not", + destinationKey: string, + sourceKey: string, + ): Pipeline<[...TCommands, BitOpCommand]>; } = ( op: "and" | "or" | "xor" | "not", destinationKey: string, sourceKey: string, ...sourceKeys: string[] - ): Pipeline => + ) => this.chain( new BitOpCommand( [op as any, destinationKey, sourceKey, ...sourceKeys], @@ -549,7 +561,7 @@ export class Pipeline { direction: "before" | "after", pivot: TData, value: TData, - ): Pipeline => + ) => this.chain( new LInsertCommand( [key, direction, pivot, value], @@ -1070,30 +1082,7 @@ export class Pipeline { /** * @see https://redis.io/commands/?group=json */ - get json(): { - arrappend: (...args: CommandArgs) => Pipeline; - arrindex: (...args: CommandArgs) => Pipeline; - arrinsert: (...args: CommandArgs) => Pipeline; - arrlen: (...args: CommandArgs) => Pipeline; - arrpop: (...args: CommandArgs) => Pipeline; - arrtrim: (...args: CommandArgs) => Pipeline; - clear: (...args: CommandArgs) => Pipeline; - del: (...args: CommandArgs) => Pipeline; - forget: (...args: CommandArgs) => Pipeline; - get: (...args: CommandArgs) => Pipeline; - mget: (...args: CommandArgs) => Pipeline; - numincrby: (...args: CommandArgs) => Pipeline; - nummultby: (...args: CommandArgs) => Pipeline; - objkeys: (...args: CommandArgs) => Pipeline; - objlen: (...args: CommandArgs) => Pipeline; - resp: (...args: CommandArgs) => Pipeline; - set: (...args: CommandArgs) => Pipeline; - strappend: (...args: CommandArgs) => Pipeline; - strlen: (...args: CommandArgs) => Pipeline; - toggle: (...args: CommandArgs) => Pipeline; - type: (...args: CommandArgs) => Pipeline; - } { - // For some reason we needed to define the types manually, otherwise Deno wouldn't build it + get json() { return { /** * @see https://redis.io/commands/json.arrappend From 4bf65d1ee1ca61d798e2d4d5d6190de6f3435164 Mon Sep 17 00:00:00 2001 From: chronark Date: Wed, 31 May 2023 12:10:21 +0200 Subject: [PATCH 2/6] test: fix build and test case --- cmd/build.ts | 2 +- examples/nextjs_edge/middleware.ts | 30 ----------------------- examples/nextjs_edge/package.json | 4 +-- examples/nextjs_edge/pages/api/counter.ts | 15 +++++++++--- examples/nextjs_edge/test.ts | 3 +-- pkg/commands/set.test.ts | 3 ++- version.ts | 2 +- 7 files changed, 18 insertions(+), 41 deletions(-) delete mode 100644 examples/nextjs_edge/middleware.ts diff --git a/cmd/build.ts b/cmd/build.ts index 3fbc00d0..4f0c7dbc 100644 --- a/cmd/build.ts +++ b/cmd/build.ts @@ -5,7 +5,7 @@ const outDir = "./dist"; await dnt.emptyDir(outDir); -const version = Deno.args.length > 0 ? Deno.args[0] : "development"; +const version = Deno.args.length > 0 ? Deno.args[0] : "v0.0.0"; Deno.writeFileSync( "version.ts", new TextEncoder().encode(`export const VERSION = "${version}"`), diff --git a/examples/nextjs_edge/middleware.ts b/examples/nextjs_edge/middleware.ts deleted file mode 100644 index 386e530e..00000000 --- a/examples/nextjs_edge/middleware.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NextResponse } from "next/server"; -import type { NextFetchEvent, NextRequest } from "next/server"; -import { Redis } from "@upstash/redis"; - -const redis = new Redis({ - url: process.env.UPSTASH_REDIS_REST_URL!, - token: process.env.UPSTASH_REDIS_REST_TOKEN!, -}); -export default async function middleware( - _request: NextRequest, - _event: NextFetchEvent, -): Promise { - const start = Date.now(); - - /** - * We're prefixing the key for our automated tests. - * This is to avoid collisions with other tests. - */ - const key = ["vercel", process.env.VERCEL_GIT_COMMIT_SHA, "nextjs_middleware"] - .join("_"); - - const counter = await redis.incr(key); - - console.log("Middleware", counter); - const res = NextResponse.next(); - res.headers.set("Counter", counter.toString()); - res.headers.set("Latency", (Date.now() - start).toString()); - - return res; -} diff --git a/examples/nextjs_edge/package.json b/examples/nextjs_edge/package.json index a326f83b..e8e405ef 100644 --- a/examples/nextjs_edge/package.json +++ b/examples/nextjs_edge/package.json @@ -7,8 +7,8 @@ "start": "next start" }, "dependencies": { - "@upstash/redis": "latest", - "next": "^13.0.5", + "@upstash/redis": "../../dist", + "next": "^13.4.4", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/nextjs_edge/pages/api/counter.ts b/examples/nextjs_edge/pages/api/counter.ts index d7a764b4..52267e17 100644 --- a/examples/nextjs_edge/pages/api/counter.ts +++ b/examples/nextjs_edge/pages/api/counter.ts @@ -1,6 +1,13 @@ -import { NextApiRequest, NextApiResponse } from "next"; +import { Redis } from "@upstash/redis"; +import { NextRequest, NextResponse } from "next/server"; -export default (_req: NextApiRequest, res: NextApiResponse) => { - res.status(200); - res.send("OK"); +export const config = { + runtime: "edge", +}; + +const redis = Redis.fromEnv(); + +export default async (_req: NextRequest) => { + const counter = await redis.incr("vercel_edge_counter"); + return NextResponse.json({ counter }); }; diff --git a/examples/nextjs_edge/test.ts b/examples/nextjs_edge/test.ts index 4ee1fb90..3161e674 100644 --- a/examples/nextjs_edge/test.ts +++ b/examples/nextjs_edge/test.ts @@ -11,8 +11,7 @@ Deno.test("works", async () => { const res = await fetch(url); assertEquals(res.status, 200); - const counterString = res.headers.get("Counter"); - const counter = parseInt(counterString!); + const { counter } = await res.json() as { counter: number }; assertEquals("number", typeof counter); assertEquals("OK", await res.text()); }); diff --git a/pkg/commands/set.test.ts b/pkg/commands/set.test.ts index eea4abe2..cc7a16bc 100644 --- a/pkg/commands/set.test.ts +++ b/pkg/commands/set.test.ts @@ -107,7 +107,8 @@ Deno.test("get with xx", async (t) => { const value = randomID(); await new SetCommand([key, old]).exec(client); - const res = await new SetCommand([key, value, { get: true, xx: true }]).exec(client); + const res = await new SetCommand([key, value, { get: true, xx: true }]) + .exec(client); assertEquals(res, old); }); }); diff --git a/version.ts b/version.ts index f205ce2a..1155b94a 100644 --- a/version.ts +++ b/version.ts @@ -1 +1 @@ -export const VERSION = "development" \ No newline at end of file +export const VERSION = "v0.0.0"; From 5c00c649ac6091f7d8e260c2def069e8b29a28dc Mon Sep 17 00:00:00 2001 From: chronark Date: Wed, 31 May 2023 12:23:24 +0200 Subject: [PATCH 3/6] test: update deps --- examples/cloudflare-workers-with-typescript/package.json | 2 +- examples/cloudflare-workers/package.json | 4 ++-- examples/nextjs_edge/test.ts | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/cloudflare-workers-with-typescript/package.json b/examples/cloudflare-workers-with-typescript/package.json index 542376e2..839b921c 100644 --- a/examples/cloudflare-workers-with-typescript/package.json +++ b/examples/cloudflare-workers-with-typescript/package.json @@ -12,6 +12,6 @@ "publish": "wrangler publish" }, "dependencies": { - "@upstash/redis": "latest" + "@upstash/redis": "../../dist" } } diff --git a/examples/cloudflare-workers/package.json b/examples/cloudflare-workers/package.json index 3fb485a7..7fec9ce3 100644 --- a/examples/cloudflare-workers/package.json +++ b/examples/cloudflare-workers/package.json @@ -9,9 +9,9 @@ "publish": "wrangler publish" }, "devDependencies": { - "wrangler": "^2.4.4" + "wrangler": "^2.20.0" }, "dependencies": { - "@upstash/redis": "latest" + "@upstash/redis": "link:../../dist" } } diff --git a/examples/nextjs_edge/test.ts b/examples/nextjs_edge/test.ts index 3161e674..795210bc 100644 --- a/examples/nextjs_edge/test.ts +++ b/examples/nextjs_edge/test.ts @@ -13,5 +13,4 @@ Deno.test("works", async () => { assertEquals(res.status, 200); const { counter } = await res.json() as { counter: number }; assertEquals("number", typeof counter); - assertEquals("OK", await res.text()); }); From ec7b6fee6ab6d5f4aa339c649b52b6b4ee2d7d0d Mon Sep 17 00:00:00 2001 From: chronark Date: Wed, 31 May 2023 12:33:05 +0200 Subject: [PATCH 4/6] ci: set environment for cf workers --- .github/workflows/tests.yaml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 04f123a8..c398bfb1 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -466,8 +466,12 @@ jobs: pnpm add -g wrangler working-directory: examples/cloudflare-workers - - name: Add account ID - run: echo 'account_id = "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}"' >> wrangler.toml + - name: Add environment + run: | + echo 'account_id = "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}"' >> wrangler.toml + echo '[vars]' >> wrangler.toml + echo 'UPSTASH_REDIS_REST_URL = "${{ secrets.UPSTASH_REDIS_REST_URL }}"' >> ./wrangler.toml + echo 'UPSTASH_REDIS_REST_TOKEN = "${{ secrets.UPSTASH_REDIS_REST_TOKEN }}"' >> ./wrangler.toml working-directory: examples/cloudflare-workers - name: Start example @@ -554,8 +558,14 @@ jobs: working-directory: examples/cloudflare-workers-with-typescript - - name: Add account ID - run: echo 'account_id = "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}"' >> wrangler.toml + - name: Add environment + run: | + echo 'account_id = "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}"' >> wrangler.toml + - name: add environment + run: | + echo '[vars]' >> wrangler.toml + echo 'UPSTASH_REDIS_REST_URL = "${{ secrets.UPSTASH_REDIS_REST_URL }}"' >> ./wrangler.toml + echo 'UPSTASH_REDIS_REST_TOKEN = "${{ secrets.UPSTASH_REDIS_REST_TOKEN }}"' >> ./wrangler.toml working-directory: examples/cloudflare-workers-with-typescript - name: Start example From 642083842b05a39b417c7a6be851c5c70fa26c3f Mon Sep 17 00:00:00 2001 From: chronark Date: Wed, 31 May 2023 12:38:19 +0200 Subject: [PATCH 5/6] refactor(tests.yaml): remove redundant job name 'add environment' fix(tests.yaml): fix indentation of 'run' command in 'Add environment' job --- .github/workflows/tests.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c398bfb1..18aecd2a 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -560,9 +560,7 @@ jobs: - name: Add environment run: | - echo 'account_id = "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}"' >> wrangler.toml - - name: add environment - run: | + echo 'account_id = "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}"' >> wrangler.toml echo '[vars]' >> wrangler.toml echo 'UPSTASH_REDIS_REST_URL = "${{ secrets.UPSTASH_REDIS_REST_URL }}"' >> ./wrangler.toml echo 'UPSTASH_REDIS_REST_TOKEN = "${{ secrets.UPSTASH_REDIS_REST_TOKEN }}"' >> ./wrangler.toml From c27c5a2a2b6564d05b506a579776faabb39cccc8 Mon Sep 17 00:00:00 2001 From: chronark Date: Wed, 31 May 2023 12:43:18 +0200 Subject: [PATCH 6/6] ci(tests.yaml): add push event to trigger workflow on main branch --- .github/workflows/tests.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 18aecd2a..982fcc20 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,5 +1,8 @@ name: Tests on: + push: + branches: + - main pull_request: schedule: - cron: "0 0 * * *" # daily