Alosaur 🦖

Alosaur - Deno web framework 🦖.

test sponsors

  • Area - these are the modules of your application.
  • Controller - are responsible for controlling the flow of the application execution.
  • Middleware - provide a convenient mechanism for filtering HTTP requests entering your application.
  • Hooks - middleware for area, controller and actions with support DI. Have 3 life cyclic functions: onPreAction, onPostAction, onCatchAction
  • Decorators - for query, cookie, parametrs, routes and etc.
  • Dependency Injection - for all controllers and hooks by default from microsoft/TSyringe (more about alosaur injection).
  • Render pages any template render engine. (more)

中文说明

How do I use Alosaur in Deno Deploy? Use the light version of Alosaur: Alosaur Lite


Features roadmap

2021

  • Microservices (TCP) example
  • CLI: run applications
  • Create REPL http tool (tool for tests API, WebSockets etc), integrate with Alosaur openapi
  • Background process, BackgroundService, WebJobs, cron
  • Docs website

Jan-March

  • Response cache store, attribute
  • CLI: alosaur/cli (generate blank app, build openapi, tests and more)

Q4 2020 – Oct-Dec

  • WebSocket
  • SSE
  • Add Alosaur security.
    • Identifications middlwares like session
    • SecurityContext: context.security.auth.signOutAsync, signInAsync, identity
    • Authentication schemas (Cookies, JWT Bearer)
    • Authorization decorators and hooks, roles, policy
    • External auth strategies, OAuth base handler (Google, Facebook, Twitter, etc, examples) Docs, Example
  • OpenAPI type reference

Examples

Simple example

app.ts:

import {
  App,
  Area,
  Controller,
  Get,
} from "https://deno.land/x/alosaur@v0.28.0/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();

And run

deno run --allow-net app.ts


TODO

Add decorators:

  • @Area

  • @QueryParam

  • @Param param from url: /:id

  • @Body

  • @Cookie

  • @Req

  • @Res

  • @Ctx

  • @Middleware with regex route

  • @UseHook for contoller and actions

  • @ResponseCache

  • Support create custom decorators with app metadata

  • Add 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

  • Transfer to Alosaur github organization

  • Add docs and more examples

  • Plugins & modules

DI in Alosaur

Note: For injects instances you should emitDecoratorMetadata for run servers

tsconfig.json:

{
  "compilerOptions": {
    "emitDecoratorMetadata": true
  }
}

Then run

deno run --allow-net --allow-read --config ./tsconfig.json app.ts

OpenAPI v3

Example

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 found" })
@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

Middleware

You can create middleware and register it in area or all application layer.

Full example

@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());

WebSocket middleware example

Use context.response.setNotRespond() for return the rest of the requests

Full example

import { acceptWebSocket } from "https://deno.land/std@0.98.0/ws/mod.ts";
import {
  HttpContext,
  PreRequestMiddleware,
} from "https://deno.land/x/alosaur/mod.ts";

export class WebsocketMiddleware implements PreRequestMiddleware {
  onPreRequest(context: HttpContext) {
    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
  }
}

SSE middleware example

Use context.response.setNotRespond() for return the rest of the requests

Full example

import {
  acceptSSE,
  HttpContext,
  PreRequestMiddleware,
} from "https://deno.land/x/alosaur/mod.ts";

export class SseMiddleware implements PreRequestMiddleware {
  async onPreRequest(context: HttpContext) {
    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

Hooks - middleware for area, controller and actions with supports DI container.

Hook in Alosaur there are three types: onPreAction, onPostAction, onCatchAction.

Full example

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 ``;
  }
}

Global error handler

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();
});

Action outputs: Content, View, Redirect

There are 3 ways of information output

  • Content similar return {}; by default Status 200 OK
  • View uses with template engine, return View("index", model);
  • Redirect and RedirectPermanent status 301,302 return Redirect('/to/page')

Full example

return {}; // return 200 status

// or
return Content("Text or Model", 404); // return 404 status

// or
return View("page", 404); // return 404 status

Render pages

Alosaur can suppport any html renderer. All you have to do is define the rendering function in the settings. For example Dejs, Handlebars, Angular, React, 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,
  },
);

Multipart form-data, upload files

Full example

By default you can use @Body in action for read form-data with files.

import { FormFile } from "https://deno.land/std@0.98.0/mime/multipart.ts";
import { move } from "https://deno.land/std@0.98.0/fs/move.ts";

...

@Post()
async formData(@Body() body: { [key: string]: FormFile | string }) {
  const file: FormFile = body.file as FormFile;

  if (file) {
    const fileDest = "./examples/form-data/files/" + file.filename;

    // write file if file has content in memory
    if (file.content) {
      await Deno.writeFile(fileDest, file.content!, { append: true });
    } else if (file.tempfile) {
      // move file if file has tempfile
      move(file.tempfile, fileDest);
    }

    return "Uploaded";
  }

  return "File not exist";
}

You can also add your custom parsing options in the decorator @Body(NoopTransform, CustomBodyParser)

const CustomBodyParser: RequestBodyParseOptions = {
  formData: {
    maxMemory: 100, // in mb by default 10mb for default parser
    parser: func, // function of custom parser; (request: ServerRequest, contentType: string) => Promise<any>;
  },
};

Transformers and validators

You can use different transformers

For example class-validator and class-transformer for body.

Full example

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,
  Body,
  Controller,
  Post,
} 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) {

}

Custom Decorators

You can add any decorator and put it in a DI system.

Full example

Example with hooks:

import {
  BusinessType,
  container,
  Content,
  Context,
  getMetadataArgsStorage,
  HookTarget,
} 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";
  }

Sponsors

Backers