diff --git a/.gitignore b/.gitignore
index ecb7d9c..e289779 100644
--- a/.gitignore
+++ b/.gitignore
@@ -132,4 +132,4 @@ dist
# custom
generate.sh
*.http
-example*
+example
diff --git a/README.md b/README.md
index 62aa295..649c875 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Fast, lightweight and zero dependency framework for [bunjs](https://bun.sh) 🚀
-![npm](https://img.shields.io/npm/v/colstonjs?color=blue&style=plastic)
+![GitHub Workflow Status](https://img.shields.io/github/workflow/status/ajimae/colstonjs/publish?style=plastic) ![npm](https://img.shields.io/npm/v/colstonjs?color=blue&style=plastic)
![GitHub](https://img.shields.io/github/license/ajimae/colstonjs?style=plastic)
![npm](https://img.shields.io/npm/dt/colstonjs?style=plastic)
@@ -13,21 +13,26 @@ Fast, lightweight and zero dependency framework for [bunjs](https://bun.sh) 🚀
- [Install](#installation)
- [Usage](#usage)
- [Examples](#examples)
- - [Hellow Bun](#hello-bun)
+ - [Hello Bun](#hello-bun)
- [Read request body as json or text](#read-request-body-as-json-or-text)
- [Using named parameters](#using-named-parameters)
- [Using query parameters](#using-query-parameters)
- [Method Chaining](#method-chaining)
+ - [Running the demo note-app](#running-the-demo-note-app)
- [Middleware](#middleware)
- [Application-Level Middleware](#application-level-middleware)
- [Route-Level Middleware](#route-level-middleware)
+ - [Context `locals`](#context-locals)
+ - [Router](#router)
+ - [Instantiating Router class](#instantiating-router-class)
+ - [Injecting Router instance into the app](#injecting-router-instance-into-the-app)
- [Application instance cache](#application-instance-cache)
- [Error Handler](#error-handler)
- [Benchmark](#benchmark)
- [Contribute](#contribute)
- [License](#license)
- [Author](#author)
-- [Note](#note:)
+- [DevNote](#devnote)
## Background
@@ -198,6 +203,17 @@ app
app.start(8000);
```
+#### Running the demo `note-app`
+Follow the steps below to run the `demo note-taking api application` in the `examples`directory.
+- Clone this repository
+- Change directory into the note-app folder by running `cd examples/note-app`
+- Start the http server to listen on port `8000` by running `bun app.js`
+- User your favourite `http client` (e.g Postman) to make requests to the `listening http server`.
+
+
+
+
+
### Middleware
Colstonjs support both `route` level middleware as well as `app` level middleware.
@@ -267,7 +283,71 @@ app.get("/", middleware-1, middleware-2, middleware-3, ..., middleware-k, (ctx:
});
...
```
+#### Context locals
+`ctx.locals` is a plain javascript object that is specifically added to allow sharing of data amongst the chain of middlewares and/or handler functions.
+
+```ts
+// server.ts
+...
+let requestCount = 0;
+app.post("/request-count", (ctx, next) => {
+ /**
+ * req.locals can be used to pass
+ * data from one middleware to another
+ */
+ ctx.locals.requestCount = requestCount;
+ next();
+}, (ctx, next) => {
+ ++ctx.locals.requestCount;
+ next();
+}, (ctx) => {
+ let count = ctx.locals.requestCount;
+ return ctx.status(200).text(count); // 1
+});
+```
+
+### Router
+#### Instantiating Router class
+Router class provide a way to separate router specific declaration/blocks from the app logic, by providing that extra abstraction layer for your project.
+```typescript
+// router.ts
+import Router from "Router";
+
+// instantiate the router class
+const router1 = new Router();
+const router2 = new Router();
+
+// define user routes - can be in a separate file or module.
+router1.post('/user', (ctx) => { return ctx.status(200).json({ user }) });
+router1.get('/users', (ctx) => { return ctx.json({ users }) });
+router1.delete('/user?id', (ctx) => { return ctx.status(204).head() });
+
+// define the notes route - can also be in separate module.
+router2.get('/note/:id', (ctx) => { return ctx.json({ note }) });
+router2.get('/notes', (ctx) => { return ctx.json({ notes }) });
+router2.post('/note', (ctx) => { return ctx.status(201).json({ note }) });
+
+export { router1, router2 };
+```
+
+#### Injecting Router instance into the app
+```typescript
+// server.ts
+import Colston from "colstonjs";
+import { router1, router2 } from "./router";
+
+const app: Colston = new Colston();
+app.all(router1, router2);
+
+// other routes can still be defined here
+app.get("/", (ctx) => {
+ return ctx.status(200).text("Welcome to colstonjs framework for bun");
+});
+
+app.start(8000)
+```
+The `app.all()` method takes in k numbers of router instance objects e.g `app.all(router-1, router-2, ..., router-k);`. The [example](example) folder contains a full note taking backend app that utilizes this pattern.
## Application instance cache
We can cache simple data which will leave throughout the application instance lifecycle.
@@ -445,5 +525,5 @@ See the TODO doc [here](todo.md), feel free to also add to the list by editing t
## Author
Coded with 💙 by [Chukwuemeka Ajima](https://github.com/ajimae)
-## Note:
+## DevNote:
Although this version is fairly stable, it is actively still under development so also is [bunjs](https://bun.sh) and might contain some bugs, hence, not ideal for a production app.
diff --git a/clean-dist b/clean-dist
index 589dbea..fd058ae 100755
--- a/clean-dist
+++ b/clean-dist
@@ -11,12 +11,7 @@ cat << EOF > dist/index.d.ts
import Colston from "./declarations/colston";
export default Colston;
export * from "./declarations/types.d";
-EOF
-
-# update imports in index.js
-cat << EOF > dist/index.js
- import Colston from "./src/colston";
- export default Colston;
+ export { default as Router } from "./src/router";
EOF
echo "✨ Done"
diff --git a/examples/note-app/app.js b/examples/note-app/app.js
new file mode 100644
index 0000000..66b68ed
--- /dev/null
+++ b/examples/note-app/app.js
@@ -0,0 +1,63 @@
+import Colston from "../../index";
+import router from "./routes";
+import { logger, requestID } from "./middleware";
+
+const app = new Colston();
+
+let requestCount = 0;
+
+app
+ .get("/one", (ctx) => {
+ requestCount++;
+ return ctx.status(200).text("One");
+ })
+ .get("/two", ctx => ctx.text("two"))
+ .post("/two", (ctx) => {
+ requestCount++;
+ return ctx.status(200).text("Two");
+ })
+ .patch("/three", (ctx) => {
+ requestCount++;
+ return ctx.status(200).text("Three");
+ });
+
+app.post("/requestCount", (ctx, next) => {
+ /**
+ * req.locals can be used to pass
+ * data from one middleware to another
+ */
+ ctx.locals.requestCount = requestCount;
+ next();
+}, (ctx, next) => {
+ ++ctx.locals.requestCount;
+ next();
+}, (ctx) => {
+ return ctx.status(200).text(ctx.locals.requestCount.toString());
+});
+
+app.get("/request-id", requestID, (ctx) => {
+ return ctx.status(200).json({
+ message: "This will give every request a unique ID and in the header too.",
+ requestID: ctx.request.id
+ });
+});
+
+/**
+ * the app.all(...route: Router) mehtod
+ * accepts k-numbers of router instance objects
+ * where each router instance object are
+ * @example
+ *
+ * router-1 = new Router().get(path, ...middlewares)
+ * router-2 = new Router().post(path, ...niddlewares)
+ * ...
+ * router-k = new Router().(path, ...middlewares)
+ *
+ * app.all(router-1, router-2, ..., router-k)
+ */
+app.use(logger);
+app.all(router);
+
+app.start(8000, function () {
+ console.log(`server running on port {8000}`);
+});
diff --git a/examples/note-app/controller/NoteController.js b/examples/note-app/controller/NoteController.js
new file mode 100644
index 0000000..1eda446
--- /dev/null
+++ b/examples/note-app/controller/NoteController.js
@@ -0,0 +1,65 @@
+/**
+ * NoteController class
+ * @class NoteController
+ * @methods getNotes
+ * @methods postNotes
+ */
+class NoteController {
+ constructor(noteRepository) {
+ this.noteRepository = noteRepository;
+ }
+
+ /**
+ * retrieve all available notes
+ * @param {*} ctx
+ * @returns bun Response instance
+ */
+ async getNotes(ctx) {
+ const notes = await this.noteRepository.getNotes(ctx);
+
+ return ctx.status(200).json({
+ success: true,
+ data: notes
+ });
+ }
+
+ /**
+ * retrieve all available notes
+ * @param {*} ctx
+ * @returns bun Response instance
+ */
+ async getAllNotes(ctx) {
+ const notes = await this.noteRepository.getAllNotes();
+
+ return ctx.status(200).json({
+ success: true,
+ data: notes
+ });
+ }
+
+ /**
+ * post a single note data
+ * @param {*} ctx
+ * @returns bun Response instance
+ */
+ async postNote(ctx) {
+ const notes = await this.noteRepository.postNote(ctx);
+
+ return ctx.status(201).json({
+ success: true,
+ data: notes
+ });
+ }
+
+ /**
+ * delte a sinlge note data
+ * @param {*} ctx
+ * @returns bun Response instance
+ */
+ async deleteNote(ctx) {
+ await this.noteRepository.deleteNote(ctx);
+ return ctx.status(204).head();
+ }
+}
+
+export default NoteController
diff --git a/examples/note-app/controller/index.js b/examples/note-app/controller/index.js
new file mode 100644
index 0000000..17f2ec8
--- /dev/null
+++ b/examples/note-app/controller/index.js
@@ -0,0 +1,2 @@
+import NoteController from "./NoteController";
+export default NoteController;
diff --git a/examples/note-app/dataStore/data.json b/examples/note-app/dataStore/data.json
new file mode 100644
index 0000000..df5da66
--- /dev/null
+++ b/examples/note-app/dataStore/data.json
@@ -0,0 +1,20 @@
+[
+ {
+ "id": "L4JPI2tLmXMpwmaPcUZZlfuLljW7JYPP2hfmYcmSQ",
+ "note": "this is my first note",
+ "createdAt": "1970-01-01T00:00:00.005Z",
+ "updatedAt": "1970-01-01T00:00:00.005Z"
+ },
+ {
+ "id": "jyBuT1zpMLc0uIQGDOKNkJJmI9YCzg5LLWU2OgkgE",
+ "note": "this is my second note",
+ "createdAt": "1980-01-01T00:00:00.035Z",
+ "updatedAt": "1980-01-01T00:00:00.035Z"
+ },
+ {
+ "id": "QbSMatSi9YVxKQBQny9yBfAh8E3VWkkWg8GMHwQHdOA",
+ "note": "this is my third note",
+ "createdAt": "1990-01-01T00:00:02.000Z",
+ "updatedAt": "1990-01-01T00:00:02.000Z"
+ }
+]
diff --git a/examples/note-app/middleware/index.js b/examples/note-app/middleware/index.js
new file mode 100644
index 0000000..e543579
--- /dev/null
+++ b/examples/note-app/middleware/index.js
@@ -0,0 +1,2 @@
+export { logger } from "./logger";
+export { requestID } from './requestID';
diff --git a/examples/note-app/middleware/logger.js b/examples/note-app/middleware/logger.js
new file mode 100644
index 0000000..a4a9678
--- /dev/null
+++ b/examples/note-app/middleware/logger.js
@@ -0,0 +1,4 @@
+export const logger = (ctx) => {
+ const { pathname } = new URL(ctx.request.url);
+ console.info("- - " + [new Date()], "- - " + ctx.request.method + " " + pathname + " HTTP 1.1" + " - ");
+}
diff --git a/examples/note-app/middleware/requestID.js b/examples/note-app/middleware/requestID.js
new file mode 100644
index 0000000..4ceacf9
--- /dev/null
+++ b/examples/note-app/middleware/requestID.js
@@ -0,0 +1,6 @@
+import crypto from "crypto"; // built into bun
+
+export const requestID = (ctx) => {
+ ctx.request.id = crypto.randomBytes(18).toString('hex');
+ ctx.setHeader('request-id', ctx.request.id);
+}
diff --git a/examples/note-app/models/index.js b/examples/note-app/models/index.js
new file mode 100644
index 0000000..e59ebfa
--- /dev/null
+++ b/examples/note-app/models/index.js
@@ -0,0 +1 @@
+export * from "./notes";
\ No newline at end of file
diff --git a/examples/note-app/models/notes.js b/examples/note-app/models/notes.js
new file mode 100644
index 0000000..da3d165
--- /dev/null
+++ b/examples/note-app/models/notes.js
@@ -0,0 +1,32 @@
+import crypto from 'crypto';
+const data = require('../dataStore/data.json')
+
+export function find(id) {
+ return data.find((v) => v.id == id);
+}
+
+export function findAll() {
+ return data;
+}
+
+export function save(datum) {
+ const id = crypto.
+ randomBytes(32)
+ .toString('base64')
+ .replace(/[_+=\/]/gi, '');
+
+ const _data = {
+ id,
+ ...datum,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString()
+ }
+
+ data.push(_data);
+ return _data;
+}
+
+export function Delete(id) {
+ const idx = data.findIndex(v => v.id == id);
+ return data.splice(idx, 1);
+}
diff --git a/examples/note-app/repository/NoteRepository.js b/examples/note-app/repository/NoteRepository.js
new file mode 100644
index 0000000..d1c0184
--- /dev/null
+++ b/examples/note-app/repository/NoteRepository.js
@@ -0,0 +1,49 @@
+/**
+ * NoteRepository class
+ * @class NotRepositorye
+ * @methods getNotes
+ * @methods postNote
+ */
+class NoteRepository {
+ constructor(noteModels) {
+ this.noteModels = noteModels;
+ }
+
+ /**
+ * retrieve all available notes
+ * @param {*} ctx
+ * @returns bun Response instance
+ */
+ async getNotes(ctx) {
+ const { id } = await ctx.request.params;
+ return this.noteModels.find(id) ?? {};
+ }
+
+ async getAllNotes() {
+ return this.noteModels.findAll();
+ }
+
+ /**
+ * post a single note data
+ * @param {*} ctx
+ * @returns bun Response instance
+ */
+ async postNote(ctx) {
+ const note = await ctx.request.body;
+ const notes = await this.noteModels.save(note);
+
+ return notes;
+ }
+
+ /**
+ * delete a single note data
+ * @param {*} ctx
+ * @returns bun Reponse instance
+ */
+ async deleteNote(ctx) {
+ const { id } = await ctx.request.query;
+ return this.noteModels.Delete(id);
+ }
+}
+
+export default NoteRepository;
diff --git a/examples/note-app/repository/index.js b/examples/note-app/repository/index.js
new file mode 100644
index 0000000..3451a97
--- /dev/null
+++ b/examples/note-app/repository/index.js
@@ -0,0 +1,2 @@
+import NoteRepository from "./NoteRepository";
+export default NoteRepository;
diff --git a/examples/note-app/routes/index.ts b/examples/note-app/routes/index.ts
new file mode 100644
index 0000000..b86ca5e
--- /dev/null
+++ b/examples/note-app/routes/index.ts
@@ -0,0 +1,2 @@
+import router from './notes';
+export default router;
diff --git a/examples/note-app/routes/notes.js b/examples/note-app/routes/notes.js
new file mode 100644
index 0000000..4284f8b
--- /dev/null
+++ b/examples/note-app/routes/notes.js
@@ -0,0 +1,15 @@
+import { Router } from '../../../index';
+import * as model from '../models';
+import NoteRepository from '../repository';
+import NoteController from '../controller';
+
+const router = new Router();
+const noteRepository = new NoteRepository(model);
+const noteController = new NoteController(noteRepository);
+
+router.get('/note/:id', noteController.getNotes.bind(noteController));
+router.get('/notes', noteController.getAllNotes.bind(noteController));
+router.post('/note', noteController.postNote.bind(noteController));
+router.delete('/?id', noteController.deleteNote.bind(noteController));
+
+export default router;
diff --git a/index.ts b/index.ts
index 0de31df..41d26ce 100644
--- a/index.ts
+++ b/index.ts
@@ -1,4 +1,12 @@
import Colston from "./src/colston";
-export * from "./src/types.d"
export default Colston;
+/**
+ * uncomment (the exported types.d)
+ * only when you are sure you are
+ * importing the root `index.ts` file
+ * into your project else leave it commented.
+ *
+ * export * from './src/types.d'
+ */
+export { default as Router } from "./src/router";
diff --git a/package.json b/package.json
index 8f3cd83..e28647c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "colstonjs",
- "version": "0.1.0-beta.2",
+ "version": "0.1.0-beta.3",
"author": "Chukwuemeka Ajima ",
"repository": "https://github.com/ajimae/colstonjs.git",
"main": "dist/index.js",
diff --git a/postman.jpeg b/postman.jpeg
new file mode 100644
index 0000000..117b03e
Binary files /dev/null and b/postman.jpeg differ
diff --git a/postman_collection.json b/postman_collection.json
new file mode 100644
index 0000000..69f4020
--- /dev/null
+++ b/postman_collection.json
@@ -0,0 +1,151 @@
+{
+ "info": {
+ "_postman_id": "b12fc21b-9fc8-4269-acc1-5be36f8b42c7",
+ "name": "colstonjs-note-api",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
+ "_exporter_id": "4784559"
+ },
+ "item": [
+ {
+ "name": "Get a single note",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://127.0.0.1:8000/note/L4JPI2tLmXMpwmaPcUZZlfuLljW7JYPP2hfmYcmSQ",
+ "protocol": "http",
+ "host": [
+ "127",
+ "0",
+ "0",
+ "1"
+ ],
+ "port": "8000",
+ "path": [
+ "note",
+ "L4JPI2tLmXMpwmaPcUZZlfuLljW7JYPP2hfmYcmSQ"
+ ],
+ "query": [
+ {
+ "key": "",
+ "value": "",
+ "disabled": true
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Post a single note",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"note\": \"this is my first test note\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://127.0.0.1:8000/note",
+ "protocol": "http",
+ "host": [
+ "127",
+ "0",
+ "0",
+ "1"
+ ],
+ "port": "8000",
+ "path": [
+ "note"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get all notes",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://127.0.0.1:8000/notes",
+ "protocol": "http",
+ "host": [
+ "127",
+ "0",
+ "0",
+ "1"
+ ],
+ "port": "8000",
+ "path": [
+ "notes"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Delete single note",
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "http://127.0.0.1:8000/?id=zjY5DDtC44ye6qN812pyY5kWXHguqxxZjksgNKvku3E",
+ "protocol": "http",
+ "host": [
+ "127",
+ "0",
+ "0",
+ "1"
+ ],
+ "port": "8000",
+ "path": [
+ ""
+ ],
+ "query": [
+ {
+ "key": "id",
+ "value": "zjY5DDtC44ye6qN812pyY5kWXHguqxxZjksgNKvku3E"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Request ID",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://127.0.0.1:8000/request-id",
+ "protocol": "http",
+ "host": [
+ "127",
+ "0",
+ "0",
+ "1"
+ ],
+ "port": "8000",
+ "path": [
+ "request-id"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Request Count",
+ "request": {
+ "method": "GET",
+ "header": []
+ },
+ "response": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/colston.ts b/src/colston.ts
index 8076c6d..6020a10 100644
--- a/src/colston.ts
+++ b/src/colston.ts
@@ -1,11 +1,12 @@
import { Errorlike, Serve, Server } from "bun";
-import type { Middleware, Options, IColston} from "./types.d";
+import type { Middleware, Options, IColston } from "./types.d";
import parse from "./params";
import queryParse from "./query";
import readBody from "./body";
import Context from "./context";
import routeRegister from "./routeRegister";
import compose from "./middlewares";
+import Router from "./router";
/**
* @class Colston
@@ -15,7 +16,7 @@ import compose from "./middlewares";
*/
export default class Colston implements IColston {
readonly options: Options = {};
- readonly routeTable: object = {};
+ readonly routeTable: Array