Skip to content

Commit

Permalink
feat: edge fn cookies, expose types (QwikDev#1937)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamdbradley authored Oct 31, 2022
1 parent 4fe2a4d commit ef638ba
Show file tree
Hide file tree
Showing 23 changed files with 191 additions and 128 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ target
*.node
todo-express/
qwik-app/
**/server/**/*.js
**/server/
.idea
.eslintcache

Expand Down
1 change: 0 additions & 1 deletion packages/qwik-city/middleware/cloudflare-pages/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
```ts

import type { Cookie as Cookie_2 } from 'packages/qwik-city/middleware/request-handler/cookie';
import type { Render } from '@builder.io/qwik/server';
import type { RenderOptions } from '@builder.io/qwik/server';
import type { RenderOptions as RenderOptions_2 } from '@builder.io/qwik';
Expand Down
5 changes: 5 additions & 0 deletions packages/qwik-city/middleware/cloudflare-pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ export function createQwikCity(opts: QwikCityCloudflarePagesOptions) {
let flushedHeaders = false;
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();

for (const cookieHeader in cookie.headers()) {
headers.append('Set-Cookie', cookieHeader);
}

const response = new Response(readable, { status, headers });

body({
Expand Down
1 change: 0 additions & 1 deletion packages/qwik-city/middleware/netlify-edge/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
```ts

import type { Context } from '@netlify/edge-functions';
import type { Cookie as Cookie_2 } from 'packages/qwik-city/middleware/request-handler/cookie';
import type { Render } from '@builder.io/qwik/server';
import type { RenderOptions } from '@builder.io/qwik/server';
import type { RenderOptions as RenderOptions_2 } from '@builder.io/qwik';
Expand Down
5 changes: 5 additions & 0 deletions packages/qwik-city/middleware/netlify-edge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export function createQwikCity(opts: QwikCityNetlifyOptions) {
let flushedHeaders = false;
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();

for (const cookieHeader in cookie.headers()) {
headers.append('Set-Cookie', cookieHeader);
}

const response = new Response(readable, { status, headers });

body({
Expand Down
1 change: 0 additions & 1 deletion packages/qwik-city/middleware/node/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

/// <reference types="node" />

import type { Cookie as Cookie_2 } from 'packages/qwik-city/middleware/request-handler/cookie';
import type { IncomingMessage } from 'node:http';
import type { Render } from '@builder.io/qwik/server';
import type { RenderOptions } from '@builder.io/qwik/server';
Expand Down
104 changes: 47 additions & 57 deletions packages/qwik-city/middleware/request-handler/cookie.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,7 @@
/**
* @alpha
*/
export interface CookieOptions {
domain?: string;
expires?: Date | string;
httpOnly?: boolean;
maxAge?: number | [number, keyof typeof UNIT];
path?: string;
sameSite?: keyof typeof SAMESITE;
secure?: boolean;
}

/**
* @alpha
*/
export interface CookieValue {
value: string;
json: <T = unknown>() => T;
number: () => number;
}
import type {
Cookie as CookieInterface,
CookieOptions,
} from '../../middleware/request-handler/types';

const SAMESITE = {
lax: 'Lax',
Expand All @@ -34,75 +17,76 @@ const UNIT = {
weeks: 1 * 60 * 60 * 24 * 7,
};

const handleOptions = (options: CookieOptions): string[] => {
const opts: string[] = [];
const createCookie = (cookieName: string, cookieValue: string, options: CookieOptions) => {
const c: string[] = [`${cookieName}=${cookieValue}`];

if (options.domain) {
opts.push(`Domain=${options.domain}`);
c.push(`Domain=${options.domain}`);
}

if (options.expires) {
const resolvedValue =
typeof options.expires === 'number' || typeof options.expires == 'string'
? options.expires
: options.expires.toUTCString();
opts.push(`Expires=${resolvedValue}`);
c.push(`Expires=${resolvedValue}`);
1;
}

if (options.httpOnly) {
opts.push('HttpOnly');
c.push('HttpOnly');
}

if (options.maxAge) {
const resolvedValue =
typeof options.maxAge === 'number'
? options.maxAge
: options.maxAge[0] * UNIT[options.maxAge[1]];
opts.push(`MaxAge=${resolvedValue}`);
c.push(`MaxAge=${resolvedValue}`);
}

if (options.path) {
opts.push(`Path=${options.path}`);
c.push(`Path=${options.path}`);
}

if (options.sameSite) {
opts.push(`SameSite=${SAMESITE[options.sameSite]}`);
c.push(`SameSite=${SAMESITE[options.sameSite]}`);
}

if (options.secure) {
opts.push('Secure');
c.push('Secure');
}

return opts;
return c.join('; ');
};

const createCookie = (name: string, value: string, options: CookieOptions = {}) => {
return [`${name}=${value}`, ...handleOptions(options)].join('; ');
};

const parseCookieString = (cookieString: string) => {
if (cookieString === '') {
return {};
const parseCookieString = (cookieString: string | undefined | null) => {
const cookie: Record<string, string> = {};
if (typeof cookieString === 'string' && cookieString !== '') {
const cookies = cookieString.split(';');
for (const cookieSegment of cookies) {
const cookieSplit = cookieSegment.split('=');
const cookieName = decodeURIComponent(cookieSplit[0].trim());
const cookieValue = decodeURIComponent(cookieSplit[1].trim());
cookie[cookieName] = cookieValue;
}
}
return cookieString.split(';').reduce((prev: Record<string, string>, cookie_value) => {
const split = cookie_value.split('=');
prev[decodeURIComponent(split[0].trim())] = decodeURIComponent(split[1].trim());
return prev;
}, {});
return cookie;
};

export class Cookie {
private _cookie: Record<string, string>;
private _headers: Record<string, string> = {};
const COOKIES = Symbol('cookies');
const HEADERS = Symbol('headers');

export class Cookie implements CookieInterface {
private [COOKIES]: Record<string, string>;
private [HEADERS]: Record<string, string> = {};

constructor(cookieString: string) {
const parsedCookie: Record<string, string> = parseCookieString(cookieString);
this._cookie = parsedCookie;
constructor(cookieString?: string | undefined | null) {
this[COOKIES] = parseCookieString(cookieString);
}

get(name: string): CookieValue | null {
const value = this._cookie[name];
get(cookieName: string) {
const value = this[COOKIES][cookieName];
if (!value) {
return null;
}
Expand All @@ -117,22 +101,28 @@ export class Cookie {
};
}

set(name: string, value: string | number | Record<string, any>, options: CookieOptions = {}) {
set(
cookieName: string,
cookieValue: string | number | Record<string, any>,
options: CookieOptions = {}
) {
const resolvedValue =
typeof value === 'string' ? value : encodeURIComponent(JSON.stringify(value));
this._headers[name] = createCookie(name, resolvedValue, options);
typeof cookieValue === 'string'
? cookieValue
: encodeURIComponent(JSON.stringify(cookieValue));
this[HEADERS][cookieName] = createCookie(cookieName, resolvedValue, options);
}

has(name: string) {
return !!this._cookie[name];
has(cookieName: string) {
return !!this[COOKIES][cookieName];
}

delete(name: string) {
this.set(name, 'deleted', { expires: new Date(0) });
}

*headers(): IterableIterator<string> {
for (const header of Object.values(this._headers)) {
for (const header of Object.values(this[HEADERS])) {
yield header;
}
}
Expand Down
3 changes: 2 additions & 1 deletion packages/qwik-city/middleware/request-handler/cookie.unit.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { test } from 'uvu';
import { equal } from 'uvu/assert';
import { Cookie, CookieOptions } from './cookie';
import { Cookie } from './cookie';
import type { CookieOptions } from './types';

export interface TestData {
key: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { Cookie } from './cookie';
import { createHeaders } from './headers';
import { HttpStatus } from './http-status-codes';
import type { QwikCityRequestContext } from './types';
import type { Cookie as CookieInterface, QwikCityRequestContext } from './types';

export class RedirectResponse {
public status: number;
public headers: Headers;
public cookie: Cookie;
public cookie: CookieInterface;
public location: string;

constructor(public url: string, status?: number, headers?: Headers, cookie?: Cookie) {
constructor(public url: string, status?: number, headers?: Headers, cookie?: CookieInterface) {
this.location = url;
this.status = isRedirectStatus(status) ? status : HttpStatus.Found;
this.headers = headers || createHeaders();
this.headers.set('Location', this.location);
this.headers.delete('Cache-Control');
this.cookie = cookie || new Cookie('');
this.cookie = cookie || new Cookie();
}
}

Expand Down
34 changes: 33 additions & 1 deletion packages/qwik-city/middleware/request-handler/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { StreamWriter } from '@builder.io/qwik';
import type { Render, RenderOptions } from '@builder.io/qwik/server';
import type { Cookie } from './cookie';
import type {
ClientPageData,
QwikCityPlan,
Expand Down Expand Up @@ -47,3 +46,36 @@ export interface QwikCityHandlerOptions extends RenderOptions {
render: Render;
qwikCityPlan: QwikCityPlan;
}

/**
* @alpha
*/
export interface Cookie {
get(name: string): CookieValue | null;
set(name: string, value: string | number | Record<string, any>, options?: CookieOptions): void;
has(name: string): boolean;
delete(name: string): void;
headers(): IterableIterator<string>;
}

/**
* @alpha
*/
export interface CookieOptions {
domain?: string;
expires?: Date | string;
httpOnly?: boolean;
maxAge?: number | [number, 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks'];
path?: string;
sameSite?: 'strict' | 'lax' | 'none';
secure?: boolean;
}

/**
* @alpha
*/
export interface CookieValue {
value: string;
json: <T = unknown>() => T;
number: () => number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export async function loadUserResponse(
headers: createHeaders(),
resolvedBody: undefined,
pendingBody: undefined,
cookie: new Cookie(requestCtx.request.headers.get('cookie') || ''),
cookie: new Cookie(request.headers.get('cookie')),
aborted: false,
};

Expand Down Expand Up @@ -71,7 +71,7 @@ export async function loadUserResponse(
};

const redirect = (url: string, status?: number) => {
return new RedirectResponse(url, status, userResponse.headers);
return new RedirectResponse(url, status, userResponse.headers, userResponse.cookie);
};

const error = (status: number, message?: string) => {
Expand Down
8 changes: 3 additions & 5 deletions packages/qwik-city/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,9 @@
"start": "cd runtime && node --inspect ../../../node_modules/vite/bin/vite.js",
"dev.ssr": "cd runtime && node --inspect ../../../node_modules/vite/bin/vite.js --mode ssr",
"dev.debug": "cd runtime && node --inspect-brk ../../../node_modules/vite/bin/vite.js --mode ssr --force",
"build": "yarn build.client && yarn build.server && yarn build.static && yarn ssg",
"build.client": "cd runtime && vite build --config vite-app.config.ts",
"build.server": "cd runtime && vite build --config vite-app.config.ts --ssr src/entry.express.tsx",
"build.static": "cd runtime && vite build --config vite-app.config.ts --ssr src/entry.static.tsx",
"ssg": "cd runtime && node server/entry.static.js",
"build": "yarn build.client && yarn build.server",
"build.client": "cd runtime && vite build -c vite.app.config.ts",
"build.server": "cd runtime && vite build -c vite.express.config.ts",
"build.runtime": "cd runtime && vite build --mode lib",
"serve": "node --inspect runtime/server/entry.express",
"serve.debug": "node --inspect-brk runtime/server/entry.express",
Expand Down
Loading

0 comments on commit ef638ba

Please sign in to comment.