├── .husky ├── .gitignore ├── pre-commit └── commit-msg ├── commitlint.config.js ├── .npmrc ├── src ├── index.ts ├── models.ts └── functionParser.ts ├── .gitignore ├── .npmignore ├── .vscode ├── settings.json └── extensions.json ├── .editorconfig ├── .eslintrc.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ └── main.yml ├── LICENSE ├── CHANGELOG.md ├── package.json ├── tsconfig.json └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://npm.pkg.github.com/@FilledStacks 2 | registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // index.ts 2 | 3 | export * from './functionParser'; 4 | export * from './models'; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | .env 6 | *.tgz 7 | .eslintcache 8 | docs 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/** 2 | etc/** 3 | *.lock 4 | coverage 5 | *.spec.js 6 | *.tgz 7 | .github 8 | *.ts 9 | *.config.js -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/node_modules": true 4 | }, 5 | "editor.formatOnSave": true 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | es2021: true 3 | node: true 4 | extends: 5 | - airbnb-base 6 | - prettier 7 | parser: '@typescript-eslint/parser' 8 | parserOptions: 9 | ecmaVersion: 12 10 | sourceType: module 11 | plugins: 12 | - '@typescript-eslint' 13 | rules: { 14 | # disables certain airbnb rules 15 | max-classes-per-file: 0, 16 | no-shadow: 0, 17 | no-unused-vars: 0, 18 | import/no-unresolved: 0, 19 | global-require: 0, 20 | import/extensions: 0, 21 | no-console: 0, 22 | import/no-dynamic-require: 0, 23 | class-methods-use-this: 0, 24 | import/prefer-default-export: 0, 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Fetch and update latest `npm` packages 4 | - package-ecosystem: npm 5 | directory: '/' 6 | schedule: 7 | interval: daily 8 | time: '00:00' 9 | open-pull-requests-limit: 10 10 | commit-message: 11 | prefix: fix 12 | prefix-development: chore 13 | include: scope 14 | # Fetch and update latest `github-actions` pkgs 15 | - package-ecosystem: github-actions 16 | directory: '/' 17 | schedule: 18 | interval: daily 19 | time: '00:00' 20 | open-pull-requests-limit: 10 21 | commit-message: 22 | prefix: fix 23 | prefix-development: chore 24 | include: scope 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "coenraads.bracket-pair-colorizer-2", 4 | "streetsidesoftware.code-spell-checker", 5 | "naumovs.color-highlight", 6 | "vivaxy.vscode-conventional-commits", 7 | "redhat.fabric8-analytics", 8 | "oouo-diogo-perdigao.docthis", 9 | "dbaeumer.vscode-eslint", 10 | "editorconfig.editorconfig", 11 | "ms-vscode.vscode-typescript-next", 12 | "davidanson.vscode-markdownlint", 13 | "yzhang.markdown-all-in-one", 14 | "eg2.vscode-npm-script", 15 | "christian-kohler.npm-intellisense", 16 | "esbenp.prettier-vscode", 17 | "webhint.vscode-webhint", 18 | "redhat.vscode-yaml", 19 | "mikestead.dotenv", 20 | "wix.vscode-import-cost", 21 | "hookyqr.minify", 22 | "sonarsource.sonarlint-vscode", 23 | "tabnine.tabnine-vscode", 24 | "enkia.tokyo-night", 25 | "visualstudioexptteam.vscodeintellicode", 26 | "vscode-icons-team.vscode-icons", 27 | "redhat.vscode-xml" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dane Mackier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2.5 4 | 5 | - Adds `verbose` flag to hide logs in production mode 6 | - Refactor `FunctionsParser` options to use named parameters 7 | 8 | ## 0.2.4 9 | 10 | - Adds the ability to pass middlewares to an endpoint 11 | 12 | ## 0.2.2 13 | 14 | - Makes `EndpointOptions` optional 15 | 16 | ## 0.2.1 17 | 18 | - Adds `express-fileupload` as an option that can be enabled per endpoint through `EndpointOptions` 19 | 20 | ## 0.2.0 21 | 22 | - Adds `ParserOptions` and `EndpointOptions` 23 | 24 | ## 0.1.5 25 | 26 | - Code clean up and build improvements 27 | 28 | ## 0.1.4 29 | 30 | - Adds a new shorthand recommended way to create RESTful endpoints 31 | - Adds shorthand way for exporting reactive functions 32 | - Adds some code chores. Improves the build time and package configs. 33 | - Updates readme to include detailed walk through of the package usage 34 | 35 | ## 0.1.3 36 | 37 | - Adds grouping by folder name 38 | 39 | ## 0.1.2 40 | 41 | - Adds recommended grouping boolean to `FunctionParser` constructor. This will allow the API's to be grouped under their intended resources as the structure recommends. 42 | 43 | ## 0.1.1 44 | 45 | - Changes Endpoint back into a class. We have to be able to construct a new one when creating a restful endpoint. 46 | - Updates readme installation instructions. 47 | 48 | ## 0.1.0 - Initial publish with package functionality 49 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Ubuntu CI/CD 2 | 3 | # triggers 4 | on: 5 | push: 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | # define job name and sequence 10 | jobs: 11 | build: 12 | name: Build, Test, and Lint 13 | 14 | # runner os 15 | runs-on: ubuntu-latest 16 | 17 | # cache build deps 18 | steps: 19 | - name: Cache 20 | uses: actions/cache@v2.1.4 21 | with: 22 | path: '**/node_modules' 23 | key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }} 24 | 25 | # setup nodejs 26 | - name: Use Node.js 14 LTS 27 | uses: actions/setup-node@v2 28 | with: 29 | node-version: 14 # latest LTS version 30 | 31 | # check out repo 32 | - name: Checkout 33 | uses: actions/checkout@v2 34 | 35 | # install node_modules 36 | - name: Install 37 | run: npm i 38 | 39 | # build package 40 | - name: Build 41 | run: npm run build 42 | env: 43 | NODE_ENV: production 44 | 45 | # test package 46 | - name: Test 47 | run: npm test 48 | 49 | # lint package 50 | - name: Lint 51 | run: npm run lint 52 | 53 | # archive production artifact 54 | - name: Archive 55 | uses: actions/upload-artifact@v2 56 | with: 57 | name: build-artifact 58 | path: dist 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.5", 3 | "description": "A package that helps with the management and expansion of a maintainable firebase backend", 4 | "license": "MIT", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "files": [ 8 | "dist", 9 | "src" 10 | ], 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "scripts": { 15 | "start": "tsdx watch", 16 | "build": "tsdx build", 17 | "test": "tsdx test --passWithNoTests", 18 | "lint": "tsdx lint src test", 19 | "lint:fix": "tsdx lint src test --fix", 20 | "prepare": "tsdx build", 21 | "size": "size-limit", 22 | "analyze": "size-limit --why", 23 | "release": "auto shipit" 24 | }, 25 | "husky": { 26 | "hooks": { 27 | "pre-commit": "tsdx lint" 28 | } 29 | }, 30 | "prettier": { 31 | "printWidth": 80, 32 | "trailingComma": "es5", 33 | "tabWidth": 2, 34 | "useTabs": false, 35 | "semi": true, 36 | "singleQuote": true, 37 | "bracketSpacing": true, 38 | "arrowParens": "always", 39 | "jsxSingleQuote": true, 40 | "quoteProps": "consistent" 41 | }, 42 | "name": "firebase-backend", 43 | "module": "dist/firebase-backend.esm.js", 44 | "size-limit": [ 45 | { 46 | "path": "dist/firebase-backend.cjs.production.min.js", 47 | "limit": "10 KB" 48 | }, 49 | { 50 | "path": "dist/firebase-backend.esm.js", 51 | "limit": "10 KB" 52 | } 53 | ], 54 | "devDependencies": { 55 | "@size-limit/preset-small-lib": "^6.0.1", 56 | "@types/express": "^4.17.11", 57 | "@types/glob": "^7.1.3", 58 | "auto": "^10.43.0", 59 | "husky": "^5.1.3", 60 | "size-limit": "^4.10.1", 61 | "tsdx": "^0.14.1", 62 | "tslib": "^2.1.0", 63 | "typescript": "^4.2.3" 64 | }, 65 | "dependencies": { 66 | "@types/cors": "^2.8.10", 67 | "@types/express-fileupload": "^1.1.6", 68 | "body-parser": "^1.19.0", 69 | "cors": "^2.8.5", 70 | "express": "^4.17.1", 71 | "express-fileupload": "^1.2.1", 72 | "firebase-admin": "^9.6.0", 73 | "firebase-functions": "^3.13.2", 74 | "glob": "^7.1.6" 75 | }, 76 | "homepage": "https://github.com/filledstacks/firebase-backend#readme", 77 | "repository": "git://github.com/filledstacks/firebase-backend.git", 78 | "bugs": { 79 | "url": "git+https://github.com/filledstacks/firebase-backend/issues" 80 | }, 81 | "author": "FilledStacks ", 82 | "auto": { 83 | "plugins": [ 84 | "npm" 85 | ], 86 | "onlyPublishWithReleaseLabel": true, 87 | "baseBranch": "main" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/models.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | // models.ts 3 | 4 | /** 5 | * @export 6 | * @enum {number} 7 | */ 8 | export enum RequestType { 9 | GET = 'GET', 10 | POST = 'POST', 11 | PUT = 'PUT', 12 | DELETE = 'DELETE', 13 | PATCH = 'PATCH', 14 | } 15 | 16 | /** 17 | * @export 18 | * @interface IExpressHandler 19 | */ 20 | export interface IExpressHandler { 21 | (req: any, res: any): any; 22 | } 23 | 24 | /** 25 | * @export 26 | * @interface ParserOptions 27 | */ 28 | export interface ParserOptions { 29 | enableCors?: boolean; 30 | buildReactive?: boolean; 31 | buildEndpoints?: boolean; 32 | groupByFolder?: boolean; 33 | } 34 | 35 | /** 36 | * @export 37 | * @type EndpointMiddleware 38 | */ 39 | export type EndpointMiddleware = (req: Request, res: Response, next: NextFunction) => any; 40 | 41 | /** 42 | * @export 43 | * @interface EndpointOptions 44 | */ 45 | export interface EndpointOptions { 46 | enableCors?: boolean; 47 | enableFileUpload?: boolean; 48 | middlewares?: EndpointMiddleware[] 49 | } 50 | 51 | /** 52 | * Stores the information to be used when creating a restful endpoint on the backend 53 | * 54 | * @export 55 | * @class Endpoint 56 | */ 57 | export class Endpoint { 58 | /** 59 | * Creates an instance of Endpoint. 60 | * 61 | * @param {string} name 62 | * @param {RequestType} requestType 63 | * @param {IExpressHandler} handler 64 | * @param {EndpointOptions} options 65 | * @memberof Endpoint 66 | */ 67 | constructor( 68 | /** 69 | * @deprecated "name" parameter is no longer needed 70 | */ 71 | public name: string | undefined, 72 | public requestType: RequestType, 73 | public handler: IExpressHandler, 74 | public options?: EndpointOptions, 75 | ) { 76 | if (!handler) { 77 | throw new Error('Please provide a endpoint request handler.'); 78 | } 79 | 80 | this.name = name; 81 | this.handler = handler; 82 | this.requestType = requestType; 83 | this.options = options; 84 | } 85 | } 86 | 87 | export class Get extends Endpoint { 88 | constructor(handler: IExpressHandler, options?: EndpointOptions) { 89 | super(undefined, RequestType.GET, handler, options); 90 | } 91 | } 92 | export class Post extends Endpoint { 93 | constructor(handler: IExpressHandler, options?: EndpointOptions) { 94 | super(undefined, RequestType.POST, handler, options); 95 | } 96 | } 97 | export class Put extends Endpoint { 98 | constructor(handler: IExpressHandler, options?: EndpointOptions) { 99 | super(undefined, RequestType.PUT, handler, options); 100 | } 101 | } 102 | export class Delete extends Endpoint { 103 | constructor(handler: IExpressHandler, options?: EndpointOptions) { 104 | super(undefined, RequestType.DELETE, handler, options); 105 | } 106 | } 107 | export class Patch extends Endpoint { 108 | constructor(handler: IExpressHandler, options?: EndpointOptions) { 109 | super(undefined, RequestType.PATCH, handler, options); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 3 | "include": ["src"], 4 | "exclude": ["dist", "docs"], 5 | "compilerOptions": { 6 | /* Basic Options */ 7 | // "incremental": true, /* Enable incremental compilation */ 8 | "target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 9 | "module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 10 | "lib": [ 11 | /* Specify library files to be included in the compilation. */ 12 | "ESNext" 13 | ], 14 | "allowJs": true /* Allow javascript files to be compiled. */, 15 | "checkJs": true /* Report errors in .js files. */, 16 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 17 | "declaration": true /* Generates corresponding '.d.ts' file. */, 18 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 19 | "sourceMap": true /* Generates corresponding '.map' file. */, 20 | // "outFile": "./", /* Concatenate and emit output to single file. */ 21 | // "outDir": "./", /* Redirect output structure to the directory. */ 22 | "rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 23 | // "composite": true, /* Enable project compilation */ 24 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 25 | "removeComments": true /* Do not emit comments to output. */, 26 | "noEmit": true /* Do not emit outputs. */, 27 | "importHelpers": true /* Import emit helpers from 'tslib'. */, 28 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 29 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 30 | 31 | /* Strict Type-Checking Options */ 32 | "strict": true /* Enable all strict type-checking options. */, 33 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 34 | "strictNullChecks": true /* Enable strict null checks. */, 35 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 36 | "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */, 37 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 38 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 39 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 40 | 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | "types": [ 48 | /* Type declaration files to be included in compilation. */ 49 | "node", 50 | "glob", 51 | "express" 52 | ], 53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 54 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | 68 | /* Advanced Options */ 69 | "skipLibCheck": true /* Skip type checking of declaration files. */, 70 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 71 | }, 72 | "typedocOptions": { 73 | "entryPoints": ["src/index.ts"], 74 | "out": "docs" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/functionParser.ts: -------------------------------------------------------------------------------- 1 | // functionParser.ts 2 | 3 | import cors from 'cors'; 4 | import express, { Application, Router } from 'express'; 5 | import fileUpload from 'express-fileupload'; 6 | import * as functions from 'firebase-functions'; 7 | import glob from 'glob'; 8 | import { parse, ParsedPath } from 'path'; 9 | import { Endpoint, ParserOptions, RequestType } from './models'; 10 | 11 | // enable short hand for console.log() 12 | const { log } = console; 13 | /** 14 | * Config for the {@link FunctionParser} constructor 15 | */ interface FunctionParserOptions { 16 | rootPath: string; 17 | exports: any; 18 | options?: ParserOptions; 19 | verbose?: boolean; 20 | } 21 | /** 22 | * This class helps with setting sup the exports for the cloud functions deployment. 23 | * 24 | * It takes in exports and then adds the required groups and their functions to it for deployment 25 | * to the cloud functions server. 26 | * 27 | * @export 28 | * @class FunctionParser 29 | */ 30 | export class FunctionParser { 31 | rootPath: string; 32 | 33 | enableCors: boolean; 34 | 35 | exports: any; 36 | 37 | verbose: boolean; 38 | /** 39 | * Creates an instance of FunctionParser. 40 | * @param {FunctionParserOptions} [config] 41 | * @memberof FunctionParser 42 | */ 43 | constructor(props: FunctionParserOptions) { 44 | const { rootPath, exports, options, verbose = false } = props; 45 | if (!rootPath) { 46 | throw new Error('rootPath is required to find the functions.'); 47 | } 48 | 49 | this.rootPath = rootPath; 50 | this.exports = exports; 51 | this.verbose = verbose; 52 | // Set default option values for if not provided 53 | this.enableCors = options?.enableCors ?? false; 54 | let groupByFolder: boolean = options?.groupByFolder ?? true; 55 | let buildReactive: boolean = options?.buildReactive ?? true; 56 | let buildEndpoints: boolean = options?.buildEndpoints ?? true; 57 | 58 | if (buildReactive) { 59 | this.buildReactiveFunctions(groupByFolder); 60 | } 61 | 62 | if (buildEndpoints) { 63 | this.buildRestfulApi(groupByFolder); 64 | } 65 | } 66 | 67 | /** 68 | * Looks for all files with .function.js and exports them on the group they belong to 69 | * 70 | * @private 71 | * @param {boolean} groupByFolder 72 | * @memberof FunctionParser 73 | */ 74 | private buildReactiveFunctions(groupByFolder: boolean) { 75 | if (this.verbose) log('Reactive Functions - Building...'); 76 | 77 | // Get all the files that has .function in the file name 78 | const functionFiles: string[] = glob.sync( 79 | `${this.rootPath}/**/*.function.js`, 80 | { 81 | cwd: this.rootPath, 82 | ignore: './node_modules/**', 83 | } 84 | ); 85 | 86 | functionFiles.forEach((file: string) => { 87 | const filePath: ParsedPath = parse(file); 88 | 89 | const directories: string[] = filePath.dir.split('/'); 90 | 91 | const groupName: string = groupByFolder 92 | ? directories[directories.length - 2] || '' 93 | : directories[directories.length - 1] || ''; 94 | 95 | const functionName = filePath.name.replace('.function', ''); 96 | 97 | if ( 98 | !process.env.FUNCTION_NAME || 99 | process.env.FUNCTION_NAME === functionName 100 | ) { 101 | if (!this.exports[groupName]) this.exports[groupName] = {}; 102 | if (this.verbose) 103 | log(`Reactive Functions - Added ${groupName}/${functionName}`); 104 | 105 | this.exports[groupName] = { 106 | ...this.exports[groupName], 107 | ...require(file), 108 | }; 109 | } 110 | }); 111 | if (this.verbose) log('Reactive Functions - Built'); 112 | } 113 | 114 | /** 115 | * Looks at all .endpoint.js files and adds them to the group they belong in 116 | * 117 | * @private 118 | * @param {boolean} groupByFolder 119 | * @memberof FunctionParser 120 | */ 121 | private buildRestfulApi(groupByFolder: boolean) { 122 | if (this.verbose) log('Restful Endpoints - Building...'); 123 | 124 | const apiFiles: string[] = glob.sync(`${this.rootPath}/**/*.endpoint.js`, { 125 | cwd: this.rootPath, 126 | ignore: './node_modules/**', 127 | }); 128 | 129 | const app: Application = express(); 130 | 131 | const groupRouters: Map = new Map(); 132 | 133 | apiFiles.forEach((file: string) => { 134 | const filePath: ParsedPath = parse(file); 135 | 136 | const directories: Array = filePath.dir.split('/'); 137 | 138 | const groupName: string = groupByFolder 139 | ? directories[directories.length - 2] || '' 140 | : directories[directories.length - 1] || ''; 141 | 142 | let router: Router | undefined = groupRouters.get(groupName); 143 | 144 | if (!router) { 145 | router = express.Router(); 146 | 147 | groupRouters.set(groupName, router); 148 | } 149 | 150 | try { 151 | this.buildEndpoint(file, groupName, router); 152 | } catch (e) { 153 | throw new Error( 154 | `Restful Endpoints - Failed to add the endpoint defined in ${file} to the ${groupName} Api.` 155 | ); 156 | } 157 | 158 | app.use('/', router); 159 | 160 | this.exports[groupName] = { 161 | ...this.exports[groupName], 162 | api: functions.https.onRequest(app), 163 | }; 164 | }); 165 | 166 | if (this.verbose) log('Restful Endpoints - Built'); 167 | } 168 | 169 | /** 170 | * Parses a .endpoint.js file and sets the endpoint path on the provided router 171 | * 172 | * @private 173 | * @param {string} file 174 | * @param {express.Router} router 175 | * @memberof FunctionParser 176 | */ 177 | private buildEndpoint( 178 | file: string, 179 | groupName: string, 180 | router: express.Router 181 | ) { 182 | const filePath: ParsedPath = parse(file); 183 | 184 | const endpoint: Endpoint = require(file).default as Endpoint; 185 | 186 | const name: string = 187 | endpoint.name || filePath.name.replace('.endpoint', ''); 188 | 189 | const { handler } = endpoint; 190 | 191 | // Enable cors if it is enabled globally else only enable it for a particular route 192 | if (this.enableCors) { 193 | router.use(cors()); 194 | } else if (endpoint.options?.enableCors) { 195 | if (this.verbose) log(`Cors enabled for ${name}`); 196 | router.use(cors()); 197 | } 198 | 199 | if (endpoint.options?.enableFileUpload) { 200 | if (this.verbose) log(`File upload enabled for ${name}`); 201 | router.use(fileUpload()); 202 | } 203 | 204 | switch (endpoint.requestType) { 205 | case RequestType.GET: 206 | router.get(`/${name}`, endpoint.options?.middlewares ?? [], handler); 207 | break; 208 | 209 | case RequestType.POST: 210 | router.post(`/${name}`, endpoint.options?.middlewares ?? [], handler); 211 | break; 212 | 213 | case RequestType.PUT: 214 | router.put(`/${name}`, endpoint.options?.middlewares ?? [], handler); 215 | break; 216 | 217 | case RequestType.DELETE: 218 | router.delete(`/${name}`, endpoint.options?.middlewares ?? [], handler); 219 | break; 220 | 221 | case RequestType.PATCH: 222 | router.patch(`/${name}`, endpoint.options?.middlewares ?? [], handler); 223 | break; 224 | 225 | default: 226 | throw new Error( 227 | `A unsupported RequestType was defined for a Endpoint.\n 228 | Please make sure that the Endpoint file exports a RequestType 229 | using the constants in src/system/constants/requests.ts.\n 230 | **This value is required to add the Endpoint to the API**` 231 | ); 232 | } 233 | if (this.verbose) 234 | log( 235 | `Restful Endpoints - Added ${groupName}/${endpoint.requestType}:${name}` 236 | ); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # firebase-backend 2 | 3 | [![CI](https://github.com/filledstacks/firebase-backend/actions/workflows/main.yml/badge.svg)](https://github.com/filledstacks/firebase-backend/actions/workflows/main.yml) 4 | [![Version](https://img.shields.io/npm/v/firebase-backend.svg)](https://www.npmjs.com/package/firebase-backend) 5 | ![Prerequisite](https://img.shields.io/badge/node-%3E%3D10-blue.svg) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) 7 | 8 | > A package that helps with the management and expansion of a maintainable 9 | > firebase backend 10 | 11 | - [firebase-backend](#firebase-backend) 12 | - [Requirements](#requirements) 13 | - [Overview](#overview) 14 | - [Types of Functions](#types-of-functions) 15 | - [Code structure](#code-structure) 16 | - [Code Setup](#code-setup) 17 | - [Installation](#installation) 18 | - [Configuration](#configuration) 19 | - [Restful Functions (Endpoints)](#restful-functions-endpoints) 20 | - [Reactive Functions](#reactive-functions) 21 | - [Environment Setup](#environment-setup) 22 | - [Deploy](#deploy) 23 | - [Author](#author) 24 | - [Contributing](#contributing) 25 | - [License](#license) 26 | 27 | ## Requirements 28 | 29 | - node >=10 30 | 31 | ## Overview 32 | 33 | Let's to start out by going through a high level overview of how the backend 34 | will be setup. This overview will go over the types of functions we use as well 35 | as the actual code structure. 36 | 37 | ### Types of Functions 38 | 39 | The backend is built around the strengths that firebase poses in their 40 | serverless cloud functions setup. Focussing on those strengths we can break the 41 | system into two types of functions (could also be called a micro-service if you 42 | choose to). Reactive and RESTul 43 | 44 | - **Reactive:** This is a function that will run in reaction to data or state 45 | updating on the backend. An example of this will be when a file is uploaded to 46 | cloud storage or the most common when a document / entry in the database has 47 | been updated. 48 | - **RESTful:** This is the function that will run when the user makes an http 49 | request to the uri the function is assigned to. Nothing special about these 50 | ones. Just 1 note that's very important. This will not be used for single CRUD 51 | operations like adding a user, deleting a user, updating a user. And it's 52 | built with that in mind. This means you won't be able to define a single api 53 | endpoint for user that behaves differently based on the HTTP verb used. This 54 | is by design and won't be changed. All CRUD should be performed directly on 55 | your Firebase DB of choice. That's how this is supposed to be used. 56 | 57 | ### Code structure 58 | 59 | We have an enforced code structure that will help with the organization of the 60 | backend as well as the overall maintenance as it grows. There's 3 major things 61 | to go over. 62 | 63 | 1. **Each function will be in its own dedicated file:** This is to get rid of 64 | the "natural" tendency, when starting with firebase cloud functions, to keep 65 | adding functions into the same index file forcing it to grow bigger as your 66 | backend requirements grow. _The file name will be the exact name of the 67 | endpoint to keep things easy to manage. This is not a requirement but I've 68 | found it to be quite helpful_. 69 | 2. Functions will be placed in a folder titled either restful or reactive 70 | 3. The backend will be split into different resource groups to ensure a 71 | structured backend in production 72 | 73 | Organize your `firebase functions` folder into api domain folders (`groups`) and function type (`reactive`, `restful`). 74 | 75 | ``` 76 | src 77 | {group_name_folder} 78 | reactive 79 | - onSomeTrigger.function.ts 80 | - onSomeOtherTrigger.function.ts 81 | restful 82 | - someEndpointName.endpoint.ts 83 | - someOtherEndpointName.endpoint.ts 84 | - index.ts 85 | package.json 86 | ``` 87 | 88 | ## Code Setup 89 | 90 | ### Installation 91 | 92 | We'll start off by installing the package dedicated to using this system 93 | `firebase-backend`. Install the package through npm 94 | 95 | ```sh 96 | npm install firebase-backend 97 | ``` 98 | 99 | ### Configuration 100 | 101 | Then you can open the index.ts file in your source folder and update it to 102 | 103 | ```ts 104 | import { FunctionParser } from 'firebase-backend'; 105 | 106 | exports = new FunctionParser({ rootPath: __dirname, exports, verbose: true }) 107 | .exports; 108 | ``` 109 | 110 | These are the two magical lines of code that allows us to dynamically add and 111 | export functions as the backend grows without ever changing the index file 🥳. 112 | And that's also all we need to set it up. Now we can start creating functions 😎 113 | 114 | #### Add a prefixed deployment 115 | 116 | If you want to prefix all the generated cloud functions, for versioning, or for any use case, see the example below. This will add the version v2\_ infront of all deployed functions keeping your previously deployed functions in tact. 117 | 118 | ```ts 119 | import { FunctionParser } from 'firebase-backend'; 120 | 121 | exports = new FunctionParser(__dirname, exports).exports; 122 | 123 | const backendVersion = 'v2'; 124 | const seperator = '_'; 125 | 126 | for (const key in exports) { 127 | if (Object.prototype.hasOwnProperty.call(exports, key)) { 128 | exports[`${backendVersion}${seperator}${key}`] = exports[key]; 129 | delete exports[key]; 130 | } 131 | } 132 | ``` 133 | 134 | ### Restful Functions (Endpoints) 135 | 136 | **Create** 137 | 138 | Let's say we wanted to make an endpoint where a client application could add a payment method for a user. 139 | 140 | - The API would be called `users` 141 | - The function would be called `addPaymentMethod` 142 | - The file would be called `src/users/restful/addPaymentMethod.endpoint.ts` 143 | - The endpoint name will be exactly the name of your file 144 | - The `endpoint.ts` file extension identifies the function as an HTTP endpoint 145 | 146 | ```ts 147 | // src/users/restful/addPaymentMethod.endpoint.ts 148 | import { Request, Response } from 'express'; 149 | import { Post } from 'firebase-backend'; // Get, Post, Put, Update, Delete available 150 | 151 | // Use the `Post` class which is extended from the `Endpoint` class. 152 | export default new Post((request: Request, response: Response) => { 153 | // Read the values out of the body 154 | const cardNumber = request.body['card_number']; 155 | const cardHolder = request.body['card_holder']; 156 | 157 | // Do your thing with the values 158 | var paymentToken = `${cardNumber}_${cardHolder}`; 159 | 160 | // Send your response. 201 to indicate the creation of a new resource 161 | return response.status(201).send({ 162 | token: paymentToken, 163 | }); 164 | }); 165 | ``` 166 | 167 | **Middleware** 168 | You can now pass an array of middleware you'd want to add to an endpoint: 169 | 170 | ```ts 171 | // src/users/restful/auth.middleware.ts 172 | import { Request, Response, NextFunction } from 'express' 173 | export const authMiddleware = (req: Request, res: Response, next: NextFunction) => { ... } 174 | 175 | // src/users/restful/addPaymentMethod.endpoint.ts 176 | import { Request, Response } from 'express' 177 | import { EndpointMiddleware, Post } from 'firebase-backend' 178 | import { authMiddleware } from './auth.middleware' 179 | 180 | export default new Post((req: Request, res: Response) => {}, { 181 | middlewares: [authMiddleware] 182 | }) 183 | ``` 184 | 185 | **Testing** 186 | 187 | To test this out we'll run the following command in the functions folder. 188 | 189 | ```sh 190 | npm run serve 191 | ``` 192 | 193 | This will build the TypeScript code and then serve the functions locally through 194 | the emulator. If this is successful you should see the following in the console. 195 | You should see the functions API has deployed (locally) a function at the 196 | following url 197 | 198 | ```sh 199 | http://localhost:5001/boxtout-fireship/us-central1/users-api 200 | ``` 201 | 202 | All the endpoints in the users resource group will be deployed under the 203 | `/user-api` function. This means that we can make a post request to the endpoint 204 | with the expected data and check if we get back a result. I'm going to use 205 | [PostMan](https://www.postman.com/) to test this out. So we'll put in the above 206 | url and add `/addpaymentmethod` at the end of it. Select post as the HTTP 207 | request type and then pass in a body. 208 | 209 | ```json 210 | { 211 | "card_number": "5418754514815181", 212 | "card_holder": "FilledStacks" 213 | } 214 | ``` 215 | 216 | When we execute this we get back the token in the format we supplied 217 | 218 | ```json 219 | { 220 | "token": "5418754514815181_FilledStacks" 221 | } 222 | ``` 223 | 224 | There we have it, your first endpoint created. Next up is reactive functions. 225 | 226 | ### Reactive Functions 227 | 228 | **Create** 229 | 230 | Let's say we wanted to make a function that would run when the firestore db had a user record updated. 231 | 232 | - The API would be called `users` 233 | - The function would be called `onUserCreated` 234 | - The file would be called `src/users/reactive/onUserCreated.function.ts` 235 | - The endpoint name will be exactly the name of your file 236 | - The `function.ts` file extension identifies the function as reactive 237 | 238 | ```ts 239 | // src/users/reactive/onUserCreated.function.ts 240 | import * as functions from 'firebase-functions'; 241 | 242 | export default functions.firestore 243 | .document('users/{userId}') 244 | .onCreate((userSnapshot, context) => { 245 | const data = userSnapshot.data(); 246 | console.log(`User Created | send an email to ${data.email}`); 247 | }); 248 | ``` 249 | 250 | Run `npm run build` in the functions folder. Then run `firebase emulators:start`. 251 | 252 | You should now have a function deployed at `users-onUserCreated` as well as at 253 | `users-api`. All the api endpoints go under the one api function, but the 254 | reactive functions are added as their own functions. Lets test this out. 255 | 256 | **Testing** 257 | 258 | At the bottom of your logs you'll see a link to firestore 259 | . Open that in your browser. You'll see an 260 | empty page. Click on start collection, make the collection id `users` . Add a 261 | field called email and put the value dane@filledstacks.com and save the 262 | document. When this is saved you should see the logs printing out the following 263 | message 264 | 265 | ```log 266 | i functions: Beginning execution of "users-onUserCreated" 267 | > User id created TybqxAwnC4X5DWLgtXOp 268 | > {"severity":"WARNING","message":"Function returned undefined, expected Promise or value"} 269 | i functions: Finished "users-onUserCreated" in ~1s 270 | > User Created | send an email to dane@filledstacks.com 271 | ``` 272 | 273 | And that's it! You've created a reactive function as well as a http endpoint. 274 | Going further when you want to expand you backend you simply create a new file 275 | in the dedicated folder depending on the function type and it'll be added 276 | automatically. 277 | 278 | ### Environment Setup 279 | 280 | The way that the default TypeScript project is setup is not sufficient for 281 | consistent deployments and debugging. Because of that we'll add some additional 282 | things into our project. We'll start by making sure that old function code don't 283 | lurk around when we're testing any new changes. To fix that we'll add a new 284 | package into the functions folder called `rimraf` 285 | 286 | ```sh 287 | npm install -D rimraf 288 | ``` 289 | 290 | Then we'll add 2 new scripts into the `package.json` . Above the `build` script 291 | we'll add `clean` and `prebuild`. 292 | 293 | ```json 294 | "scripts": { 295 | "lint": "eslint --ext .js,.ts .", 296 | "clean": "rimraf lib/", 297 | "prebuild": "npm run clean", 298 | "build": "tsc", 299 | "serve": "npm run build && firebase emulators:start --only functions", 300 | "shell": "npm run build && firebase functions:shell", 301 | "start": "npm run shell", 302 | "deploy": "firebase deploy --only functions", 303 | "logs": "firebase functions:log" 304 | } 305 | ``` 306 | 307 | This will now clean out your generated code before building the new code. 308 | 309 | ### Deploy 310 | 311 | And finally we can deploy our backend. We first run `npm run build` when that's 312 | complete we run `npm run deploy` and that will push all the latest function code 313 | to your firebase project. 314 | 315 | ## Author 316 | 317 | **FilledStacks ** 318 | 319 | - Website: 320 | - GitHub: [@FilledStacks](https://github.com/FilledStacks) 321 | 322 | ## Contributing 323 | 324 | Contributions, issues and feature requests are welcome! 325 | 326 | Feel free to check 327 | [issues page](git+https://github.com/filledstacks/firebase-backend/issues). 328 | 329 | ## License 330 | 331 | Copyright © 2021 332 | [FilledStacks ](https://github.com/FilledStacks). 333 | 334 | This project is [MIT](LICENSE) licensed. 335 | --------------------------------------------------------------------------------