Skip to content

Commit

Permalink
Merge branch 'main' of github.com:redwoodjs/redwood into feat/ssr-mid…
Browse files Browse the repository at this point in the history
…dleware

* 'main' of github.com:redwoodjs/redwood:
  chore(structure): switch to vitest (redwoodjs#9878)
  chore(cli): switch to vitest (redwoodjs#9863)
  feat(dbAuth): Refactor dbAuthHandler to support WebAPI Request events (redwoodjs#9835)
  fix(crwa): remove yarn-install option for yarn 1 (redwoodjs#9881)
  chore(esbuild): dedupe esbuild config (redwoodjs#9875)
  chore(esm): convert `@redwoodjs/cli-helpers` to ESM (redwoodjs#9872)
  fix(studio): Add version checks when first running Studio (redwoodjs#9876)
  • Loading branch information
dac09 committed Jan 25, 2024
2 parents 8d64b1c + c29019c commit 7cac8aa
Show file tree
Hide file tree
Showing 200 changed files with 5,922 additions and 2,771 deletions.
73 changes: 73 additions & 0 deletions buildDefaults.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import path from 'node:path'

import * as esbuild from 'esbuild'
import fg from 'fast-glob'
import fs from 'fs-extra'

export const defaultBuildOptions = {
outdir: 'dist',

platform: 'node',
target: ['node20'],

format: 'cjs',

logLevel: 'info',

// For visualizing dist. See:
// - https://esbuild.github.io/api/#metafile
// - https://esbuild.github.io/analyze/
metafile: true,
}

export const defaultPatterns = ['./src/**/*.{ts,js}']
export const defaultIgnorePatterns = ['**/__tests__', '**/*.test.{ts,js}']

/**
* @typedef {{
* cwd?: string
* buildOptions?: import('esbuild').BuildOptions
* entryPointOptions?: {
* patterns?: string[]
* ignore?: string[]
* }
* metafileName?: string
* }} BuildOptions
*
* @param {BuildOptions} options
*/
export async function build({
cwd,
buildOptions,
entryPointOptions,
metafileName,
} = {}) {
// Yarn and Nx both set this to the package's root dir path
cwd ??= process.cwd()

buildOptions ??= defaultBuildOptions
metafileName ??= 'meta.json'

// If the user didn't explicitly provide entryPoints,
// then we'll use fg to find all the files in `${cwd}/src`
let entryPoints = buildOptions.entryPoints

if (!entryPoints) {
const patterns = entryPointOptions?.patterns ?? defaultPatterns
const ignore = entryPointOptions?.ignore ?? defaultIgnorePatterns

entryPoints = await fg(patterns, {
cwd,
ignore,
})
}

const result = await esbuild.build({
entryPoints,
...buildOptions,
})

await fs.writeJSON(path.join(cwd, metafileName), result.metafile, {
spaces: 2,
})
}
1 change: 1 addition & 0 deletions nx.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"!{projectRoot}/**/*.test.{js,jsx,ts,tsx}",
"{workspaceRoot}/babel.config.js",
"{workspaceRoot}/tsconfig.json",
"{workspaceRoot}/buildDefaults.mjs",
{
"runtime": "node -v"
},
Expand Down
129 changes: 98 additions & 31 deletions packages/api/src/__tests__/normalizeRequest.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Headers } from '@whatwg-node/fetch'
import type { APIGatewayProxyEvent } from 'aws-lambda'
import { test, expect } from 'vitest'
import { test, expect, describe } from 'vitest'

import { normalizeRequest } from '../transforms'

export const createMockedEvent = (
export const createMockedLambdaEvent = (
httpMethod = 'POST',
body: any = undefined,
isBase64Encoded = false
Expand Down Expand Up @@ -54,41 +54,108 @@ export const createMockedEvent = (
}
}

test('Normalizes an aws event with base64', () => {
const corsEventB64 = createMockedEvent(
'POST',
Buffer.from(JSON.stringify({ bazinga: 'hello_world' }), 'utf8').toString(
'base64'
),
true
)

expect(normalizeRequest(corsEventB64)).toEqual({
headers: new Headers(corsEventB64.headers),
method: 'POST',
query: null,
body: {
bazinga: 'hello_world',
},
describe('Lambda Request', () => {
test('Normalizes an aws event with base64', async () => {
const corsEventB64 = createMockedLambdaEvent(
'POST',
Buffer.from(JSON.stringify({ bazinga: 'hello_world' }), 'utf8').toString(
'base64'
),
true
)

expect(await normalizeRequest(corsEventB64)).toEqual({
headers: new Headers(corsEventB64.headers as Record<string, string>),
method: 'POST',
query: null,
jsonBody: {
bazinga: 'hello_world',
},
})
})

test('Handles CORS requests with and without b64 encoded', async () => {
const corsEventB64 = createMockedLambdaEvent('OPTIONS', undefined, true)

expect(await normalizeRequest(corsEventB64)).toEqual({
headers: new Headers(corsEventB64.headers as Record<string, string>), // headers returned as symbol
method: 'OPTIONS',
query: null,
jsonBody: {},
})

const corsEventWithoutB64 = createMockedLambdaEvent(
'OPTIONS',
undefined,
false
)

expect(await normalizeRequest(corsEventWithoutB64)).toEqual({
headers: new Headers(corsEventB64.headers as Record<string, string>), // headers returned as symbol
method: 'OPTIONS',
query: null,
jsonBody: {},
})
})
})

test('Handles CORS requests with and without b64 encoded', () => {
const corsEventB64 = createMockedEvent('OPTIONS', undefined, true)
describe('Fetch API Request', () => {
test('Normalizes a fetch event', async () => {
const fetchEvent = new Request(
'http://localhost:9210/graphql?whatsup=doc&its=bugs',
{
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({ bazinga: 'kittens_purr_purr' }),
}
)

expect(normalizeRequest(corsEventB64)).toEqual({
headers: new Headers(corsEventB64.headers), // headers returned as symbol
method: 'OPTIONS',
query: null,
body: undefined,
const partial = await normalizeRequest(fetchEvent)

expect(partial).toMatchObject({
// headers: fetchEvent.headers,
method: 'POST',
query: {
whatsup: 'doc',
its: 'bugs',
},
jsonBody: {
bazinga: 'kittens_purr_purr',
},
})

expect(partial.headers.get('content-type')).toEqual('application/json')
})

const corsEventWithoutB64 = createMockedEvent('OPTIONS', undefined, false)
test('Handles an empty body', async () => {
const headers = {
'content-type': 'application/json',
'x-custom-header': 'bazinga',
}

const fetchEvent = new Request(
'http://localhost:9210/graphql?whatsup=doc&its=bugs',
{
method: 'PUT',
headers,
body: '',
}
)

const partial = await normalizeRequest(fetchEvent)

expect(partial).toMatchObject({
method: 'PUT',
query: {
whatsup: 'doc',
its: 'bugs',
},
jsonBody: {}, // @NOTE empty body is {} not undefined
})

expect(normalizeRequest(corsEventWithoutB64)).toEqual({
headers: new Headers(corsEventB64.headers), // headers returned as symbol
method: 'OPTIONS',
query: null,
body: undefined,
expect(partial.headers.get('content-type')).toEqual(headers['content-type'])
expect(partial.headers.get('x-custom-header')).toEqual('bazinga')
})
})
35 changes: 25 additions & 10 deletions packages/api/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ export * from './parseJWT'

import type { APIGatewayProxyEvent, Context as LambdaContext } from 'aws-lambda'

import { getEventHeader } from '../event'

import type { Decoded } from './parseJWT'
export type { Decoded }

// This is shared by `@redwoodjs/web`
const AUTH_PROVIDER_HEADER = 'auth-provider'

export const getAuthProviderHeader = (event: APIGatewayProxyEvent) => {
export const getAuthProviderHeader = (
event: APIGatewayProxyEvent | Request
) => {
const authProviderKey = Object.keys(event?.headers ?? {}).find(
(key) => key.toLowerCase() === AUTH_PROVIDER_HEADER
)
if (authProviderKey) {
return event?.headers[authProviderKey]
return getEventHeader(event, authProviderKey)
}
return undefined
}
Expand All @@ -27,11 +31,9 @@ export interface AuthorizationHeader {
* Split the `Authorization` header into a schema and token part.
*/
export const parseAuthorizationHeader = (
event: APIGatewayProxyEvent
event: APIGatewayProxyEvent | Request
): AuthorizationHeader => {
const parts = (
event.headers?.authorization || event.headers?.Authorization
)?.split(' ')
const parts = getEventHeader(event, 'authorization')?.split(' ')
if (parts?.length !== 2) {
throw new Error('The `Authorization` header is not valid.')
}
Expand All @@ -42,16 +44,24 @@ export const parseAuthorizationHeader = (
return { schema, token }
}

/** @MARK Note that we do not send LambdaContext when making fetch requests
*
* This part is incomplete, as we need to decide how we will make the breaking change to
* 1. getCurrentUser
* 2. authDecoders
*/

export type AuthContextPayload = [
Decoded,
{ type: string } & AuthorizationHeader,
{ event: APIGatewayProxyEvent; context: LambdaContext }
{ event: APIGatewayProxyEvent | Request; context: LambdaContext }
]

export type Decoder = (
token: string,
type: string,
req: { event: APIGatewayProxyEvent; context: LambdaContext }
req: { event: APIGatewayProxyEvent | Request; context: LambdaContext }
) => Promise<Decoded>

/**
Expand All @@ -64,7 +74,7 @@ export const getAuthenticationContext = async ({
context,
}: {
authDecoder?: Decoder | Decoder[]
event: APIGatewayProxyEvent
event: APIGatewayProxyEvent | Request
context: LambdaContext
}): Promise<undefined | AuthContextPayload> => {
const type = getAuthProviderHeader(event)
Expand All @@ -89,7 +99,12 @@ export const getAuthenticationContext = async ({

let i = 0
while (!decoded && i < authDecoders.length) {
decoded = await authDecoders[i](token, type, { event, context })
decoded = await authDecoders[i](token, type, {
// @TODO: We will need to make a breaking change to support `Request` objects.
// We can remove this typecast
event: event,
context,
})
i++
}

Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/cors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Headers } from '@whatwg-node/fetch'

import type { Request } from './transforms'
import type { PartialRequest } from './transforms'

export type CorsConfig = {
origin?: boolean | string | string[]
Expand Down Expand Up @@ -59,10 +59,10 @@ export function createCorsContext(cors: CorsConfig | undefined) {
}

return {
shouldHandleCors(request: Request) {
shouldHandleCors(request: PartialRequest) {
return request.method === 'OPTIONS'
},
getRequestHeaders(request: Request): CorsHeaders {
getRequestHeaders(request: PartialRequest): CorsHeaders {
const eventHeaders = new Headers(request.headers as HeadersInit)
const requestCorsHeaders = new Headers(corsHeaders)

Expand Down
15 changes: 15 additions & 0 deletions packages/api/src/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { APIGatewayProxyEvent } from 'aws-lambda'

import { isFetchApiRequest } from './transforms'

// Extracts the header from an event, handling lower and upper case header names.
export const getEventHeader = (
event: APIGatewayProxyEvent | Request,
headerName: string
) => {
if (isFetchApiRequest(event)) {
return event.headers.get(headerName)
}

return event.headers[headerName] || event.headers[headerName.toLowerCase()]
}
1 change: 1 addition & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './types'

export * from './transforms'
export * from './cors'
export * from './event'

// @NOTE: use require, to avoid messing around with tsconfig and nested output dirs
const packageJson = require('../package.json')
Expand Down
Loading

0 comments on commit 7cac8aa

Please sign in to comment.