Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions packages/app/api-contracts/src/apiContracts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ZodSchema, z } from 'zod/v4'
import type { AnyDeleteRoute, AnyGetRoute, AnyPayloadRoute, AnyRoute } from './contractService.js'
import type { HttpStatusCode } from './HttpStatusCodes.ts'

export type { HttpStatusCode }
Expand Down Expand Up @@ -264,3 +265,72 @@ export function mapRouteToPath(

return routeDefinition.pathResolver(resolverParams)
}

export type InferGetDetails<Route extends AnyGetRoute> = Route extends GetRouteDefinition<
infer SuccessResponseBodySchema,
infer PathParamsSchema,
infer RequestQuerySchema,
infer RequestHeaderSchema,
infer IsNonJSONResponseExpected,
infer IsEmptyResponseExpected
>
? {
responseBodySchema: SuccessResponseBodySchema
pathParamsSchema: PathParamsSchema
requestQuerySchema: RequestQuerySchema
requestHeaderSchema: RequestHeaderSchema
isNonJSONResponseExpected: IsNonJSONResponseExpected
isEmptyResponseExpected: IsEmptyResponseExpected
}
: never

export type InferDeleteDetails<Route extends AnyDeleteRoute> = Route extends DeleteRouteDefinition<
infer SuccessResponseBodySchema,
infer PathParamsSchema,
infer RequestQuerySchema,
infer RequestHeaderSchema,
infer IsNonJSONResponseExpected,
infer IsEmptyResponseExpected
>
? {
responseBodySchema: SuccessResponseBodySchema
pathParamsSchema: PathParamsSchema
requestQuerySchema: RequestQuerySchema
requestHeaderSchema: RequestHeaderSchema
isNonJSONResponseExpected: IsNonJSONResponseExpected
isEmptyResponseExpected: IsEmptyResponseExpected
}
: never

export type InferPayloadDetails<Route extends AnyPayloadRoute> =
Route extends PayloadRouteDefinition<
infer RequestBodySchema,
infer SuccessResponseBodySchema,
infer PathParamsSchema,
infer RequestQuerySchema,
infer RequestHeaderSchema,
infer IsNonJSONResponseExpected,
infer IsEmptyResponseExpected
>
? {
requestBodySchema: RequestBodySchema
responseBodySchema: SuccessResponseBodySchema
pathParamsSchema: PathParamsSchema
requestQuerySchema: RequestQuerySchema
requestHeaderSchema: RequestHeaderSchema
isNonJSONResponseExpected: IsNonJSONResponseExpected
isEmptyResponseExpected: IsEmptyResponseExpected
}
: never

Comment on lines +269 to +325
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Add JSDoc documentation for the new inference types

These type utilities would benefit from documentation explaining their purpose and usage.

Example documentation:

+/**
+ * Extracts type information from a GET route definition including schemas,
+ * parameters, and response metadata
+ * @example
+ * type Details = InferGetDetails<typeof myGetRoute>
+ */
 export type InferGetDetails<Route extends AnyGetRoute> = Route extends GetRouteDefinition<
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type InferGetDetails<Route extends AnyGetRoute> = Route extends GetRouteDefinition<
infer SuccessResponseBodySchema,
infer PathParamsSchema,
infer RequestQuerySchema,
infer RequestHeaderSchema,
infer IsNonJSONResponseExpected,
infer IsEmptyResponseExpected
>
? {
responseBodySchema: SuccessResponseBodySchema
pathParamsSchema: PathParamsSchema
requestQuerySchema: RequestQuerySchema
requestHeaderSchema: RequestHeaderSchema
isNonJSONResponseExpected: IsNonJSONResponseExpected
isEmptyResponseExpected: IsEmptyResponseExpected
}
: never
export type InferDeleteDetails<Route extends AnyDeleteRoute> = Route extends DeleteRouteDefinition<
infer SuccessResponseBodySchema,
infer PathParamsSchema,
infer RequestQuerySchema,
infer RequestHeaderSchema,
infer IsNonJSONResponseExpected,
infer IsEmptyResponseExpected
>
? {
responseBodySchema: SuccessResponseBodySchema
pathParamsSchema: PathParamsSchema
requestQuerySchema: RequestQuerySchema
requestHeaderSchema: RequestHeaderSchema
isNonJSONResponseExpected: IsNonJSONResponseExpected
isEmptyResponseExpected: IsEmptyResponseExpected
}
: never
export type InferPayloadDetails<Route extends AnyPayloadRoute> =
Route extends PayloadRouteDefinition<
infer RequestBodySchema,
infer SuccessResponseBodySchema,
infer PathParamsSchema,
infer RequestQuerySchema,
infer RequestHeaderSchema,
infer IsNonJSONResponseExpected,
infer IsEmptyResponseExpected
>
? {
requestBodySchema: RequestBodySchema
responseBodySchema: SuccessResponseBodySchema
pathParamsSchema: PathParamsSchema
requestQuerySchema: RequestQuerySchema
requestHeaderSchema: RequestHeaderSchema
isNonJSONResponseExpected: IsNonJSONResponseExpected
isEmptyResponseExpected: IsEmptyResponseExpected
}
: never
/**
* Extracts type information from a GET route definition including schemas,
* parameters, and response metadata
* @example
* type Details = InferGetDetails<typeof myGetRoute>
*/
export type InferGetDetails<Route extends AnyGetRoute> = Route extends GetRouteDefinition<
infer SuccessResponseBodySchema,
infer PathParamsSchema,
infer RequestQuerySchema,
infer RequestHeaderSchema,
infer IsNonJSONResponseExpected,
infer IsEmptyResponseExpected
>
? {
responseBodySchema: SuccessResponseBodySchema
pathParamsSchema: PathParamsSchema
requestQuerySchema: RequestQuerySchema
requestHeaderSchema: RequestHeaderSchema
isNonJSONResponseExpected: IsNonJSONResponseExpected
isEmptyResponseExpected: IsEmptyResponseExpected
}
: never
🤖 Prompt for AI Agents
In packages/app/api-contracts/src/apiContracts.ts between lines 269 and 325, the
new type inference utilities lack JSDoc comments explaining their purpose and
usage. Add clear JSDoc documentation above each type definition describing what
the type infers, the generic parameters it accepts, and what the resulting type
represents to improve code readability and maintainability.

export type InferRouteDetails<Route extends AnyRoute> = Route extends AnyGetRoute
? InferGetDetails<Route>
: Route extends AnyDeleteRoute
? InferDeleteDetails<Route>
: Route extends AnyPayloadRoute
? InferPayloadDetails<Route>
: never

export * from './contractService.js'
export * from './headers/createHeaderBuilderMiddleware.js'
export * from './headers/headerBuilder.js'
35 changes: 35 additions & 0 deletions packages/app/api-contracts/src/contractService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type {
DeleteRouteDefinition,
GetRouteDefinition,
PayloadRouteDefinition,
} from './apiContracts.js'

// biome-ignore lint/suspicious/noExplicitAny: This can actually be any type of route
export type AnyGetRoute = GetRouteDefinition<any, any, any, any, any, any>
// biome-ignore lint/suspicious/noExplicitAny: This can actually be any type of route
export type AnyDeleteRoute = DeleteRouteDefinition<any, any, any, any, any, any>
// biome-ignore lint/suspicious/noExplicitAny: This can actually be any type of route
export type AnyPayloadRoute = PayloadRouteDefinition<any, any, any, any, any, any>
export type AnyRoute = AnyGetRoute | AnyDeleteRoute | AnyPayloadRoute

// biome-ignore lint/suspicious/noExplicitAny: This can actually be any type of route
export type AnyCacheKeyFn = (...args: any[]) => Array<any>

export type AnyRoutes = {
[key: string]: {
route: AnyRoute
cacheKey: AnyCacheKeyFn
}
}

export type ContractDefinitions<Routes extends AnyRoutes> = {
serviceName: string
config: { routes: Routes }
}

export function definedContract<R extends AnyRoutes>(
service: string,
routes: R,
): ContractDefinitions<R> {
return { serviceName: service, config: { routes } }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { createHeaderBuilderMiddleware } from './createHeaderBuilderMiddleware.js'
import { HeaderBuilder } from './headerBuilder.js'
import { describe, expect, it } from 'vitest'

describe('createHeaderBuilderMiddleware', () => {
it('should create a middleware that adds a header', async () => {
const middleware = createHeaderBuilderMiddleware((builder) =>
builder.add('X-Test-Header', 'test-value'),
)

const builder = HeaderBuilder.create().with(middleware)
const actual = await builder.resolve()

expect(actual).toEqual({ 'X-Test-Header': 'test-value' })
})

it('should create a middleware that adds multiple headers', async () => {
const middleware = createHeaderBuilderMiddleware((builder) =>
builder.and({
'X-Test-Header-1': 'value1',
'X-Test-Header-2': 'value2',
}),
)

const builder = HeaderBuilder.create().with(middleware)
const actual = await builder.resolve()

expect(actual).toEqual({
'X-Test-Header-1': 'value1',
'X-Test-Header-2': 'value2',
})
})

it('should create a middleware that adds headers asynchronously', async () => {
const middleware = createHeaderBuilderMiddleware(async (builder) => {
const token = await new Promise<string>((resolve) => resolve('async-token'))
return builder.add('authorization', `Bearer ${token}`)
})

const builder = HeaderBuilder.create().with(middleware)
const actual = await builder.resolve()

expect(actual).toEqual({ authorization: 'Bearer async-token' })
})

it('should create a middleware that merges headers from another builder', async () => {
const otherBuilder = HeaderBuilder.create().add('X-Other-Header', 'other-value')
const middleware = createHeaderBuilderMiddleware((builder) => builder.merge(otherBuilder))

const builder = HeaderBuilder.create().with(middleware)
const actual = await builder.resolve()

expect(actual).toEqual({ 'X-Other-Header': 'other-value' })
})

it('should handle middleware that returns a promise of a builder', async () => {
const middleware = createHeaderBuilderMiddleware((builder) =>
Promise.resolve(builder.add('X-Promise-Header', 'promise-value')),
)

const builder = HeaderBuilder.create().with(middleware)
const actual = await builder.resolve()

expect(actual).toEqual({ 'X-Promise-Header': 'promise-value' })
})
})
Comment on lines +5 to +66
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider adding tests for error scenarios and edge cases

The current tests cover the happy path well, but since this is a draft implementation seeking feedback, consider adding tests for:

  • Error handling when middleware throws an exception
  • Chaining multiple middleware together
  • Header value overriding behavior when the same header is set multiple times
  • Type inference validation to ensure the middleware preserves type safety

Would you like me to generate additional test cases for these scenarios?

🤖 Prompt for AI Agents
In packages/app/api-contracts/src/headers/createHeaderBuilderMiddleware.test.ts
between lines 5 and 66, the current tests only cover successful middleware
behavior. To improve coverage, add tests that verify error handling when
middleware throws exceptions, chaining multiple middleware functions to ensure
combined behavior, how header values are overridden when the same header is set
multiple times, and validate type inference to confirm the middleware preserves
type safety. Implement these tests by simulating errors, composing multiple
middleware, setting duplicate headers, and using TypeScript assertions or type
checks.

Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { HeaderBuilder, type Headers } from './headerBuilder.js'

/**
* A helper function that creates a HeaderBuilderMiddleware, it removed the
* complexity of creating a new instance of the middleware class and tracking the input types.
*
* @param middleware - A function that modifies a HeaderBuilder
* @returns - A new instance of HeaderBuilderMiddleware to be used with a HeaderBuilder
*/
export function createHeaderBuilderMiddleware<const H extends Headers>(
middleware: (builder: HeaderBuilder) => HeaderBuilder<H> | Promise<HeaderBuilder<H>>,
) {
return new HeaderBuilderMiddleware<H>(middleware)
}

// There is no need to have access to the implementation of the HeaderBuilderMiddleware class
// all users should use the createHeaderBuilderMiddleware function to create a new instance,
// but I have to export the type for reference outside the file
export type { HeaderBuilderMiddleware }

type MiddlewareFn<H extends Headers> = (
builder: HeaderBuilder<Headers>,
) => HeaderBuilder<H> | Promise<HeaderBuilder<H>>

/**
* A middleware class that allows you to modify a HeaderBuilder in a type-safe way.
* It receives a builder and returns a new builder with the modifications applied.
*
* @example
* ```typescript
* const authMiddleware = createHeaderBuilderMiddleware(async (builder) => {
* const token = await fetchToken()
* return builder.add('authorization', `Bearer ${token}`)
* })
*
* const builder = HeaderBuilder.create()
* .with(authMiddleware)
*
* const headers = await builder.resolve() // Type of headers is { 'authorization': string }
* console.log(headers) // { 'authorization': 'Bearer <token>' }
*/
class HeaderBuilderMiddleware<const H extends Headers> {
private readonly middleware: MiddlewareFn<H>

constructor(middleware: MiddlewareFn<H>) {
this.middleware = middleware
}

apply<const BH extends Headers>(base: HeaderBuilder<BH>): HeaderBuilder<BH & H> {
// Using the `from` method to make the promise lazy - it should only resolve when the builder is resolved
return base.from(async () => {
const middlewareBuilder = this.middleware(HeaderBuilder.create())

return middlewareBuilder instanceof Promise
? middlewareBuilder.then((r) => r.resolve())
: middlewareBuilder.resolve()
})
}
}
134 changes: 134 additions & 0 deletions packages/app/api-contracts/src/headers/headerBuilder.test.ts
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: This header stuff was copied from harmony.. It might be over kill since we can provide global hearers at the config level - but I heave left it in for now, for simplicity.

Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { createHeaderBuilderMiddleware } from './createHeaderBuilderMiddleware.js'
import { HeaderBuilder } from './headerBuilder.js'
import { describe, expect, vitest } from 'vitest'

// biome-ignore lint/complexity/noBannedTypes: To test empty headers, we need to use an empty object type.
type ExpectedEmptyHeaders = {}

// It is critical that type safety is maintained when refactoring - this offers a way to unit test the type-system.
// The call will fail to build if the expected type is not correct.
const typeCheck = <const Expected>(_actual: Expected): void => {}

describe('HeaderBuilder', () => {
it('should create an empty header builder', async () => {
const builder = HeaderBuilder.create()

const actual = await builder.resolve()

typeCheck<ExpectedEmptyHeaders>(actual)

expect(actual).toEqual({})
})

it('should create a header builder with default headers', async () => {
const builder = HeaderBuilder.create({ 'Content-Type': 'application/json' })

const actual = await builder.resolve()

typeCheck<{ 'Content-Type': 'application/json' }>(actual)

expect(actual).toEqual({ 'Content-Type': 'application/json' })
})

it('should allow individual headers to be added to the builder by name', async () => {
const builder = HeaderBuilder.create().add('Content-Type', 'application/json')

const actual = await builder.resolve()

typeCheck<{ 'Content-Type': 'application/json' }>(actual)

expect(actual).toEqual({ 'Content-Type': 'application/json' })
})

it('should allow multiple headers to be added in one static object', async () => {
const builder = HeaderBuilder.create().and({
'Content-Type': 'application/json',
'X-Api-Key': '1234',
})

const actual = await builder.resolve()

typeCheck<{ 'Content-Type': 'application/json'; 'X-Api-Key': '1234' }>(actual)

expect(actual).toEqual({ 'Content-Type': 'application/json', 'X-Api-Key': '1234' })
})

it('should allow multiple headers to be added in a promise of one static object', async () => {
const builder = HeaderBuilder.create().and(
Promise.resolve({
'Content-Type': 'application/json',
'X-Api-Key': '1234',
}),
)

const actual = await builder.resolve()

typeCheck<{ 'Content-Type': 'application/json'; 'X-Api-Key': '1234' }>(actual)

expect(actual).toEqual({ 'Content-Type': 'application/json', 'X-Api-Key': '1234' })
})

it('should allow headers to be added from a factory function', async () => {
const builder = HeaderBuilder.create().from(async () => {
const token = await mockGetToken()
return { authorization: `Bearer ${token}` }
})

const actual = await builder.resolve()

typeCheck<{ authorization: string }>(actual)

expect(actual).toEqual({ authorization: 'Bearer 1234' })
})

const mockGetToken = async () => '1234'

it('should allow you to extend the builder with middleware functions', async () => {
const middleware = createHeaderBuilderMiddleware(async (builder) => {
const token = await mockGetToken()
return builder.and({ authorization: `Bearer ${token}` })
})

const builder = HeaderBuilder.create().with(middleware)

const actual = await builder.resolve()

typeCheck<{ authorization: string }>(actual)

expect(actual).toEqual({ authorization: 'Bearer 1234' })
})

it('should lazy load promises and only resolve them when the headers are resolved', async () => {
const factory = vitest.fn(async () => ({ 'Content-Type': 'application/json' }) as const)
const middlewareMock = vitest.fn(async (builder: HeaderBuilder) =>
builder.add('X-Api-Key', '1234'),
)

const middleware = createHeaderBuilderMiddleware(middlewareMock)

const builder = HeaderBuilder.create().from(factory).with(middleware)

expect(factory).not.toHaveBeenCalled()
expect(middlewareMock).not.toHaveBeenCalled()

const actual = await builder.resolve()

typeCheck<{ 'Content-Type': 'application/json'; 'X-Api-Key': '1234' }>(actual)

expect(factory).toHaveBeenCalled()
expect(actual).toEqual({ 'Content-Type': 'application/json', 'X-Api-Key': '1234' })
})

it('should merge two header builders', async () => {
const builder1 = HeaderBuilder.create().add('Content-Type', 'application/json')
const builder2 = HeaderBuilder.create().add('authorization', 'Bearer token')

const mergedBuilder = builder1.merge(builder2)

const actual = await mergedBuilder.resolve()

typeCheck<{ 'Content-Type': 'application/json'; authorization: 'Bearer token' }>(actual)

expect(actual).toEqual({ 'Content-Type': 'application/json', authorization: 'Bearer token' })
})
})
Comment on lines +1 to +134
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Comprehensive test suite with excellent coverage.

The test suite thoroughly covers the HeaderBuilder functionality with appropriate type safety verification. The tests are well-structured and cover both synchronous and asynchronous operations, lazy evaluation, middleware integration, and builder merging.

The typeCheck function is an excellent approach for ensuring type safety is maintained during refactoring.

Minor organizational improvement:

+const mockGetToken = async () => '1234'
+
 describe('HeaderBuilder', () => {
 	it('should create an empty header builder', async () => {
 		const builder = HeaderBuilder.create()
@@ -81,8 +82,6 @@
 		expect(actual).toEqual({ authorization: 'Bearer 1234' })
 	})
 
-	const mockGetToken = async () => '1234'
-
 	it('should allow you to extend the builder with middleware functions', async () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { createHeaderBuilderMiddleware } from './createHeaderBuilderMiddleware.js'
import { HeaderBuilder } from './headerBuilder.js'
import { describe, expect, vitest } from 'vitest'
// biome-ignore lint/complexity/noBannedTypes: To test empty headers, we need to use an empty object type.
type ExpectedEmptyHeaders = {}
// It is critical that type safety is maintained when refactoring - this offers a way to unit test the type-system.
// The call will fail to build if the expected type is not correct.
const typeCheck = <const Expected>(_actual: Expected): void => {}
describe('HeaderBuilder', () => {
it('should create an empty header builder', async () => {
const builder = HeaderBuilder.create()
const actual = await builder.resolve()
typeCheck<ExpectedEmptyHeaders>(actual)
expect(actual).toEqual({})
})
it('should create a header builder with default headers', async () => {
const builder = HeaderBuilder.create({ 'Content-Type': 'application/json' })
const actual = await builder.resolve()
typeCheck<{ 'Content-Type': 'application/json' }>(actual)
expect(actual).toEqual({ 'Content-Type': 'application/json' })
})
it('should allow individual headers to be added to the builder by name', async () => {
const builder = HeaderBuilder.create().add('Content-Type', 'application/json')
const actual = await builder.resolve()
typeCheck<{ 'Content-Type': 'application/json' }>(actual)
expect(actual).toEqual({ 'Content-Type': 'application/json' })
})
it('should allow multiple headers to be added in one static object', async () => {
const builder = HeaderBuilder.create().and({
'Content-Type': 'application/json',
'X-Api-Key': '1234',
})
const actual = await builder.resolve()
typeCheck<{ 'Content-Type': 'application/json'; 'X-Api-Key': '1234' }>(actual)
expect(actual).toEqual({ 'Content-Type': 'application/json', 'X-Api-Key': '1234' })
})
it('should allow multiple headers to be added in a promise of one static object', async () => {
const builder = HeaderBuilder.create().and(
Promise.resolve({
'Content-Type': 'application/json',
'X-Api-Key': '1234',
}),
)
const actual = await builder.resolve()
typeCheck<{ 'Content-Type': 'application/json'; 'X-Api-Key': '1234' }>(actual)
expect(actual).toEqual({ 'Content-Type': 'application/json', 'X-Api-Key': '1234' })
})
it('should allow headers to be added from a factory function', async () => {
const builder = HeaderBuilder.create().from(async () => {
const token = await mockGetToken()
return { authorization: `Bearer ${token}` }
})
const actual = await builder.resolve()
typeCheck<{ authorization: string }>(actual)
expect(actual).toEqual({ authorization: 'Bearer 1234' })
})
const mockGetToken = async () => '1234'
it('should allow you to extend the builder with middleware functions', async () => {
const middleware = createHeaderBuilderMiddleware(async (builder) => {
const token = await mockGetToken()
return builder.and({ authorization: `Bearer ${token}` })
})
const builder = HeaderBuilder.create().with(middleware)
const actual = await builder.resolve()
typeCheck<{ authorization: string }>(actual)
expect(actual).toEqual({ authorization: 'Bearer 1234' })
})
it('should lazy load promises and only resolve them when the headers are resolved', async () => {
const factory = vitest.fn(async () => ({ 'Content-Type': 'application/json' }) as const)
const middlewareMock = vitest.fn(async (builder: HeaderBuilder) =>
builder.add('X-Api-Key', '1234'),
)
const middleware = createHeaderBuilderMiddleware(middlewareMock)
const builder = HeaderBuilder.create().from(factory).with(middleware)
expect(factory).not.toHaveBeenCalled()
expect(middlewareMock).not.toHaveBeenCalled()
const actual = await builder.resolve()
typeCheck<{ 'Content-Type': 'application/json'; 'X-Api-Key': '1234' }>(actual)
expect(factory).toHaveBeenCalled()
expect(actual).toEqual({ 'Content-Type': 'application/json', 'X-Api-Key': '1234' })
})
it('should merge two header builders', async () => {
const builder1 = HeaderBuilder.create().add('Content-Type', 'application/json')
const builder2 = HeaderBuilder.create().add('authorization', 'Bearer token')
const mergedBuilder = builder1.merge(builder2)
const actual = await mergedBuilder.resolve()
typeCheck<{ 'Content-Type': 'application/json'; authorization: 'Bearer token' }>(actual)
expect(actual).toEqual({ 'Content-Type': 'application/json', authorization: 'Bearer token' })
})
})
import { createHeaderBuilderMiddleware } from './createHeaderBuilderMiddleware.js'
import { HeaderBuilder } from './headerBuilder.js'
import { describe, expect, vitest } from 'vitest'
// biome-ignore lint/complexity/noBannedTypes: To test empty headers, we need to use an empty object type.
type ExpectedEmptyHeaders = {}
// It is critical that type safety is maintained when refactoring - this offers a way to unit test the type-system.
// The call will fail to build if the expected type is not correct.
const typeCheck = <const Expected>(_actual: Expected): void => {}
const mockGetToken = async () => '1234'
describe('HeaderBuilder', () => {
it('should create an empty header builder', async () => {
const builder = HeaderBuilder.create()
const actual = await builder.resolve()
typeCheck<ExpectedEmptyHeaders>(actual)
expect(actual).toEqual({})
})
it('should create a header builder with default headers', async () => {
const builder = HeaderBuilder.create({ 'Content-Type': 'application/json' })
const actual = await builder.resolve()
typeCheck<{ 'Content-Type': 'application/json' }>(actual)
expect(actual).toEqual({ 'Content-Type': 'application/json' })
})
it('should allow individual headers to be added to the builder by name', async () => {
const builder = HeaderBuilder.create().add('Content-Type', 'application/json')
const actual = await builder.resolve()
typeCheck<{ 'Content-Type': 'application/json' }>(actual)
expect(actual).toEqual({ 'Content-Type': 'application/json' })
})
it('should allow multiple headers to be added in one static object', async () => {
const builder = HeaderBuilder.create().and({
'Content-Type': 'application/json',
'X-Api-Key': '1234',
})
const actual = await builder.resolve()
typeCheck<{ 'Content-Type': 'application/json'; 'X-Api-Key': '1234' }>(actual)
expect(actual).toEqual({ 'Content-Type': 'application/json', 'X-Api-Key': '1234' })
})
it('should allow multiple headers to be added in a promise of one static object', async () => {
const builder = HeaderBuilder.create().and(
Promise.resolve({
'Content-Type': 'application/json',
'X-Api-Key': '1234',
}),
)
const actual = await builder.resolve()
typeCheck<{ 'Content-Type': 'application/json'; 'X-Api-Key': '1234' }>(actual)
expect(actual).toEqual({ 'Content-Type': 'application/json', 'X-Api-Key': '1234' })
})
it('should allow headers to be added from a factory function', async () => {
const builder = HeaderBuilder.create().from(async () => {
const token = await mockGetToken()
return { authorization: `Bearer ${token}` }
})
const actual = await builder.resolve()
typeCheck<{ authorization: string }>(actual)
expect(actual).toEqual({ authorization: 'Bearer 1234' })
})
it('should allow you to extend the builder with middleware functions', async () => {
const middleware = createHeaderBuilderMiddleware(async (builder) => {
const token = await mockGetToken()
return builder.and({ authorization: `Bearer ${token}` })
})
const builder = HeaderBuilder.create().with(middleware)
const actual = await builder.resolve()
typeCheck<{ authorization: string }>(actual)
expect(actual).toEqual({ authorization: 'Bearer 1234' })
})
it('should lazy load promises and only resolve them when the headers are resolved', async () => {
const factory = vitest.fn(async () => ({ 'Content-Type': 'application/json' }) as const)
const middlewareMock = vitest.fn(async (builder: HeaderBuilder) =>
builder.add('X-Api-Key', '1234'),
)
const middleware = createHeaderBuilderMiddleware(middlewareMock)
const builder = HeaderBuilder.create().from(factory).with(middleware)
expect(factory).not.toHaveBeenCalled()
expect(middlewareMock).not.toHaveBeenCalled()
const actual = await builder.resolve()
typeCheck<{ 'Content-Type': 'application/json'; 'X-Api-Key': '1234' }>(actual)
expect(factory).toHaveBeenCalled()
expect(actual).toEqual({ 'Content-Type': 'application/json', 'X-Api-Key': '1234' })
})
it('should merge two header builders', async () => {
const builder1 = HeaderBuilder.create().add('Content-Type', 'application/json')
const builder2 = HeaderBuilder.create().add('authorization', 'Bearer token')
const mergedBuilder = builder1.merge(builder2)
const actual = await mergedBuilder.resolve()
typeCheck<{ 'Content-Type': 'application/json'; authorization: 'Bearer token' }>(actual)
expect(actual).toEqual({ 'Content-Type': 'application/json', authorization: 'Bearer token' })
})
})
🤖 Prompt for AI Agents
In packages/app/api-contracts/src/headers/headerBuilder.test.ts from lines 1 to
134, the test suite is comprehensive and well-structured. To improve
organization, consider grouping related tests into nested describe blocks for
better readability and maintainability. For example, group tests for adding
headers, middleware, and merging separately. This will not change functionality
but will enhance clarity and navigation within the test file.

Loading