create-sipere 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/create-sipere.js +39 -0
- package/lib/generator.js +60 -0
- package/lib/utils.js +14 -0
- package/package.json +33 -0
- package/templates/js-rest-api/.env.example +10 -0
- package/templates/js-rest-api/.env.test +7 -0
- package/templates/js-rest-api/README.md +12 -0
- package/templates/js-rest-api/app/app.js +17 -0
- package/templates/js-rest-api/app/controllers/authController.js +103 -0
- package/templates/js-rest-api/app/controllers/userController.js +129 -0
- package/templates/js-rest-api/app/database/database.js +23 -0
- package/templates/js-rest-api/app/index.js +10 -0
- package/templates/js-rest-api/app/middleware/authjwt.js +25 -0
- package/templates/js-rest-api/app/models/modrels.js +16 -0
- package/templates/js-rest-api/app/models/user.js +11 -0
- package/templates/js-rest-api/app/routes/api.js +14 -0
- package/templates/js-rest-api/database/migrations/2025_10_08_075231_create_user.js +36 -0
- package/templates/js-rest-api/docs/dev_doc.md +31 -0
- package/templates/js-rest-api/docs/user_doc.md +340 -0
- package/templates/js-rest-api/nodemon.json +4 -0
- package/templates/js-rest-api/op +5 -0
- package/templates/js-rest-api/package.json +42 -0
- package/templates/js-rest-api/test/user.spec.js +43 -0
- package/templates/ts-rest-api/package.json +20 -0
- package/templates/ts-rest-api/src/index.ts +0 -0
- package/test/create-project.spec.js +33 -0
package/create-sipere.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const prompts = require('prompts');
|
|
5
|
+
const createProject = require('./lib/generator');
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.description('create a new sipere project')
|
|
11
|
+
.argument('[project-name]', 'project name')
|
|
12
|
+
.option('-t, --typescript', 'use typescript')
|
|
13
|
+
.action(async (name, options) => {
|
|
14
|
+
await startGenerator(name, options);
|
|
15
|
+
});
|
|
16
|
+
program.parse(process.argv);
|
|
17
|
+
|
|
18
|
+
async function startGenerator(name, options) {
|
|
19
|
+
let projectName;
|
|
20
|
+
if(!name) {
|
|
21
|
+
const questions = [
|
|
22
|
+
{
|
|
23
|
+
type: 'text',
|
|
24
|
+
name: 'name',
|
|
25
|
+
message: 'Project name?',
|
|
26
|
+
initial: 'my-sipere'
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
const answer = await prompts(questions);
|
|
30
|
+
projectName = answer.name;
|
|
31
|
+
}else {
|
|
32
|
+
projectName = name;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!projectName) return;
|
|
36
|
+
const CURRENT_DIR = __dirname;
|
|
37
|
+
await createProject(projectName, options, CURRENT_DIR);
|
|
38
|
+
}
|
|
39
|
+
|
package/lib/generator.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const installDependencies = require('./utils');
|
|
4
|
+
|
|
5
|
+
const TARGET_DIR = process.cwd();
|
|
6
|
+
|
|
7
|
+
module.exports = async function createProject(projectName, options, CURRENT_DIR) {
|
|
8
|
+
const isTypeScript = options.typescript;
|
|
9
|
+
|
|
10
|
+
const templatePath = path.join(
|
|
11
|
+
CURRENT_DIR,
|
|
12
|
+
'templates',
|
|
13
|
+
isTypeScript ? 'ts-rest-api' : 'js-rest-api'
|
|
14
|
+
);
|
|
15
|
+
const targetPath = path.join(TARGET_DIR, projectName);
|
|
16
|
+
if (fs.existsSync(targetPath)) {
|
|
17
|
+
console.log(`${targetPath} already exists`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
console.log('creating project...');
|
|
23
|
+
fs.mkdirSync(targetPath);
|
|
24
|
+
console.log('copying files...');
|
|
25
|
+
await fs.copy(templatePath, targetPath);
|
|
26
|
+
await updatePackageJson(targetPath, projectName);
|
|
27
|
+
console.log('installing dependencies...');
|
|
28
|
+
await installDependencies(targetPath);
|
|
29
|
+
printBasicHelp(projectName);
|
|
30
|
+
}catch (error) {
|
|
31
|
+
console.error(error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function updatePackageJson(targetPath, projectName) {
|
|
36
|
+
const packageJsonPath = path.join(targetPath, 'package.json');
|
|
37
|
+
if(fs.existsSync(packageJsonPath)) {
|
|
38
|
+
let pkg = await fs.readJson(packageJsonPath);
|
|
39
|
+
pkg.name = projectName;
|
|
40
|
+
pkg.description = `REST API project for ${projectName}`;
|
|
41
|
+
await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function printBasicHelp(name) {
|
|
46
|
+
console.log(`
|
|
47
|
+
Sipere REST API skeleton created
|
|
48
|
+
Read docs/user_doc.md
|
|
49
|
+
Run next commands:
|
|
50
|
+
cd ${name}
|
|
51
|
+
node op key:generate
|
|
52
|
+
npm run dev
|
|
53
|
+
Usable commands:
|
|
54
|
+
node op make:model thing
|
|
55
|
+
node op make:controller thing
|
|
56
|
+
The model and controller names must be
|
|
57
|
+
given in the singular. More info:
|
|
58
|
+
node op help
|
|
59
|
+
`);
|
|
60
|
+
}
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const { execa } = require('execa');
|
|
2
|
+
|
|
3
|
+
module.exports = async function installDependencies(projectPath) {
|
|
4
|
+
try {
|
|
5
|
+
await execa('npm', ['install'], {
|
|
6
|
+
cwd: projectPath,
|
|
7
|
+
stdio: 'inherit'
|
|
8
|
+
});
|
|
9
|
+
console.log('dependencies installed.');
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error('Error! An error occurred during "npm install".', error);
|
|
12
|
+
throw new Error('npm installation error.');
|
|
13
|
+
}
|
|
14
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-sipere",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"main": "create-sipere.js",
|
|
5
|
+
"bin": {
|
|
6
|
+
"create-sipere": "create-sipere.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "mocha"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"express",
|
|
13
|
+
"generator"
|
|
14
|
+
],
|
|
15
|
+
"author": "Sallai András",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"description": "Backend framework for Express",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/sipere/create-sipere.git"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"commander": "^14.0.2",
|
|
24
|
+
"execa": "^9.6.0",
|
|
25
|
+
"fs-extra": "^11.3.2",
|
|
26
|
+
"prompts": "^2.4.2"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"chai": "^6.2.1",
|
|
30
|
+
"exec": "^0.2.1",
|
|
31
|
+
"mocha": "^11.7.5"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import morgan from 'morgan'
|
|
3
|
+
import cors from 'cors'
|
|
4
|
+
import fs from 'fs'
|
|
5
|
+
import router from './routes/api.js'
|
|
6
|
+
import './models/modrels.js'
|
|
7
|
+
|
|
8
|
+
const app = express()
|
|
9
|
+
|
|
10
|
+
const logfile = 'access.log'
|
|
11
|
+
var accessLogStream = fs.createWriteStream(logfile, { flags: 'a' })
|
|
12
|
+
app.use(morgan('dev', { stream: accessLogStream }))
|
|
13
|
+
app.use(cors())
|
|
14
|
+
app.use(express.json())
|
|
15
|
+
app.use('/api', router);
|
|
16
|
+
|
|
17
|
+
export default app
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import bcrypt from 'bcryptjs'
|
|
2
|
+
import jwt from 'jsonwebtoken'
|
|
3
|
+
import User from '../models/user.js'
|
|
4
|
+
import dotenv from '@dotenvx/dotenvx'
|
|
5
|
+
dotenv.config({ quiet: true })
|
|
6
|
+
|
|
7
|
+
const AuthController = {
|
|
8
|
+
async register(req, res) {
|
|
9
|
+
var clientError = false;
|
|
10
|
+
try {
|
|
11
|
+
if(!req.body.name ||
|
|
12
|
+
!req.body.email ||
|
|
13
|
+
!req.body.password ||
|
|
14
|
+
!req.body.password_confirmation) {
|
|
15
|
+
clientError = true
|
|
16
|
+
throw new Error('Error! Bad request data!')
|
|
17
|
+
}
|
|
18
|
+
if(req.body.password != req.body.password_confirmation) {
|
|
19
|
+
clientError = true
|
|
20
|
+
throw new Error('Error! The two password is not same!')
|
|
21
|
+
}
|
|
22
|
+
const user = await User.findOne({
|
|
23
|
+
where: { name: req.body.name }
|
|
24
|
+
})
|
|
25
|
+
if(user) {
|
|
26
|
+
clientError = true
|
|
27
|
+
throw new Error('Error! User already exists: ' + user.name)
|
|
28
|
+
}
|
|
29
|
+
AuthController.tryRegister(req, res)
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (clientError) {
|
|
32
|
+
res.status(400)
|
|
33
|
+
}else {
|
|
34
|
+
res.status(500)
|
|
35
|
+
}
|
|
36
|
+
await res.json({
|
|
37
|
+
success: false,
|
|
38
|
+
message: 'Error! User creation failed!',
|
|
39
|
+
error: error.message
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
async tryRegister(req, res) {
|
|
44
|
+
const user = {
|
|
45
|
+
name: req.body.name,
|
|
46
|
+
email: req.body.email,
|
|
47
|
+
password: bcrypt.hashSync(req.body.password)
|
|
48
|
+
}
|
|
49
|
+
const result = await User.create(user)
|
|
50
|
+
|
|
51
|
+
res.status(201).json({
|
|
52
|
+
succes: true,
|
|
53
|
+
data: result
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
},
|
|
57
|
+
async login(req, res) {
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
if(!req.body.name || !req.body.password) {
|
|
61
|
+
res.status(400)
|
|
62
|
+
throw new Error('Error! Bad name or password!')
|
|
63
|
+
}
|
|
64
|
+
const user = await User.findOne({
|
|
65
|
+
where: { name: req.body.name }
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
if(!user) {
|
|
69
|
+
res.status(404)
|
|
70
|
+
throw new Error('Error! User not found!')
|
|
71
|
+
}
|
|
72
|
+
var passwordIsValid = await bcrypt.compare(
|
|
73
|
+
req.body.password,
|
|
74
|
+
user.dataValues.password
|
|
75
|
+
);
|
|
76
|
+
if(!passwordIsValid) {
|
|
77
|
+
res.status(401)
|
|
78
|
+
throw new Error('Error! Password is not valid!')
|
|
79
|
+
}
|
|
80
|
+
AuthController.tryLogin(req, res, user)
|
|
81
|
+
|
|
82
|
+
} catch (error) {
|
|
83
|
+
res.json({
|
|
84
|
+
success: false,
|
|
85
|
+
message: 'Error! The login is failed!',
|
|
86
|
+
error: error.message
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
async tryLogin(req, res, user) {
|
|
91
|
+
var token = jwt.sign({ id: user.id }, process.env.APP_KEY, {
|
|
92
|
+
expiresIn: 86400 //24 óra
|
|
93
|
+
})
|
|
94
|
+
res.status(200).json({
|
|
95
|
+
id: user.id,
|
|
96
|
+
name: user.name,
|
|
97
|
+
email: user.email,
|
|
98
|
+
accessToken: token
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default AuthController
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import bcrypt from 'bcryptjs'
|
|
2
|
+
import User from '../models/user.js'
|
|
3
|
+
|
|
4
|
+
const UserController = {
|
|
5
|
+
async index(req, res) {
|
|
6
|
+
try {
|
|
7
|
+
UserController.tryIndex(req, res)
|
|
8
|
+
}catch(error) {
|
|
9
|
+
res.status(500)
|
|
10
|
+
res.json({
|
|
11
|
+
success: false,
|
|
12
|
+
message: 'Error! The query is failed!'
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
async tryIndex(req, res) {
|
|
17
|
+
const users = await User.findAll()
|
|
18
|
+
res.status(200)
|
|
19
|
+
res.json({
|
|
20
|
+
success: true,
|
|
21
|
+
data: users
|
|
22
|
+
})
|
|
23
|
+
},
|
|
24
|
+
async show(req, res) {
|
|
25
|
+
try {
|
|
26
|
+
await UserController.tryShow(req, res)
|
|
27
|
+
}catch(error) {
|
|
28
|
+
res.status(500)
|
|
29
|
+
res.json({
|
|
30
|
+
success: false,
|
|
31
|
+
message: 'Error! The query is failed!'
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
async tryShow(req, res) {
|
|
36
|
+
const user = await User.findByPk(req.params.id)
|
|
37
|
+
res.status(200)
|
|
38
|
+
res.json({
|
|
39
|
+
success: true,
|
|
40
|
+
data: user
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
async create(req, res) {
|
|
44
|
+
var clientError = false;
|
|
45
|
+
try {
|
|
46
|
+
if(!req.body.name ||
|
|
47
|
+
!req.body.email ||
|
|
48
|
+
!req.body.password ||
|
|
49
|
+
!req.body.password_confirmation) {
|
|
50
|
+
clientError = true
|
|
51
|
+
throw new Error('Error! Bad request data!')
|
|
52
|
+
}
|
|
53
|
+
if(req.body.password != req.body.password_confirmation) {
|
|
54
|
+
clientError = true
|
|
55
|
+
throw new Error('Error! The two password is not same!')
|
|
56
|
+
}
|
|
57
|
+
const user = await User.findOne({
|
|
58
|
+
where: { name: req.body.name }
|
|
59
|
+
})
|
|
60
|
+
if(user) {
|
|
61
|
+
clientError = true
|
|
62
|
+
throw new Error('Error! User already exists: ' + user.name)
|
|
63
|
+
}
|
|
64
|
+
await UserController.tryCreate(req, res)
|
|
65
|
+
}catch(error) {
|
|
66
|
+
if (clientError) {
|
|
67
|
+
res.status(400)
|
|
68
|
+
}else {
|
|
69
|
+
res.status(500)
|
|
70
|
+
}
|
|
71
|
+
res.json({
|
|
72
|
+
success: false,
|
|
73
|
+
message: 'Error! The query is failed!',
|
|
74
|
+
error: error.message
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
async tryCreate(req, res) {
|
|
79
|
+
const newUser = {
|
|
80
|
+
name: req.body.name,
|
|
81
|
+
email: req.body.email,
|
|
82
|
+
password: bcrypt.hashSync(req.body.password)
|
|
83
|
+
}
|
|
84
|
+
const userData = await User.create(newUser)
|
|
85
|
+
res.status(201)
|
|
86
|
+
res.json({
|
|
87
|
+
success: true,
|
|
88
|
+
data: userData
|
|
89
|
+
})
|
|
90
|
+
},
|
|
91
|
+
async updatePassword(req, res) {
|
|
92
|
+
var clientError = false;
|
|
93
|
+
try {
|
|
94
|
+
if(!req.body.password ||
|
|
95
|
+
!req.body.password_confirmation) {
|
|
96
|
+
clientError = true
|
|
97
|
+
throw new Error('Error! Bad request data!')
|
|
98
|
+
}
|
|
99
|
+
if(req.body.password != req.body.password_confirmation) {
|
|
100
|
+
clientError = true
|
|
101
|
+
throw new Error('Error! The two password is not same!')
|
|
102
|
+
}
|
|
103
|
+
await UserController.tryUpdatePassword(req, res)
|
|
104
|
+
}catch(error) {
|
|
105
|
+
if (clientError) {
|
|
106
|
+
res.status(400)
|
|
107
|
+
}else {
|
|
108
|
+
res.status(500)
|
|
109
|
+
}
|
|
110
|
+
res.json({
|
|
111
|
+
success: false,
|
|
112
|
+
message: 'Error! The query is failed!',
|
|
113
|
+
error: error.message
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
async tryUpdatePassword(req, res) {
|
|
118
|
+
const user = await User.findByPk(req.params.id)
|
|
119
|
+
user.password = bcrypt.hashSync(req.body.password)
|
|
120
|
+
await user.save()
|
|
121
|
+
res.status(200)
|
|
122
|
+
res.json({
|
|
123
|
+
success: true,
|
|
124
|
+
data: user
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default UserController
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Sequelize from 'sequelize'
|
|
2
|
+
import dotenvFlow from 'dotenv-flow'
|
|
3
|
+
|
|
4
|
+
if (process.env.NODE_ENV === 'test') {
|
|
5
|
+
dotenvFlow.config({ path: '.env.test' })
|
|
6
|
+
}else {
|
|
7
|
+
dotenvFlow.config()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const sequelize = new Sequelize(
|
|
11
|
+
process.env.DB_NAME,
|
|
12
|
+
process.env.DB_USER,
|
|
13
|
+
process.env.DB_PASS,
|
|
14
|
+
{
|
|
15
|
+
dialect: process.env.DB_DIALECT,
|
|
16
|
+
storage: process.env.DB_STORAGE,
|
|
17
|
+
host: process.env.DB_HOST,
|
|
18
|
+
logging: process.env.APP_LOG === 'true' ? console.log : false,
|
|
19
|
+
dialectOptions: {}
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
export default sequelize
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
import dotenvFlow from 'dotenv-flow';
|
|
3
|
+
dotenvFlow.config()
|
|
4
|
+
|
|
5
|
+
const verifyToken = (req, res, next) => {
|
|
6
|
+
let authData = req.headers.authorization;
|
|
7
|
+
if(!authData) {
|
|
8
|
+
return res.status(403).send({
|
|
9
|
+
message: 'No token provided!'
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
let token = authData.split(' ')[1];
|
|
13
|
+
|
|
14
|
+
jwt.verify(token, process.env.APP_KEY, (err, decoded) => {
|
|
15
|
+
if(err) {
|
|
16
|
+
return res.status(401).send({
|
|
17
|
+
message: "Unauthorized!"
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
req.userId = decoded.id;
|
|
21
|
+
next()
|
|
22
|
+
})
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default verifyToken
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import User from './user.js';
|
|
2
|
+
import sequelize from '../database/database.js'
|
|
3
|
+
|
|
4
|
+
const db = {};
|
|
5
|
+
|
|
6
|
+
/* Import your models and write here.
|
|
7
|
+
For example User: */
|
|
8
|
+
db.User = User;
|
|
9
|
+
|
|
10
|
+
// await sequelize.sync({ alter: true })
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
Write the relationships between the models here.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export default db;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize'
|
|
2
|
+
import sequelize from '../database/database.js'
|
|
3
|
+
|
|
4
|
+
const User = sequelize.define('user', {
|
|
5
|
+
name: { type: DataTypes.STRING, allowNull: false },
|
|
6
|
+
email: { type: DataTypes.STRING, allowNull: true },
|
|
7
|
+
password: { type: DataTypes.STRING , allowNull: false },
|
|
8
|
+
roleId: { type: DataTypes.INTEGER, defaultValue: 0 }
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export default User
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import Router from 'express'
|
|
2
|
+
const router = Router()
|
|
3
|
+
|
|
4
|
+
import AuthController from '../controllers/authController.js';
|
|
5
|
+
import UserController from '../controllers/userController.js';
|
|
6
|
+
import verifyToken from '../middleware/authjwt.js';
|
|
7
|
+
|
|
8
|
+
router.post('/register', AuthController.register)
|
|
9
|
+
router.post('/login', AuthController.login)
|
|
10
|
+
router.get('/users', [verifyToken], UserController.index)
|
|
11
|
+
router.get('/users/:id', [verifyToken], UserController.show)
|
|
12
|
+
router.put('/users/:id/password', [verifyToken], UserController.updatePassword)
|
|
13
|
+
|
|
14
|
+
export default router
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
|
|
3
|
+
async function up({context: QueryInterface}) {
|
|
4
|
+
await QueryInterface.createTable('users', {
|
|
5
|
+
id: {
|
|
6
|
+
allowNull: false,
|
|
7
|
+
autoIncrement: true,
|
|
8
|
+
primaryKey: true,
|
|
9
|
+
type: DataTypes.INTEGER
|
|
10
|
+
},
|
|
11
|
+
name: {
|
|
12
|
+
type: DataTypes.STRING,
|
|
13
|
+
allowNull: false
|
|
14
|
+
},
|
|
15
|
+
email: {
|
|
16
|
+
type: DataTypes.STRING,
|
|
17
|
+
allowNull: true
|
|
18
|
+
},
|
|
19
|
+
password: {
|
|
20
|
+
type: DataTypes.STRING,
|
|
21
|
+
allowNull: false
|
|
22
|
+
},
|
|
23
|
+
roleId: {
|
|
24
|
+
type: DataTypes.INTEGER,
|
|
25
|
+
defaultValue: 0
|
|
26
|
+
},
|
|
27
|
+
createdAt: { type: DataTypes.DATE },
|
|
28
|
+
updatedAt: { type: DataTypes.DATE }
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function down({context: QueryInterface}) {
|
|
33
|
+
await QueryInterface.dropTable('users');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { up, down }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# expressapi
|
|
2
|
+
|
|
3
|
+
## API KEY
|
|
4
|
+
|
|
5
|
+
The app key generated with generate-api-key package.
|
|
6
|
+
|
|
7
|
+
## Testing
|
|
8
|
+
|
|
9
|
+
Run tests:
|
|
10
|
+
|
|
11
|
+
```cmd
|
|
12
|
+
npm test
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The test using the .env.test file and run in the memory database.
|
|
16
|
+
|
|
17
|
+
Tests can be placed in the test directory, where Mocha runs them.
|
|
18
|
+
|
|
19
|
+
## Development
|
|
20
|
+
|
|
21
|
+
Start the application:
|
|
22
|
+
|
|
23
|
+
```cmd
|
|
24
|
+
npm run dev
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Restarting the application when saving is done by nodemon.
|
|
28
|
+
|
|
29
|
+
## Migartaions and seeders
|
|
30
|
+
|
|
31
|
+
The project uses umzug for migrations and seeders.
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# User documentation
|
|
2
|
+
|
|
3
|
+
## Install dependencies
|
|
4
|
+
|
|
5
|
+
Dependencies must be installed before use.
|
|
6
|
+
|
|
7
|
+
```cmd
|
|
8
|
+
npm install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use your favorite package manager.
|
|
12
|
+
|
|
13
|
+
## Generate config file
|
|
14
|
+
|
|
15
|
+
The settings are located in a file called:
|
|
16
|
+
|
|
17
|
+
* .env
|
|
18
|
+
|
|
19
|
+
To generate the .env file:
|
|
20
|
+
|
|
21
|
+
```cmd
|
|
22
|
+
node op conf:generate
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Test configurations file generate
|
|
26
|
+
|
|
27
|
+
```cmd
|
|
28
|
+
node op testconf:generate
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Result: .env.test file.
|
|
32
|
+
|
|
33
|
+
## App key generation
|
|
34
|
+
|
|
35
|
+
Te generate the application key:
|
|
36
|
+
|
|
37
|
+
```cmd
|
|
38
|
+
node op key:generate
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Database setup
|
|
42
|
+
|
|
43
|
+
Edit the .env file.
|
|
44
|
+
|
|
45
|
+
## Endpoints
|
|
46
|
+
|
|
47
|
+
All endpoint have a /api prefix.
|
|
48
|
+
|
|
49
|
+
| Endpoint | Method | Auth | Description |
|
|
50
|
+
|-|-|-|-|
|
|
51
|
+
| /register | POST | no | create user |
|
|
52
|
+
| /login | POST | no | login |
|
|
53
|
+
| /users | GET | yes | read users |
|
|
54
|
+
| /users/:id | GET | yes | read user |
|
|
55
|
+
| /users/:id/password | PUT | yes | change password |
|
|
56
|
+
|
|
57
|
+
## The register endpoint
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"name": "joe",
|
|
62
|
+
"email": "joe@green.lan",
|
|
63
|
+
"password": "secret",
|
|
64
|
+
"password_confirmation": "secret"
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## The login endpoint
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"name": "joe",
|
|
73
|
+
"password": "secret"
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
You receive the bearear token with accessToken key.
|
|
78
|
+
|
|
79
|
+
## The users endpoint
|
|
80
|
+
|
|
81
|
+
To query users or user, send the bearer token to endpoint.
|
|
82
|
+
|
|
83
|
+
## Model and controller generation
|
|
84
|
+
|
|
85
|
+
Use the following instructions to generate models and controllers:
|
|
86
|
+
|
|
87
|
+
```cmd
|
|
88
|
+
node op make:model thing
|
|
89
|
+
node op make:controller thing
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Key generation
|
|
93
|
+
|
|
94
|
+
You can generate the application key with the following command:
|
|
95
|
+
|
|
96
|
+
```cmd
|
|
97
|
+
node op key:generate
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Generate admin user
|
|
101
|
+
|
|
102
|
+
You can create an admin user if it does not already exist in the database:
|
|
103
|
+
|
|
104
|
+
```cmd
|
|
105
|
+
node op admin:generate
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Database import
|
|
109
|
+
|
|
110
|
+
It is possible to import data from JSON and CSV files.
|
|
111
|
+
|
|
112
|
+
```cmd
|
|
113
|
+
node op db:import thing things.json
|
|
114
|
+
node op db:import thing things.csv
|
|
115
|
+
node op db:import thing things.csv ,
|
|
116
|
+
node op db:import thing things.csv ";"
|
|
117
|
+
node op db:import thing things.csv :
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The last option is the separator.
|
|
121
|
+
|
|
122
|
+
For example JSON file:
|
|
123
|
+
|
|
124
|
+
employees.json:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
[
|
|
128
|
+
{ "id": 1, "name": "Tom Large" },
|
|
129
|
+
{ "id": 2, "name": "Jack Small" }
|
|
130
|
+
]
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The default separator is comma.
|
|
134
|
+
|
|
135
|
+
```cmd
|
|
136
|
+
node op db:import employee employees.json
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
For example CSV file:
|
|
140
|
+
|
|
141
|
+
employees.csv:
|
|
142
|
+
|
|
143
|
+
```csv
|
|
144
|
+
id,name
|
|
145
|
+
1,Joe Kitin
|
|
146
|
+
2,Peter Fall
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
If you have colon separator, use sep parameter.
|
|
150
|
+
|
|
151
|
+
```csv
|
|
152
|
+
id:name
|
|
153
|
+
1:Joe Kitin
|
|
154
|
+
2:Peter Fall
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```cmd
|
|
158
|
+
node op db:import employee employees.csv --sep :
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
If the file has semicolon separator, use sep parameter, for example:
|
|
162
|
+
|
|
163
|
+
```csv
|
|
164
|
+
id;name
|
|
165
|
+
1;Joe Kitin
|
|
166
|
+
2;Peter Fall
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Use next command:
|
|
170
|
+
|
|
171
|
+
```cmd
|
|
172
|
+
node op db:import employee employees.csv --sep ";"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Database
|
|
176
|
+
|
|
177
|
+
### Database synchronization
|
|
178
|
+
|
|
179
|
+
Models and database tables can be synchronized, but this can be dangerous.
|
|
180
|
+
|
|
181
|
+
Database synchronization can be set up in the app/models/modrels.js file. Default values are:
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
{ alter: true }
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
This preserves the data and existing structure.
|
|
188
|
+
|
|
189
|
+
Possible values:
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
{ force: true }
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The latter deletes the contents of the database table!
|
|
196
|
+
|
|
197
|
+
If the value is false, there is no synchronization in either case.
|
|
198
|
+
|
|
199
|
+
### Migration
|
|
200
|
+
|
|
201
|
+
Generate a migration:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
node op make/migration thing
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
For example migration for employee table:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
node op make/migration employee
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```javascript
|
|
214
|
+
import { DataTypes } from 'sequelize';
|
|
215
|
+
|
|
216
|
+
async function up({context: QueryInterface}) {
|
|
217
|
+
await QueryInterface.createTable('employees', {
|
|
218
|
+
id: {
|
|
219
|
+
allowNull: false,
|
|
220
|
+
autoIncrement: true,
|
|
221
|
+
primaryKey: true,
|
|
222
|
+
type: DataTypes.INTEGER
|
|
223
|
+
},
|
|
224
|
+
name: {
|
|
225
|
+
type: DataTypes.STRING
|
|
226
|
+
},
|
|
227
|
+
city: {
|
|
228
|
+
type: DataTypes.STRING
|
|
229
|
+
},
|
|
230
|
+
createdAt: { type: DataTypes.DATE },
|
|
231
|
+
updatedAt: { type: DataTypes.DATE }
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function down({context: QueryInterface}) {
|
|
236
|
+
await QueryInterface.dropTable('employees');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export { up, down }
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Run all migration:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
node op migration:run
|
|
246
|
+
node op migrate
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Run a migration:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
node op migration:run <migration_name>
|
|
253
|
+
node op migrate <migration_name>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Rollback the last migration:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
node op migration:rollback
|
|
260
|
+
node op migrate:rollback
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Rollback two migrations:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
node op migration:rollback 2
|
|
267
|
+
node op migrate:rollback 2
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Reset the database:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
node op migration:reset
|
|
274
|
+
node op migrate:reset
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
This command also undoes seeder operations.
|
|
278
|
+
|
|
279
|
+
Reset the database and run all migrations:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
node op migration:fresh
|
|
283
|
+
node op migrate:fresh
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Database seed
|
|
287
|
+
|
|
288
|
+
Generate a seeder:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
node op make/seeder thing
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Example seeder for employee table:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
node op make/seeder employee
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
```javascript
|
|
301
|
+
|
|
302
|
+
async function up({context: QueryInterface}) {
|
|
303
|
+
if(db.Employee) {
|
|
304
|
+
await db.Employee.bulkCreate([
|
|
305
|
+
{ id: 1, name: "John Smith" },
|
|
306
|
+
{ id: 2, name: "Alice Johnson" }
|
|
307
|
+
]);
|
|
308
|
+
}else {
|
|
309
|
+
const now = new Date()
|
|
310
|
+
await QueryInterface.bulkInsert('things', [
|
|
311
|
+
{
|
|
312
|
+
id: 1, name: "John Smith",
|
|
313
|
+
createdAt: now, updatedAt: now
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: 2, name: "Alice Johnson",
|
|
317
|
+
createdAt: now, updatedAt: now
|
|
318
|
+
}
|
|
319
|
+
]);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function down({context: QueryInterface}) {
|
|
324
|
+
await QueryInterface.bulkDelete('things', null, {});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export { up, down }
|
|
328
|
+
````
|
|
329
|
+
|
|
330
|
+
Run all seeders:
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
node op db:seed
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Run a seeder:
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
node op db:seed path_thing
|
|
340
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "expressapi",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Express API with simple Sequelize",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "NODE_ENV=test mocha",
|
|
8
|
+
"dev": "nodemon app",
|
|
9
|
+
"start": "node app"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"express",
|
|
13
|
+
"api"
|
|
14
|
+
],
|
|
15
|
+
"author": "your name",
|
|
16
|
+
"license": "ISC",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@dotenvx/dotenvx": "^1.51.0",
|
|
19
|
+
"bcryptjs": "^2.4.3",
|
|
20
|
+
"cors": "^2.8.5",
|
|
21
|
+
"dotenv-flow": "^4.1.0",
|
|
22
|
+
"express": "^4.18.2",
|
|
23
|
+
"generate-api-key": "^1.0.2",
|
|
24
|
+
"jsonwebtoken": "^9.0.0",
|
|
25
|
+
"mariadb": "^3.1.2",
|
|
26
|
+
"morgan": "^1.10.0",
|
|
27
|
+
"read": "^4.1.0",
|
|
28
|
+
"replace": "^1.2.2",
|
|
29
|
+
"sequelize": "^6.32.0",
|
|
30
|
+
"sqlite3": "^5.1.6",
|
|
31
|
+
"umzug": "^3.8.2",
|
|
32
|
+
"yargs": "^18.0.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/bcryptjs": "^2.4.6",
|
|
36
|
+
"fs-extra": "^11.3.0",
|
|
37
|
+
"mocha": "^11.7.4",
|
|
38
|
+
"nodemon": "^2.0.22",
|
|
39
|
+
"supertest": "^6.3.3"
|
|
40
|
+
},
|
|
41
|
+
"type": "module"
|
|
42
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import supertest from 'supertest'
|
|
2
|
+
import app from '../app/app.js'
|
|
3
|
+
|
|
4
|
+
describe('/api/register and /api/login', () => {
|
|
5
|
+
const restype= 'application/json; charset=utf-8'
|
|
6
|
+
var token = null
|
|
7
|
+
|
|
8
|
+
it('post /register ', function(done) {
|
|
9
|
+
supertest(app)
|
|
10
|
+
.post('/api/register')
|
|
11
|
+
.set('Accept', 'application/json')
|
|
12
|
+
.send({
|
|
13
|
+
name: 'mari',
|
|
14
|
+
email: 'mari@zold.lan',
|
|
15
|
+
password: 'titok',
|
|
16
|
+
password_confirmation: 'titok'
|
|
17
|
+
})
|
|
18
|
+
.expect('Content-Type', restype)
|
|
19
|
+
.expect(201, done)
|
|
20
|
+
|
|
21
|
+
})
|
|
22
|
+
it('post /login ', (done) => {
|
|
23
|
+
supertest(app)
|
|
24
|
+
.post('/api/login')
|
|
25
|
+
.set('Accept', 'application/json')
|
|
26
|
+
.send({
|
|
27
|
+
name: 'mari',
|
|
28
|
+
password: 'titok'
|
|
29
|
+
})
|
|
30
|
+
.expect('Content-Type', restype)
|
|
31
|
+
.expect(200, done)
|
|
32
|
+
.expect(res => {
|
|
33
|
+
token = res.body.accessToken
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
it('get /users ', function(done) {
|
|
37
|
+
supertest(app)
|
|
38
|
+
.get('/api/users')
|
|
39
|
+
.set('Accept', 'application/json')
|
|
40
|
+
.set('Authorization', 'Bearer ' + token)
|
|
41
|
+
.expect(200, done)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "js-rest-api",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
7
|
+
},
|
|
8
|
+
"keywords": [],
|
|
9
|
+
"author": "",
|
|
10
|
+
"license": "ISC",
|
|
11
|
+
"description": "",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"cors": "^2.8.5",
|
|
14
|
+
"express": "^5.1.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"nodemon": "^3.1.11"
|
|
18
|
+
},
|
|
19
|
+
"type": "module"
|
|
20
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const { exec } = require('child_process');
|
|
2
|
+
const assert = require('assert');
|
|
3
|
+
|
|
4
|
+
describe('create-sipere', function() {
|
|
5
|
+
this.timeout(8000);
|
|
6
|
+
it('create project', (done) => {
|
|
7
|
+
const cmd = './create-sipere.js';
|
|
8
|
+
const child = exec(cmd);
|
|
9
|
+
let output = '';
|
|
10
|
+
child.stdout.on('data', (data) => {
|
|
11
|
+
output += data;
|
|
12
|
+
if(output.includes('Project name?')) {
|
|
13
|
+
child.stdin.write('tesztprojekt\n');
|
|
14
|
+
}
|
|
15
|
+
// if(output.includes('Típus:')) {
|
|
16
|
+
// child.stdin.write('typescript\n');
|
|
17
|
+
// }
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
child.on('close', (code) => {
|
|
21
|
+
assert.ok(output.includes('installing'));
|
|
22
|
+
exec('rm -rf tesztprojekt', () => {});
|
|
23
|
+
done();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|