Skip to content
master
Go to file
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
cli
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

attain

Attain - v1.1.1 - Website

attain ci license

nest badge

A middleware web framework for Deno which is using http standard library inspired by express and Oak.

Attain is blazingly fast due to handled the multi-structured middleware and routes effectively. It also strictly manage memory consumption.

Only for Deno - Require Deno version up to: 1.4.x

Any contributions to the code would be appreciated. :)


Download and use

Important: If you're using React Framework, highly recommend including with a version tag.

import { App, Router, Request, Response } from "https://deno.land/x/attain/mod.ts";
// or
import { App, Router, Request, Response } from "https://deno.land/x/attain@1.1.1/mod.ts";
// or
import { App, Router, Request, Response } from "https://x.nest.land/attain@1.1.1/mod.ts";
// or
import { App, Router, Request, Response } from "https://raw.githubusercontent.com/aaronwlee/attain/1.1.1/mod.ts";

This CLI is beta version!

// download cli
deno install -A -f --unstable -n attain https://deno.land/x/Attain/attain-cli.ts
// or
deno install -A -f --unstable -n attain https://deno.land/x/attain@1.1.1/attain-cli.ts
# deno run --allow-net --unstable main.ts

Contents

Getting Start

import { App, Request, Response } from "https://deno.land/x/attain/mod.ts";

const app = new App();

const sampleMiddleware = (req: Request, res: Response) => {
  console.log("before send");
};

app.get("/:id", (req, res) => {
  console.log(req.params);
  res.status(200).send(`id: ${req.params.id}`);
});

app.use(sampleMiddleware, (req, res) => {
  res.status(200).send({ status: "Good" });
});

app.listen({ port: 3500 });

Procedure explain

The middleware process the function step by step based on registered order.

alt text

import { App } from "https://deno.land/x/attain/mod.ts";

const app = new App();

const sleep = (time: number) => {
  return new Promise(resolve => setTimeout(() => resolve(), time)
};

app.use((req, res) => {
  console.log("First step");
}, async (req, res) => {
  await sleep(2000); // the current request procedure will stop here for two seconds.
  console.log("Second step");
});

app.use((req, res) => {
  // pend a job
  res.pend((afterReq, afterRes) => {
    console.log("Fourth step");
    console.log("Fifth step with error");
    console.log("You can finalize your procedure right before respond.")
    console.log("For instance, add a header or caching.")
  })
})

// last step
app.use("/", (req, res) => {
  console.log("Third step with GET '/'");
  // this is the end point
  res.status(200).send({status: "Good"});
});

app.use("/", (req, res) => {
  console.log("Will not executed");
});

app.get("/error", (req, res) => {
  console.log("Third step with GET '/error'");
  throw new Error("I have error!")
})

app.error((err, req, res) => {
  console.log("Fourth step with error");
  console.log("A sequence of error handling.", err)
  res.status(500).send("Critical error.");
})

app.listen({ port: 3500 });

CLI

This is beta version!

Important: If you're using React Framework, highly recommend including with a version tag.

deno install -A -f --unstable -n attain https://deno.land/x/attain@1.1.1/attain-cli.ts

It's providing a full-stack server-side rendering development environment combine with React and Attain.

It's a beta version and there are possibly exist some bugs.

note: This beta project yet to supporting any type of CSS modules.

TODO

  • - Dynamic routing supporting
  • - CSS supporting
  • - Implement self request method for SSR
  • - Improve hot reload

All commands are must be executed in the project directory

  • -h
    Get Help.

  • init [path]
    Initialize the project to the path.
    ex) attain init react-attain

  • dev | development
    Starts the dev server and watch the front-end file changes.
    ex) attain dev

  • build
    Build the bundles to the dist folder with "PRODUCTION" scripts
    ex) attain build

  • start
    Starts the production server
    ex) attain start

React

This react framework is only provided through the CLI. Please initialize and start with the init command line.

Routing and Page Guidline

Attain framework's used the high order component to serving routing and other contexts. At the compiler level, moreover, all the pages automatically imported into it. Thus, you don't need to worry about creating a route index.

You just need to create a page component into the following path to create a new page. /view/pages/<pathname>.tsx

Important: Page component's name must be unique.

Now, if the browser requests to <pathname> like "http://localhost:3000/", Attain server automatically serves a rendered page.

Dynamic Routing

Any browser request route like /user/1, /user/abc, will be matched by pages/user/[id].tsx in the server. The matched path parameter will be sent as a parameter to the page, and search parameters will be merged with query parameter.

ex) /view/pages/user/[id].tsx equals /user/1

Linking between pages

To client-side routing, the Attain framework provides you convince hook method which called useRouter(). This allows you to do client-side route transitions between pages, similarly to a single-page application.

  • const router = useRouter() import path deps.tsx
    useRouter returns you pathname, push, query, params.
    pathname is current pathname
    push("/") is for client-side route transition
    query is current search params
    params is current matched params like [id]
function Component() {
  const router = useRouter()

  return (
    <a onClick={() => router.push("/user")}>
      view user
    </a>
  )
}

Server Side Rendering

  • Component.ServerSideAttain
    ServerSideAttain enabled server-side rendering in a page and allows you to do the initial data population, it means sending the page with the data already populated from the server.


    This static function will be executed only one time when the browser requests the page. which means client-side rendering will not execute it.


    In the server-side execution, you can get req, res, Component, query, isServer as parameters.
    However, if it's been executed in the client, you only can get req, Component, query, isServer


    **You can only define it in the page component.

  • req
    In the server, you can get the entire request object from the Attain middleware.
    In the client, you only can get url object.

  • res
    Entire Attain response object, only available on the server.

  • Component
    This is current Component

  • isServer
    Determinator you're on the client or server. true or false

  • return
    You must return the object and it can be any.

UserComponent.ServerSideAttain = async () => {
  // due to windows can't get proxy, must have prefix with http://localhost:
  const response: any = await fetch("http://localhost:3000/api/user");
  const data = await response.json();

  return {
    data,
  };
};

SEO tools

The Attain framework also provides you with elegant SEO tools.

Import path: deps.tsx

  • useDocument
    Due to Deno does not provide document API, it's useful for avoiding errors when you use the document in the client-side. Which means the server can't handle the document code.
const dom = useDocument()
if(dom) {
  // use document
}
  • addMeta
addMeta("description", {
  content: "This is an example of a description"
})
  • addScript
addScript({
  id: "google-recaptcha-v3",
  src: "https://google...."
})
  • setTitle
setTitle("Welcome to my home page :)");

Fullstack apps made easier

  • useAsyncFetch
const { data, status, error } = useAsyncFetch('/api/users/login', { 
  method: 'POST', 
  body: JSON.stringify({ email, password }) 
})

How To

Web Socket Example

Auto Recovery

Boilerplate

A Deno web boilerplate by burhanahmeed

Methods and Properies

Response

Methods Getter

  • getResponse(): AttainResponse
    Get current response object, It will contain the body, status and headers.

  • headers(): Headers
    Get current header map

  • getStatus(): number | undefined
    Get current status

  • getBody(): Uint8Array
    Get current body contents

Functions

  • pend(...fn: CallBackType[]): void
    Pend the jobs. It'll start right before responding.

  • status(status: number)
    Set status number

  • body(body: ContentsType)
    Set body. Allows setting Uint8Array, Deno.Reader, string, object, boolean. This will not respond.

  • setHeaders(headers: Headers)
    You can overwrite the response header.

  • getHeader(name: string)
    Get a header from the response by key name.

  • setHeader(name: string, value: string)
    Set a header.

  • setContentType(type: string)
    This is a shortcut for the "Content-Type" in the header. It will try to find "Content-Type" from the header then set or append the values.

  • send(contents: ContentsType): Promise<void | this>
    Setting the body then executing the end() method.

  • await sendFile(filePath: string): Promise<void>
    Transfers the file at the given path. Sets the Content-Type response HTTP header field based on the filename's extension.
    Required to be await
    These response headers might be needed to set for fully functioning

Property Description
maxAge Sets the max-age property of the Cache-Control header in milliseconds or a string in ms format
root Root directory for relative filenames.
cacheControl Enable or disable setting Cache-Control response header.
  • await download(filePath: string, name?: string): Promise<void>
    Transfers the file at the path as an "attachment". Typically, browsers will prompt the user to download and save it as a name if provided.
    Required to be await

  • redirect(url: string | "back")
    Redirecting the current response.

  • end(): Promise<void>
    Executing the pended job then respond back to the current request. It'll end the current procedure.

Request

Oak for deno

This class used Oak's request library. Check this.
Note: to access Oak's Context.params use Request.params. but require to use a app.use(parser) plugin.

Router

Methods

  • use(app: App | Router): void
  • use(callBack: CallBackType): void
  • use(...callBack: CallBackType[]): void
  • use(url: string, callBack: CallBackType): void
  • use(url: string, ...callBack: CallBackType[]): void
  • use(url: string, app: App | Router): void
  • get...
  • post...
  • put...
  • patch...
  • delete...
  • error(app: App | Router): void;
  • error(callBack: ErrorCallBackType): void;
  • error(...callBack: ErrorCallBackType[]): void;
  • error(url: string, callBack: ErrorCallBackType): void;
  • error(url: string, ...callBack: ErrorCallBackType[]): void;
  • error(url: string, app: App | Router): void;
    It'll handle the error If thrown from one of the above procedures.

Example

app.use((req, res) => {
  throw new Error("Something wrong!");
});

app.error((error, req, res) => {
  console.error("I handle the Error!", error);
  res.status(500).send("It's critical!");
});
  • param(paramName: string, ...callback: ParamCallBackType[]): void;
    Parameter handler router.param

Example

const userController = new Router();

userController.param("username", (req, res, username) => {
  const user = await User.findOne({ username: username });
  if (!user) {
    throw new Error("user not found");
  }
  req.profile = user;
});

userController.get("/:username", (req, res) => {
  res.status(200).send({ profile: req.profile });
});

userController.post("/:username/follow", (req, res) => {
  const user = await User.findById(req.payload.id);
  if (user.following.indexOf(req.profile._id) === -1) {
    user.following.push(req.profile._id);
  }
  const profile = await user.save();
  return res.status(200).send({ profile: profile });
});

export default userController;

These are middleware methods and it's like express.js.

App

App extends Router Methods

  • This has all router's methods

Properties

  • listen(options)
    Start the Attain server.
  options: {
    port: number;             // required
    debug?: boolean;          // debug mode
    hostname?: string;        // hostname default as 0.0.0.0
    secure?: boolean;         // https use
    certFile?: string;        // if secure is true, it's required
    keyFile?: string;         // if secure is true, it's required
  }
  • database(dbCls) NEW FEATURE!
    Register a database to use in all of your middleware functions.
    Example:
/* ExampleDatabase.ts */
class ExampleDatabase extends AttainDatabase {
    async connect() {
        console.log('database connected');
    }
    async getAllUsers() {
        return [{ name: 'Shaun' }, { name: 'Mike' }];
    }
}

/* router.ts */
const router = new Router();

router.get('/', async (req: Request, res: Response, db: ExampleDatabase) => {
  const users = await db.getAllUsers();
  res.status(200).send(users);
})

/* index.ts */
const app = new App();

await app.database(ExampleDatabase);

app.use('/api/users', router);

NOTE: for this feature to work as expected, you must:

  • provide a connect() method to your database class
  • extend the AttainDatabase class

This feature is brand new and any contributins and ideas will be welcomed
  • static startWith(connectFunc) Automatically initialize the app and connect to the database with a connect function.

  • static startWith(dbClass) Automatically initialize the app create a database instance.

Nested Routing

Path - router.ts

warn: async await will block your procedures.

import { Router } from "https://deno.land/x/attain/mod.ts";

const api = new Router();
// or
// const api = new App();

const sleep = (time: number) => {
  new Promise((resolve) => setTimeout(() => resolve(), time));
};

// It will stop here for 1 second.
api.get("/block", async (req, res) => {
  console.log("here '/block'");
  await sleep(1000);
  res.status(200).send(`
  <!doctype html>
  <html lang="en">
    <body>
      <h1>Hello</h1>
    </body>
  </html>
  `);
});

// It will not stop here
api.get("/nonblock", (req, res) => {
  console.log("here '/nonblock'");
  sleep(1000).then((_) => {
    res.status(200).send(`
      <!doctype html>
      <html lang="en">
        <body>
          <h1>Hello</h1>
        </body>
      </html>
      `);
  });
});

export default api;

Path - main.ts

import { App } from "https://deno.land/x/attain/mod.ts";
import api from "./router.ts";

const app = new App();

// nested router applied
app.use("/api", api);

app.use((req, res) => {
  res.status(404).send("page not found");
});

app.listen({ port: 3500 });
# start with: deno run -A ./main.ts

Extra plugins

  • logger : Logging response "response - method - status - path - time"
  • parser : Parsing the request body and save it to request.params
  • security: Helping you make secure application by setting various HTTP headers Helmet

Security options

Options Default?
xss (adds some small XSS protections) yes
removePoweredBy (remove the X-Powered-By header) yes
DNSPrefetchControl (controls browser DNS prefetching) yes
noSniff (to keep clients from sniffing the MIME type) yes
frameguard (prevent clickjacking) yes
  • staticServe : It'll serve the static files from a provided path by joining the request path.

Out of box

import {
  App,
  logger,
  parser,
  security,
  staticServe,
} from "https://deno.land/x/Attain/mod.ts";

const app = new App();

// Set Extra Security setting
app.use(security());

// Logging response method status path time
app.use(logger);

// Parsing the request body and save it to request.params
// Also, updated to parse the queries from search params
app.use(parser);

// Serve static files
// This path must be started from your command line path.
app.use(staticServe("./public", { maxAge: 1000 }));

app.use("/", (req, res) => {
  res.status(200).send("hello");
});

app.use("/google", (req, res) => {
  res.redirect("https://www.google.ca");
});

app.use("/:id", (req, res) => {
  // This data has parsed by the embedded URL parser.
  console.log(req.params);
  res.status(200).send(`id: ${req.params.id}`);
});

app.post("/submit", (req, res) => {
  // By the parser middleware, the body and search query get parsed and saved.
  console.log(req.params);
  console.log(req.query);
  res.status(200).send({ data: "has received" });
});

app.listen({ port: 4000 });

More Features

Switch your database with just one line of code

Using the app.database() option, you can switch your database with just one line of code! To use this feature, create a database class that extends the AttainDatabase class:

class PostgresDatabase extends AttainDatabase {
  #client: Client
  async connect() {
    const client = new Client({
      user: Deno.env.get('USER'),
      database: Deno.env.get('DB'),
      hostname: Deno.env.get('HOST'),
      password: Deno.env.get('PASSWORD')!,
      port: parseInt(Deno.env.get('PORT')),
    });
    await client.connect();
    this.#client = client;
  }
  async getAllProducts() {
    const data = await this.#client.query('SELECT * FROM products');
    /* map data */
    return products;
  }
}

/* OR */
class MongoDatabase extends AttainDatabase {
  #Product: Collection<Product>
  async connect() {
     const client = new MongoClient();
     await client.connectWithUri(Deno.env.get('DB_URL'));
     const database = client.database(Deno.env.get('DB_NAME'));
     this.#Product = database.collection('Product');
  }
  async getAllProducts() {
    return await this.#Product.findAll()
  }
}

Then pick one of the databases to use in your app:

await app.database(MongoDatabase);
/* OR */
await app.database(PostgresDatabase);
/* OR */
const app = App.startWith(MongoDatabase);

app.get('/products', (req, res, db) => {
  const products = await db.getAllProducts();
  res.status(200).send(products); /* will work the same! */
})

You can also provide a function that returns a database connection

import { App, Router, Request, Response, AttainDatabase } from "./mod.ts";
import { MongoClient, Database } from "https://deno.land/x/mongo@v0.9.2/mod.ts";

async function DB() {
  const client = new MongoClient()
  await client.connectWithUri("mongodb://localhost:27017")
  const database = client.database("test")
  return database;
}

// allow auto inherit mode (auto inherit the types to the middleware)

const app = App.startWith(DB);
// or
const app = new App<Database>()
app.database(DB)

// this db params will have automatically inherited types from the app<> or startWith method.
app.use((req, res, db) => {

})

There are several modules that are directly adapted from other modules. They have preserved their individual licenses and copyrights. All of the modules, including those directly adapted are licensed under the MIT License.

All additional work is copyright 2020 the Attain authors. All rights reserved.

You can’t perform that action at this time.