diff --git a/src/convenience/session.ts b/src/convenience/session.ts index c5b8e7bc..5d767116 100644 --- a/src/convenience/session.ts +++ b/src/convenience/session.ts @@ -118,6 +118,15 @@ export interface SessionOptions { * session data. */ initial?: () => S; + /** + * An optional prefix to prepend to the session key after it was generated. + * + * This makes it easier to store session data under a namespace. You can + * technically achieve the same functionality by returning an already + * prefixed key from `getSessionKey`. This option is merely more convenient, + * as it does not require you to think about session key generation. + */ + prefix?: string; /** * This option lets you generate your own session keys per context object. * The session key determines how to map the different session objects to @@ -424,7 +433,12 @@ class PropertySession { } function fillDefaults(opts: SessionOptions = {}) { - let { getSessionKey = defaultGetSessionKey, initial, storage } = opts; + let { + prefix = "", + getSessionKey = defaultGetSessionKey, + initial, + storage, + } = opts; if (storage == null) { debug( "Storing session data in memory, all data will be lost when the bot restarts.", @@ -432,7 +446,15 @@ function fillDefaults(opts: SessionOptions = {}) { storage = new MemorySessionStorage(); } const custom = getSessionKey !== defaultGetSessionKey; - return { initial, storage, getSessionKey, custom }; + return { + initial, + storage, + getSessionKey: async (ctx: C) => { + const key = await getSessionKey(ctx); + return key === undefined ? undefined : prefix + key; + }, + custom, + }; } /** Stores session data per chat by default */ diff --git a/test/convenience/session.test.ts b/test/convenience/session.test.ts index 8f9108ff..b412e7ae 100644 --- a/test/convenience/session.test.ts +++ b/test/convenience/session.test.ts @@ -126,6 +126,50 @@ describe("session", () => { assertEquals(storage.delete.calls[0].args, ["42"]); }); + it("should work with custom session keys", async () => { + let val: number | undefined = 0; + const storage = { + read: spy((_key: string) => val), + write: spy((_key: string, value: number) => { + val = value; + }), + delete: spy((_key: string) => { + val = undefined; + }), + }; + type C = Context & SessionFlavor; + const composer = new Composer(); + let ctx = { chatId: 42 } as C; + composer.use( + session({ + prefix: "xyz-", + getSessionKey: (ctx) => + ctx.chatId ? (ctx.chatId ** 2).toString() : undefined, + storage, + }), + ) + .use((ctx) => { + if (ctx.session === 0) ctx.session = 1; + else ctx.session = null; + }); + + await composer.middleware()(ctx, next); + ctx = { chatId: 42 } as C; + await composer.middleware()(ctx, next); + + assertEquals(storage.read.calls.length, 2); + assertEquals(storage.read.calls[0].args, ["xyz-1764"]); + assertEquals(storage.read.calls[1].args, ["xyz-1764"]); + assertEquals(storage.read.calls[0].returned, 0); + assertEquals(storage.read.calls[1].returned, 1); + + assertEquals(storage.write.calls.length, 1); + assertEquals(storage.write.calls[0].args, ["xyz-1764", 1]); + + assertEquals(storage.delete.calls.length, 1); + assertEquals(storage.delete.calls[0].args, ["xyz-1764"]); + }); + it("should do IO with objects", async () => { let val: Record | undefined; const storage = {