Skip to content

Commit

Permalink
v0.1.2 release (#1)
Browse files Browse the repository at this point in the history
* v0.1.2 release
  • Loading branch information
Thesephi authored Mar 4, 2024
1 parent 590cb3f commit 3a592f5
Show file tree
Hide file tree
Showing 15 changed files with 216 additions and 109 deletions.
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"editor.tabSize": 2,
"deno.enable": true,
"editor.formatOnSave": true,
"editor.defaultFormatter": "denoland.vscode-deno"
}
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## [0.1.2] - 2024-04-03

### Added

- Decorator `@Patch` to use on controller method handling HTTP Patch requests

### Fixed

- calls without a request payload should no longer result in 400 'Unable to
parse request body' error
- internal TypeScript update chores

## [0.1.1] - 2024-03-03

### Added
Expand Down
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ routing-controllers -like library for `jsr:@oak/oak` (https://jsr.io/@oak/oak)

## Forewords

If you're familiar with the [npm library routing-controllers](https://www.npmjs.com/package/routing-controllers), you'll find yourself very much at home.

However, please note that this libray is **not** meant to be a drop-in replacement for routing-controllers, as it attempts to conform to [TC39 Decorators proposal](https://github.com/tc39/proposal-decorators) which [doesn't support Method Parameter Decorator](https://github.com/tc39/proposal-decorators?tab=readme-ov-file#comparison-with-typescript-experimental-decorators) yet. There's currently no plan to support TypeScript "experimental" decorators, but if you feel strongly for it, please feel free to fork this repo!
If you're familiar with the
[npm library routing-controllers](https://www.npmjs.com/package/routing-controllers),
you'll find yourself very much at home.

However, please note that this libray is **not** meant to be a drop-in
replacement for routing-controllers, as it attempts to conform to
[TC39 Decorators proposal](https://github.com/tc39/proposal-decorators) which
[doesn't support Method Parameter Decorator](https://github.com/tc39/proposal-decorators?tab=readme-ov-file#comparison-with-typescript-experimental-decorators)
yet. There's currently no plan to support TypeScript "experimental" decorators,
but if you feel strongly for it, please feel free to fork this repo!

## Example Usage

Expand Down
9 changes: 8 additions & 1 deletion deps.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
export { join } from "jsr:@std/path@^0.218.2";

export {
Application,
Context,
RouteContext,
Router,
} from "jsr:@oak/oak@^14.1.1";
export type { Next, RouterMiddleware, State } from "jsr:@oak/oak@^14.1.1";

export type {
Next,
RouteParams,
RouterMiddleware,
State,
} from "jsr:@oak/oak@^14.1.1";
2 changes: 1 addition & 1 deletion jsr.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dklab/oak-routing-ctrl",
"version": "0.1.1",
"version": "0.1.2",
"exports": {
".": "./mod.ts"
}
Expand Down
3 changes: 2 additions & 1 deletion mod.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export * from "./src/useOakServer.ts";
export { useOakServer } from "./src/useOakServer.ts";
export { Controller } from "./src/Controller.ts";
export { ControllerActionArgs } from "./src/ControllerActionArgs.ts";
export { Get } from "./src/Get.ts";
export { Post } from "./src/Post.ts";
export { Put } from "./src/Put.ts";
export { Patch } from "./src/Patch.ts";
export { Delete } from "./src/Delete.ts";
11 changes: 5 additions & 6 deletions src/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@ import { debug } from "./utils/logger.ts";
import { join } from "../deps.ts";
import { store } from "./Store.ts";

export abstract class BaseController {
// [key: string]: (ctx: Context) => any
}

export type Controller = new () => BaseController;
/**
* Just a standard Class, that can be decorated with the `@Controller` decorator
*/
export type ControllerClass = new (args?: unknown) => unknown;

/**
* Decorator that should be used on the Controller Class
*/
export const Controller =
(pathPrefix: string = "") =>
(target: Controller, context: ClassDecoratorContext) => {
(target: ControllerClass, context: ClassDecoratorContext) => {
debug(`invoking ControllerDecorator for ${target.name}`, context);
const fnNames: string[] = Object.getOwnPropertyNames(target.prototype);
for (const fnName of fnNames) {
Expand Down
145 changes: 98 additions & 47 deletions src/ControllerActionArgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,56 +5,107 @@ import { Context, RouteContext } from "../deps.ts";
* Decorator that should be used on the Controller Class Method
* when we need to refer to request body, params, etc. in the
* method body
* @returns a function with the signature (arguments) matching that
* of the provided `desirableParams`, which can then be **decorated**
* with one of the Class Method decorators (e.g. `Get`, `Post`, etc.)
* @example
* ```ts
* (@)Controller('/api/v1')
* class MyClass {
* (@)Get('/:resource')
* (@)ControllerActionArgs('resource')
* doSomething(resource: string): void {
* console.log(`endpoint called: /api/v1/${resource}`)
* }
* }
* ```
*/
export const ControllerActionArgs =
(...desirableParams: string[]) =>
(target: Function, context: ClassMethodDecoratorContext): any => {
debug(`invoking ControllerActionCtx Decorator`, target);

const methodName = context.name;
async function retVal(this: ThisType<unknown>, ...args: any[]) {
// original args: (ctx)
const ctx: Context & RouteContext<string> = args.shift();
// expected (decorated) args: desirableParams e.g. (tenant, language, body, ctx)

let parsedReqBody: unknown;
try {
// https://github.com/oakserver/oak?tab=readme-ov-file#request-body
// @TODO make body parsing more sophisticated?
parsedReqBody = await ctx.request.body.json();
} catch (e) {
return ctx.throw(400, `Unable to parse request body: ${e.message}`, {
stack: e.stack,
});
}
export const ControllerActionArgs = (...desirableParams: string[]) =>
(
// deno-lint-ignore ban-types
target: Function,
context: ClassMethodDecoratorContext,
// deno-lint-ignore no-explicit-any
): any => {
debug(`invoking ControllerActionCtx Decorator`, target);

let decoratedArgs: any[] = [];
for (const p of desirableParams) {
switch (true) {
case ["ctx", "context"].includes(p):
// ctx to be handled separately
decoratedArgs.push(ctx);
break;
case p === "body":
// body to be handled separately
decoratedArgs.push(parsedReqBody);
break;
default:
// otherwise assume it's all ctx params
decoratedArgs.push(ctx.params[p]);
}
}
const methodName = context.name;

// squeze ctx at the end anyways, as many users might be familiar
// with this (and some often forget to declare `ctx` explicitly when using the Decorator)
decoratedArgs.push(ctx);
// deno-lint-ignore no-explicit-any
async function retVal(this: ThisType<unknown>, ...args: any[]) {
// original args passed in by the framework: (ctx)
const ctx: Context & RouteContext<string> = args.shift();

return await target.call(this, ...decoratedArgs);
let parsedReqBody: unknown;
try {
parsedReqBody = await parseOakReqBody(ctx);
} catch (e) {
return ctx.throw(400, `Unable to parse request body: ${e.message}`, {
stack: e.stack,
});
}

const decoratedArgs: unknown[] = [];
// expected (decorated) args are "extracted" from `ctx`
// per `desirableParams`
for (const p of desirableParams) {
switch (true) {
case ["ctx", "context"].includes(p):
// ctx to be handled separately
decoratedArgs.push(ctx);
break;
case p === "body":
// body to be handled separately
decoratedArgs.push(parsedReqBody);
break;
default:
// otherwise assume it's all from ctx params
decoratedArgs.push(ctx.params[p]);
// @TODO consider if it's also desirable to extract the arg
// from `parsedReqBody` as an automatic 'fallback' after `ctx.params`
}
}

Object.defineProperty(retVal, "name", {
value: methodName,
writable: false,
});
return retVal;
};
// squeze ctx at the end anyways, as many users might be familiar
// with it being last (and also people often forget to declare `ctx` explicitly
// when using the Decorator)
decoratedArgs.push(ctx);

return await target.call(this, ...decoratedArgs);
}

Object.defineProperty(retVal, "name", {
value: methodName,
writable: false,
});

return retVal;
};

async function parseOakReqBody(
ctx: Context & RouteContext<string>,
): Promise<unknown> {
let retVal: unknown;
// https://github.com/oakserver/oak?tab=readme-ov-file#request-body
switch (ctx.request.body.type()) {
case "json":
retVal = await ctx.request.body.json();
break;
case "text":
retVal = await ctx.request.body.text();
break;
case "binary":
retVal = await ctx.request.body.blob();
break;
case "form":
retVal = await ctx.request.body.form();
break;
case "form-data":
retVal = await ctx.request.body.formData();
break;
case "unknown":
default:
retVal = await ctx.request.body.arrayBuffer();
}
return retVal;
}
18 changes: 9 additions & 9 deletions src/Delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { register } from "./Store.ts";
* Decorator that should be used on the Controller Class Method
* for DELETE endpoints
*/
export const Delete =
(path: string = "") =>
(target: Function, context: ClassMethodDecoratorContext) => {
debug(
`invoking Delete MethodDecorator for ${target.name} with pathPrefix ${path}`,
context,
);
register("delete", path, target.name);
};
export const Delete = (path: string = "") =>
// deno-lint-ignore ban-types
(target: Function, context: ClassMethodDecoratorContext) => {
debug(
`invoking Delete MethodDecorator for ${target.name} with pathPrefix ${path}`,
context,
);
register("delete", path, target.name);
};
18 changes: 9 additions & 9 deletions src/Get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { register } from "./Store.ts";
* Decorator that should be used on the Controller Class Method
* for GET endpoints
*/
export const Get =
(path: string = "") =>
(target: Function, context: ClassMethodDecoratorContext) => {
debug(
`invoking Get MethodDecorator for ${target.name} with pathPrefix ${path}`,
context,
);
register("get", path, target.name);
};
export const Get = (path: string = "") =>
// deno-lint-ignore ban-types
(target: Function, context: ClassMethodDecoratorContext) => {
debug(
`invoking Get MethodDecorator for ${target.name} with pathPrefix ${path}`,
context,
);
register("get", path, target.name);
};
16 changes: 16 additions & 0 deletions src/Patch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { debug } from "./utils/logger.ts";
import { register } from "./Store.ts";

/**
* Decorator that should be used on the Controller Class Method
* for PATCH endpoints
*/
export const Patch = (path: string = "") =>
// deno-lint-ignore ban-types
(target: Function, context: ClassMemberDecoratorContext) => {
debug(
`invoking Patch MethodDecorator for ${target.name} with pathPrefix ${path}`,
context,
);
register("patch", path, target.name);
};
20 changes: 10 additions & 10 deletions src/Post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { register } from "./Store.ts";
* Decorator that should be used on the Controller Class Method
* for POST endpoints
*/
export const Post =
(path: string = "") =>
(target: Function, context: ClassMethodDecoratorContext) => {
debug(
`invoking Post MethodDecorator for ${target.name} with pathPrefix ${path}`,
context,
// target.constructor[Symbol.metadata],
);
register("post", path, target.name);
};
export const Post = (path: string = "") =>
// deno-lint-ignore ban-types
(target: Function, context: ClassMethodDecoratorContext) => {
debug(
`invoking Post MethodDecorator for ${target.name} with pathPrefix ${path}`,
context,
// target.constructor[Symbol.metadata],
);
register("post", path, target.name);
};
18 changes: 9 additions & 9 deletions src/Put.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { register } from "./Store.ts";
* Decorator that should be used on the Controller Class Method
* for PUT endpoints
*/
export const Put =
(path: string = "") =>
(target: Function, context: ClassMemberDecoratorContext) => {
debug(
`invoking Put MethodDecorator for ${target.name} with pathPrefix ${path}`,
context,
);
register("put", path, target.name);
};
export const Put = (path: string = "") =>
// deno-lint-ignore ban-types
(target: Function, context: ClassMemberDecoratorContext) => {
debug(
`invoking Put MethodDecorator for ${target.name} with pathPrefix ${path}`,
context,
);
register("put", path, target.name);
};
Loading

0 comments on commit 3a592f5

Please sign in to comment.