@prism-engineer/router
Version:
Type-safe Express.js router with automatic client generation
415 lines • 17.4 kB
JavaScript
"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 - Path Parameters', () => {
(0, vitest_1.it)('should extract single path parameter', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/users/{id}',
method: 'GET',
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
id: typebox_1.Type.String(),
name: typebox_1.Type.String()
})
}
},
handler: async (req) => {
const id = req.params.id;
return {
status: 200,
body: { id, name: 'John Doe' }
};
}
});
(0, vitest_1.expect)(route.path).toBe('/api/users/{id}');
(0, vitest_1.expect)(typeof route.handler).toBe('function');
});
(0, vitest_1.it)('should extract multiple path parameters', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/users/{userId}/posts/{postId}',
method: 'GET',
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
userId: typebox_1.Type.String(),
postId: typebox_1.Type.String(),
title: typebox_1.Type.String()
})
}
},
handler: async (req) => {
const userId = req.params.userId;
const postId = req.params.postId;
return {
status: 200,
body: { userId, postId, title: 'Sample Post' }
};
}
});
(0, vitest_1.expect)(route.path).toBe('/api/users/{userId}/posts/{postId}');
(0, vitest_1.expect)(typeof route.handler).toBe('function');
});
(0, vitest_1.it)('should handle complex path parameter patterns', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/organizations/{orgId}/projects/{projectId}/issues/{issueId}',
method: 'GET',
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
orgId: typebox_1.Type.String(),
projectId: typebox_1.Type.String(),
issueId: typebox_1.Type.String(),
issue: typebox_1.Type.Object({
title: typebox_1.Type.String(),
status: typebox_1.Type.String()
})
})
}
},
handler: async (req) => {
const orgId = req.params.orgId;
const projectId = req.params.projectId;
const issueId = req.params.issueId;
return {
status: 200,
body: {
orgId,
projectId,
issueId,
issue: {
title: 'Sample Issue',
status: 'open'
}
}
};
}
});
(0, vitest_1.expect)(route.path).toBe('/api/organizations/{orgId}/projects/{projectId}/issues/{issueId}');
(0, vitest_1.expect)(typeof route.handler).toBe('function');
});
(0, vitest_1.it)('should handle mixed path parameters with static segments', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/v1/users/{userId}/settings/profile/{profileId}',
method: 'PUT',
request: {
body: typebox_1.Type.Object({
displayName: typebox_1.Type.String(),
bio: typebox_1.Type.Optional(typebox_1.Type.String())
})
},
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
userId: typebox_1.Type.String(),
profileId: typebox_1.Type.String(),
updated: typebox_1.Type.Boolean()
})
}
},
handler: async (req) => {
const userId = req.params.userId;
const profileId = req.params.profileId;
return {
status: 200,
body: { userId, profileId, updated: true }
};
}
});
(0, vitest_1.expect)(route.path).toBe('/api/v1/users/{userId}/settings/profile/{profileId}');
(0, vitest_1.expect)(route.request?.body).toBeDefined();
});
(0, vitest_1.it)('should handle path parameters with query parameters', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/users/{userId}/posts',
method: 'GET',
request: {
query: typebox_1.Type.Object({
page: typebox_1.Type.Optional(typebox_1.Type.Number()),
limit: typebox_1.Type.Optional(typebox_1.Type.Number()),
status: typebox_1.Type.Optional(typebox_1.Type.Union([
typebox_1.Type.Literal('draft'),
typebox_1.Type.Literal('published'),
typebox_1.Type.Literal('archived')
]))
})
},
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
userId: typebox_1.Type.String(),
posts: typebox_1.Type.Array(typebox_1.Type.Object({
id: typebox_1.Type.String(),
title: typebox_1.Type.String(),
status: typebox_1.Type.String()
})),
pagination: typebox_1.Type.Object({
page: typebox_1.Type.Number(),
limit: typebox_1.Type.Number(),
total: typebox_1.Type.Number()
})
})
}
},
handler: async (req) => {
const userId = req.params.userId;
const page = req.query.page;
const limit = req.query.limit;
const status = req.query.status;
return {
status: 200,
body: {
userId,
posts: [{
id: '1',
title: 'Sample Post',
status: status || 'published'
}],
pagination: {
page: page || 1,
limit: limit || 10,
total: 1
}
}
};
}
});
(0, vitest_1.expect)(route.path).toBe('/api/users/{userId}/posts');
(0, vitest_1.expect)(route.request?.query).toBeDefined();
});
(0, vitest_1.it)('should handle path parameters with request body', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/users/{userId}/posts/{postId}',
method: 'PUT',
request: {
body: typebox_1.Type.Object({
title: typebox_1.Type.String(),
content: typebox_1.Type.String(),
tags: typebox_1.Type.Array(typebox_1.Type.String()),
metadata: typebox_1.Type.Optional(typebox_1.Type.Object({
category: typebox_1.Type.String(),
priority: typebox_1.Type.Number()
}))
})
},
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
userId: typebox_1.Type.String(),
postId: typebox_1.Type.String(),
updated: typebox_1.Type.Boolean(),
updatedAt: typebox_1.Type.String()
})
}
},
handler: async (req) => {
const userId = req.params.userId;
const postId = req.params.postId;
const title = req.body.title;
const content = req.body.content;
return {
status: 200,
body: {
userId,
postId,
updated: true,
updatedAt: new Date().toISOString()
}
};
}
});
(0, vitest_1.expect)(route.path).toBe('/api/users/{userId}/posts/{postId}');
(0, vitest_1.expect)(route.request?.body).toBeDefined();
});
(0, vitest_1.it)('should handle path parameters with headers', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/users/{userId}/avatar',
method: 'POST',
request: {
headers: typebox_1.Type.Object({
'content-type': typebox_1.Type.String(),
'content-length': typebox_1.Type.String(),
'x-upload-session': typebox_1.Type.Optional(typebox_1.Type.String())
})
},
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
userId: typebox_1.Type.String(),
avatarUrl: typebox_1.Type.String(),
uploaded: typebox_1.Type.Boolean()
})
}
},
handler: async (req) => {
const userId = req.params.userId;
const contentType = req.headers['content-type'];
const contentLength = req.headers['content-length'];
return {
status: 200,
body: {
userId,
avatarUrl: `https://example.com/avatars/${userId}.jpg`,
uploaded: true
}
};
}
});
(0, vitest_1.expect)(route.path).toBe('/api/users/{userId}/avatar');
(0, vitest_1.expect)(route.request?.headers).toBeDefined();
});
(0, vitest_1.it)('should handle path parameters with different HTTP methods', () => {
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
methods.forEach(method => {
const route = (0, createApiRoute_1.createApiRoute)({
path: `/api/resources/{id}`,
method,
request: method === 'GET' || method === 'DELETE' ? undefined : {
body: typebox_1.Type.Object({
name: typebox_1.Type.String(),
value: typebox_1.Type.String()
})
},
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
id: typebox_1.Type.String(),
method: typebox_1.Type.String(),
success: typebox_1.Type.Boolean()
})
}
},
handler: async (req) => {
const id = req.params.id;
return {
status: 200,
body: { id, method, success: true }
};
}
});
(0, vitest_1.expect)(route.path).toBe('/api/resources/{id}');
(0, vitest_1.expect)(route.method).toBe(method);
});
});
(0, vitest_1.it)('should handle numeric-looking path parameters as strings', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/users/{userId}/orders/{orderId}',
method: 'GET',
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
userId: typebox_1.Type.String(),
orderId: typebox_1.Type.String(),
userIdAsNumber: typebox_1.Type.Number(),
orderIdAsNumber: typebox_1.Type.Number()
})
}
},
handler: async (req) => {
// Path parameters are always strings, but can be converted
const userId = req.params.userId;
const orderId = req.params.orderId;
const userIdAsNumber = parseInt(userId);
const orderIdAsNumber = parseInt(orderId);
return {
status: 200,
body: {
userId,
orderId,
userIdAsNumber,
orderIdAsNumber
}
};
}
});
(0, vitest_1.expect)(route.path).toBe('/api/users/{userId}/orders/{orderId}');
(0, vitest_1.expect)(typeof route.handler).toBe('function');
});
(0, vitest_1.it)('should handle UUID-style path parameters', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/sessions/{sessionId}/tokens/{tokenId}',
method: 'DELETE',
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
sessionId: typebox_1.Type.String(),
tokenId: typebox_1.Type.String(),
revoked: typebox_1.Type.Boolean()
})
},
404: {
contentType: 'application/json',
body: typebox_1.Type.Object({
error: typebox_1.Type.String(),
sessionId: typebox_1.Type.String(),
tokenId: typebox_1.Type.String()
})
}
},
handler: async (req) => {
const sessionId = req.params.sessionId;
const tokenId = req.params.tokenId;
// Simulate UUID validation
const isValidUUID = (str) => str.length === 36 && str.includes('-');
if (!isValidUUID(sessionId) || !isValidUUID(tokenId)) {
return {
status: 404,
body: { error: 'Invalid ID format', sessionId, tokenId }
};
}
return {
status: 200,
body: { sessionId, tokenId, revoked: true }
};
}
});
(0, vitest_1.expect)(route.path).toBe('/api/sessions/{sessionId}/tokens/{tokenId}');
(0, vitest_1.expect)(route.response?.[200]).toBeDefined();
(0, vitest_1.expect)(route.response?.[404]).toBeDefined();
});
(0, vitest_1.it)('should handle path parameters with special characters in route names', () => {
const route = (0, createApiRoute_1.createApiRoute)({
path: '/api/files/{fileId}/versions/{versionId}',
method: 'GET',
response: {
200: {
contentType: 'application/json',
body: typebox_1.Type.Object({
fileId: typebox_1.Type.String(),
versionId: typebox_1.Type.String(),
fileName: typebox_1.Type.String(),
version: typebox_1.Type.String()
})
}
},
handler: async (req) => {
const fileId = req.params.fileId;
const versionId = req.params.versionId;
return {
status: 200,
body: {
fileId,
versionId,
fileName: `file-${fileId}`,
version: `v${versionId}`
}
};
}
});
(0, vitest_1.expect)(route.path).toBe('/api/files/{fileId}/versions/{versionId}');
(0, vitest_1.expect)(typeof route.handler).toBe('function');
});
});
//# sourceMappingURL=path-parameters.test.js.map