UNPKG

@prism-engineer/router

Version:

Type-safe Express.js router with automatic client generation

428 lines 18.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const vitest_1 = require("vitest"); const typebox_1 = require("@sinclair/typebox"); const createApiRoute_1 = require("../../../createApiRoute"); (0, vitest_1.describe)('createApiRoute - Request/Response Schema Validation', () => { (0, vitest_1.it)('should validate TypeBox request body schema', () => { const bodySchema = typebox_1.Type.Object({ name: typebox_1.Type.String(), age: typebox_1.Type.Number(), email: typebox_1.Type.String({ format: 'email' }), metadata: typebox_1.Type.Optional(typebox_1.Type.Object({ tags: typebox_1.Type.Array(typebox_1.Type.String()), priority: typebox_1.Type.Union([typebox_1.Type.Literal('low'), typebox_1.Type.Literal('medium'), typebox_1.Type.Literal('high')]) })) }); const route = (0, createApiRoute_1.createApiRoute)({ path: '/api/users', method: 'POST', request: { body: bodySchema }, response: { 201: { contentType: 'application/json', body: typebox_1.Type.Object({ id: typebox_1.Type.Number(), message: typebox_1.Type.String() }) } }, handler: async (_req) => { return { status: 201, body: { id: 1, message: 'User created' } }; } }); (0, vitest_1.expect)(route.request?.body).toBe(bodySchema); (0, vitest_1.expect)(route.request?.body?.type).toBe('object'); (0, vitest_1.expect)(route.request?.body?.properties).toBeDefined(); (0, vitest_1.expect)(route.request?.body?.properties?.name).toBeDefined(); (0, vitest_1.expect)(route.request?.body?.properties?.age).toBeDefined(); (0, vitest_1.expect)(route.request?.body?.properties.email).toBeDefined(); }); (0, vitest_1.it)('should validate TypeBox query parameter schema', () => { const querySchema = typebox_1.Type.Object({ search: typebox_1.Type.String(), page: typebox_1.Type.Optional(typebox_1.Type.Number({ minimum: 1 })), limit: typebox_1.Type.Optional(typebox_1.Type.Number({ minimum: 1, maximum: 100 })), sortBy: typebox_1.Type.Optional(typebox_1.Type.Union([ typebox_1.Type.Literal('name'), typebox_1.Type.Literal('date'), typebox_1.Type.Literal('relevance') ])), filters: typebox_1.Type.Optional(typebox_1.Type.Array(typebox_1.Type.String())) }); const route = (0, createApiRoute_1.createApiRoute)({ path: '/api/search', method: 'GET', request: { query: querySchema }, response: { 200: { contentType: 'application/json', body: typebox_1.Type.Array(typebox_1.Type.Object({ id: typebox_1.Type.Number(), title: typebox_1.Type.String() })) } }, handler: async (_req) => { return { status: 200, body: [{ id: 1, title: 'Result' }] }; } }); (0, vitest_1.expect)(route.request?.query).toBe(querySchema); (0, vitest_1.expect)(route.request?.query?.type).toBe('object'); (0, vitest_1.expect)(route.request?.query?.properties.search).toBeDefined(); (0, vitest_1.expect)(route.request?.query?.properties.page).toBeDefined(); (0, vitest_1.expect)(route.request?.query?.properties.limit).toBeDefined(); }); (0, vitest_1.it)('should validate TypeBox header schema', () => { const headerSchema = typebox_1.Type.Object({ authorization: typebox_1.Type.String({ pattern: '^Bearer .+' }), 'x-api-version': typebox_1.Type.Union([typebox_1.Type.Literal('v1'), typebox_1.Type.Literal('v2')]), 'x-request-id': typebox_1.Type.String({ format: 'uuid' }), 'x-client-version': typebox_1.Type.Optional(typebox_1.Type.String()), 'accept': typebox_1.Type.Optional(typebox_1.Type.Literal('application/json')) }); const route = (0, createApiRoute_1.createApiRoute)({ path: '/api/protected', method: 'GET', request: { headers: headerSchema }, response: { 200: { contentType: 'application/json', body: typebox_1.Type.Object({ message: typebox_1.Type.String() }) } }, handler: async (_req) => { return { status: 200, body: { message: 'Access granted' } }; } }); (0, vitest_1.expect)(route.request?.headers).toBe(headerSchema); (0, vitest_1.expect)(route.request?.headers?.type).toBe('object'); (0, vitest_1.expect)(route.request?.headers?.properties.authorization).toBeDefined(); (0, vitest_1.expect)(route.request?.headers?.properties['x-api-version']).toBeDefined(); }); (0, vitest_1.it)('should validate TypeBox response body schema', () => { const responseSchema = typebox_1.Type.Object({ user: typebox_1.Type.Object({ id: typebox_1.Type.Number(), name: typebox_1.Type.String(), email: typebox_1.Type.String(), profile: typebox_1.Type.Object({ avatar: typebox_1.Type.Optional(typebox_1.Type.String()), bio: typebox_1.Type.Optional(typebox_1.Type.String()), location: typebox_1.Type.Optional(typebox_1.Type.String()) }) }), metadata: typebox_1.Type.Object({ createdAt: typebox_1.Type.String(), updatedAt: typebox_1.Type.String(), version: typebox_1.Type.Number() }) }); const route = (0, createApiRoute_1.createApiRoute)({ path: '/api/users/{id}', method: 'GET', response: { 200: { contentType: 'application/json', body: responseSchema }, 404: { contentType: 'application/json', body: typebox_1.Type.Object({ error: typebox_1.Type.String(), code: typebox_1.Type.Number(), details: typebox_1.Type.Optional(typebox_1.Type.Array(typebox_1.Type.String())) }) } }, handler: async (_req) => { return { status: 200, body: { user: { id: 1, name: 'John Doe', email: 'john@example.com', profile: { avatar: 'https://example.com/avatar.png', bio: 'Software developer' } }, metadata: { createdAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-01T00:00:00Z', version: 1 } } }; } }); (0, vitest_1.expect)(route.response?.[200]?.body).toBe(responseSchema); (0, vitest_1.expect)(route.response?.[200]?.body.type).toBe('object'); (0, vitest_1.expect)(route.response?.[200]?.body.properties.user).toBeDefined(); (0, vitest_1.expect)(route.response?.[200]?.body.properties.metadata).toBeDefined(); }); (0, vitest_1.it)('should validate complex nested schemas', () => { const complexSchema = typebox_1.Type.Object({ data: typebox_1.Type.Array(typebox_1.Type.Object({ id: typebox_1.Type.Number(), attributes: typebox_1.Type.Object({ name: typebox_1.Type.String(), tags: typebox_1.Type.Array(typebox_1.Type.String()), metadata: typebox_1.Type.Record(typebox_1.Type.String(), typebox_1.Type.Union([ typebox_1.Type.String(), typebox_1.Type.Number(), typebox_1.Type.Boolean() ])) }), relationships: typebox_1.Type.Optional(typebox_1.Type.Object({ parent: typebox_1.Type.Optional(typebox_1.Type.Object({ id: typebox_1.Type.Number(), type: typebox_1.Type.String() })), children: typebox_1.Type.Array(typebox_1.Type.Object({ id: typebox_1.Type.Number(), type: typebox_1.Type.String() })) })) })), meta: typebox_1.Type.Object({ total: typebox_1.Type.Number(), page: typebox_1.Type.Number(), limit: typebox_1.Type.Number(), hasMore: typebox_1.Type.Boolean() }) }); const route = (0, createApiRoute_1.createApiRoute)({ path: '/api/complex', method: 'GET', response: { 200: { contentType: 'application/json', body: complexSchema } }, handler: async (_req) => { return { status: 200, body: { data: [{ id: 1, attributes: { name: 'Item 1', tags: ['tag1', 'tag2'], metadata: { key1: 'string_value', key2: 42, key3: true } }, relationships: { children: [] } }], meta: { total: 1, page: 1, limit: 10, hasMore: false } } }; } }); (0, vitest_1.expect)(route.response?.[200]?.body).toBe(complexSchema); (0, vitest_1.expect)(route.response?.[200]?.body.type).toBe('object'); (0, vitest_1.expect)(route.response?.[200]?.body.properties.data).toBeDefined(); (0, vitest_1.expect)(route.response?.[200]?.body.properties.meta).toBeDefined(); }); (0, vitest_1.it)('should handle different JSON content types', () => { const jsonContentTypes = [ 'application/json', 'application/vnd.api+json', 'application/ld+json', 'text/json' ]; jsonContentTypes.forEach(contentType => { const route = (0, createApiRoute_1.createApiRoute)({ path: `/api/test-${contentType.replace(/[^a-zA-Z0-9]/g, '')}`, method: 'GET', response: { 200: { contentType: contentType, body: typebox_1.Type.Object({ message: typebox_1.Type.String(), contentType: typebox_1.Type.String() }) } }, handler: async (_req) => { return { status: 200, body: { message: 'Success', contentType } }; } }); (0, vitest_1.expect)(route.response?.[200]?.contentType).toBe(contentType); (0, vitest_1.expect)(route.response?.[200]?.body).toBeDefined(); }); }); (0, vitest_1.it)('should handle custom content types without body schema', () => { const customContentTypes = [ 'text/plain', 'text/html', 'application/octet-stream', 'image/png', 'application/pdf', 'text/csv' ]; customContentTypes.forEach(contentType => { const route = (0, createApiRoute_1.createApiRoute)({ path: `/api/custom-${contentType.replace(/[^a-zA-Z0-9]/g, '')}`, method: 'GET', response: { 200: { contentType: contentType } }, handler: async (_req) => { return { status: 200, custom: (res) => { res.setHeader('Content-Type', contentType); res.send('Custom content'); } }; } }); (0, vitest_1.expect)(route.response?.[200]?.contentType).toBe(contentType); (0, vitest_1.expect)(route.response?.[200]).not.toHaveProperty('body'); }); }); (0, vitest_1.it)('should validate response headers schema', () => { const responseHeaderSchema = typebox_1.Type.Object({ 'x-rate-limit': typebox_1.Type.String(), 'x-rate-limit-remaining': typebox_1.Type.String(), 'x-rate-limit-reset': typebox_1.Type.String(), 'cache-control': typebox_1.Type.Optional(typebox_1.Type.String()), 'etag': typebox_1.Type.Optional(typebox_1.Type.String()) }); const route = (0, createApiRoute_1.createApiRoute)({ path: '/api/with-headers', method: 'GET', response: { 200: { contentType: 'application/json', body: typebox_1.Type.Object({ message: typebox_1.Type.String() }), headers: responseHeaderSchema } }, handler: async (_req) => { return { status: 200, body: { message: 'Success' }, headers: { 'x-rate-limit': '1000', 'x-rate-limit-remaining': '999', 'x-rate-limit-reset': '2023-01-01T00:00:00Z', 'cache-control': 'no-cache' } }; } }); (0, vitest_1.expect)(route.response?.[200]?.headers).toBe(responseHeaderSchema); (0, vitest_1.expect)(route.response?.[200]?.headers?.type).toBe('object'); (0, vitest_1.expect)(route.response?.[200]?.headers?.properties['x-rate-limit']).toBeDefined(); }); (0, vitest_1.it)('should validate multiple response status codes with different schemas', () => { const route = (0, createApiRoute_1.createApiRoute)({ path: '/api/multi-status', method: 'POST', request: { body: typebox_1.Type.Object({ action: typebox_1.Type.Union([ typebox_1.Type.Literal('success'), typebox_1.Type.Literal('not-found'), typebox_1.Type.Literal('error') ]) }) }, response: { 200: { contentType: 'application/json', body: typebox_1.Type.Object({ result: typebox_1.Type.String(), data: typebox_1.Type.Object({ id: typebox_1.Type.Number(), name: typebox_1.Type.String() }) }) }, 404: { contentType: 'application/json', body: typebox_1.Type.Object({ error: typebox_1.Type.String(), resource: typebox_1.Type.String() }) }, 500: { contentType: 'application/json', body: typebox_1.Type.Object({ error: typebox_1.Type.String(), stack: typebox_1.Type.Optional(typebox_1.Type.String()) }) } }, handler: async (req) => { switch (req.body.action) { case 'success': return { status: 200, body: { result: 'success', data: { id: 1, name: 'Test' } } }; case 'not-found': return { status: 404, body: { error: 'Not found', resource: 'user' } }; case 'error': return { status: 500, body: { error: 'Internal server error' } }; default: return { status: 500, body: { error: 'Unknown action' } }; } } }); (0, vitest_1.expect)(route.response?.[200]?.body).toBeDefined(); (0, vitest_1.expect)(route.response?.[404]?.body).toBeDefined(); (0, vitest_1.expect)(route.response?.[500]?.body).toBeDefined(); (0, vitest_1.expect)(route.response?.[200]?.body.properties.result).toBeDefined(); (0, vitest_1.expect)(route.response?.[404]?.body.properties.error).toBeDefined(); (0, vitest_1.expect)(route.response?.[500]?.body.properties.error).toBeDefined(); }); }); //# sourceMappingURL=schema-validation.test.js.map