Alosaur - Deno web framework 🦖.
onPreAction, onPostAction, onCatchAction
microsoft/TSyringe
(more about alosaur injection).Q4 2020 – Oct-Dec
context.security.auth.signOutAsync
, signInAsync
, identity
app.ts:
import { Controller, Get, Area, App } from 'https://deno.land/x/alosaur@v0.21.1/mod.ts';
@Controller() // or specific path @Controller("/home")
export class HomeController {
@Get() // or specific path @Get("/hello")
text() {
return 'Hello world';
}
}
// Declare module
@Area({
controllers: [HomeController],
})
export class HomeArea {}
// Create alosaur application
const app = new App({
areas: [HomeArea],
});
app.listen();
tsconfig.app.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
And run
deno run --allow-net --allow-read --config ./tsconfig.app.json app.ts
Add render views: Dejs and Handlebars
Add return value JSON
Add decorators:
@Area
@QueryParam
@Param
param from url: /:id
@Body
@Cookie
@Req
@Res
@Ctx
@Middleware
with regex route@UseHook
for contoller and actionsAdd middleware
Add static middleware (example: app.useStatic)
Add CORS middleware
Add SPA middleware
Add DI
Add std exceptions
Add CI with minimal tests.
Add OpenAPI v3 generator (see /examples/basic/openapi.ts)
Add OpenAPI type reference
Add Hooks example
Add WebSocket
Add SSE
Add validators example class-validator
Add microservice connector with WASM
Transfer to Alosaur github organization
Add docs and more examples
Plugins & modules
Examples
Example with parse type reference
Basic example:
AlosaurOpenApiBuilder.create(settings)
.registerControllers()
.addTitle("Basic Application")
.addVersion("1.0.0")
.addDescription("Example Alosaur OpenApi generate")
.addServer({
url: "http://localhost:8000",
description: "Local server",
})
.saveToFile("./examples/basic/api.json");
Generate OpenAPI file:
deno run -A --config ./src/tsconfig.lib.json examples/basic/openapi.ts
For support type references you can use JSDoc with Deno doc parse like this:
// Parse controllers. Input path to your application
const docs = await AlosaurOpenApiBuilder.parseDenoDoc("./openapi/e2e/app.ts");
// create builder and add docs, then register controllers and add scheme components
const builder = AlosaurOpenApiBuilder.create(ProductAppSettings)
.addDenoDocs(docs)
.registerControllers()
.addSchemeComponents()
...
How to teaching how to correctly assemble controllers?
You must put in the JsDoc decorator as @decorator
ECMAScript decorators are sometimes an important part of an API contract. However, today the TypeScript compiler does not represent decorators in the .d.ts output files used by API consumers. The @decorator tag provides a workaround, enabling a decorator expression to be quoted in a doc comment. https://tsdoc.org/pages/tags/decorator/
Example:
@Controller()
/**
* Product controller
* @decorator Controller
*/
export class ProductController {
/**
* Gets product by id
* @summary action test
* @remarks Awesomeness!
* @param {id} The product id
* @decorator Get
*/
@Get("/:id")
GetById(@Param("id") id: string) {
return new Product();
}
}
You can also add what media types can be expected in the body. Use RequestBody param in JsDoc
/**
* Create product
* @param product
* @decorator Post
* @RequestBody application/xml
* @RequestBody application/json
*/
@Post("/")
Create(@Body() product: Product) {
}
You can also add what types can be returned from a controller method. Use decorator ProducesResponse
/**
* Gets product by id
* @summary action test
* @remarks Awesomeness!
* @param {id} The product id
* @decorator Get
*/
@Get("/:id")
@ProducesResponse({ code: 200, type: Product, description: "Product founded" })
@ProducesResponse({ code: 404, type: NotFoundResult, description: "Product has missing/invalid values" })
@ProducesResponse({ code: 500, description: "Oops! Can't create your product right now" })
GetById(@Param("id") id: string) {
return new Product();
}
To declare more information in types and models you can add other JsDoc parameters
/**
* Entity of product
*/
export class Product {
/**
* @summary Identifer of code
* @example 1
*/
id?: number;
/**
* @summary Array of test case
* @example [1,2,3]
*/
arr?: number[];
/**
* @summary Type of product
* @example {id:1}
*/
type?: ProductType;
/**
* @maximum 100
*/
count?: number;
}
Alosaur openapi parser currently supports the following types and expressions:
interface PropertyJsDocObject{
title? : string;
pattern? : string;
multipleOf? : number;
maximum? : number;
minimum? : number;
exclusiveMaximum? : boolean;
exclusiveMinimum? : boolean;
maxLength? : number;
minLength? : number;
maxItems? : number;
minItems? : number;
uniqueItems? : boolean;
maxProperties? : number;
minProperties? : number;
required? : boolean;
}
Keywords:
export interface JsDocObject {
example?: string;
decorator?: string;
default?: string;
description?: string;
deprecated?: boolean;
required?: boolean;
remarks?: string;
summary?: string;
format?: string;
params?: string[];
/**
* Request body media type uses in controllers
* application/json, application/xml, text/plain, etc
* https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#requestBodyObject
*/
RequestBody?: string[];
}
Ts types, Object Date Symbol Map JSON RegExp String ArrayBuffer DataView Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array Float32Array Float64Array
You can create middleware and register it in area or all application layer.
@Middleware(new RegExp('/'))
export class Log implements MiddlewareTarget<TState> {
date: Date = new Date();
onPreRequest(context: Context<TState>) {
return new Promise<void>((resolve, reject) => {
this.date = new Date();
resolve();
});
}
onPostRequest(context: Context<TState>) {
return new Promise<void>((resolve, reject) => {
console.log(new Date().getTime() - this.date.getTime());
resolve();
});
}
}
Register in app settings
const settings: AppSettings = {
areas: [HomeArea, InfoArea],
middlewares: [Log], // The order in this array corresponds to the order of the run middleware
};
or in app
const app = new App(settings);
app.use(/\//, new Log());
Use context.response.setNotRespond()
for return the rest of the requests
import { acceptWebSocket } from "https://deno.land/std@0.80.0/ws/mod.ts";
import { Context, PreRequestMiddleware } from "https://deno.land/x/alosaur/mod.ts";
export class WebsocketMiddleware implements PreRequestMiddleware {
onPreRequest(context: Context) {
const { conn, r: bufReader, w: bufWriter, headers } =
context.request.serverRequest;
acceptWebSocket({
conn,
bufReader,
bufWriter,
headers,
})
.then(ChatHandler) // execute chat
.catch(async (e) => {
console.error(`failed to accept websocket: ${e}`);
await context.request.serverRequest.respond({ status: 400 });
});
context.response.setNotRespond(); // It is necessary to return the rest of the requests by standard
}
}
Use context.response.setNotRespond()
for return the rest of the requests
import { acceptSSE, Context, PreRequestMiddleware } from "https://deno.land/x/alosaur/mod.ts";
export class SseMiddleware implements PreRequestMiddleware {
async onPreRequest(context: Context) {
acceptSSE(context).then(ChatHandler) // execute chat
.catch(async (e) => {
console.error(`failed to accept sse: ${e}`);
await context.request.serverRequest.respond({ status: 400 });
});
context.response.setNotRespond();
}
}
Hooks - middleware for area, controller and actions with supports DI container.
Hook in Alosaur there are three types: onPreAction, onPostAction, onCatchAction
.
type PayloadType = string; // can use any type for payload
type State = any;
export class MyHook implements HookTarget<State, PayloadType> {
// this hook run before controller action
onPreAction(context: Context<State>, payload: PayloadType) {
// you can rewrite result and set request immediately
context.response.result = Content({error: {token: false}}, 403);
context.response.setImmediately();
// if response setted immediately no further action will be taken
};
// this hook run after controller action
onPostAction(context: Context<State>, payload: PayloadType) {
// you can filtered response result here
};
// this hook run only throw exception in controller action
onCatchAction(context: Context<State>, payload: PayloadType) {
};
}
uses:
@UseHook(MyContollerHook) // or @UseHook(MyHook, 'payload') for all actions in controller
@Controller()
export class HomeController {
@UseHook(MyHook, 'payload') // only for one action
@Get('/')
text(@Res() res: any) {
return ``;
}
}
Errors that haven’t been caught elsewhere get in here
const app = new App(
// app settings
);
// added global error handler
app.error((context: Context<any>, error: Error) => {
context.response.result = Content("This page unprocessed error", (error as HttpError).httpCode || 500);
context.response.setImmediately();
});
There are 3 ways of information output
return {};
by default Status 200 OKreturn View("index", model);
return Redirect('/to/page')
return {}; // return 200 status
// or
return Content("Text or Model", 404); // return 404 status
// or
return View("page", 404); // return 404 status
Alosaur can suppport any html renderer. All you have to do is define the rendering function in the settings. For example Dejs, Handlebars, Angular, Eta
// Handlebars
...
// Basedir path
const viewPath = `${Deno.cwd()}/examples/handlebars/views`;
// Create Handlebars render
const handle = new Handlebars();
app.useViewRender({
type: 'handlebars',
basePath: viewPath,
getBody: async (path: string, model: any, config: ViewRenderConfig) => await handle.renderView(path, model),
});
...
Handlebars support custom config, more about handlebars for deno
new Handlebars(
{
baseDir: viewPath,
extname: '.hbs',
layoutsDir: 'layouts/',
partialsDir: 'partials/',
defaultLayout: 'main',
helpers: undefined,
compilerOptions: undefined,
}
)
You can use different transformers
For example class-validator
and class-transformer
for body.
post.model.ts:
import validator from "https://jspm.dev/class-validator@0.8.5";
const { Length, Contains, IsInt, Min, Max, IsEmail, IsFQDN, IsDate } =
validator;
export class PostModel {
@Length(10, 20)
title?: string;
@Contains("hello")
text?: string;
@IsInt()
@Min(0)
@Max(10)
rating?: number;
@IsEmail()
email?: string;
}
app.ts
import validator from "https://jspm.dev/class-validator@0.8.5";
import transformer from "https://jspm.dev/class-transformer@0.2.3";
import { App, Area, Controller, Post, Body } from 'https://deno.land/x/alosaur/mod.ts';
import { PostModel } from './post.model.ts';
const { validate } = validator;
const { plainToClass } = transformer;
// Create controller
@Controller()
export class HomeController {
@Post('/')
async post(@Body(PostModel) data: PostModel) {
return {
data,
errors: await validate(data)
}
}
}
// Declare controller in area
@Area({
controllers: [HomeController],
})
export class HomeArea { }
// Create app
const app = new App({
areas: [HomeArea],
});
// add transform function
app.useTransform({
type: 'body', // parse body params
getTransform: (transform: any, body: any) => {
return plainToClass(transform, body);
}
})
// serve application
app.listen();
You can also use just a function instead of a transformer.
function parser(body): ParsedObject {
// your code
return body;
}
...
@Post('/')
post(@Body(parser) data: ParsedObject) {
}
You can add any decorator and put it in a DI system.
Example with hooks:
import {
Content,
Context,
HookTarget,
BusinessType,
getMetadataArgsStorage,
container
} from "https://deno.land/x/alosaur/mod.ts";
type AuthorizeRoleType = string | undefined;
/**
* Authorize decorator with role
*/
export function Authorize(role?: AuthorizeRoleType): Function {
return function (object: any, methodName?: string) {
// add hook to global metadata
getMetadataArgsStorage().hooks.push({
type: methodName ? BusinessType.Action : BusinessType.Controller,
object,
target: object.constructor,
method: methodName,
instance: container.resolve(AutorizeHook),
payload: role,
});
};
}
export class AutorizeHook implements HookTarget<unknown, AuthorizeRoleType> {
onPreAction(context: Context<unknown>, role: AuthorizeRoleType) {
const queryParams = getQueryParams(context.request.url);
if (queryParams == undefined || queryParams.get("role") !== role) {
context.response.result = Content({ error: { token: false } }, 403);
context.response.setImmediately();
}
}
}
Then you can add anywhere you want. For example action of controller:
// ..controller
// action
@Authorize("admin")
@Get("/protected")
getAdminPage() {
return "Hi! this protected info";
}
Author: alosaur
Source Code: https://github.com/alosaur/alosaur
#deno #nodejs #node #javascript