├── example ├── awsLambda │ ├── deploy_to_aws_lambda.sh │ ├── awsLambda.js │ ├── package.json │ ├── create_lambdb_layer.sh │ ├── serverless.yml │ └── config │ │ └── meanapi.config.js ├── netlify │ ├── functions │ │ └── meanapi │ │ │ ├── meanapi.js │ │ │ └── meanapi.config.js │ ├── package.json │ ├── netlify.toml │ └── public │ │ └── index.html ├── localhost │ ├── server.js │ ├── package.json │ └── meanapi.config.js └── vercel │ ├── package.json │ ├── api │ └── vercel.js │ ├── vercel.json │ ├── config │ └── meanapi.config.js │ └── public │ └── index.html ├── reactAdmin ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── src │ ├── setupTests.js │ ├── App.test.js │ ├── index.css │ ├── reportWebVitals.js │ ├── index.js │ ├── App.css │ ├── App.js │ ├── user.js │ ├── logo.svg │ ├── pet.js │ ├── species.js │ ├── story.js │ └── comment.js ├── .gitignore ├── package.json └── README.md ├── azureFunctions ├── index.js └── function.json ├── netlifyFunctions └── serverlessApi.js ├── .gitignore ├── vercel.js ├── .npmignore ├── vercel.json ├── server.js ├── awsLambda.js ├── app ├── models │ ├── universal.model.js │ └── index.js ├── middlewares │ └── checkAuth.js ├── express │ └── universal.app.js ├── config │ ├── api.config-BAK.js │ └── api.config.js ├── routes │ └── express-resource-alex.js ├── helper │ └── commonHelper.js └── controllers │ └── universal.controller.js ├── package.json ├── serverless.yml └── README.md /example/awsLambda/deploy_to_aws_lambda.sh: -------------------------------------------------------------------------------- 1 | serverless deploy -------------------------------------------------------------------------------- /reactAdmin/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /azureFunctions/index.js: -------------------------------------------------------------------------------- 1 | const { app, serverless } = require("../app/express/universal.app"); 2 | 3 | module.exports = serverless(app); 4 | -------------------------------------------------------------------------------- /reactAdmin/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexStack/NodeJs-Express-MongoDB-Universal-CRUD-API/master/reactAdmin/public/favicon.ico -------------------------------------------------------------------------------- /reactAdmin/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexStack/NodeJs-Express-MongoDB-Universal-CRUD-API/master/reactAdmin/public/logo192.png -------------------------------------------------------------------------------- /reactAdmin/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexStack/NodeJs-Express-MongoDB-Universal-CRUD-API/master/reactAdmin/public/logo512.png -------------------------------------------------------------------------------- /netlifyFunctions/serverlessApi.js: -------------------------------------------------------------------------------- 1 | const { app, serverless } = require("../app/express/universal.app"); 2 | 3 | module.exports.handler = serverless(app); -------------------------------------------------------------------------------- /example/awsLambda/awsLambda.js: -------------------------------------------------------------------------------- 1 | const { app, serverless, API_CONFIG } = require("universal-mean-api"); 2 | 3 | module.exports.lambdaHandler = serverless(app); 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | node_modules 3 | node_lambda_layer 4 | .serverless 5 | .env 6 | *.tgz 7 | *.gz 8 | *.zip 9 | *.git* 10 | 11 | package-lock.json 12 | package-lock.bak.json -------------------------------------------------------------------------------- /example/netlify/functions/meanapi/meanapi.js: -------------------------------------------------------------------------------- 1 | const { app, serverless } = require("universal-mean-api"); 2 | 3 | // console.log(process.env); 4 | 5 | module.exports.handler = serverless(app); -------------------------------------------------------------------------------- /example/localhost/server.js: -------------------------------------------------------------------------------- 1 | const { app, API_CONFIG } = require("universal-mean-api"); 2 | 3 | app.listen(API_CONFIG.PORT, () => { 4 | console.log(`API is running on port ${API_CONFIG.PORT}.`); 5 | }); 6 | -------------------------------------------------------------------------------- /vercel.js: -------------------------------------------------------------------------------- 1 | const { app, API_CONFIG } = require("./app/express/universal.app"); 2 | 3 | // module.exports = serverless(app); 4 | app.listen(API_CONFIG.PORT, () => { 5 | console.log(`Server is running on port ${API_CONFIG.PORT}.`); 6 | }); 7 | -------------------------------------------------------------------------------- /example/netlify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-mean-api", 3 | "homepage": "https://github.com/AlexStack/NodeJs-Express-MongoDB-Universal-CRUD-API", 4 | "license": "MIT", 5 | "dependencies": { 6 | "universal-mean-api": "^0.9.4" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /example/vercel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-mean-api", 3 | "homepage": "https://github.com/AlexStack/NodeJs-Express-MongoDB-Universal-CRUD-API", 4 | "license": "MIT", 5 | "dependencies": { 6 | "universal-mean-api": "^0.9.4" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /example/awsLambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-mean-api", 3 | "homepage": "https://github.com/AlexStack/NodeJs-Express-MongoDB-Universal-CRUD-API", 4 | "license": "MIT", 5 | "dependencies": { 6 | "universal-mean-api": "^0.9.4" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /example/localhost/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-mean-api", 3 | "homepage": "https://github.com/AlexStack/NodeJs-Express-MongoDB-Universal-CRUD-API", 4 | "license": "MIT", 5 | "dependencies": { 6 | "universal-mean-api": "^0.9.4" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /reactAdmin/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /example/vercel/api/vercel.js: -------------------------------------------------------------------------------- 1 | const { app, serverless, API_CONFIG } = require("universal-mean-api"); 2 | 3 | // module.exports = serverless(app); 4 | app.listen(API_CONFIG.PORT, () => { 5 | console.log(`Server is running on port ${API_CONFIG.PORT}. process.cwd()=` + process.cwd()); 6 | }); 7 | -------------------------------------------------------------------------------- /reactAdmin/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # test .npmignore by run "npm pack" before run "npm publish" 2 | vendor 3 | node_modules 4 | node_lambda_layer 5 | .serverless 6 | .env 7 | reactAdmin 8 | azureFunctions 9 | netlifyFunctions 10 | example 11 | vercel.* 12 | serverless.* 13 | *.zip 14 | *.tgz 15 | *.gz 16 | *bak.js 17 | *BAK.js -------------------------------------------------------------------------------- /example/vercel/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "routes": [ 4 | { 5 | "src": "/api/(.*)", 6 | "dest": "/api/vercel.js" 7 | } 8 | ], 9 | "functions": { 10 | "api/vercel.js": { 11 | "includeFiles": "config/**" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "./vercel.js", 6 | "use": "@vercel/node" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/serverlessApi/(.*)", 12 | "dest": "/vercel.js" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const { app, serverless, API_CONFIG } = require("./app/express/universal.app"); 2 | 3 | if (API_CONFIG.IS_SERVERLESS) { 4 | module.exports.lambdaHandler = serverless(app); 5 | } else { 6 | app.listen(app.get('config').PORT, () => { 7 | console.log(`API is running on port ${API_CONFIG.PORT}.`); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /awsLambda.js: -------------------------------------------------------------------------------- 1 | const { app, serverless, API_CONFIG } = require("./app/express/universal.app"); 2 | 3 | if (API_CONFIG.IS_SERVERLESS) { 4 | module.exports.lambdaHandler = serverless(app); 5 | } else { 6 | app.listen(app.get('config').PORT, () => { 7 | console.log(`API is running on port ${API_CONFIG.PORT}.`); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /azureFunctions/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled": false, 3 | "bindings": [ 4 | { 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "authLevel": "anonymous", 9 | "route": "{*path}" 10 | }, 11 | { 12 | "type": "http", 13 | "direction": "out", 14 | "name": "res" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /example/awsLambda/create_lambdb_layer.sh: -------------------------------------------------------------------------------- 1 | npm install 2 | mkdir -p node_modules_layer/nodejs 3 | mv node_modules node_modules_layer/nodejs/node_modules 4 | cd node_modules_layer/ 5 | zip -r nodejs.zip ./* 6 | # upload nodejs.zip to aws lambda layer via aws lambda layer UI 7 | # add the layer to lambda function via aws lambda function UI 8 | # rm -rf node_modules_layer && rm -rf package-lock.json -------------------------------------------------------------------------------- /reactAdmin/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /reactAdmin/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /reactAdmin/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /app/models/universal.model.js: -------------------------------------------------------------------------------- 1 | module.exports = (mongoose, mongooseSchema, mongooseOption, collectionName) => { 2 | var schema = mongoose.Schema( 3 | mongooseSchema, mongooseOption 4 | ); 5 | 6 | schema.method("toJSON", function () { 7 | const { __v, _id, ...object } = this.toObject(); 8 | object.id = _id; 9 | return object; 10 | }); 11 | 12 | const Universal = mongoose.model(collectionName, schema); 13 | return Universal; 14 | }; 15 | -------------------------------------------------------------------------------- /reactAdmin/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /reactAdmin/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /example/netlify/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | functions = "functions" 3 | 4 | [context.production] 5 | publish = "public/" 6 | environment = { API_BASE = ".netlify/functions/meanapi/", API_CONFIG_FILE = "/var/task/src/functions/meanapi/meanapi.config.js" } 7 | 8 | [context.deploy-preview] 9 | publish = "public/" 10 | environment = { API_BASE = ".netlify/functions/meanapi/", API_CONFIG_FILE = "/var/task/src/functions/meanapi/meanapi.config.js" } 11 | 12 | [context.branch-deploy] 13 | publish = "public/" 14 | environment = { API_BASE = ".netlify/functions/meanapi/", API_CONFIG_FILE = "/var/task/src/functions/meanapi/meanapi.config.js" } -------------------------------------------------------------------------------- /app/models/index.js: -------------------------------------------------------------------------------- 1 | const helper = require("../helper/commonHelper"); 2 | 3 | // const API_CONFIG = require("../config/api.config"); 4 | const API_CONFIG = helper.getApiConfig(); 5 | 6 | const mongoose = require("mongoose"); 7 | mongoose.Promise = global.Promise; 8 | 9 | const db = {}; 10 | db.mongoose = mongoose; 11 | 12 | API_CONFIG.API_SCHEMAS.forEach(apiSchema => { 13 | db[apiSchema.apiRoute] = require("./universal.model.js")(mongoose, apiSchema.schema, apiSchema.mongooseOption, apiSchema.collectionName); 14 | 15 | // console.log(apiSchema); 16 | }); 17 | // console.log(apiSchemas.SimpleMessage); 18 | 19 | module.exports = db; 20 | -------------------------------------------------------------------------------- /reactAdmin/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/awsLambda/serverless.yml: -------------------------------------------------------------------------------- 1 | service: Universal-MEAN-API 2 | frameworkVersion: "2" 3 | provider: 4 | name: aws 5 | runtime: nodejs12.x 6 | stage: dev 7 | region: ap-southeast-2 8 | environment: 9 | DB: "mongodb-uri" 10 | 11 | # packaging information here 12 | package: 13 | exclude: 14 | - "node_modules/**" 15 | - "node_lambda_layer/**" 16 | - ".git/**" 17 | - "vendor/**" 18 | - ".serverless" 19 | - ".env" 20 | - "reactAdmin/**" 21 | - "*.sh" 22 | 23 | # function information here, you can change the function name MeanApiExample 24 | functions: 25 | MeanApiExample: 26 | handler: awsLambda.lambdaHandler 27 | memorySize: 128 28 | description: Mean-Api-aws-example 29 | events: 30 | - http: "ANY /" 31 | - http: "ANY /{proxy+}" 32 | -------------------------------------------------------------------------------- /reactAdmin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactAdmin", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.1", 7 | "@testing-library/react": "^11.2.7", 8 | "@testing-library/user-event": "^12.8.3", 9 | "prop-types": "^15.7.2", 10 | "ra-data-json-server": "^3.19.3", 11 | "react": "^17.0.2", 12 | "react-admin": "^3.19.3", 13 | "react-dom": "^17.0.2", 14 | "react-scripts": "4.0.0", 15 | "web-vitals": "^0.2.4" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-mean-api", 3 | "version": "0.9.4", 4 | "description": "This package allows you to set up a backend API using node.js & mongodb in 2 minutes. NodeJs-Express-MongoDB-Universal-CRUD-API(Universal MEAN API) for dynamic MongoDB tables, no need to repeat the CRUD API code for every table. Focus on run it in a serverless environment.", 5 | "homepage": "https://github.com/AlexStack/NodeJs-Express-MongoDB-Universal-CRUD-API", 6 | "author": "Alex Zeng", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/AlexStack/NodeJs-Express-MongoDB-Universal-CRUD-API.git" 11 | }, 12 | "main": "./app/express/universal.app.js", 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "dependencies": { 17 | "bcrypt": "^5.0.1", 18 | "body-parser": "^1.19.0", 19 | "cors": "^2.8.5", 20 | "dotenv": "^8.2.0", 21 | "express": "^4.17.1", 22 | "firebase-admin": "^9.4.2", 23 | "ini": "^2.0.0", 24 | "jsonwebtoken": "^8.5.1", 25 | "mongoose": "^5.10.8", 26 | "serverless-http": "^2.6.0" 27 | }, 28 | "engines": { 29 | "node": "12.x" 30 | }, 31 | "devDependencies": { 32 | "nodemon": "^2.0.6" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /reactAdmin/src/App.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Admin, Resource } from 'react-admin'; 3 | import { UserList, UserEdit, UserCreate } from './user'; 4 | import jsonServerProvider from 'ra-data-json-server'; 5 | import PostIcon from '@material-ui/icons/Book'; 6 | import UserIcon from '@material-ui/icons/Group'; 7 | import { PetList, PetEdit, PetCreate } from "./pet"; 8 | import { StoryList, StoryEdit, StoryCreate } from "./story"; 9 | import { CommentList, CommentEdit, CommentCreate } from "./comment"; 10 | import { SpeciesList, SpeciesEdit, SpeciesCreate } from "./species"; 11 | 12 | // const dataProvider = jsonServerProvider('http://localhost:8080/api'); 13 | 14 | const dataProvider = jsonServerProvider(process.env.REACT_APP_MEAN_API || 'http://localhost:8080/api'); 15 | 16 | const App = () => ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | export default App; 29 | -------------------------------------------------------------------------------- /example/vercel/config/meanapi.config.js: -------------------------------------------------------------------------------- 1 | module.exports.DB = process.env.DB || 'please-set-database-connect-uri-first'; 2 | module.exports.API_BASE = process.env.API_BASE || 'api/'; 3 | module.exports.PORT = process.env.PORT || '8080'; 4 | module.exports.CORS_ORIGIN = process.env.CORS_ORIGIN || [/localhost/, /\.test$/]; 5 | module.exports.IS_SERVERLESS = !!(process.env.LAMBDA_TASK_ROOT || process.env.AWS_LAMBDA_FUNCTION_NAME); 6 | 7 | module.exports.API_SCHEMAS = [ 8 | { 9 | "apiRoute": "posts", 10 | "collectionName": "posts", 11 | "schema": { 12 | title: String, 13 | content: String, 14 | category: String, 15 | parentId: String 16 | }, 17 | "mongooseOption": { timestamps: true, strict: false }, 18 | "searchFields": ["title", "content", "category"], 19 | }, 20 | { 21 | "apiRoute": "comments", 22 | "collectionName": "comments", 23 | "schema": { 24 | title: { 25 | type: String, 26 | required: [true, 'Title is required.'] 27 | }, 28 | description: String, 29 | userId: { type: Number, required: [true, 'userId is required.'] } 30 | }, 31 | "mongooseOption": { timestamps: true, strict: false }, 32 | "searchFields": ["description", "title"], 33 | }, 34 | { 35 | "apiRoute": "users", 36 | "collectionName": "users", 37 | "schema": { 38 | name: String, 39 | dob: Date, 40 | age: { 41 | type: Number, 42 | min: [18, 'Too young'], 43 | max: 80 44 | } 45 | }, 46 | "mongooseOption": { timestamps: false, strict: false }, 47 | "searchFields": ["name"], 48 | } 49 | ]; -------------------------------------------------------------------------------- /example/awsLambda/config/meanapi.config.js: -------------------------------------------------------------------------------- 1 | module.exports.DB = process.env.DB || 'please-set-database-connect-uri-first'; 2 | module.exports.API_BASE = process.env.API_BASE || 'api/'; 3 | module.exports.PORT = process.env.PORT || '8080'; 4 | module.exports.CORS_ORIGIN = process.env.CORS_ORIGIN || [/localhost/, /\.test$/]; 5 | module.exports.IS_SERVERLESS = !!(process.env.LAMBDA_TASK_ROOT || process.env.AWS_LAMBDA_FUNCTION_NAME); 6 | 7 | module.exports.API_SCHEMAS = [ 8 | { 9 | "apiRoute": "posts", 10 | "collectionName": "posts", 11 | "schema": { 12 | title: String, 13 | content: String, 14 | category: String, 15 | parentId: String 16 | }, 17 | "mongooseOption": { timestamps: true, strict: false }, 18 | "searchFields": ["title", "content", "category"], 19 | }, 20 | { 21 | "apiRoute": "comments", 22 | "collectionName": "comments", 23 | "schema": { 24 | title: { 25 | type: String, 26 | required: [true, 'Title is required.'] 27 | }, 28 | description: String, 29 | userId: { type: Number, required: [true, 'userId is required.'] } 30 | }, 31 | "mongooseOption": { timestamps: true, strict: false }, 32 | "searchFields": ["description", "title"], 33 | }, 34 | { 35 | "apiRoute": "users", 36 | "collectionName": "users", 37 | "schema": { 38 | name: String, 39 | dob: Date, 40 | age: { 41 | type: Number, 42 | min: [18, 'Too young'], 43 | max: 80 44 | } 45 | }, 46 | "mongooseOption": { timestamps: false, strict: false }, 47 | "searchFields": ["name"], 48 | } 49 | ]; -------------------------------------------------------------------------------- /example/netlify/functions/meanapi/meanapi.config.js: -------------------------------------------------------------------------------- 1 | module.exports.DB = process.env.DB || 'please-set-database-connect-uri-first'; 2 | module.exports.API_BASE = process.env.API_BASE || 'api/'; 3 | module.exports.PORT = process.env.PORT || '8080'; 4 | module.exports.CORS_ORIGIN = process.env.CORS_ORIGIN || [/localhost/, /\.test$/]; 5 | module.exports.IS_SERVERLESS = !!(process.env.LAMBDA_TASK_ROOT || process.env.AWS_LAMBDA_FUNCTION_NAME); 6 | 7 | module.exports.API_SCHEMAS = [ 8 | { 9 | "apiRoute": "posts", 10 | "collectionName": "posts", 11 | "schema": { 12 | title: String, 13 | content: String, 14 | category: String, 15 | parentId: String 16 | }, 17 | "mongooseOption": { timestamps: true, strict: false }, 18 | "searchFields": ["title", "content", "category"], 19 | }, 20 | { 21 | "apiRoute": "comments", 22 | "collectionName": "comments", 23 | "schema": { 24 | title: { 25 | type: String, 26 | required: [true, 'Title is required.'] 27 | }, 28 | description: String, 29 | userId: { type: Number, required: [true, 'userId is required.'] } 30 | }, 31 | "mongooseOption": { timestamps: true, strict: false }, 32 | "searchFields": ["description", "title"], 33 | }, 34 | { 35 | "apiRoute": "users", 36 | "collectionName": "users", 37 | "schema": { 38 | name: String, 39 | dob: Date, 40 | age: { 41 | type: Number, 42 | min: [18, 'Too young'], 43 | max: 80 44 | } 45 | }, 46 | "mongooseOption": { timestamps: false, strict: false }, 47 | "searchFields": ["name"], 48 | } 49 | ]; -------------------------------------------------------------------------------- /reactAdmin/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /example/vercel/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | MEAN Universal API for Vercel! 13 | 14 | 15 | 16 |

MEAN Universal API for Vercel!

17 |
18 | Please set up below environment variables from the Vercel website UI manually: 19 | 20 | 23 | 24 |
25 |
26 | Root API Endpoint: /api 27 |
28 | Posts API Endpoint: /api/posts 29 |
30 | Comments API Endpoint: /api/comments 31 |
32 | Users API Endpoint: /api/users 33 | 34 |
35 | GitHub: 36 | https://github.com/AlexStack/NodeJs-Express-MongoDB-Universal-CRUD-API 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /example/localhost/meanapi.config.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const isServerless = !!(process.env.LAMBDA_TASK_ROOT || process.env.AWS_LAMBDA_FUNCTION_NAME); 3 | 4 | const envFile = process.cwd() + '/.env'; 5 | if (!isServerless && fs.existsSync(envFile)) { 6 | const envResult = require('dotenv').config({ path: envFile }); 7 | if (envResult.error) { 8 | throw envResult.error; 9 | } else if (process.env.DEBUG == 'yes') { 10 | console.log('envResult:', envResult.parsed); 11 | } 12 | } 13 | 14 | module.exports.DB = process.env.DB || 'please-set-database-connect-uri-first'; 15 | module.exports.API_BASE = process.env.API_BASE || 'api/'; 16 | module.exports.PORT = process.env.PORT || '8080'; 17 | module.exports.CORS_ORIGIN = process.env.CORS_ORIGIN || [/localhost/, /\.test$/]; 18 | module.exports.IS_SERVERLESS = !!(process.env.LAMBDA_TASK_ROOT || process.env.AWS_LAMBDA_FUNCTION_NAME); 19 | 20 | module.exports.API_SCHEMAS = [ 21 | { 22 | "apiRoute": "posts", 23 | "collectionName": "posts", 24 | "schema": { 25 | title: String, 26 | content: String, 27 | category: String, 28 | parentId: String 29 | }, 30 | "mongooseOption": { timestamps: true, strict: false }, 31 | "searchFields": ["title", "content", "category"], 32 | }, 33 | { 34 | "apiRoute": "comments", 35 | "collectionName": "comments", 36 | "schema": { 37 | title: { 38 | type: String, 39 | required: [true, 'Title is required.'] 40 | }, 41 | description: String, 42 | userId: { type: Number, required: [true, 'userId is required.'] } 43 | }, 44 | "mongooseOption": { timestamps: true, strict: false }, 45 | "searchFields": ["description", "title"], 46 | }, 47 | { 48 | "apiRoute": "users", 49 | "collectionName": "users", 50 | "schema": { 51 | name: String, 52 | dob: Date, 53 | age: { 54 | type: Number, 55 | min: [18, 'Too young'], 56 | max: 80 57 | } 58 | }, 59 | "mongooseOption": { timestamps: false, strict: false }, 60 | "searchFields": ["name"], 61 | } 62 | ]; -------------------------------------------------------------------------------- /reactAdmin/src/user.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { List, Edit, Create, Datagrid, TextField, EmailField, EditButton, SimpleForm, TextInput, NumberInput, DateInput, PasswordInput, SelectInput, SelectField } from 'react-admin'; 3 | 4 | const UserRoles = [ 5 | { id: 'normalUser', name: 'Registered User' }, 6 | { id: 'vipUser', name: 'VIP User' }, 7 | { id: 'webAdmin', name: 'Website Admin' }, 8 | ]; 9 | export const UserList = props => ( 10 | 11 | 12 | {/* */} 13 | 14 | 15 | {/* */} 16 | 17 | 18 | {/* 19 | 20 | */} 21 | 22 | 23 | 24 | ); 25 | 26 | const EditTitle = ({ record }) => { 27 | return Edit {record ? `"${record.firstName}"` : ''}; 28 | }; 29 | 30 | export const UserEdit = props => ( 31 | } {...props}> 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | 45 | export const UserCreate = props => ( 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ); -------------------------------------------------------------------------------- /example/netlify/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | MEAN Universal API Works! 13 | 14 | 15 | 16 |

MEAN Universal API for NetLify

17 | 18 |
19 | According to some research, the Netlify Functions cannot access the Env Variables from netlify.toml. So, Please set up below environment variables from the Netlify website UI manually: 20 | 21 | 26 | 27 |
28 | 29 |
30 | Posts API Endpoint: /.netlify/functions/meanapi/posts 31 |
32 | Comments API Endpoint: /.netlify/functions/meanapi/comments 33 |
34 | Users API Endpoint: /.netlify/functions/meanapi/users 35 | 36 |
37 | GitHub: 38 | https://github.com/AlexStack/NodeJs-Express-MongoDB-Universal-CRUD-API 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /reactAdmin/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/middlewares/checkAuth.js: -------------------------------------------------------------------------------- 1 | // const API_CONFIG = require("../config/api.config"); 2 | const jwt = require('jsonwebtoken'); 3 | const helper = require("../helper/commonHelper"); 4 | 5 | const API_CONFIG = helper.getApiConfig(); 6 | 7 | module.exports = async (req, res, next) => { 8 | const apiRoute = req._parsedUrl.pathname.replace('/' + API_CONFIG.API_BASE, ''); 9 | const apiSchema = API_CONFIG.API_SCHEMAS.find(item => item.apiRoute == apiRoute.split('/')[0]); 10 | 11 | let shouldPassAuth = false; 12 | if (API_CONFIG.ENABLE_AUTH) { 13 | if (apiRoute.indexOf('/getUserToken') != -1 || apiRoute.indexOf('/userLogin') != -1 || apiRoute.indexOf('/userRegister') != -1) { 14 | shouldPassAuth = true; 15 | } 16 | // by default all GET request no need check auth 17 | if (req.method == 'GET') { 18 | shouldPassAuth = true; 19 | if (apiSchema && apiSchema.readRules && apiSchema.readRules.checkAuth) { 20 | // if checkAuth = true in api schema 21 | shouldPassAuth = false; 22 | } 23 | } 24 | // pass auth if writeRules.ignoreCreateAuth & no req.body.userId 25 | // allow anonymous create/add item 26 | // require login if there is a req.body.userId for security reason 27 | if (req.method == 'POST' && apiSchema && apiSchema.writeRules && apiSchema.writeRules.ignoreCreateAuth && !req.body[API_CONFIG.FIELD_USER_ID]) { 28 | shouldPassAuth = true; 29 | } 30 | 31 | // pass auth if all req.body are SelfUpdateFields 32 | if (req.method == 'PUT' && helper.hasAllSelfUpdateFields(apiSchema, 'allSelfUpdateFieldsOnly', req, res)) { 33 | shouldPassAuth = true; 34 | } 35 | 36 | console.log('checkAuth apiRoute', apiRoute, req.method, process.cwd()); 37 | 38 | } else { 39 | shouldPassAuth = true; 40 | } 41 | 42 | 43 | const { authorization } = req.headers; 44 | if (!shouldPassAuth && !authorization) { 45 | return res.status(401).send({ 46 | error: "Please login first!" 47 | }) 48 | } 49 | 50 | if (authorization && authorization.indexOf('Bearer ') != -1 && authorization.length > 100) { 51 | // set req.currentUser even shouldPassAuth = true 52 | jwt.verify( 53 | authorization.replace('Bearer ', ''), 54 | API_CONFIG.JWT_SECRET, 55 | async (err, currentUser) => { 56 | if (err) { 57 | if (shouldPassAuth) { 58 | console.log('=====checkAuth err', err); 59 | } else { 60 | return res.status(401).send({ 61 | error: "Please login first! Wrong token" 62 | }) 63 | } 64 | } else { 65 | console.log('=====checkAuth currentUser', currentUser.id, currentUser.firstName, currentUser.role); 66 | } 67 | req.currentUser = currentUser; 68 | next(); 69 | }) 70 | } else { 71 | next(); 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /reactAdmin/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /reactAdmin/src/pet.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { List, Edit, Create, Datagrid, TextField, ReferenceField, EditButton, SimpleForm, TextInput, NumberInput, DateInput, RadioButtonGroupInput, SelectInput, SelectField, ReferenceInput, Filter } from 'react-admin'; 3 | 4 | const PetSpecies = [ 5 | { id: 'Dog', name: 'Dog' }, 6 | { id: 'Cat', name: 'Cat' }, 7 | { id: 'Guinea Pig', name: 'Guinea Pig' }, 8 | { id: 'Rabbit', name: 'Rabbit' }, 9 | { id: 'Chicken', name: 'Chicken' }, 10 | { id: 'Duck', name: 'Duck' }, 11 | { id: 'Bird', name: 'Bird' }, 12 | { id: 'Horse', name: 'Horse' }, 13 | ]; 14 | 15 | const PetGenders = [ 16 | { id: 'Male', name: 'Male' }, 17 | { id: 'Female', name: 'Female' }, 18 | { id: 'Unknown', name: 'Unknown' }, 19 | ]; 20 | 21 | const SearchFilter = (props) => ( 22 | 23 | 24 | {/* */} 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | 33 | export const PetList = props => ( 34 | } {...props}> 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | 49 | 50 | export const PetCreate = props => ( 51 | 52 | 53 | 54 | 55 | 56 | {/* */} 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ); 67 | 68 | 69 | const EditTitle = ({ record }) => { 70 | return Edit {record ? `"${record.name}"` : ''}; 71 | }; 72 | 73 | export const PetEdit = props => ( 74 | } {...props}> 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | ); 90 | -------------------------------------------------------------------------------- /reactAdmin/src/species.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { List, Edit, Create, Datagrid, TextField, ReferenceField, EditButton, SimpleForm, TextInput, NumberInput, DateInput, RadioButtonGroupInput, SelectInput, SelectField, ReferenceInput, Filter } from 'react-admin'; 3 | 4 | const SpeciesSpecies = [ 5 | { id: 'Dog', name: 'Dog' }, 6 | { id: 'Cat', name: 'Cat' }, 7 | { id: 'Guinea Pig', name: 'Guinea Pig' }, 8 | { id: 'Rabbit', name: 'Rabbit' }, 9 | { id: 'Chicken', name: 'Chicken' }, 10 | { id: 'Duck', name: 'Duck' }, 11 | { id: 'Bird', name: 'Bird' }, 12 | { id: 'Horse', name: 'Horse' }, 13 | ]; 14 | 15 | const SpeciesGenders = [ 16 | { id: 'Male', name: 'Male' }, 17 | { id: 'Female', name: 'Female' }, 18 | { id: 'Unknown', name: 'Unknown' }, 19 | ]; 20 | 21 | const SearchFilter = (props) => ( 22 | 23 | 24 | {/* */} 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | 33 | export const SpeciesList = props => ( 34 | } {...props}> 35 | 36 | 37 | {/* */} 38 | {/* */} 39 | {/* */} 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | 47 | 48 | export const SpeciesCreate = props => ( 49 | 50 | 51 | 52 | {/* 53 | */} 54 | {/* */} 55 | {/* */} 56 | 57 | 58 | 59 | 60 | {/* 61 | 62 | */} 63 | 64 | 65 | 66 | ); 67 | 68 | 69 | const EditTitle = ({ record }) => { 70 | return Edit {record ? `"${record.name}"` : ''}; 71 | }; 72 | 73 | export const SpeciesEdit = props => ( 74 | } {...props}> 75 | 76 | 77 | {/* */} 78 | {/* */} 79 | {/* */} 80 | 81 | {/* */} 82 | 83 | 84 | 85 | {/* 86 | 87 | */} 88 | 89 | 90 | 91 | ); 92 | -------------------------------------------------------------------------------- /app/express/universal.app.js: -------------------------------------------------------------------------------- 1 | const serverless = require("serverless-http"); 2 | const express = require("express"); 3 | const bodyParser = require("body-parser"); 4 | const cors = require("cors"); 5 | const resource = require("../routes/express-resource-alex"); 6 | const app = express(); 7 | const db = require("../models"); 8 | const helper = require("../helper/commonHelper") 9 | const checkAuth = require("../middlewares/checkAuth"); 10 | 11 | // // read config file from the app 12 | // const configFilePaths = ['/config/meanapi.config.js', '/config/api.config.js', '/meanapi.config.js', '/api.config.js']; 13 | // let apiConfigFile = '../config/api.config.js'; 14 | // const appRootDir = process.cwd(); 15 | // for (i = 0; i < configFilePaths.length; i++) { 16 | // if (fs.existsSync(appRootDir + configFilePaths[i])) { 17 | // apiConfigFile = appRootDir + configFilePaths[i]; 18 | // break; 19 | // } 20 | // } 21 | // console.log('apiConfigFile=' + apiConfigFile); 22 | // const API_CONFIG = require(apiConfigFile); 23 | 24 | const API_CONFIG = helper.getApiConfig(); 25 | 26 | if (process.env.DEBUG == 'yes') { 27 | console.log('API_CONFIG:', API_CONFIG); 28 | } 29 | 30 | let corsOptions = { 31 | origin: API_CONFIG.CORS_ORIGIN, 32 | }; 33 | 34 | if (typeof API_CONFIG.CORS_ORIGIN == 'string' && API_CONFIG.CORS_ORIGIN.startsWith('[')) { 35 | const corsStr = API_CONFIG.CORS_ORIGIN.replace('[', '').replace(']', '').replace(/ /g, ''); 36 | const corsAry = corsStr.split(','); 37 | corsOptions.origin = corsAry; 38 | } 39 | 40 | console.log(corsOptions); 41 | 42 | app.use(cors(corsOptions)); 43 | 44 | // parse requests of content-type - application/json 45 | app.use(bodyParser.json()); 46 | 47 | // parse requests of content-type - application/x-www-form-urlencoded 48 | app.use(bodyParser.urlencoded({ extended: true })); 49 | 50 | // auto middleware 51 | app.use(checkAuth); 52 | 53 | // simple route for api index page 54 | app.get("/" + API_CONFIG.API_BASE, (req, res) => { 55 | res.json({ message: "Welcome to Alex' Universal API(Node Express Mongodb) application" }); 56 | }); 57 | 58 | 59 | db.mongoose 60 | .connect(API_CONFIG.DB, { 61 | useNewUrlParser: true, 62 | useUnifiedTopology: true, 63 | }) 64 | .then(() => { 65 | console.log("Connected to the database! API is ready!"); 66 | }) 67 | .catch((err) => { 68 | console.log("Cannot connect to the database with DB URI: " + API_CONFIG.DB, err); 69 | process.exit(); 70 | }); 71 | 72 | 73 | /** 74 | * Generate RESTful API routes for each schema 75 | * Controller methods and routes examples: 76 | * index() => GET / 77 | * create() => GET /create 78 | * store() => POST / 79 | * show() => GET /:id 80 | * edit() => GET /edit/:id 81 | * update() => PUT /:id 82 | * patch() => PATCH /:id 83 | * destroy() => DEL /:id 84 | */ 85 | let universal = null; 86 | universal = require("../controllers/universal.controller"); 87 | 88 | // user auth routes 89 | if (API_CONFIG.ENABLE_AUTH && API_CONFIG.USER_ROUTE) { 90 | app.post("/" + API_CONFIG.API_BASE + API_CONFIG.USER_ROUTE + "/getUserToken", (req, res, next) => { 91 | universal.getUserToken(req, res, next); 92 | }); 93 | 94 | } 95 | 96 | 97 | 98 | // other universal routes 99 | API_CONFIG.API_SCHEMAS.forEach(apiSchema => { 100 | // universal = require("../controllers/universal.controller"); 101 | app.resource(API_CONFIG.API_BASE + apiSchema.apiRoute, universal); 102 | // app.get("/" + API_CONFIG.API_BASE + apiSchema.apiRoute + "/search/:keyword", (req, res, next) => { 103 | // universal.search(req, res); 104 | // }); 105 | }); 106 | 107 | 108 | 109 | app.set('config', API_CONFIG); 110 | 111 | module.exports.db = db; 112 | module.exports.app = app; 113 | module.exports.serverless = serverless; 114 | module.exports.API_CONFIG = API_CONFIG; 115 | module.exports.helper = helper; 116 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Serverless! 2 | # 3 | # This file is the main config file for your service. 4 | # It's very minimal at this point and uses default values. 5 | # You can always add more config options for more control. 6 | # We've included some commented out config examples here. 7 | # Just uncomment any of them to get that config option. 8 | # 9 | # For full config options, check the docs: 10 | # docs.serverless.com 11 | # 12 | # Happy Coding! 13 | 14 | service: Express-MongoDB-Universal-API 15 | # app and org for use with dashboard.serverless.com 16 | #app: your-app-name 17 | #org: your-org-name 18 | 19 | # You can pin your service to only deploy with a specific Serverless version 20 | # Check out our docs for more details 21 | frameworkVersion: "2" 22 | 23 | provider: 24 | name: aws 25 | runtime: nodejs12.x 26 | stage: dev 27 | region: ap-southeast-2 28 | environment: 29 | DB: "mongodb-uri" 30 | # you can add statements to the Lambda function's IAM Role here 31 | # iamRoleStatements: 32 | # - Effect: "Allow" 33 | # Action: 34 | # - "s3:ListBucket" 35 | # Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ] } 36 | # - Effect: "Allow" 37 | # Action: 38 | # - "s3:PutObject" 39 | # Resource: 40 | # Fn::Join: 41 | # - "" 42 | # - - "arn:aws:s3:::" 43 | # - "Ref" : "ServerlessDeploymentBucket" 44 | # - "/*" 45 | 46 | # you can define service wide environment variables here 47 | # environment: 48 | # variable1: value1 49 | 50 | # you can add packaging information here 51 | package: 52 | exclude: 53 | - "node_modules/**" 54 | - "node_lambda_layer/**" 55 | - ".git/**" 56 | - "vendor/**" 57 | - ".serverless" 58 | - ".env" 59 | - "reactAdmin/**" 60 | 61 | functions: 62 | universalAPI: 63 | handler: awsLambda.lambdaHandler 64 | memorySize: 128 65 | description: Express-MongoDB-Universal-API 66 | events: 67 | - http: "ANY /" 68 | - http: "ANY /{proxy+}" 69 | # hello: 70 | # handler: handler.hello 71 | # The following are a few example events you can configure 72 | # NOTE: Please make sure to change your handler code to work with those events 73 | # Check the event documentation for details 74 | # events: 75 | # - http: 76 | # path: users/create 77 | # method: get 78 | # - websocket: $connect 79 | # - s3: ${env:BUCKET} 80 | # - schedule: rate(10 minutes) 81 | # - sns: greeter-topic 82 | # - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000 83 | # - alexaSkill: amzn1.ask.skill.xx-xx-xx-xx 84 | # - alexaSmartHome: amzn1.ask.skill.xx-xx-xx-xx 85 | # - iot: 86 | # sql: "SELECT * FROM 'some_topic'" 87 | # - cloudwatchEvent: 88 | # event: 89 | # source: 90 | # - "aws.ec2" 91 | # detail-type: 92 | # - "EC2 Instance State-change Notification" 93 | # detail: 94 | # state: 95 | # - pending 96 | # - cloudwatchLog: '/aws/lambda/hello' 97 | # - cognitoUserPool: 98 | # pool: MyUserPool 99 | # trigger: PreSignUp 100 | # - alb: 101 | # listenerArn: arn:aws:elasticloadbalancing:us-east-1:XXXXXX:listener/app/my-load-balancer/50dc6c495c0c9188/ 102 | # priority: 1 103 | # conditions: 104 | # host: example.com 105 | # path: /hello 106 | 107 | # Define function environment variables here 108 | # environment: 109 | # variable2: value2 110 | 111 | # you can add CloudFormation resource templates here 112 | #resources: 113 | # Resources: 114 | # NewResource: 115 | # Type: AWS::S3::Bucket 116 | # Properties: 117 | # BucketName: my-new-bucket 118 | # Outputs: 119 | # NewOutput: 120 | # Description: "Description for the output" 121 | # Value: "Some output value" 122 | -------------------------------------------------------------------------------- /reactAdmin/src/story.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { List, Edit, Create, Datagrid, TextField, ReferenceField, EditButton, SimpleForm, TextInput, NumberInput, DateInput, RadioButtonGroupInput, SelectInput, SelectField, ReferenceInput, BooleanInput, useDataProvider } from 'react-admin'; 3 | 4 | const PublicOrPrivate = [ 5 | { id: true, name: 'Public' }, 6 | { id: false, name: 'Private' } 7 | ]; 8 | export const StoryList = props => ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | 25 | 26 | export const StoryCreate = props => { 27 | const dataProvider = useDataProvider(); 28 | const transform = async data => { 29 | let pet; 30 | await dataProvider 31 | .getOne('pets', { id: data.petId }) 32 | .then(response => { 33 | pet = response.data; 34 | }); 35 | // console.log('transform-data:', data, pet); 36 | return ({ 37 | ...data, 38 | userId: `${pet.userId}` 39 | }) 40 | }; 41 | 42 | return ( 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {/* 53 | 54 | */} 55 | 56 | 57 | 58 | ) 59 | }; 60 | 61 | 62 | const EditTitle = ({ record }) => { 63 | return Edit {record ? `"${record.title}"` : ''}; 64 | }; 65 | 66 | 67 | export const StoryEdit = (props) => { 68 | const dataProvider = useDataProvider(); 69 | const transform = async data => { 70 | let pet; 71 | await dataProvider 72 | .getOne('pets', { id: data.petId }) 73 | .then(response => { 74 | pet = response.data; 75 | }); 76 | // console.log('transform-data:', data, pet); 77 | return ({ 78 | ...data, 79 | userId: `${pet.userId}` 80 | }) 81 | }; 82 | 83 | return ( 84 | } {...props} transform={transform}> 85 | 86 | 87 | {/* */} 88 | 89 | 90 | 91 | 92 | 93 | 94 | {/* 95 | 96 | */} 97 | 98 | 99 | 100 | ) 101 | }; 102 | -------------------------------------------------------------------------------- /reactAdmin/src/comment.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { List, Edit, Create, Datagrid, TextField, ReferenceField, EditButton, SimpleForm, TextInput, NumberInput, DateInput, RadioButtonGroupInput, SelectInput, SelectField, ReferenceInput, BooleanInput, useDataProvider } from 'react-admin'; 3 | 4 | const PublicOrPrivate = [ 5 | { id: true, name: 'Public' }, 6 | { id: false, name: 'Private' } 7 | ]; 8 | export const CommentList = props => ( 9 | 10 | 11 | 12 | {/* */} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | 29 | 30 | export const CommentCreate = props => { 31 | const dataProvider = useDataProvider(); 32 | const transform = async data => { 33 | let pet; 34 | await dataProvider 35 | .getOne('comments', { id: data.petId }) 36 | .then(response => { 37 | pet = response.data; 38 | }); 39 | // console.log('transform-data:', data, pet); 40 | return ({ 41 | ...data, 42 | userId: `${pet.userId}` 43 | }) 44 | }; 45 | 46 | return ( 47 | 48 | 49 | 50 | 51 | {/* */} 52 | 53 | 54 | 55 | 56 | 57 | {/* 58 | 59 | */} 60 | 61 | 62 | 63 | ) 64 | }; 65 | 66 | 67 | const EditTitle = ({ record }) => { 68 | return Edit {record ? `"${record.title}"` : ''}; 69 | }; 70 | 71 | 72 | export const CommentEdit = (props) => { 73 | const dataProvider = useDataProvider(); 74 | const transform = async data => { 75 | let pet; 76 | await dataProvider 77 | .getOne('pets', { id: data.petId }) 78 | .then(response => { 79 | pet = response.data; 80 | }); 81 | // console.log('transform-data:', data, pet); 82 | return ({ 83 | ...data, 84 | userId: `${pet.userId}` 85 | }) 86 | }; 87 | 88 | return ( 89 | } {...props} > 90 | 91 | 92 | {/* */} 93 | {/* */} 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | {/* 104 | 105 | */} 106 | 107 | 108 | 109 | ) 110 | }; 111 | -------------------------------------------------------------------------------- /app/config/api.config-BAK.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const isServerless = !!(process.env.LAMBDA_TASK_ROOT || process.env.AWS_LAMBDA_FUNCTION_NAME); 3 | 4 | // try to load .env if not serverless 5 | const envFile = __dirname + '/../../.env'; 6 | if (!isServerless && fs.existsSync(envFile)) { 7 | const envResult = require('dotenv').config({ path: envFile }); 8 | if (envResult.error) { 9 | throw envResult.error; 10 | } else if (process.env.DEBUG == 'yes') { 11 | console.log('envResult:', envResult.parsed); 12 | } 13 | } 14 | 15 | module.exports.DB = process.env.DB || 'please-set-database-connect-uri-first'; 16 | module.exports.API_BASE = process.env.API_BASE || 'api/'; 17 | module.exports.PORT = process.env.PORT || '8080'; 18 | module.exports.CORS_ORIGIN = process.env.CORS_ORIGIN || [/localhost/, /\.test$/]; 19 | module.exports.IS_SERVERLESS = isServerless; 20 | 21 | 22 | /** 23 | * 24 | * MongoDB collection name: plural camelCase, e.g. pets, stories, users, user_story, pet_story 25 | * MongoDB field/column name: singular camelCase (keep same as javascript variable convention) 26 | * 27 | * API Route: plural camelCase 28 | * Use plural route name convention instead of singular 29 | * Note: controller file name still use singular 30 | * Ref: https://github.com/alexeymezenin/laravel-best-practices#follow-laravel-naming-conventions 31 | * Ref: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md 32 | * Ref: https://www.php-fig.org/psr/psr-2/ 33 | * Ref: https://www.restapitutorial.com/lessons/restfulresourcenaming.html 34 | * 35 | */ 36 | 37 | 38 | module.exports.API_SCHEMAS = [ 39 | { 40 | "apiRoute": "server4", 41 | "collectionName": "table4", 42 | "schema": { 43 | name: String, 44 | description: String, 45 | type: String, 46 | gender: String, 47 | dob: Date, 48 | userId: { type: Number, required: [true, 'userId is required.'] } 49 | }, 50 | "mongooseOption": { timestamps: true }, 51 | "searchFields": ["name", "description", "type"], 52 | }, 53 | { 54 | "apiRoute": "test4", 55 | "collectionName": "test4", 56 | "schema": { 57 | title: { 58 | type: String, 59 | required: [true, 'Title is required.'], 60 | unique: true, 61 | }, 62 | description: String, 63 | age: { 64 | type: Number, 65 | min: [18, 'Too young'], 66 | max: 80 67 | } 68 | }, 69 | "mongooseOption": { timestamps: true, strict: true }, 70 | "searchFields": ["description", "title"], 71 | }, 72 | { 73 | "apiRoute": "test5", 74 | "collectionName": "test5", 75 | "schema": { 76 | address: String, 77 | description: String 78 | }, 79 | "mongooseOption": { timestamps: false, strict: true }, 80 | "searchFields": ["address"], 81 | }, 82 | { 83 | "apiRoute": "users", 84 | "collectionName": "users", 85 | "schema": { 86 | name: String, 87 | email: String, 88 | password: { 89 | type: String, 90 | required: true, 91 | select: false, 92 | }, 93 | confirmPassword: { 94 | type: String, 95 | select: false, 96 | }, 97 | role: String 98 | }, 99 | "mongooseOption": { timestamps: true, strict: false }, 100 | "searchFields": ["name"], 101 | "selectFields": "-email -firebaseUid", 102 | }, 103 | { 104 | "apiRoute": "pets", 105 | "collectionName": "pets", 106 | "schema": { 107 | name: String, 108 | species: String, 109 | gender: String, 110 | type: String, 111 | detail: String, 112 | priority: Number, 113 | userId: String 114 | }, 115 | "mongooseOption": { timestamps: true, strict: false }, 116 | "searchFields": ["name", "description"], 117 | }, 118 | { 119 | "apiRoute": "stories", 120 | "collectionName": "stories", 121 | "schema": { 122 | title: String, 123 | content: String, 124 | tags: String, 125 | public: Boolean, 126 | priority: Number, 127 | petId: String, 128 | userId: String 129 | }, 130 | "mongooseOption": { timestamps: true, strict: false }, 131 | "searchFields": ["title", "content", "tags"], 132 | }, 133 | { 134 | "apiRoute": "files", 135 | "collectionName": "files", 136 | "schema": { 137 | title: String, 138 | description: String, 139 | type: String, // image, video, other file type 140 | url: String, 141 | priority: Number, 142 | storyId: String, 143 | petId: String, 144 | userId: String 145 | }, 146 | "mongooseOption": { timestamps: true, strict: false }, 147 | "searchFields": ["title", "description"], 148 | } 149 | ]; 150 | 151 | -------------------------------------------------------------------------------- /app/routes/express-resource-alex.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Express - Resource 3 | * Copyright(c) 2010-2012 TJ Holowaychuk 4 | * Copyright(c) 2011 Daniel Gasienica 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Alex changes: 10 | * 1. Fixing deprecated app.del: Use app.delete 11 | * 2. change 'new' to 'create', 'create' to 'store' 12 | * 3. remove singularize for route parameter 13 | */ 14 | 15 | /** 16 | * Module dependencies. 17 | */ 18 | 19 | var express = require("express"), 20 | methods = require("methods"), 21 | // debug = require("debug")("express-resource"), 22 | // lingo = require("lingo"), 23 | app = express.application; 24 | // en = lingo.en; 25 | 26 | /** 27 | * Pre-defined action ordering. 28 | */ 29 | 30 | var orderedActions = [ 31 | "index", // GET / 32 | "create", // GET /create 33 | "store", // POST / 34 | "show", // GET /:id 35 | "edit", // GET /edit/:id 36 | "update", // PUT /:id 37 | "patch", // PATCH /:id 38 | "destroy", // DEL /:id 39 | ]; 40 | 41 | /** 42 | * Expose `Resource`. 43 | */ 44 | 45 | module.exports = Resource; 46 | 47 | /** 48 | * Initialize a new `Resource` with the given `name` and `actions`. 49 | * 50 | * @param {String} name 51 | * @param {Object} actions 52 | * @param {Server} app 53 | * @api private 54 | */ 55 | 56 | function Resource(name, actions, app) { 57 | this.name = name; 58 | this.app = app; 59 | this.routes = {}; 60 | actions = actions || {}; 61 | this.base = actions.base || "/"; 62 | if ("/" != this.base[this.base.length - 1]) this.base += "/"; 63 | this.format = actions.format; 64 | this.id = actions.id || this.defaultId; 65 | this.param = ":" + this.id; 66 | 67 | // default actions 68 | for (var i = 0, key; i < orderedActions.length; ++i) { 69 | key = orderedActions[i]; 70 | if (actions[key]) this.mapDefaultAction(key, actions[key]); 71 | } 72 | 73 | // auto-loader 74 | if (actions.load) this.load(actions.load); 75 | } 76 | 77 | /** 78 | * Set the auto-load `fn`. 79 | * 80 | * @param {Function} fn 81 | * @return {Resource} for chaining 82 | * @api public 83 | */ 84 | 85 | Resource.prototype.load = function (fn) { 86 | var self = this, 87 | id = this.id; 88 | 89 | this.loadFunction = fn; 90 | this.app.param(this.id, function (req, res, next) { 91 | function callback(err, obj) { 92 | if (err) return next(err); 93 | // TODO: ideally we should next() passed the 94 | // route handler 95 | if (null == obj) return res.send(404); 96 | req[id] = obj; 97 | next(); 98 | } 99 | 100 | // Maintain backward compatibility 101 | if (2 == fn.length) { 102 | fn(req.params[id], callback); 103 | } else { 104 | fn(req, req.params[id], callback); 105 | } 106 | }); 107 | 108 | return this; 109 | }; 110 | 111 | /** 112 | * Retun this resource's default id string. 113 | * 114 | * @return {String} 115 | * @api private 116 | */ 117 | 118 | Resource.prototype.__defineGetter__("defaultId", function () { 119 | // return this.name ? en.singularize(this.name.split("/").pop()) : "id"; 120 | return this.name ? this.name.split("/").pop() : "id"; 121 | }); 122 | 123 | /** 124 | * Map http `method` and optional `path` to `fn`. 125 | * 126 | * @param {String} method 127 | * @param {String|Function|Object} path 128 | * @param {Function} fn 129 | * @return {Resource} for chaining 130 | * @api public 131 | */ 132 | 133 | Resource.prototype.map = function (method, path, fn) { 134 | var self = this, 135 | orig = path; 136 | 137 | if (method instanceof Resource) return this.add(method); 138 | if ("function" == typeof path) (fn = path), (path = ""); 139 | if ("object" == typeof path) (fn = path), (path = ""); 140 | if ("/" == path[0]) path = path.substr(1); 141 | else path = path ? this.param + "/" + path : this.param; 142 | method = method.toLowerCase(); 143 | 144 | // setup route pathname 145 | var route = this.base + (this.name || ""); 146 | if (this.name && path) route += "/"; 147 | route += path; 148 | route += ".:format?"; 149 | 150 | // register the route so we may later remove it 151 | (this.routes[method] = this.routes[method] || {})[route] = { 152 | method: method, 153 | path: route, 154 | orig: orig, 155 | fn: fn, 156 | }; 157 | 158 | // apply the route 159 | this.app[method](route, function (req, res, next) { 160 | req.format = req.params.format || req.format || self.format; 161 | if (req.format) res.type(req.format); 162 | if ("object" == typeof fn) { 163 | if (fn[req.format]) { 164 | fn[req.format](req, res, next); 165 | } else { 166 | res.format(fn); 167 | } 168 | } else { 169 | fn(req, res, next); 170 | } 171 | }); 172 | 173 | return this; 174 | }; 175 | 176 | /** 177 | * Nest the given `resource`. 178 | * 179 | * @param {Resource} resource 180 | * @return {Resource} for chaining 181 | * @see Resource#map() 182 | * @api public 183 | */ 184 | 185 | Resource.prototype.add = function (resource) { 186 | var app = this.app, 187 | routes, 188 | route; 189 | 190 | // relative base 191 | resource.base = 192 | this.base + (this.name ? this.name + "/" : "") + this.param + "/"; 193 | 194 | // re-define previous actions 195 | for (var method in resource.routes) { 196 | routes = resource.routes[method]; 197 | for (var key in routes) { 198 | route = routes[key]; 199 | delete routes[key]; 200 | if (method == "del") method = "delete"; 201 | app.routes[method].forEach(function (route, i) { 202 | if (route.path == key) { 203 | app.routes[method].splice(i, 1); 204 | } 205 | }); 206 | resource.map(route.method, route.orig, route.fn); 207 | } 208 | } 209 | 210 | return this; 211 | }; 212 | 213 | /** 214 | * Map the given action `name` with a callback `fn()`. 215 | * 216 | * @param {String} key 217 | * @param {Function} fn 218 | * @api private 219 | */ 220 | 221 | Resource.prototype.mapDefaultAction = function (key, fn) { 222 | switch (key) { 223 | case "index": 224 | this.get("/", fn); 225 | break; 226 | case "create": 227 | this.get("/create", fn); 228 | break; 229 | case "store": 230 | this.post("/", fn); 231 | break; 232 | case "show": 233 | this.get(fn); 234 | break; 235 | case "edit": 236 | this.get("edit", fn); 237 | break; 238 | case "update": 239 | this.put(fn); 240 | break; 241 | case "patch": 242 | this.patch(fn); 243 | break; 244 | case "destroy": 245 | this.delete(fn); 246 | break; 247 | } 248 | }; 249 | 250 | /** 251 | * Setup http verb methods. 252 | */ 253 | 254 | methods.concat(["del", "all"]).forEach(function (method) { 255 | Resource.prototype[method] = function (path, fn) { 256 | if ("function" == typeof path || "object" == typeof path) 257 | (fn = path), (path = ""); 258 | this.map(method, path, fn); 259 | return this; 260 | }; 261 | }); 262 | 263 | /** 264 | * Define a resource with the given `name` and `actions`. 265 | * 266 | * @param {String|Object} name or actions 267 | * @param {Object} actions 268 | * @return {Resource} 269 | * @api public 270 | */ 271 | 272 | app.resource = function (name, actions, opts) { 273 | var options = actions || {}; 274 | if ("object" == typeof name) (actions = name), (name = null); 275 | if (options.id) actions.id = options.id; 276 | this.resources = this.resources || {}; 277 | if (!actions) return this.resources[name] || new Resource(name, null, this); 278 | for (var key in opts) options[key] = opts[key]; 279 | var res = (this.resources[name] = new Resource(name, actions, this)); 280 | return res; 281 | }; 282 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Universal MEAN API 2 | 3 | - This package allow you to set up a backend API using node.js & mongodb in 2 minutes 4 | - An Universal RESTful CRUD API for dynamic multiple collections of mongodb (database tables) with Node.js and Express.js, we DO NOT need to code the CRUD route/controller for each collection/table anymore. 5 | - Designed to run on Serverless environment, such as AWS Lambda, Azure, Google, NetLify, Vercel 6 | - Universal MEAN API stands for MongoDB(M) + Express.js(E) + Universal CRUD API(A) + Node.js(N) 7 | - All schemas and api routes can define in ONE file: api.config.js (No need to create mongodb collection beforehand) 8 | - Come with a react-admin demo 9 | - Support field search(=, %like%, full-text) and some json-sever standard parameters: \_sort, \_order, \_start, \_end, \_limit, \_like, \_gte, \_lte, id=1,2,3,4,5 10 | 11 | ## How to install 12 | 13 | - npm install universal-mean-api 14 | 15 | ## AWS Lambda example 16 | 17 | - Demo: https://k2qt3w8a07.execute-api.ap-southeast-2.amazonaws.com/dev/api/posts 18 | - [AWS Lambda Code example is here](https://github.com/AlexStack/NodeJs-Express-MongoDB-Universal-CRUD-API/tree/master/example/awsLambda) 19 | - Change the serverless.yml before deploy 20 | - You need remove - "node_modules/\*\*" from the exclude in serverless.yml if you want to deploy node_modules together 21 | - You can upload a nodejs layer instead of deploy node_modules every time 22 | - create_lambda_layer.sh can be used to create a node_modules layer 23 | - Change config/meanapi.config.js 24 | - Add DB environment via aws lambda UI 25 | - Done 26 | 27 | ## Serverless function example for NetLify 28 | 29 | - Demo: https://meanapi.netlify.app/ 30 | - [NetLify serverless function Code example is here](https://github.com/AlexStack/NodeJs-Express-MongoDB-Universal-CRUD-API/tree/master/example/netlify) 31 | - Change netlify/functions/meanapi/meanapi.config.js 32 | - Change the netlify.toml if need 33 | - According to some research, the Netlify Functions cannot access the Env Variables from netlify.toml. So, Please set up below environment variables from the Netlify website UI manually: 34 | DB = "YOUR-MONGODB-URI" 35 | API_BASE = ".netlify/functions/.netlify/functions/meanapi/" 36 | API_CONFIG_FILE = "/var/task/src/functions/meanapi/meanapi.config.js" 37 | 38 | ## Serverless function example for Vercel 39 | 40 | - Demo: https://meanapi.vercel.app/ 41 | - [Vercel function Code example is here](https://github.com/AlexStack/NodeJs-Express-MongoDB-Universal-CRUD-API/tree/master/example/vercel) 42 | - Change config/meanapi.config.js 43 | - Change the vercel.json if need 44 | 45 | ## How to use locally 46 | 47 | - You only need create 2 js files after install: server.js & meanapi.config.js 48 | - Then run command: node server.js to see the result 49 | - Done! 50 | - The file structure example: 51 | 52 | ```JavaScript 53 | /node_modules 54 | /package-lock.json 55 | /server.js 56 | /meanapi.config.js or /config/meanapi.config.js or /config/api.config.js 57 | ``` 58 | 59 | - Basic example code for server.js: 60 | 61 | ```JavaScript 62 | const { app, serverless, API_CONFIG } = require("universal-mean-api"); 63 | if (API_CONFIG.IS_SERVERLESS) { 64 | module.exports.lambdaHandler = serverless(app); // handler for serverless function 65 | } else { 66 | app.listen(app.get('config').PORT, () => { 67 | console.log(`API is running on port ${API_CONFIG.PORT}.`); 68 | }); 69 | } 70 | ``` 71 | 72 | - Basic example of meanapi.config.js (API variables & MongoDB schemas & routes) 73 | 74 | ```JavaScript 75 | module.exports.DB = process.env.DB || 'please-set-database-connect-uri-first'; 76 | module.exports.API_BASE = process.env.API_BASE || 'api/'; 77 | module.exports.PORT = process.env.PORT || '8080'; 78 | module.exports.CORS_ORIGIN = process.env.CORS_ORIGIN || [/localhost/, /\.test$/]; 79 | module.exports.IS_SERVERLESS = !!(process.env.LAMBDA_TASK_ROOT || process.env.AWS_LAMBDA_FUNCTION_NAME); 80 | 81 | module.exports.API_SCHEMAS = [ 82 | { 83 | "apiRoute": "test1", 84 | "collectionName": "test1", 85 | "schema": { 86 | name: String, 87 | description: String, 88 | type: String, 89 | gender: String, 90 | dob: Date, 91 | userId: { type: Number, required: [true, 'userId is required.'] } 92 | }, 93 | "mongooseOption": { timestamps: true, strict: false }, 94 | "searchFields": ["name", "description", "type"], 95 | }, 96 | { 97 | "apiRoute": "test2", 98 | "collectionName": "test2", 99 | "schema": { 100 | title: { 101 | type: String, 102 | required: [true, 'Title is required.'] 103 | }, 104 | description: String, 105 | age: { 106 | type: Number, 107 | min: [18, 'Too young'], 108 | max: 80 109 | } 110 | }, 111 | "mongooseOption": { timestamps: true, strict: true }, 112 | "searchFields": ["description", "title"], 113 | }, 114 | { 115 | "apiRoute": "test3", 116 | "collectionName": "test3", 117 | "schema": { 118 | address: String, 119 | description: String 120 | }, 121 | "mongooseOption": { timestamps: false, strict: false }, 122 | "searchFields": ["address"], 123 | } 124 | ]; 125 | 126 | 127 | ``` 128 | 129 | ## How to access the RESTful API 130 | 131 | - Based on above example server.js & meanapi.config.js files, we can access the localhost:8080 by run command node server.js or nodemon server.js 132 | - RESTful API endpoint for collection < tablename >: http://localhost:8080/api/< tablename > 133 | - RESTful API endpoint for collection test1: http://localhost:8080/api/test1 134 | - RESTful API endpoint for collection test2: http://localhost:8080/api/test2 135 | - RESTful API endpoint for collection test3: http://localhost:8080/api/test3 136 | - RESTful API endpoint support GET/POST/PUT/DELETE and fields search 137 | 138 | ## RESTful API CRUD routes example 139 | 140 | ```JavaScript 141 | GET http://localhost:8080/api/test1 142 | GET http://localhost:8080/api/test1/ 143 | POST http://localhost:8080/api/test1 144 | PUT http://localhost:8080/api/test1/ 145 | PATCH http://localhost:8080/api/test1/ 146 | DELETE http://localhost:8080/api/test1/ 147 | ``` 148 | 149 | ## Field search route examples 150 | 151 | - GET /api/test1?name_like=mean&description_like=api 152 | - GET /api/test1?age_lgt=18&\_limit=20 153 | - GET /api/test1?id=1&id=2&id=3 154 | - GET /api/test1?id=1,2,3,4 or ?\_id=1,2,3,4 155 | 156 | ## Sort & order by examples 157 | 158 | - GET /api/test1?\_sort=age&\_order=dasc&\_start=10&\_end=20&name_like=api 159 | 160 | ## Get data from relationship collections or tables 161 | 162 | - To include children resources, add \_embed 163 | - To include parent resource, add \_expand 164 | - e.g. /pets/5fcd8f4a3b755f0008556057?\_expand=user,file|mainImageId&\_embed=pets,stories 165 | - \_expand=user,file|mainImageId means get parent data from parent table users and files. it will use userId and fileId as the foreign key by default unless you set it by table|foreignKey. e.g. file|mainImageId, file is the singular table name and mainImageId is the foreignKey 166 | - Only support the detail route at this moment, will add to the list route 167 | 168 | ## User auth check system (check if the user is the owner or admin via JWT access token) 169 | 170 | - ENABLE_AUTH: enable user auth check system or not 171 | - ENABLE_AUTH=true, all edit/delete/add(update/destroy/create) requests has to be the owner or admin 172 | - ENABLE_AUTH=false, test & debug mode, everyone can view/edit/delete 173 | - e.g. module.exports.ENABLE_AUTH = true; 174 | 175 | - USER_ROUTE: user table api endpoint route 176 | - e.g. module.exports.USER_ROUTE = 'users'; 177 | 178 | - FIELD_USER_ID: the foreign key id in other tables to user id 179 | - e.g. module.exports.FIELD_USER_ID = 'userId'; 180 | 181 | - by default all GET request no need check user auth 182 | - if we want enable auth check for a table/collection list/show GET request 183 | - set readRules for each table/collection like below 184 | - "readRules": { "checkAuth": true, "checkOwner": true } 185 | - e.g. CRUD for orders table/collection, need to login first to do anything, only the owner itself can view/add/edit/delete his/her orders 186 | 187 | - Who is admin? Set role field in users table to admin or xxxAdmin (any role value contains string "admin" will consider is an admin role) 188 | - e.g. role=webAdmin, role=admin, role=SuperAdmin, role=Dashboard Admin 189 | - admin has all permissions for now 190 | 191 | ## How to set admin only add/edit/delete permission 192 | 193 | - add/edit/delete request allow owner itself by default 194 | - if you want set admin only for some system tables, e.g. settings, categories 195 | - Add "writeRules": { "checkAdmin": true } to the schema settings in api.config.js 196 | 197 | ## How to allow create item anonymous 198 | 199 | - Sometimes we want allow create item anonymous. e.g. a contact us form 200 | - Add "writeRules": { "ignoreCreateAuth": true } to the schema settings in api.config.js 201 | - NOTE: It still requires login if there is a \_POST[userId] parameter for security reason 202 | - edit/delete still requires owner or admin 203 | 204 | ## How to set up a private item 205 | 206 | - Sometimes we want create item privately. e.g. something is public while others is private, or private message & public comment 207 | - Add "isPublic: Boolean" to the schema settings in api.config.js 208 | - Set FIELD_PUBLIC = 'isPublic' and FIELD_TARGET_USER_ID = 'ownerId' to fit your needs 209 | - The API will check if there is a isPublic field in the schema first, then check the current user or target user is match or not 210 | - Example 1: a story set isPublic=false, means it is private. only the owner itself can view/edit it. Other users can not event list or view it. 211 | - Example 2: a private message sent to userA from userB, only those two users both can view the message, however, only userB can edit/delete the message. 212 | 213 | ## How to update some fields anonymous 214 | 215 | - selfUpdateFields only allow 1 increase/decrease self update unless use is admin 216 | - "writeRules": { "selfUpdateFields": ["viewNum", "likeNum", "commentNum"] } 217 | - Note: selfUpdateFields will exclude from owner itself update 218 | 219 | ## return data - all JSON format 220 | 221 | - add or update request, returns the NEW item object if success 222 | - list request, returns an array contains all item objects 223 | - show/detail request, returns the item object if success 224 | - delete request, the http status returns 200 if success 225 | 226 | ## Debug locally 227 | 228 | - npm install universal-mean-api 229 | - npx nodemon server.js (or nodemon server.js) 230 | - cd reactAdmin 231 | - npm start 232 | 233 | ## Example of adding a custom route to server.js? 234 | 235 | - Below example is add a custom /api/sitemap route to server.js 236 | - It read data from collection posts and output a text format sitemap for google 237 | 238 | ```javascript 239 | const { app, API_CONFIG, db } = require("universal-mean-api"); 240 | 241 | // custom route start 242 | app.get("/" + API_CONFIG.API_BASE + "sitemap", async (req, res, next) => { 243 | const urls = await db["posts"] 244 | .find() 245 | .sort({ _id: -1 }) 246 | .limit(100) 247 | .then((data) => { 248 | let urls = []; 249 | data.map((item) => { 250 | urls.push("https://your-website.com/posts/" + item.id); 251 | }); 252 | return urls; 253 | }); 254 | res.send(urls.join("\n")); 255 | }); 256 | // custom route end 257 | 258 | app.set("test1111", "/" + API_CONFIG.API_BASE + "sitemap"); 259 | 260 | app.listen(API_CONFIG.PORT, () => { 261 | console.log( 262 | `Server is running on port ${API_CONFIG.PORT}. process.cwd()=` + 263 | process.cwd() 264 | ); 265 | }); 266 | ``` 267 | -------------------------------------------------------------------------------- /app/config/api.config.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const isServerless = !!(process.env.LAMBDA_TASK_ROOT || process.env.AWS_LAMBDA_FUNCTION_NAME); 3 | 4 | // try to load .env if not serverless 5 | const envFile = process.cwd() + '/.env'; 6 | if (!isServerless && fs.existsSync(envFile)) { 7 | const envResult = require('dotenv').config({ path: envFile }); 8 | if (envResult.error) { 9 | throw envResult.error; 10 | } else if (process.env.DEBUG == 'yes') { 11 | console.log('envResult:', envResult.parsed); 12 | } 13 | } 14 | 15 | module.exports.DB = process.env.DB || 'please-set-database-connect-uri-first'; 16 | module.exports.API_BASE = process.env.API_BASE || 'api/'; 17 | module.exports.PORT = process.env.PORT || '8080'; 18 | module.exports.CORS_ORIGIN = process.env.CORS_ORIGIN || [/localhost/, /\.test$/]; 19 | module.exports.IS_SERVERLESS = isServerless; 20 | module.exports.JWT_SECRET = process.env.JWT_SECRET || 'Ki8H1-JWT-U&HJs92dj2.d32slU&dk2+petStory' + this.DB; 21 | 22 | /** 23 | * 24 | * MongoDB collection name: plural camelCase, e.g. pets, stories, users, user_story, pet_story 25 | * MongoDB field/column name: singular camelCase (keep same as javascript variable convention) 26 | * 27 | * API Route: plural camelCase 28 | * Use plural route name convention instead of singular 29 | * Note: controller file name still use singular 30 | * Ref: https://github.com/alexeymezenin/laravel-best-practices#follow-laravel-naming-conventions 31 | * Ref: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md 32 | * Ref: https://www.php-fig.org/psr/psr-2/ 33 | * Ref: https://www.restapitutorial.com/lessons/restfulresourcenaming.html 34 | * 35 | */ 36 | 37 | 38 | module.exports.API_SCHEMAS = [ 39 | { 40 | "apiRoute": "server4", 41 | "collectionName": "table4", 42 | "schema": { 43 | name: String, 44 | description: String, 45 | type: String, 46 | gender: String, 47 | dob: Date, 48 | userId: { type: Number, required: [true, 'userId is required.'] } 49 | }, 50 | "mongooseOption": { timestamps: true }, 51 | "searchFields": ["name", "description", "type"], 52 | }, 53 | { 54 | "apiRoute": "test4", 55 | "collectionName": "test4", 56 | "schema": { 57 | title: { 58 | type: String, 59 | required: [true, 'Title is required.'], 60 | unique: true, 61 | }, 62 | description: String, 63 | age: { 64 | type: Number, 65 | min: [18, 'Too young'], 66 | max: 80 67 | } 68 | }, 69 | "mongooseOption": { timestamps: true, strict: true }, 70 | "searchFields": ["description", "title"], 71 | }, 72 | { 73 | "apiRoute": "test5", 74 | "collectionName": "test5", 75 | "schema": { 76 | address: String, 77 | description: String 78 | }, 79 | "mongooseOption": { timestamps: false, strict: true }, 80 | "searchFields": ["address"], 81 | }, 82 | /** 83 | * "collectionName": "users" 84 | */ 85 | { 86 | "apiRoute": "users", 87 | "collectionName": "users", 88 | "schema": { 89 | 90 | email: { 91 | type: String, 92 | required: true, 93 | select: true, 94 | }, 95 | password: { 96 | type: String, 97 | required: true, 98 | select: true, 99 | }, 100 | confirmPassword: { 101 | type: String, 102 | select: false, 103 | }, 104 | role: String, 105 | firstName: String, 106 | lastName: String, 107 | username: String, 108 | viewNum: Number, 109 | likeNum: Number, 110 | commentNum: Number, 111 | }, 112 | "mongooseOption": { timestamps: true, strict: false }, 113 | "searchFields": ["name"], 114 | // "selectFields": "-email -firebaseUid", 115 | "writeRules": { "ignoreCreateAuth": true, "selfUpdateFields": ["viewNum", "likeNum", "commentNum"] }, // allow register without check auth 116 | "aggregatePipeline": [ 117 | { 118 | "$addFields": 119 | { id: "$_id" } 120 | }, 121 | { 122 | "$project": 123 | { password: 0, confirmPassword: 0, email2: 0, "pets2.createdAt": 0, __v: 0, _id: 0, firebaseUid: 0, accessToken: 0 } 124 | } 125 | ] 126 | }, 127 | /** 128 | * "collectionName": "pets" 129 | */ 130 | { 131 | "apiRoute": "pets", 132 | "collectionName": "pets", 133 | "schema": { 134 | name: String, 135 | species: String, 136 | gender: String, 137 | type: String, 138 | content: String, 139 | priority: Number, 140 | editorChoice: Number, 141 | viewNum: Number, 142 | likeNum: Number, 143 | commentNum: Number, 144 | storyNum: Number, 145 | dob: Date, 146 | userId: String 147 | }, 148 | "mongooseOption": { timestamps: true, strict: false }, 149 | "searchFields": ["name", "content"], 150 | "writeRules": { "selfUpdateFields": ["viewNum", "likeNum", "commentNum", "storyNum"] }, 151 | // "readRules": { "checkAuth": true, "checkOwner": false }, 152 | "aggregatePipeline": [ 153 | { 154 | "$addFields": 155 | { id: "$_id" } 156 | }, 157 | { 158 | "$project": 159 | { __v: 0, _id: 0 } 160 | } 161 | ] 162 | }, 163 | /** 164 | * "collectionName": "stories" 165 | */ 166 | { 167 | "apiRoute": "stories", 168 | "collectionName": "stories", 169 | "schema": { 170 | title: String, 171 | content: String, 172 | species: String, 173 | tags: String, 174 | // isPublic: Boolean, 175 | priority: Number, 176 | editorChoice: Number, 177 | viewNum: Number, 178 | likeNum: Number, 179 | commentNum: Number, 180 | petId: String, 181 | userId: String 182 | }, 183 | "mongooseOption": { timestamps: true, strict: false }, 184 | "searchFields": ["title", "content", "tags"], 185 | // "readRules": { "checkAuth": true, "checkOwner": false }, 186 | "writeRules": { "selfUpdateFields": ["viewNum", "likeNum", "commentNum"] }, 187 | "aggregatePipeline": [ 188 | { 189 | "$addFields": 190 | { id: "$_id" } 191 | }, 192 | { 193 | "$project": 194 | { __v: 0, _id: 0 } 195 | } 196 | ] 197 | }, 198 | /** 199 | * "collectionName": "files" 200 | */ 201 | { 202 | "apiRoute": "files", 203 | "collectionName": "files", 204 | "schema": { 205 | title: String, 206 | content: String, 207 | type: String, // image, video, other file type 208 | url: String, 209 | priority: Number, 210 | editorChoice: Number, 211 | storyId: String, 212 | petId: String, 213 | userId: String 214 | }, 215 | "mongooseOption": { timestamps: true, strict: false }, 216 | "searchFields": ["title", "content"], 217 | "aggregatePipeline": [ 218 | { 219 | "$addFields": 220 | { id: "$_id" } 221 | }, 222 | { 223 | "$project": 224 | { __v: 0, _id: 0 } 225 | } 226 | ] 227 | }, 228 | /** 229 | * "collectionName": "species" 230 | */ 231 | { 232 | "apiRoute": "species", 233 | "collectionName": "species", 234 | "schema": { 235 | name: String, 236 | content: String, 237 | mainImageUrl: String, 238 | editorChoice: Number, 239 | priority: Number 240 | }, 241 | "mongooseOption": { timestamps: true, strict: false }, 242 | "searchFields": ["title", "content"], 243 | "writeRules": { "checkAdmin": true }, 244 | "aggregatePipeline": [ 245 | { 246 | "$addFields": 247 | { id: "$_id" } 248 | }, 249 | { 250 | "$project": 251 | { __v: 0, _id: 0 } 252 | } 253 | ] 254 | }, 255 | /** 256 | * "collectionName": "comments" 257 | */ 258 | { 259 | "apiRoute": "comments", 260 | "collectionName": "comments", 261 | "schema": { 262 | title: String, 263 | content: String, 264 | category: String, // petProfile, petStory, petOwner 265 | parentId: String, 266 | priority: Number, 267 | editorChoice: Number, 268 | isPublic: Boolean, 269 | storyId: String, 270 | petId: String, 271 | ownerId: String, 272 | userId: String, 273 | nickname: String 274 | }, 275 | "mongooseOption": { timestamps: true, strict: false }, 276 | "searchFields": ["title", "content"], 277 | "writeRules": { "ignoreCreateAuth": true }, 278 | "aggregatePipeline": [ 279 | { 280 | "$addFields": 281 | { id: "$_id" } 282 | }, 283 | { 284 | "$project": 285 | { __v: 0, _id: 0 } 286 | } 287 | ] 288 | }, 289 | /** 290 | * "collectionName": "likes" 291 | */ 292 | { 293 | "apiRoute": "likes", 294 | "collectionName": "likes", 295 | "schema": { 296 | type: String, // thumb up, heart, sad, cry, lol, surprise 297 | targetId: String, // target item id 298 | category: String, // petProfile, petStory, petOwner, comment 299 | userId: String, 300 | appId: String // app unique id 301 | }, 302 | "mongooseOption": { timestamps: true, strict: true }, 303 | "searchFields": ["type", "category"], 304 | "aggregatePipeline": [ 305 | { 306 | "$addFields": 307 | { id: "$_id" } 308 | }, 309 | { 310 | "$project": 311 | { __v: 0, _id: 0 } 312 | } 313 | ] 314 | }, 315 | /** 316 | * "collectionName": "orders" 317 | * Test readRules, only owner itself can view/edit 318 | */ 319 | { 320 | "apiRoute": "orders", 321 | "collectionName": "orders", 322 | "schema": { 323 | userId: String, 324 | productName: String, 325 | productId: String, 326 | productPrice: Number, 327 | orderAmount: Number, 328 | orderStatus: String, 329 | paymentStatus: String, 330 | name: String, 331 | contact: String, 332 | note: String, 333 | }, 334 | "mongooseOption": { timestamps: true, strict: false }, 335 | "searchFields": ["name", "note", "contact", "productTitle"], 336 | "writeRules": { "checkOwner": true }, 337 | "readRules": { "checkAuth": true, "checkOwner": true }, 338 | "aggregatePipeline": [ 339 | { 340 | "$addFields": 341 | { id: "$_id" } 342 | }, 343 | { 344 | "$project": 345 | { __v: 0, _id: 0 } 346 | } 347 | ] 348 | }, 349 | /** 350 | * "collectionName": "favorites" 351 | * Test readRules, only owner itself can view/edit 352 | */ 353 | { 354 | "apiRoute": "favorites", 355 | "collectionName": "favorites", 356 | "schema": { 357 | userId: String, 358 | productId: String, 359 | rate: Number, 360 | }, 361 | "mongooseOption": { timestamps: true, strict: false }, 362 | "searchFields": ["userId", "productId"], 363 | "writeRules": { "checkOwner": false }, 364 | "readRules": { "checkAuth": false, "checkOwner": true }, 365 | "aggregatePipeline": [ 366 | { 367 | "$addFields": 368 | { id: "$_id" } 369 | }, 370 | { 371 | "$project": 372 | { __v: 0, _id: 0 } 373 | } 374 | ] 375 | }, 376 | ]; 377 | // ENABLE_AUTH=true, all edit/delete/add has to be the owner or admin 378 | // ENABLE_AUTH=false, test & debug mode, everyone can edit/delete 379 | module.exports.ENABLE_AUTH = true; 380 | 381 | // user table api endpoint route 382 | module.exports.USER_ROUTE = 'users'; 383 | 384 | // the foreign key id field in other tables refers to user id 385 | module.exports.FIELD_USER_ID = 'userId'; 386 | module.exports.FIELD_PUBLIC = 'isPublic'; 387 | module.exports.FIELD_TARGET_USER_ID = 'ownerId'; 388 | module.exports.FIELD_PASSWORD = 'password'; 389 | 390 | module.exports.FIREBASE_DB_URL = 'https://pet-story-react.firebaseio.com'; 391 | 392 | module.exports.FIREBASE_SDK_KEY = process.env.FIREBASE_SDK_KEY && JSON.parse(process.env.FIREBASE_SDK_KEY) || 'please-set-FIREBASE_SDK_KEY-first'; -------------------------------------------------------------------------------- /app/helper/commonHelper.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | // const API_CONFIG = require("../config/api.config"); 4 | 5 | let helper = {}; 6 | 7 | helper.getApiConfig = () => { 8 | // read config file from the app 9 | const configFilePaths = ['/config/meanapi.config.js', '/config/api.config.js', '/meanapi.config.js', '/api.config.js']; 10 | let apiConfigFile = '../config/api.config.js'; 11 | if (process.env.API_CONFIG_FILE) { 12 | // for serverless environment 13 | apiConfigFile = process.env.API_CONFIG_FILE; 14 | } else { 15 | const appRootDir = process.cwd(); 16 | for (i = 0; i < configFilePaths.length; i++) { 17 | if (fs.existsSync(appRootDir + configFilePaths[i])) { 18 | apiConfigFile = appRootDir + configFilePaths[i]; 19 | break; 20 | } 21 | } 22 | } 23 | console.log('======= apiConfigFile=' + apiConfigFile + ' ->' + __filename); 24 | const config = require(apiConfigFile); 25 | return config; 26 | }; 27 | 28 | const API_CONFIG = helper.getApiConfig(); 29 | 30 | 31 | helper.parseEnvFile = (filePath) => { 32 | // try to load .env if not serverless 33 | const envFile = filePath ? filePath : process.cwd() + '/.env'; 34 | if (fs.existsSync(envFile)) { 35 | const envResult = require('dotenv').config({ path: envFile }); 36 | if (envResult.error) { 37 | throw envResult.error; 38 | } else if (process.env.DEBUG == 'yes') { 39 | console.log('envResult:', envResult.parsed); 40 | } 41 | } 42 | }; 43 | 44 | helper.getApiRoute = (req, res) => { 45 | // console.log('req.route.path',req.route.path); 46 | // console.log(req._parsedUrl); 47 | const apiRoute = req._parsedUrl.pathname.replace('/' + API_CONFIG.API_BASE, ''); 48 | if (apiRoute.indexOf('/search') != -1) { 49 | return apiRoute.split('/search')[0]; 50 | } else if (apiRoute.indexOf('/') != -1) { 51 | return apiRoute.split('/')[0]; 52 | } 53 | return apiRoute; 54 | }; 55 | helper.getUniversalDb = (db, req, res) => { 56 | // get dynamic dbModel via api router 57 | return db[helper.getApiRoute(req, res)]; 58 | }; 59 | 60 | helper.getApiSchema = (req, res) => { 61 | const apiRoute = helper.getApiRoute(req, res); 62 | return API_CONFIG.API_SCHEMAS.find(apiSchema => apiSchema.apiRoute == apiRoute); 63 | }; 64 | 65 | 66 | 67 | helper.hasAllSelfUpdateFields = (apiSchema, existItem, req, res) => { 68 | 69 | // check if all req.body fields are selfUpdateFields 70 | if (apiSchema.writeRules && apiSchema.writeRules.selfUpdateFields && apiSchema.writeRules.selfUpdateFields.length > 0) { 71 | let checkPoint = null; // not selfUpdateFields 72 | let noOtherFields = true; 73 | const fields = apiSchema.writeRules.selfUpdateFields; 74 | 75 | for (let [pName, pValue] of Object.entries(req.body)) { 76 | if (fields.includes(pName)) { 77 | if (existItem == 'allSelfUpdateFieldsOnly') { 78 | checkPoint = true; 79 | continue; 80 | } 81 | if (!existItem[pName]) { 82 | if (pValue == 'increment') { pValue = 1; req.body[pName] = pValue; } 83 | if (parseInt(pValue) != 1 && parseInt(pValue) != 0) { 84 | checkPoint = false; 85 | console.log('------selfUpdateFields checkPoint1', checkPoint); 86 | return false; 87 | } else { 88 | checkPoint = true; 89 | } 90 | } else if (pValue == 'increment') { 91 | checkPoint = true; 92 | pValue = parseInt(existItem[pName]) + 1; 93 | req.body[pName] = pValue; 94 | console.log('------selfUpdateFields checkPoint2 pValue=', pValue); 95 | } else if (pValue == 'decrement') { 96 | checkPoint = true; 97 | pValue = parseInt(existItem[pName]) - 1; 98 | if (pValue < 0) { pValue = 0; } 99 | req.body[pName] = pValue; 100 | console.log('------selfUpdateFields checkPoint3', pValue); 101 | } else if (Math.abs(parseInt(existItem[pName]) - parseInt(pValue)) > 1) { 102 | checkPoint = false; 103 | console.log('------selfUpdateFields checkPoint4', checkPoint); 104 | return false; 105 | } else { 106 | checkPoint = true; 107 | } 108 | } else { 109 | noOtherFields = false; 110 | } 111 | } 112 | console.log('------selfUpdateFields checkPoint5', checkPoint); 113 | if (noOtherFields && checkPoint) { 114 | return true; // pass all check points and noOtherFields 115 | } else if (checkPoint === false) { 116 | // only === false means has invalid selfUpdateFields, NOT ===null 117 | // even owner itself can not change selfUpdateFields with step>1 118 | return false; 119 | } 120 | } 121 | return false; 122 | }; 123 | 124 | 125 | 126 | helper.hasWritePermission = async (apiSchema, Universal, id, req, res) => { 127 | if (!API_CONFIG.ENABLE_AUTH) { 128 | return true; 129 | } 130 | if (!req.currentUser) { 131 | if (req.method == 'PUT' && helper.hasAllSelfUpdateFields(apiSchema, 'allSelfUpdateFieldsOnly', req, res)) { 132 | //shouldPassAuth but no req.currentUser 133 | } else { 134 | console.log('-----currentUser no req.currentUser'); 135 | return false; 136 | } 137 | } 138 | if (req.currentUser && req.currentUser.role && req.currentUser.role.toLowerCase().indexOf('admin') != -1) { 139 | // currentUser is admin 140 | console.log('=====currentUser is admin', req.currentUser.firstName); 141 | return true; 142 | } else { 143 | // check if must be admin 144 | if (apiSchema.writeRules && apiSchema.writeRules.checkAdmin) { 145 | return false; 146 | } 147 | // normal user, must be owner itself 148 | const existItem = id ? await Universal.findById(id) : req.body; 149 | if (!id) { 150 | console.log('======no id, add new item, check auth:', req.currentUser.id, existItem[API_CONFIG.FIELD_USER_ID]); 151 | // if no id, refactor the formData(req.body) with req.currentUser.id 152 | existItem[API_CONFIG.FIELD_USER_ID] = req.currentUser.id; 153 | } 154 | if (!existItem) { 155 | console.log(`Item not exists`); 156 | return false; 157 | } 158 | 159 | if (id && helper.hasAllSelfUpdateFields(apiSchema, existItem, req, res)) { 160 | return true; 161 | } 162 | // // if is update, check if all req.body fields are selfUpdateFields 163 | // if (id && apiSchema.writeRules && apiSchema.writeRules.selfUpdateFields && apiSchema.writeRules.selfUpdateFields.length > 0) { 164 | // let checkPoint = null; // not selfUpdateFields 165 | // let noOtherFields = true; 166 | // const fields = apiSchema.writeRules.selfUpdateFields; 167 | 168 | // for (let [pName, pValue] of Object.entries(req.body)) { 169 | // if (fields.includes(pName)) { 170 | // if (!existItem[pName]) { 171 | // if (pValue == 'increment') { pValue = 1; req.body[pName] = pValue; } 172 | // if (parseInt(pValue) != 1 && parseInt(pValue) != 0) { 173 | // checkPoint = false; 174 | // console.log('------selfUpdateFields checkPoint1', checkPoint) 175 | // return false; 176 | // } else { 177 | // checkPoint = true; 178 | // } 179 | // } else if (pValue == 'increment') { 180 | // checkPoint = true; 181 | // pValue = parseInt(existItem[pName]) + 1; 182 | // req.body[pName] = pValue; 183 | // console.log('------selfUpdateFields checkPoint4', pValue) 184 | // } else if (pValue == 'decrement') { 185 | // checkPoint = true; 186 | // pValue = parseInt(existItem[pName]) - 1; 187 | // req.body[pName] = pValue; 188 | // console.log('------selfUpdateFields checkPoint5', pValue) 189 | // } else if (Math.abs(parseInt(existItem[pName]) - parseInt(pValue)) > 1) { 190 | // checkPoint = false; 191 | // console.log('------selfUpdateFields checkPoint2', checkPoint) 192 | // return false; 193 | // } else { 194 | // checkPoint = true; 195 | // } 196 | // } else { 197 | // noOtherFields = false; 198 | // } 199 | // } 200 | // console.log('------selfUpdateFields checkPoint3', checkPoint) 201 | // if (noOtherFields && checkPoint) { 202 | // return true; // pass all check points and noOtherFields 203 | // } else if (checkPoint === false) { 204 | // // only === false means has invalid selfUpdateFields, NOT ===null 205 | // // even owner itself can not change selfUpdateFields with step>1 206 | // return false; 207 | // } 208 | // } 209 | 210 | // hasOwnProperty return false if userId not defined in api.config.js 211 | if ((apiSchema.schema.hasOwnProperty(API_CONFIG.FIELD_USER_ID) || existItem[API_CONFIG.FIELD_USER_ID]) && req.currentUser.id != existItem[API_CONFIG.FIELD_USER_ID]) { 212 | console.log("It not your item, CAN NOT UPDATE ITEM WITH ID " + (id || existItem[API_CONFIG.FIELD_USER_ID])); 213 | return false; 214 | } 215 | // if it's user table 216 | if (apiSchema.apiRoute == API_CONFIG.USER_ROUTE && id && req.currentUser.id != existItem.id) { 217 | console.log("Sorry, You are not allowed to update item " + (id || existItem.id)); 218 | return false; 219 | } 220 | console.log('=====currentUser id', req.currentUser.id, existItem[API_CONFIG.FIELD_USER_ID], apiSchema.schema.hasOwnProperty(API_CONFIG.FIELD_USER_ID), existItem); 221 | 222 | return true; 223 | } 224 | 225 | }; 226 | 227 | 228 | 229 | helper.hasReadPermission = (apiSchema, Universal, existItem, req, res) => { 230 | let hasPermission = false; 231 | if (apiSchema.readRules && apiSchema.readRules.checkAuth && apiSchema.readRules.checkOwner) { 232 | if (!req.currentUser) { 233 | console.log('=====hasReadPermission, NO req.currentUser for', apiSchema.apiRoute); 234 | return false; 235 | } 236 | if (req.currentUser.role && req.currentUser.role.toLowerCase().indexOf('admin') != -1) { 237 | // currentUser is admin 238 | console.log('=====hasReadPermission, currentUser is admin', req.currentUser.firstName); 239 | hasPermission = true; 240 | } else { 241 | // normal user, must be owner itself 242 | if (apiSchema.schema.hasOwnProperty(API_CONFIG.FIELD_USER_ID)) { 243 | // const existItem = itemData === null ? req.body : itemData; 244 | if (existItem && existItem[API_CONFIG.FIELD_USER_ID] && req.currentUser.id == existItem[API_CONFIG.FIELD_USER_ID]) { 245 | console.log('=====hasReadPermission, passed, currentUser is the owner'); 246 | hasPermission = true; 247 | } 248 | if (!existItem) { 249 | console.log('=====hasReadPermission, item not find '); 250 | } 251 | } else { 252 | // property not defined in schema(api.config.js) 253 | hasPermission = false; 254 | } 255 | 256 | } 257 | } else { 258 | console.log('=====hasReadPermission,passed, no need to check owner for ', apiSchema.apiRoute); 259 | hasPermission = true; 260 | } 261 | return hasPermission; 262 | }; 263 | 264 | helper.hasPrivateConstraint = (apiSchema, existItem, req, res) => { 265 | // check if the schema has isPublic field 266 | let hasPrivateConstraint = false; 267 | if (apiSchema.schema.hasOwnProperty(API_CONFIG.FIELD_PUBLIC)) { 268 | if (!req.currentUser) { 269 | console.log('hasPrivateConstraint: no req.currentUser'); 270 | hasPrivateConstraint = true; 271 | } else { 272 | if (req.currentUser.role && req.currentUser.role.toLowerCase().indexOf('admin') != -1) { 273 | // is admin 274 | } else if (existItem[API_CONFIG.FIELD_USER_ID] && existItem[API_CONFIG.FIELD_USER_ID] == req.currentUser.id) { 275 | // is owner 276 | } else if (existItem[API_CONFIG.FIELD_TARGET_USER_ID] && existItem[API_CONFIG.FIELD_TARGET_USER_ID] == req.currentUser.id) { 277 | // is target user, e.g. the user who receive a comment/message/reply 278 | } else { 279 | hasPrivateConstraint = true; 280 | } 281 | } 282 | } 283 | return hasPrivateConstraint; 284 | }; 285 | 286 | 287 | 288 | helper.decodeFirebaseIdToken = async (firebaseIdToken) => { 289 | const firebaseAdmin = require("firebase-admin"); 290 | if (!firebaseIdToken || firebaseIdToken.length < 250 || !API_CONFIG.FIREBASE_DB_URL || !API_CONFIG.FIREBASE_SDK_KEY || !API_CONFIG.FIREBASE_SDK_KEY.hasOwnProperty('private_key')) { 291 | console.log('Wrong firebaseIdToken length or wrong API_CONFIG.FIREBASE_SDK_KEY'); 292 | return false; 293 | } 294 | // !admin.apps.length ? admin.initializeApp() : admin.app(); 295 | if (firebaseAdmin.apps.length == 0) { 296 | firebaseAdmin.initializeApp({ 297 | credential: firebaseAdmin.credential.cert(API_CONFIG.FIREBASE_SDK_KEY), 298 | databaseURL: API_CONFIG.FIREBASE_DB_URL 299 | }); 300 | } else { 301 | firebaseAdmin.app(); 302 | } 303 | 304 | try { 305 | const decodedToken = await firebaseAdmin 306 | .auth() 307 | .verifyIdToken(firebaseIdToken); 308 | // .then((decodedToken) => { 309 | // const uid = decodedToken.uid; 310 | // console.log('firebaseIdToken decodedToken1 = ', decodedToken); 311 | // return false; 312 | // }) 313 | // .catch((error) => { 314 | // // Handle error 315 | // console.log('firebaseIdToken decodedToken error = ', error); 316 | // }); 317 | 318 | // console.log('firebaseIdToken decodedToken2 = ', decodedToken); 319 | return decodedToken; 320 | } catch (err) { 321 | console.log('decodedToken error = ', err.message); 322 | return false; 323 | } 324 | 325 | }; 326 | 327 | 328 | 329 | helper.getTableId = (tableName) => { 330 | let tableId = tableName + 'Id'; 331 | if (tableName.slice(-3) == 'ies') { 332 | tableId = tableName.slice(0, -3) + 'yId'; 333 | } else if (tableName.slice(-2) == 'es') { 334 | tableId = tableName.slice(0, -2) + 'Id'; 335 | } else if (tableName.slice(-1) == 's') { 336 | tableId = tableName.slice(0, -1) + 'Id'; 337 | } 338 | return tableId; 339 | }; 340 | 341 | helper.getPluralName = (tableName) => { 342 | let pluralName = tableName + 's'; 343 | const lastLetter = tableName.slice(-1); 344 | const last2Letter = tableName.slice(-2); 345 | if (lastLetter == 's' || lastLetter == 'x' || lastLetter == 'z' || last2Letter == 'ch' || last2Letter == 'sh') { 346 | pluralName = tableName + 'es'; 347 | } else if (lastLetter == 'y' && last2Letter != 'oy' && last2Letter != 'ey') { 348 | pluralName = tableName.slice(0, -1) + 'ies'; 349 | } else if (lastLetter == 'f' && last2Letter != 'of' && last2Letter != 'ef') { 350 | pluralName = tableName.slice(0, -1) + 'ves'; 351 | } else if (last2Letter == 'fe') { 352 | pluralName = tableName.slice(0, -2) + 'ves'; 353 | } 354 | return pluralName; 355 | }; 356 | 357 | helper.getChildrenLookupOperator = (id, tableName, apiSchema, embedSchema, foreignId, hasPrivate) => { 358 | const foreignField = foreignId ? foreignId : helper.getTableId(apiSchema.collectionName); 359 | let pipeline = embedSchema.aggregatePipeline ? embedSchema.aggregatePipeline : []; 360 | let match = {}; 361 | if (id) { 362 | // for show() 363 | match[foreignField] = id; 364 | } else { 365 | match['$expr'] = { "$eq": ["$" + foreignField, "$$strId"] }; 366 | } 367 | 368 | if (hasPrivate) { 369 | match[API_CONFIG.FIELD_PUBLIC] = true; 370 | } 371 | 372 | pipeline = [{ '$match': match }, ...pipeline]; 373 | const lookupOperator = { 374 | '$lookup': { 375 | 'from': tableName, 376 | "let": { "strId": { $ifNull: ["$id", "id-is-null"] } }, // maybe $_id in some case 377 | // "let": { "strId": { $ifNull: ["$id", "$_id"] } }, 378 | 'as': tableName, 379 | 'pipeline': pipeline 380 | } 381 | }; 382 | //BUG: users table(users?id=5fdbf8dc098f0a77130d4123&_embed=pets,stories,comments|ownerId) not working, but other tables works 383 | console.log('lookupOperator pipeline.match', match); 384 | return lookupOperator; 385 | }; 386 | 387 | helper.getParentLookupOperator = (foreignField, singularTableName, pluralTableName, expandSchema) => { 388 | let pipeline = expandSchema.aggregatePipeline ? expandSchema.aggregatePipeline : []; 389 | let match = {}; 390 | // match['_id'] = db.mongoose.Types.ObjectId(id); 391 | // match['$expr'] = { "$eq": ["$_id", "$$userId"] }; 392 | // match['$expr'] = { "$eq": ["$email", "Aaron@test.com"] }; 393 | // match['$expr'] = { "$eq": ["$_id", "5f9e18f0d9886400089675eb"] }; 394 | // match['$expr'] = { "$eq": ["$_id", db.mongoose.Types.ObjectId("5f9e18f0d9886400089675eb")] }; 395 | match['$expr'] = { "$eq": ["$_id", "$objId"] }; 396 | 397 | pipeline = [ 398 | ...[ 399 | { 400 | $addFields: { 401 | objId: { 402 | $convert: { 403 | input: "$$strId", 404 | to: "objectId", 405 | onError: 0 406 | } 407 | // "$toObjectId": "$$strId" 408 | } 409 | } 410 | }, 411 | { '$match': match } 412 | ], 413 | ...pipeline 414 | ]; 415 | const lookupOperator = { 416 | '$lookup': { 417 | 'from': pluralTableName, 418 | "let": { "strId": "$" + foreignField }, 419 | 'as': singularTableName, 420 | 'pipeline': pipeline 421 | } 422 | }; 423 | // console.log('getParentLookupOperator pipeline', pipeline); 424 | return lookupOperator; 425 | }; 426 | 427 | module.exports = helper; -------------------------------------------------------------------------------- /app/controllers/universal.controller.js: -------------------------------------------------------------------------------- 1 | // const API_CONFIG = require("../config/api.config"); 2 | const db = require("../models"); 3 | const jwt = require('jsonwebtoken'); 4 | const bcrypt = require('bcrypt'); 5 | const helper = require("../helper/commonHelper"); 6 | const API_CONFIG = helper.getApiConfig(); 7 | 8 | // Create and Save a new Universal 9 | exports.store = async (req, res) => { 10 | const apiSchema = helper.getApiSchema(req, res); 11 | const Universal = helper.getUniversalDb(db, req, res); 12 | if (apiSchema.writeRules && apiSchema.writeRules.ignoreCreateAuth && !req.body[API_CONFIG.FIELD_USER_ID]) { 13 | // ignore auth check, allow create item anonymous. e.g. contact us form 14 | } else { 15 | const hasPermission = await helper.hasWritePermission(apiSchema, Universal, null, req, res); 16 | if (!hasPermission) { 17 | res.status(401).send({ 18 | message: "User do not has the permission to create new item", 19 | }); 20 | return false; 21 | } 22 | } 23 | 24 | // handle user register with password 25 | if (apiSchema.apiRoute == API_CONFIG.USER_ROUTE && req.body.password) { 26 | if (req.body.email && req.body.email.indexOf('@') != -1) { 27 | userData = await Universal.findOne({ email: req.body.email }).exec(); 28 | if (userData) { 29 | return res.status(401).json({ error: "The email already exists" }); 30 | } 31 | } 32 | if (req.body.username && req.body.username.length > 2) { 33 | userData = await Universal.findOne({ username: req.body.username }).exec(); 34 | if (userData) { 35 | return res.status(401).json({ error: "The username already exists" }); 36 | } 37 | } 38 | // encrypt password 39 | req.body.password = bcrypt.hashSync(req.body.password, 10); 40 | } 41 | 42 | const universal = new Universal(req.body); 43 | // Save Universal in the database 44 | universal 45 | .save(universal) 46 | .then((data) => { 47 | res.send(data); 48 | }) 49 | .catch((err) => { 50 | res.status(500).send({ 51 | message: err.message || "Some error occurred while creating the item.", 52 | }); 53 | }); 54 | }; 55 | 56 | // Retrieve all universals from the database by find() 57 | exports.indexByFind = (req, res) => { 58 | 59 | // Universal = db[req.url.replace('/' + API_CONFIG.API_BASE, '')]; 60 | const Universal = helper.getUniversalDb(db, req, res); 61 | 62 | // console.log('===== query', req.query) 63 | const apiSchema = helper.getApiSchema(req, res); 64 | // console.log('apiSchema=', apiSchema); 65 | 66 | let condition = {}; 67 | let tempVar; 68 | for (paramName in req.query) { 69 | let paramValue = req.query[paramName]; 70 | 71 | // Add _like to filter %keyword%, e.g. name_like=ca 72 | // support simple regex syntax 73 | // e.g.name_like=^v name_like=n$ name_like=jack|alex name_like=or?n 74 | if (paramName.indexOf('_like') != -1) { 75 | paramName = paramName.replace('_like', ''); 76 | paramValue = '%' + paramValue + '%'; 77 | } 78 | 79 | // Add _gte or _lte for getting a range(cannot use together yet) 80 | // , e.g. age_lte=60 age_gte=18 81 | if (paramName.indexOf('_gte') != -1) { 82 | paramName = paramName.replace('_gte', ''); 83 | paramValue = '>' + paramValue; 84 | } 85 | if (paramName.indexOf('_lte') != -1) { 86 | paramName = paramName.replace('_lte', ''); 87 | paramValue = '<' + paramValue; 88 | } 89 | 90 | if (paramName in apiSchema.schema) { 91 | if (apiSchema.schema[paramName] == String || ("type" in apiSchema.schema[paramName] && apiSchema.schema[paramName].type == String)) { 92 | if (paramValue.indexOf('==') === 0) { 93 | // e.g. title===Cat 94 | // case sensitive equal match, better to set up index 95 | condition[paramName] = paramValue.replace('==', ''); 96 | } else if (paramValue.substr(0, 1) == '%' && paramValue.substr(-1) == '%') { 97 | // e.g. title=%ca% 98 | // case insensitive full text match, will not use index, slow 99 | condition[paramName] = { $regex: new RegExp(paramValue.replace(/%/g, '')), $options: "i" }; 100 | } else { 101 | // e.g. title=cat 102 | // case insensitive equal match, may not use the index 103 | // https://docs.mongodb.com/manual/reference/operator/query/regex/ 104 | condition[paramName] = { $regex: new RegExp("^" + paramValue.toLowerCase() + "$", "i") }; 105 | } 106 | 107 | } else if (apiSchema.schema[paramName] == Number || ("type" in apiSchema.schema[paramName] && apiSchema.schema[paramName].type == Number)) { 108 | if (paramValue.indexOf('>') === 0) { 109 | // e.g. age=>18 110 | tempVar = Number(paramValue.replace('>', '')); 111 | if (!isNaN(tempVar)) { 112 | condition[paramName] = { $gte: tempVar }; 113 | } 114 | } else if (paramValue.indexOf('<') === 0) { 115 | // e.g. age=<18 116 | tempVar = Number(paramValue.replace('<', '')); 117 | if (!isNaN(tempVar)) { 118 | condition[paramName] = { $lte: tempVar }; 119 | } 120 | } else if (!isNaN(paramValue)) { 121 | // e.g. age=18 122 | condition[paramName] = { $eq: paramValue }; 123 | } 124 | } 125 | } 126 | // console.log(paramName, paramValue) 127 | } 128 | 129 | // find multiple ids, e.g. ?id=1&id=2&id=3 or ?id=1,2,3,4,5 130 | if (req.query.id || req.query._id) { 131 | const originId = req.query.id ? req.query.id : req.query._id; 132 | const idAry = (typeof originId == 'string') ? originId.split(',') : originId; 133 | condition["_id"] = { "$in": idAry }; 134 | } 135 | 136 | // full text search 137 | if (req.query.q && req.query.q.trim() != '') { 138 | return this.search(req, res); 139 | } 140 | 141 | console.log('find query condition:', condition); 142 | 143 | // Add _sort and _order (ascending order by default) 144 | let defaultSort = {}; 145 | if (req.query._sort && req.query._order) { 146 | if (req.query._sort in apiSchema.schema) { 147 | defaultSort[req.query._sort] = (req.query._order == 'DESC') ? -1 : 1; 148 | } 149 | } 150 | if (Object.keys(defaultSort).length === 0) { 151 | defaultSort = { _id: -1 }; 152 | } 153 | 154 | // Add _start and _end or _limit 155 | let defaultSkip = 0; 156 | let defaultLimit = 0; 157 | if (req.query._start && req.query._end) { 158 | defaultSkip = parseInt(req.query._start); 159 | defaultLimit = req.query._limit ? parseInt(req.query._limit) : (parseInt(req.query._end) - parseInt(req.query._start)); 160 | } 161 | 162 | 163 | let query = Universal.find(condition).sort(defaultSort); 164 | 165 | // only display specific fields or exclude some fields 166 | if (apiSchema.selectFields && apiSchema.selectFields.length > 0) { 167 | query.select(apiSchema.selectFields); 168 | } 169 | 170 | if (defaultSkip > 0) { 171 | query.skip(defaultSkip); 172 | } 173 | if (defaultLimit > 0) { 174 | query.limit(defaultLimit); 175 | } 176 | query.then(async (data) => { 177 | const totalNumber = await Universal.countDocuments(condition); 178 | console.log('defaultSort', defaultSort, totalNumber); 179 | // const totalNumber = data.length; 180 | res.set("Access-Control-Expose-Headers", "X-Total-Count"); 181 | res.set("x-total-count", totalNumber); 182 | res.send(data); 183 | }).catch((err) => { 184 | res.status(500).send({ 185 | message: 186 | err.message || "Some error occurred while retrieving items.", 187 | }); 188 | }); 189 | }; 190 | 191 | 192 | // Retrieve all universals from the database by aggregate() 193 | exports.index = async (req, res) => { 194 | 195 | // Universal = db[req.url.replace('/' + API_CONFIG.API_BASE, '')]; 196 | const Universal = helper.getUniversalDb(db, req, res); 197 | 198 | // console.log('===== query', req.query) 199 | const apiSchema = helper.getApiSchema(req, res); 200 | // console.log('apiSchema=', apiSchema); 201 | 202 | let condition = {}; 203 | let tempVar; 204 | for (paramName in req.query) { 205 | let paramValue = req.query[paramName]; 206 | 207 | // Add _like to filter %keyword%, e.g. name_like=ca 208 | // support simple regex syntax 209 | // e.g.name_like=^v name_like=n$ name_like=jack|alex name_like=or?n 210 | if (paramName.indexOf('_like') != -1) { 211 | paramName = paramName.replace('_like', ''); 212 | paramValue = '%' + paramValue + '%'; 213 | } 214 | 215 | // Add _gte or _lte for getting a range(cannot use together yet) 216 | // , e.g. age_lte=60 age_gte=18 217 | if (paramName.indexOf('_gte') != -1) { 218 | paramName = paramName.replace('_gte', ''); 219 | paramValue = '>' + paramValue; 220 | } 221 | if (paramName.indexOf('_lte') != -1) { 222 | paramName = paramName.replace('_lte', ''); 223 | paramValue = '<' + paramValue; 224 | } 225 | 226 | if (paramName in apiSchema.schema) { 227 | if (apiSchema.schema[paramName] == String || ("type" in apiSchema.schema[paramName] && apiSchema.schema[paramName].type == String)) { 228 | if (paramValue.indexOf('==') === 0) { 229 | // e.g. title===Cat 230 | // case sensitive equal match, better to set up index 231 | condition[paramName] = paramValue.replace('==', ''); 232 | } else if (paramValue.substr(0, 1) == '%' && paramValue.substr(-1) == '%') { 233 | // e.g. title=%ca% 234 | // case insensitive full text match, will not use index, slow 235 | condition[paramName] = { $regex: new RegExp(paramValue.replace(/%/g, '')), $options: "i" }; 236 | } else if (paramValue.indexOf('i=') === 0) { 237 | // e.g. title=i=cat 238 | // case insensitive equal match, may not use the index 239 | // https://docs.mongodb.com/manual/reference/operator/query/regex/ 240 | condition[paramName] = { $regex: new RegExp("^" + paramValue.replace('i=', '').toLowerCase() + "$", "i") }; 241 | } else { 242 | // e.g. title=cat, same as title===cat 243 | // case sensitive equal match, better to set up index 244 | condition[paramName] = paramValue.replace('==', ''); 245 | } 246 | 247 | } else if (apiSchema.schema[paramName] == Number || ("type" in apiSchema.schema[paramName] && apiSchema.schema[paramName].type == Number)) { 248 | if (paramValue.indexOf('>') === 0) { 249 | // e.g. age=>18 250 | tempVar = Number(paramValue.replace('>', '')); 251 | if (!isNaN(tempVar)) { 252 | condition[paramName] = { $gte: tempVar }; 253 | } 254 | } else if (paramValue.indexOf('<') === 0) { 255 | // e.g. age=<18 256 | tempVar = Number(paramValue.replace('<', '')); 257 | if (!isNaN(tempVar)) { 258 | condition[paramName] = { $lte: tempVar }; 259 | } 260 | } else if (!isNaN(paramValue)) { 261 | // e.g. age=18 262 | condition[paramName] = { $eq: Number(paramValue) }; 263 | } 264 | } 265 | } 266 | // console.log(paramName, paramValue) 267 | } 268 | 269 | // find multiple ids, e.g. ?id=1&id=2&id=3 or ?id=1,2,3,4,5 270 | if (req.query.id || req.query._id) { 271 | const originId = req.query.id ? req.query.id : req.query._id; 272 | const idAry = (typeof originId == 'string') ? originId.split(',') : originId; 273 | let objIdAry = []; 274 | idAry.map(idStr => { 275 | if (db.mongoose.isValidObjectId(idStr)) { 276 | objIdAry.push(db.mongoose.Types.ObjectId(idStr)); 277 | } 278 | }); 279 | condition["_id"] = { "$in": objIdAry }; 280 | } 281 | 282 | // full text search 283 | if (req.query.q && req.query.q.trim() != '') { 284 | return this.search(req, res); 285 | } 286 | 287 | // check if it's set only the owner can view the list 288 | const hasPermission = helper.hasReadPermission(apiSchema, Universal, req.body, req, res); 289 | if (!hasPermission) { 290 | condition[API_CONFIG.FIELD_USER_ID] = req.currentUser.id; 291 | } 292 | 293 | // check if the schema hasPrivateConstraint 294 | if (helper.hasPrivateConstraint(apiSchema, condition, req, res)) { 295 | condition[API_CONFIG.FIELD_PUBLIC] = true; 296 | } 297 | 298 | console.log('find query condition:', condition); 299 | 300 | // Add _sort and _order (ascending order by default) 301 | let defaultSort = {}; 302 | if (req.query._sort && req.query._order) { 303 | if (req.query._sort in apiSchema.schema) { 304 | defaultSort[req.query._sort] = (req.query._order.toUpperCase() == 'DESC') ? -1 : 1; 305 | } 306 | } 307 | if (Object.keys(defaultSort).length === 0) { 308 | defaultSort = { _id: -1 }; 309 | } 310 | 311 | // Add _start and _end or _limit 312 | let defaultSkip = 0; 313 | let defaultLimit = 2000; 314 | if (req.query._start && req.query._end) { 315 | defaultSkip = parseInt(req.query._start); 316 | defaultLimit = req.query._limit ? parseInt(req.query._limit) : (parseInt(req.query._end) - parseInt(req.query._start)); 317 | } 318 | 319 | 320 | let pipelineOperators = [ 321 | { 322 | // '$match': { _id: db.mongoose.Types.ObjectId(id) } 323 | '$match': condition, 324 | }, 325 | { 326 | '$sort': defaultSort, 327 | }, 328 | { 329 | '$skip': defaultSkip, 330 | }, 331 | { 332 | '$limit': defaultLimit, 333 | }, 334 | // { 335 | // "$addFields": 336 | // { id: "$_id" } 337 | // } 338 | ]; 339 | 340 | // To include children resources, add _embed 341 | if (req.query._embed) { 342 | await req.query._embed.split(',').map(async (tableName) => { 343 | const embedTable = tableName.split('|'); 344 | const pluralTableName = embedTable[0]; 345 | const foreignField = embedTable[1] ? embedTable[1] : null; 346 | 347 | const embedSchema = API_CONFIG.API_SCHEMAS.find(apiSchema => apiSchema.collectionName == pluralTableName); 348 | if (embedSchema) { 349 | const hasPermission = helper.hasReadPermission(embedSchema, Universal, req.body, req, res); 350 | if (hasPermission) { 351 | const hasPrivate = helper.hasPrivateConstraint(embedSchema, condition, req, res); 352 | 353 | console.log('tableName embedSchema hasPrivate', hasPrivate); 354 | pipelineOperators.push(helper.getChildrenLookupOperator(null, pluralTableName, apiSchema, embedSchema, foreignField, hasPrivate)); 355 | } else { 356 | console.log('No read permission for embedSchema ', embedSchema.apiRoute); 357 | } 358 | 359 | } else { 360 | console.log('tableName schema not defined', tableName); 361 | } 362 | }); 363 | // console.log('pipelineOperators after req.query._embed', pipelineOperators); 364 | 365 | } 366 | 367 | // To include parent resource, add _expand 368 | if (req.query._expand) { 369 | await req.query._expand.split(',').map(async (tableName) => { 370 | const expandTable = tableName.split('|'); 371 | const singularTableName = expandTable[0]; 372 | const foreignField = expandTable[1] ? expandTable[1] : singularTableName + 'Id'; 373 | const pluralTableName = helper.getPluralName(singularTableName); 374 | const expandSchema = API_CONFIG.API_SCHEMAS.find(apiSchema => apiSchema.collectionName == pluralTableName); 375 | if (expandSchema) { 376 | const hasPermission = helper.hasReadPermission(expandSchema, Universal, req.body, req, res); 377 | if (hasPermission) { 378 | console.log('tableName expandSchema', expandSchema.apiRoute); 379 | pipelineOperators.push(helper.getParentLookupOperator(foreignField, singularTableName, pluralTableName, expandSchema)); 380 | 381 | pipelineOperators.push({ $unwind: { path: "$" + singularTableName, "preserveNullAndEmptyArrays": true } }); 382 | } else { 383 | console.log('No read permission for expandSchema ', expandSchema.apiRoute); 384 | } 385 | 386 | } else { 387 | console.log('tableName schema not defined', singularTableName, pluralTableName); 388 | } 389 | }); 390 | // console.log('pipelineOperators after req.query._expand', pipelineOperators); 391 | } 392 | 393 | // test nested query, not working 394 | // pipelineOperators.push( 395 | // { '$match': { "$pet.species": "Cat" } } 396 | // ) 397 | 398 | // combine pre-defined pipeline from api.config.js 399 | if (apiSchema.aggregatePipeline) { 400 | pipelineOperators = [...pipelineOperators, ...apiSchema.aggregatePipeline]; 401 | } 402 | 403 | // console.log('===exports.show pipelineOperators', pipelineOperators); 404 | 405 | const aggregateData = Universal.aggregate(pipelineOperators).exec((err, data) => { 406 | if (err) { 407 | return console.log('aggregate error', err); 408 | } 409 | // if (req.query?._responseType == 'returnData') { 410 | // console.log('aggregate data', data) 411 | // return data; 412 | // } else { 413 | // console.log('aggregate data.length', data.length) 414 | 415 | res.set("Access-Control-Expose-Headers", "X-Total-Count"); 416 | res.set("x-total-count", data.length); 417 | res.send(data); 418 | // } 419 | 420 | }); 421 | 422 | // console.log('aggregate aggregateQuery', aggregateData) 423 | return aggregateData; 424 | }; 425 | 426 | 427 | 428 | // Find a single Universal with an id via findById 429 | exports.showByFind = async (req, res) => { 430 | const apiSchema = helper.getApiSchema(req, res); 431 | 432 | // console.log(apiSchema, req.params); 433 | const id = req.params[apiSchema.apiRoute]; 434 | 435 | const Universal = helper.getUniversalDb(db, req, res); 436 | let query = Universal.findById(id); 437 | 438 | // only display specific fields or exclude some schema fields 439 | if (apiSchema.selectFields && apiSchema.selectFields.length > 0) { 440 | // query.select(apiSchema.selectFields); 441 | } 442 | 443 | query.then((data) => { 444 | if (!data) 445 | res.status(404).send({ message: "Not found the item with id " + id }); 446 | else { 447 | res.send(data); 448 | }; 449 | }).catch((err) => { 450 | res.status(500).send({ message: "Error retrieving the item with id=" + id }); 451 | }); 452 | }; 453 | 454 | 455 | // Find a single Universal with an id via aggregate 456 | // To include children resources, add _embed 457 | // To include parent resource, add _expand 458 | // e.g. /pets/5fcd8f4a3b755f0008556057?_expand=user,file|mainImageId&_embed=pets,stories 459 | exports.show = async (req, res) => { 460 | const apiSchema = helper.getApiSchema(req, res); 461 | 462 | // console.log(apiSchema, req.params); 463 | const id = req.params[apiSchema.apiRoute]; 464 | if (!db.mongoose.isValidObjectId(id)) { 465 | res.status(500).send({ message: id + " is not a ValidObjectId " }); 466 | return false; 467 | } 468 | 469 | const Universal = helper.getUniversalDb(db, req, res); 470 | 471 | // check if it's set only the owner can view the list 472 | const existItem = await Universal.findById(id); 473 | const hasPermission = helper.hasReadPermission(apiSchema, Universal, existItem, req, res); 474 | if (!hasPermission) { 475 | res.status(401).send({ message: " No read permission for " + req.currentUser.id }); 476 | return false; 477 | } 478 | 479 | // check if the schema hasPrivateConstraint 480 | if (helper.hasPrivateConstraint(apiSchema, existItem, req, res)) { 481 | res.status(401).send({ message: " This item is private" }); 482 | return false; 483 | } 484 | 485 | let pipelineOperators = [ 486 | { 487 | '$match': { _id: db.mongoose.Types.ObjectId(id) } 488 | } 489 | ]; 490 | 491 | // To include children resources, add _embed 492 | if (req.query._embed) { 493 | await req.query._embed.split(',').map(async (tableName) => { 494 | const embedTable = tableName.split('|'); 495 | const pluralTableName = embedTable[0]; 496 | const foreignField = embedTable[1] ? embedTable[1] : null; 497 | 498 | const embedSchema = API_CONFIG.API_SCHEMAS.find(apiSchema => apiSchema.collectionName == pluralTableName);; 499 | if (embedSchema) { 500 | // check embedSchema permission as well 501 | const hasPermission = helper.hasReadPermission(embedSchema, Universal, existItem, req, res); 502 | if (hasPermission) { 503 | let hasPrivate = helper.hasPrivateConstraint(embedSchema, existItem, req, res); 504 | 505 | if (hasPrivate && apiSchema.apiRoute == API_CONFIG.USER_ROUTE && req.currentUser && existItem.id == req.currentUser.id) { 506 | // if is user table 507 | hasPrivate = false; 508 | } 509 | console.log('tableName embedSchema hasPrivate', hasPrivate); pipelineOperators.push(helper.getChildrenLookupOperator(id, pluralTableName, apiSchema, embedSchema, foreignField, hasPrivate)); 510 | } else { 511 | // ignore the embed schema 512 | console.log('No read permission for embed schema ' + embedSchema.apiRoute + ''); 513 | } 514 | } else { 515 | console.log('tableName schema not defined', tableName); 516 | } 517 | 518 | }); 519 | } 520 | 521 | // To include parent resource, add _expand 522 | if (req.query._expand) { 523 | await req.query._expand.split(',').map(async tableName => { 524 | const expandTable = tableName.split('|'); 525 | const singularTableName = expandTable[0]; 526 | const foreignField = expandTable[1] ? expandTable[1] : singularTableName + 'Id'; 527 | const pluralTableName = helper.getPluralName(singularTableName); 528 | const expandSchema = API_CONFIG.API_SCHEMAS.find(apiSchema => apiSchema.collectionName == pluralTableName); 529 | if (expandSchema) { 530 | // check embedSchema permission as well 531 | const hasPermission = helper.hasReadPermission(expandSchema, Universal, existItem, req, res); 532 | if (hasPermission) { 533 | // console.log('tableName expandSchema', expandSchema) 534 | pipelineOperators.push(helper.getParentLookupOperator(foreignField, singularTableName, pluralTableName, expandSchema)); 535 | 536 | pipelineOperators.push({ $unwind: { path: "$" + singularTableName, "preserveNullAndEmptyArrays": true } }); 537 | } else { 538 | // ignore the expand schema 539 | console.log('No read permission for expand schema ' + expandSchema.apiRoute + ''); 540 | } 541 | 542 | } else { 543 | console.log('tableName schema not defined', singularTableName, pluralTableName); 544 | } 545 | 546 | }); 547 | } 548 | if (apiSchema.aggregatePipeline) { 549 | pipelineOperators = [...pipelineOperators, ...apiSchema.aggregatePipeline]; 550 | } 551 | 552 | 553 | // console.log('===exports.show pipelineOperators', pipelineOperators, apiSchema); 554 | 555 | let query = await Universal.aggregate(pipelineOperators).exec((err, data) => { 556 | if (err) { 557 | return console.log('aggregate error', err); 558 | } 559 | // console.log('aggregate data', data) 560 | if (!data || data.length == 0) 561 | res.status(404).send({ message: "Not found the item with id " + id }); 562 | else { 563 | res.send(data[0]); 564 | }; 565 | }); 566 | 567 | // console.log(query) 568 | return query; 569 | }; 570 | 571 | // Update a Universal by the id in the request 572 | exports.update = async (req, res) => { 573 | if (!req.body) { 574 | return res.status(400).send({ 575 | message: "Data to update can not be empty!", 576 | }); 577 | } 578 | const apiSchema = helper.getApiSchema(req, res); 579 | console.log('====update req', req.currentUser, req.body); 580 | const id = req.params[apiSchema.apiRoute]; 581 | const Universal = helper.getUniversalDb(db, req, res); 582 | 583 | const hasPermission = await helper.hasWritePermission(apiSchema, Universal, id, req, res); 584 | if (!hasPermission) { 585 | res.status(401).send({ 586 | message: `No permission to update item with id=${id}. `, 587 | }); 588 | return false; 589 | } 590 | // console.log('update req.body', req.body) 591 | 592 | // handle user update 593 | if (apiSchema.apiRoute == API_CONFIG.USER_ROUTE) { 594 | 595 | if (req.body.email && req.body.email.indexOf('@') != -1) { 596 | userData = await Universal.find({ email: req.body.email }).exec(); 597 | if (userData && userData.email && userData.email != req.body.email) { 598 | return res.status(401).json({ error: "The email already exists" }); 599 | } 600 | } 601 | if (req.body.username && req.body.username.length > 2) { 602 | userData = await Universal.find({ username: req.body.username }).exec(); 603 | if (userData && userData.username && userData.username != req.body.username) { 604 | return res.status(401).json({ error: "The username already exists" }); 605 | } 606 | } 607 | if (req.body.password && req.body.password.length > 2) { 608 | // encrypt password 609 | req.body.password = bcrypt.hashSync(req.body.password, 10); 610 | } 611 | } 612 | 613 | // TODO: if id not exist, create a new item -- for PUT method 614 | Universal.findByIdAndUpdate(id, req.body, { 615 | useFindAndModify: false, 616 | upsert: true, 617 | new: true, // return new data instead old data 618 | }) 619 | .then((data) => { 620 | if (!data) { 621 | res.status(404).send({ 622 | message: `Cannot find the item with id=${id}. Try to Create a new one!!`, 623 | }); 624 | // console.log("the item was not found! Create a new one!"); 625 | // create(req, res); 626 | } else { 627 | // res.send({ 628 | // message: "Item was updated successfully.", 629 | // data: data, 630 | // total: 1 631 | // }); 632 | res.send(data); 633 | } 634 | }) 635 | .catch((err) => { 636 | res.status(500).send({ 637 | message: "Error updating the item with id=" + id, 638 | }); 639 | }); 640 | }; 641 | 642 | 643 | 644 | 645 | // Delete a Universal with the specified id in the request 646 | exports.destroy = async (req, res) => { 647 | const apiSchema = helper.getApiSchema(req, res); 648 | // console.log(apiSchema, req.params); 649 | const id = req.params[apiSchema.apiRoute]; 650 | const Universal = helper.getUniversalDb(db, req, res); 651 | 652 | const hasPermission = await helper.hasWritePermission(apiSchema, Universal, id, req, res); 653 | if (!hasPermission) { 654 | res.status(401).send({ 655 | message: `No permission to DELETE item with id=${id}. currentUser: ${req.currentUser.id}`, 656 | }); 657 | return false; 658 | } 659 | 660 | Universal.findByIdAndRemove(id) 661 | .then((data) => { 662 | if (!data) { 663 | res.status(404).send({ 664 | message: `Cannot delete the item with id=${id}. Maybe the item was not found!`, 665 | }); 666 | } else { 667 | res.send({ 668 | message: "The item was deleted successfully!", 669 | }); 670 | } 671 | }) 672 | .catch((err) => { 673 | res.status(500).send({ 674 | message: "Could not delete the item with id=" + id, 675 | }); 676 | }); 677 | }; 678 | 679 | 680 | // full text search 681 | exports.search = (req, res) => { 682 | const keyword = req.params.keyword ? req.params.keyword : req.query.q; 683 | const Universal = helper.getUniversalDb(db, req, res); 684 | // Universal.find({ $text: { $search: keyword } }) 685 | const apiSchema = helper.getApiSchema(req, res); 686 | // console.log('apiSchema=', apiSchema); 687 | Universal.aggregate([ 688 | { 689 | $search: { 690 | text: { 691 | query: keyword.trim(), 692 | path: apiSchema.searchFields, 693 | }, 694 | }, 695 | }, 696 | ]) 697 | .then((data) => { 698 | res.send(data); 699 | }) 700 | .catch((err) => { 701 | res.status(500).send({ 702 | message: 703 | err.message || "Some error occurred while searching. ", 704 | }); 705 | }); 706 | }; 707 | 708 | // user token, todo: security 709 | exports.getUserToken = async (req, res) => { 710 | 711 | if (!req.body.firebaseIdToken && !req.body.awsIdToken && !req.body.password) { 712 | console.log('getUserToken req.body', req.body, req.body); 713 | return res.status(400).json({ error: "Missing params for getUserToken" }); 714 | } 715 | 716 | let findCondition = {}; 717 | let userData = null; 718 | const Universal = helper.getUniversalDb(db, req, res); 719 | 720 | if (req.body.firebaseIdToken) { 721 | // use cloud firebase login & return access jwt token 722 | const decodedToken = await helper.decodeFirebaseIdToken(req.body.firebaseIdToken); 723 | console.log('decodedToken decodedToken ', decodedToken); 724 | if (!decodedToken) { 725 | return res.status(401).json({ error: "Wrong firebaseIdToken" }); 726 | } 727 | if (!decodedToken.uid || decodedToken.uid != req.body.firebaseUid) { 728 | return res.status(401).json({ error: "firebaseUid not match firebaseIdToken" }); 729 | } 730 | findCondition.firebaseUid = decodedToken.uid; 731 | userData = await Universal.findOne(findCondition).exec(); 732 | 733 | } else if (req.body.password && (req.body.email || req.body.username)) { 734 | // use email/username and password login & return access jwt token 735 | if (req.body.email && req.body.email.indexOf('@') != -1) { 736 | findCondition.email = req.body.email; 737 | } else if (req.body.username && req.body.username.length > 2) { 738 | findCondition.username = req.body.username; 739 | } 740 | userData = await Universal.findOne(findCondition).exec(); 741 | console.log('password userData', userData); 742 | if (userData) { 743 | const passwordIsValid = bcrypt.compareSync( 744 | req.body.password, 745 | userData.password 746 | ); 747 | 748 | if (!passwordIsValid) { 749 | return res.status(400).json({ error: "Wrong password " }); 750 | } 751 | } 752 | 753 | } 754 | 755 | 756 | // if (!req.body.email) { 757 | // return res.status(400).json({ error: "no email" }); 758 | // } 759 | req.body._responseType = 'returnData'; 760 | // return this.index(req, res); 761 | // const returnData = this.index(req, res); 762 | 763 | 764 | console.log('_responseType userData', userData); 765 | if (!userData) { 766 | console.log('No such user findCondition', findCondition); 767 | return res.status(401).json({ error: "No such user" }); 768 | } 769 | // userProfile needs to be flexible, no hard code fields 770 | let userProfile = { 771 | id: userData._id ? userData._id : userData.id, 772 | email: userData.email, 773 | firstName: userData.firstName, 774 | lastName: userData.lastName, 775 | username: userData.username, 776 | role: userData.role, 777 | }; 778 | console.log('_responseType userProfile', userData['firstName']); 779 | // create token 780 | userProfile.accessToken = jwt.sign( 781 | userProfile,// payload data 782 | API_CONFIG.JWT_SECRET 783 | ); 784 | 785 | 786 | res.send(userProfile); 787 | // res.set("x-total-count", data.length); 788 | // res.send(data); 789 | }; --------------------------------------------------------------------------------