From cc8ab99a92efd6bc566bd98353f440a02fda84ab Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Thu, 19 May 2022 15:04:58 +0200 Subject: [PATCH] Edge Cookies: Add `.getWithOptions` method (#36943) Hello, This is an iteration after first work at https://github.com/vercel/next.js/pull/36478. What that PR missed is a way to just get a cookie value. Well, this PR adds two new things: `cookies.get` returns the cookie value that could be `string | undefined`: ```js const response = new NextResponse() response.cookies.set('foo', 'bar', { path: '/test' }) const value = response.cookies.get('foo') console.log(value) // => 'bar' ``` Additionally, if you want to know all the cookie details, you can use `cookies.getWithOptions`: ```js const response = new NextResponse() response.cookies.set('foo', 'bar', { path: '/test' }) const { value, options } response.cookies.getWithOptions('foo') console.log(value) // => 'bar' console.log(options) // => { Path: '/test' } ``` --- .../next/server/web/spec-extension/cookies.ts | 25 +++++-- .../app/pages/rewrites/_middleware.ts | 3 - test/unit/web-runtime/next-cookies.test.ts | 71 +++++++++++++------ 3 files changed, 71 insertions(+), 28 deletions(-) diff --git a/packages/next/server/web/spec-extension/cookies.ts b/packages/next/server/web/spec-extension/cookies.ts index e1b06f646ddb..b167fd034f41 100644 --- a/packages/next/server/web/spec-extension/cookies.ts +++ b/packages/next/server/web/spec-extension/cookies.ts @@ -1,6 +1,11 @@ import cookie from 'next/dist/compiled/cookie' import { CookieSerializeOptions } from '../types' +type GetWithOptionsOutput = { + value: string | undefined + options: { [key: string]: string } +} + const normalizeCookieOptions = (options: CookieSerializeOptions) => { options = Object.assign({}, options) @@ -59,10 +64,22 @@ export class NextCookies extends Cookies { super(response.headers.get('cookie')) this.response = response } + get = (...args: Parameters) => { + return this.getWithOptions(...args).value + } + getWithOptions = ( + ...args: Parameters + ): GetWithOptionsOutput => { + const raw = super.get(...args) + if (typeof raw !== 'string') return { value: raw, options: {} } + const { [args[0]]: value, ...options } = cookie.parse(raw) + return { value, options } + } set = (...args: Parameters) => { const isAlreadyAdded = super.has(args[0]) - const store = super.set(...args) - const currentCookie = store.get(args[0]) + + super.set(...args) + const currentCookie = super.get(args[0]) if (typeof currentCookie !== 'string') { throw new Error( @@ -89,9 +106,9 @@ export class NextCookies extends Cookies { this.response.headers.append('set-cookie', currentCookie) } - return store + return this } - delete = (key: any, options: CookieSerializeOptions = {}) => { + delete = (key: string, options: CookieSerializeOptions = {}) => { const isDeleted = super.delete(key) if (isDeleted) { diff --git a/test/production/middleware-typescript/app/pages/rewrites/_middleware.ts b/test/production/middleware-typescript/app/pages/rewrites/_middleware.ts index 441e171245ab..402ce456671d 100644 --- a/test/production/middleware-typescript/app/pages/rewrites/_middleware.ts +++ b/test/production/middleware-typescript/app/pages/rewrites/_middleware.ts @@ -10,9 +10,6 @@ export const middleware: NextMiddleware = async function (request) { const response = NextResponse.rewrite(`/rewrites/${bucket}`) response.cookies.set('bucket', bucket, { maxAge: 10 }) return response - } else { - // check that `bucket` is type "string", not "any" - bucket.toUpperCase() } return NextResponse.rewrite(`/rewrites/${bucket}`) diff --git a/test/unit/web-runtime/next-cookies.test.ts b/test/unit/web-runtime/next-cookies.test.ts index 604819ecd0ce..4292608293e9 100644 --- a/test/unit/web-runtime/next-cookies.test.ts +++ b/test/unit/web-runtime/next-cookies.test.ts @@ -34,26 +34,30 @@ it('reflect .set into `set-cookie`', async () => { const response = new NextResponse() - response.cookies.set('foo', 'bar') - expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( - 'foo=bar; Path=/' - ) - expect(response.cookies.get('foo')).toBe('foo=bar; Path=/') - - response.cookies.set('foo', 'barz') - expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( - 'foo=barz; Path=/' - ) - expect(response.cookies.get('foo')).toBe('foo=barz; Path=/') - - response.cookies.set('fooz', 'barz') - expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( - 'foo=barz; Path=/, fooz=barz; Path=/' - ) + expect(response.cookies.get('foo')).toBe(undefined) + expect(response.cookies.getWithOptions('foo')).toEqual({ + value: undefined, + options: {}, + }) + + response.cookies + .set('foo', 'bar', { path: '/test' }) + .set('fooz', 'barz', { path: '/test2' }) + + expect(response.cookies.get('foo')).toBe('bar') + expect(response.cookies.get('fooz')).toBe('barz') + + expect(response.cookies.getWithOptions('foo')).toEqual({ + value: 'bar', + options: { Path: '/test' }, + }) + expect(response.cookies.getWithOptions('fooz')).toEqual({ + value: 'barz', + options: { Path: '/test2' }, + }) - response.cookies.set('foo', 'bar') expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( - 'foo=bar; Path=/, fooz=barz; Path=/' + 'foo=bar; Path=/test, fooz=barz; Path=/test2' ) }) @@ -68,13 +72,23 @@ it('reflect .delete into `set-cookie`', async () => { expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( 'foo=bar; Path=/' ) - expect(response.cookies.get('foo')).toBe('foo=bar; Path=/') + + expect(response.cookies.get('foo')).toBe('bar') + expect(response.cookies.getWithOptions('foo')).toEqual({ + value: 'bar', + options: { Path: '/' }, + }) response.cookies.set('fooz', 'barz') expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( 'foo=bar; Path=/, fooz=barz; Path=/' ) - expect(response.cookies.get('fooz')).toBe('fooz=barz; Path=/') + + expect(response.cookies.get('fooz')).toBe('barz') + expect(response.cookies.getWithOptions('fooz')).toEqual({ + value: 'barz', + options: { Path: '/' }, + }) const firstDelete = response.cookies.delete('foo') expect(firstDelete).toBe(true) @@ -83,13 +97,23 @@ it('reflect .delete into `set-cookie`', async () => { ) expect(response.cookies.get('foo')).toBe(undefined) + expect(response.cookies.getWithOptions('foo')).toEqual({ + value: undefined, + options: {}, + }) const secondDelete = response.cookies.delete('fooz') expect(secondDelete).toBe(true) + expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( 'fooz=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT, foo=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT' ) + expect(response.cookies.get('fooz')).toBe(undefined) + expect(response.cookies.getWithOptions('fooz')).toEqual({ + value: undefined, + options: {}, + }) expect(response.cookies.size).toBe(0) }) @@ -109,7 +133,12 @@ it('reflect .clear into `set-cookie`', async () => { expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe( 'foo=bar; Path=/' ) - expect(response.cookies.get('foo')).toBe('foo=bar; Path=/') + + expect(response.cookies.get('foo')).toBe('bar') + expect(response.cookies.getWithOptions('foo')).toEqual({ + value: 'bar', + options: { Path: '/' }, + }) response.cookies.set('fooz', 'barz') expect(Object.fromEntries(response.headers.entries())['set-cookie']).toBe(