add a a lots of things(you can see VERSION.md) flushing in version 1.0.1-aplha

This commit is contained in:
zzy 2024-04-13 17:28:38 +08:00
parent 534b397583
commit def10b10b3
37 changed files with 1414 additions and 354 deletions

View File

@ -1,26 +1,36 @@
## 项目文件结构 ## 项目文件结构
│root/ // 项目根目录
root/ ├── server.js
|- main/ ├── utils.js // 各项通用接口(未来将会更改位置)
| |- web.js 启动代码 ├── package.json
|- routers/ ├── package-lock.json
| |- *.js 路由文件 ├── .env
| |- */ 各路由文件的子文件及使用的中间件 ├── .gitignore
|- models/ 未来数据模型文件 ├── README.md
|- views/ 视图文件(一般是不允许直接访问的文件,使用中间件访问) ├── main/ // 主函数入口
|- static/ 静态文件(所有网页的静态资源存放处) │ ├── web.js // 启动代码
| |- css/ css文件 ├── routes/ // 路由
| |- js/ js文件 │ ├── index.js // 连接所有路由(其中api路由在controllers/api/index.js中)
| |- pic/ 图片文件 │ ├── files.js // 文件相关路由
| |- html/ html文件 │ └── users.js // 用户相关路由
| |- index.html 默认首页 ├── controllers/ // 控制器(主要中间件执行逻辑,用到services和utils)
|- .data/ 配置文件 │ ├── api/ // API控制器
|- .gitignore git忽略文件 │ ├── files/ // 文件控制器
|- package.json 项目依赖包 │ └── users/ // 用户控制器
|- package-lock.json 项目依赖包(自动生成) ├── services/ // 服务(主要服务逻辑,用到utils)
|- .env 配置文件(如需自己启用项目请根据default.env文件修改后启用项目) │ ├── auth.js // 身份验证服务(废案)
|- server.js 主路由代码 │ ├── files.js // 文件服务
|- utils.js 工具代码(包含路径,api返回格式) │ └── users.js // 用户服务
├── utils/ // 工具库
│ ├── db.js // 数据库定义
│ ├── auth_utils.js // 身份验证服务
│ ├── db_utils.js // 数据库接口
│ └── db_api.js // better-sqlite官方文档截至于24-04-11
└── static/
├── css/ // css
├── js/ // js
├── img/ // 图片
└── index.html // 公开的静态首页
- 默认首页简易解释 - 默认首页简易解释
static/index.html 文件需要调用 static/js/base_script.js 动态加载在 static/html/base/*.html 的共享资源且需要动态引入.data内的json数据以渲染出一张张cards static/index.html 文件需要调用 static/js/base_script.js 动态加载在 static/html/base/*.html 的共享资源且需要动态引入.data内的json数据以渲染出一张张cards

23
VERSION.md Normal file
View File

@ -0,0 +1,23 @@
# web_zzyxyz Version History
## [Version 1.0.0] - 2024-04-11
### History
0. 较于曾今版本重新开发由于安全问题重新启用git。
1. 修改了静态网页的代码并使用booststrap框架和anchor模板。
2. 对于曾经的express框架对代码进行重构。
3. 使用了dotenv模块加载配置项。
4. 修改部分文件架构尤其是各类隐藏文件夹。
## [Version 1.0.1-alpha] - 2023-04-13
### New Features
0. 大量修改文件结构详情见README.md。
1. 添加了登录,注册,管理员的功能。
2. 使用better-sqlite3重构数据库接口。
3. 重构代码结构使其路由更清晰更易于使用cookie(未来将会重构cookie代码)。
4. 添加VERSION.md文件记录版本历史。
5. 添加各种细节。
### Bugs Fixed
0. 修改default.env文件使其初始化密码简易包括硬性写入代码中的密码(default.env文件就是代码内的各项参数的默认值)。

6
controllers/api/index.js Normal file
View File

@ -0,0 +1,6 @@
const router = require('express').Router();
router.use('/users', require('../users/api.js'));
router.use('/files', require('../files/api.js'));
module.exports = router;

6
controllers/files/api.js Normal file
View File

@ -0,0 +1,6 @@
const router = require('express').Router();
const { getJsonFile } = require('./json');
router.get('/json/:filename', getJsonFile);
module.exports = router;

View File

@ -0,0 +1,8 @@
const { getJsonPath } = require('../../services/files');
function getJsonFile(req, res, next) {
const filename = req.params.filename;
res.sendFile(getJsonPath(filename));
};
module.exports = { getJsonFile };

17
controllers/users/api.js Normal file
View File

@ -0,0 +1,17 @@
const router = require('express').Router();
const {login, getJson, register } = require('./users');
router.post('/login', login);
router.post('/register', register);
router.get('/admin/:filename', getJson);
//remote-data-processing-display
// router.post('/_rdpd', function(req, res, next) {
// express.response.send = function(data) {
// res.json({...utils.json.success,
// data: data
// })
// }
// });
module.exports = router;

View File

@ -0,0 +1,63 @@
const {checkAdmin, checkUsers, registerUsers, getAllUsersToJson, VIEWS_USERS_PATH} = require('../../services/users');
const path = require('path');
const auth = require('../../utils/auth_utils');
// <reference types="express"/>
// const { Request, Response } = require('express');
function login(req, res, next) {
const data = req.body;
const isAdmin = req.query.admin !== undefined; // 检查查询参数
if (isAdmin) {
res.json(checkAdmin(data.username, data.password));
} else {
res.json(checkUsers(data.username, data.password));
}
};
function getJson(req, res, next) {
const filename = req.params.filename;
if (filename === 'users_cards.json') {
return res.json(getAllUsersToJson(token));
}
}
function register(req, res, next) {
const data = req.body;
res.json(registerUsers(data.username, data.password));
};
//------------------------------------//
function getLogin(req, res, next) {
res.sendFile(path.join(VIEWS_USERS_PATH, 'login.html'));
}
function getRoot(req, res, next) {
token = req.cookies.token;
if (typeof token === 'string') {
return auth.verifyToken(token, (err, data) => {
if (err) {
res.redirect('/users/login');
return;
}
res.sendFile(path.join(VIEWS_USERS_PATH, 'users.html'));
})
}
res.redirect('/users/login');
}
function getAdmin(req, res, next) {
token = req.cookies.token;
if (typeof token === 'string') {
return auth.verifyToken(token, (err, data) => {
if (err) {
res.redirect('/users/login?admin');
return;
}
res.sendFile(path.join(VIEWS_USERS_PATH, 'admin.html'));
})
}
res.redirect('/users/login?admin');
}
module.exports = { getRoot, getAdmin, getLogin, login, register, getJson };

View File

@ -1,5 +1,5 @@
LISTEN_PORT=3000 LISTEN_PORT=3000
JWT_SECRET=YZ2Cgx82t4wEBJ7w8ibLNwYoQSoXuxLG JWT_SECRET=123456
DATABASE_NAME=sqlite3.db DATABASE_NAME=sqlite3.db
USER_LOGIN_NAME=admin ADMIN_LOGIN_NAME=admin
USER_LOGIN_PASSWORD=MoAHzbzmZ9HZT5SGrhu9XyD7wUtxJRbW ADMIN_LOGIN_PASSWORD=admin

View File

@ -1,6 +1,6 @@
{ {
"name": "zzy_web", "name": "web_zzyxyz",
"version": "1.0.0", "version": "1.0.1",
"description": "None", "description": "None",
"main": "./main/web.js", "main": "./main/web.js",
"scripts": { "scripts": {
@ -10,11 +10,11 @@
"author": "zzy", "author": "zzy",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"better-sqlite3": "^9.5.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"nodemon": "^3.1.0", "nodemon": "^3.1.0"
"sqlite3": "^5.1.7"
} }
} }

View File

@ -1,10 +1,7 @@
const utils = require('../utils.js');
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
router.use('/users', require('./api/users.js')); router.use(require('../controllers/api/index'));
router.use('/files', require('./api/files.js'));
router.use('/json', require('./api/json.js'));
module.exports = router; module.exports = router;

View File

@ -1,9 +0,0 @@
const utils = require('../../utils.js');
const router = require('express').Router();
router.get('/:filename', function(req, res, next) {
const filename = req.params.filename;
res.sendFile(utils.path.join(utils.DATA_JSON_PATH, filename));
});
module.exports = router;

View File

@ -1,36 +0,0 @@
const utils = require('../../utils.js');
const express = require('express');
const auth = require('../utils/auth_utils');
const router = express.Router();
router.post('/login', function(req, res, next) {
const data = req.body;
if (data.username === (utils.env.USER_LOGIN_NAME || 'admin') &&
data.password === (utils.env.USER_LOGIN_PASSWORD || 'admin') ){
const token = auth.generateToken({
username: data.username,
id: -1
})
res.json({...utils.json.success,
data: token
})
} else {
res.json({...utils.json.user_is_valid})
}
});
//remote-data-processing-display
router.post('/_rdpd', function(req, res, next) {
express.response.send = function(data) {
res.json({...utils.json.success,
data: data
})
}
});
router.get('/admin', function(req, res, next) {
res.send('admin');
});
module.exports = router;

View File

@ -1,7 +0,0 @@
const utils = require('../../utils.js');
const sqlite3DB = require(utils.path.join(utils.RT_UTILS_PATH, 'db_utils'));
const db_path = utils.path.join(utils.DATA_DB_PATH, utils.env.DATABASE_NAME || 'sqlite3.db');
const db = new sqlite3DB(db_path);
module.exports = db;

View File

@ -1,19 +0,0 @@
const db = require('./db.js');
db.createTable('users', [
{name :'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'},
{name :'name', type: 'VARCHAR(32) NOT NULL UNIQUE'},
{name :'password', type: `VARCHAR(255) NOT NULL DEFAULT '123456'`},
{name :'update_time', type: 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP'}
]);
// db.insertData('users', data);
// db.deleteData('users', `name='${data[0].name}'`);
// db.updateData('users', data[2], `name='${data[1].name}'`);
// db.selectData('users', ['name', 'email', 'password'], `name='${data[0].name}'`, (err, rows) => {
// console.log(rows);
// })
// db.selectData('users', ['name', 'email', 'password'], null, function(err, rows) {
// console.log(rows);
// });
// db.closeConnection();

View File

@ -1,7 +1,8 @@
const utils = require('../utils.js');
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
router.use('/users', require('./users.js'));
router.use('/files', require('./files.js'));
router.use('/api', require('../controllers/api/index.js'));
module.exports = router;
module.exports = router;

View File

@ -1,38 +1,11 @@
const utils = require('../utils.js'); const router = require('express').Router();
const express = require('express'); const { getRoot, getAdmin, getLogin } = require('../controllers/users/users.js');
const db = require(utils.path.join(utils.RT_DB_PATH, 'users.js'));
const auth = require(utils.path.join(utils.RT_UTILS_PATH, 'auth_utils.js'));
const router = express.Router();
/* GET users listing. */ /* GET users listing. */
router.get('/', function(req, res, next) { router.get('/', getRoot);
token = req.cookies.token; router.get('/admin', getAdmin);
if (typeof token === 'string') { router.get('/login', getLogin);
return auth.verifyToken(token, (err, data) => {
if (err) {
res.redirect('/users/login');
return;
}
res.sendFile(utils.path.join(utils.VIEWS_PATH, 'users/users.html'));
})
}
res.redirect('/users/login');
});
/**
* GET /users/login
* @param {*} req
* @param {*} res
* @param {*} next
*/
router.get('/login', function(req, res, next) {
res.sendFile(utils.path.join(utils.VIEWS_PATH, 'users/login.html'));
})
router.use('/admin', function(req, res, next) {
res.send('admin');
});
// WILL BE FILLING // WILL BE FILLING
router.get('/register', function(req, res, next) { router.get('/register', function(req, res, next) {

View File

@ -1,40 +0,0 @@
const utils = require('../../utils.js');
const jwt = require('jsonwebtoken');
const generateToken = (payload, options) => {
return jwt.sign(payload, utils.env.JWT_SECRET || 'YZ2Cgx82t4wEBJ7w8ibLNwYoQSoXuxLG', {
expiresIn: 60,
...options
});
};
const verifyToken = (token, callback) => {
return jwt.verify(token, utils.env.JWT_SECRET || 'YZ2Cgx82t4wEBJ7w8ibLNwYoQSoXuxLG', callback);
};
function checkToken(req, res, next) {
const token = req.get('token');
if (!token) {
return res.json({
code: '0001',
message: 'token is required',
data: null
});
}
verifyToken(token, (err, data) => {
if (err) {
return res.json({
code: '0002',
message: 'token is invalid',
data: null
});
}
req.tokenData = data;
next();
})
}
module.exports = {
generateToken,
verifyToken
};

View File

@ -1,138 +0,0 @@
const sqlite3 = require('sqlite3').verbose();
class sqlite3DB {
constructor(databaseName, errorFunc = null) {
if (!databaseName) {
throw new Error('Database name is required.');
}
this.errorFunc = errorFunc || (err => {
console.error(`sql: ${err.SQL}\nerror: ${err.message}`);
});
this.db = new sqlite3.Database(databaseName, (err) => {
if (err) {
console.error(`Failed to open database: ${err.message}`);
}
});
}
runSql(sql, data, callback) {
if (data == null) {
data = undefined;
}
this.db.run(sql, data, (err) => {
if (err) {
err.SQL = sql;
this.errorFunc(err);
if (typeof callback === 'function') {
callback(err);
}
}
});
}
runSerialize(sql, data, callback) {
this.db.serialize(() => {
this.runSql(sql, data, callback);
});
}
prepareSql(sql, callback) {
return this.db.prepare(sql, (stmt, err) => {
if (err) {
err.SQL = sql;
this.errorFunc(err);
if (typeof callback === 'function') {
callback(err);
}
return;
}
return stmt;
});
}
createTable(tableName, columnDefinitions, callback) {
const columns = columnDefinitions.map(column => `${column.name} ${column.type}`).join(', ');
const sql = `CREATE TABLE IF NOT EXISTS ${tableName} (${columns})`;
this.db.serialize(() => {
this.runSql(sql, null, callback);
});
}
insertData(tableName, data, callback) {
if (!Array.isArray(data)) {
data = [data];
}
const columns = Object.keys(data[0]).join(', ');
const values = Object.keys(data[0]).map(() => '?').join(', ');
const sql = `INSERT INTO ${tableName} (${columns}) VALUES (${values})`;
this.db.serialize(() => {
const stmt = this.prepareSql(sql, callback);
data.forEach((item) => {
stmt.run(Object.values(item), (err) => { if (err) {
err.SQL = sql;
this.errorFunc(err);
if (typeof callback === 'function') {
callback(err);
}
} });
});
stmt.finalize((err) => { if(err) {
err.SQL = sql;
this.errorFunc(err);
if (typeof callback === 'function') {
callback(err);
}
} });
});
}
selectData(tableName, columns, where, callback) {
const sql = `SELECT ${columns} FROM ${tableName}
${where && typeof where !== 'function' ? ` WHERE ${where}` : ''}`;
return this.db.all(sql, [], (err, rows) => {
if (err) {
err.SQL = sql;
this.errorFunc(err);
if (typeof callback === 'function') {
callback(err);
}
}
if (typeof callback === 'function') {
callback(undefined, rows);
} else {
return rows;
}
});
}
updateData(tableName, _data, where, callback) {
var data = null;
if (Array.isArray(_data)) {
data = _data[0];
} else {
data = _data;
}
const columns = Object.keys(data).map((values) => {
return `${values}=?`;
}).join(', ');
const sql = `UPDATE ${tableName} SET ${columns}
${where && typeof where !== 'function' ? ` WHERE ${where}` : ''}`;
const values = Object.values(data);
this.runSql(sql, values, callback);
}
deleteData(tableName, where, callback) {
const sql = `DELETE FROM ${tableName}
${where && typeof where !== 'function' ? ` WHERE ${where}` : ''}`;
this.runSql(sql, callback);
}
closeConnection() {
this.db.close((err) => { if (err) {
err.SQL = 'ERROR close connection'
this.errorFunc('ERROR db close', err);
}});
}
}
module.exports = sqlite3DB;

View File

@ -11,9 +11,6 @@ app.use(require('cookie-parser')());
app.use(express.static(path.join(__dirname, 'static'))); app.use(express.static(path.join(__dirname, 'static')));
app.use('/', require('./routes/index.js')); app.use('/', require('./routes/index.js'));
app.use('/users', require('./routes/users.js'));
app.use('/files', require('./routes/files.js'));
app.use('/api', require('./routes/api.js'));
// catch 404 and forward to error handler // catch 404 and forward to error handler
app.use(function(req, res, next) { app.use(function(req, res, next) {

15
services/files.js Normal file
View File

@ -0,0 +1,15 @@
const utils = require('../utils.js');
const path = require('path');
const fs = require('fs');
const DATA_JSON_PATH = path.resolve(utils.DATA_PATH, 'json');
if (!fs.existsSync(DATA_JSON_PATH)) {
fs.mkdirSync(DATA_JSON_PATH, { recursive: true });
}
function getJsonPath(filename) {
return path.resolve(DATA_JSON_PATH, filename);
}
module.exports = { getJsonPath };

102
services/users.js Normal file
View File

@ -0,0 +1,102 @@
const utils = require('../utils.js');
const path = require('path');
const auth = require('../utils/auth_utils');
const db = require('../utils/db');
db.createTable('users', [
{name :'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'},
{name :'name', type: 'VARCHAR(32) NOT NULL UNIQUE'},
{name :'password', type: `VARCHAR(255) NOT NULL DEFAULT '123456'`},
{name :'update_time', type: 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP'}
]);
function getUser(username) {
let ret = db.selectData('users', null, `name='${username}'`);
let info = db.info;
return [ret, info];
}
function addUser(username, password) {
let ret = db.insertData('users', {name: username, password: password});
if (ret == false) {
return [false, db.err.code];
}
let info = db.info;
return [ret, info];
}
function getAllUsers() {
let ret = db.selectData('users');
let info = db.info;
return [ret, info];
}
function checkAdmin(adminname, password) {
if (adminname === (utils.env.ADMIN_LOGIN_NAME || 'admin') &&
password === (utils.env.ADMIN_LOGIN_PASSWORD || 'admin') ) {
const token = auth.generateToken({
username: adminname,
id: -1
});
return {...utils.json.success, data: {token: token, href: '/users/admin'}};
} else {
return {...utils.json.user_is_invalid};
}
}
function checkUsers(username, password) {
let [ret, info] = getUser(username);
if (!ret) {
return {...utils.json.user_is_invalid, data: info};
} else if (!info || (Array.isArray(info) && info.length === 1)) {
info = info[0];
if (info.password !== password) {
return {...utils.json.user_is_invalid, data: 'password error'};
} else {
const token = auth.generateToken({
username: username,
id: info.id
})
return {...utils.json.success, data: {token: token, href: '/users'}};
}
} else {
return {...utils.json.user_is_invalid, data: 'user name error'};
}
}
function registerUsers(username, password) {
let [ret, info] = addUser(username, password);
if (!ret) {
if (info === 'SQLITE_CONSTRAINT_UNIQUE') {
info = 'user name is exist';
} else {
info = 'unknown error';
}
return {...utils.json.user_register_is_invalid, data: info};
} else {
return {...utils.json.success};
}
}
function getAllUsersToJson(token) {
if (typeof token === 'string') {
return auth.verifyToken(token, (err) => {
if (err) {
return {...utils.json.token_is_invalid, data: info};
}
let [ret, info] = getAllUsers();
if (ret) {
return {...utils.json.success, data: info};
} else {
return {...utils.json.error};
}
});
} else {
return {...utils.json.token_is_invalid, data: null};
}
}
const VIEWS_USERS_PATH = path.resolve(utils.VIEWS_PATH, 'users')
module.exports = { checkAdmin, checkUsers, registerUsers, getAllUsersToJson, VIEWS_USERS_PATH };

View File

@ -15,10 +15,10 @@
<div> <div>
联系作者: 联系作者:
</div> </div>
<a href="./html/autor-link/author-linking-wechat.html" target="_blank" class=""> <a href="/html/autor-link/author-linking-wechat.html" target="_blank" class="">
<img class="rounded-circle shadow" src="/pic/ico/wechat.png" alt="wechat-link" width="25"> <img class="rounded-circle shadow" src="/pic/ico/wechat.png" alt="wechat-link" width="25">
</a> </a>
<a href="./html/autor-link/author-linking-qq.html" target="_blank" class=""> <a href="/html/autor-link/author-linking-qq.html" target="_blank" class="">
<img class="rounded-circle shadow" src="/pic/ico/qq.png" alt="qq-link" width="25"> <img class="rounded-circle shadow" src="/pic/ico/qq.png" alt="qq-link" width="25">
</a> </a>
</div> </div>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -33,7 +33,7 @@
<script src="/js/base_script.js" type="text/javascript" charset="utf-8"></script> <script src="/js/base_script.js" type="text/javascript" charset="utf-8"></script>
<script> <script>
$(function() { $(function() {
$.getJSON("/api/json/cs-study-link-content.json", function(data) { $.getJSON("/api/files/json/cs-study-link-content.json", function(data) {
const default_item = data.default; const default_item = data.default;
data.datas.forEach(i => { data.datas.forEach(i => {
item = {...default_item, ...i} item = {...default_item, ...i}

View File

@ -33,7 +33,7 @@
<script src="/js/base_script.js" type="text/javascript" charset="utf-8"></script> <script src="/js/base_script.js" type="text/javascript" charset="utf-8"></script>
<script> <script>
$(document).ready( function() { $(document).ready( function() {
$.getJSON("/api/json/office-link.json", function(data) { $.getJSON("/api/files/json/office-link.json", function(data) {
const default_item = data.default; const default_item = data.default;
data.datas.forEach(i => { data.datas.forEach(i => {
item = {...default_item, ...i} item = {...default_item, ...i}

View File

@ -33,7 +33,7 @@
<script src="/js/base_script.js" type="text/javascript" charset="utf-8"></script> <script src="/js/base_script.js" type="text/javascript" charset="utf-8"></script>
<script> <script>
$(document).ready( function() { $(document).ready( function() {
$.getJSON("/api/json/self-study-link.json", function(data) { $.getJSON("/api/files/json/self-study-link.json", function(data) {
const default_item = data.default; const default_item = data.default;
data.datas.forEach(i => { data.datas.forEach(i => {
item = {...default_item, ...i} item = {...default_item, ...i}

View File

@ -52,7 +52,7 @@
<script src="./js/base_script.js" type="text/javascript"></script> <script src="./js/base_script.js" type="text/javascript"></script>
<script> <script>
$(function() { $(function() {
$.getJSON("/api/json/index-content.json", function(data) { $.getJSON("/api/files/json/index-content.json", function(data) {
const default_item = data.default; const default_item = data.default;
data.datas.forEach(i => { data.datas.forEach(i => {
item = {...default_item, ...i} item = {...default_item, ...i}

View File

@ -7,6 +7,17 @@ $(".include").each(function() {
} }
}); });
function alertMessage(alertPosId = '#alert-pos', title = 'alert', msg = 'msg', color = 'info') {
const alertCard = `
<div id="alert-message" class="alert alert-${color} alert-dismissible fade show" role="alert">
<strong>${title}</strong> ${msg}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>`;
$(`${alertPosId}`).append(alertCard);
}
// FIX ME // FIX ME
setTimeout(function() { setTimeout(function() {
const $_zzy_base_modal_ = $('#_zzy_base_modal_'); const $_zzy_base_modal_ = $('#_zzy_base_modal_');

View File

@ -10,10 +10,6 @@ utils.ROOT_PATH = path.resolve(__dirname);
utils.DATA_DB_PATH = path.resolve(utils.DATA_PATH, 'db'); utils.DATA_DB_PATH = path.resolve(utils.DATA_PATH, 'db');
utils.DATA_CONFIG_PATH = path.resolve(utils.DATA_PATH, 'config'); utils.DATA_CONFIG_PATH = path.resolve(utils.DATA_PATH, 'config');
utils.DATA_FILE_PATH = path.resolve(utils.DATA_PATH, 'file'); utils.DATA_FILE_PATH = path.resolve(utils.DATA_PATH, 'file');
utils.ROUTES_PATH = path.resolve(utils.ROOT_PATH, 'routes');
utils.RT_DB_PATH = path.resolve(utils.ROUTES_PATH, 'db');
utils.RT_API_PATH = path.resolve(utils.ROUTES_PATH, 'api');
utils.RT_UTILS_PATH = path.resolve(utils.ROUTES_PATH, 'utils');
// 检查并创建路径 // 检查并创建路径
[utils.DATA_PATH, utils.DATA_JSON_PATH, utils.DATA_DB_PATH, utils.DATA_CONFIG_PATH, utils.DATA_FILE_PATH].forEach((p) => { [utils.DATA_PATH, utils.DATA_JSON_PATH, utils.DATA_DB_PATH, utils.DATA_CONFIG_PATH, utils.DATA_FILE_PATH].forEach((p) => {
@ -38,11 +34,16 @@ utils.json = {
msg: 'token invalid', msg: 'token invalid',
data: null data: null
}, },
user_is_valid: { user_is_invalid: {
code: '0100', code: '0100',
msg: 'login error', msg: 'login error',
data: null data: null
}, },
user_register_is_invalid: {
code: '0102',
msg: 'register error',
data: null
},
} }
utils.path = path; utils.path = path;

40
utils/auth_utils.js Normal file
View File

@ -0,0 +1,40 @@
const utils = require('../utils.js');
const jwt = require('jsonwebtoken');
const generateToken = (payload, options) => {
return jwt.sign(payload, utils.env.JWT_SECRET || '123456', {
expiresIn: 60,
...options
});
};
const verifyToken = (token, callback) => {
return jwt.verify(token, utils.env.JWT_SECRET || '123456', callback);
};
// function checkToken(req, res, next) {
// const token = req.get('token');
// if (!token) {
// return res.json({
// code: '0001',
// message: 'token is required',
// data: null
// });
// }
// verifyToken(token, (err, data) => {
// if (err) {
// return res.json({
// code: '0002',
// message: 'token is invalid',
// data: null
// });
// }
// req.tokenData = data;
// next();
// })
// }
module.exports = {
generateToken,
verifyToken
};

8
utils/db.js Normal file
View File

@ -0,0 +1,8 @@
const utils = require('../utils.js');
const path = require('path');
const betterSqlite3DB = require('./db_utils');
const db_path = path.join(utils.DATA_DB_PATH, utils.env.DATABASE_NAME || 'sqlite3.db');
const db = new betterSqlite3DB(db_path);
module.exports = db;

661
utils/db_api.md Normal file
View File

@ -0,0 +1,661 @@
# API
- [class `Database`](#class-database)
- [class `Statement`](#class-statement)
- [class `SqliteError`](#class-sqliteerror)
- [Binding Parameters](#binding-parameters)
# class *Database*
- [new Database()](#new-databasepath-options)
- [Database#prepare()](#preparestring---statement) (see [`Statement`](#class-statement))
- [Database#transaction()](#transactionfunction---function)
- [Database#pragma()](#pragmastring-options---results)
- [Database#backup()](#backupdestination-options---promise)
- [Database#serialize()](#serializeoptions---buffer)
- [Database#function()](#functionname-options-function---this)
- [Database#aggregate()](#aggregatename-options---this)
- [Database#table()](#tablename-definition---this)
- [Database#loadExtension()](#loadextensionpath-entrypoint---this)
- [Database#exec()](#execstring---this)
- [Database#close()](#close---this)
- [Properties](#properties)
### new Database(*path*, [*options*])
Creates a new database connection. If the database file does not exist, it is created. This happens synchronously, which means you can start executing queries right away. You can create an [in-memory database](https://www.sqlite.org/inmemorydb.html) by passing `":memory:"` as the first argument. You can create a temporary database by passing an empty string (or by omitting all arguments).
> In-memory databases can also be created by passing a buffer returned by [`.serialize()`](#serializeoptions---buffer), instead of passing a string as the first argument.
Various options are accepted:
- `options.readonly`: open the database connection in readonly mode (default: `false`).
- `options.fileMustExist`: if the database does not exist, an `Error` will be thrown instead of creating a new file. This option is ignored for in-memory, temporary, or readonly database connections (default: `false`).
- `options.timeout`: the number of milliseconds to wait when executing queries on a locked database, before throwing a `SQLITE_BUSY` error (default: `5000`).
- `options.verbose`: provide a function that gets called with every SQL string executed by the database connection (default: `null`).
- `options.nativeBinding`: if you're using a complicated build system that moves, transforms, or concatenates your JS files, `better-sqlite3` might have trouble locating its native C++ addon (`better_sqlite3.node`). If you get an error that looks like [this](https://github.com/JoshuaWise/better-sqlite3/issues/534#issuecomment-757907190), you can solve it by using this option to provide the file path of `better_sqlite3.node` (relative to the current working directory).
```js
const Database = require('better-sqlite3');
const db = new Database('foobar.db', { verbose: console.log });
```
### .prepare(*string*) -> *Statement*
Creates a new prepared [`Statement`](#class-statement) from the given SQL string.
```js
const stmt = db.prepare('SELECT name, age FROM cats');
```
### .transaction(*function*) -> *function*
Creates a function that always runs inside a [transaction](https://sqlite.org/lang_transaction.html). When the function is invoked, it will begin a new transaction. When the function returns, the transaction will be committed. If an exception is thrown, the transaction will be rolled back (and the exception will propagate as usual).
```js
const insert = db.prepare('INSERT INTO cats (name, age) VALUES (@name, @age)');
const insertMany = db.transaction((cats) => {
for (const cat of cats) insert.run(cat);
});
insertMany([
{ name: 'Joey', age: 2 },
{ name: 'Sally', age: 4 },
{ name: 'Junior', age: 1 },
]);
```
Transaction functions can be called from inside other transaction functions. When doing so, the inner transaction becomes a [savepoint](https://www.sqlite.org/lang_savepoint.html).
If an error is thrown inside of a nested transaction function, the nested transaction function will roll back to the state just before the savepoint and rethrow the error. If the error is not caught in the outer transaction function, this will cause the outer transaction function to roll back as well.
```js
const newExpense = db.prepare('INSERT INTO expenses (note, dollars) VALUES (?, ?)');
const adopt = db.transaction((cats) => {
newExpense.run('adoption fees', 20);
insertMany(cats); // nested transaction
});
```
Transactions also come with `deferred`, `immediate`, and `exclusive` versions.
```js
insertMany(cats); // uses "BEGIN"
insertMany.deferred(cats); // uses "BEGIN DEFERRED"
insertMany.immediate(cats); // uses "BEGIN IMMEDIATE"
insertMany.exclusive(cats); // uses "BEGIN EXCLUSIVE"
```
Any arguments passed to the transaction function will be forwarded to the wrapped function, and any values returned from the wrapped function will be returned from the transaction function. The wrapped function will also have access to the same [`this`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this) binding as the transaction function.
#### Caveats
If you'd like to manage transactions manually, you're free to do so with regular [prepared statements](#preparestring---statement) (using `BEGIN`, `COMMIT`, etc.). However, manually managed transactions should not be mixed with transactions managed by this `.transaction()` method. In other words, using raw `COMMIT` or `ROLLBACK` statements inside a transaction function is not supported.
Transaction functions do not work with async functions. Technically speaking, async functions always return after the first `await`, which means the transaction will already be committed before any async code executes. Also, because SQLite3 serializes all transactions, it's generally a very bad idea to keep a transaction open across event loop ticks anyways.
It's important to know that SQLite3 may sometimes rollback a transaction without us asking it to. This can happen either because of an [`ON CONFLICT`](https://sqlite.org/lang_conflict.html) clause, the [`RAISE()`](https://www.sqlite.org/lang_createtrigger.html) trigger function, or certain errors such as `SQLITE_FULL` or `SQLITE_BUSY`. In other words, if you catch an SQLite3 error *within* a transaction, you must be aware that any further SQL that you execute might not be within the same transaction. Usually, the best course of action for such cases is to simply re-throw the error, exiting the transaction function.
```js
try {
...
} catch (err) {
if (!db.inTransaction) throw err; // (transaction was forcefully rolled back)
...
}
```
### .pragma(*string*, [*options*]) -> *results*
Executes the given PRAGMA and returns its result. By default, the return value will be an array of result rows. Each row is represented by an object whose keys correspond to column names.
Since most PRAGMA statements return a single value, the `simple` option is provided to make things easier. When `simple` is `true`, only the first column of the first row will be returned.
```js
db.pragma('cache_size = 32000');
console.log(db.pragma('cache_size', { simple: true })); // => 32000
```
If execution of the PRAGMA fails, an `Error` is thrown.
It's better to use this method instead of normal [prepared statements](#preparestring---statement) when executing PRAGMA, because this method normalizes some odd behavior that may otherwise be experienced. The documentation on SQLite3 PRAGMA can be found [here](https://www.sqlite.org/pragma.html).
### .backup(*destination*, [*options*]) -> *promise*
Initiates a [backup](https://www.sqlite.org/backup.html) of the database, returning a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) for when the backup is complete. If the backup fails, the promise will be rejected with an `Error`. You can optionally backup an attached database instead by setting the `attached` option to the name of the desired attached database. A backup file is just a regular SQLite3 database file. It can be opened by [`new Database()`](#new-databasepath-options) just like any SQLite3 database.
```js
db.backup(`backup-${Date.now()}.db`)
.then(() => {
console.log('backup complete!');
})
.catch((err) => {
console.log('backup failed:', err);
});
```
You can continue to use the database normally while a backup is in progress. If the same database connection mutates the database while performing a backup, those mutations will be reflected in the backup automatically. However, if a *different* connection mutates the database during a backup, the backup will be forcefully restarted. Therefore, it's recommended that only a single connection is responsible for mutating the database if online backups are being performed.
You can monitor the progress of the backup by setting the `progress` option to a callback function. That function will be invoked every time the backup makes progress, providing an object with two properties:
- `.totalPages`: the total number of pages in the source database (and thus, the number of pages that the backup will have when completed) at the time of this progress report.
- `.remainingPages`: the number of pages that still must be transferred before the backup is complete.
By default, `100` [pages](https://www.sqlite.org/fileformat.html#pages) will be transferred after each cycle of the event loop. However, you can change this setting as often as you like by returning a number from the `progress` callback. You can even return `0` to effectively pause the backup altogether. In general, the goal is to maximize throughput while reducing pause times. If the transfer size is very low, pause times will be low, but it may take a while to complete the backup. On the flip side, if the setting is too high, pause times will be greater, but the backup might complete sooner. In most cases, `100` has proven to be a strong compromise, but the best setting is dependent on your computer's specifications and the nature of your program. Do not change this setting without measuring the effectiveness of your change. You should not assume that your change will even have the intended effect, unless you measure it for your unique situation.
If the backup is successful, the returned promise will contain an object that has the same properties as the one provided to the `progress` callback, but `.remainingPages` will be `0`. If the `progress` callback throws an exception, the backup will be aborted. Usually this happens due to an unexpected error, but you can also use this behavior to voluntarily cancel the backup operation. If the parent database connection is closed, all pending backups will be automatically aborted.
```js
let paused = false;
db.backup(`backup-${Date.now()}.db`, {
progress({ totalPages: t, remainingPages: r }) {
console.log(`progress: ${((t - r) / t * 100).toFixed(1)}%`);
return paused ? 0 : 200;
}
});
```
### .serialize([*options*]) -> *Buffer*
Returns a [buffer](https://nodejs.org/api/buffer.html#buffer_class_buffer) containing the serialized contents of the database. You can optionally serialize an attached database instead by setting the `attached` option to the name of the desired attached database.
The returned buffer can be written to disk to create a regular SQLite3 database file, or it can be opened directly as an in-memory database by passing it to [`new Database()`](#new-databasepath-options).
```js
const buffer = db.serialize();
db.close();
db = new Database(buffer);
```
### .function(*name*, [*options*], *function*) -> *this*
Registers a user-defined `function` so that it can be used by SQL statements.
```js
db.function('add2', (a, b) => a + b);
db.prepare('SELECT add2(?, ?)').pluck().get(12, 4); // => 16
db.prepare('SELECT add2(?, ?)').pluck().get('foo', 'bar'); // => "foobar"
db.prepare('SELECT add2(?, ?, ?)').pluck().get(12, 4, 18); // => Error: wrong number of arguments
```
By default, user-defined functions have a strict number of arguments (determined by `function.length`). You can register multiple functions of the same name, each with a different number of arguments, causing SQLite3 to execute a different function depending on how many arguments were passed to it. If you register two functions with same name and the same number of arguments, the second registration will erase the first one.
If `options.varargs` is `true`, the registered function can accept any number of arguments.
If `options.directOnly` is `true`, the registered function can only be invoked from top-level SQL, and cannot be used in [VIEWs](https://sqlite.org/lang_createview.html), [TRIGGERs](https://sqlite.org/lang_createtrigger.html), or schema structures such as [CHECK constraints](https://www.sqlite.org/lang_createtable.html#ckconst), [DEFAULT clauses](https://www.sqlite.org/lang_createtable.html#dfltval), etc.
If your function is [deterministic](https://en.wikipedia.org/wiki/Deterministic_algorithm), you can set `options.deterministic` to `true`, which may improve performance under some circumstances.
```js
db.function('void', { deterministic: true, varargs: true }, () => {});
db.prepare("SELECT void()").pluck().get(); // => null
db.prepare("SELECT void(?, ?)").pluck().get(55, 19); // => null
```
### .aggregate(*name*, *options*) -> *this*
Registers a user-defined [aggregate function](https://sqlite.org/lang_aggfunc.html).
```js
db.aggregate('addAll', {
start: 0,
step: (total, nextValue) => total + nextValue,
});
db.prepare('SELECT addAll(dollars) FROM expenses').pluck().get(); // => 92
```
The `step()` function will be invoked once for each row passed to the aggregate, using its return value as the new aggregate value. This works similarly to [Array#reduce()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce).
If `options.start` is a function, it will be invoked at the beginning of each aggregate, using its return value as the initial aggregate value. If `options.start` is *not* a function, it will be used as the initial aggregate value *as-is* (shown in the example above). If not provided, the initial aggregate value will be `null`.
You can also provide a `result()` function to transform the final aggregate value:
```js
db.aggregate('getAverage', {
start: () => [],
step: (array, nextValue) => {
array.push(nextValue);
},
result: array => array.reduce(sum) / array.length,
});
db.prepare('SELECT getAverage(dollars) FROM expenses').pluck().get(); // => 20.2
```
As shown above, you can use arbitrary JavaScript objects as your aggregation context, as long as a valid SQLite3 value is returned by `result()` in the end. If `step()` doesn't return anything (`undefined`), the aggregate value will not be replaced (be careful of this when using functions that return `undefined` when `null` is desired).
Just like regular [user-defined functions](#functionname-options-function---this), user-defined aggregates can accept multiple arguments. Furthermore, `options.varargs`, `options.directOnly`, and `options.deterministic` [are also](#functionname-options-function---this) accepted.
If you provide an `inverse()` function, the aggregate can be used as a [window function](https://www.sqlite.org/windowfunctions.html). Where `step()` is used to add a row to the current window, `inverse()` is used to remove a row from the current window. When using window functions, `result()` may be invoked multiple times.
```js
db.aggregate('addAll', {
start: 0,
step: (total, nextValue) => total + nextValue,
inverse: (total, droppedValue) => total - droppedValue,
result: total => Math.round(total),
});
db.prepare(`
SELECT timestamp, dollars, addAll(dollars) OVER day as dayTotal
FROM expenses
WINDOW day AS (PARTITION BY date(timestamp))
ORDER BY timestamp
`).all();
```
### .table(*name*, *definition*) -> *this*
Registers a [virtual table](https://www.sqlite.org/vtab.html). Virtual tables can be queried just like real tables, except their results do not exist in the database file; instead, they are calculated on-the-fly by a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) in JavaScript.
```js
const fs = require('fs');
db.table('filesystem_directory', {
columns: ['filename', 'data'],
rows: function* () {
for (const filename of fs.readdirSync(process.cwd())) {
const data = fs.readFileSync(filename);
yield { filename, data };
}
},
});
const files = db.prepare('SELECT * FROM filesystem_directory').all();
// => [{ filename, data }, { filename, data }]
```
To generate a row in a virtual table, you can either yield an object whose keys correspond to column names, or yield an array whose elements represent columns in the order that they were declared. Every virtual table **must** declare its columns via the `columns` option.
Virtual tables can be used like [table-valued functions](https://www.sqlite.org/vtab.html#tabfunc2); you can pass parameters to them, unlike regular tables.
```js
db.table('regex_matches', {
columns: ['match', 'capture'],
rows: function* (pattern, text) {
const regex = new RegExp(pattern, 'g');
let match;
while (match = regex.exec(text)) {
yield [match[0], match[1]];
}
},
});
const stmt = db.prepare("SELECT * FROM regex('\\$(\\d+)', ?)");
stmt.all('Desks cost $500 and chairs cost $27');
// => [{ match: '$500', capture: '500' }, { match: '$27', capture: '27' }]
```
By default, the number of parameters accepted by a virtual table is inferred by `function.length`, and the parameters are automatically named `$1`, `$2`, etc. However, you can optionally provide an explicit list of parameters via the `parameters` option.
```js
db.table('regex_matches', {
columns: ['match', 'capture'],
parameters: ['pattern', 'text'],
rows: function* (pattern, text) {
...
},
});
```
> In virtual tables, parameters are actually [*hidden columns*](https://www.sqlite.org/vtab.html#hidden_columns_in_virtual_tables), and they can be selected in the result set of a query, just like any other column. That's why it may sometimes be desirable to give them explicit names.
When querying a virtual table, any omitted parameters will be `undefined`. You can use this behavior to implement required parameters and default parameter values.
```js
db.table('sequence', {
columns: ['value'],
parameters: ['length', 'start'],
rows: function* (length, start = 0) {
if (length === undefined) {
throw new TypeError('missing required parameter "length"');
}
const end = start + length;
for (let n = start; n < end; ++n) {
yield { value: n };
}
},
});
db.prepare('SELECT * FROM sequence(10)').pluck().all();
// => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
```
> Note that when using syntax like `start = 0` for default parameter values (shown above), the function's `.length` property does not include the optional parameter, so you need to explicitly declare `parameters` in this case.
Normally, when you register a virtual table, the virtual table *automatically exists* without needing to run a `CREATE VIRTUAL TABLE` statement. However, if you provide a factory function as the second argument (a function that *returns* virtual table definitions), then no virtual table will be created automatically. Instead, you can create multiple similar virtual tables by running [`CREATE VIRTUAL TABLE`](https://sqlite.org/lang_createvtab.html) statements, each with their own module arguments. Think of it like defining a virtual table "class" that can be instantiated by running `CREATE VIRTUAL TABLE` statements.
```js
const fs = require('fs');
db.table('csv', (filename) => {
const firstLine = getFirstLineOfFile(filename);
return {
columns: firstLine.split(','),
rows: function* () {
// This is just an example. Real CSV files are more complicated to parse.
const contents = fs.readFileSync(filename, 'utf8');
for (const line of contents.split('\n')) {
yield line.split(',');
}
},
};
});
db.exec('CREATE VIRTUAL TABLE my_data USING csv(my_data.csv)');
const allData = db.prepare('SELECT * FROM my_data').all();
```
The factory function will be invoked each time a corresponding `CREATE VIRTUAL TABLE` statement runs. The arguments to the factory function correspond to the module arguments passed in the `CREATE VIRTUAL TABLE` statement; always a list of arbitrary strings separated by commas. It's your responsibility to parse and interpret those module arguments. Note that SQLite3 does not allow [bound parameters](#binding-parameters) inside module arguments.
Just like [user-defined functions](#functionname-options-function---this) and [user-defined aggregates](#aggregatename-options---this), virtual tables support `options.directOnly`, which prevents the table from being used inside [VIEWs](https://sqlite.org/lang_createview.html), [TRIGGERs](https://sqlite.org/lang_createtrigger.html), or schema structures such as [CHECK constraints](https://www.sqlite.org/lang_createtable.html#ckconst), [DEFAULT clauses](https://www.sqlite.org/lang_createtable.html#dfltval), etc.
> Some [extensions](#loadextensionpath-entrypoint---this) can provide virtual tables that have write capabilities, but `db.table()` is only capable of creating read-only virtual tables, primarily for the purpose of supporting table-valued functions.
### .loadExtension(*path*, [*entryPoint*]) -> *this*
Loads a compiled [SQLite3 extension](https://sqlite.org/loadext.html) and applies it to the current database connection.
It's your responsibility to make sure the extensions you load are compiled/linked against a version of [SQLite3](https://www.sqlite.org/) that is compatible with `better-sqlite3`. Keep in mind that new versions of `better-sqlite3` will periodically use newer versions of [SQLite3](https://www.sqlite.org/). You can see which version is being used [here](./compilation.md#bundled-configuration).
```js
db.loadExtension('./my-extensions/compress.so');
```
### .exec(*string*) -> *this*
Executes the given SQL string. Unlike [prepared statements](#preparestring---statement), this can execute strings that contain multiple SQL statements. This function performs worse and is less safe than using [prepared statements](#preparestring---statement). You should only use this method when you need to execute SQL from an external source (usually a file). If an error occurs, execution stops and further statements are not executed. You must rollback changes manually.
```js
const migration = fs.readFileSync('migrate-schema.sql', 'utf8');
db.exec(migration);
```
### .close() -> *this*
Closes the database connection. After invoking this method, no statements can be created or executed.
```js
process.on('exit', () => db.close());
process.on('SIGHUP', () => process.exit(128 + 1));
process.on('SIGINT', () => process.exit(128 + 2));
process.on('SIGTERM', () => process.exit(128 + 15));
```
## Properties
**.open -> _boolean_** - Whether the database connection is currently open.
**.inTransaction -> _boolean_** - Whether the database connection is currently in an open transaction.
**.name -> _string_** - The string that was used to open the database connection.
**.memory -> _boolean_** - Whether the database is an in-memory or temporary database.
**.readonly -> _boolean_** - Whether the database connection was created in readonly mode.
# class *Statement*
An object representing a single SQL statement.
- [Statement#run()](#runbindparameters---object)
- [Statement#get()](#getbindparameters---row)
- [Statement#all()](#allbindparameters---array-of-rows)
- [Statement#iterate()](#iteratebindparameters---iterator)
- [Statement#pluck()](#plucktogglestate---this)
- [Statement#expand()](#expandtogglestate---this)
- [Statement#raw()](#rawtogglestate---this)
- [Statement#columns()](#columns---array-of-objects)
- [Statement#bind()](#bindbindparameters---this)
- [Properties](#properties-1)
### .run([*...bindParameters*]) -> *object*
Executes the prepared statement. When execution completes it returns an `info` object describing any changes made. The `info` object has two properties:
- `info.changes`: the total number of rows that were inserted, updated, or deleted by this operation. Changes made by [foreign key actions](https://www.sqlite.org/foreignkeys.html#fk_actions) or [trigger programs](https://www.sqlite.org/lang_createtrigger.html) do not count.
- `info.lastInsertRowid`: the [rowid](https://www.sqlite.org/lang_createtable.html#rowid) of the last row inserted into the database (ignoring those caused by [trigger programs](https://www.sqlite.org/lang_createtrigger.html)). If the current statement did not insert any rows into the database, this number should be completely ignored.
If execution of the statement fails, an `Error` is thrown.
You can specify [bind parameters](#binding-parameters), which are only bound for the given execution.
```js
const stmt = db.prepare('INSERT INTO cats (name, age) VALUES (?, ?)');
const info = stmt.run('Joey', 2);
console.log(info.changes); // => 1
```
### .get([*...bindParameters*]) -> *row*
**(only on statements that return data)*
Executes the prepared statement. When execution completes it returns an object that represents the first row retrieved by the query. The object's keys represent column names.
If the statement was successful but found no data, `undefined` is returned. If execution of the statement fails, an `Error` is thrown.
You can specify [bind parameters](#binding-parameters), which are only bound for the given execution.
```js
const stmt = db.prepare('SELECT age FROM cats WHERE name = ?');
const cat = stmt.get('Joey');
console.log(cat.age); // => 2
```
### .all([*...bindParameters*]) -> *array of rows*
**(only on statements that return data)*
Similar to [`.get()`](#getbindparameters---row), but instead of only retrieving one row all matching rows will be retrieved. The return value is an array of row objects.
If no rows are found, the array will be empty. If execution of the statement fails, an `Error` is thrown.
You can specify [bind parameters](#binding-parameters), which are only bound for the given execution.
```js
const stmt = db.prepare('SELECT * FROM cats WHERE name = ?');
const cats = stmt.all('Joey');
console.log(cats.length); // => 1
```
### .iterate([*...bindParameters*]) -> *iterator*
**(only on statements that return data)*
Similar to [`.all()`](#allbindparameters---array-of-rows), but instead of returning every row together, an [iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) is returned so you can retrieve the rows one by one. If you plan on retrieving every row anyways, [`.all()`](#allbindparameters---array-of-rows) will perform slightly better.
If execution of the statement fails, an `Error` is thrown and the iterator is closed.
You can specify [bind parameters](#binding-parameters), which are only bound for the given execution.
```js
const stmt = db.prepare('SELECT * FROM cats');
for (const cat of stmt.iterate()) {
if (cat.name === 'Joey') {
console.log('found him!');
break;
}
}
```
### .pluck([toggleState]) -> *this*
**(only on statements that return data)*
Causes the prepared statement to only return the value of the first column of any rows that it retrieves, rather than the entire row object.
You can toggle this on/off as you please:
```js
stmt.pluck(); // plucking ON
stmt.pluck(true); // plucking ON
stmt.pluck(false); // plucking OFF
```
> When plucking is turned on, [expansion](#expandtogglestate---this) and [raw mode](#rawtogglestate---this) are turned off (they are mutually exclusive options).
### .expand([toggleState]) -> *this*
**(only on statements that return data)*
Causes the prepared statement to return data namespaced by table. Each key in a row object will be a table name, and each corresponding value will be a nested object that contains the associated column data. This is useful when performing a `JOIN` between two tables that have overlapping column names. If a result column is an expression or subquery, it will be available within the special `$` namespace.
You can toggle this on/off as you please:
```js
stmt.expand(); // expansion ON
stmt.expand(true); // expansion ON
stmt.expand(false); // expansion OFF
```
> When expansion is turned on, [plucking](#plucktogglestate---this) and [raw mode](#rawtogglestate---this) are turned off (they are mutually exclusive options).
### .raw([toggleState]) -> *this*
**(only on statements that return data)*
Causes the prepared statement to return rows as arrays instead of objects. This is primarily used as a performance optimization when retrieving a very high number of rows. Column names can be recovered by using the [`.columns()`](#columns---array-of-objects) method.
You can toggle this on/off as you please:
```js
stmt.raw(); // raw mode ON
stmt.raw(true); // raw mode ON
stmt.raw(false); // raw mode OFF
```
> When raw mode is turned on, [plucking](#plucktogglestate---this) and [expansion](#expandtogglestate---this) are turned off (they are mutually exclusive options).
### .columns() -> *array of objects*
**(only on statements that return data)*
This method is primarily used in conjunction with [raw mode](#rawtogglestate---this). It returns an array of objects, where each object describes a result column of the prepared statement. Each object has the following properties:
- `.name`: the name (or alias) of the result column.
- `.column`: the name of the originating table column, or `null` if it's an expression or subquery.
- `.table`: the name of the originating table, or `null` if it's an expression or subquery.
- `.database`: the name of the originating database, or `null` if it's an
expression or subquery.
- `.type`: the name of the [declared type](https://www.sqlite.org/datatype3.html#determination_of_column_affinity), or `null` if it's an expression or subquery.
```js
const fs = require('fs');
function* toRows(stmt) {
yield stmt.columns().map(column => column.name);
yield* stmt.raw().iterate();
}
function writeToCSV(filename, stmt) {
return new Promise((resolve, reject) => {
const stream = fs.createWriteStream(filename);
for (const row of toRows(stmt)) {
stream.write(row.join(',') + '\n');
}
stream.on('error', reject);
stream.end(resolve);
});
}
```
> When a table's schema is altered, existing prepared statements might start returning different result columns. However, such changes will not be reflected by this method until the prepared statement is re-executed. For this reason, it's perhaps better to invoke `.columns()` _after_ `.get()`, `.all()`, or `.iterate()`.
### .bind([*...bindParameters*]) -> *this*
[Binds the given parameters](#binding-parameters) to the statement *permanently*. Unlike binding parameters upon execution, these parameters will stay bound to the prepared statement for its entire life.
After a statement's parameters are bound this way, you may no longer provide it with execution-specific (temporary) bound parameters.
This method is primarily used as a performance optimization when you need to execute the same prepared statement many times with the same bound parameters.
```js
const stmt = db.prepare('SELECT * FROM cats WHERE name = ?').bind('Joey');
const cat = stmt.get();
console.log(cat.name); // => "Joey"
```
## Properties
**.database -> _object_** - The parent database object.
**.source -> _string_** - The source string that was used to create the prepared statement.
**.reader -> _boolean_** - Whether the prepared statement returns data.
**.readonly -> _boolean_** - Whether the prepared statement is readonly, meaning it does not mutate the database (note that [SQL functions might still change the database indirectly](https://www.sqlite.org/c3ref/stmt_readonly.html) as a side effect, even if the `.readonly` property is `true`).
**.busy -> _boolean_** - Whether the prepared statement is busy executing a query via the [`.iterate()`](#iteratebindparameters---iterator) method.
# class *SqliteError*
Whenever an error occurs within SQLite3, a `SqliteError` object will be thrown. `SqliteError` is a subclass of `Error`. Every `SqliteError` object has a `code` property, which is a string matching one of the "extended result codes" defined [here](https://sqlite.org/rescode.html) (for example, `"SQLITE_CONSTRAINT_UNIQUE"`).
If you receive a `SqliteError`, it probably means you're using SQLite3 incorrectly. The error didn't originate in `better-sqlite3`, so it's probably not an issue with `better-sqlite3`. It's recommended that you learn about the meaning of the error [here](https://sqlite.org/rescode.html), and perhaps learn more about how to use SQLite3 by reading [their docs](https://sqlite.org/docs.html).
> In the unlikely scenario that SQLite3 throws an error that is not recognized by `better-sqlite3` (this would be considered a bug in `better-sqlite3`), the `code` property will be `"UNKNOWN_SQLITE_ERROR_NNNN"`, where `NNNN` is the numeric error code. If this happens to you, please report it as an [issue](https://github.com/JoshuaWise/better-sqlite3/issues).
# Binding Parameters
This section refers to anywhere in the documentation that specifies the optional argument [*`...bindParameters`*].
There are many ways to bind parameters to a prepared statement. The simplest way is with anonymous parameters:
```js
const stmt = db.prepare('INSERT INTO people VALUES (?, ?, ?)');
// The following are equivalent.
stmt.run('John', 'Smith', 45);
stmt.run(['John', 'Smith', 45]);
stmt.run(['John'], ['Smith', 45]);
```
You can also use named parameters. SQLite3 provides [3 different syntaxes for named parameters](https://www.sqlite.org/lang_expr.html) (`@foo`, `:foo`, and `$foo`), all of which are supported by `better-sqlite3`.
```js
// The following are equivalent.
const stmt = db.prepare('INSERT INTO people VALUES (@firstName, @lastName, @age)');
const stmt = db.prepare('INSERT INTO people VALUES (:firstName, :lastName, :age)');
const stmt = db.prepare('INSERT INTO people VALUES ($firstName, $lastName, $age)');
const stmt = db.prepare('INSERT INTO people VALUES (@firstName, :lastName, $age)');
stmt.run({
firstName: 'John',
lastName: 'Smith',
age: 45
});
```
Below is an example of mixing anonymous parameters with named parameters.
```js
const stmt = db.prepare('INSERT INTO people VALUES (@name, @name, ?)');
stmt.run(45, { name: 'Henry' });
```
Here is how `better-sqlite3` converts values between SQLite3 and JavaScript:
|SQLite3|JavaScript|
|---|---|
|`NULL`|`null`|
|`REAL`|`number`|
|`INTEGER`|`number` [or `BigInt`](https://github.com/JoshuaWise/better-sqlite3/blob/master/docs/integer.md#the-bigint-primitive-type)|
|`TEXT`|`string`|
|`BLOB`|[`Buffer`](https://nodejs.org/api/buffer.html#buffer_class_buffer)|

113
utils/db_utils.js Normal file
View File

@ -0,0 +1,113 @@
const betterSqlite3 = require('better-sqlite3');
class betterSqlite3DB {
constructor(databaseName, errorFunc = undefined) {
if (!databaseName) {
throw new Error('Database name is required.');
}
this.errorFunc = errorFunc === null ? undefined : (errorFunc || (() => {
console.error(`DB error: ${this.err.code} - sql: ${this.err.sql} - data: ${this.err.data}`);
}));
this.err = {};
this.info = null;
this.db = new betterSqlite3(databaseName, { });
}
runStmt(stmt, data = [], func = undefined, throwError = false) {
try {
if (typeof func === 'function') {
this.info = func(stmt);
} else {
this.info = stmt.run(data);
}
this.err.err = null;
this.err.sql = null;
this.err.data = null;
return true; // No error occurred
} catch (err) {
this.err.code = err.code !== undefined ? err.code : 'none err code';
this.err.sql = stmt.source;
this.err.data = data;
if (this.errorFunc) {
this.errorFunc();
}
if (throwError) {
throw err;
}
return false; // Return the error if it occurred
}
}
runSql(sql, data = [], func = undefined, throwError = false) {
const stmt = this.db.prepare(sql);
return this.runStmt(stmt, data, func, throwError);
}
createTable(tableName, columnDefinitions) {
const columns = columnDefinitions.map(column => `${column.name} ${column.type}`).join(', ');
const sql = `CREATE TABLE IF NOT EXISTS ${tableName} (${columns})`;
this.db.exec(sql);
}
insertData(tableName, data) {
if (!Array.isArray(data)) {
data = [data];
}
const columns = Object.keys(data[0]).join(', ');
const values = Object.keys(data[0]).map(() => '?').join(', ');
const sql = `INSERT INTO ${tableName} (${columns}) VALUES (${values})`;
const stmt = this.db.prepare(sql);
try {
this.db.transaction(() => {
data.forEach(item => this.runStmt(stmt, Object.values(item), undefined, true));
})();
} catch (err) {
return false;
}
return true;
}
selectData(tableName, columns, where) {
const sql = `SELECT ${columns ? columns : '*'} FROM ${tableName}
${where && typeof where !== 'function' ? ` WHERE ${where}` : ''}`;
return this.runSql(sql, where, (stmt) => {
return stmt.all();
});
}
updateData(tableName, _data, where) {
let data = null;
if (Array.isArray(_data)) {
data = _data[0];
} else {
data = _data;
}
const columns = Object.keys(data).map((values) => {
return `${values}=?`;
}).join(', ');
const sql = `UPDATE ${tableName} SET ${columns}
${where && typeof where !== 'function' ? ` WHERE ${where}` : ''}`;
const values = Object.values(data);
return this.runSql(sql, values);
}
deleteData(tableName, where) {
const sql = `DELETE FROM ${tableName}
${where && typeof where !== 'function' ? ` WHERE ${where}` : ''}`;
return this.runSql(sql);
}
closeConnection() {
this.db.close();
}
runInTransaction(callback) {
try {
this.db.transaction(callback)();
} catch (err) {
return err;
}
}
}
module.exports = betterSqlite3DB;

75
utils/test/test_db.js Normal file
View File

@ -0,0 +1,75 @@
const utils = require('../../utils.js');
const betterSqlite3DB = require('../db_utils');
const db_path = utils.path.join(utils.DATA_DB_PATH, utils.env.DATABASE_NAME || 'sqlite3.db');
const db = new betterSqlite3DB(db_path, null);
db.createTable('test', [
{name :'id', type: 'INTEGER PRIMARY KEY AUTOINCREMENT'},
{name :'name', type: 'TEXT NOT NULL UNIQUE'},
{name :'age', type: `INTEGER NOT NULL DEFAULT '123456'`},
{name :'update_time', type: 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP'}
]);
if (!db.deleteData('test')) {
console.log('delete error');
}
if (!db.insertData('test', {
name: 'a',
age: 18
})) {
console.log('insert 1 error');
} else {
console.log('insert 1 success');
}
if (!db.insertData('test', {
name: 'a',
age: 18
})) {
console.log('insert 2 error');
} else {
console.log('insert 2 success');
}
if (!db.insertData('test', [
{name: 'b', age: 19},
{name: 'c', age: 20}
])) {
console.log('insert 3 error');
} else {
console.log('insert 3 success');
}
if (db.selectData('test', ['id', 'name', 'age'])) {
console.log(db.info);
} else {
console.log('select error');
}
if (db.selectData('test', ['id', 'name', 'age'], `name='b'`)) {
console.log(db.info);
} else {
console.log('select error');
}
if (db.selectData('test', ['id', 'name', 'age'], `name='e'`)) {
console.log(db.info);
} else {
console.log('select error');
}
if (db.updateData('test', {name: 'b', age: 18}, `name='b'`)) {
console.log(db.info);
} else {
console.log('select error');
}
if (db.selectData('test')) {
console.log(db.info);
} else {
console.log('select error');
}
module.exports = db;

86
views/users/admin.html Normal file
View File

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<meta content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, shrink-to-fit=no' name='viewport'/>
<!-- Fonts -->
<!-- <link href="https://fonts.googleapis.com/css?family=Nunito:300,300i,400,600,700" rel="stylesheet"> -->
<!-- <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous"> -->
<!-- <link href="./css/anchor/fonts_all.css" rel="stylesheet"/> -->
<!-- CSS -->
<link href="/css/anchor/main.css" rel="stylesheet"/>
<link href="/css/anchor/vendor/aos.css" rel="stylesheet"/>
<title>zzyxyz</title>
<link rel="icon" href="/pic/ico/Home.png">
</head>
<body>
<div class="include" src="/html/base/base_nav.html"></div>
<main class="container pb-5 pt-5">
<div id="linking-cards" class="row"></div>
<!-- table -->
<table class="table">
<thead class="thead-light">
<tr>
<th scope="col"> # </th>
<th scope="col"> First </th>
<th scope="col"> Last </th>
<th scope="col"> create-time </th>
</tr>
</thead>
<tbody id="table-body">
</tbody>
</table>
</main>
<div class="include" src="/html/base/base_footer.html"></div>
<!-- Javascript -->
<script src="/js/anchor/vendor/jquery.min.js" type="text/javascript"></script>
<script src="/js/anchor/vendor/popper.min.js" type="text/javascript"></script>
<script src="/js/anchor/vendor/bootstrap.min.js" type="text/javascript"></script>
<script src="/js/anchor/functions.js" type="text/javascript"></script>
<script src="/js/base_script.js" type="text/javascript" charset="utf-8"></script>
<script src="/fetch-wrapper.js" type="text/javascript" charset="utf-8"></script>
<script>
(async () => {
try {
const wrapper = new FetchWrapper(window.location.origin);
const response = await wrapper.get('/api/users/admin/users_cards.json');
if (response.data.code === '0000') {
response.data.data.forEach(item => {
const card = `
<tr>
<th scope="row"> ${item.id} </th>
<td> ${item.name} </td>
<td> ${item.password} </td>
<td> ${item.update_time} </td>
</tr>`;
$("#table-body").append(card);
});
}
} catch (error) {
console.error('Error fetching:', error);
}
// $.getJSON(, function(data) {
// const default_item = data.default;
// data.datas.forEach(i => {
// item = {...default_item, ...i}
// $("#linking-cards").append(card);
// });
// })
// <div class="card shadow-sm border-0 col-md-4">
// <!-- <img class="card-img-top" src="./assets/img/demo/2.jpg" alt="Card image cap"> -->
// <div class="card-body">
// <h5 class="card-title">${item.id}</h5>
// <p class="card-text text-muted">${item.name}</p>
// <p class="card-text text-muted">${item.password}</p>
// <p class="card-text text-muted">${item.update_time}</p>
// </div>
// </div>
})();
</script>
</body>
</html>

View File

@ -35,14 +35,40 @@
<label class="form-check-label" for="exampleCheck1">Check me out</label> <label class="form-check-label" for="exampleCheck1">Check me out</label>
</div> </div>
<button type="submit" class="btn btn-success btn-round">Login</button> <button type="submit" class="btn btn-success btn-round">Login</button>
<button type="button" class="btn btn-success btn-round disabled">Register</button> <button type="button" class="btn btn-success btn-round" data-toggle="modal" data-target="#register">Register</button>
<p class="message"></p> <div id="alert-pos"></div>
</form> </form>
</div> </div>
</main> </main>
<!-- The modal itself -->
<div class="modal fade" id="register" tabindex="-1" role="dialog" aria-labelledby="base_modal_center_title" aria-modal="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="container col-md-8">
<form id="register-form">
<div class="form-group">
<h4>Register Form</h4>
</div>
<div class="form-group">
<input type="text" class="form-control input-round" id="r_username" placeholder="Enter Username" required="">
<small id="usernameHelp" class="form-text text-muted">We'll never share your username and password with anyone else.</small>
</div>
<div class="form-group">
<input type="password" class="form-control input-round" id="r_password" placeholder="Password" required="">
</div>
<div class="form-group form-inline">
<button type="submit" class="btn btn-success btn-round">Register</button>
<button type="button" class="btn btn-info btn-round" data-dismiss="modal">Close</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="include" src="/html/base/base_footer.html"></div> <div class="include" src="/html/base/base_footer.html"></div>
<!-- Javascript --> <!-- Javascript -->
<script src="/js/anchor/vendor/jquery.min.js" type="text/javascript"></script> <script src="/js/anchor/vendor/jquery.min.js" type="text/javascript"></script>
<script src="/js/anchor/vendor/popper.min.js" type="text/javascript"></script> <script src="/js/anchor/vendor/popper.min.js" type="text/javascript"></script>
@ -51,45 +77,115 @@
<script src="/js/base_script.js" type="text/javascript" charset="utf-8"></script> <script src="/js/base_script.js" type="text/javascript" charset="utf-8"></script>
<script src="/fetch-wrapper.js" type="text/javascript" charset="utf-8"></script> <script src="/fetch-wrapper.js" type="text/javascript" charset="utf-8"></script>
<script> <script>
document.getElementById('login-form').addEventListener('submit', function(event) { $(() => {
event.preventDefault();
$('#login-form').on('submit', function(event) {
event.preventDefault();
var username = document.getElementById('username').value;
var password = document.getElementById('password').value;
// 这里仅做简单的输入非空验证,实际应用中需要后端配合进行账号密码验证
if (!username || !password) {
alertMessage(
undefined,
undefined,
undefined,
undefined);
} else {
// 假设这里发起一个异步登录请求
login(username, password);
}
});
$('#register-form').on('submit', function(event) {
event.preventDefault();
var username = document.getElementById('r_username').value;
var password = document.getElementById('r_password').value;
// 这里仅做简单的输入非空验证,实际应用中需要后端配合进行账号密码验证
if (!username || !password) {
alertMessage(
undefined,
undefined,
undefined,
undefined);
} else {
// 假设这里发起一个异步注册请求
register(username, password);
}
});
})
var username = document.getElementById('username').value;
var password = document.getElementById('password').value;
// 这里仅做简单的输入非空验证,实际应用中需要后端配合进行账号密码验证
if (!username || !password) {
document.querySelector('.message').textContent = 'Both username and password are required.';
} else {
document.querySelector('.message').textContent = '';
// 假设这里发起一个异步登录请求
login(username, password);
}
});
async function login(username, password) { async function login(username, password) {
try { try {
// 这里应该调用后端API进行登录操作以下仅为模拟示例 // 这里应该调用后端API进行登录操作以下仅为模拟示例
const wrapper = new FetchWrapper(window.location.origin); const wrapper = new FetchWrapper(window.location.origin);
const response = await wrapper.post('/api/users/login', { const search = window.location.search.toString();
const response = await wrapper.post(`/api/users/login${search}`, {
username: username, username: username,
password: password password: password
}) })
if (response.data.code === '0000') { if (response.data.code === '0000') {
// 登录成功,跳转到主页 // 登录成功,跳转到主页
document.cookie = `token=${response.data.data};`; document.cookie = `token=${response.data.data.token}; path=/;`;
window.location.href = '/users'; // expires=${expirationDate.toUTCString()}; SameSite=Strict; Secure; HttpOnly;
window.location.href = response.data.data.href;
} else { } else {
// 登录失败,给出错误提示 // 登录失败,给出错误提示
document.querySelector('.message').textContent = alertMessage(
'Login failed.登录失败...' + response.data.code + undefined,
'请检查用户名和密码是否正确, 或联系管理员添加新账号'; 'Login failed.登录失败...',
`error code:${response.data.code} 请检查用户名和密码是否正确, 或联系管理员添加新账号`,
'danger');
} }
} catch (error) { } catch (error) {
console.error('Error fetching:', error); console.error('Error fetching:', error);
} }
} }
async function register(username, password) {
try {
// 这里应该调用后端API进行登录操作以下仅为模拟示例
const wrapper = new FetchWrapper(window.location.origin);
const response = await wrapper.post('/api/users/register', {
username: username,
password: password
})
if (response.data.code === '0000') {
// 登录成功,跳转到主页
alertMessage(
undefined,
'Register success.注册成功...',
undefined,
'success');
} else {
// 登录失败,给出错误提示
alertMessage(
undefined,
'register failed.注册失败...',
`error code:${response.data.code} 请检查用户名和密码是否正确, 或联系管理员添加新账号`,
'danger');
}
} catch (error) {
console.error('Error fetching:', error);
} finally {
$('#register').modal('hide');
}
}
</script>
<script>
// Disable the default closing behavior for the modal
$('#register').on('show.bs.modal', function (event) {
$(this).data('bs.modal')._config.backdrop = 'static';
$(this).data('bs.modal')._config.keyboard = false;
});
</script> </script>
</body> </body>
</html> </html>