Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
eda41ff
chore: make /tests participate in type checking
imdhemy Jun 3, 2026
1023dfe
chore: add watch scripts
imdhemy Jun 3, 2026
4570dd1
wip: simplify imports
imdhemy Jun 3, 2026
a8211b3
wip: rename legacy decorator
imdhemy Jun 3, 2026
23f2004
wip: trim public api
imdhemy Jun 3, 2026
771fc65
wip: mark decorator as deprecated by dir name
imdhemy Jun 3, 2026
47a33ba
wip: move verifier to legacy decorator
imdhemy Jun 3, 2026
15bdfc4
wip: rename http method type
imdhemy Jun 3, 2026
af2ab24
wip: move http-method to the verb concern
imdhemy Jun 3, 2026
23bc53b
wip: rename type
imdhemy Jun 3, 2026
6c51786
wip: consolidate based on concerns
imdhemy Jun 3, 2026
2b67344
wip: fill testing gap
imdhemy Jun 3, 2026
efa1433
wip: rename type
imdhemy Jun 3, 2026
1d9451d
wip: move type
imdhemy Jun 3, 2026
9982166
wip: reorganize modules
imdhemy Jun 3, 2026
8dc8845
wip: extract type
imdhemy Jun 3, 2026
9f517ee
wip: delegate to fp route helper
imdhemy Jun 3, 2026
ef4a6fb
wip: rename type
imdhemy Jun 4, 2026
ed39b32
wip: consolidate create route def
imdhemy Jun 4, 2026
22cb013
wip: remove redundant test
imdhemy Jun 4, 2026
e94a9ec
wip: decouple modules
imdhemy Jun 4, 2026
11db04e
wip: consolidate source module
imdhemy Jun 4, 2026
0269796
wip: fix data types
imdhemy Jun 4, 2026
b3bd12a
wip: inline call
imdhemy Jun 4, 2026
1f51797
wip: reorder
imdhemy Jun 4, 2026
5b03682
wip: consolidate
imdhemy Jun 4, 2026
8b9a355
Update create-route-definition.ts
imdhemy Jun 4, 2026
827a6e4
wip: remove unnecessary type
imdhemy Jun 4, 2026
b0d3b5f
wip: consolidate into route
imdhemy Jun 4, 2026
d19d859
wip: move type to root
imdhemy Jun 4, 2026
39b79f7
wip: rename internal type
imdhemy Jun 4, 2026
527a571
wip: rename method
imdhemy Jun 4, 2026
af8aafa
wip: move route props to declaration
imdhemy Jun 4, 2026
659a970
wip: reogranize into declaration normalization
imdhemy Jun 4, 2026
f265cca
wip: consolidate
imdhemy Jun 4, 2026
0f4fb31
wip: move method type
imdhemy Jun 4, 2026
d0beaa2
wip: move route options to declaration
imdhemy Jun 4, 2026
bdf5b6c
wip: route method
imdhemy Jun 4, 2026
3861e79
wip: consolidate path
imdhemy Jun 4, 2026
1b72234
wip: move route source to normalization
imdhemy Jun 4, 2026
ad8f858
wip: flatten registration
imdhemy Jun 4, 2026
d2c8044
wip: new names
imdhemy Jun 4, 2026
c80375b
wip: introduce our type
imdhemy Jun 4, 2026
0b897fa
wip: introduce our types
imdhemy Jun 4, 2026
be8017e
wip: move http method type to root
imdhemy Jun 4, 2026
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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check ."
"format:check": "prettier --check .",
"watch:test": "vitest watch",
"watch:build": "tsc --noEmit --watch"
},
"dependencies": {
"@koa/router": "^15.1.1",
Expand Down
3 changes: 2 additions & 1 deletion src/Http/Request/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import type { JsonValue } from 'type-fest';

export type UploadedFile = File;
export type UploadedFilesMap = Record<string, UploadedFile | UploadedFile[]>;
export type HttpRequestBase = Request;

export interface HttpRequest extends Request {
export interface HttpRequest extends HttpRequestBase {
body?: Record<string, unknown> & JsonValue;
files?: UploadedFilesMap;
params: Record<string, unknown>;
Expand Down
8 changes: 4 additions & 4 deletions src/Http/Scope/types.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import type { Context, DefaultState, Next, Request } from 'koa';
import { type HttpRequest } from '../Request';
import type { Context, DefaultState, Next } from 'koa';
import { type HttpRequest, type HttpRequestBase } from '../Request';
import { type HttpResponse } from '../Response';
import { type User } from '@/Security/types';

export interface HttpScope<TRequest extends Request = HttpRequest, TUser extends User = User> extends Context {
export interface HttpScope<TRequest extends HttpRequestBase = HttpRequest, TUser extends User = User> extends Context {
request: TRequest;
response: HttpResponse;
user?: TUser;
}

export type NextMiddleware = Next;

export type HttpMiddleware<TRequest extends Request = HttpRequest> = (
export type HttpMiddleware<TRequest extends HttpRequestBase = HttpRequest> = (
scope: HttpScope<TRequest>,
next: NextMiddleware,
) => Promise<unknown>;
Expand Down
6 changes: 5 additions & 1 deletion src/Testing/TestAgentFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import { create } from '@/application/create-application';
import { type KoalaConfig } from '@/config/koala-config';
import { type HttpMiddleware, type HttpScope, type NextMiddleware } from '@/Http';
import { type User } from '@/Security/types';
import type { Request } from 'koa';
import supertest from 'supertest';
import { type TestAgent } from './types';

export function createTestAgent(config: KoalaConfig, agentConfig?: { actAs?: User }): TestAgent {
export function createTestAgent<TRequest extends Request>(
config: KoalaConfig<TRequest>,
agentConfig?: { actAs?: User },
): TestAgent {
const globalMiddleware = config.globalMiddleware ?? [];
const testConfig = { ...config };

Expand Down
2 changes: 1 addition & 1 deletion src/application/create-application.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { create } from '@/application/create-application';
import { koalaDefaultConfig } from '@/config/default-config';
import { Get, Route, RouteGroup } from '@/routing';
import { exclusiveRoutingModeError } from '@/routing/verify-routing-mode';
import { exclusiveRoutingModeError } from '@/routing/deprecated-decorator/verify-routing-mode';
import { expect, test } from 'vitest';

test('create app with default config', () => {
Expand Down
7 changes: 4 additions & 3 deletions src/application/create-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { serveStaticFiles } from '@/Http/Files';
import { applyConfiguredGlobalMiddleware } from '@/Http/middleware/apply-configured-global-middleware';
import { initializeRequestScopeStorage } from '@/Http/Scope/request-scope-storage';
import { registerEventSubscribers } from '@/Kernel';
import { registerLegacyRoutes } from '@/routing/decorator/legacy-router';
import { registerLegacyRoutes } from '@/routing/deprecated-decorator/legacy-router';
import { verifyRoutingMode } from '@/routing/deprecated-decorator/verify-routing-mode';
import { registerRoutes } from '@/routing/registration/register-routes';
import { verifyRoutingMode } from '@/routing/verify-routing-mode';
import type { Request } from 'koa';
import Koa from 'koa';
import { type Application } from './application';

export function create(config: KoalaConfig): Application {
export function create<TRequest extends Request>(config: KoalaConfig<TRequest>): Application {
const app = new Koa() as Application;
const controllers = config.controllers ?? [];

Expand Down
5 changes: 2 additions & 3 deletions src/config/koala-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { type StaticFilesOptions } from '@/Http/Files';
import { type EventSubscriber } from '@/Kernel';
import type { RouteSource } from '@/routing';
import type { DotenvConfigOptions } from 'dotenv';
import type { HttpRequest } from '@/Http';
import type { Request } from 'koa';
import type { HttpRequest, HttpRequestBase } from '@/Http';

/**
* @deprecated Use function-first routes from `@koala-ts/framework/routing` with `KoalaConfig.routes` instead.
Expand All @@ -13,7 +12,7 @@ export type Controller = new (...args: unknown[]) => unknown;

export type KoalaDotenvOptions = Pick<DotenvConfigOptions, 'debug' | 'encoding' | 'override' | 'quiet'>;

export interface KoalaConfig<TRequest extends Request = HttpRequest> {
export interface KoalaConfig<TRequest extends HttpRequestBase = HttpRequest> {
/**
* @deprecated Use `routes` with `Route` from `@koala-ts/framework/routing` instead.
*/
Expand Down
12 changes: 6 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Application
export { create } from '@/application/create-application';
export type { Application } from '@/application/application';
import { getLegacyRoutes, registerLegacyRoutes } from '@/routing/decorator/legacy-router';
import type { RouteMetadata as LegacyRouteMetadata } from '@/routing/decorator/route-metadata';
import { getLegacyRoutes, registerLegacyRoutes } from '@/routing/deprecated-decorator/legacy-router';
import type { RouteMetadata as LegacyRouteMetadata } from '@/routing/deprecated-decorator/route-metadata';

// Core modules
export type * from '@/config/koala-config';
Expand All @@ -12,10 +12,10 @@ export * from '@/Http';
export * from '@/Kernel';

// Routing
export * from '@/routing/decorator/route';
export type { HttpMethod } from '@/routing/http-method';
export type { RouteOptions } from '@/routing/route-options';
export type { RouterMethod } from '@/routing/router-method';
export * from '@/routing/deprecated-decorator/route';
export type { HttpMethod } from '@/routing/http-method.type';
export type { RouteOptions } from '@/routing/declaration/route-options.type';
export type { RouterMethod } from '@/routing/declaration/router-method.type';
/**
* @deprecated Use `Route` from `@koala-ts/framework/routing` instead.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,46 @@
import type { HttpMiddleware, HttpRequest } from '@/Http';
import type { HttpMethod } from '@/routing/http-method';
import type { RouteDefinition } from '@/routing/definition/route-definition';
import { Route } from '@/routing/route';
import type { Request } from 'koa';
import type { HttpMiddleware, HttpRequest, HttpRequestBase } from '@/Http';
import type { NormalizedRouteProps } from '@/routing/declaration/normalized-route-props.type';
import { Route } from '@/routing/declaration/route';
import type { HttpMethod } from '@/routing/http-method.type';

type RouteHandler<TRequest extends Request = HttpRequest> = HttpMiddleware<TRequest>;
type MiddlewareAndHandler<TRequest extends Request = HttpRequest> = [
type RouteHandler<TRequest extends HttpRequestBase = HttpRequest> = HttpMiddleware<TRequest>;
type MiddlewareAndHandler<TRequest extends HttpRequestBase = HttpRequest> = [
...middleware: HttpMiddleware<TRequest>[],
handler: RouteHandler<TRequest>,
];
type NamedMiddlewareAndHandler<TRequest extends Request = HttpRequest> = [
type NamedMiddlewareAndHandler<TRequest extends HttpRequestBase = HttpRequest> = [
name: string,
...middlewareAndHandler: MiddlewareAndHandler<TRequest>,
];
type VerbHelperRouteArguments<TRequest extends Request = HttpRequest> =
type VerbHelperRouteArguments<TRequest extends HttpRequestBase = HttpRequest> =
| MiddlewareAndHandler<TRequest>
| NamedMiddlewareAndHandler<TRequest>;
type UnsafeVerbHelperRouteArguments<TRequest extends Request = HttpRequest> =
type UnsafeVerbHelperRouteArguments<TRequest extends HttpRequestBase = HttpRequest> =
| VerbHelperRouteArguments<TRequest>
| [name: string]
| [];

interface VerbHelperArguments<TRequest extends Request = HttpRequest> {
interface VerbHelperArguments<TRequest extends HttpRequestBase = HttpRequest> {
path: string;
name?: string;
middleware: HttpMiddleware<TRequest>[];
handler: RouteHandler<TRequest>;
}

type NamedVerbHelper = {
<TRequest extends Request = HttpRequest>(
<TRequest extends HttpRequestBase = HttpRequest>(
path: string,
...middlewareAndHandler: MiddlewareAndHandler<TRequest>
): RouteDefinition<TRequest>;
<TRequest extends Request = HttpRequest>(
): NormalizedRouteProps<TRequest>;
<TRequest extends HttpRequestBase = HttpRequest>(
path: string,
name: string,
...middlewareAndHandler: MiddlewareAndHandler<TRequest>
): RouteDefinition<TRequest>;
): NormalizedRouteProps<TRequest>;
};

function createVerbHelper(method: HttpMethod): NamedVerbHelper {
return <TRequest extends Request = HttpRequest>(
return <TRequest extends HttpRequestBase = HttpRequest>(
path: string,
...routeArguments: UnsafeVerbHelperRouteArguments<TRequest>
) =>
Expand All @@ -51,7 +50,7 @@ function createVerbHelper(method: HttpMethod): NamedVerbHelper {
});
}

function resolveVerbHelperArguments<TRequest extends Request = HttpRequest>(
function resolveVerbHelperArguments<TRequest extends HttpRequestBase = HttpRequest>(
path: string,
routeArguments: UnsafeVerbHelperRouteArguments<TRequest>,
): VerbHelperArguments<TRequest> {
Expand All @@ -71,13 +70,13 @@ function resolveVerbHelperArguments<TRequest extends Request = HttpRequest>(
};
}

function isNamedVerbHelperRouteArguments<TRequest extends Request = HttpRequest>(
function isNamedVerbHelperRouteArguments<TRequest extends HttpRequestBase = HttpRequest>(
routeArguments: UnsafeVerbHelperRouteArguments<TRequest>,
): routeArguments is NamedMiddlewareAndHandler<TRequest> | [name: string] {
return typeof routeArguments[0] === 'string';
}

function requireVerbHelperHandler<TRequest extends Request = HttpRequest>(
function requireVerbHelperHandler<TRequest extends HttpRequestBase = HttpRequest>(
middlewareAndHandler: MiddlewareAndHandler<TRequest> | [],
isNamedRoute: boolean,
): RouteHandler<TRequest> {
Expand Down
13 changes: 13 additions & 0 deletions src/routing/declaration/normalized-route-props.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { HttpMiddleware, HttpRequest, HttpRequestBase } from '@/Http';
import type { RouteBodyOptions } from '@/routing/declaration/route-body-options.type';
import type { RouterMethod } from '@/routing/declaration/router-method.type';

export interface NormalizedRouteProps<TRequest extends HttpRequestBase = HttpRequest> {
name?: string;
path: string;
methods: RouterMethod[];
handler: HttpMiddleware<TRequest>;
middleware: HttpMiddleware<TRequest>[];
parseBody: boolean;
bodyOptions: RouteBodyOptions;
}
4 changes: 4 additions & 0 deletions src/routing/declaration/route-body-options.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface RouteBodyOptions {
multipart?: boolean;
[option: string]: unknown;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Get } from '@/routing/declaration/http-verb-helpers';
import { describe, expect, test } from 'vitest';
import { Get } from '@/routing/helpers/http-verb-helpers';
import { RouteGroup } from './route-group';

describe('route group', () => {
Expand Down
33 changes: 33 additions & 0 deletions src/routing/declaration/route-group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { HttpMiddleware, HttpRequest, HttpRequestBase } from '@/Http';

import { RouteProps } from '@/routing/declaration/route-props.type';
import type { RouteSource } from '@/routing/declaration/route-source.type';

export type RouteConfigOverlay<TRequest extends HttpRequestBase = HttpRequest> = Pick<
RouteProps<TRequest>,
'middleware' | 'options'
>;

export interface RouteGroupOptions<TRequest extends HttpRequestBase = HttpRequest> {
prefix?: string;
namePrefix?: string;
middleware?: HttpMiddleware<TRequest>[];
routeConfig?: Record<string, RouteConfigOverlay<TRequest>>;
}

export interface RouteGroupDefinition<TRequest extends HttpRequestBase = HttpRequest> {
kind: 'route-group';
options: RouteGroupOptions<TRequest>;
resolveRoutes: () => RouteSource<TRequest>[];
}

export function RouteGroup<TRequest extends HttpRequestBase = HttpRequest>(
options: RouteGroupOptions<TRequest>,
resolveRoutes: () => RouteSource<TRequest>[],
): RouteGroupDefinition<TRequest> {
return {
kind: 'route-group',
options,
resolveRoutes,
};
}
7 changes: 7 additions & 0 deletions src/routing/declaration/route-options.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { RouteBodyOptions } from '@/routing/declaration/route-body-options.type';

export type RouteOptions = Partial<
RouteBodyOptions & {
parseBody?: boolean;
}
>;
12 changes: 12 additions & 0 deletions src/routing/declaration/route-props.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { HttpMiddleware, HttpRequest, HttpRequestBase } from '@/Http';
import type { RouteOptions } from '@/routing/declaration/route-options.type';
import type { HttpMethod } from '@/routing/http-method.type';

export interface RouteProps<TRequest extends HttpRequestBase = HttpRequest> {
name?: string;
path: string;
method: HttpMethod | HttpMethod[];
handler: HttpMiddleware<TRequest>;
middleware?: HttpMiddleware<TRequest>[];
options?: RouteOptions;
}
7 changes: 7 additions & 0 deletions src/routing/declaration/route-source.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { HttpRequest, HttpRequestBase } from '@/Http';
import type { NormalizedRouteProps } from '@/routing/declaration/normalized-route-props.type';
import type { RouteGroupDefinition } from '@/routing/declaration/route-group';

export type RouteSource<TRequest extends HttpRequestBase = HttpRequest> =
| NormalizedRouteProps<TRequest>
| RouteGroupDefinition<TRequest>;
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { describe, expect, test, vi } from 'vitest';
import { createRouteDefinition } from './create-route-definition';
import { Route } from './route';

describe('create route definition', () => {
describe('routing route', () => {
test('it creates a route definition for a single method', () => {
const handler = vi.fn(async () => undefined);

const route = createRouteDefinition({
method: 'GET',
path: '/users',
handler,
});
const route = Route({ method: 'GET', path: '/users', handler });

expect(route).toEqual({
path: '/users',
Expand All @@ -25,7 +21,7 @@ describe('create route definition', () => {
const handler = vi.fn(async () => undefined);
const middleware = [vi.fn(async () => undefined)];

const route = createRouteDefinition({
const route = Route({
name: 'users.create',
method: 'POST',
path: '/users',
Expand Down Expand Up @@ -53,47 +49,31 @@ describe('create route definition', () => {
test('it qualifies multiple methods', () => {
const handler = vi.fn(async () => undefined);

const route = createRouteDefinition({
method: ['GET', 'POST'],
path: '/users',
handler,
});
const route = Route({ method: ['GET', 'POST'], path: '/users', handler });

expect(route.methods).toEqual(['get', 'post']);
});

test('it normalizes any and all to all', () => {
const handler = vi.fn(async () => undefined);

const route = createRouteDefinition({
method: ['ANY', 'ALL'],
path: '/users',
handler,
});
const route = Route({ method: ['ANY', 'ALL'], path: '/users', handler });

expect(route.methods).toEqual(['all']);
});

test('it normalizes any with specific methods to all', () => {
const handler = vi.fn(async () => undefined);

const route = createRouteDefinition({
method: ['ANY', 'GET'],
path: '/users',
handler,
});
const route = Route({ method: ['ANY', 'GET'], path: '/users', handler });

expect(route.methods).toEqual(['all']);
});

test('it removes duplicate specific methods case-insensitively', () => {
test('it removes duplicate specific methods case insensitively', () => {
const handler = vi.fn(async () => undefined);

const route = createRouteDefinition({
method: ['GET', 'get', 'POST'],
path: '/users',
handler,
});
const route = Route({ method: ['GET', 'get', 'POST'], path: '/users', handler });

expect(route.methods).toEqual(['get', 'post']);
});
Expand Down
Loading
Loading