├── 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 | You need to enable JavaScript to run this app.
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 |
21 | DB = "YOUR-MONGODB-URI"
22 |
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 |
22 | DB = "YOUR-MONGODB-URI"
23 | API_BASE = ".netlify/functions/.netlify/functions/meanapi/"
24 | API_CONFIG_FILE = "/var/task/src/functions/meanapi/meanapi.config.js"
25 |
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 | };
--------------------------------------------------------------------------------