diff --git a/.changeset/brown-meals-matter.md b/.changeset/brown-meals-matter.md new file mode 100644 index 0000000..e2b1773 --- /dev/null +++ b/.changeset/brown-meals-matter.md @@ -0,0 +1,5 @@ +--- +"consumejs": minor +--- + +Added reading and writing of cookies diff --git a/README.md b/README.md index 936ecf3..725d902 100644 --- a/README.md +++ b/README.md @@ -1,182 +1,185 @@ -# Consume - -> :warning: **This is not a production ready package!** :warning:
-> This is in no way a production ready product and is simply just a personal learning/exploratory project. Use at your own risk! - -- [:globe_with_meridians: Consume](#consume) - - [:page_with_curl: Intro](#page_with_curl-intro) - - [:clipboard: To-do](#clipboard-to-do) - - [:minidisc: Installing](#minidisc-installing) - - [:bulb: Example Usage](#bulb-example-usage) - - [:gear: Setup for dev](#gear-setup-for-dev) - - [Install Dependencies](#install-dependencies) - - [Running for local testing](#running-for-local-testing) - - [Running the test suite](#running-the-test-suite) - - [:hammer_and_wrench: Quality & Automation](#hammer_and_wrench-quality-automation) - - [Linting](#linting) - - [Formatting](#formatting) - - [Pre-commit](#pre-commit) - - [Catch-all Command](#catch-all-command) - - [Build & Release](#build-release) - ---- - -## :page_with_curl: Intro - -The developer experience of this wrapper library takes big big big inspiration from [ExpressJS](https://expressjs.com/), which I love and use often. With that being said, if you want to contribute to the project, feel free to open issues and/or PRs. :zap: - -This is an exploratory project to build my own REST app library to consume endpoint requests. -The project is essentially a big wrapper for the [NodeJS HTTP Library](https://nodejs.org/api/http.html), it supplies a similar developer experience to that of express with a sprinkle of a few extras. Ingested requests will be wrapped parsed with simple functions to pull out the data and do with it what you need, the servers response will also be wrapped and offered to the developer in the controller with simple functions for replying to requests and sending different flavours of data. - ---- - -## :clipboard: To-do - -- :white_check_mark: ~~Consume basic HTTP requests~~ -- :white_check_mark: ~~Option to populate some default security headers~~ -- :white_check_mark: ~~Request (http.IncomingMessage) wrapper with a simple api~~ -- :white_check_mark: ~~Response (http.ServerResponse) wrapper with a simple api~~ -- :white_square_button: Support different body formats in and out (currently just JSON or plain text) -- :white_check_mark: ~~In-built scheme validation (can leverage the same method as my [SchemeIt library](https://github.com/jacoobia/schemeit))~~ -- :white_square_button: In-built Rate limiting -- :white_check_mark: ~~Middlewares (optionally define them before the controller in the meth functions like ExpressJS)~~ -- :white_square_button: Sending files in responses not just data -- :white_check_mark: ~~Root 'routes' allowing logical breakdown of disciplines by files~~ -- :white_check_mark: ~~Body data~~ -- :white_check_mark: ~~Search params i.e /example?foo=bar~~ -- :white_check_mark: ~~URL params i.e /example/:id/profile~~ -- :white_square_button: Client cache validation -- :white_square_button: Surface cookies -- :white_square_button: Validate if requests are secure (using https, which uses the TLS protocol) -- :white_check_mark: ~~Header manipulations~~ -- :white_square_button: Support for other HTTP request methods besides GET & POST - ---- - -## :minidisc: Installing - -``` - > npm install consumejs -``` - -``` - > pnpm install consumejs -``` - -``` - > yarn install consumejs -``` - ---- - -## :bulb: Example Usage - -``` -// Define your server -const server: ConsumeServer = createServer({ - port: 3000, - useSecureHeaders: true, - logRequests: false -}); - -// Define any routes and/or endpoints -server.get('/example', (request: Request, response: Response) => { - response.reply(StatusCodes.Ok, { message: 'Hello World!' }); -}); - -// Start your server and hit the endpoint! -server.start(() => { - console.log('Test API is live!'); -}); - -``` - -Querying `localhost:3000/example` would yield the following result: - -``` -{ - "message": "Hello World!" -} -``` - ---- - -## :gear: Setup for dev - -**:heavy_exclamation_mark: The project is setup using PNPM, to switch to your preferred package manager delete `pnpm-lock.yaml` and reinstall.** - -### Installing Dependencies - -Once the project is cloned, simply install all of the dependencies - -``` - > pnpm install -``` - -### Running for local testing - -ConsumeJS comes with a bundled example project under under the `./example/` folder. You can edit this to try out Any new features you're working on and then run it with the following command - -``` - > pnpm run dev -``` - -This will build the lib under the `./dist/` directory and run the example project, it includes `nodemon` so that you can make changes to the example with hot-reloading, but this will **not** hot-reload the library build. - -### Running the test suite - -Tests are stored under `root/__tests__/` in a hierachy that mimics the lib and all extend `.test.ts`, the test suite uses [Jest](https://jestjs.io/) and [ts-jest](https://www.npmjs.com/package/ts-jest). I'm **not** aiming for any level of coverage as at that point I would be writing tests for the sake of coverage and just to have tests instead of tests as insurance for logic. Plus some of the implementations cannot be tested in a meaningful way and should be thoroughly dev-tested. - -``` - > pnpm test -``` - ---- - -## :hammer_and_wrench: Quality & Automation - -### Linting - -Linting is configured using [ESLint](https://eslint.org/), you can find the configuration under `.eslintrc.js`. By default it's extending the `eslint:recommended` and `plugin:@typescript-eslint/recommended` with some rules overriden to match my personal preference such as: - -- Indentation: 2 -- String Quotes: Single -- Semi-colon: Always -- Linebreak: Unix - -You can run some linting on the typescript files using: - -``` - > pnpm run lint -``` - -And you can also use eslint to clean up as many problems as it can using: - -``` - > pnpm run clean -``` - -### Formatting - -Formatting is done using [Prettier](https://prettier.io/) and is configured to match the ESLint configuration and should be setup to run on file save. However, if this isn't the case don't worry because linting happens as part of the pre-commit phase. - -### Pre-commit - -[Husky](https://typicode.github.io/husky/) is configured with [lint-staged](https://www.npmjs.com/package/lint-staged) which are both used to enforce some rules as part of the pre-commit phase. This phase will run prettier with the `write` flag, eslint and jest. - -### Catch-all Command - -For on the fly code quality and linting, feel free to make use of the `quality` command that runs prettier against the lib folder and then lints the output afterwars. - -``` - > pnpm run quality -``` - -### Build & Release - -You can build the project using the `build` script but the `relase` script is for the pipeline only and will fail when ran locally. -Building is useful for if you want to work on the example project that lives under `./example/`, which you run using the `dev` script, otherwise it will be run by the pipeline as part of the build and release stage. - -``` - > pnpm run build -``` +# Consume + +> :warning: **This is not a production ready package!** :warning:
+> This is in no way a production ready product and is simply just a personal learning/exploratory project. Use at your own risk! + +- [:globe_with_meridians: Consume](#consume) + - [:page_with_curl: Intro](#page_with_curl-intro) + - [:clipboard: To-do](#clipboard-to-do) + - [:minidisc: Installing](#minidisc-installing) + - [:bulb: Example Usage](#bulb-example-usage) + - [:gear: Setup for dev](#gear-setup-for-dev) + - [Install Dependencies](#install-dependencies) + - [Running for local testing](#running-for-local-testing) + - [Running the test suite](#running-the-test-suite) + - [:hammer_and_wrench: Quality & Automation](#hammer_and_wrench-quality-automation) + - [Linting](#linting) + - [Formatting](#formatting) + - [Pre-commit](#pre-commit) + - [Catch-all Command](#catch-all-command) + - [Build & Release](#build-release) + +--- + +## :page_with_curl: Intro + +The developer experience of this wrapper library takes big big big inspiration from [ExpressJS](https://expressjs.com/), which I love and use often. With that being said, if you want to contribute to the project, feel free to open issues and/or PRs. :zap: + +This is an exploratory project to build my own REST app library to consume endpoint requests. +The project is essentially a big wrapper for the [NodeJS HTTP Library](https://nodejs.org/api/http.html), it supplies a similar developer experience to that of express with a sprinkle of a few extras. Ingested requests will be wrapped parsed with simple functions to pull out the data and do with it what you need, the servers response will also be wrapped and offered to the developer in the controller with simple functions for replying to requests and sending different flavours of data. + +--- + +## :clipboard: To-do + +- :white_check_mark: ~~Consume basic HTTP requests~~ +- :white_check_mark: ~~Option to populate some default security headers~~ +- :white_check_mark: ~~Request (http.IncomingMessage) wrapper with a simple api~~ +- :white_check_mark: ~~Response (http.ServerResponse) wrapper with a simple api~~ +- :white_square_button: Support different body formats in and out (currently just JSON or plain text) +- :white_check_mark: ~~In-built scheme validation (can leverage the same method as my [SchemeIt library](https://github.com/jacoobia/schemeit))~~ +- :white_square_button: In-built Rate limiting +- :white_check_mark: ~~Middlewares (optionally define them before the controller in the meth functions like ExpressJS)~~ +- :white_square_button: Sending files in responses not just data +- :white_check_mark: ~~Root 'routes' allowing logical breakdown of disciplines by files~~ +- :white_check_mark: ~~Body data~~ +- :white_check_mark: ~~Search params i.e /example?foo=bar~~ +- :white_check_mark: ~~URL params i.e /example/:id/profile~~ +- :white_square_button: Client cache validation +- ~~:white_check_mark: Read request cookies~~ +- ~~:white_check_mark: Write response cookies~~ +- :white_square_button: Validate if requests are secure (using https, which uses the TLS protocol) +- :white_check_mark: ~~Header manipulations~~ +- :white_square_button: Support for other HTTP request methods besides GET & POST + +--- + +## :minidisc: Installing + +``` + > npm install consumejs +``` + +``` + > pnpm install consumejs +``` + +``` + > yarn install consumejs +``` + +--- + +## :bulb: Example Usage + +``` +// Define your server +const server: ConsumeServer = createServer({ + port: 3000, + useSecureHeaders: true, + logRequests: false +}); + +// Define any routes and/or endpoints +server.get('/example', (request: Request, response: Response) => { + response.reply(StatusCodes.Ok, { message: 'Hello World!' }); +}); + +// Start your server and hit the endpoint! +server.start(() => { + console.log('Test API is live!'); +}); + +``` + +Querying `localhost:3000/example` would yield the following result: + +``` +{ + "message": "Hello World!" +} +``` + +--- + +## :gear: Setup for dev + +**:heavy_exclamation_mark: The project is setup using PNPM, to switch to your preferred package manager delete `pnpm-lock.yaml` and reinstall.** + +### Installing Dependencies + +This project enforces Unix style line-endings (LF), this can be an issue if you're working on a windows machine as the lint-staged pre-commit phase will enforce the CRLF line endings. You can disable autoCrlf like so: `git config --global core.autocrlf false` + +Once the project is cloned, simply install all of the dependencies + +``` + > pnpm install +``` + +### Running for local testing + +ConsumeJS comes with a bundled example project under under the `./example/` folder. You can edit this to try out Any new features you're working on and then run it with the following command + +``` + > pnpm run dev +``` + +This will build the lib under the `./dist/` directory and run the example project, it includes `nodemon` so that you can make changes to the example with hot-reloading, but this will **not** hot-reload the library build. + +### Running the test suite + +Tests are stored under `root/__tests__/` in a hierachy that mimics the lib and all extend `.test.ts`, the test suite uses [Jest](https://jestjs.io/) and [ts-jest](https://www.npmjs.com/package/ts-jest). I'm **not** aiming for any level of coverage as at that point I would be writing tests for the sake of coverage and just to have tests instead of tests as insurance for logic. Plus some of the implementations cannot be tested in a meaningful way and should be thoroughly dev-tested. + +``` + > pnpm test +``` + +--- + +## :hammer_and_wrench: Quality & Automation + +### Linting + +Linting is configured using [ESLint](https://eslint.org/), you can find the configuration under `.eslintrc.js`. By default it's extending the `eslint:recommended` and `plugin:@typescript-eslint/recommended` with some rules overriden to match my personal preference such as: + +- Indentation: 2 +- String Quotes: Single +- Semi-colon: Always +- Linebreak: Unix + +You can run some linting on the typescript files using: + +``` + > pnpm run lint +``` + +And you can also use eslint to clean up as many problems as it can using: + +``` + > pnpm run clean +``` + +### Formatting + +Formatting is done using [Prettier](https://prettier.io/) and is configured to match the ESLint configuration and should be setup to run on file save. However, if this isn't the case don't worry because linting happens as part of the pre-commit phase. + +### Pre-commit + +[Husky](https://typicode.github.io/husky/) is configured with [lint-staged](https://www.npmjs.com/package/lint-staged) which are both used to enforce some rules as part of the pre-commit phase. This phase will run prettier with the `write` flag, eslint and jest. + +### Catch-all Command + +For on the fly code quality and linting, feel free to make use of the `quality` command that runs prettier against the lib folder and then lints the output afterwars. + +``` + > pnpm run quality +``` + +### Build & Release + +You can build the project using the `build` script but the `relase` script is for the pipeline only and will fail when ran locally. +Building is useful for if you want to work on the example project that lives under `./example/`, which you run using the `dev` script, otherwise it will be run by the pipeline as part of the build and release stage. + +``` + > pnpm run build +``` diff --git a/example/controller/userController.ts b/example/controller/userController.ts index 337ecb7..74923fb 100644 --- a/example/controller/userController.ts +++ b/example/controller/userController.ts @@ -13,6 +13,8 @@ const userRoute: ConsumeRoute = createRoute(); */ userRoute.get('', (request: Request, response: Response) => { const users: User[] = getUsers(); + console.log(request.getAllCookies()); + response.setCookie('test', 'test', { secure: true }); response.reply(StatusCodes.Ok, { users }); }); diff --git a/lib/@types/index.ts b/lib/@types/index.ts index e2eb0bd..c91568c 100644 --- a/lib/@types/index.ts +++ b/lib/@types/index.ts @@ -1,3 +1,5 @@ +export type Primitive = string | number | boolean; + /** Possible header types */ export type Header = string | number | string[]; @@ -81,6 +83,15 @@ export type ValidatorFunction = (() => ElementValidator) & { optional: () => Ele /** Validation error response */ export type ValidatorError = Record; +export type CookieOptions = { + expires?: Date; + httpOnly?: boolean; + secure?: boolean; + path?: string; + domain?: string; + sameSite?: 'Strict' | 'Lax' | 'None'; +}; + /** * A ConsumeServer */ @@ -194,6 +205,14 @@ export interface Response { */ forbidden(reason?: string): void; + /** + * Sets a cookie for a http response for a client + * @param {string} name The name of the cookie + * @param {string} value The value of the cookie + * @param {CookieOptions} options The options for the cookie + */ + setCookie(name: string, value: string, options: CookieOptions): void; + /** * Set a header for a http response for a client * @param {string} name The name of the header @@ -221,27 +240,45 @@ export interface Request { /** * Parses the body to a T type + * @returns {T} The parsed body */ parseBody(): T; /** * Parses the URL params to a T type + * @returns {T} The parsed URL params */ parseUrlParams(): T; /** * Parses the search params to a T type + * @returns {T} The parsed search params */ parseSearchParams(): T; /** * Internal use only * Parses the request data (body, query, param etc) + * @returns {Promise} Whether the request was parsed successfully */ parse(): Promise; /** - * Returns the full URL including the host + * Gets a cookie from the request + * @param {string} name The name of the cookie to get + * @returns {string | undefined} The cookie value or undefined if not found + */ + getCookie(name: string): string | undefined; + + /** + * Gets all cookies from the request + * @returns {Record} The cookies as pairs + */ + getAllCookies(): Record; + + /** + * Gets the full URL of the request + * @returns {string} The full URL */ fullUrl(): string; } diff --git a/lib/util/http.ts b/lib/util/http.ts new file mode 100644 index 0000000..7a932ca --- /dev/null +++ b/lib/util/http.ts @@ -0,0 +1,30 @@ +import { CookieOptions } from '../@types'; + +/** + * Builds a cookie string with the provided key value pair and options + * @param {string} cookie The cookie key=value pair + * @param {CookieOptions} options The options for the cookie + * @returns {string} The built cookie string. + */ +export const buildCookie = (cookie: string, options: CookieOptions): string => { + if (options.expires) { + cookie += `; Expires=${options.expires.toUTCString()}`; + } + if (options.httpOnly) { + cookie += '; HttpOnly'; + } + if (options.secure) { + cookie += '; Secure'; + } + if (options.path) { + cookie += `; Path=${options.path}`; + } + if (options.domain) { + cookie += `; Domain=${options.domain}`; + } + if (options.sameSite) { + cookie += `; SameSite=${options.sameSite}`; + } + + return cookie; +}; diff --git a/lib/wrapper/request.ts b/lib/wrapper/request.ts index 423ed62..c1d9996 100644 --- a/lib/wrapper/request.ts +++ b/lib/wrapper/request.ts @@ -59,6 +59,28 @@ class ConsumeRequest implements Request { } } + public getCookie(name: string): string { + const cookies = this.request.headers.cookie; + if (!cookies) return undefined; + + const cookieValue = cookies.split(';').find((cookie) => cookie.trim().startsWith(`${name}=`)); + return cookieValue ? decodeURIComponent(cookieValue.split('=')[1]) : undefined; + } + + public getAllCookies(): Record { + const cookies = this.request.headers.cookie; + const cookiesObj: Record = {}; + + if (cookies) { + cookies.split(';').forEach((cookie) => { + const [name, value] = cookie.split('=').map((c) => c.trim()); + cookiesObj[decodeURIComponent(name)] = decodeURIComponent(value); + }); + } + + return cookiesObj; + } + public fullUrl(): string { const url: string | undefined = this.request.url; const req: http.IncomingMessage = this.request; diff --git a/lib/wrapper/response.ts b/lib/wrapper/response.ts index 0fc1822..3bc274e 100644 --- a/lib/wrapper/response.ts +++ b/lib/wrapper/response.ts @@ -1,5 +1,6 @@ import * as http from 'http'; -import { Response, StatusCodes } from '../@types'; +import { CookieOptions, Response, StatusCodes } from '../@types'; +import { buildCookie } from '../util/http'; class ConsumeResponse implements Response { private response: http.ServerResponse; @@ -8,6 +9,12 @@ class ConsumeResponse implements Response { this.response = response; } + public setCookie(name: string, value: string, options: CookieOptions): void { + const cookieKey = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`; + const cookie = buildCookie(cookieKey, options); + this.setHeader('Set-Cookie', cookie); + } + public forbidden(reason: string = 'Forbidden request'): void { this.reply(StatusCodes.Forbidden, reason); } diff --git a/package.json b/package.json index 53e3c84..5b3ad41 100644 --- a/package.json +++ b/package.json @@ -17,14 +17,10 @@ "test": "jest" }, "lint-staged": { - "lib/**/*.ts": [ + "{lib,__tests__}/**/*.ts": [ "prettier --write", - "eslint \"**/*.ts\"", - "jest" - ], - "__tests__/**/*.test.ts": [ - "prettier --write", - "eslint \"**/*.ts\"" + "eslint --fix", + "jest --findRelatedTests" ] }, "keywords": [],