Skip to content

Commit ab36634

Browse files
chronarksmartinio
andauthored
infer chained pipeline responses (#355)
* Infer chained response types in Pipeline * test: fix build and test case * test: update deps * ci: set environment for cf workers * refactor(tests.yaml): remove redundant job name 'add environment' fix(tests.yaml): fix indentation of 'run' command in 'Add environment' job * ci(tests.yaml): add push event to trigger workflow on main branch --------- Co-authored-by: Sam Martin <[email protected]>
1 parent 6d83184 commit ab36634

File tree

11 files changed

+64
-88
lines changed

11 files changed

+64
-88
lines changed

.github/workflows/tests.yaml

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
name: Tests
22
on:
3+
push:
4+
branches:
5+
- main
36
pull_request:
47
schedule:
58
- cron: "0 0 * * *" # daily
@@ -466,8 +469,12 @@ jobs:
466469
pnpm add -g wrangler
467470
working-directory: examples/cloudflare-workers
468471

469-
- name: Add account ID
470-
run: echo 'account_id = "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}"' >> wrangler.toml
472+
- name: Add environment
473+
run: |
474+
echo 'account_id = "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}"' >> wrangler.toml
475+
echo '[vars]' >> wrangler.toml
476+
echo 'UPSTASH_REDIS_REST_URL = "${{ secrets.UPSTASH_REDIS_REST_URL }}"' >> ./wrangler.toml
477+
echo 'UPSTASH_REDIS_REST_TOKEN = "${{ secrets.UPSTASH_REDIS_REST_TOKEN }}"' >> ./wrangler.toml
471478
working-directory: examples/cloudflare-workers
472479

473480
- name: Start example
@@ -554,8 +561,12 @@ jobs:
554561
555562
working-directory: examples/cloudflare-workers-with-typescript
556563

557-
- name: Add account ID
558-
run: echo 'account_id = "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}"' >> wrangler.toml
564+
- name: Add environment
565+
run: |
566+
echo 'account_id = "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}"' >> wrangler.toml
567+
echo '[vars]' >> wrangler.toml
568+
echo 'UPSTASH_REDIS_REST_URL = "${{ secrets.UPSTASH_REDIS_REST_URL }}"' >> ./wrangler.toml
569+
echo 'UPSTASH_REDIS_REST_TOKEN = "${{ secrets.UPSTASH_REDIS_REST_TOKEN }}"' >> ./wrangler.toml
559570
working-directory: examples/cloudflare-workers-with-typescript
560571

561572
- name: Start example

cmd/build.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const outDir = "./dist";
55

66
await dnt.emptyDir(outDir);
77

8-
const version = Deno.args.length > 0 ? Deno.args[0] : "development";
8+
const version = Deno.args.length > 0 ? Deno.args[0] : "v0.0.0";
99
Deno.writeFileSync(
1010
"version.ts",
1111
new TextEncoder().encode(`export const VERSION = "${version}"`),

examples/cloudflare-workers-with-typescript/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
"publish": "wrangler publish"
1313
},
1414
"dependencies": {
15-
"@upstash/redis": "latest"
15+
"@upstash/redis": "../../dist"
1616
}
1717
}

examples/cloudflare-workers/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
"publish": "wrangler publish"
1010
},
1111
"devDependencies": {
12-
"wrangler": "^2.4.4"
12+
"wrangler": "^2.20.0"
1313
},
1414
"dependencies": {
15-
"@upstash/redis": "latest"
15+
"@upstash/redis": "link:../../dist"
1616
}
1717
}

examples/nextjs_edge/middleware.ts

Lines changed: 0 additions & 30 deletions
This file was deleted.

examples/nextjs_edge/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
"start": "next start"
88
},
99
"dependencies": {
10-
"@upstash/redis": "latest",
11-
"next": "^13.0.5",
10+
"@upstash/redis": "../../dist",
11+
"next": "^13.4.4",
1212
"react": "^18.2.0",
1313
"react-dom": "^18.2.0"
1414
},
Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
import { NextApiRequest, NextApiResponse } from "next";
1+
import { Redis } from "@upstash/redis";
2+
import { NextRequest, NextResponse } from "next/server";
23

3-
export default (_req: NextApiRequest, res: NextApiResponse) => {
4-
res.status(200);
5-
res.send("OK");
4+
export const config = {
5+
runtime: "edge",
6+
};
7+
8+
const redis = Redis.fromEnv();
9+
10+
export default async (_req: NextRequest) => {
11+
const counter = await redis.incr("vercel_edge_counter");
12+
return NextResponse.json({ counter });
613
};

examples/nextjs_edge/test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ Deno.test("works", async () => {
1111

1212
const res = await fetch(url);
1313
assertEquals(res.status, 200);
14-
const counterString = res.headers.get("Counter");
15-
const counter = parseInt(counterString!);
14+
const { counter } = await res.json() as { counter: number };
1615
assertEquals("number", typeof counter);
17-
assertEquals("OK", await res.text());
1816
});

pkg/commands/set.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ Deno.test("get with xx", async (t) => {
107107
const value = randomID();
108108
await new SetCommand([key, old]).exec(client);
109109

110-
const res = await new SetCommand([key, value, { get: true, xx: true }]).exec(client);
110+
const res = await new SetCommand([key, value, { get: true, xx: true }])
111+
.exec(client);
111112
assertEquals(res, old);
112113
});
113114
});

pkg/pipeline.ts

Lines changed: 28 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,10 @@ import { ZMScoreCommand } from "./commands/zmscore.ts";
152152
import { HRandFieldCommand } from "./commands/hrandfield.ts";
153153
import { ZDiffStoreCommand } from "./commands/zdiffstore.ts";
154154

155-
type Chain = <T>(command: Command<any, T>) => Pipeline;
155+
// Given a tuple of commands, returns a tuple of the response data of each command
156+
type InferResponseData<T extends unknown[]> = {
157+
[K in keyof T]: T[K] extends Command<any, infer TData> ? TData : unknown;
158+
};
156159

157160
/**
158161
* Upstash REST API supports command pipelining to send multiple commands in
@@ -182,7 +185,7 @@ type Chain = <T>(command: Command<any, T>) => Pipeline;
182185
* const res = await p.set("key","value").get("key").exec()
183186
* ```
184187
*
185-
* It's not possible to infer correct types with a dynamic pipeline, so you can
188+
* Return types are inferred if all commands are chained, but you can still
186189
* override the response type manually:
187190
* ```ts
188191
* redis.pipeline()
@@ -192,9 +195,9 @@ type Chain = <T>(command: Command<any, T>) => Pipeline;
192195
*
193196
* ```
194197
*/
195-
export class Pipeline {
198+
export class Pipeline<TCommands extends Command<any, any>[] = []> {
196199
private client: Requester;
197-
private commands: Command<unknown, unknown>[];
200+
private commands: TCommands;
198201
private commandOptions?: CommandOptions<any, any>;
199202
private multiExec: boolean;
200203
constructor(opts: {
@@ -204,7 +207,7 @@ export class Pipeline {
204207
}) {
205208
this.client = opts.client;
206209

207-
this.commands = [];
210+
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
208211
this.commandOptions = opts.commandOptions;
209212
this.multiExec = opts.multiExec ?? false;
210213
}
@@ -214,13 +217,16 @@ export class Pipeline {
214217
*
215218
* Returns an array with the results of all pipelined commands.
216219
*
217-
* You can define a return type manually to make working in typescript easier
220+
* If all commands are statically chained from start to finish, types are inferred. You can still define a return type manually if necessary though:
218221
* ```ts
219-
* redis.pipeline().get("key").exec<[{ greeting: string }]>()
222+
* const p = redis.pipeline()
223+
* p.get("key")
224+
* const result = p.exec<[{ greeting: string }]>()
220225
* ```
221226
*/
222227
exec = async <
223-
TCommandResults extends unknown[] = unknown[],
228+
TCommandResults extends unknown[] = [] extends TCommands ? unknown[]
229+
: InferResponseData<TCommands>,
224230
>(): Promise<TCommandResults> => {
225231
if (this.commands.length === 0) {
226232
throw new Error("Pipeline is empty");
@@ -245,12 +251,14 @@ export class Pipeline {
245251
};
246252

247253
/**
248-
* Pushes a command into the pipelien and returns a chainable instance of the
254+
* Pushes a command into the pipeline and returns a chainable instance of the
249255
* pipeline
250256
*/
251-
private chain<T>(command: Command<any, T>): this {
257+
private chain<T>(
258+
command: Command<any, T>,
259+
): Pipeline<[...TCommands, Command<any, T>]> {
252260
this.commands.push(command);
253-
return this;
261+
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
254262
}
255263

256264
/**
@@ -274,14 +282,18 @@ export class Pipeline {
274282
destinationKey: string,
275283
sourceKey: string,
276284
...sourceKeys: string[]
277-
): Pipeline;
278-
(op: "not", destinationKey: string, sourceKey: string): Pipeline;
285+
): Pipeline<[...TCommands, BitOpCommand]>;
286+
(
287+
op: "not",
288+
destinationKey: string,
289+
sourceKey: string,
290+
): Pipeline<[...TCommands, BitOpCommand]>;
279291
} = (
280292
op: "and" | "or" | "xor" | "not",
281293
destinationKey: string,
282294
sourceKey: string,
283295
...sourceKeys: string[]
284-
): Pipeline =>
296+
) =>
285297
this.chain(
286298
new BitOpCommand(
287299
[op as any, destinationKey, sourceKey, ...sourceKeys],
@@ -549,7 +561,7 @@ export class Pipeline {
549561
direction: "before" | "after",
550562
pivot: TData,
551563
value: TData,
552-
): Pipeline =>
564+
) =>
553565
this.chain(
554566
new LInsertCommand<TData>(
555567
[key, direction, pivot, value],
@@ -1070,30 +1082,7 @@ export class Pipeline {
10701082
/**
10711083
* @see https://redis.io/commands/?group=json
10721084
*/
1073-
get json(): {
1074-
arrappend: (...args: CommandArgs<typeof JsonArrAppendCommand>) => Pipeline;
1075-
arrindex: (...args: CommandArgs<typeof JsonArrIndexCommand>) => Pipeline;
1076-
arrinsert: (...args: CommandArgs<typeof JsonArrInsertCommand>) => Pipeline;
1077-
arrlen: (...args: CommandArgs<typeof JsonArrLenCommand>) => Pipeline;
1078-
arrpop: (...args: CommandArgs<typeof JsonArrPopCommand>) => Pipeline;
1079-
arrtrim: (...args: CommandArgs<typeof JsonArrTrimCommand>) => Pipeline;
1080-
clear: (...args: CommandArgs<typeof JsonClearCommand>) => Pipeline;
1081-
del: (...args: CommandArgs<typeof JsonDelCommand>) => Pipeline;
1082-
forget: (...args: CommandArgs<typeof JsonForgetCommand>) => Pipeline;
1083-
get: (...args: CommandArgs<typeof JsonGetCommand>) => Pipeline;
1084-
mget: (...args: CommandArgs<typeof JsonMGetCommand>) => Pipeline;
1085-
numincrby: (...args: CommandArgs<typeof JsonNumIncrByCommand>) => Pipeline;
1086-
nummultby: (...args: CommandArgs<typeof JsonNumMultByCommand>) => Pipeline;
1087-
objkeys: (...args: CommandArgs<typeof JsonObjKeysCommand>) => Pipeline;
1088-
objlen: (...args: CommandArgs<typeof JsonObjLenCommand>) => Pipeline;
1089-
resp: (...args: CommandArgs<typeof JsonRespCommand>) => Pipeline;
1090-
set: (...args: CommandArgs<typeof JsonSetCommand>) => Pipeline;
1091-
strappend: (...args: CommandArgs<typeof JsonStrAppendCommand>) => Pipeline;
1092-
strlen: (...args: CommandArgs<typeof JsonStrLenCommand>) => Pipeline;
1093-
toggle: (...args: CommandArgs<typeof JsonToggleCommand>) => Pipeline;
1094-
type: (...args: CommandArgs<typeof JsonTypeCommand>) => Pipeline;
1095-
} {
1096-
// For some reason we needed to define the types manually, otherwise Deno wouldn't build it
1085+
get json() {
10971086
return {
10981087
/**
10991088
* @see https://redis.io/commands/json.arrappend

version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const VERSION = "development"
1+
export const VERSION = "v0.0.0";

0 commit comments

Comments
 (0)