Adapt code style for new repository

This commit is contained in:
T. R. Bernstein
2025-10-03 20:06:19 +02:00
parent b36911f0b1
commit b212a83178
61 changed files with 4818 additions and 5154 deletions

View File

@@ -1,82 +0,0 @@
{
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"commitType": "docs",
"commitConvention": "angular",
"contributors": [
{
"login": "thevahidal",
"name": "Vahid Al",
"avatar_url": "https://avatars.githubusercontent.com/u/20302825?v=4",
"profile": "http://linktr.ee/thevahidal",
"contributions": [
"code",
"review"
]
},
{
"login": "AbegaM",
"name": "Abenezer Melkamu",
"avatar_url": "https://avatars.githubusercontent.com/u/70259638?v=4",
"profile": "https://github.com/AbegaM",
"contributions": [
"code"
]
},
{
"login": "IanMayo",
"name": "Ian Mayo",
"avatar_url": "https://avatars.githubusercontent.com/u/1108513?v=4",
"profile": "https://github.com/IanMayo",
"contributions": [
"code",
"review"
]
},
{
"login": "HanzCEO",
"name": "Hanz",
"avatar_url": "https://avatars.githubusercontent.com/u/40712686?v=4",
"profile": "https://godot.id",
"contributions": [
"code"
]
},
{
"login": "KoenDG",
"name": "Koen De Groote",
"avatar_url": "https://avatars.githubusercontent.com/u/1440619?v=4",
"profile": "https://github.com/KoenDG",
"contributions": [
"code"
]
},
{
"login": "TahaKhanAbdalli",
"name": "Muhammad Taha Khan",
"avatar_url": "https://avatars.githubusercontent.com/u/50602678?v=4",
"profile": "https://github.com/TahaKhanAbdalli",
"contributions": [
"code"
]
},
{
"login": "thoughtsunificator",
"name": "Romain Lebesle",
"avatar_url": "https://avatars.githubusercontent.com/u/85041649?v=4",
"profile": "http://thoughtsunificator.me",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
"skipCi": true,
"repoType": "github",
"repoHost": "https://github.com",
"projectName": "soul",
"projectOwner": "thevahidal"
}

View File

@@ -1,26 +1,18 @@
# EditorConfig is awesome: https://github.com/editorconfig/editorconfig
# Top-most EditorConfig file
root = true
[*.{js,jsx,ts,tsx}]
quote_type = single
[*]
# Set default charset to utf-8
charset = utf-8
# Set default indentation to spaces
indent_style = space
# Linux-style newlines with a newline ending every file
end_of_line = lf
insert_final_newline = true
# Remove whitespace characters preceding newline characters
charset = utf-8
[*.{js,ts,json,cjs,mjs,tsx}]
indent_style = tab
trim_trailing_whitespace = true
tab_width = 4
# Two space indentation for JavaScript files
[*.{js,json}]
[*.json]
tab_width = 2
[*.{yml,yaml}]
indent_style = space
indent_size = 2
# Disable trimming trailing whitespaces so that double space newlines work
[*.md]
trim_trailing_whitespace = false

View File

@@ -1,24 +1,24 @@
module.exports = {
env: {
node: true,
commonjs: true,
es2021: true,
jest: true,
},
extends: ["eslint:recommended", "prettier"],
overrides: [
{
env: {
node: true,
},
files: [".eslintrc.{js,cjs}"],
parserOptions: {
sourceType: "script",
},
},
],
parserOptions: {
ecmaVersion: "latest",
},
rules: {},
};
env: {
node: true,
commonjs: true,
es2021: true,
jest: true
},
extends: ['eslint:recommended', 'prettier'],
overrides: [
{
env: {
node: true
},
files: ['.eslintrc.{js,cjs}'],
parserOptions: {
sourceType: 'script'
}
}
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {}
}

View File

@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within

View File

@@ -1,17 +1,17 @@
# Contributing
When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.
email, or any other method with the owners of this repository before making a change.
Please note we have a code of conduct, please follow it in all your interactions with the project.
## Pull Request Process
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
build.
2. Update the README.md with details of changes to the interface, this includes new environment
2. Update the README.md with details of changes to the interface, this includes new environment
variables, exposed ports, useful file locations and container parameters.
3. Increase the version numbers in any examples files and the README.md to the new version that this
Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
do not have permission to do that, you may request the second reviewer to merge it for you.

View File

@@ -18,8 +18,8 @@ Using the following Dockerfile:
<details>
<summary>Dockerfile</summary>
```nginx
```nginx
# node:19-alpine amd64
FROM node@sha256:d0ba7111bc031323ce2706f8e424afc868db289ba40ff55b05561cf59c123be1 AS node
@@ -34,7 +34,8 @@ RUN apk update && apk add python3=3.11.10-r1 build-base=0.5-r3 && npm ci
COPY . .
CMD [ "npm", "start" ]
```
```
</details>
You can proceed [to building the application](https://docs.docker.com/get-started/workshop/02_our_app/#build-the-apps-image).

View File

@@ -1,6 +1,7 @@
---
---
@import "https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.1/github-markdown.min.css";
@import 'https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.1/github-markdown.min.css';
body {
margin: 0;

View File

@@ -6,7 +6,6 @@ Soul extensions are a way to extend the functionality of Soul. Extensions are wr
- API Extensions: Add new endpoints to Soul
## Setup Environment
To follow the below examples we need to download a sample database and also install Soul CLI.
@@ -18,6 +17,7 @@ wget https://raw.githubusercontent.com/lerocha/chinook-database/master/ChinookDa
```
### Using Soul CLI
```bash
npm install -g soul-cli
soul -d ./Chinook_Sqlite.sqlite -p 8000 -e "/absolute/path/to/_extensions/"
@@ -32,8 +32,8 @@ npm install # Install dependencies
npm link # might need `sudo`
soul -d ./Chinook_Sqlite.sqlite -p 8000 -e "/absolute/path/to/_extensions/"
```
</details>
</details>
## Creating an API extension
@@ -45,65 +45,64 @@ const hello = {
path: '/api/hello-soul',
handler: (req, res, db) => {
res.status(200).json({
message: 'Hello Soul!'
});
},
};
message: 'Hello Soul!'
})
}
}
const timestamp = {
method: 'GET',
path: '/api/timestamp',
handler: (req, res, db) => {
res.status(200).json({
timestamp: Date.now(),
});
},
};
timestamp: Date.now()
})
}
}
const greetings = {
method: 'POST',
path: '/api/greetings/:name',
handler: (req, res, db) => {
const { name } = req.params;
const { greeting } = req.body;
const { name } = req.params
const { greeting } = req.body
res.status(200).json({
message: `${greeting} ${name}!`,
});
},
message: `${greeting} ${name}!`
})
}
}
const searchTables = {
method: 'GET',
path: '/api/search-tables',
handler: (req, res, db) => {
const { q } = req.query;
const { q } = req.query
const sql = `
SELECT name FROM sqlite_master
WHERE type='table'
AND name LIKE $searchQuery
`;
`
try {
const tables = db.prepare(sql).all({
searchQuery: `%${q}%`,
});
searchQuery: `%${q}%`
})
res.status(200).json({
tables,
});
tables
})
} catch (error) {
res.status(500).json({
error: error.message,
});
error: error.message
})
}
},
};
}
}
module.exports = {
hello,
timestamp,
greetings,
searchTables,
};
searchTables
}
```
Alright, now we can test if the extension is working:

View File

@@ -22,14 +22,14 @@ In this guide, we will demonstrate how to host a static `React Admin` applicatio
```js
const reactAdminApp = {
method: "GET",
path: "/api/client",
method: 'GET',
path: '/api/client',
handler: (req, res, db) => {
const clientPath = path.join(__dirname, "../dist", "index.html");
res.app.use(express.static(path.join(__dirname, "../dist")));
res.sendFile(clientPath);
},
};
const clientPath = path.join(__dirname, '../dist', 'index.html')
res.app.use(express.static(path.join(__dirname, '../dist')))
res.sendFile(clientPath)
}
}
```
3. Build your React Admin client:

View File

@@ -1,71 +1,71 @@
{
"name": "soul-cli",
"version": "0.8.1",
"description": "A SQLite REST and Realtime server",
"main": "src/server.js",
"bin": {
"soul": "./src/server.js"
},
"scripts": {
"start": "node src/server.js",
"dev": "npm run swagger-autogen && cross-env NO_CLI=true nodemon src/server.js",
"cli": "nodemon src/server.js --database foobar.db",
"swagger-autogen": "cross-env NO_CLI=true node src/swagger/index.js",
"test": "cross-env CI=true NODE_ENV=test NO_CLI=true DB=test.db CORE_PORT=8001 jest --testTimeout=10000",
"prepare": "husky install",
"lint": "eslint . --fix --max-warnings=0",
"format": "prettier . --write"
},
"repository": {
"type": "git",
"url": "git+https://github.com/thevahidal/soul.git"
},
"author": "Vahid Al @thevahidal",
"license": "MIT",
"bugs": {
"url": "https://github.com/thevahidal/soul/issues"
},
"homepage": "https://github.com/thevahidal/soul#readme",
"dependencies": {
"bcrypt": "^5.1.1",
"better-sqlite3": "^8.1.0",
"body-parser": "^1.20.2",
"check-password-strength": "^2.0.7",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"express-winston": "^4.2.0",
"joi": "^17.8.3",
"jsonwebtoken": "^9.0.2",
"soul-studio": "^0.0.1",
"swagger-ui-express": "^4.6.1",
"winston": "^3.8.2",
"ws": "^8.12.1",
"yargs": "^17.7.1"
},
"devDependencies": {
"cross-env": "^7.0.3",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"husky": "^8.0.3",
"jest": "^29.4.3",
"nodemon": "^3.1.3",
"prettier": "3.1.0",
"supertest": "^6.3.3",
"swagger-autogen": "^2.23.1"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": [
"/node_modules/"
],
"globalSetup": "./src/tests/globalSetup.js",
"globalTeardown": "./src/tests/globalTeardown.js"
},
"lint-staged": {
"*.js": "eslint --fix",
"*.{js,css,md,html,json}": "prettier --write"
}
"name": "soul-cli",
"version": "0.8.1",
"description": "A SQLite REST and Realtime server",
"main": "src/server.js",
"bin": {
"soul": "./src/server.js"
},
"scripts": {
"start": "node src/server.js",
"dev": "npm run swagger-autogen && cross-env NO_CLI=true nodemon src/server.js",
"cli": "nodemon src/server.js --database foobar.db",
"swagger-autogen": "cross-env NO_CLI=true node src/swagger/index.js",
"test": "cross-env CI=true NODE_ENV=test NO_CLI=true DB=test.db CORE_PORT=8001 jest --testTimeout=10000",
"prepare": "husky install",
"lint": "eslint . --fix --max-warnings=0",
"format": "prettier . --write"
},
"repository": {
"type": "git",
"url": "git+https://github.com/thevahidal/soul.git"
},
"author": "Vahid Al @thevahidal",
"license": "MIT",
"bugs": {
"url": "https://github.com/thevahidal/soul/issues"
},
"homepage": "https://github.com/thevahidal/soul#readme",
"dependencies": {
"bcrypt": "^5.1.1",
"better-sqlite3": "^8.1.0",
"body-parser": "^1.20.2",
"check-password-strength": "^2.0.7",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"express-winston": "^4.2.0",
"joi": "^17.8.3",
"jsonwebtoken": "^9.0.2",
"soul-studio": "^0.0.1",
"swagger-ui-express": "^4.6.1",
"winston": "^3.8.2",
"ws": "^8.12.1",
"yargs": "^17.7.1"
},
"devDependencies": {
"cross-env": "^7.0.3",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"husky": "^8.0.3",
"jest": "^29.4.3",
"nodemon": "^3.1.3",
"prettier": "3.1.0",
"supertest": "^6.3.3",
"swagger-autogen": "^2.23.1"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": [
"/node_modules/"
],
"globalSetup": "./src/tests/globalSetup.js",
"globalTeardown": "./src/tests/globalTeardown.js"
},
"lint-staged": {
"*.js": "eslint --fix",
"*.{js,css,md,html,json}": "prettier --write"
}
}

View File

@@ -1,122 +1,122 @@
const yargs = require('yargs');
const yargs = require('yargs')
const usage = `
Soul | REST and realtime server for SQLite
Usage: soul [options]
`;
`
let options = undefined;
let options = undefined
if (process.env.NO_CLI !== 'true') {
options = yargs
.usage(usage)
.option('d', {
alias: 'database',
describe: 'SQLite database file or :memory:',
type: 'string',
demandOption: true,
})
.option('p', {
alias: 'port',
describe: 'Port to listen on',
type: 'number',
demandOption: false,
})
.option('r', {
alias: 'rate-limit-enabled',
describe: 'Enable rate limiting',
type: 'boolean',
demandOption: false,
})
.option('c', {
alias: 'cors',
describe: 'CORS whitelist origins',
type: 'string',
demandOption: false,
})
.option('V', {
alias: 'verbose',
describe: 'Enable verbose logging',
type: 'string',
demandOption: false,
choices: ['console', null],
})
.options('e', {
alias: 'extensions',
describe: 'Extensions directory path to load',
type: 'string',
demandOption: false,
})
.options('env', {
alias: 'envpath',
describe: 'Environment variable file path to load',
type: 'string',
demandOption: false,
})
.options('a', {
alias: 'auth',
describe: 'Enable authentication and authorization',
type: 'boolean',
demandOption: false,
})
.options('ts', {
alias: 'tokensecret',
describe: 'JWT secret for the access and refresh tokens',
type: 'string',
demandOption: false,
})
.options('atet', {
alias: 'accesstokenexpirationtime',
describe: 'JWT expiration time for access token',
type: 'string',
demandOption: false,
})
.options('rtet', {
alias: 'refreshtokenexpirationtime',
describe: 'JWT expiration time for refresh token',
type: 'string',
demandOption: false,
})
.options('iuu', {
alias: 'initialuserusername',
describe: 'Initial superuser username',
type: 'string',
demandOption: false,
})
.options('iup', {
alias: 'initialuserpassword',
describe: 'Initial superuser password',
type: 'string',
demandOption: false,
})
.options('S', {
alias: 'studio',
describe: 'Start Soul Studio in parallel',
type: 'boolean',
demandOption: false,
})
.command('updatesuperuser', 'Update a superuser', (yargs) => {
return yargs
.option('id', {
describe: 'The ID of the superuser you want to update',
type: 'number',
demandOption: true,
})
.option('password', {
describe: 'The new password for the superuser you want to update',
type: 'string',
demandOption: false,
})
.option('is_superuser', {
describe: 'The role of the superuser you want to update',
type: 'boolean',
demandOption: false,
});
})
.help(true).argv;
options = yargs
.usage(usage)
.option('d', {
alias: 'database',
describe: 'SQLite database file or :memory:',
type: 'string',
demandOption: true
})
.option('p', {
alias: 'port',
describe: 'Port to listen on',
type: 'number',
demandOption: false
})
.option('r', {
alias: 'rate-limit-enabled',
describe: 'Enable rate limiting',
type: 'boolean',
demandOption: false
})
.option('c', {
alias: 'cors',
describe: 'CORS whitelist origins',
type: 'string',
demandOption: false
})
.option('V', {
alias: 'verbose',
describe: 'Enable verbose logging',
type: 'string',
demandOption: false,
choices: ['console', null]
})
.options('e', {
alias: 'extensions',
describe: 'Extensions directory path to load',
type: 'string',
demandOption: false
})
.options('env', {
alias: 'envpath',
describe: 'Environment variable file path to load',
type: 'string',
demandOption: false
})
.options('a', {
alias: 'auth',
describe: 'Enable authentication and authorization',
type: 'boolean',
demandOption: false
})
.options('ts', {
alias: 'tokensecret',
describe: 'JWT secret for the access and refresh tokens',
type: 'string',
demandOption: false
})
.options('atet', {
alias: 'accesstokenexpirationtime',
describe: 'JWT expiration time for access token',
type: 'string',
demandOption: false
})
.options('rtet', {
alias: 'refreshtokenexpirationtime',
describe: 'JWT expiration time for refresh token',
type: 'string',
demandOption: false
})
.options('iuu', {
alias: 'initialuserusername',
describe: 'Initial superuser username',
type: 'string',
demandOption: false
})
.options('iup', {
alias: 'initialuserpassword',
describe: 'Initial superuser password',
type: 'string',
demandOption: false
})
.options('S', {
alias: 'studio',
describe: 'Start Soul Studio in parallel',
type: 'boolean',
demandOption: false
})
.command('updatesuperuser', 'Update a superuser', (yargs) => {
return yargs
.option('id', {
describe: 'The ID of the superuser you want to update',
type: 'number',
demandOption: true
})
.option('password', {
describe: 'The new password for the superuser you want to update',
type: 'string',
demandOption: false
})
.option('is_superuser', {
describe: 'The role of the superuser you want to update',
type: 'boolean',
demandOption: false
})
})
.help(true).argv
}
module.exports = {
yargs,
usage,
options,
};
yargs,
usage,
options
}

View File

@@ -1,21 +1,21 @@
const { yargs } = require('./cli');
const { updateSuperuser } = require('./controllers/auth');
const { yargs } = require('./cli')
const { updateSuperuser } = require('./controllers/auth')
const { argv } = yargs;
const { argv } = yargs
const runCLICommands = () => {
// if the updatesuperuser command is passed from the CLI execute the updatesuperuser function
if (argv._.includes('updatesuperuser')) {
const { id, password, is_superuser } = argv;
if (!password && is_superuser === undefined) {
console.log(
'Please provide either the --password or --is_superuser flag when using the updateuser command.',
);
process.exit(1);
} else {
updateSuperuser({ id, password, is_superuser });
}
}
};
// if the updatesuperuser command is passed from the CLI execute the updatesuperuser function
if (argv._.includes('updatesuperuser')) {
const { id, password, is_superuser } = argv
if (!password && is_superuser === undefined) {
console.log(
'Please provide either the --password or --is_superuser flag when using the updateuser command.'
)
process.exit(1)
} else {
updateSuperuser({ id, password, is_superuser })
}
}
}
module.exports = { runCLICommands };
module.exports = { runCLICommands }

View File

@@ -1,143 +1,131 @@
const dotenv = require('dotenv');
const Joi = require('joi');
const path = require('path');
const dotenv = require('dotenv')
const Joi = require('joi')
const path = require('path')
const { yargs } = require('../cli');
const { yargs } = require('../cli')
const { argv } = yargs;
const { argv } = yargs
dotenv.config({ path: argv.envpath || path.join(__dirname, '../../.env') });
dotenv.config({ path: argv.envpath || path.join(__dirname, '../../.env') })
const envVarsSchema = Joi.object()
.keys({
CORE_PORT: Joi.number().positive().default(8000),
.keys({
CORE_PORT: Joi.number().positive().default(8000),
NODE_ENV: Joi.string()
.valid('production', 'development', 'test')
.default('production'),
NODE_ENV: Joi.string().valid('production', 'development', 'test').default('production'),
DB: Joi.string().required(),
VERBOSE: Joi.string().valid('console', null).default(null),
DB: Joi.string().required(),
VERBOSE: Joi.string().valid('console', null).default(null),
CORS_ORIGIN_WHITELIST: Joi.string().default('*'),
AUTH: Joi.boolean(),
CORS_ORIGIN_WHITELIST: Joi.string().default('*'),
AUTH: Joi.boolean(),
RATE_LIMIT_ENABLED: Joi.boolean().default(false),
RATE_LIMIT_WINDOW_MS: Joi.number().positive().default(1000),
RATE_LIMIT_MAX_REQUESTS: Joi.number().positive().default(10),
RATE_LIMIT_ENABLED: Joi.boolean().default(false),
RATE_LIMIT_WINDOW_MS: Joi.number().positive().default(1000),
RATE_LIMIT_MAX_REQUESTS: Joi.number().positive().default(10),
EXTENSIONS: Joi.string().default(null),
EXTENSIONS: Joi.string().default(null),
START_WITH_STUDIO: Joi.boolean().default(false),
START_WITH_STUDIO: Joi.boolean().default(false),
INITIAL_USER_USERNAME: Joi.string(),
INITIAL_USER_PASSWORD: Joi.string(),
INITIAL_USER_USERNAME: Joi.string(),
INITIAL_USER_PASSWORD: Joi.string(),
TOKEN_SECRET: Joi.string(),
ACCESS_TOKEN_EXPIRATION_TIME: Joi.string(),
REFRESH_TOKEN_EXPIRATION_TIME: Joi.string(),
})
.unknown();
TOKEN_SECRET: Joi.string(),
ACCESS_TOKEN_EXPIRATION_TIME: Joi.string(),
REFRESH_TOKEN_EXPIRATION_TIME: Joi.string()
})
.unknown()
const env = {
...process.env,
};
...process.env
}
if (argv.port) {
env.CORE_PORT = argv.port;
env.CORE_PORT = argv.port
}
if (argv.verbose) {
env.VERBOSE = argv.verbose;
env.VERBOSE = argv.verbose
}
if (argv.database) {
env.DB = argv.database;
env.DB = argv.database
}
if (argv.cors) {
env.CORS_ORIGIN_WHITELIST = argv.cors;
env.CORS_ORIGIN_WHITELIST = argv.cors
}
if (argv.auth) {
env.AUTH = argv.auth;
env.AUTH = argv.auth
}
if (argv['rate-limit-enabled']) {
env.RATE_LIMIT_ENABLED = argv['rate-limit-enabled'];
env.RATE_LIMIT_ENABLED = argv['rate-limit-enabled']
}
if (argv.tokensecret) {
env.TOKEN_SECRET = argv.tokensecret;
env.TOKEN_SECRET = argv.tokensecret
}
if (argv.accesstokenexpirationtime) {
env.ACCESS_TOKEN_EXPIRATION_TIME = argv.accesstokenexpirationtime;
env.ACCESS_TOKEN_EXPIRATION_TIME = argv.accesstokenexpirationtime
}
if (argv.refreshtokenexpirationtime) {
env.REFRESH_TOKEN_EXPIRATION_TIME = argv.refreshtokenexpirationtime;
env.REFRESH_TOKEN_EXPIRATION_TIME = argv.refreshtokenexpirationtime
}
if (argv.initialuserusername) {
env.INITIAL_USER_USERNAME = argv.initialuserusername;
env.INITIAL_USER_USERNAME = argv.initialuserusername
}
if (argv.initialuserpassword) {
env.INITIAL_USER_PASSWORD = argv.initialuserpassword;
env.INITIAL_USER_PASSWORD = argv.initialuserpassword
}
const { value: envVars, error } = envVarsSchema
.prefs({ errors: { label: 'key' } })
.validate(env);
const { value: envVars, error } = envVarsSchema.prefs({ errors: { label: 'key' } }).validate(env)
if (error) {
throw new Error(`Config validation error: ${error.message}`);
throw new Error(`Config validation error: ${error.message}`)
}
module.exports = {
env: envVars.NODE_ENV,
env: envVars.NODE_ENV,
isProduction: envVars.NODE_ENV === 'production',
isDevelopment: envVars.NODE_ENV === 'development',
isTest: envVars.NODE_ENV === 'test',
isProduction: envVars.NODE_ENV === 'production',
isDevelopment: envVars.NODE_ENV === 'development',
isTest: envVars.NODE_ENV === 'test',
port: argv.port || envVars.CORE_PORT,
verbose: argv['verbose'] || envVars.VERBOSE,
port: argv.port || envVars.CORE_PORT,
verbose: argv['verbose'] || envVars.VERBOSE,
db: {
filename: argv.database || envVars.DB || ':memory:',
},
cors: {
origin: argv.cors?.split(',') ||
envVars.CORS_ORIGIN_WHITELIST?.split(',') || ['*'],
},
db: {
filename: argv.database || envVars.DB || ':memory:'
},
cors: {
origin: argv.cors?.split(',') || envVars.CORS_ORIGIN_WHITELIST?.split(',') || ['*']
},
auth: argv.auth || envVars.AUTH || false,
tokenSecret: argv.tokensecret || envVars.TOKEN_SECRET || null,
accessTokenExpirationTime:
argv.accesstokenexpirationtime ||
envVars.ACCESS_TOKEN_EXPIRATION_TIME ||
'5H',
refreshTokenExpirationTime:
argv.refreshtokenexpirationtime ||
envVars.REFRESH_TOKEN_EXPIRATION_TIME ||
'3D',
auth: argv.auth || envVars.AUTH || false,
tokenSecret: argv.tokensecret || envVars.TOKEN_SECRET || null,
accessTokenExpirationTime: argv.accesstokenexpirationtime || envVars.ACCESS_TOKEN_EXPIRATION_TIME || '5H',
refreshTokenExpirationTime:
argv.refreshtokenexpirationtime || envVars.REFRESH_TOKEN_EXPIRATION_TIME || '3D',
initialUserUsername:
argv.initialuserusername || envVars.INITIAL_USER_USERNAME,
initialUserPassword:
argv.initialuserpassword || envVars.INITIAL_USER_PASSWORD,
initialUserUsername: argv.initialuserusername || envVars.INITIAL_USER_USERNAME,
initialUserPassword: argv.initialuserpassword || envVars.INITIAL_USER_PASSWORD,
rateLimit: {
enabled: argv['rate-limit-enabled'] || envVars.RATE_LIMIT_ENABLED,
windowMs: envVars.RATE_LIMIT_WINDOW,
max: envVars.RATE_LIMIT_MAX,
},
rateLimit: {
enabled: argv['rate-limit-enabled'] || envVars.RATE_LIMIT_ENABLED,
windowMs: envVars.RATE_LIMIT_WINDOW,
max: envVars.RATE_LIMIT_MAX
},
extensions: {
path: argv.extensions || envVars.EXTENSIONS,
},
extensions: {
path: argv.extensions || envVars.EXTENSIONS
},
startWithStudio: argv.studio || envVars.START_WITH_STUDIO,
};
startWithStudio: argv.studio || envVars.START_WITH_STUDIO
}

View File

@@ -1,26 +1,26 @@
module.exports = {
authEndpoints: ['_users', '_roles', '_roles_permissions', '_users_roles'],
baseTableUrl: '/api/tables',
universalAccessEndpoints: ['/api/auth/change-password'],
authEndpoints: ['_users', '_roles', '_roles_permissions', '_users_roles'],
baseTableUrl: '/api/tables',
universalAccessEndpoints: ['/api/auth/change-password'],
DEFAULT_PAGE_LIMIT: 10,
DEFAULT_PAGE_INDEX: 0,
PASSWORD: {
TOO_WEAK: 'Too weak',
WEAK: 'Weak',
},
DEFAULT_PAGE_LIMIT: 10,
DEFAULT_PAGE_INDEX: 0,
PASSWORD: {
TOO_WEAK: 'Too weak',
WEAK: 'Weak'
},
httpVerbs: {
POST: 'POST',
GET: 'GET',
PUT: 'PUT',
DELETE: 'DELETE',
},
httpVerbs: {
POST: 'POST',
GET: 'GET',
PUT: 'PUT',
DELETE: 'DELETE'
},
httpMethodDefinitions: {
POST: 'CREATE',
GET: 'READ',
PUT: 'UPDATE',
DELETE: 'DELETE',
},
};
httpMethodDefinitions: {
POST: 'CREATE',
GET: 'READ',
PUT: 'UPDATE',
DELETE: 'DELETE'
}
}

View File

@@ -1,6 +1,6 @@
module.exports = {
SALT_ROUNDS: 10,
ACCESS_TOKEN_SUBJECT: 'accessToken',
REFRESH_TOKEN_SUBJECT: 'refreshToken',
REVOKED_REFRESH_TOKENS_REMOVAL_TIME_RANGE: 3 * 24 * 60 * 60 * 1000, // 3 days * 24 hours * 60 minutes * 60 seconds * 1000 milliseconds =
};
SALT_ROUNDS: 10,
ACCESS_TOKEN_SUBJECT: 'accessToken',
REFRESH_TOKEN_SUBJECT: 'refreshToken',
REVOKED_REFRESH_TOKENS_REMOVAL_TIME_RANGE: 3 * 24 * 60 * 60 * 1000 // 3 days * 24 hours * 60 minutes * 60 seconds * 1000 milliseconds =
}

View File

@@ -1,13 +1,13 @@
const dbConstants = require('./tables');
const apiConstants = require('./api');
const constantRoles = require('./roles');
const responseMessages = require('./messages');
const authConstants = require('./auth');
const dbConstants = require('./tables')
const apiConstants = require('./api')
const constantRoles = require('./roles')
const responseMessages = require('./messages')
const authConstants = require('./auth')
module.exports = {
dbConstants,
apiConstants,
constantRoles,
responseMessages,
authConstants,
};
dbConstants,
apiConstants,
constantRoles,
responseMessages,
authConstants
}

View File

@@ -1,43 +1,41 @@
module.exports = {
successMessage: {
SUCCESS: 'Success',
ROW_INSERTED: 'Row Inserted',
PASSWORD_UPDATE_SUCCESS: 'Password updated successfully',
USER_UPDATE_SUCCESS: 'User updated successfully',
INITIAL_USER_CREATED_SUCCESS: 'Initial user created successfully',
LOGOUT_MESSAGE: 'Logout successful',
},
successMessage: {
SUCCESS: 'Success',
ROW_INSERTED: 'Row Inserted',
PASSWORD_UPDATE_SUCCESS: 'Password updated successfully',
USER_UPDATE_SUCCESS: 'User updated successfully',
INITIAL_USER_CREATED_SUCCESS: 'Initial user created successfully',
LOGOUT_MESSAGE: 'Logout successful'
},
errorMessage: {
USERNAME_TAKEN_ERROR: 'This username is taken',
WEAK_PASSWORD_ERROR:
'This password is weak, it should be at least 8 characters long and contain a combination of lowercase letters, uppercase letters, numbers, and special characters',
DEFAULT_ROLE_NOT_CREATED_ERROR:
'Please restart soul so a default role can be created',
INVALID_USERNAME_PASSWORD_ERROR: 'Invalid username or password',
INVALID_REFRESH_TOKEN_ERROR: 'Invalid refresh token',
INVALID_ACCESS_TOKEN_ERROR: 'Invalid access token',
USER_NOT_FOUND_ERROR: 'User not found',
INVALID_CURRENT_PASSWORD_ERROR: 'Invalid current password',
NOT_AUTHORIZED_ERROR: 'Not authorized',
PERMISSION_NOT_DEFINED_ERROR: 'Permission not defined for this role',
ROLE_NOT_FOUND_ERROR: 'Role not found for this user',
AUTH_SET_TO_FALSE_ERROR:
'You can not access this endpoint while AUTH is set to false',
RESERVED_TABLE_NAME_ERROR:
'The table name is reserved. Please choose a different name for the table.',
SERVER_ERROR: 'Server error',
errorMessage: {
USERNAME_TAKEN_ERROR: 'This username is taken',
WEAK_PASSWORD_ERROR:
'This password is weak, it should be at least 8 characters long and contain a combination of lowercase letters, uppercase letters, numbers, and special characters',
DEFAULT_ROLE_NOT_CREATED_ERROR: 'Please restart soul so a default role can be created',
INVALID_USERNAME_PASSWORD_ERROR: 'Invalid username or password',
INVALID_REFRESH_TOKEN_ERROR: 'Invalid refresh token',
INVALID_ACCESS_TOKEN_ERROR: 'Invalid access token',
USER_NOT_FOUND_ERROR: 'User not found',
INVALID_CURRENT_PASSWORD_ERROR: 'Invalid current password',
NOT_AUTHORIZED_ERROR: 'Not authorized',
PERMISSION_NOT_DEFINED_ERROR: 'Permission not defined for this role',
ROLE_NOT_FOUND_ERROR: 'Role not found for this user',
AUTH_SET_TO_FALSE_ERROR: 'You can not access this endpoint while AUTH is set to false',
RESERVED_TABLE_NAME_ERROR:
'The table name is reserved. Please choose a different name for the table.',
SERVER_ERROR: 'Server error',
INITIAL_USER_USERNAME_NOT_PASSED_ERROR:
'Error: You should pass the initial users username either from the CLI with the --iuu or from the environment variable using the INITIAL_USER_USERNAME flag',
INITIAL_USER_PASSWORD_NOT_PASSED_ERROR:
'Error: You should pass the initial users password either from the CLI with the --iup or from the environment variable using the INITIAL_USER_PASSWORD flag',
INITIAL_USER_USERNAME_NOT_PASSED_ERROR:
'Error: You should pass the initial users username either from the CLI with the --iuu or from the environment variable using the INITIAL_USER_USERNAME flag',
INITIAL_USER_PASSWORD_NOT_PASSED_ERROR:
'Error: You should pass the initial users password either from the CLI with the --iup or from the environment variable using the INITIAL_USER_PASSWORD flag',
USERNAME_REQUIRED_ERROR: 'username is required',
PASSWORD_REQUIRED_ERROR: 'password is required',
},
USERNAME_REQUIRED_ERROR: 'username is required',
PASSWORD_REQUIRED_ERROR: 'password is required'
},
infoMessage: {
INITIAL_USER_ALREADY_CREATED: 'Initial user is already created',
},
};
infoMessage: {
INITIAL_USER_ALREADY_CREATED: 'Initial user is already created'
}
}

View File

@@ -1,3 +1,3 @@
module.exports = {
DEFAULT_ROLE: 'default',
};
DEFAULT_ROLE: 'default'
}

View File

@@ -1,54 +1,49 @@
const USERS_TABLE = '_users';
const ROLES_TABLE = '_roles';
const USERS_ROLES_TABLE = '_users_roles';
const ROLES_PERMISSIONS_TABLE = '_roles_permissions';
const REVOKED_REFRESH_TOKENS_TABLE = '_revoked_refresh_tokens';
const USERS_TABLE = '_users'
const ROLES_TABLE = '_roles'
const USERS_ROLES_TABLE = '_users_roles'
const ROLES_PERMISSIONS_TABLE = '_roles_permissions'
const REVOKED_REFRESH_TOKENS_TABLE = '_revoked_refresh_tokens'
module.exports = {
// db table names
USERS_TABLE,
ROLES_TABLE,
USERS_ROLES_TABLE,
ROLES_PERMISSIONS_TABLE,
REVOKED_REFRESH_TOKENS_TABLE,
// db table names
USERS_TABLE,
ROLES_TABLE,
USERS_ROLES_TABLE,
ROLES_PERMISSIONS_TABLE,
REVOKED_REFRESH_TOKENS_TABLE,
reservedTableNames: [
USERS_TABLE,
ROLES_TABLE,
USERS_ROLES_TABLE,
ROLES_PERMISSIONS_TABLE,
],
reservedTableNames: [USERS_TABLE, ROLES_TABLE, USERS_ROLES_TABLE, ROLES_PERMISSIONS_TABLE],
constraints: {
UNIQUE_USERS_ROLE: 'unique_users_role',
UNIQUE_ROLES_TABLE: 'unique_ROLES_TABLE',
},
constraints: {
UNIQUE_USERS_ROLE: 'unique_users_role',
UNIQUE_ROLES_TABLE: 'unique_ROLES_TABLE'
},
tableFields: {
ID: 'id',
tableFields: {
ID: 'id',
// _role fields
ROLE_NAME: 'name',
// _role fields
ROLE_NAME: 'name',
// _user fields
USERNAME: 'username',
HASHED_PASSWORD: 'hashed_password',
SALT: 'salt',
IS_SUPERUSER: 'is_superuser',
// _user fields
USERNAME: 'username',
HASHED_PASSWORD: 'hashed_password',
SALT: 'salt',
IS_SUPERUSER: 'is_superuser',
// _roles_permissions fields
ROLE_ID: 'role_id',
TABLE_NAME: 'table_name',
CREATE: 'create',
READ: 'read',
UPDATE: 'update',
DELETE: 'delete',
// _roles_permissions fields
ROLE_ID: 'role_id',
TABLE_NAME: 'table_name',
CREATE: 'create',
READ: 'read',
UPDATE: 'update',
DELETE: 'delete',
// _users_roles fields
USER_ID: 'user_id',
// _users_roles fields
USER_ID: 'user_id',
//_revoked_refresh_tokens
REFRESH_TOKEN: 'refresh_token',
EXPIRES_AT: 'expires_at',
},
};
//_revoked_refresh_tokens
REFRESH_TOKEN: 'refresh_token',
EXPIRES_AT: 'expires_at'
}
}

View File

@@ -1,372 +1,364 @@
const supertest = require('supertest');
const supertest = require('supertest')
const app = require('../index');
const config = require('../config');
const { generateToken } = require('../utils');
const { testData } = require('../tests/testData');
const app = require('../index')
const config = require('../config')
const { generateToken } = require('../utils')
const { testData } = require('../tests/testData')
const requestWithSupertest = supertest(app);
const requestWithSupertest = supertest(app)
describe('Auth Endpoints', () => {
describe('User Endpoints', () => {
it('POST /tables/_users/rows should register a user', async () => {
const accessToken = await generateToken(
{ username: 'John', userId: 1, isSuperuser: true },
config.tokenSecret,
'1H',
);
describe('User Endpoints', () => {
it('POST /tables/_users/rows should register a user', async () => {
const accessToken = await generateToken(
{ username: 'John', userId: 1, isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.post('/api/tables/_users/rows')
.set('Cookie', [`accessToken=${accessToken}`])
.send({
fields: {
username: testData.users.user1.username,
password: testData.strongPassword,
},
});
const res = await requestWithSupertest
.post('/api/tables/_users/rows')
.set('Cookie', [`accessToken=${accessToken}`])
.send({
fields: {
username: testData.users.user1.username,
password: testData.strongPassword
}
})
expect(res.status).toEqual(201);
expect(res.type).toEqual(expect.stringContaining('json'));
expect(res.status).toEqual(201)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toHaveProperty('message');
expect(res.body.message).toBe('Row Inserted');
expect(res.body).toHaveProperty('message')
expect(res.body.message).toBe('Row Inserted')
expect(res.body).not.toHaveProperty('password');
expect(res.body).not.toHaveProperty('hashed_password');
expect(res.body).not.toHaveProperty('salt');
});
expect(res.body).not.toHaveProperty('password')
expect(res.body).not.toHaveProperty('hashed_password')
expect(res.body).not.toHaveProperty('salt')
})
it('POST /tables/_users/rows should throw 400 error if username is not passed', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('POST /tables/_users/rows should throw 400 error if username is not passed', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.post('/api/tables/_users/rows')
.set('Cookie', [`accessToken=${accessToken}`])
.send({
fields: { password: testData.strongPassword },
});
const res = await requestWithSupertest
.post('/api/tables/_users/rows')
.set('Cookie', [`accessToken=${accessToken}`])
.send({
fields: { password: testData.strongPassword }
})
expect(res.status).toEqual(400);
expect(res.body.message).toBe('username is required');
expect(res.status).toEqual(400)
expect(res.body.message).toBe('username is required')
expect(res.body).not.toHaveProperty('password');
expect(res.body).not.toHaveProperty('hashed_password');
expect(res.body).not.toHaveProperty('salt');
});
expect(res.body).not.toHaveProperty('password')
expect(res.body).not.toHaveProperty('hashed_password')
expect(res.body).not.toHaveProperty('salt')
})
it('POST /tables/_users/rows should throw 400 error if the password is not strong', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('POST /tables/_users/rows should throw 400 error if the password is not strong', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.post('/api/tables/_users/rows')
.set('Cookie', [`accessToken=${accessToken}`])
.send({
fields: {
username: testData.users.user2.username,
password: testData.weakPassword,
},
});
const res = await requestWithSupertest
.post('/api/tables/_users/rows')
.set('Cookie', [`accessToken=${accessToken}`])
.send({
fields: {
username: testData.users.user2.username,
password: testData.weakPassword
}
})
expect(res.status).toEqual(400);
expect(res.body.message).toBe(
'This password is weak, it should be at least 8 characters long and contain a combination of lowercase letters, uppercase letters, numbers, and special characters',
);
expect(res.status).toEqual(400)
expect(res.body.message).toBe(
'This password is weak, it should be at least 8 characters long and contain a combination of lowercase letters, uppercase letters, numbers, and special characters'
)
expect(res.body).not.toHaveProperty('password');
expect(res.body).not.toHaveProperty('hashed_password');
expect(res.body).not.toHaveProperty('salt');
});
expect(res.body).not.toHaveProperty('password')
expect(res.body).not.toHaveProperty('hashed_password')
expect(res.body).not.toHaveProperty('salt')
})
it('POST /tables/_users/rows should throw 409 error if the username is taken', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('POST /tables/_users/rows should throw 409 error if the username is taken', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.post('/api/tables/_users/rows')
.set('Cookie', [`accessToken=${accessToken}`])
.send({
fields: {
username: testData.users.user1.username,
password: testData.strongPassword,
},
});
const res = await requestWithSupertest
.post('/api/tables/_users/rows')
.set('Cookie', [`accessToken=${accessToken}`])
.send({
fields: {
username: testData.users.user1.username,
password: testData.strongPassword
}
})
expect(res.status).toEqual(409);
expect(res.body.message).toBe('This username is taken');
expect(res.status).toEqual(409)
expect(res.body.message).toBe('This username is taken')
expect(res.body).not.toHaveProperty('password');
expect(res.body).not.toHaveProperty('hashed_password');
expect(res.body).not.toHaveProperty('salt');
});
expect(res.body).not.toHaveProperty('password')
expect(res.body).not.toHaveProperty('hashed_password')
expect(res.body).not.toHaveProperty('salt')
})
it('GET /tables/_users/rows should return list of users', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('GET /tables/_users/rows should return list of users', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.get('/api/tables/_users/rows')
.set('Cookie', [`accessToken=${accessToken}`]);
const res = await requestWithSupertest
.get('/api/tables/_users/rows')
.set('Cookie', [`accessToken=${accessToken}`])
expect(res.status).toEqual(200);
expect(res.body.data[0]).toHaveProperty('id');
expect(res.body.data[0]).toHaveProperty('username');
expect(res.body.data[0]).toHaveProperty('is_superuser');
expect(res.body.data[0]).toHaveProperty('createdAt');
expect(res.status).toEqual(200)
expect(res.body.data[0]).toHaveProperty('id')
expect(res.body.data[0]).toHaveProperty('username')
expect(res.body.data[0]).toHaveProperty('is_superuser')
expect(res.body.data[0]).toHaveProperty('createdAt')
expect(res.body.data[0]).not.toHaveProperty('password');
expect(res.body.data[0]).not.toHaveProperty('hashed_password');
expect(res.body.data[0]).not.toHaveProperty('salt');
});
expect(res.body.data[0]).not.toHaveProperty('password')
expect(res.body.data[0]).not.toHaveProperty('hashed_password')
expect(res.body.data[0]).not.toHaveProperty('salt')
})
it('GET /tables/_users/rows/:id should retrive a single user', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('GET /tables/_users/rows/:id should retrive a single user', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.get('/api/tables/_users/rows/1')
.set('Cookie', [`accessToken=${accessToken}`]);
const res = await requestWithSupertest
.get('/api/tables/_users/rows/1')
.set('Cookie', [`accessToken=${accessToken}`])
expect(res.status).toEqual(200);
expect(res.body.data[0]).toHaveProperty('id');
expect(res.body.data[0]).toHaveProperty('username');
expect(res.body.data[0]).toHaveProperty('is_superuser');
expect(res.body.data[0]).toHaveProperty('createdAt');
expect(res.status).toEqual(200)
expect(res.body.data[0]).toHaveProperty('id')
expect(res.body.data[0]).toHaveProperty('username')
expect(res.body.data[0]).toHaveProperty('is_superuser')
expect(res.body.data[0]).toHaveProperty('createdAt')
expect(res.body.data[0]).not.toHaveProperty('password');
expect(res.body.data[0]).not.toHaveProperty('hashed_password');
expect(res.body.data[0]).not.toHaveProperty('salt');
});
expect(res.body.data[0]).not.toHaveProperty('password')
expect(res.body.data[0]).not.toHaveProperty('hashed_password')
expect(res.body.data[0]).not.toHaveProperty('salt')
})
it('PUT /tables/_users/rows/:id should update a user', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('PUT /tables/_users/rows/:id should update a user', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.put('/api/tables/_users/rows/1')
.set('Cookie', [`accessToken=${accessToken}`])
.send({
fields: {
username: testData.users.user3.username,
},
});
const res = await requestWithSupertest
.put('/api/tables/_users/rows/1')
.set('Cookie', [`accessToken=${accessToken}`])
.send({
fields: {
username: testData.users.user3.username
}
})
expect(res.status).toEqual(200);
expect(res.type).toEqual(expect.stringContaining('json'));
expect(res.status).toEqual(200)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toHaveProperty('message');
expect(res.body.message).toBe('Row updated');
expect(res.body).toHaveProperty('message')
expect(res.body.message).toBe('Row updated')
expect(res.body).not.toHaveProperty('password');
expect(res.body).not.toHaveProperty('hashed_password');
expect(res.body).not.toHaveProperty('salt');
});
expect(res.body).not.toHaveProperty('password')
expect(res.body).not.toHaveProperty('hashed_password')
expect(res.body).not.toHaveProperty('salt')
})
it('DELETE /tables/_users/rows/:id should remove a user', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('DELETE /tables/_users/rows/:id should remove a user', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.delete('/api/tables/_users/rows/2')
.set('Cookie', [`accessToken=${accessToken}`]);
const res = await requestWithSupertest
.delete('/api/tables/_users/rows/2')
.set('Cookie', [`accessToken=${accessToken}`])
expect(res.status).toEqual(400);
expect(res.body.message).toBe('FOREIGN KEY constraint failed');
expect(res.status).toEqual(400)
expect(res.body.message).toBe('FOREIGN KEY constraint failed')
expect(res.body).not.toHaveProperty('password');
expect(res.body).not.toHaveProperty('hashed_password');
expect(res.body).not.toHaveProperty('salt');
});
});
expect(res.body).not.toHaveProperty('password')
expect(res.body).not.toHaveProperty('hashed_password')
expect(res.body).not.toHaveProperty('salt')
})
})
describe('Obtain Access Token Endpoint', () => {
it('POST /auth/token/obtain should return an access token and refresh token values and a success message', async () => {
const res = await requestWithSupertest
.post('/api/auth/token/obtain')
.send({
fields: {
username: testData.users.user1.username,
password: testData.strongPassword,
},
});
describe('Obtain Access Token Endpoint', () => {
it('POST /auth/token/obtain should return an access token and refresh token values and a success message', async () => {
const res = await requestWithSupertest.post('/api/auth/token/obtain').send({
fields: {
username: testData.users.user1.username,
password: testData.strongPassword
}
})
expect(res.status).toEqual(201);
expect(res.type).toEqual(expect.stringContaining('json'));
expect(res.body).toHaveProperty('message');
expect(res.body.message).toBe('Success');
expect(res.status).toEqual(201)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toHaveProperty('message')
expect(res.body.message).toBe('Success')
expect(res.headers['set-cookie']).toBeDefined();
expect(res.headers['set-cookie']).toEqual(
expect.arrayContaining([
expect.stringContaining('refreshToken='),
expect.stringContaining('accessToken='),
]),
);
expect(res.headers['set-cookie']).toBeDefined()
expect(res.headers['set-cookie']).toEqual(
expect.arrayContaining([
expect.stringContaining('refreshToken='),
expect.stringContaining('accessToken=')
])
)
expect(res.body).not.toHaveProperty('password');
expect(res.body).not.toHaveProperty('hashed_password');
expect(res.body).not.toHaveProperty('salt');
});
expect(res.body).not.toHaveProperty('password')
expect(res.body).not.toHaveProperty('hashed_password')
expect(res.body).not.toHaveProperty('salt')
})
it('POST /auth/token/obtain should throw a 401 error if the username does not exist in the DB', async () => {
const res = await requestWithSupertest
.post('/api/auth/token/obtain')
.send({
fields: {
username: testData.invalidUsername,
password: testData.strongPassword,
},
});
it('POST /auth/token/obtain should throw a 401 error if the username does not exist in the DB', async () => {
const res = await requestWithSupertest.post('/api/auth/token/obtain').send({
fields: {
username: testData.invalidUsername,
password: testData.strongPassword
}
})
expect(res.status).toEqual(401);
expect(res.type).toEqual(expect.stringContaining('json'));
expect(res.body).toHaveProperty('message');
expect(res.body.message).toBe('Invalid username or password');
expect(res.status).toEqual(401)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toHaveProperty('message')
expect(res.body.message).toBe('Invalid username or password')
expect(res.body).not.toHaveProperty('password');
expect(res.body).not.toHaveProperty('hashed_password');
expect(res.body).not.toHaveProperty('salt');
});
expect(res.body).not.toHaveProperty('password')
expect(res.body).not.toHaveProperty('hashed_password')
expect(res.body).not.toHaveProperty('salt')
})
it('POST /auth/token/obtain should throw a 401 error if the password is invalid', async () => {
const res = await requestWithSupertest
.post('/api/auth/token/obtain')
.send({
fields: {
username: testData.users.user1.username,
password: testData.invalidPassword,
},
});
it('POST /auth/token/obtain should throw a 401 error if the password is invalid', async () => {
const res = await requestWithSupertest.post('/api/auth/token/obtain').send({
fields: {
username: testData.users.user1.username,
password: testData.invalidPassword
}
})
expect(res.status).toEqual(401);
expect(res.type).toEqual(expect.stringContaining('json'));
expect(res.body).toHaveProperty('message');
expect(res.body.message).toBe('Invalid username or password');
expect(res.status).toEqual(401)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toHaveProperty('message')
expect(res.body.message).toBe('Invalid username or password')
expect(res.body).not.toHaveProperty('password');
expect(res.body).not.toHaveProperty('hashed_password');
expect(res.body).not.toHaveProperty('salt');
});
});
expect(res.body).not.toHaveProperty('password')
expect(res.body).not.toHaveProperty('hashed_password')
expect(res.body).not.toHaveProperty('salt')
})
})
describe('Refresh Access Token Endpoint', () => {
it('GET /auth/token/refresh should refresh the access and refresh tokens', async () => {
const refreshToken = await generateToken(
{ username: 'John', userId: 1, isSuperuser: true },
config.tokenSecret,
'1H',
);
describe('Refresh Access Token Endpoint', () => {
it('GET /auth/token/refresh should refresh the access and refresh tokens', async () => {
const refreshToken = await generateToken(
{ username: 'John', userId: 1, isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.get('/api/auth/token/refresh')
.set('Cookie', [`refreshToken=${refreshToken}`]);
const res = await requestWithSupertest
.get('/api/auth/token/refresh')
.set('Cookie', [`refreshToken=${refreshToken}`])
expect(res.status).toEqual(200);
expect(res.type).toEqual(expect.stringContaining('json'));
expect(res.body).toHaveProperty('message');
expect(res.body.message).toBe('Success');
expect(res.status).toEqual(200)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toHaveProperty('message')
expect(res.body.message).toBe('Success')
expect(res.headers['set-cookie']).toBeDefined();
expect(res.headers['set-cookie']).toEqual(
expect.arrayContaining([
expect.stringContaining('refreshToken='),
expect.stringContaining('accessToken='),
]),
);
});
});
expect(res.headers['set-cookie']).toBeDefined()
expect(res.headers['set-cookie']).toEqual(
expect.arrayContaining([
expect.stringContaining('refreshToken='),
expect.stringContaining('accessToken=')
])
)
})
})
describe('Change Password Endpoint', () => {
it('PUT /auth/change-password/ should change a password', async () => {
const accessToken = await generateToken(
{ username: 'John', userId: 2, isSuperuser: true },
config.tokenSecret,
'1H',
);
describe('Change Password Endpoint', () => {
it('PUT /auth/change-password/ should change a password', async () => {
const accessToken = await generateToken(
{ username: 'John', userId: 2, isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.put('/api/auth/change-password')
.set('Cookie', [`accessToken=${accessToken}`])
.send({
fields: {
currentPassword: testData.strongPassword,
newPassword: testData.strongPassword2,
},
});
const res = await requestWithSupertest
.put('/api/auth/change-password')
.set('Cookie', [`accessToken=${accessToken}`])
.send({
fields: {
currentPassword: testData.strongPassword,
newPassword: testData.strongPassword2
}
})
expect(res.status).toEqual(200);
expect(res.type).toEqual(expect.stringContaining('json'));
expect(res.body).toHaveProperty('message');
expect(res.body.message).toBe('Password updated successfully');
expect(res.status).toEqual(200)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toHaveProperty('message')
expect(res.body.message).toBe('Password updated successfully')
expect(res.body).not.toHaveProperty('password');
expect(res.body).not.toHaveProperty('hashed_password');
expect(res.body).not.toHaveProperty('salt');
expect(res.body).not.toHaveProperty('password')
expect(res.body).not.toHaveProperty('hashed_password')
expect(res.body).not.toHaveProperty('salt')
// check if the password is really updated
const res2 = await requestWithSupertest
.post('/api/auth/token/obtain')
.send({
fields: {
username: testData.users.user1.username,
password: testData.strongPassword2,
},
});
// check if the password is really updated
const res2 = await requestWithSupertest.post('/api/auth/token/obtain').send({
fields: {
username: testData.users.user1.username,
password: testData.strongPassword2
}
})
expect(res2.status).toEqual(201);
expect(res2.type).toEqual(expect.stringContaining('json'));
expect(res2.body).toHaveProperty('message');
expect(res2.body.message).toBe('Success');
});
expect(res2.status).toEqual(201)
expect(res2.type).toEqual(expect.stringContaining('json'))
expect(res2.body).toHaveProperty('message')
expect(res2.body.message).toBe('Success')
})
it('PUT /auth/change-password/ should throw 401 error if the current password is not valid', async () => {
const accessToken = await generateToken(
{ username: 'John', userId: 2, isSuperuser: true },
config.tokenSecret,
'1H',
);
it('PUT /auth/change-password/ should throw 401 error if the current password is not valid', async () => {
const accessToken = await generateToken(
{ username: 'John', userId: 2, isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.put('/api/auth/change-password')
.set('Cookie', [`accessToken=${accessToken}`])
.send({
fields: {
currentPassword: testData.invalidPassword,
newPassword: testData.strongPassword2,
},
});
const res = await requestWithSupertest
.put('/api/auth/change-password')
.set('Cookie', [`accessToken=${accessToken}`])
.send({
fields: {
currentPassword: testData.invalidPassword,
newPassword: testData.strongPassword2
}
})
expect(res.status).toEqual(401);
expect(res.type).toEqual(expect.stringContaining('json'));
expect(res.body).toHaveProperty('message');
expect(res.body.message).toBe('Invalid current password');
expect(res.status).toEqual(401)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toHaveProperty('message')
expect(res.body.message).toBe('Invalid current password')
expect(res.body).not.toHaveProperty('password');
expect(res.body).not.toHaveProperty('hashed_password');
expect(res.body).not.toHaveProperty('salt');
});
});
});
expect(res.body).not.toHaveProperty('password')
expect(res.body).not.toHaveProperty('hashed_password')
expect(res.body).not.toHaveProperty('salt')
})
})
})

View File

@@ -1,24 +1,24 @@
const { rowService } = require('../../services');
const { dbConstants } = require('../../constants');
const { rowService } = require('../../services')
const { dbConstants } = require('../../constants')
const { USERS_TABLE } = dbConstants;
const { USERS_TABLE } = dbConstants
const isUsernameTaken = (username) => {
const users = rowService.get({
tableName: USERS_TABLE,
whereString: 'WHERE username=?',
whereStringValues: [username],
});
const users = rowService.get({
tableName: USERS_TABLE,
whereString: 'WHERE username=?',
whereStringValues: [username]
})
return users.length > 0;
};
return users.length > 0
}
const checkAuthConfigs = ({ auth, tokenSecret }) => {
if (auth && !tokenSecret) {
throw new Error(
'You need to provide a token secret either from the CLI or from your environment variables',
);
}
};
if (auth && !tokenSecret) {
throw new Error(
'You need to provide a token secret either from the CLI or from your environment variables'
)
}
}
module.exports = { isUsernameTaken, checkAuthConfigs };
module.exports = { isUsernameTaken, checkAuthConfigs }

View File

@@ -1,6 +1,6 @@
const users = require('./user');
const token = require('./token');
const tables = require('./tables');
const { checkAuthConfigs } = require('./common');
const users = require('./user')
const token = require('./token')
const tables = require('./tables')
const { checkAuthConfigs } = require('./common')
module.exports = { ...users, ...token, ...tables, checkAuthConfigs };
module.exports = { ...users, ...token, ...tables, checkAuthConfigs }

View File

@@ -1,108 +1,97 @@
const { tableService, rowService } = require('../../services');
const { constantRoles, dbConstants } = require('../../constants');
const schema = require('../../db/schema');
const { tableService, rowService } = require('../../services')
const { constantRoles, dbConstants } = require('../../constants')
const schema = require('../../db/schema')
const {
USERS_TABLE,
ROLES_TABLE,
USERS_ROLES_TABLE,
ROLES_PERMISSIONS_TABLE,
REVOKED_REFRESH_TOKENS_TABLE,
constraints,
tableFields,
} = dbConstants;
USERS_TABLE,
ROLES_TABLE,
USERS_ROLES_TABLE,
ROLES_PERMISSIONS_TABLE,
REVOKED_REFRESH_TOKENS_TABLE,
constraints,
tableFields
} = dbConstants
const createDefaultTables = async () => {
let roleId;
let roleId
// check if the default tables are already created
const roleTable = tableService.checkTableExists(ROLES_TABLE);
const usersTable = tableService.checkTableExists(USERS_TABLE);
const rolesPermissionTable = tableService.checkTableExists(
ROLES_PERMISSIONS_TABLE,
);
const usersRolesTable = tableService.checkTableExists(USERS_ROLES_TABLE);
const revokedRefreshTokensTable = tableService.checkTableExists(
REVOKED_REFRESH_TOKENS_TABLE,
);
// check if the default tables are already created
const roleTable = tableService.checkTableExists(ROLES_TABLE)
const usersTable = tableService.checkTableExists(USERS_TABLE)
const rolesPermissionTable = tableService.checkTableExists(ROLES_PERMISSIONS_TABLE)
const usersRolesTable = tableService.checkTableExists(USERS_ROLES_TABLE)
const revokedRefreshTokensTable = tableService.checkTableExists(REVOKED_REFRESH_TOKENS_TABLE)
// create _users table
if (!usersTable) {
tableService.createTable(USERS_TABLE, schema.userSchema);
}
// create _users table
if (!usersTable) {
tableService.createTable(USERS_TABLE, schema.userSchema)
}
// create _users_roles table
if (!usersRolesTable) {
tableService.createTable(
USERS_ROLES_TABLE,
// create _users_roles table
if (!usersRolesTable) {
tableService.createTable(
USERS_ROLES_TABLE,
schema.usersRoleSchema,
{
multipleUniqueConstraints: {
name: constraints.UNIQUE_USERS_ROLE,
fields: [tableFields.USER_ID, tableFields.USER_ID],
},
},
);
}
schema.usersRoleSchema,
{
multipleUniqueConstraints: {
name: constraints.UNIQUE_USERS_ROLE,
fields: [tableFields.USER_ID, tableFields.USER_ID]
}
}
)
}
// create _roles table
if (!roleTable) {
tableService.createTable(ROLES_TABLE, schema.roleSchema);
// create _roles table
if (!roleTable) {
tableService.createTable(ROLES_TABLE, schema.roleSchema)
// create a default role in the _roles table
const role = rowService.save({
tableName: ROLES_TABLE,
fields: { name: constantRoles.DEFAULT_ROLE },
});
roleId = role.lastInsertRowid;
}
// create a default role in the _roles table
const role = rowService.save({
tableName: ROLES_TABLE,
fields: { name: constantRoles.DEFAULT_ROLE }
})
roleId = role.lastInsertRowid
}
// create _roles_permissions table
if (!rolesPermissionTable && roleId) {
tableService.createTable(
ROLES_PERMISSIONS_TABLE,
schema.rolePermissionSchema,
{
multipleUniqueConstraints: {
name: constraints.UNIQUE_ROLES_TABLE,
fields: [tableFields.ROLE_ID, tableFields.TABLE_NAME],
},
},
);
// create _roles_permissions table
if (!rolesPermissionTable && roleId) {
tableService.createTable(ROLES_PERMISSIONS_TABLE, schema.rolePermissionSchema, {
multipleUniqueConstraints: {
name: constraints.UNIQUE_ROLES_TABLE,
fields: [tableFields.ROLE_ID, tableFields.TABLE_NAME]
}
})
// fetch all DB tables
const tables = tableService.listTables();
// fetch all DB tables
const tables = tableService.listTables()
// add permission for the default role (for each db table)
const permissions = [];
for (const table of tables) {
permissions.push({
role_id: roleId,
table_name: table.name,
create: 0,
read: 1,
update: 0,
delete: 0,
});
}
// add permission for the default role (for each db table)
const permissions = []
for (const table of tables) {
permissions.push({
role_id: roleId,
table_name: table.name,
create: 0,
read: 1,
update: 0,
delete: 0
})
}
// store the permissions in the db
rowService.bulkWrite({
tableName: ROLES_PERMISSIONS_TABLE,
fields: permissions,
});
}
// store the permissions in the db
rowService.bulkWrite({
tableName: ROLES_PERMISSIONS_TABLE,
fields: permissions
})
}
// create _revoked_refresh_tokens table
if (!revokedRefreshTokensTable) {
tableService.createTable(
REVOKED_REFRESH_TOKENS_TABLE,
schema.revokedRefreshTokensSchema,
);
}
};
// create _revoked_refresh_tokens table
if (!revokedRefreshTokensTable) {
tableService.createTable(REVOKED_REFRESH_TOKENS_TABLE, schema.revokedRefreshTokensSchema)
}
}
module.exports = {
createDefaultTables,
};
createDefaultTables
}

View File

@@ -1,19 +1,14 @@
const { authService } = require('../../services');
const { responseMessages, authConstants } = require('../../constants');
const config = require('../../config');
const {
comparePasswords,
generateToken,
decodeToken,
toBoolean,
} = require('../../utils');
const { authService } = require('../../services')
const { responseMessages, authConstants } = require('../../constants')
const config = require('../../config')
const { comparePasswords, generateToken, decodeToken, toBoolean } = require('../../utils')
const { successMessage, errorMessage } = responseMessages;
const { successMessage, errorMessage } = responseMessages
const obtainAccessToken = async (req, res) => {
/*
/*
#swagger.tags = ['Auth']
#swagger.summary = 'Obtain Access Token'
#swagger.summary = 'Obtain Access Token'
#swagger.description = 'Endpoint to generate access and refresh tokens'
#swagger.parameters['body'] = {
@@ -24,28 +19,24 @@ const obtainAccessToken = async (req, res) => {
}
*/
// extract payload
const { username, password } = req.body.fields;
// extract payload
const { username, password } = req.body.fields
try {
// check if the username exists in the Db
const users = authService.getUsersByUsername({ username });
try {
// check if the username exists in the Db
const users = authService.getUsersByUsername({ username })
if (users.length <= 0) {
return res
.status(401)
.send({ message: errorMessage.INVALID_USERNAME_PASSWORD_ERROR });
}
if (users.length <= 0) {
return res.status(401).send({ message: errorMessage.INVALID_USERNAME_PASSWORD_ERROR })
}
// check if the password is valid
const user = users[0];
const isMatch = await comparePasswords(password, user.hashed_password);
// check if the password is valid
const user = users[0]
const isMatch = await comparePasswords(password, user.hashed_password)
if (!isMatch) {
return res
.status(401)
.send({ message: errorMessage.INVALID_USERNAME_PASSWORD_ERROR });
/*
if (!isMatch) {
return res.status(401).send({ message: errorMessage.INVALID_USERNAME_PASSWORD_ERROR })
/*
#swagger.responses[401] = {
description: 'Invalid username or password error',
schema: {
@@ -53,61 +44,53 @@ const obtainAccessToken = async (req, res) => {
}
}
*/
}
}
let roleIds;
let roleIds
// if the user is not a superuser get the role and its permission from the DB
if (!toBoolean(user.is_superuser)) {
try {
const roleData = getUsersRoleAndPermission({
userId: user.id,
res,
});
// if the user is not a superuser get the role and its permission from the DB
if (!toBoolean(user.is_superuser)) {
try {
const roleData = getUsersRoleAndPermission({
userId: user.id,
res
})
roleIds = roleData.roleIds;
} catch (err) {
return res
.status(401)
.send({ message: errorMessage.ROLE_NOT_FOUND_ERROR });
}
}
roleIds = roleData.roleIds
} catch (err) {
return res.status(401).send({ message: errorMessage.ROLE_NOT_FOUND_ERROR })
}
}
const payload = {
username: user.username,
userId: user.id,
isSuperuser: user.is_superuser,
roleIds,
};
const payload = {
username: user.username,
userId: user.id,
isSuperuser: user.is_superuser,
roleIds
}
// generate an access token
const accessToken = await generateToken(
{ subject: authConstants.ACCESS_TOKEN_SUBJECT, ...payload },
config.tokenSecret,
config.accessTokenExpirationTime,
);
// generate an access token
const accessToken = await generateToken(
{ subject: authConstants.ACCESS_TOKEN_SUBJECT, ...payload },
config.tokenSecret,
config.accessTokenExpirationTime
)
// generate a refresh token
const refreshToken = await generateToken(
{ subject: authConstants.REFRESH_TOKEN_SUBJECT, ...payload },
config.tokenSecret,
config.refreshTokenExpirationTime,
);
// generate a refresh token
const refreshToken = await generateToken(
{ subject: authConstants.REFRESH_TOKEN_SUBJECT, ...payload },
config.tokenSecret,
config.refreshTokenExpirationTime
)
// set the token in the cookie
let cookieOptions = { httpOnly: true, secure: false, Path: '/' };
res.cookie(authConstants.ACCESS_TOKEN_SUBJECT, accessToken, cookieOptions);
res.cookie(
authConstants.REFRESH_TOKEN_SUBJECT,
refreshToken,
cookieOptions,
);
// set the token in the cookie
let cookieOptions = { httpOnly: true, secure: false, Path: '/' }
res.cookie(authConstants.ACCESS_TOKEN_SUBJECT, accessToken, cookieOptions)
res.cookie(authConstants.REFRESH_TOKEN_SUBJECT, refreshToken, cookieOptions)
res
.status(201)
.send({ message: successMessage.SUCCESS, data: { userId: user.id } });
res.status(201).send({ message: successMessage.SUCCESS, data: { userId: user.id } })
/*
/*
#swagger.responses[201] = {
description: 'Access token and Refresh token generated',
schema: {
@@ -115,45 +98,38 @@ const obtainAccessToken = async (req, res) => {
}
}
*/
} catch (error) {
console.log(error);
return res.status(500).json({
message: errorMessage.SERVER_ERROR,
});
}
};
} catch (error) {
console.log(error)
return res.status(500).json({
message: errorMessage.SERVER_ERROR
})
}
}
const refreshAccessToken = async (req, res) => {
/*
/*
#swagger.tags = ['Auth']
#swagger.summary = 'Refresh Access Token'
#swagger.summary = 'Refresh Access Token'
#swagger.description = 'Endpoint to refresh access and refresh tokens'
*/
const refreshTokenFromCookies = req.cookies.refreshToken;
const refreshTokenFromCookies = req.cookies.refreshToken
try {
// check if the refresh token is revoked
if (isRefreshTokenRevoked({ refreshToken: refreshTokenFromCookies })) {
return res
.status(403)
.send({ message: errorMessage.INVALID_REFRESH_TOKEN_ERROR });
}
try {
// check if the refresh token is revoked
if (isRefreshTokenRevoked({ refreshToken: refreshTokenFromCookies })) {
return res.status(403).send({ message: errorMessage.INVALID_REFRESH_TOKEN_ERROR })
}
// extract the payload from the token and verify it
const payload = await decodeToken(
refreshTokenFromCookies,
config.tokenSecret,
);
// extract the payload from the token and verify it
const payload = await decodeToken(refreshTokenFromCookies, config.tokenSecret)
// find the user
const users = authService.getUsersById({ userId: payload.userId });
// find the user
const users = authService.getUsersById({ userId: payload.userId })
if (users.length <= 0) {
return res
.status(401)
.send({ message: errorMessage.USER_NOT_FOUND_ERROR });
if (users.length <= 0) {
return res.status(401).send({ message: errorMessage.USER_NOT_FOUND_ERROR })
/*
/*
#swagger.responses[401] = {
description: 'User not found error',
schema: {
@@ -161,61 +137,53 @@ const refreshAccessToken = async (req, res) => {
}
}
*/
}
}
let roleIds;
const user = users[0];
let roleIds
const user = users[0]
// if the user is not a superuser get the role and its permission from the DB
if (!toBoolean(user.is_superuser)) {
try {
const roleData = getUsersRoleAndPermission({
userId: user.id,
});
// if the user is not a superuser get the role and its permission from the DB
if (!toBoolean(user.is_superuser)) {
try {
const roleData = getUsersRoleAndPermission({
userId: user.id
})
roleIds = roleData.roleIds;
} catch (err) {
return res
.status(401)
.send({ message: errorMessage.ROLE_NOT_FOUND_ERROR });
}
}
roleIds = roleData.roleIds
} catch (err) {
return res.status(401).send({ message: errorMessage.ROLE_NOT_FOUND_ERROR })
}
}
const newPayload = {
username: user.username,
userId: user.id,
isSuperuser: user.is_superuser,
roleIds,
};
const newPayload = {
username: user.username,
userId: user.id,
isSuperuser: user.is_superuser,
roleIds
}
// generate an access token
const accessToken = await generateToken(
{ subject: authConstants.ACCESS_TOKEN_SUBJECT, ...newPayload },
config.tokenSecret,
config.accessTokenExpirationTime,
);
// generate an access token
const accessToken = await generateToken(
{ subject: authConstants.ACCESS_TOKEN_SUBJECT, ...newPayload },
config.tokenSecret,
config.accessTokenExpirationTime
)
// generate a refresh token
const refreshToken = await generateToken(
{ subject: authConstants.REFRESH_TOKEN_SUBJECT, ...newPayload },
config.tokenSecret,
config.refreshTokenExpirationTime,
);
// generate a refresh token
const refreshToken = await generateToken(
{ subject: authConstants.REFRESH_TOKEN_SUBJECT, ...newPayload },
config.tokenSecret,
config.refreshTokenExpirationTime
)
// set the token in the cookie
let cookieOptions = { httpOnly: true, secure: false, Path: '/' };
res.cookie(authConstants.ACCESS_TOKEN_SUBJECT, accessToken, cookieOptions);
res.cookie(
authConstants.REFRESH_TOKEN_SUBJECT,
refreshToken,
cookieOptions,
);
// set the token in the cookie
let cookieOptions = { httpOnly: true, secure: false, Path: '/' }
res.cookie(authConstants.ACCESS_TOKEN_SUBJECT, accessToken, cookieOptions)
res.cookie(authConstants.REFRESH_TOKEN_SUBJECT, refreshToken, cookieOptions)
res
.status(200)
.send({ message: successMessage.SUCCESS, data: { userId: user.id } });
res.status(200).send({ message: successMessage.SUCCESS, data: { userId: user.id } })
/*
/*
#swagger.responses[200] = {
description: 'Access token refreshed',
schema: {
@@ -223,9 +191,9 @@ const refreshAccessToken = async (req, res) => {
}
}
*/
} catch (error) {
res.status(403).send({ message: errorMessage.INVALID_REFRESH_TOKEN_ERROR });
/*
} catch (error) {
res.status(403).send({ message: errorMessage.INVALID_REFRESH_TOKEN_ERROR })
/*
#swagger.responses[401] = {
description: 'Invalid refresh token error',
schema: {
@@ -233,37 +201,35 @@ const refreshAccessToken = async (req, res) => {
}
}
*/
}
};
}
}
const removeTokens = async (req, res) => {
/*
/*
#swagger.tags = ['Auth']
#swagger.summary = 'Remove Tokens'
#swagger.description = 'Endpoint to remove access and refresh tokens'
*/
const refreshToken = req.cookies.refreshToken;
const refreshToken = req.cookies.refreshToken
try {
// decode the token
const payload = await decodeToken(refreshToken, config.tokenSecret);
try {
// decode the token
const payload = await decodeToken(refreshToken, config.tokenSecret)
// store the refresh token in the _revoked_refresh_tokens table
authService.saveRevokedRefreshToken({
refreshToken,
expiresAt: payload.exp,
});
// store the refresh token in the _revoked_refresh_tokens table
authService.saveRevokedRefreshToken({
refreshToken,
expiresAt: payload.exp
})
// remove the token from the cookie
res.clearCookie(authConstants.ACCESS_TOKEN_SUBJECT);
res.clearCookie(authConstants.REFRESH_TOKEN_SUBJECT);
// remove the token from the cookie
res.clearCookie(authConstants.ACCESS_TOKEN_SUBJECT)
res.clearCookie(authConstants.REFRESH_TOKEN_SUBJECT)
res
.status(200)
.send({ message: responseMessages.successMessage.LOGOUT_MESSAGE });
res.status(200).send({ message: responseMessages.successMessage.LOGOUT_MESSAGE })
/*
/*
#swagger.responses[200] = {
description: 'Tokens Removed',
schema: {
@@ -271,40 +237,40 @@ const removeTokens = async (req, res) => {
}
}
*/
} catch (error) {
res.status(500).send({ message: errorMessage.SERVER_ERROR });
}
};
} catch (error) {
res.status(500).send({ message: errorMessage.SERVER_ERROR })
}
}
const removeRevokedRefreshTokens = () => {
authService.deleteRevokedRefreshTokens({
lookupField: `WHERE expires_at < CURRENT_TIMESTAMP`,
});
};
authService.deleteRevokedRefreshTokens({
lookupField: `WHERE expires_at < CURRENT_TIMESTAMP`
})
}
const getUsersRoleAndPermission = ({ userId }) => {
const userRoles = authService.getUserRoleByUserId({ userId });
const userRoles = authService.getUserRoleByUserId({ userId })
if (userRoles.length <= 0) {
throw new Error(errorMessage.ROLE_NOT_FOUND_ERROR);
}
if (userRoles.length <= 0) {
throw new Error(errorMessage.ROLE_NOT_FOUND_ERROR)
}
const roleIds = userRoles.map((role) => role.role_id);
const roleIds = userRoles.map((role) => role.role_id)
// get the permission of the role
const permissions = authService.getPermissionByRoleIds({ roleIds });
// get the permission of the role
const permissions = authService.getPermissionByRoleIds({ roleIds })
return { userRoles, roleIds, permissions };
};
return { userRoles, roleIds, permissions }
}
const isRefreshTokenRevoked = ({ refreshToken }) => {
const tokens = authService.getRevokedRefreshToken({ refreshToken });
return tokens.length > 0;
};
const tokens = authService.getRevokedRefreshToken({ refreshToken })
return tokens.length > 0
}
module.exports = {
obtainAccessToken,
refreshAccessToken,
removeTokens,
removeRevokedRefreshTokens,
};
obtainAccessToken,
refreshAccessToken,
removeTokens,
removeRevokedRefreshTokens
}

View File

@@ -1,87 +1,75 @@
const { rowService, authService } = require('../../services');
const {
apiConstants,
dbConstants,
responseMessages,
authConstants,
} = require('../../constants');
const config = require('../../config');
const {
hashPassword,
checkPasswordStrength,
comparePasswords,
} = require('../../utils');
const { rowService, authService } = require('../../services')
const { apiConstants, dbConstants, responseMessages, authConstants } = require('../../constants')
const config = require('../../config')
const { hashPassword, checkPasswordStrength, comparePasswords } = require('../../utils')
const { USERS_TABLE, USERS_ROLES_TABLE, tableFields } = dbConstants;
const { USERS_TABLE, USERS_ROLES_TABLE, tableFields } = dbConstants
const { SALT_ROUNDS } = authConstants;
const { SALT_ROUNDS } = authConstants
const { successMessage, errorMessage, infoMessage } = responseMessages;
const { successMessage, errorMessage, infoMessage } = responseMessages
const updateSuperuser = async (fields) => {
const { id, password, is_superuser } = fields;
let newHashedPassword, newSalt;
let fieldsString = '';
const { id, password, is_superuser } = fields
let newHashedPassword, newSalt
let fieldsString = ''
try {
// find the user by using the id field
const users = authService.getUsersById({ userId: id });
try {
// find the user by using the id field
const users = authService.getUsersById({ userId: id })
// abort if the id is invalid
if (users.length === 0) {
console.log(errorMessage.USER_NOT_FOUND_ERROR);
process.exit(1);
}
// abort if the id is invalid
if (users.length === 0) {
console.log(errorMessage.USER_NOT_FOUND_ERROR)
process.exit(1)
}
// check if the is_superuser field is passed
if (is_superuser !== undefined) {
fieldsString = `${tableFields.IS_SUPERUSER} = '${is_superuser}'`;
}
// check if the is_superuser field is passed
if (is_superuser !== undefined) {
fieldsString = `${tableFields.IS_SUPERUSER} = '${is_superuser}'`
}
// if the password is sent from the CLI, update it
if (password) {
// check if the password is weak
if (
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
checkPasswordStrength(password),
)
) {
console.log(errorMessage.WEAK_PASSWORD_ERROR);
process.exit(1);
}
// if the password is sent from the CLI, update it
if (password) {
// check if the password is weak
if (
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
checkPasswordStrength(password)
)
) {
console.log(errorMessage.WEAK_PASSWORD_ERROR)
process.exit(1)
}
//hash the password
const { hashedPassword, salt } = await hashPassword(
password,
SALT_ROUNDS,
);
newHashedPassword = hashedPassword;
newSalt = salt;
//hash the password
const { hashedPassword, salt } = await hashPassword(password, SALT_ROUNDS)
newHashedPassword = hashedPassword
newSalt = salt
fieldsString = `${fieldsString ? fieldsString + ', ' : ''} ${
tableFields.HASHED_PASSWORD
} = '${newHashedPassword}', ${tableFields.SALT} = '${newSalt}'`;
}
fieldsString = `${fieldsString ? fieldsString + ', ' : ''} ${
tableFields.HASHED_PASSWORD
} = '${newHashedPassword}', ${tableFields.SALT} = '${newSalt}'`
}
// update the user
rowService.update({
tableName: USERS_TABLE,
lookupField: tableFields.ID,
fieldsString,
pks: `${id}`,
});
// update the user
rowService.update({
tableName: USERS_TABLE,
lookupField: tableFields.ID,
fieldsString,
pks: `${id}`
})
console.log(successMessage.USER_UPDATE_SUCCESS);
process.exit(1);
} catch (error) {
console.log(error);
}
};
console.log(successMessage.USER_UPDATE_SUCCESS)
process.exit(1)
} catch (error) {
console.log(error)
}
}
const registerUser = async (req, res) => {
/*
/*
#swagger.tags = ['Auth']
#swagger.summary = 'Register User'
#swagger.summary = 'Register User'
#swagger.description = 'Endpoint to signup'
#swagger.parameters['username'] = {
@@ -92,30 +80,24 @@ const registerUser = async (req, res) => {
}
*/
const { username, password, ...optionalFields } = req.body.fields;
const { username, password, ...optionalFields } = req.body.fields
try {
if (!username) {
return res
.status(400)
.send({ message: errorMessage.USERNAME_REQUIRED_ERROR });
}
try {
if (!username) {
return res.status(400).send({ message: errorMessage.USERNAME_REQUIRED_ERROR })
}
if (!password) {
return res
.status(400)
.send({ message: errorMessage.PASSWORD_REQUIRED_ERROR });
}
if (!password) {
return res.status(400).send({ message: errorMessage.PASSWORD_REQUIRED_ERROR })
}
// check if the username is taken
const users = authService.getUsersByUsername({ username });
// check if the username is taken
const users = authService.getUsersByUsername({ username })
if (users.length > 0) {
return res
.status(409)
.send({ message: errorMessage.USERNAME_TAKEN_ERROR });
if (users.length > 0) {
return res.status(409).send({ message: errorMessage.USERNAME_TAKEN_ERROR })
/*
/*
#swagger.responses[409] = {
description: 'Username taken error',
schema: {
@@ -123,19 +105,19 @@ const registerUser = async (req, res) => {
}
}
*/
}
}
// check if the password is weak
if (
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
checkPasswordStrength(password),
)
) {
return res.status(400).send({
message: errorMessage.WEAK_PASSWORD_ERROR,
});
// check if the password is weak
if (
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
checkPasswordStrength(password)
)
) {
return res.status(400).send({
message: errorMessage.WEAK_PASSWORD_ERROR
})
/*
/*
#swagger.responses[400] = {
description: 'Weak password error',
schema: {
@@ -143,31 +125,31 @@ const registerUser = async (req, res) => {
}
}
*/
}
}
// hash the password
const { salt, hashedPassword } = await hashPassword(password, SALT_ROUNDS);
// hash the password
const { salt, hashedPassword } = await hashPassword(password, SALT_ROUNDS)
// create the user
const newUser = rowService.save({
tableName: USERS_TABLE,
fields: {
username,
salt,
hashed_password: hashedPassword,
is_superuser: 'false',
...optionalFields,
},
});
// create the user
const newUser = rowService.save({
tableName: USERS_TABLE,
fields: {
username,
salt,
hashed_password: hashedPassword,
is_superuser: 'false',
...optionalFields
}
})
// find the default role from the DB
const defaultRole = authService.getDefaultRole();
// find the default role from the DB
const defaultRole = authService.getDefaultRole()
if (defaultRole.length <= 0) {
return res.status(500).send({
message: errorMessage.DEFAULT_ROLE_NOT_CREATED_ERROR,
});
/*
if (defaultRole.length <= 0) {
return res.status(500).send({
message: errorMessage.DEFAULT_ROLE_NOT_CREATED_ERROR
})
/*
#swagger.responses[500] = {
description: 'Server error',
schema: {
@@ -175,17 +157,17 @@ const registerUser = async (req, res) => {
}
}
*/
}
}
// create a role for the user
rowService.save({
tableName: USERS_ROLES_TABLE,
fields: { user_id: newUser.lastInsertRowid, role_id: defaultRole[0].id },
});
// create a role for the user
rowService.save({
tableName: USERS_ROLES_TABLE,
fields: { user_id: newUser.lastInsertRowid, role_id: defaultRole[0].id }
})
res.status(201).send({ message: successMessage.ROW_INSERTED });
res.status(201).send({ message: successMessage.ROW_INSERTED })
/*
/*
#swagger.responses[201] = {
description: 'Row inserted',
schema: {
@@ -193,16 +175,16 @@ const registerUser = async (req, res) => {
}
}
*/
} catch (error) {
console.log(error);
res.status(500).send({ message: errorMessage.SERVER_ERROR });
}
};
} catch (error) {
console.log(error)
res.status(500).send({ message: errorMessage.SERVER_ERROR })
}
}
const changePassword = async (req, res) => {
/*
/*
#swagger.tags = ['Auth']
#swagger.summary = 'Change Password'
#swagger.summary = 'Change Password'
#swagger.description = 'Endpoint to change a password'
#swagger.parameters['body'] = {
@@ -215,32 +197,25 @@ const changePassword = async (req, res) => {
}
*/
const userInfo = req.user;
const { currentPassword, newPassword } = req.body.fields;
const userInfo = req.user
const { currentPassword, newPassword } = req.body.fields
try {
// get the user from the Db
const users = authService.getUsersById({ userId: userInfo.userId });
try {
// get the user from the Db
const users = authService.getUsersById({ userId: userInfo.userId })
if (users.length <= 0) {
return res
.status(401)
.send({ message: errorMessage.USER_NOT_FOUND_ERROR });
}
if (users.length <= 0) {
return res.status(401).send({ message: errorMessage.USER_NOT_FOUND_ERROR })
}
const user = users[0];
const user = users[0]
// check if the users current password is valid
const isMatch = await comparePasswords(
currentPassword,
user.hashed_password,
);
// check if the users current password is valid
const isMatch = await comparePasswords(currentPassword, user.hashed_password)
if (!isMatch) {
return res
.status(401)
.send({ message: errorMessage.INVALID_CURRENT_PASSWORD_ERROR });
/*
if (!isMatch) {
return res.status(401).send({ message: errorMessage.INVALID_CURRENT_PASSWORD_ERROR })
/*
#swagger.responses[401] = {
description: 'User not found error',
schema: {
@@ -248,19 +223,19 @@ const changePassword = async (req, res) => {
}
}
*/
}
}
// check if the new password is strong
if (
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
checkPasswordStrength(newPassword),
)
) {
return res.status(400).send({
message: errorMessage.WEAK_PASSWORD_ERROR,
});
// check if the new password is strong
if (
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
checkPasswordStrength(newPassword)
)
) {
return res.status(400).send({
message: errorMessage.WEAK_PASSWORD_ERROR
})
/*
/*
#swagger.responses[400] = {
description: 'Weak password error',
schema: {
@@ -268,31 +243,28 @@ const changePassword = async (req, res) => {
}
}
*/
}
}
// hash the password
const { salt, hashedPassword } = await hashPassword(
newPassword,
SALT_ROUNDS,
);
// hash the password
const { salt, hashedPassword } = await hashPassword(newPassword, SALT_ROUNDS)
user.salt = salt;
user.hashed_password = hashedPassword;
user.salt = salt
user.hashed_password = hashedPassword
// update the user
rowService.update({
tableName: USERS_TABLE,
lookupField: tableFields.ID,
fieldsString: `${tableFields.HASHED_PASSWORD} = '${hashedPassword}', ${tableFields.SALT} = '${salt}'`,
pks: `${user.id}`,
});
// update the user
rowService.update({
tableName: USERS_TABLE,
lookupField: tableFields.ID,
fieldsString: `${tableFields.HASHED_PASSWORD} = '${hashedPassword}', ${tableFields.SALT} = '${salt}'`,
pks: `${user.id}`
})
res.status(200).send({
message: successMessage.PASSWORD_UPDATE_SUCCESS,
data: { id: user.id, username: user.username },
});
res.status(200).send({
message: successMessage.PASSWORD_UPDATE_SUCCESS,
data: { id: user.id, username: user.username }
})
/*
/*
#swagger.responses[200] = {
description: 'Weak password error',
schema: {
@@ -300,96 +272,92 @@ const changePassword = async (req, res) => {
}
}
*/
} catch (error) {
res.status(500).send({ message: errorMessage.SERVER_ERROR });
}
};
} catch (error) {
res.status(500).send({ message: errorMessage.SERVER_ERROR })
}
}
const createInitialUser = async () => {
// extract some fields from the environment variables or from the CLI
const { initialUserUsername: username, initialUserPassword: password } =
config;
// extract some fields from the environment variables or from the CLI
const { initialUserUsername: username, initialUserPassword: password } = config
try {
// check if there are users in the DB
const users = authService.getAllUsers();
try {
// check if there are users in the DB
const users = authService.getAllUsers()
if (users.length <= 0) {
// check if initial users username is passed from the env or CLI
if (!username) {
console.error(errorMessage.INITIAL_USER_USERNAME_NOT_PASSED_ERROR);
process.exit(1);
}
if (users.length <= 0) {
// check if initial users username is passed from the env or CLI
if (!username) {
console.error(errorMessage.INITIAL_USER_USERNAME_NOT_PASSED_ERROR)
process.exit(1)
}
// check if initial users password is passed from the env or CLI
if (!password) {
console.error(errorMessage.INITIAL_USER_PASSWORD_NOT_PASSED_ERROR);
process.exit(1);
}
// check if initial users password is passed from the env or CLI
if (!password) {
console.error(errorMessage.INITIAL_USER_PASSWORD_NOT_PASSED_ERROR)
process.exit(1)
}
// check if the usernmae is taken
const users = authService.getUsersByUsername({ username });
// check if the usernmae is taken
const users = authService.getUsersByUsername({ username })
if (users.length > 0) {
console.error(errorMessage.USERNAME_TAKEN_ERROR);
process.exit(1);
}
if (users.length > 0) {
console.error(errorMessage.USERNAME_TAKEN_ERROR)
process.exit(1)
}
// check if the password is strong
if (
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
checkPasswordStrength(password),
)
) {
console.error(errorMessage.WEAK_PASSWORD_ERROR);
process.exit(1);
}
// check if the password is strong
if (
[apiConstants.PASSWORD.TOO_WEAK, apiConstants.PASSWORD.WEAK].includes(
checkPasswordStrength(password)
)
) {
console.error(errorMessage.WEAK_PASSWORD_ERROR)
process.exit(1)
}
// hash the password
const { hashedPassword, salt } = await hashPassword(
password,
SALT_ROUNDS,
);
// hash the password
const { hashedPassword, salt } = await hashPassword(password, SALT_ROUNDS)
// create the initial user
const { lastInsertRowid: userId } = rowService.save({
tableName: USERS_TABLE,
fields: {
username,
hashed_password: hashedPassword,
salt,
is_superuser: 'false',
},
});
// create the initial user
const { lastInsertRowid: userId } = rowService.save({
tableName: USERS_TABLE,
fields: {
username,
hashed_password: hashedPassword,
salt,
is_superuser: 'false'
}
})
// get the default role from the DB
const roles = authService.getDefaultRole();
// get the default role from the DB
const roles = authService.getDefaultRole()
if (roles.length <= 0) {
console.log(errorMessage.DEFAULT_ROLE_NOT_CREATED_ERROR);
process.exit(1);
}
if (roles.length <= 0) {
console.log(errorMessage.DEFAULT_ROLE_NOT_CREATED_ERROR)
process.exit(1)
}
const defaultRoleId = roles[0].id;
const defaultRoleId = roles[0].id
// create a _users_role for the initial user
rowService.save({
tableName: USERS_ROLES_TABLE,
fields: { user_id: userId, role_id: defaultRoleId },
});
// create a _users_role for the initial user
rowService.save({
tableName: USERS_ROLES_TABLE,
fields: { user_id: userId, role_id: defaultRoleId }
})
console.log(successMessage.INITIAL_USER_CREATED_SUCCESS);
} else {
console.log(infoMessage.INITIAL_USER_ALREADY_CREATED);
}
} catch (error) {
console.log(error);
}
};
console.log(successMessage.INITIAL_USER_CREATED_SUCCESS)
} else {
console.log(infoMessage.INITIAL_USER_ALREADY_CREATED)
}
} catch (error) {
console.log(error)
}
}
module.exports = {
updateSuperuser,
registerUser,
changePassword,
createInitialUser,
};
updateSuperuser,
registerUser,
changePassword,
createInitialUser
}

View File

@@ -1,33 +1,33 @@
const version = require('../../package.json').version;
const version = require('../../package.json').version
// Root endpoint
const root = async (req, res) => {
/*
/*
#swagger.tags = ['Root']
#swagger.summary = 'Timestamp'
#swagger.description = 'Endpoint to return server timestamp'
*/
res.json({
message: 'Soul is running...',
data: {
version,
timestamp: new Date().toISOString(),
},
});
};
res.json({
message: 'Soul is running...',
data: {
version,
timestamp: new Date().toISOString()
}
})
}
const health = async (req, res) => {
/*
/*
#swagger.tags = ['Root']
#swagger.summary = 'Health Check'
#swagger.description = 'Endpoint to return server health status'
*/
res.send('OK');
};
res.send('OK')
}
module.exports = {
root,
health,
};
root,
health
}

View File

@@ -1,24 +1,24 @@
const supertest = require('supertest');
const supertest = require('supertest')
const app = require('../index');
const requestWithSupertest = supertest(app);
const app = require('../index')
const requestWithSupertest = supertest(app)
describe('Root Endpoints', () => {
it('GET / should return server version and timestamp', async () => {
const res = await requestWithSupertest.get('/api');
expect(res.status).toEqual(200);
expect(res.type).toEqual(expect.stringContaining('json'));
expect(res.body).toHaveProperty('message');
expect(res.body).toHaveProperty('data');
expect(res.body.data).toHaveProperty('version');
expect(res.body.data).toHaveProperty('timestamp');
});
});
it('GET / should return server version and timestamp', async () => {
const res = await requestWithSupertest.get('/api')
expect(res.status).toEqual(200)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toHaveProperty('message')
expect(res.body).toHaveProperty('data')
expect(res.body.data).toHaveProperty('version')
expect(res.body.data).toHaveProperty('timestamp')
})
})
describe('Health Endpoints', () => {
it('GET /health should return server version and timestamp', async () => {
const res = await requestWithSupertest.get('/api/health');
expect(res.status).toEqual(200);
expect(res.type).toEqual(expect.stringContaining('text'));
});
});
it('GET /health should return server version and timestamp', async () => {
const res = await requestWithSupertest.get('/api/health')
expect(res.status).toEqual(200)
expect(res.type).toEqual(expect.stringContaining('text'))
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,332 +1,330 @@
const { not } = require('joi');
const supertest = require('supertest');
const { not } = require('joi')
const supertest = require('supertest')
const app = require('../index');
const config = require('../config');
const { generateToken } = require('../utils');
const app = require('../index')
const config = require('../config')
const { generateToken } = require('../utils')
const requestWithSupertest = supertest(app);
const requestWithSupertest = supertest(app)
function queryString(params) {
const queryString = Object.keys(params)
.map((key) => `${key}=${params[key]}`)
.join('&');
const queryString = Object.keys(params)
.map((key) => `${key}=${params[key]}`)
.join('&')
return queryString;
return queryString
}
describe('Rows Endpoints', () => {
it('GET /tables/:name/rows should return a list of all rows', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('GET /tables/:name/rows should return a list of all rows', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.get('/api/tables/users/rows')
.set('Cookie', [`accessToken=${accessToken}`]);
const res = await requestWithSupertest
.get('/api/tables/users/rows')
.set('Cookie', [`accessToken=${accessToken}`])
expect(res.status).toEqual(200);
expect(res.type).toEqual(expect.stringContaining('json'));
expect(res.body).toHaveProperty('data');
expect(res.body.data).toEqual(expect.any(Array));
expect(res.body.data[0]).toHaveProperty('id');
expect(res.body.data[0]).toHaveProperty('firstName');
expect(res.body.data[0]).toHaveProperty('lastName');
});
expect(res.status).toEqual(200)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toHaveProperty('data')
expect(res.body.data).toEqual(expect.any(Array))
expect(res.body.data[0]).toHaveProperty('id')
expect(res.body.data[0]).toHaveProperty('firstName')
expect(res.body.data[0]).toHaveProperty('lastName')
})
it('GET /tables/:name/rows?_limit=8&_schema=firstName,lastName&_ordering:-firstName&_page=2: should query the rows by the provided query params', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('GET /tables/:name/rows?_limit=8&_schema=firstName,lastName&_ordering:-firstName&_page=2: should query the rows by the provided query params', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const params = {
_search: 'a',
_ordering: '-firstName',
_schema: 'firstName,lastName',
_limit: 8,
_page: 2,
};
const query = queryString(params);
const res = await requestWithSupertest
.get(`/api/tables/users/rows?${query}`)
.set('Cookie', [`accessToken=${accessToken}`]);
const params = {
_search: 'a',
_ordering: '-firstName',
_schema: 'firstName,lastName',
_limit: 8,
_page: 2
}
const query = queryString(params)
const res = await requestWithSupertest
.get(`/api/tables/users/rows?${query}`)
.set('Cookie', [`accessToken=${accessToken}`])
expect(res.status).toEqual(200);
expect(res.type).toEqual(expect.stringContaining('json'));
expect(res.body).toHaveProperty('data');
expect(res.body.data).toEqual(expect.any(Array));
expect(res.body.data[0]).toHaveProperty('firstName');
expect(res.body.data[0]).toHaveProperty('lastName');
expect(res.status).toEqual(200)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toHaveProperty('data')
expect(res.body.data).toEqual(expect.any(Array))
expect(res.body.data[0]).toHaveProperty('firstName')
expect(res.body.data[0]).toHaveProperty('lastName')
expect(res.body.next).toEqual(
`/tables/users/rows?${queryString({
...params,
_page: params._page + 1,
}).toString()}`,
);
expect(res.body.next).toEqual(
`/tables/users/rows?${queryString({
...params,
_page: params._page + 1
}).toString()}`
)
expect(res.body.previous).toEqual(
`/tables/users/rows?${queryString({
...params,
_page: params._page - 1,
}).toString()}`,
);
});
expect(res.body.previous).toEqual(
`/tables/users/rows?${queryString({
...params,
_page: params._page - 1
}).toString()}`
)
})
it('GET /tables/:name/rows: should return a null field', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('GET /tables/:name/rows: should return a null field', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.get('/api/tables/users/rows?_filters=firstName__null,lastName__notnull')
.set('Cookie', [`accessToken=${accessToken}`]);
const res = await requestWithSupertest
.get('/api/tables/users/rows?_filters=firstName__null,lastName__notnull')
.set('Cookie', [`accessToken=${accessToken}`])
expect(res.status).toEqual(200);
expect(res.body.data[0].firstName).toBeNull();
expect(res.body.data[0].lastName).not.toBeNull();
});
expect(res.status).toEqual(200)
expect(res.body.data[0].firstName).toBeNull()
expect(res.body.data[0].lastName).not.toBeNull()
})
it('GET /tables/:name/rows: should successfully retrieve users created after 2010-01-01 00:00:00.', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('GET /tables/:name/rows: should successfully retrieve users created after 2010-01-01 00:00:00.', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const date = '2010-01-01 00:00:00';
const res = await requestWithSupertest
.get(`/api/tables/users/rows?_filters=createdAt__gte:${date}`)
.set('Cookie', [`accessToken=${accessToken}`]);
const date = '2010-01-01 00:00:00'
const res = await requestWithSupertest
.get(`/api/tables/users/rows?_filters=createdAt__gte:${date}`)
.set('Cookie', [`accessToken=${accessToken}`])
res.body.data.map((user) => {
const createdAt = new Date(user.createdAt);
const referenceDate = new Date(date);
expect(createdAt.getTime()).toBeGreaterThan(referenceDate.getTime());
});
res.body.data.map((user) => {
const createdAt = new Date(user.createdAt)
const referenceDate = new Date(date)
expect(createdAt.getTime()).toBeGreaterThan(referenceDate.getTime())
})
expect(res.status).toEqual(200);
expect(res.body.data[0]).toHaveProperty('id');
expect(res.body.data[0]).toHaveProperty('firstName');
expect(res.body.data[0]).toHaveProperty('lastName');
expect(res.body.data[0]).toHaveProperty('createdAt');
});
expect(res.status).toEqual(200)
expect(res.body.data[0]).toHaveProperty('id')
expect(res.body.data[0]).toHaveProperty('firstName')
expect(res.body.data[0]).toHaveProperty('lastName')
expect(res.body.data[0]).toHaveProperty('createdAt')
})
it('GET /tables/:name/rows: should successfully retrieve users created before 2008-01-20 00:00:00.', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('GET /tables/:name/rows: should successfully retrieve users created before 2008-01-20 00:00:00.', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const date = '2008-01-20 00:00:00';
const res = await requestWithSupertest
.get(`/api/tables/users/rows?_filters=createdAt__lte:${date}`)
.set('Cookie', [`accessToken=${accessToken}`]);
const date = '2008-01-20 00:00:00'
const res = await requestWithSupertest
.get(`/api/tables/users/rows?_filters=createdAt__lte:${date}`)
.set('Cookie', [`accessToken=${accessToken}`])
res.body.data.map((user) => {
const createdAt = new Date(user.createdAt);
const referenceDate = new Date(date);
expect(createdAt.getTime()).toBeLessThan(referenceDate.getTime());
});
res.body.data.map((user) => {
const createdAt = new Date(user.createdAt)
const referenceDate = new Date(date)
expect(createdAt.getTime()).toBeLessThan(referenceDate.getTime())
})
expect(res.status).toEqual(200);
expect(res.body.data[0]).toHaveProperty('id');
expect(res.body.data[0]).toHaveProperty('firstName');
expect(res.body.data[0]).toHaveProperty('lastName');
expect(res.body.data[0]).toHaveProperty('createdAt');
});
expect(res.status).toEqual(200)
expect(res.body.data[0]).toHaveProperty('id')
expect(res.body.data[0]).toHaveProperty('firstName')
expect(res.body.data[0]).toHaveProperty('lastName')
expect(res.body.data[0]).toHaveProperty('createdAt')
})
it('GET /tables/:name/rows: should successfully retrieve users created at 2013-01-08 00:00:00', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('GET /tables/:name/rows: should successfully retrieve users created at 2013-01-08 00:00:00', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const date = '2013-01-08 00:00:00';
const res = await requestWithSupertest
.get(`/api/tables/users/rows?_filters=createdAt__eq:${date}`)
.set('Cookie', [`accessToken=${accessToken}`]);
const date = '2013-01-08 00:00:00'
const res = await requestWithSupertest
.get(`/api/tables/users/rows?_filters=createdAt__eq:${date}`)
.set('Cookie', [`accessToken=${accessToken}`])
res.body.data.map((user) => {
const createdAt = new Date(user.createdAt);
const referenceDate = new Date(date);
expect(createdAt.getTime()).toEqual(referenceDate.getTime());
});
res.body.data.map((user) => {
const createdAt = new Date(user.createdAt)
const referenceDate = new Date(date)
expect(createdAt.getTime()).toEqual(referenceDate.getTime())
})
expect(res.status).toEqual(200);
expect(res.body.data[0]).toHaveProperty('id');
expect(res.body.data[0]).toHaveProperty('firstName');
expect(res.body.data[0]).toHaveProperty('lastName');
expect(res.body.data[0]).toHaveProperty('createdAt');
});
expect(res.status).toEqual(200)
expect(res.body.data[0]).toHaveProperty('id')
expect(res.body.data[0]).toHaveProperty('firstName')
expect(res.body.data[0]).toHaveProperty('lastName')
expect(res.body.data[0]).toHaveProperty('createdAt')
})
it('GET /tables/:name/rows: should successfully retrieve users created at 2007-01-08 00:00:00', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('GET /tables/:name/rows: should successfully retrieve users created at 2007-01-08 00:00:00', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const date = '2007-01-08 00:00:00';
const res = await requestWithSupertest
.get(`/api/tables/users/rows?_filters=createdAt__eq:${date}`)
.set('Cookie', [`accessToken=${accessToken}`]);
const date = '2007-01-08 00:00:00'
const res = await requestWithSupertest
.get(`/api/tables/users/rows?_filters=createdAt__eq:${date}`)
.set('Cookie', [`accessToken=${accessToken}`])
//There are no users that are created at 2007-01-08 00:00:00 so the API should return empty data
expect(res.body.data).toHaveLength(0);
expect(res.status).toEqual(200);
});
//There are no users that are created at 2007-01-08 00:00:00 so the API should return empty data
expect(res.body.data).toHaveLength(0)
expect(res.status).toEqual(200)
})
it('GET /tables/:name/rows: should successfully retrieve users that are not created at 2021-01-08 00:00:00', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('GET /tables/:name/rows: should successfully retrieve users that are not created at 2021-01-08 00:00:00', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const date = '2021-01-08 00:00:00';
const res = await requestWithSupertest
.get(`/api/tables/users/rows?_filters=createdAt__neq:${date}`)
.set('Cookie', [`accessToken=${accessToken}`]);
const date = '2021-01-08 00:00:00'
const res = await requestWithSupertest
.get(`/api/tables/users/rows?_filters=createdAt__neq:${date}`)
.set('Cookie', [`accessToken=${accessToken}`])
res.body.data.map((user) => {
const createdAt = new Date(user.createdAt);
const referenceDate = new Date(date);
expect(createdAt.getTime()).not.toEqual(referenceDate.getTime());
});
res.body.data.map((user) => {
const createdAt = new Date(user.createdAt)
const referenceDate = new Date(date)
expect(createdAt.getTime()).not.toEqual(referenceDate.getTime())
})
expect(res.status).toEqual(200);
expect(res.body.data[0]).toHaveProperty('id');
expect(res.body.data[0]).toHaveProperty('firstName');
expect(res.body.data[0]).toHaveProperty('lastName');
expect(res.body.data[0]).toHaveProperty('createdAt');
});
expect(res.status).toEqual(200)
expect(res.body.data[0]).toHaveProperty('id')
expect(res.body.data[0]).toHaveProperty('firstName')
expect(res.body.data[0]).toHaveProperty('lastName')
expect(res.body.data[0]).toHaveProperty('createdAt')
})
it('POST /tables/:name/rows should insert a new row and return the lastInsertRowid', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('POST /tables/:name/rows should insert a new row and return the lastInsertRowid', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.post('/api/tables/users/rows')
.set('Cookie', [`accessToken=${accessToken}`])
.send({ fields: { firstName: 'Jane', lastName: 'Doe' } });
const res = await requestWithSupertest
.post('/api/tables/users/rows')
.set('Cookie', [`accessToken=${accessToken}`])
.send({ fields: { firstName: 'Jane', lastName: 'Doe' } })
expect(res.status).toEqual(201);
expect(res.type).toEqual(expect.stringContaining('json'));
expect(res.body).toHaveProperty('data');
});
expect(res.status).toEqual(201)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toHaveProperty('data')
})
it('GET /tables/:name/rows/:pks should return a row by its primary key', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('GET /tables/:name/rows/:pks should return a row by its primary key', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.get('/api/tables/users/rows/1')
.set('Cookie', [`accessToken=${accessToken}`]);
const res = await requestWithSupertest
.get('/api/tables/users/rows/1')
.set('Cookie', [`accessToken=${accessToken}`])
expect(res.status).toEqual(200);
expect(res.type).toEqual(expect.stringContaining('json'));
expect(res.body).toHaveProperty('data');
expect(res.body.data[0]).toHaveProperty('id');
expect(res.body.data[0]).toHaveProperty('firstName');
expect(res.body.data[0]).toHaveProperty('lastName');
});
expect(res.status).toEqual(200)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toHaveProperty('data')
expect(res.body.data[0]).toHaveProperty('id')
expect(res.body.data[0]).toHaveProperty('firstName')
expect(res.body.data[0]).toHaveProperty('lastName')
})
it('PUT /tables/:name/rows/:pks should update a row by its primary key and return the number of changes', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
const res = await requestWithSupertest
.put('/api/tables/users/rows/1')
.set('Cookie', [`accessToken=${accessToken}`])
.send({ fields: { firstName: 'Jane', lastName: 'Doe' } });
expect(res.status).toEqual(200);
expect(res.type).toEqual(expect.stringContaining('json'));
});
it('PUT /tables/:name/rows/:pks should update a row by its primary key and return the number of changes', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.put('/api/tables/users/rows/1')
.set('Cookie', [`accessToken=${accessToken}`])
.send({ fields: { firstName: 'Jane', lastName: 'Doe' } })
expect(res.status).toEqual(200)
expect(res.type).toEqual(expect.stringContaining('json'))
})
it('DELETE /tables/:name/rows/:pks should delete a row by its primary key and return the number of changes', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('DELETE /tables/:name/rows/:pks should delete a row by its primary key and return the number of changes', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.delete('/api/tables/users/rows/1')
.set('Cookie', [`accessToken=${accessToken}`]);
expect(res.status).toEqual(200);
expect(res.type).toEqual(expect.stringContaining('json'));
});
const res = await requestWithSupertest
.delete('/api/tables/users/rows/1')
.set('Cookie', [`accessToken=${accessToken}`])
expect(res.status).toEqual(200)
expect(res.type).toEqual(expect.stringContaining('json'))
})
it('POST /tables/:name/rows should insert a new row if any of the value of the object being inserted is null', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
const res = await requestWithSupertest
.post('/api/tables/users/rows')
.send({
fields: {
firstName: null,
lastName: 'Doe',
email: null,
username: 'Jane',
},
})
.set('Cookie', [`accessToken=${accessToken}`]);
expect(res.status).toEqual(201);
expect(res.type).toEqual(expect.stringContaining('json'));
expect(res.body).toHaveProperty('data');
});
it('POST /tables/:name/rows should insert a new row if any of the value of the object being inserted is null', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.post('/api/tables/users/rows')
.send({
fields: {
firstName: null,
lastName: 'Doe',
email: null,
username: 'Jane'
}
})
.set('Cookie', [`accessToken=${accessToken}`])
expect(res.status).toEqual(201)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toHaveProperty('data')
})
it('GET /tables/:name/rows should return values if any of the IDs from the array match the user ID.', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('GET /tables/:name/rows should return values if any of the IDs from the array match the user ID.', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.get('/api/tables/users/rows?_filters=id:[2,3]')
.set('Cookie', [`accessToken=${accessToken}`]);
expect(res.status).toEqual(200);
expect(res.body).toHaveProperty('data');
expect(res.body.data).toEqual(expect.any(Array));
expect(res.body.data.length).toEqual(2);
});
const res = await requestWithSupertest
.get('/api/tables/users/rows?_filters=id:[2,3]')
.set('Cookie', [`accessToken=${accessToken}`])
expect(res.status).toEqual(200)
expect(res.body).toHaveProperty('data')
expect(res.body.data).toEqual(expect.any(Array))
expect(res.body.data.length).toEqual(2)
})
it('GET /tables/:name/rows should return values if the provided ID matches the user ID.', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('GET /tables/:name/rows should return values if the provided ID matches the user ID.', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.get(
'/api/tables/users/rows?_filters=id:2,firstName:Michael,lastName:Lee',
)
.set('Cookie', [`accessToken=${accessToken}`]);
expect(res.status).toEqual(200);
expect(res.body).toHaveProperty('data');
expect(res.body.data).toEqual(expect.any(Array));
expect(res.body.data.length).toEqual(1);
});
});
const res = await requestWithSupertest
.get('/api/tables/users/rows?_filters=id:2,firstName:Michael,lastName:Lee')
.set('Cookie', [`accessToken=${accessToken}`])
expect(res.status).toEqual(200)
expect(res.body).toHaveProperty('data')
expect(res.body.data).toEqual(expect.any(Array))
expect(res.body.data.length).toEqual(1)
})
})

View File

@@ -1,7 +1,7 @@
const db = require('../db/index');
const db = require('../db/index')
const createTable = async (req, res) => {
/*
/*
#swagger.tags = ['Tables']
#swagger.summary = 'Create Table'
#swagger.description = 'Endpoint to create a table'
@@ -12,104 +12,88 @@ const createTable = async (req, res) => {
schema: { $ref: "#/definitions/CreateTableRequestBody" }
}
*/
const {
name: tableName,
schema,
autoAddCreatedAt = true,
autoAddUpdatedAt = true,
} = req.body;
const { name: tableName, schema, autoAddCreatedAt = true, autoAddUpdatedAt = true } = req.body
let indices = [];
let schemaString = schema
// support name, type, default, not null, unique, primary key, foreign key, index
// e.g. { name: 'id', type: 'INTEGER', primaryKey: true }
let indices = []
let schemaString = schema
// support name, type, default, not null, unique, primary key, foreign key, index
// e.g. { name: 'id', type: 'INTEGER', primaryKey: true }
.map(
({
name,
type,
default: defaultValue,
notNull,
unique,
primaryKey,
foreignKey,
index,
}) => {
let column = `${name} ${type}`;
if (defaultValue) {
column += ` DEFAULT ${defaultValue}`;
}
if (notNull) {
column += ' NOT NULL';
}
if (unique) {
column += ' UNIQUE';
}
if (primaryKey) {
column += ' PRIMARY KEY';
}
if (foreignKey) {
column += ` REFERENCES ${foreignKey.table}(${foreignKey.column})`;
}
if (foreignKey && foreignKey.onDelete) {
column += ` ON DELETE ${foreignKey.onDelete}`;
}
if (foreignKey && foreignKey.onUpdate) {
column += ` ON UPDATE ${foreignKey.onUpdate}`;
}
if (index) {
indices.push(name);
}
.map(({ name, type, default: defaultValue, notNull, unique, primaryKey, foreignKey, index }) => {
let column = `${name} ${type}`
if (defaultValue) {
column += ` DEFAULT ${defaultValue}`
}
if (notNull) {
column += ' NOT NULL'
}
if (unique) {
column += ' UNIQUE'
}
if (primaryKey) {
column += ' PRIMARY KEY'
}
if (foreignKey) {
column += ` REFERENCES ${foreignKey.table}(${foreignKey.column})`
}
if (foreignKey && foreignKey.onDelete) {
column += ` ON DELETE ${foreignKey.onDelete}`
}
if (foreignKey && foreignKey.onUpdate) {
column += ` ON UPDATE ${foreignKey.onUpdate}`
}
if (index) {
indices.push(name)
}
return column;
}
)
.join(', ');
return column
})
.join(', ')
// add id if primary key is not defined
if (!schema.find((field) => field.primaryKey)) {
schemaString = `
// add id if primary key is not defined
if (!schema.find((field) => field.primaryKey)) {
schemaString = `
id INTEGER PRIMARY KEY AUTOINCREMENT,
${schemaString}
`;
}
`
}
// add created at and updated at
if (autoAddCreatedAt) {
schemaString = `
// add created at and updated at
if (autoAddCreatedAt) {
schemaString = `
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
${schemaString}
`;
}
`
}
if (autoAddUpdatedAt) {
schemaString = `
if (autoAddUpdatedAt) {
schemaString = `
updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
${schemaString}
`;
}
`
}
let indicesString = indices
.map((field) => {
return `
let indicesString = indices
.map((field) => {
return `
CREATE INDEX ${tableName}_${field}_index
ON ${tableName} (${field})
`;
})
.join(';');
`
})
.join(';')
const query = `CREATE TABLE ${tableName} (${schemaString})`;
const query = `CREATE TABLE ${tableName} (${schemaString})`
try {
db.prepare(query).run();
try {
db.prepare(query).run()
if (indicesString) {
db.prepare(indicesString).run();
}
if (indicesString) {
db.prepare(indicesString).run()
}
const generatedSchema = db.prepare(`PRAGMA table_info(${tableName})`).all();
const generatedSchema = db.prepare(`PRAGMA table_info(${tableName})`).all()
/*
/*
#swagger.responses[201] = {
description: 'Table created',
schema: {
@@ -117,15 +101,15 @@ const createTable = async (req, res) => {
}
}
*/
res.status(201).json({
message: 'Table created',
data: {
name: tableName,
schema: generatedSchema,
},
});
} catch (error) {
/*
res.status(201).json({
message: 'Table created',
data: {
name: tableName,
schema: generatedSchema
}
})
} catch (error) {
/*
#swagger.responses[400] = {
description: 'Bad request',
schema: {
@@ -133,16 +117,16 @@ const createTable = async (req, res) => {
}
}
*/
res.status(400).json({
message: error.message,
error: error,
});
}
};
res.status(400).json({
message: error.message,
error: error
})
}
}
// Return all tables
const listTables = async (req, res) => {
/*
/*
#swagger.tags = ['Tables']
#swagger.summary = 'List Tables'
#swagger.description = 'Endpoint to list all tables'
@@ -160,44 +144,42 @@ const listTables = async (req, res) => {
description: 'Ordering term'
}
*/
const { _search, _ordering } = req.query;
const { _search, _ordering } = req.query
let query = `SELECT name FROM sqlite_master WHERE type IN ('table', 'view')`;
let query = `SELECT name FROM sqlite_master WHERE type IN ('table', 'view')`
// if search is provided, search the tables
// e.g. ?_search=users
if (_search) {
query += ` AND name LIKE $searchQuery`;
}
// if search is provided, search the tables
// e.g. ?_search=users
if (_search) {
query += ` AND name LIKE $searchQuery`
}
// if ordering is provided, order the tables
// e.g. ?_ordering=name (ascending) or ?_ordering=-name (descending)
if (_ordering) {
query += ` ORDER BY $ordering`;
}
// if ordering is provided, order the tables
// e.g. ?_ordering=name (ascending) or ?_ordering=-name (descending)
if (_ordering) {
query += ` ORDER BY $ordering`
}
try {
const tables = db.prepare(query).all({
searchQuery: `%${_search}%`,
ordering: `${_ordering?.replace('-', '')} ${
_ordering?.startsWith('-') ? 'DESC' : 'ASC'
}`,
});
try {
const tables = db.prepare(query).all({
searchQuery: `%${_search}%`,
ordering: `${_ordering?.replace('-', '')} ${_ordering?.startsWith('-') ? 'DESC' : 'ASC'}`
})
res.json({
data: tables,
});
} catch (error) {
res.status(400).json({
message: error.message,
error: error,
});
}
};
res.json({
data: tables
})
} catch (error) {
res.status(400).json({
message: error.message,
error: error
})
}
}
// TODO: Return the schema of a table by name
const getTableSchema = async (req, res) => {
/*
/*
#swagger.tags = ['Tables']
#swagger.summary = 'Get Table Schema'
#swagger.description = 'Endpoint to get the schema of a table'
@@ -209,25 +191,25 @@ const getTableSchema = async (req, res) => {
}
*/
const { name: tableName } = req.params;
const query = `PRAGMA table_info(${tableName})`;
try {
const schema = db.prepare(query).all();
const { name: tableName } = req.params
const query = `PRAGMA table_info(${tableName})`
try {
const schema = db.prepare(query).all()
res.json({
data: schema,
});
} catch (error) {
res.status(400).json({
message: error.message,
error: error,
});
}
};
res.json({
data: schema
})
} catch (error) {
res.status(400).json({
message: error.message,
error: error
})
}
}
// Delete a table by name
const deleteTable = async (req, res) => {
/*
/*
#swagger.tags = ['Tables']
#swagger.summary = 'Delete Table'
#swagger.description = 'Endpoint to delete a table'
@@ -239,26 +221,26 @@ const deleteTable = async (req, res) => {
}
*/
const { name: tableName } = req.params;
const query = `DROP TABLE ${tableName}`;
try {
const data = db.prepare(query).run();
const { name: tableName } = req.params
const query = `DROP TABLE ${tableName}`
try {
const data = db.prepare(query).run()
res.json({
message: 'Table deleted',
data,
});
} catch (error) {
res.status(400).json({
message: error.message,
error: error,
});
}
};
res.json({
message: 'Table deleted',
data
})
} catch (error) {
res.status(400).json({
message: error.message,
error: error
})
}
}
module.exports = {
listTables,
createTable,
getTableSchema,
deleteTable,
};
listTables,
createTable,
getTableSchema,
deleteTable
}

View File

@@ -1,92 +1,92 @@
const supertest = require('supertest');
const supertest = require('supertest')
const app = require('../index');
const { generateToken } = require('../utils');
const config = require('../config');
const app = require('../index')
const { generateToken } = require('../utils')
const config = require('../config')
const requestWithSupertest = supertest(app);
const requestWithSupertest = supertest(app)
describe('Tables Endpoints', () => {
it('GET /tables should return a list of all tables', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('GET /tables should return a list of all tables', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.get('/api/tables')
.set('Cookie', [`accessToken=${accessToken}`]);
const res = await requestWithSupertest
.get('/api/tables')
.set('Cookie', [`accessToken=${accessToken}`])
expect(res.status).toEqual(200);
expect(res.type).toEqual(expect.stringContaining('json'));
expect(res.body).toHaveProperty('data');
expect(res.body.data).toEqual(expect.any(Array));
expect(res.body.data[0]).toHaveProperty('name');
});
expect(res.status).toEqual(200)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toHaveProperty('data')
expect(res.body.data).toEqual(expect.any(Array))
expect(res.body.data[0]).toHaveProperty('name')
})
it('POST /tables should create a new table and return generated schema', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('POST /tables should create a new table and return generated schema', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.post('/api/tables')
.send({
name: 'pets',
autoAddCreatedAt: true,
autoAddUpdatedAt: false,
schema: [
{
name: 'owner',
type: 'INTEGER',
foreignKey: {
table: 'users',
column: 'id',
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
},
{
name: 'name',
type: 'TEXT',
notNull: true,
},
{
name: 'petId',
unique: true,
type: 'INTEGER',
},
],
})
.set('Cookie', [`accessToken=${accessToken}`]);
const res = await requestWithSupertest
.post('/api/tables')
.send({
name: 'pets',
autoAddCreatedAt: true,
autoAddUpdatedAt: false,
schema: [
{
name: 'owner',
type: 'INTEGER',
foreignKey: {
table: 'users',
column: 'id',
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
}
},
{
name: 'name',
type: 'TEXT',
notNull: true
},
{
name: 'petId',
unique: true,
type: 'INTEGER'
}
]
})
.set('Cookie', [`accessToken=${accessToken}`])
expect(res.status).toEqual(201);
expect(res.type).toEqual(expect.stringContaining('json'));
expect(res.body).toHaveProperty('data');
expect(res.body.data).toHaveProperty('name');
expect(res.body.data).toHaveProperty('schema');
expect(res.body.data.schema).toEqual(expect.any(Array));
expect(res.body.data.schema[0]).toHaveProperty('name');
expect(res.body.data.schema[0]).toHaveProperty('cid');
});
expect(res.status).toEqual(201)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toHaveProperty('data')
expect(res.body.data).toHaveProperty('name')
expect(res.body.data).toHaveProperty('schema')
expect(res.body.data.schema).toEqual(expect.any(Array))
expect(res.body.data.schema[0]).toHaveProperty('name')
expect(res.body.data.schema[0]).toHaveProperty('cid')
})
it('GET /tables/:name should return schema of the table', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H',
);
it('GET /tables/:name should return schema of the table', async () => {
const accessToken = await generateToken(
{ username: 'John', isSuperuser: true },
config.tokenSecret,
'1H'
)
const res = await requestWithSupertest
.get('/api/tables/users')
.set('Cookie', [`accessToken=${accessToken}`]);
const res = await requestWithSupertest
.get('/api/tables/users')
.set('Cookie', [`accessToken=${accessToken}`])
expect(res.status).toEqual(200);
expect(res.type).toEqual(expect.stringContaining('json'));
expect(res.body).toHaveProperty('data');
expect(res.body.data).toEqual(expect.any(Array));
});
});
expect(res.status).toEqual(200)
expect(res.type).toEqual(expect.stringContaining('json'))
expect(res.body).toHaveProperty('data')
expect(res.body.data).toEqual(expect.any(Array))
})
})

View File

@@ -1,26 +1,26 @@
const BS3Database = require('better-sqlite3');
const BS3Database = require('better-sqlite3')
const config = require('../config/index');
const config = require('../config/index')
class Database {
constructor(filename, options) {
this.db = new BS3Database(filename, {
verbose: this.getVerbose(),
...options,
});
}
constructor(filename, options) {
this.db = new BS3Database(filename, {
verbose: this.getVerbose(),
...options
})
}
getVerbose() {
if (config.verbose === 'console') {
return console.log;
} else if (config.verbose === null) {
return null;
}
}
getVerbose() {
if (config.verbose === 'console') {
return console.log
} else if (config.verbose === null) {
return null
}
}
get() {
return this.db;
}
get() {
return this.db
}
}
module.exports = new Database(config.db.filename).get();
module.exports = new Database(config.db.filename).get()

View File

@@ -1,136 +1,136 @@
const { dbConstants } = require('../constants');
const { dbConstants } = require('../constants')
const { tableFields, ROLES_TABLE, USERS_TABLE } = dbConstants;
const { tableFields, ROLES_TABLE, USERS_TABLE } = dbConstants
module.exports = {
roleSchema: [
{
name: tableFields.ROLE_NAME,
type: 'TEXT',
primaryKey: false,
notNull: true,
unique: true,
},
],
roleSchema: [
{
name: tableFields.ROLE_NAME,
type: 'TEXT',
primaryKey: false,
notNull: true,
unique: true
}
],
userSchema: [
{
name: tableFields.USERNAME,
type: 'TEXT',
primaryKey: false,
notNull: true,
unique: true,
},
{
name: tableFields.HASHED_PASSWORD,
type: 'TEXT',
primaryKey: false,
notNull: true,
unique: false,
},
{
name: tableFields.SALT,
type: 'TEXT',
primaryKey: false,
notNull: true,
unique: false,
},
userSchema: [
{
name: tableFields.USERNAME,
type: 'TEXT',
primaryKey: false,
notNull: true,
unique: true
},
{
name: tableFields.HASHED_PASSWORD,
type: 'TEXT',
primaryKey: false,
notNull: true,
unique: false
},
{
name: tableFields.SALT,
type: 'TEXT',
primaryKey: false,
notNull: true,
unique: false
},
{
name: tableFields.IS_SUPERUSER,
type: 'BOOLEAN',
primaryKey: false,
notNull: true,
unique: false,
},
],
{
name: tableFields.IS_SUPERUSER,
type: 'BOOLEAN',
primaryKey: false,
notNull: true,
unique: false
}
],
rolePermissionSchema: [
{
name: tableFields.ROLE_ID,
type: 'NUMERIC',
primaryKey: false,
notNull: true,
unique: false,
foreignKey: { table: ROLES_TABLE, column: tableFields.ID },
},
rolePermissionSchema: [
{
name: tableFields.ROLE_ID,
type: 'NUMERIC',
primaryKey: false,
notNull: true,
unique: false,
foreignKey: { table: ROLES_TABLE, column: tableFields.ID }
},
{
name: tableFields.TABLE_NAME,
type: 'TEXT',
primaryKey: false,
notNull: true,
unique: false,
},
{
name: tableFields.TABLE_NAME,
type: 'TEXT',
primaryKey: false,
notNull: true,
unique: false
},
{
name: tableFields.CREATE,
type: 'BOOLEAN',
primaryKey: false,
notNull: true,
unique: false,
},
{
name: tableFields.CREATE,
type: 'BOOLEAN',
primaryKey: false,
notNull: true,
unique: false
},
{
name: tableFields.READ,
type: 'BOOLEAN',
primaryKey: false,
notNull: true,
unique: false,
},
{
name: tableFields.READ,
type: 'BOOLEAN',
primaryKey: false,
notNull: true,
unique: false
},
{
name: tableFields.UPDATE,
type: 'BOOLEAN',
primaryKey: false,
notNull: true,
unique: false,
},
{
name: tableFields.UPDATE,
type: 'BOOLEAN',
primaryKey: false,
notNull: true,
unique: false
},
{
name: tableFields.DELETE,
type: 'BOOLEAN',
primaryKey: false,
notNull: true,
unique: false,
},
],
{
name: tableFields.DELETE,
type: 'BOOLEAN',
primaryKey: false,
notNull: true,
unique: false
}
],
usersRoleSchema: [
{
name: tableFields.USER_ID,
type: 'NUMERIC',
primaryKey: false,
notNull: true,
unique: false,
foreignKey: { table: USERS_TABLE, column: tableFields.ID },
},
usersRoleSchema: [
{
name: tableFields.USER_ID,
type: 'NUMERIC',
primaryKey: false,
notNull: true,
unique: false,
foreignKey: { table: USERS_TABLE, column: tableFields.ID }
},
{
name: tableFields.ROLE_ID,
type: 'NUMERIC',
primaryKey: false,
notNull: true,
unique: false,
foreignKey: { table: ROLES_TABLE, column: tableFields.ID },
},
],
{
name: tableFields.ROLE_ID,
type: 'NUMERIC',
primaryKey: false,
notNull: true,
unique: false,
foreignKey: { table: ROLES_TABLE, column: tableFields.ID }
}
],
revokedRefreshTokensSchema: [
{
name: tableFields.REFRESH_TOKEN,
type: 'TEXT',
primaryKey: false,
notNull: true,
unique: false,
},
revokedRefreshTokensSchema: [
{
name: tableFields.REFRESH_TOKEN,
type: 'TEXT',
primaryKey: false,
notNull: true,
unique: false
},
{
name: tableFields.EXPIRES_AT,
type: 'NUMERIC',
primaryKey: false,
notNull: true,
unique: false,
},
],
};
{
name: tableFields.EXPIRES_AT,
type: 'NUMERIC',
primaryKey: false,
notNull: true,
unique: false
}
]
}

View File

@@ -1,47 +1,47 @@
const fs = require('fs');
const fs = require('fs')
const { extensions: extensionsConfig } = require('./config');
const { extensions: extensionsConfig } = require('./config')
const { path: extensionsPath } = extensionsConfig;
const { path: extensionsPath } = extensionsConfig
const setupExtensions = async (app, db) => {
if (extensionsPath) {
const extensions = fs.readdirSync(extensionsPath);
extensions.forEach((extension) => {
if (extension === 'api.js') {
const apiExtensions = require(`${extensionsPath}/${extension}`);
if (extensionsPath) {
const extensions = fs.readdirSync(extensionsPath)
extensions.forEach((extension) => {
if (extension === 'api.js') {
const apiExtensions = require(`${extensionsPath}/${extension}`)
console.log('API extensions loaded');
console.log('API extensions loaded')
Object.keys(apiExtensions).forEach((key) => {
const api = apiExtensions[key];
switch (api.method) {
case 'GET':
app.get(api.path, (req, res) => api.handler(req, res, db));
break;
case 'POST':
app.post(api.path, api.handler);
break;
case 'PUT':
app.put(api.path, api.handler);
break;
case 'DELETE':
app.delete(api.path, api.handler);
break;
Object.keys(apiExtensions).forEach((key) => {
const api = apiExtensions[key]
switch (api.method) {
case 'GET':
app.get(api.path, (req, res) => api.handler(req, res, db))
break
case 'POST':
app.post(api.path, api.handler)
break
case 'PUT':
app.put(api.path, api.handler)
break
case 'DELETE':
app.delete(api.path, api.handler)
break
default:
break;
}
console.log(' >', api.path);
});
console.log('\n');
}
});
} else {
console.log('No extensions directory provided');
}
};
default:
break
}
console.log(' >', api.path)
})
console.log('\n')
}
})
} else {
console.log('No extensions directory provided')
}
}
module.exports = {
setupExtensions,
};
setupExtensions
}

View File

@@ -1,117 +1,114 @@
#! /usr/bin/env node
const express = require('express');
const bodyParser = require('body-parser');
const winston = require('winston');
const expressWinston = require('express-winston');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const swaggerUi = require('swagger-ui-express');
const cookieParser = require('cookie-parser');
const express = require('express')
const bodyParser = require('body-parser')
const winston = require('winston')
const expressWinston = require('express-winston')
const cors = require('cors')
const rateLimit = require('express-rate-limit')
const swaggerUi = require('swagger-ui-express')
const cookieParser = require('cookie-parser')
const config = require('./config/index');
const db = require('./db/index');
const config = require('./config/index')
const db = require('./db/index')
const rootRoutes = require('./routes/index');
const tablesRoutes = require('./routes/tables');
const rowsRoutes = require('./routes/rows');
const authRoutes = require('./routes/auth');
const rootRoutes = require('./routes/index')
const tablesRoutes = require('./routes/tables')
const rowsRoutes = require('./routes/rows')
const authRoutes = require('./routes/auth')
const swaggerFile = require('./swagger/swagger.json');
const { setupExtensions } = require('./extensions');
const swaggerFile = require('./swagger/swagger.json')
const { setupExtensions } = require('./extensions')
const {
createDefaultTables,
createInitialUser,
removeRevokedRefreshTokens,
checkAuthConfigs,
} = require('./controllers/auth');
createDefaultTables,
createInitialUser,
removeRevokedRefreshTokens,
checkAuthConfigs
} = require('./controllers/auth')
const { runCLICommands } = require('./commands');
const { authConstants } = require('./constants');
const { runCLICommands } = require('./commands')
const { authConstants } = require('./constants')
const app = express();
const app = express()
app.use(bodyParser.json());
app.use(cookieParser());
app.use(bodyParser.json())
app.use(cookieParser())
// Activate wal mode
db.exec('PRAGMA journal_mode = WAL');
db.exec('PRAGMA journal_mode = WAL')
// Enable CORS
let corsOrigin = config.cors.origin;
let corsOrigin = config.cors.origin
if (corsOrigin.includes('*')) {
corsOrigin = '*';
corsOrigin = '*'
}
const corsOptions = {
origin: corsOrigin,
credentials: true,
allowedHeaders: ['Content-Type', 'Authorization'],
};
origin: corsOrigin,
credentials: true,
allowedHeaders: ['Content-Type', 'Authorization']
}
app.use(cors(corsOptions));
app.use(cors(corsOptions))
// Log requests
if (config.verbose !== null) {
app.use(
expressWinston.logger({
transports: [new winston.transports.Console()],
format: winston.format.combine(
winston.format.colorize(),
app.use(
expressWinston.logger({
transports: [new winston.transports.Console()],
format: winston.format.combine(
winston.format.colorize(),
winston.format.json(),
),
meta: false,
msg: 'HTTP {{req.method}} {{req.url}}',
expressFormat: true,
winston.format.json()
),
meta: false,
msg: 'HTTP {{req.method}} {{req.url}}',
expressFormat: true,
colorize: false,
}),
);
colorize: false
})
)
}
if (config.rateLimit.enabled) {
const limiter = rateLimit({
windowMs: config.rateLimit.windowMs,
max: config.rateLimit.max, // Limit each IP to {max} requests per `window`
standardHeaders: true, // Return rate limit info in the `RateLimit*` headers
legacyHeaders: false, // Disable the `XRateLimit*` headers
});
const limiter = rateLimit({
windowMs: config.rateLimit.windowMs,
max: config.rateLimit.max, // Limit each IP to {max} requests per `window`
standardHeaders: true, // Return rate limit info in the `RateLimit*` headers
legacyHeaders: false // Disable the `XRateLimit*` headers
})
// Apply the rate limiting middleware to all requests
app.use(limiter);
// Apply the rate limiting middleware to all requests
app.use(limiter)
}
// If Auth mode is activated but if the tokenSecret value is undefined then throw an error
checkAuthConfigs({ auth: config.auth, tokenSecret: config.tokenSecret });
checkAuthConfigs({ auth: config.auth, tokenSecret: config.tokenSecret })
// If Auth mode is activated then create auth tables in the DB & create a super user if there are no users in the DB
if (config.auth) {
createDefaultTables();
createInitialUser();
createDefaultTables()
createInitialUser()
} else {
console.warn(
'Warning: Soul is running in open mode without authentication or authorization for API endpoints. Please be aware that your API endpoints will not be secure.',
);
console.warn(
'Warning: Soul is running in open mode without authentication or authorization for API endpoints. Please be aware that your API endpoints will not be secure.'
)
}
// remove revoked refresh tokens every X days
setInterval(
removeRevokedRefreshTokens,
authConstants.REVOKED_REFRESH_TOKENS_REMOVAL_TIME_RANGE,
);
setInterval(removeRevokedRefreshTokens, authConstants.REVOKED_REFRESH_TOKENS_REMOVAL_TIME_RANGE)
// If the user has passed custom CLI commands run the command and exit to avoid running the server
runCLICommands();
runCLICommands()
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerFile));
app.use('/api', rootRoutes);
app.use('/api/tables', tablesRoutes);
app.use('/api/tables', rowsRoutes);
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
app.use('/api', rootRoutes)
app.use('/api/tables', tablesRoutes)
app.use('/api/tables', rowsRoutes)
app.use('/api/auth', authRoutes);
app.use('/api/auth', authRoutes)
setupExtensions(app, db);
setupExtensions(app, db)
module.exports = app;
module.exports = app

View File

@@ -1,101 +1,89 @@
const config = require('../config');
const { registerUser } = require('../controllers/auth');
const {
apiConstants,
dbConstants,
responseMessages,
} = require('../constants/');
const { removeFields } = require('../utils');
const { customValidator } = require('../middlewares/validation');
const schema = require('../schemas/auth');
const config = require('../config')
const { registerUser } = require('../controllers/auth')
const { apiConstants, dbConstants, responseMessages } = require('../constants/')
const { removeFields } = require('../utils')
const { customValidator } = require('../middlewares/validation')
const schema = require('../schemas/auth')
const { httpVerbs } = apiConstants;
const {
reservedTableNames,
USERS_TABLE,
ROLES_PERMISSIONS_TABLE,
tableFields,
} = dbConstants;
const { errorMessage } = responseMessages;
const { httpVerbs } = apiConstants
const { reservedTableNames, USERS_TABLE, ROLES_PERMISSIONS_TABLE, tableFields } = dbConstants
const { errorMessage } = responseMessages
const processRowRequest = async (req, res, next) => {
const resource = req.params.name;
const { method } = req;
const resource = req.params.name
const { method } = req
// If the user sends a request to the auth tables while AUTH is set to false, throw an error
if (apiConstants.authEndpoints.includes(resource) && !config.auth) {
return res.status(403).send({
message: errorMessage.AUTH_SET_TO_FALSE_ERROR,
});
}
// If the user sends a request to the auth tables while AUTH is set to false, throw an error
if (apiConstants.authEndpoints.includes(resource) && !config.auth) {
return res.status(403).send({
message: errorMessage.AUTH_SET_TO_FALSE_ERROR
})
}
// Redirect this request to the registerUser controller => POST /api/tables/_users/rows
if (resource === USERS_TABLE && method === httpVerbs.POST) {
return registerUser(req, res);
}
// Redirect this request to the registerUser controller => POST /api/tables/_users/rows
if (resource === USERS_TABLE && method === httpVerbs.POST) {
return registerUser(req, res)
}
// Remove some fields for this request and check the username field => PUT /api/tables/_users/rows
if (resource === USERS_TABLE && method === httpVerbs.PUT) {
/**
* remove some user fields from the request like (is_superuser, hashed_password, salt).
* NOTE: password can be updated via the /change-password API and superuser status can be only updated from the CLI
*/
removeFields(
[req.body.fields],
[tableFields.SALT, tableFields.IS_SUPERUSER, tableFields.HASHED_PASSWORD],
);
}
// Remove some fields for this request and check the username field => PUT /api/tables/_users/rows
if (resource === USERS_TABLE && method === httpVerbs.PUT) {
/**
* remove some user fields from the request like (is_superuser, hashed_password, salt).
* NOTE: password can be updated via the /change-password API and superuser status can be only updated from the CLI
*/
removeFields(
[req.body.fields],
[tableFields.SALT, tableFields.IS_SUPERUSER, tableFields.HASHED_PASSWORD]
)
}
// Validate fields for the _roles_permission API on POST and PUT requests
if (
resource === ROLES_PERMISSIONS_TABLE &&
(method === httpVerbs.POST || method === httpVerbs.PUT)
) {
const validation = customValidator(schema.updateRolePermissions)(req);
// Validate fields for the _roles_permission API on POST and PUT requests
if (resource === ROLES_PERMISSIONS_TABLE && (method === httpVerbs.POST || method === httpVerbs.PUT)) {
const validation = customValidator(schema.updateRolePermissions)(req)
if (validation.errorStatus) {
return res.status(400).json({
message: validation.message,
error: validation.details,
});
}
}
if (validation.errorStatus) {
return res.status(400).json({
message: validation.message,
error: validation.details
})
}
}
next();
};
next()
}
const processRowResponse = async (req, res, next) => {
// Extract payload data
const resource = req.params.name;
const status = req.response.status;
const payload = req.response.payload;
// Extract payload data
const resource = req.params.name
const status = req.response.status
const payload = req.response.payload
// Remove some fields from the response
if (resource === USERS_TABLE) {
removeFields(payload.data, [tableFields.SALT, tableFields.HASHED_PASSWORD]);
}
// Remove some fields from the response
if (resource === USERS_TABLE) {
removeFields(payload.data, [tableFields.SALT, tableFields.HASHED_PASSWORD])
}
res.status(status).send(payload);
next();
};
res.status(status).send(payload)
next()
}
const processTableRequest = async (req, res, next) => {
const { method, body, baseUrl } = req;
const { method, body, baseUrl } = req
// if the user tries to create a table with the reserved table names throw an error. Request => POST /api/tables
if (baseUrl === apiConstants.baseTableUrl && method === httpVerbs.POST) {
if (reservedTableNames.includes(body.name)) {
return res.status(409).send({
message: errorMessage.RESERVED_TABLE_NAME_ERROR,
});
}
}
// if the user tries to create a table with the reserved table names throw an error. Request => POST /api/tables
if (baseUrl === apiConstants.baseTableUrl && method === httpVerbs.POST) {
if (reservedTableNames.includes(body.name)) {
return res.status(409).send({
message: errorMessage.RESERVED_TABLE_NAME_ERROR
})
}
}
next();
};
next()
}
module.exports = {
processRowRequest,
processRowResponse,
processTableRequest,
};
processRowRequest,
processRowResponse,
processTableRequest
}

View File

@@ -1,90 +1,76 @@
const config = require('../config');
const { decodeToken, toBoolean } = require('../utils/index');
const { apiConstants, responseMessages } = require('../constants');
const { authService } = require('../services');
const config = require('../config')
const { decodeToken, toBoolean } = require('../utils/index')
const { apiConstants, responseMessages } = require('../constants')
const { authService } = require('../services')
const { errorMessage } = responseMessages;
const { errorMessage } = responseMessages
const hasAccess = async (req, res, next) => {
let payload;
const { name: tableName } = req.params;
const verb = req.method;
const originalURL = req.originalUrl;
let payload
const { name: tableName } = req.params
const verb = req.method
const originalURL = req.originalUrl
try {
if (config.auth) {
// extract the payload from the token and verify it
try {
payload = await decodeToken(
req.cookies.accessToken,
config.tokenSecret,
);
req.user = payload;
} catch (error) {
return res
.status(401)
.send({ message: errorMessage.INVALID_ACCESS_TOKEN_ERROR });
}
try {
if (config.auth) {
// extract the payload from the token and verify it
try {
payload = await decodeToken(req.cookies.accessToken, config.tokenSecret)
req.user = payload
} catch (error) {
return res.status(401).send({ message: errorMessage.INVALID_ACCESS_TOKEN_ERROR })
}
// if the user is a super_user, allow access on the resource
if (toBoolean(payload.isSuperuser)) {
return next();
}
// if the user is a super_user, allow access on the resource
if (toBoolean(payload.isSuperuser)) {
return next()
}
// if the endpoint is set to be accessed by any user regardless of there roles, then allow access
if (apiConstants.universalAccessEndpoints.includes(originalURL)) {
return next();
}
// if the endpoint is set to be accessed by any user regardless of there roles, then allow access
if (apiConstants.universalAccessEndpoints.includes(originalURL)) {
return next()
}
// if table_name is not passed from the router throw unauthorized error
if (!tableName) {
return res
.status(403)
.send({ message: errorMessage.NOT_AUTHORIZED_ERROR });
}
// if table_name is not passed from the router throw unauthorized error
if (!tableName) {
return res.status(403).send({ message: errorMessage.NOT_AUTHORIZED_ERROR })
}
// if the user is not a super user, fetch the permission of the user from the DB
const rolePermissions = authService.getPermissionByRoleIds({
roleIds: payload.roleIds,
});
// if the user is not a super user, fetch the permission of the user from the DB
const rolePermissions = authService.getPermissionByRoleIds({
roleIds: payload.roleIds
})
const resourcePermission = rolePermissions.filter(
(row) => row.table_name === tableName,
);
const resourcePermission = rolePermissions.filter((row) => row.table_name === tableName)
if (resourcePermission.length <= 0) {
return res
.status(403)
.send({ message: errorMessage.PERMISSION_NOT_DEFINED_ERROR });
}
if (resourcePermission.length <= 0) {
return res.status(403).send({ message: errorMessage.PERMISSION_NOT_DEFINED_ERROR })
}
// If the user has permission on the table in at least in one of the roles then allow access on the table
let hasPermission = false;
// If the user has permission on the table in at least in one of the roles then allow access on the table
let hasPermission = false
resourcePermission.some((resource) => {
const httpMethod =
apiConstants.httpMethodDefinitions[verb].toLowerCase();
resourcePermission.some((resource) => {
const httpMethod = apiConstants.httpMethodDefinitions[verb].toLowerCase()
if (toBoolean(resource[httpMethod])) {
hasPermission = true;
return true;
}
});
if (toBoolean(resource[httpMethod])) {
hasPermission = true
return true
}
})
if (hasPermission) {
next();
} else {
return res
.status(403)
.send({ message: errorMessage.NOT_AUTHORIZED_ERROR });
}
} else {
next();
}
} catch (error) {
console.log(error);
res.status(401).send({ message: error.message });
}
};
if (hasPermission) {
next()
} else {
return res.status(403).send({ message: errorMessage.NOT_AUTHORIZED_ERROR })
}
} else {
next()
}
} catch (error) {
console.log(error)
res.status(401).send({ message: error.message })
}
}
module.exports = { hasAccess };
module.exports = { hasAccess }

View File

@@ -1,18 +1,18 @@
const { websocketSubscribers } = require('../websocket');
const { websocketSubscribers } = require('../websocket')
const broadcast = (req) => {
const data = req.broadcast;
const { name: tableName } = req.params;
const data = req.broadcast
const { name: tableName } = req.params
const subscribers = websocketSubscribers?.get(tableName);
const subscribers = websocketSubscribers?.get(tableName)
if (subscribers) {
subscribers.forEach(({ ws }) => {
ws.send(JSON.stringify(data));
});
}
};
if (subscribers) {
subscribers.forEach(({ ws }) => {
ws.send(JSON.stringify(data))
})
}
}
module.exports = {
broadcast,
};
broadcast
}

View File

@@ -1,13 +1,13 @@
// including the method, url, status code and response time
const logger = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const delta = Date.now() - start;
console.log(`${req.method} ${req.url} ${res.statusCode} ${delta}ms`);
});
next();
};
const start = Date.now()
res.on('finish', () => {
const delta = Date.now() - start
console.log(`${req.method} ${req.url} ${res.statusCode} ${delta}ms`)
})
next()
}
module.exports = {
logger,
};
logger
}

View File

@@ -1,42 +1,42 @@
const validator = (schema) => (req, res, next) => {
const { body, params, query, cookies } = req;
const data = { body, params, query, cookies };
const { body, params, query, cookies } = req
const data = { body, params, query, cookies }
const { value, error } = schema.validate(data);
const { value, error } = schema.validate(data)
if (error) {
res.status(400).json({
message: error.message,
error: error.details,
});
} else {
req.body = value.body;
req.params = value.params;
req.query = value.query;
req.cookies = value.cookies;
if (error) {
res.status(400).json({
message: error.message,
error: error.details
})
} else {
req.body = value.body
req.params = value.params
req.query = value.query
req.cookies = value.cookies
next();
}
};
next()
}
}
const customValidator = (schema) => (req) => {
const response = { errorStatus: false, message: '', error: '' };
const response = { errorStatus: false, message: '', error: '' }
const { body, params, query, cookies } = req;
const data = { body, params, query, cookies };
const { body, params, query, cookies } = req
const data = { body, params, query, cookies }
const { error } = schema.validate(data);
const { error } = schema.validate(data)
if (error) {
response.errorStatus = true;
response.message = error.message;
response.error = error.details;
}
if (error) {
response.errorStatus = true
response.message = error.message
response.error = error.details
}
return response;
};
return response
}
module.exports = {
validator,
customValidator,
};
validator,
customValidator
}

View File

@@ -1,35 +1,18 @@
const express = require('express');
const express = require('express')
const controllers = require('../controllers/auth');
const { validator } = require('../middlewares/validation');
const schema = require('../schemas/auth');
const { hasAccess } = require('../middlewares/auth');
const controllers = require('../controllers/auth')
const { validator } = require('../middlewares/validation')
const schema = require('../schemas/auth')
const { hasAccess } = require('../middlewares/auth')
const router = express.Router();
const router = express.Router()
router.post(
'/token/obtain',
validator(schema.obtainAccessToken),
controllers.obtainAccessToken,
);
router.post('/token/obtain', validator(schema.obtainAccessToken), controllers.obtainAccessToken)
router.get(
'/token/refresh',
validator(schema.refreshAccessToken),
controllers.refreshAccessToken,
);
router.get('/token/refresh', validator(schema.refreshAccessToken), controllers.refreshAccessToken)
router.put(
'/change-password',
hasAccess,
validator(schema.changePassword),
controllers.changePassword,
);
router.put('/change-password', hasAccess, validator(schema.changePassword), controllers.changePassword)
router.get(
'/logout',
validator(schema.removeAccessTokens),
controllers.removeTokens,
);
router.get('/logout', validator(schema.removeAccessTokens), controllers.removeTokens)
module.exports = router;
module.exports = router

View File

@@ -1,10 +1,10 @@
const express = require('express');
const express = require('express')
const controllers = require('../controllers/index');
const controllers = require('../controllers/index')
const router = express.Router();
const router = express.Router()
router.get('/', controllers.root);
router.get('/health', controllers.health);
router.get('/', controllers.root)
router.get('/health', controllers.health)
module.exports = router;
module.exports = router

View File

@@ -1,51 +1,51 @@
const express = require('express');
const express = require('express')
const controllers = require('../controllers/rows');
const { broadcast } = require('../middlewares/broadcast');
const { validator } = require('../middlewares/validation');
const { processRowRequest, processRowResponse } = require('../middlewares/api');
const { hasAccess } = require('../middlewares/auth');
const schema = require('../schemas/rows');
const controllers = require('../controllers/rows')
const { broadcast } = require('../middlewares/broadcast')
const { validator } = require('../middlewares/validation')
const { processRowRequest, processRowResponse } = require('../middlewares/api')
const { hasAccess } = require('../middlewares/auth')
const schema = require('../schemas/rows')
const router = express.Router();
const router = express.Router()
router.get(
'/:name/rows',
hasAccess,
validator(schema.listTableRows),
processRowRequest,
controllers.listTableRows,
processRowResponse,
);
'/:name/rows',
hasAccess,
validator(schema.listTableRows),
processRowRequest,
controllers.listTableRows,
processRowResponse
)
router.post(
'/:name/rows',
hasAccess,
validator(schema.insertRowInTable),
processRowRequest,
controllers.insertRowInTable,
broadcast,
);
'/:name/rows',
hasAccess,
validator(schema.insertRowInTable),
processRowRequest,
controllers.insertRowInTable,
broadcast
)
router.get(
'/:name/rows/:pks',
hasAccess,
validator(schema.getRowInTableByPK),
controllers.getRowInTableByPK,
processRowResponse,
);
'/:name/rows/:pks',
hasAccess,
validator(schema.getRowInTableByPK),
controllers.getRowInTableByPK,
processRowResponse
)
router.put(
'/:name/rows/:pks',
hasAccess,
validator(schema.updateRowInTableByPK),
processRowRequest,
controllers.updateRowInTableByPK,
broadcast,
);
'/:name/rows/:pks',
hasAccess,
validator(schema.updateRowInTableByPK),
processRowRequest,
controllers.updateRowInTableByPK,
broadcast
)
router.delete(
'/:name/rows/:pks',
hasAccess,
validator(schema.deleteRowInTableByPK),
controllers.deleteRowInTableByPK,
broadcast,
);
'/:name/rows/:pks',
hasAccess,
validator(schema.deleteRowInTableByPK),
controllers.deleteRowInTableByPK,
broadcast
)
module.exports = router;
module.exports = router

View File

@@ -1,40 +1,19 @@
const express = require('express');
const express = require('express')
const controllers = require('../controllers/tables');
const { validator } = require('../middlewares/validation');
const schema = require('../schemas/tables');
const { hasAccess } = require('../middlewares/auth');
const { processTableRequest } = require('../middlewares/api');
const controllers = require('../controllers/tables')
const { validator } = require('../middlewares/validation')
const schema = require('../schemas/tables')
const { hasAccess } = require('../middlewares/auth')
const { processTableRequest } = require('../middlewares/api')
const router = express.Router();
const router = express.Router()
router.get(
'/',
hasAccess,
validator(schema.listTables),
controllers.listTables,
);
router.get('/', hasAccess, validator(schema.listTables), controllers.listTables)
router.post(
'/',
processTableRequest,
hasAccess,
validator(schema.createTable),
controllers.createTable,
);
router.post('/', processTableRequest, hasAccess, validator(schema.createTable), controllers.createTable)
router.get(
'/:name',
hasAccess,
validator(schema.getTableSchema),
controllers.getTableSchema,
);
router.get('/:name', hasAccess, validator(schema.getTableSchema), controllers.getTableSchema)
router.delete(
'/:name',
hasAccess,
validator(schema.deleteTable),
controllers.deleteTable,
);
router.delete('/:name', hasAccess, validator(schema.deleteTable), controllers.deleteTable)
module.exports = router;
module.exports = router

View File

@@ -1,109 +1,109 @@
const Joi = require('joi');
const Joi = require('joi')
const obtainAccessToken = Joi.object({
query: Joi.object().required(),
params: Joi.object({}).required(),
query: Joi.object().required(),
params: Joi.object({}).required(),
body: Joi.object({
fields: Joi.object({
username: Joi.string().required(),
password: Joi.string().required(),
}).required(),
}).required(),
body: Joi.object({
fields: Joi.object({
username: Joi.string().required(),
password: Joi.string().required()
}).required()
}).required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}).unknown(true),
});
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional()
}).unknown(true)
})
const refreshAccessToken = Joi.object({
query: Joi.object().required(),
params: Joi.object({}).required(),
body: Joi.object({}).required(),
cookies: Joi.object({
refreshToken: Joi.string().required(),
accessToken: Joi.string().optional(),
})
.unknown(true)
.required(),
});
query: Joi.object().required(),
params: Joi.object({}).required(),
body: Joi.object({}).required(),
cookies: Joi.object({
refreshToken: Joi.string().required(),
accessToken: Joi.string().optional()
})
.unknown(true)
.required()
})
const changePassword = Joi.object({
query: Joi.object().required(),
params: Joi.object().required(),
query: Joi.object().required(),
params: Joi.object().required(),
body: Joi.object({
fields: Joi.object({
currentPassword: Joi.string().required(),
newPassword: Joi.string().required(),
}).required(),
}).required(),
cookies: Joi.object({
accessToken: Joi.string().required(),
refreshToken: Joi.string().optional(),
})
.unknown(true)
.required(),
});
body: Joi.object({
fields: Joi.object({
currentPassword: Joi.string().required(),
newPassword: Joi.string().required()
}).required()
}).required(),
cookies: Joi.object({
accessToken: Joi.string().required(),
refreshToken: Joi.string().optional()
})
.unknown(true)
.required()
})
const registerUser = Joi.object({
query: Joi.object().required(),
params: Joi.object({}).required(),
body: Joi.object({
fields: Joi.object({
username: Joi.string().required(),
password: Joi.string().required(),
}).required(),
}).required(),
query: Joi.object().required(),
params: Joi.object({}).required(),
body: Joi.object({
fields: Joi.object({
username: Joi.string().required(),
password: Joi.string().required()
}).required()
}).required(),
cookies: Joi.object({
accessToken: Joi.string().required(),
refreshToken: Joi.string().optional(),
})
.unknown(true)
.required(),
});
cookies: Joi.object({
accessToken: Joi.string().required(),
refreshToken: Joi.string().optional()
})
.unknown(true)
.required()
})
const updateRolePermissions = Joi.object({
query: Joi.object({}).required(),
params: Joi.object({ name: Joi.string(), pks: Joi.string() }).required(),
body: Joi.object({
fields: Joi.object({
role_id: Joi.number().required(),
table_name: Joi.string().required(),
create: Joi.number().valid(0, 1).required(),
read: Joi.number().valid(0, 1).required(),
update: Joi.number().valid(0, 1).required(),
delete: Joi.number().valid(0, 1).required(),
}).required(),
}).required(),
query: Joi.object({}).required(),
params: Joi.object({ name: Joi.string(), pks: Joi.string() }).required(),
body: Joi.object({
fields: Joi.object({
role_id: Joi.number().required(),
table_name: Joi.string().required(),
create: Joi.number().valid(0, 1).required(),
read: Joi.number().valid(0, 1).required(),
update: Joi.number().valid(0, 1).required(),
delete: Joi.number().valid(0, 1).required()
}).required()
}).required(),
cookies: Joi.object({
accessToken: Joi.string().required(),
refreshToken: Joi.string().optional(),
})
.unknown(true)
.required(),
});
cookies: Joi.object({
accessToken: Joi.string().required(),
refreshToken: Joi.string().optional()
})
.unknown(true)
.required()
})
const removeAccessTokens = Joi.object({
query: Joi.object().required(),
params: Joi.object({}).required(),
body: Joi.object({}).required(),
cookies: Joi.object({
refreshToken: Joi.string().required(),
accessToken: Joi.string().required(),
})
.unknown(true)
.required(),
});
query: Joi.object().required(),
params: Joi.object({}).required(),
body: Joi.object({}).required(),
cookies: Joi.object({
refreshToken: Joi.string().required(),
accessToken: Joi.string().required()
})
.unknown(true)
.required()
})
module.exports = {
obtainAccessToken,
refreshAccessToken,
changePassword,
registerUser,
updateRolePermissions,
removeAccessTokens,
};
obtainAccessToken,
refreshAccessToken,
changePassword,
registerUser,
updateRolePermissions,
removeAccessTokens
}

View File

@@ -1,28 +1,28 @@
const Joi = require('joi');
const Joi = require('joi')
const transaction = Joi.object({
query: Joi.object().required(),
params: Joi.object().required(),
body: Joi.object({
transaction: Joi.array()
.items(
Joi.object({
statement: Joi.string().required(),
values: Joi.object().required(),
}),
Joi.object({
query: Joi.string().required(),
}),
)
.required(),
}).required(),
query: Joi.object().required(),
params: Joi.object().required(),
body: Joi.object({
transaction: Joi.array()
.items(
Joi.object({
statement: Joi.string().required(),
values: Joi.object().required()
}),
Joi.object({
query: Joi.string().required()
})
)
.required()
}).required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional()
})
})
module.exports = {
transaction,
};
transaction
}

View File

@@ -1,117 +1,117 @@
const Joi = require('joi');
const Joi = require('joi')
const listTableRows = Joi.object({
query: Joi.object({
_page: Joi.number().integer().min(1).default(1),
_limit: Joi.number().integer().min(1).default(10),
_search: Joi.string(),
_ordering: Joi.string().regex(/^[\w-]+$/),
_schema: Joi.string(),
_extend: Joi.string(),
_filters: Joi.string(),
}).required(),
params: Joi.object({
name: Joi.string(),
}).required(),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
query: Joi.object({
_page: Joi.number().integer().min(1).default(1),
_limit: Joi.number().integer().min(1).default(10),
_search: Joi.string(),
_ordering: Joi.string().regex(/^[\w-]+$/),
_schema: Joi.string(),
_extend: Joi.string(),
_filters: Joi.string()
}).required(),
params: Joi.object({
name: Joi.string()
}).required(),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional()
})
})
const insertRowInTable = Joi.object({
query: Joi.object().required(),
params: Joi.object({
name: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30)
.required(),
}).required(),
body: Joi.object({
fields: Joi.object().required(),
}).required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
query: Joi.object().required(),
params: Joi.object({
name: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30)
.required()
}).required(),
body: Joi.object({
fields: Joi.object().required()
}).required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional()
})
})
const getRowInTableByPK = Joi.object({
query: Joi.object({
_lookup_field: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30),
_schema: Joi.string(),
_extend: Joi.string(),
}).required(),
params: Joi.object({
name: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30)
.required(),
pks: Joi.string().required(),
}).required(),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
query: Joi.object({
_lookup_field: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30),
_schema: Joi.string(),
_extend: Joi.string()
}).required(),
params: Joi.object({
name: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30)
.required(),
pks: Joi.string().required()
}).required(),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional()
})
})
const updateRowInTableByPK = Joi.object({
query: Joi.object({
_lookup_field: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30),
}).required(),
params: Joi.object({
name: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30)
.required(),
pks: Joi.string().required(),
}).required(),
body: Joi.object({
fields: Joi.object().required(),
}).required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
query: Joi.object({
_lookup_field: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30)
}).required(),
params: Joi.object({
name: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30)
.required(),
pks: Joi.string().required()
}).required(),
body: Joi.object({
fields: Joi.object().required()
}).required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional()
})
})
const deleteRowInTableByPK = Joi.object({
query: Joi.object({
_lookup_field: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30),
}).required(),
params: Joi.object({
name: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30)
.required(),
pks: Joi.string().required(),
}).required(),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
query: Joi.object({
_lookup_field: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30)
}).required(),
params: Joi.object({
name: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30)
.required(),
pks: Joi.string().required()
}).required(),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional()
})
})
module.exports = {
listTableRows,
insertRowInTable,
getRowInTableByPK,
updateRowInTableByPK,
deleteRowInTableByPK,
};
listTableRows,
insertRowInTable,
getRowInTableByPK,
updateRowInTableByPK,
deleteRowInTableByPK
}

View File

@@ -1,121 +1,112 @@
const Joi = require('joi');
const Joi = require('joi')
const listTables = Joi.object({
query: Joi.object({
_search: Joi.string(),
_ordering: Joi.string(),
}).required(),
params: Joi.object().required(),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
query: Joi.object({
_search: Joi.string(),
_ordering: Joi.string()
}).required(),
params: Joi.object().required(),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional()
})
})
const createTable = Joi.object({
query: Joi.object().required(),
params: Joi.object().required(),
body: Joi.object({
name: Joi.string()
.regex(/^[\w-]+$/)
.min(2)
.max(30)
.required(),
autoAddCreatedAt: Joi.boolean().default(true),
autoAddUpdatedAt: Joi.boolean().default(true),
schema: Joi.array()
.items(
Joi.object({
name: Joi.string()
.regex(/^[\w-]+$/)
.min(2)
.max(30)
.required(),
// type one of sqlite3 types
type: Joi.string()
.valid(
'TEXT',
'NUMERIC',
'INTEGER',
'REAL',
'BLOB',
'BOOLEAN',
'DATE',
'DATETIME',
)
.insensitive()
.required(),
default: Joi.any(),
notNull: Joi.boolean(),
unique: Joi.boolean(),
primaryKey: Joi.boolean(),
foreignKey: Joi.object({
table: Joi.string()
.regex(/^[\w-]+$/)
.min(2)
.max(30)
.required(),
column: Joi.string()
.regex(/^[\w-]+$/)
.min(2)
.max(30)
.required(),
onDelete: Joi.string()
.valid('CASCADE', 'SET NULL', 'SET DEFAULT', 'RESTRICT')
.insensitive()
.default('CASCADE'),
onUpdate: Joi.string()
.valid('CASCADE', 'SET NULL', 'SET DEFAULT', 'RESTRICT')
.insensitive()
.default('RESTRICT'),
}),
index: Joi.boolean(),
}),
)
.required(),
}),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
query: Joi.object().required(),
params: Joi.object().required(),
body: Joi.object({
name: Joi.string()
.regex(/^[\w-]+$/)
.min(2)
.max(30)
.required(),
autoAddCreatedAt: Joi.boolean().default(true),
autoAddUpdatedAt: Joi.boolean().default(true),
schema: Joi.array()
.items(
Joi.object({
name: Joi.string()
.regex(/^[\w-]+$/)
.min(2)
.max(30)
.required(),
// type one of sqlite3 types
type: Joi.string()
.valid('TEXT', 'NUMERIC', 'INTEGER', 'REAL', 'BLOB', 'BOOLEAN', 'DATE', 'DATETIME')
.insensitive()
.required(),
default: Joi.any(),
notNull: Joi.boolean(),
unique: Joi.boolean(),
primaryKey: Joi.boolean(),
foreignKey: Joi.object({
table: Joi.string()
.regex(/^[\w-]+$/)
.min(2)
.max(30)
.required(),
column: Joi.string()
.regex(/^[\w-]+$/)
.min(2)
.max(30)
.required(),
onDelete: Joi.string()
.valid('CASCADE', 'SET NULL', 'SET DEFAULT', 'RESTRICT')
.insensitive()
.default('CASCADE'),
onUpdate: Joi.string()
.valid('CASCADE', 'SET NULL', 'SET DEFAULT', 'RESTRICT')
.insensitive()
.default('RESTRICT')
}),
index: Joi.boolean()
})
)
.required()
}),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional()
})
})
const getTableSchema = Joi.object({
query: Joi.object().required(),
params: Joi.object({
name: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30)
.required(),
}),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
query: Joi.object().required(),
params: Joi.object({
name: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30)
.required()
}),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional()
})
})
const deleteTable = Joi.object({
query: Joi.object().required(),
params: Joi.object({
name: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30)
.required(),
}),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional(),
}),
});
query: Joi.object().required(),
params: Joi.object({
name: Joi.string()
.regex(/^[\w-]+$/)
.min(3)
.max(30)
.required()
}),
body: Joi.object().required(),
cookies: Joi.object({
refreshToken: Joi.string().optional(),
accessToken: Joi.string().optional()
})
})
module.exports = {
listTables,
createTable,
getTableSchema,
deleteTable,
};
listTables,
createTable,
getTableSchema,
deleteTable
}

View File

@@ -1,42 +1,40 @@
#!/usr/bin/env node
const http = require('http');
const http = require('http')
const app = require('./index');
const { wss } = require('./websocket');
const config = require('./config/index');
const app = require('./index')
const { wss } = require('./websocket')
const config = require('./config/index')
if (config.startWithStudio) {
(async () => {
const { handler: soulStudioHandler } = await import(
'soul-studio/build/handler.js'
);
app.use('/studio', soulStudioHandler);
})();
;(async () => {
const { handler: soulStudioHandler } = await import('soul-studio/build/handler.js')
app.use('/studio', soulStudioHandler)
})()
}
const server = http.createServer(app);
const server = http.createServer(app)
const port = config.port;
const port = config.port
const baseURL = `http://localhost:${port}`;
const coreURL = `${baseURL}/api/`;
const studioURL = `${baseURL}/studio/`;
const baseURL = `http://localhost:${port}`
const coreURL = `${baseURL}/api/`
const studioURL = `${baseURL}/studio/`
server.listen(port, () => {
console.log(`Soul is running...`);
console.log(` > Core API at ${coreURL}`);
console.log(`Soul is running...`)
console.log(` > Core API at ${coreURL}`)
if (config.startWithStudio) {
console.log(` > Studio at ${studioURL}`);
require('child_process').exec(`open ${studioURL}`);
}
});
if (config.startWithStudio) {
console.log(` > Studio at ${studioURL}`)
require('child_process').exec(`open ${studioURL}`)
}
})
server.on('upgrade', (request, socket, head) => {
wss.handleUpgrade(request, socket, head, (socket) => {
wss.emit('connection', socket, request);
});
});
wss.handleUpgrade(request, socket, head, (socket) => {
wss.emit('connection', socket, request)
})
})
module.exports = {};
module.exports = {}

View File

@@ -1,110 +1,108 @@
const db = require('../db');
const rowService = require('./rowService')(db);
const db = require('../db')
const rowService = require('./rowService')(db)
const { constantRoles, dbConstants } = require('../constants');
const { constantRoles, dbConstants } = require('../constants')
const {
USERS_TABLE,
ROLES_TABLE,
USERS_ROLES_TABLE,
ROLES_PERMISSIONS_TABLE,
REVOKED_REFRESH_TOKENS_TABLE,
tableFields,
} = dbConstants;
USERS_TABLE,
ROLES_TABLE,
USERS_ROLES_TABLE,
ROLES_PERMISSIONS_TABLE,
REVOKED_REFRESH_TOKENS_TABLE,
tableFields
} = dbConstants
module.exports = (db) => {
return {
getUsersByUsername({ username }) {
const users = rowService.get({
tableName: USERS_TABLE,
whereString: `WHERE ${tableFields.USERNAME} =?`,
whereStringValues: [username],
});
return {
getUsersByUsername({ username }) {
const users = rowService.get({
tableName: USERS_TABLE,
whereString: `WHERE ${tableFields.USERNAME} =?`,
whereStringValues: [username]
})
return users;
},
return users
},
getUsersById({ userId }) {
const users = rowService.get({
tableName: USERS_TABLE,
whereString: `WHERE ${tableFields.ID}=?`,
whereStringValues: [userId],
});
getUsersById({ userId }) {
const users = rowService.get({
tableName: USERS_TABLE,
whereString: `WHERE ${tableFields.ID}=?`,
whereStringValues: [userId]
})
return users;
},
return users
},
getAllUsers() {
const users = rowService.get({
tableName: USERS_TABLE,
whereString: '',
whereStringValues: [],
});
getAllUsers() {
const users = rowService.get({
tableName: USERS_TABLE,
whereString: '',
whereStringValues: []
})
return users;
},
return users
},
// TODO: bypass pagination by providing query param for number of rows
getPermissionByRoleIds({ roleIds }) {
const permissions = rowService.get({
tableName: ROLES_PERMISSIONS_TABLE,
whereString: `WHERE ${tableFields.ROLE_ID} IN (${roleIds.map(
() => '?',
)})`,
whereStringValues: [...roleIds],
limit: 10000,
});
// TODO: bypass pagination by providing query param for number of rows
getPermissionByRoleIds({ roleIds }) {
const permissions = rowService.get({
tableName: ROLES_PERMISSIONS_TABLE,
whereString: `WHERE ${tableFields.ROLE_ID} IN (${roleIds.map(() => '?')})`,
whereStringValues: [...roleIds],
limit: 10000
})
return permissions;
},
return permissions
},
getUserRoleByUserId({ userId }) {
const userRoles = rowService.get({
tableName: USERS_ROLES_TABLE,
whereString: `WHERE ${tableFields.USER_ID} =?`,
whereStringValues: [userId],
});
getUserRoleByUserId({ userId }) {
const userRoles = rowService.get({
tableName: USERS_ROLES_TABLE,
whereString: `WHERE ${tableFields.USER_ID} =?`,
whereStringValues: [userId]
})
return userRoles;
},
return userRoles
},
getDefaultRole() {
const defaultRole = rowService.get({
tableName: ROLES_TABLE,
whereString: `WHERE ${tableFields.ROLE_NAME}=?`,
whereStringValues: [constantRoles.DEFAULT_ROLE],
});
getDefaultRole() {
const defaultRole = rowService.get({
tableName: ROLES_TABLE,
whereString: `WHERE ${tableFields.ROLE_NAME}=?`,
whereStringValues: [constantRoles.DEFAULT_ROLE]
})
return defaultRole;
},
return defaultRole
},
saveRevokedRefreshToken({ refreshToken, expiresAt }) {
const { lastInsertRowid } = rowService.save({
tableName: REVOKED_REFRESH_TOKENS_TABLE,
fields: {
refresh_token: refreshToken,
expires_at: expiresAt,
},
});
saveRevokedRefreshToken({ refreshToken, expiresAt }) {
const { lastInsertRowid } = rowService.save({
tableName: REVOKED_REFRESH_TOKENS_TABLE,
fields: {
refresh_token: refreshToken,
expires_at: expiresAt
}
})
return { id: lastInsertRowid };
},
return { id: lastInsertRowid }
},
getRevokedRefreshToken({ refreshToken }) {
const token = rowService.get({
tableName: REVOKED_REFRESH_TOKENS_TABLE,
whereString: `WHERE ${tableFields.REFRESH_TOKEN}=?`,
whereStringValues: [refreshToken],
});
getRevokedRefreshToken({ refreshToken }) {
const token = rowService.get({
tableName: REVOKED_REFRESH_TOKENS_TABLE,
whereString: `WHERE ${tableFields.REFRESH_TOKEN}=?`,
whereStringValues: [refreshToken]
})
return token;
},
return token
},
deleteRevokedRefreshTokens({ lookupField }) {
const query = `DELETE FROM ${REVOKED_REFRESH_TOKENS_TABLE} ${lookupField}`;
const statement = db.prepare(query);
const result = statement.run();
return result;
},
};
};
deleteRevokedRefreshTokens({ lookupField }) {
const query = `DELETE FROM ${REVOKED_REFRESH_TOKENS_TABLE} ${lookupField}`
const statement = db.prepare(query)
const result = statement.run()
return result
}
}
}

View File

@@ -1,7 +1,7 @@
const db = require('../db');
const db = require('../db')
const rowService = require('./rowService')(db);
const tableService = require('./tableService')(db);
const authService = require('./authService')(db);
const rowService = require('./rowService')(db)
const tableService = require('./tableService')(db)
const authService = require('./authService')(db)
module.exports = { rowService, tableService, authService };
module.exports = { rowService, tableService, authService }

View File

@@ -1,98 +1,94 @@
const { apiConstants } = require('../constants');
const { apiConstants } = require('../constants')
module.exports = (db) => {
return {
get(data) {
const query = `SELECT ${data.schemaString || '*'} FROM ${
data.tableName
} ${data.extendString || ''} ${data.whereString || ''} ${
data.orderString || ''
} LIMIT ? OFFSET ?`;
return {
get(data) {
const query = `SELECT ${data.schemaString || '*'} FROM ${data.tableName} ${
data.extendString || ''
} ${data.whereString || ''} ${data.orderString || ''} LIMIT ? OFFSET ?`
const statement = db.prepare(query);
const result = statement.all(
...data.whereStringValues,
data.limit || apiConstants.DEFAULT_PAGE_LIMIT,
data.page || apiConstants.DEFAULT_PAGE_INDEX,
);
const statement = db.prepare(query)
const result = statement.all(
...data.whereStringValues,
data.limit || apiConstants.DEFAULT_PAGE_LIMIT,
data.page || apiConstants.DEFAULT_PAGE_INDEX
)
return result;
},
return result
},
getById(data) {
const pks = data.pks.split(',');
const placeholders = pks.map(() => '?').join(',');
const query = `SELECT ${data.schemaString} FROM ${data.tableName} ${data.extendString} WHERE ${data.tableName}.${data.lookupField} in (${placeholders})`;
const statement = db.prepare(query);
const result = statement.all(...pks);
return result;
},
getById(data) {
const pks = data.pks.split(',')
const placeholders = pks.map(() => '?').join(',')
const query = `SELECT ${data.schemaString} FROM ${data.tableName} ${data.extendString} WHERE ${data.tableName}.${data.lookupField} in (${placeholders})`
const statement = db.prepare(query)
const result = statement.all(...pks)
return result
},
getCount(data) {
const query = `SELECT COUNT(*) as total FROM ${data.tableName} ${data.whereString}`;
const statement = db.prepare(query);
const result = statement.get(...data.whereStringValues).total;
return result;
},
getCount(data) {
const query = `SELECT COUNT(*) as total FROM ${data.tableName} ${data.whereString}`
const statement = db.prepare(query)
const result = statement.get(...data.whereStringValues).total
return result
},
save(data) {
// wrap text values in quotes
const fieldsString = Object.keys(data.fields)
.map((field) => `'${field}'`)
.join(', ');
save(data) {
// wrap text values in quotes
const fieldsString = Object.keys(data.fields)
.map((field) => `'${field}'`)
.join(', ')
// wrap text values in quotes
const valuesString = Object.values(data.fields).map((value) => value);
const placeholders = Object.values(data.fields)
.map(() => '?')
.join(',');
// wrap text values in quotes
const valuesString = Object.values(data.fields).map((value) => value)
const placeholders = Object.values(data.fields)
.map(() => '?')
.join(',')
let values = `(${fieldsString}) VALUES (${placeholders})`;
if (valuesString === '') {
values = 'DEFAULT VALUES';
}
let values = `(${fieldsString}) VALUES (${placeholders})`
if (valuesString === '') {
values = 'DEFAULT VALUES'
}
const query = `INSERT INTO ${data.tableName} ${values}`;
const statement = db.prepare(query);
const result = statement.run(...valuesString);
return result;
},
const query = `INSERT INTO ${data.tableName} ${values}`
const statement = db.prepare(query)
const result = statement.run(...valuesString)
return result
},
bulkWrite(data) {
const { tableName, fields } = data;
const fieldNames = Object.keys(fields[0]);
const valueSets = fields.map((row) => Object.values(row));
bulkWrite(data) {
const { tableName, fields } = data
const fieldNames = Object.keys(fields[0])
const valueSets = fields.map((row) => Object.values(row))
const placeholders = fieldNames.map(() => '?');
const valuesString = valueSets
.map(() => `(${placeholders.join(',')})`)
.join(',');
const placeholders = fieldNames.map(() => '?')
const valuesString = valueSets.map(() => `(${placeholders.join(',')})`).join(',')
const query = `INSERT INTO ${tableName} (${fieldNames
.map((field) => `'${field}'`)
.join(', ')}) VALUES ${valuesString}`;
const query = `INSERT INTO ${tableName} (${fieldNames
.map((field) => `'${field}'`)
.join(', ')}) VALUES ${valuesString}`
const statement = db.prepare(query);
const result = statement.run(...valueSets.flat());
return result;
},
const statement = db.prepare(query)
const result = statement.run(...valueSets.flat())
return result
},
update(data) {
const pks = data.pks.split(',');
const placeholders = pks.map(() => '?').join(',');
const query = `UPDATE ${data.tableName} SET ${data.fieldsString} WHERE ${data.lookupField} in (${placeholders})`;
const statement = db.prepare(query);
const result = statement.run(...pks);
return result;
},
update(data) {
const pks = data.pks.split(',')
const placeholders = pks.map(() => '?').join(',')
const query = `UPDATE ${data.tableName} SET ${data.fieldsString} WHERE ${data.lookupField} in (${placeholders})`
const statement = db.prepare(query)
const result = statement.run(...pks)
return result
},
delete(data) {
const pks = data.pks.split(',');
const placeholders = pks.map(() => '?').join(',');
const query = `DELETE FROM ${data.tableName} WHERE ${data.lookupField} in (${placeholders})`;
const statement = db.prepare(query);
const result = statement.run(...pks);
return result;
},
};
};
delete(data) {
const pks = data.pks.split(',')
const placeholders = pks.map(() => '?').join(',')
const query = `DELETE FROM ${data.tableName} WHERE ${data.lookupField} in (${placeholders})`
const statement = db.prepare(query)
const result = statement.run(...pks)
return result
}
}
}

View File

@@ -1,131 +1,123 @@
module.exports = (db) => {
return {
createTable(tableName, schema, options = {}) {
const {
autoAddCreatedAt = true,
autoAddUpdatedAt = true,
multipleUniqueConstraints,
} = options;
return {
createTable(tableName, schema, options = {}) {
const { autoAddCreatedAt = true, autoAddUpdatedAt = true, multipleUniqueConstraints } = options
let indices = [];
let indices = []
let schemaString = schema
.map(({ name, type, notNull, unique, primaryKey, foreignKey }) => {
let column = `'${name}' '${type}'`;
let schemaString = schema
.map(({ name, type, notNull, unique, primaryKey, foreignKey }) => {
let column = `'${name}' '${type}'`
if (notNull) {
column += ' NOT NULL';
}
if (unique) {
column += ' UNIQUE';
}
if (primaryKey) {
column += ' PRIMARY KEY';
}
if (foreignKey) {
column += ` REFERENCES ${foreignKey.table}(${foreignKey.column})`;
}
if (foreignKey && foreignKey.onDelete) {
column += ` ON DELETE ${foreignKey.onDelete}`;
}
if (foreignKey && foreignKey.onUpdate) {
column += ` ON UPDATE ${foreignKey.onUpdate}`;
}
if (notNull) {
column += ' NOT NULL'
}
if (unique) {
column += ' UNIQUE'
}
if (primaryKey) {
column += ' PRIMARY KEY'
}
if (foreignKey) {
column += ` REFERENCES ${foreignKey.table}(${foreignKey.column})`
}
if (foreignKey && foreignKey.onDelete) {
column += ` ON DELETE ${foreignKey.onDelete}`
}
if (foreignKey && foreignKey.onUpdate) {
column += ` ON UPDATE ${foreignKey.onUpdate}`
}
return column;
})
.join(', ');
return column
})
.join(', ')
// add id if primary key is not defined
if (!schema.find((field) => field.primaryKey)) {
schemaString = `
// add id if primary key is not defined
if (!schema.find((field) => field.primaryKey)) {
schemaString = `
id INTEGER PRIMARY KEY AUTOINCREMENT,
${schemaString}
`;
}
`
}
// add created at and updated at
if (autoAddCreatedAt) {
schemaString = `${schemaString}, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP`;
}
// add created at and updated at
if (autoAddCreatedAt) {
schemaString = `${schemaString}, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP`
}
if (autoAddUpdatedAt) {
schemaString = `${schemaString}, updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP`;
}
if (autoAddUpdatedAt) {
schemaString = `${schemaString}, updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP`
}
if (multipleUniqueConstraints) {
schemaString = `${schemaString}, CONSTRAINT ${
multipleUniqueConstraints.name
} UNIQUE (${multipleUniqueConstraints.fields
.map((field) => field)
.join(' ,')})`;
}
if (multipleUniqueConstraints) {
schemaString = `${schemaString}, CONSTRAINT ${
multipleUniqueConstraints.name
} UNIQUE (${multipleUniqueConstraints.fields.map((field) => field).join(' ,')})`
}
let indicesString = indices
.map((field) => {
return `
let indicesString = indices
.map((field) => {
return `
CREATE INDEX ${tableName}_${field}_index
ON ${tableName} (${field})
`;
})
.join(';');
`
})
.join(';')
const query = `CREATE TABLE ${tableName} (${schemaString})`;
const query = `CREATE TABLE ${tableName} (${schemaString})`
try {
db.prepare(query).run();
try {
db.prepare(query).run()
if (indicesString) {
db.prepare(indicesString).run();
}
if (indicesString) {
db.prepare(indicesString).run()
}
db.prepare(`PRAGMA table_info(${tableName})`).all();
} catch (error) {
console.log(error);
}
},
db.prepare(`PRAGMA table_info(${tableName})`).all()
} catch (error) {
console.log(error)
}
},
listTables(options = {}) {
const { search, ordering, exclude } = options;
listTables(options = {}) {
const { search, ordering, exclude } = options
let query = `SELECT name FROM sqlite_master WHERE type IN ('table', 'view')`;
let query = `SELECT name FROM sqlite_master WHERE type IN ('table', 'view')`
// if search is provided, search the tables
// e.g. search=users
if (search) {
query += ` AND name LIKE $searchQuery`;
}
// if search is provided, search the tables
// e.g. search=users
if (search) {
query += ` AND name LIKE $searchQuery`
}
// if exclude is passed don't return the some tables
// e.g. exclude=['_users', '_roles']
if (exclude) {
const excludeTables = exclude.map((field) => `'${field}'`).join(' ,');
query += `AND name NOT IN (${excludeTables});`;
}
// if exclude is passed don't return the some tables
// e.g. exclude=['_users', '_roles']
if (exclude) {
const excludeTables = exclude.map((field) => `'${field}'`).join(' ,')
query += `AND name NOT IN (${excludeTables});`
}
// if ordering is provided, order the tables
// e.g. ordering=name (ascending) or ?_ordering=-name (descending)
if (ordering) {
query += ` ORDER BY $ordering`;
}
// if ordering is provided, order the tables
// e.g. ordering=name (ascending) or ?_ordering=-name (descending)
if (ordering) {
query += ` ORDER BY $ordering`
}
try {
const tables = db.prepare(query).all({
searchQuery: `%${search}%`,
ordering: `${ordering?.replace('-', '')} ${
ordering?.startsWith('-') ? 'DESC' : 'ASC'
}`,
});
return tables;
} catch (error) {
console.log(error);
}
},
try {
const tables = db.prepare(query).all({
searchQuery: `%${search}%`,
ordering: `${ordering?.replace('-', '')} ${ordering?.startsWith('-') ? 'DESC' : 'ASC'}`
})
return tables
} catch (error) {
console.log(error)
}
},
checkTableExists(tableName) {
const query = `SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`;
const result = db.prepare(query).get();
return result;
},
};
};
checkTableExists(tableName) {
const query = `SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`
const result = db.prepare(query).get()
return result
}
}
}

View File

@@ -1,200 +1,195 @@
const swaggerAutogen = require('swagger-autogen')();
const swaggerAutogen = require('swagger-autogen')()
const config = require('../config/index');
const version = require('../../package.json').version;
const config = require('../config/index')
const version = require('../../package.json').version
const outputFile = './swagger.json';
const endpointsFiles = ['../index.js'];
const outputFile = './swagger.json'
const endpointsFiles = ['../index.js']
const doc = {
info: {
version: version,
title: 'Soul API',
description:
'API Documentation for <b>Soul</b>, a SQLite REST and realtime server. ',
},
host: `localhost:${config.port}`,
basePath: '/',
schemes: ['http', 'https'],
consumes: ['application/json'],
produces: ['application/json'],
tags: [
{
name: 'Root',
description: 'Root endpoints',
},
{
name: 'Tables',
description: 'Tables endpoints',
},
{
name: 'Rows',
description: 'Rows endpoints',
},
{
name: 'Auth',
description: 'Auth endpoints',
},
],
securityDefinitions: {},
definitions: {
Table: {
name: 'users',
},
Row: {},
Query: {
query: 'SELECT * FROM users',
},
Statement: {
statement:
'INSERT INTO users (id, firstName, lastName) VALUES (:id, :firstName, :lastName)',
values: { id: 1, firstName: 'John', lastName: 'Doe' },
},
Transaction: {
transaction: [
{ $ref: '#/definitions/Query' },
{ $ref: '#/definitions/Statement' },
],
},
ForeignKey: {
table: 'users',
column: 'id',
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
Field: {
name: 'user_id',
type: 'INTEGER',
default: 1,
notNull: true,
unique: false,
primaryKey: false,
foreignKey: { $ref: '#/definitions/ForeignKey' },
index: false,
},
Schema: [{ $ref: '#/definitions/Field' }],
info: {
version: version,
title: 'Soul API',
description: 'API Documentation for <b>Soul</b>, a SQLite REST and realtime server. '
},
host: `localhost:${config.port}`,
basePath: '/',
schemes: ['http', 'https'],
consumes: ['application/json'],
produces: ['application/json'],
tags: [
{
name: 'Root',
description: 'Root endpoints'
},
{
name: 'Tables',
description: 'Tables endpoints'
},
{
name: 'Rows',
description: 'Rows endpoints'
},
{
name: 'Auth',
description: 'Auth endpoints'
}
],
securityDefinitions: {},
definitions: {
Table: {
name: 'users'
},
Row: {},
Query: {
query: 'SELECT * FROM users'
},
Statement: {
statement: 'INSERT INTO users (id, firstName, lastName) VALUES (:id, :firstName, :lastName)',
values: { id: 1, firstName: 'John', lastName: 'Doe' }
},
Transaction: {
transaction: [{ $ref: '#/definitions/Query' }, { $ref: '#/definitions/Statement' }]
},
ForeignKey: {
table: 'users',
column: 'id',
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
Field: {
name: 'user_id',
type: 'INTEGER',
default: 1,
notNull: true,
unique: false,
primaryKey: false,
foreignKey: { $ref: '#/definitions/ForeignKey' },
index: false
},
Schema: [{ $ref: '#/definitions/Field' }],
CreateTableRequestBody: {
name: 'users',
schema: { $ref: '#/definitions/Schema' },
autoAddCreatedAt: true,
autoAddUpdatedAt: true,
},
CreateTableSuccessResponse: {
message: 'Table created',
data: {
name: 'users',
fields: [{ $ref: '#/definitions/Field' }],
},
},
CreateTableErrorResponse: {
message: 'Table not created',
error: 'already_exists',
data: {},
},
CreateTableRequestBody: {
name: 'users',
schema: { $ref: '#/definitions/Schema' },
autoAddCreatedAt: true,
autoAddUpdatedAt: true
},
CreateTableSuccessResponse: {
message: 'Table created',
data: {
name: 'users',
fields: [{ $ref: '#/definitions/Field' }]
}
},
CreateTableErrorResponse: {
message: 'Table not created',
error: 'already_exists',
data: {}
},
InsertRowRequestBody: {
$ref: '#/definitions/Row',
},
InsertRowSuccessResponse: {
message: 'Row inserted',
data: {
id: 1,
createdAt: '2022-10-10 10:55:29',
updatedAt: '2022-10-10 10:55:29',
firstName: 'John',
},
},
InsertRowErrorResponse: {
message: 'Row not inserted',
error: 'not_found',
},
InsertRowRequestBody: {
$ref: '#/definitions/Row'
},
InsertRowSuccessResponse: {
message: 'Row inserted',
data: {
id: 1,
createdAt: '2022-10-10 10:55:29',
updatedAt: '2022-10-10 10:55:29',
firstName: 'John'
}
},
InsertRowErrorResponse: {
message: 'Row not inserted',
error: 'not_found'
},
UpdateRowRequestBody: {
fields: [{ $ref: '#/definitions/Field' }],
},
UpdateRowRequestBody: {
fields: [{ $ref: '#/definitions/Field' }]
},
BulkUpdateRowsRequestBody: {
pks: [1, 2, 3],
fields: [{ $ref: '#/definitions/Field' }],
},
BulkUpdateRowsRequestBody: {
pks: [1, 2, 3],
fields: [{ $ref: '#/definitions/Field' }]
},
BulkDeleteRowsRequestBody: {
pks: [1, 2, 3],
},
BulkDeleteRowsRequestBody: {
pks: [1, 2, 3]
},
TransactionRequestBody: {
$ref: '#/definitions/Transaction',
},
ObtainAccessTokenRequestBody: {
fields: {
username: '@john',
password: 'Ak22#cPM33@v*#',
},
},
TransactionRequestBody: {
$ref: '#/definitions/Transaction'
},
ObtainAccessTokenRequestBody: {
fields: {
username: '@john',
password: 'Ak22#cPM33@v*#'
}
},
ObtainAccessTokenSuccessResponse: {
message: 'Success',
data: {
userId: 1,
},
},
ObtainAccessTokenSuccessResponse: {
message: 'Success',
data: {
userId: 1
}
},
InvalidCredentialErrorResponse: {
message: 'Invalid username or password',
},
InvalidCredentialErrorResponse: {
message: 'Invalid username or password'
},
UserRegisterationRequestBody: {
fields: {
username: '@john',
password: 'Ak22#cPM33@v*#',
},
},
UserRegisterationRequestBody: {
fields: {
username: '@john',
password: 'Ak22#cPM33@v*#'
}
},
WeakPasswordErrorResponse: {
message:
'This password is weak, it should be at least 8 characters long and contain a combination of lowercase letters, uppercase letters, numbers, and special characters',
},
WeakPasswordErrorResponse: {
message:
'This password is weak, it should be at least 8 characters long and contain a combination of lowercase letters, uppercase letters, numbers, and special characters'
},
UsernameTakenErrorResponse: {
message: 'This username is taken',
},
UsernameTakenErrorResponse: {
message: 'This username is taken'
},
DefaultRoleNotCreatedErrorResponse: {
message: 'Please restart soul so a default role can be created',
},
DefaultRoleNotCreatedErrorResponse: {
message: 'Please restart soul so a default role can be created'
},
UserNotFoundErrorResponse: {
message: 'User not found',
},
UserNotFoundErrorResponse: {
message: 'User not found'
},
InvalidRefreshTokenErrorResponse: {
message: 'Invalid refresh token',
},
InvalidRefreshTokenErrorResponse: {
message: 'Invalid refresh token'
},
ChangePasswordRequestBody: {
fields: {
currentPassword: 'Ak22#cPM33@v*#',
newPassword: 'hKB33o@3245CD$',
},
},
ChangePasswordRequestBody: {
fields: {
currentPassword: 'Ak22#cPM33@v*#',
newPassword: 'hKB33o@3245CD$'
}
},
ChangePasswordSuccessResponse: {
message: 'Password updated successfully',
data: { id: 1, username: '@john' },
},
ChangePasswordSuccessResponse: {
message: 'Password updated successfully',
data: { id: 1, username: '@john' }
},
RefreshAccessTokenSuccessResponse: {
message: 'Success',
data: { userId: 1 },
},
RefreshAccessTokenSuccessResponse: {
message: 'Success',
data: { userId: 1 }
},
InvalidPasswordErrorResponse: { message: 'Invalid password' },
InvalidPasswordErrorResponse: { message: 'Invalid password' },
RemoveTokensResponse: {
message: 'Logout successful',
},
},
};
RemoveTokensResponse: {
message: 'Logout successful'
}
}
}
swaggerAutogen(outputFile, endpointsFiles, doc);
swaggerAutogen(outputFile, endpointsFiles, doc)

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
const { createTestTable, insertIntoTestTable } = require('.');
const { createTestTable, insertIntoTestTable } = require('.')
const setup = () => {
console.log('Test suite started');
console.log('Creating test table...');
createTestTable();
console.log('Inserting a row into test table...');
insertIntoTestTable();
};
console.log('Test suite started')
console.log('Creating test table...')
createTestTable()
console.log('Inserting a row into test table...')
insertIntoTestTable()
}
module.exports = setup;
module.exports = setup

View File

@@ -1,9 +1,9 @@
const { dropTestDatabase } = require('.');
const { dropTestDatabase } = require('.')
const globalTearDown = () => {
console.log('Test suite finished');
console.log('Dropping test database...');
dropTestDatabase();
};
console.log('Test suite finished')
console.log('Dropping test database...')
dropTestDatabase()
}
module.exports = globalTearDown;
module.exports = globalTearDown

View File

@@ -1,48 +1,46 @@
const fs = require('fs');
const { unlink } = require('fs/promises');
const db = require('../db/index');
const { testNames } = require('./testData');
const fs = require('fs')
const { unlink } = require('fs/promises')
const db = require('../db/index')
const { testNames } = require('./testData')
const dropTestTable = (table = 'users') => {
db.prepare(`DROP TABLE IF EXISTS ${table}`).run();
};
db.prepare(`DROP TABLE IF EXISTS ${table}`).run()
}
const dropTestDatabase = async (path = 'test.db') => {
// delete test database file e.g. test.db
try {
await unlink(path), console.log(`successfully deleted ${path}`);
} catch (error) {
console.error('there was an error:', error);
}
// delete test database file e.g. test.db
try {
await unlink(path), console.log(`successfully deleted ${path}`)
} catch (error) {
console.error('there was an error:', error)
}
if (fs.existsSync(path + '-wal')) {
try {
await Promise.allSettled([unlink(path + '-wal'), unlink(path + '-shm')]);
} catch (error) {
console.error('there was an error:', error);
}
}
};
if (fs.existsSync(path + '-wal')) {
try {
await Promise.allSettled([unlink(path + '-wal'), unlink(path + '-shm')])
} catch (error) {
console.error('there was an error:', error)
}
}
}
const createTestTable = (table = 'users') => {
db.prepare(
`CREATE TABLE ${table} (id INTEGER PRIMARY KEY, firstName TEXT, lastName TEXT, email TEXT, username TEXT, createdAt TEXT)`,
).run();
};
db.prepare(
`CREATE TABLE ${table} (id INTEGER PRIMARY KEY, firstName TEXT, lastName TEXT, email TEXT, username TEXT, createdAt TEXT)`
).run()
}
const insertIntoTestTable = (table = 'users') => {
const statement = db.prepare(
`INSERT INTO ${table} (firstName, lastName, createdAt) VALUES (?, ?, ?)`,
);
const statement = db.prepare(`INSERT INTO ${table} (firstName, lastName, createdAt) VALUES (?, ?, ?)`)
for (const user of testNames) {
statement.run(user.firstName, user.lastName, user.createdAt);
}
};
for (const user of testNames) {
statement.run(user.firstName, user.lastName, user.createdAt)
}
}
module.exports = {
dropTestTable,
dropTestDatabase,
createTestTable,
insertIntoTestTable,
};
dropTestTable,
dropTestDatabase,
createTestTable,
insertIntoTestTable
}

View File

@@ -1,75 +1,75 @@
const testNames = [
{ firstName: 'Emily', lastName: 'William', createdAt: '2008-01-08 00:00:00' },
{ firstName: 'Michael', lastName: 'Lee', createdAt: '2009-01-08 00:00:00' },
{ firstName: 'Sarah', lastName: 'Johnson', createdAt: '2010-01-08 00:00:00' },
{ firstName: 'David', lastName: 'Chen', createdAt: '2011-01-08 00:00:00' },
{
firstName: 'Olivia',
lastName: 'William',
createdAt: '2012-01-08 00:00:00',
},
{ firstName: 'William', lastName: 'Kim', createdAt: '2013-01-08 00:00:00' },
{ firstName: 'Sophia', lastName: 'Singh', createdAt: '2013-02-08 00:00:00' },
{
firstName: 'James',
lastName: 'Rodriguez',
createdAt: '2013-03-08 00:00:00',
},
{ firstName: 'Ava', lastName: 'Patel', createdAt: '2013-01-04 00:00:00' },
{
firstName: 'Benjamin',
lastName: 'Garcia',
createdAt: '2015-01-08 00:00:00',
},
{
firstName: 'Isabella',
lastName: 'Nguyen',
createdAt: '2014-01-08 00:00:00',
},
{ firstName: 'Ethan', lastName: 'Lee', createdAt: '2016-01-08 00:00:00' },
{ firstName: 'Mia', lastName: 'Wilson', createdAt: '2017-01-08 00:00:00' },
{
firstName: 'Alexander',
lastName: 'William',
createdAt: '2018-01-08 00:00:00',
},
{
firstName: 'Charlotte',
lastName: 'Hernandez',
createdAt: '2019-01-08 00:00:00',
},
{ firstName: 'Liam', lastName: 'Gonzalez', createdAt: '2020-01-08 00:00:00' },
{ firstName: 'Emma', lastName: 'Gomez', createdAt: '2021-01-08 00:00:00' },
{ firstName: 'Noah', lastName: 'Perez', createdAt: '2021-01-08 00:00:00' },
{ firstName: 'Avery', lastName: 'Ramirez', createdAt: '2023-02-08 00:00:00' },
{ firstName: 'Jacob', lastName: 'Turner', createdAt: '2023-03-08 00:00:00' },
{
firstName: 'Abigail',
lastName: 'Williams',
createdAt: '2023-02-10 00:00:00',
},
{ firstName: 'Elijah', lastName: 'Hall', createdAt: '2023-04-02 00:00:00' },
{ firstName: 'Mila', lastName: 'Flores', createdAt: '2023-05-13 00:00:00' },
{
firstName: 'Evelyn',
lastName: 'Morales',
createdAt: '2023-06-05 00:00:00',
},
{ firstName: 'Logan', lastName: 'Collins', createdAt: '2023-06-07 00:00:00' },
{ firstName: null, lastName: 'Flores', createdAt: '2023-06-09 00:00:00' },
];
{ firstName: 'Emily', lastName: 'William', createdAt: '2008-01-08 00:00:00' },
{ firstName: 'Michael', lastName: 'Lee', createdAt: '2009-01-08 00:00:00' },
{ firstName: 'Sarah', lastName: 'Johnson', createdAt: '2010-01-08 00:00:00' },
{ firstName: 'David', lastName: 'Chen', createdAt: '2011-01-08 00:00:00' },
{
firstName: 'Olivia',
lastName: 'William',
createdAt: '2012-01-08 00:00:00'
},
{ firstName: 'William', lastName: 'Kim', createdAt: '2013-01-08 00:00:00' },
{ firstName: 'Sophia', lastName: 'Singh', createdAt: '2013-02-08 00:00:00' },
{
firstName: 'James',
lastName: 'Rodriguez',
createdAt: '2013-03-08 00:00:00'
},
{ firstName: 'Ava', lastName: 'Patel', createdAt: '2013-01-04 00:00:00' },
{
firstName: 'Benjamin',
lastName: 'Garcia',
createdAt: '2015-01-08 00:00:00'
},
{
firstName: 'Isabella',
lastName: 'Nguyen',
createdAt: '2014-01-08 00:00:00'
},
{ firstName: 'Ethan', lastName: 'Lee', createdAt: '2016-01-08 00:00:00' },
{ firstName: 'Mia', lastName: 'Wilson', createdAt: '2017-01-08 00:00:00' },
{
firstName: 'Alexander',
lastName: 'William',
createdAt: '2018-01-08 00:00:00'
},
{
firstName: 'Charlotte',
lastName: 'Hernandez',
createdAt: '2019-01-08 00:00:00'
},
{ firstName: 'Liam', lastName: 'Gonzalez', createdAt: '2020-01-08 00:00:00' },
{ firstName: 'Emma', lastName: 'Gomez', createdAt: '2021-01-08 00:00:00' },
{ firstName: 'Noah', lastName: 'Perez', createdAt: '2021-01-08 00:00:00' },
{ firstName: 'Avery', lastName: 'Ramirez', createdAt: '2023-02-08 00:00:00' },
{ firstName: 'Jacob', lastName: 'Turner', createdAt: '2023-03-08 00:00:00' },
{
firstName: 'Abigail',
lastName: 'Williams',
createdAt: '2023-02-10 00:00:00'
},
{ firstName: 'Elijah', lastName: 'Hall', createdAt: '2023-04-02 00:00:00' },
{ firstName: 'Mila', lastName: 'Flores', createdAt: '2023-05-13 00:00:00' },
{
firstName: 'Evelyn',
lastName: 'Morales',
createdAt: '2023-06-05 00:00:00'
},
{ firstName: 'Logan', lastName: 'Collins', createdAt: '2023-06-07 00:00:00' },
{ firstName: null, lastName: 'Flores', createdAt: '2023-06-09 00:00:00' }
]
const testData = {
strongPassword: 'HeK34#C44DMJ',
strongPassword2: 'Mk22#c9@Cv!K',
weakPassword: '12345678',
invalidUsername: 'invalid_username',
invalidPassword: 'invalid_password',
users: {
user1: { username: 'Jane' },
user2: { username: 'Mike' },
user3: { username: 'John' },
},
};
strongPassword: 'HeK34#C44DMJ',
strongPassword2: 'Mk22#c9@Cv!K',
weakPassword: '12345678',
invalidUsername: 'invalid_username',
invalidPassword: 'invalid_password',
users: {
user1: { username: 'Jane' },
user2: { username: 'Mike' },
user3: { username: 'John' }
}
}
module.exports = { testNames, testData };
module.exports = { testNames, testData }

View File

@@ -1,78 +1,78 @@
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')
const { passwordStrength } = require('check-password-strength');
const { passwordStrength } = require('check-password-strength')
const hashPassword = async (password, saltRounds) => {
const salt = await bcrypt.genSalt(saltRounds);
const hashedPassword = await bcrypt.hash(password, saltRounds);
return { hashedPassword, salt };
};
const salt = await bcrypt.genSalt(saltRounds)
const hashedPassword = await bcrypt.hash(password, saltRounds)
return { hashedPassword, salt }
}
const comparePasswords = async (plainPassword, hashedPassword) => {
const isMatch = await bcrypt.compare(plainPassword, hashedPassword);
return isMatch;
};
const isMatch = await bcrypt.compare(plainPassword, hashedPassword)
return isMatch
}
const checkPasswordStrength = (password) => {
const value = passwordStrength(password).value;
return value;
};
const value = passwordStrength(password).value
return value
}
const generateToken = async (payload, secret, expiresIn) => {
return jwt.sign(payload, secret, { expiresIn });
};
return jwt.sign(payload, secret, { expiresIn })
}
const decodeToken = async (token, secret) => {
try {
const decoded = jwt.verify(token, secret);
return decoded;
} catch (error) {
throw new Error('Invalid token');
}
};
try {
const decoded = jwt.verify(token, secret)
return decoded
} catch (error) {
throw new Error('Invalid token')
}
}
const toBoolean = (value) => {
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'boolean') {
return value
}
if (typeof value === 'string') {
const lowerCaseValue = value.toLowerCase();
if (lowerCaseValue === 'true') {
return true;
} else if (lowerCaseValue === 'false') {
return false;
}
}
if (typeof value === 'string') {
const lowerCaseValue = value.toLowerCase()
if (lowerCaseValue === 'true') {
return true
} else if (lowerCaseValue === 'false') {
return false
}
}
if (typeof value === 'number') {
if (value === 1) {
return true;
} else if (value === 0) {
return false;
}
}
if (typeof value === 'number') {
if (value === 1) {
return true
} else if (value === 0) {
return false
}
}
throw new Error('Invalid value. Cannot convert to boolean.');
};
throw new Error('Invalid value. Cannot convert to boolean.')
}
const removeFields = async (rows, fields) => {
const newPayload = rows.map((row) => {
fields.map((field) => {
delete row[field];
});
});
const newPayload = rows.map((row) => {
fields.map((field) => {
delete row[field]
})
})
return newPayload;
};
return newPayload
}
module.exports = {
hashPassword,
comparePasswords,
checkPasswordStrength,
generateToken,
decodeToken,
toBoolean,
removeFields,
};
hashPassword,
comparePasswords,
checkPasswordStrength,
generateToken,
decodeToken,
toBoolean,
removeFields
}

View File

@@ -1,70 +1,70 @@
const { WebSocketServer } = require('ws');
const { WebSocketServer } = require('ws')
const db = require('./db/index');
const db = require('./db/index')
const wss = new WebSocketServer({ noServer: true });
const wss = new WebSocketServer({ noServer: true })
const websocketSubscribers = new Map();
const websocketSubscribers = new Map()
wss.on('connection', function (ws, request) {
const [_path, params] = request?.url?.split('?');
const [_path, params] = request?.url?.split('?')
if (!_path.startsWith('/ws')) {
ws.close();
return;
}
if (!_path.startsWith('/ws')) {
ws.close()
return
}
const tableName = _path.replace('/ws/tables/', '').replace('/', '');
const tableName = _path.replace('/ws/tables/', '').replace('/', '')
// if table does not exists close the connection
const query = `SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`;
try {
const table = db.prepare(query).get();
// if table does not exists close the connection
const query = `SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`
try {
const table = db.prepare(query).get()
if (!table) {
ws.send(
JSON.stringify({
message: `Table ${tableName} does not exist`,
})
);
if (!table) {
ws.send(
JSON.stringify({
message: `Table ${tableName} does not exist`
})
)
ws.close();
return;
}
} catch (error) {
ws.send(
JSON.stringify({
message: error.message,
})
);
ws.close();
return;
}
ws.close()
return
}
} catch (error) {
ws.send(
JSON.stringify({
message: error.message
})
)
ws.close()
return
}
if (!websocketSubscribers.has(tableName)) {
websocketSubscribers.set(tableName, new Set());
}
if (!websocketSubscribers.has(tableName)) {
websocketSubscribers.set(tableName, new Set())
}
const subscriber = {
ws,
params: new URLSearchParams(params),
};
const subscriber = {
ws,
params: new URLSearchParams(params)
}
console.log(`New subscriber for table ${tableName}`);
websocketSubscribers.get(tableName).add(subscriber);
console.log(`New subscriber for table ${tableName}`)
websocketSubscribers.get(tableName).add(subscriber)
ws.send(JSON.stringify({ message: `Subscribed to table "${tableName}"` }));
ws.send(JSON.stringify({ message: `Subscribed to table "${tableName}"` }))
ws.on('message', function (message) {
console.log('received: %s', message);
});
ws.on('message', function (message) {
console.log('received: %s', message)
})
ws.on('close', function () {
websocketSubscribers.get(tableName).delete(subscriber);
});
});
ws.on('close', function () {
websocketSubscribers.get(tableName).delete(subscriber)
})
})
module.exports = {
wss,
websocketSubscribers,
};
wss,
websocketSubscribers
}