jsql-neo 5.4.2 → 6.0.0-beta2
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/README.md +198 -14
- package/index.d.ts +29 -0
- package/index.js +12 -2
- package/lib/ast.js +214 -0
- package/lib/database.js +26 -60
- package/lib/mongo_server.js +1 -1
- package/lib/native_client.js +43 -66
- package/lib/plugin.js +227 -3
- package/lib/plugins/audit.js +37 -0
- package/lib/plugins/index.js +72 -0
- package/lib/plugins/logger.js +35 -0
- package/lib/plugins/metrics.js +38 -0
- package/lib/plugins/timestamps.js +43 -0
- package/lib/plugins/validation.js +128 -0
- package/lib/sql.js +1212 -84
- package/lib/wasm_client.js +41 -57
- package/native/jsql-neo-native.node +0 -0
- package/nativesrc/jsql-neo-core/Cargo.lock +8 -1
- package/nativesrc/jsql-neo-core/Cargo.toml +3 -1
- package/nativesrc/jsql-neo-core/src/engine/hybrid.rs +50 -13
- package/nativesrc/jsql-neo-core/src/engine/memory.rs +46 -23
- package/nativesrc/jsql-neo-core/src/engine/mod.rs +13 -1
- package/nativesrc/jsql-neo-core/src/engine/table.rs +338 -42
- package/nativesrc/jsql-neo-core/src/storage/wal.rs +245 -32
- package/nativesrc/jsql-neo-native/Cargo.lock +9 -2
- package/nativesrc/jsql-neo-native/Cargo.toml +9 -1
- package/nativesrc/jsql-neo-native/src/lib.rs +14 -2
- package/nativesrc/jsql-neo-wasm/Cargo.lock +9 -2
- package/nativesrc/jsql-neo-wasm/Cargo.toml +8 -1
- package/nativesrc/jsql-neo-wasm/src/lib.rs +18 -2
- package/package.json +4 -3
- package/test/sql-parser.test.js +450 -0
- package/wasm/browser.mjs +12 -0
- package/wasm/browser_bg.mjs +269 -248
- package/wasm/jsql_neo_wasm.d.ts +6 -0
- package/wasm/jsql_neo_wasm.js +248 -127
- package/wasm/jsql_neo_wasm_bg.wasm +0 -0
- package/wasm/jsql_neo_wasm_bg.wasm.d.ts +22 -22
- package/wasm/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* logger —— 把数据库操作记录到自定义 logger。
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* db.use(require('jsql-neo/lib/plugins').logger({ logger: myLogger, tables: ['users'] }));
|
|
6
|
+
*/
|
|
7
|
+
const { definePlugin } = require('../plugin');
|
|
8
|
+
|
|
9
|
+
function count(rows) {
|
|
10
|
+
if (Array.isArray(rows)) return rows.length;
|
|
11
|
+
return rows === undefined || rows === null ? 0 : 1;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function createLogger(opts = {}) {
|
|
15
|
+
const logger = typeof opts.logger === 'function' ? opts.logger : (...a) => console.log('[jsql]', ...a);
|
|
16
|
+
const only = opts.tables ? new Set(opts.tables) : null;
|
|
17
|
+
const show = t => !only || only.has(t);
|
|
18
|
+
|
|
19
|
+
return definePlugin('logger', {
|
|
20
|
+
config: { tables: opts.tables || null },
|
|
21
|
+
hooks: {
|
|
22
|
+
afterInsert(table, rows) { if (show(table)) logger(`insert ${table} +${count(rows)}`); },
|
|
23
|
+
afterUpdate(table) { if (show(table)) logger(`update ${table}`); },
|
|
24
|
+
afterDelete(table) { if (show(table)) logger(`delete ${table}`); },
|
|
25
|
+
afterFind(table) { if (opts.logFinds && show(table)) logger(`find ${table}`); },
|
|
26
|
+
afterQuery(sql) { if (opts.logQueries) logger(`query ${sql}`); },
|
|
27
|
+
afterCreateTable(name) { if (show(name)) logger(`createTable ${name}`); },
|
|
28
|
+
afterDropTable(name) { if (show(name)) logger(`dropTable ${name}`); },
|
|
29
|
+
onStart() { logger('engine start'); },
|
|
30
|
+
onStop() { logger('engine stop'); }
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { createLogger, logger: createLogger };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* metrics —— 统计各类操作的次数,通过 db.plugin('metrics') 读取。
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* db.use(require('jsql-neo/lib/plugins').metrics());
|
|
6
|
+
* db.plugin('metrics').snapshot(); // { insert: 3, update: 1, find: 5, ... }
|
|
7
|
+
* db.plugin('metrics').reset();
|
|
8
|
+
*/
|
|
9
|
+
const { definePlugin } = require('../plugin');
|
|
10
|
+
|
|
11
|
+
function createMetrics(opts = {}) {
|
|
12
|
+
const counters = {};
|
|
13
|
+
|
|
14
|
+
const bump = (key, n = 1) => { counters[key] = (counters[key] || 0) + n; };
|
|
15
|
+
const rowCount = rows => (Array.isArray(rows) ? rows.length : 1);
|
|
16
|
+
|
|
17
|
+
return definePlugin('metrics', {
|
|
18
|
+
api: {
|
|
19
|
+
snapshot: () => ({ ...counters }),
|
|
20
|
+
get: key => counters[key] || 0,
|
|
21
|
+
reset: () => { for (const k of Object.keys(counters)) delete counters[k]; }
|
|
22
|
+
},
|
|
23
|
+
hooks: {
|
|
24
|
+
afterInsert(_t, rows) { bump('insert', rowCount(rows)); bump('insertOps'); },
|
|
25
|
+
afterUpdate() { bump('update'); },
|
|
26
|
+
afterDelete() { bump('delete'); },
|
|
27
|
+
afterFind() { bump('find'); },
|
|
28
|
+
afterCount() { bump('count'); },
|
|
29
|
+
afterQuery() { bump('query'); },
|
|
30
|
+
afterCreateTable() { bump('createTable'); },
|
|
31
|
+
afterDropTable() { bump('dropTable'); },
|
|
32
|
+
onStart() { bump('start'); },
|
|
33
|
+
onStop() { bump('stop'); }
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { createMetrics, metrics: createMetrics };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* timestamps —— 自动维护创建/更新时间戳。
|
|
3
|
+
*
|
|
4
|
+
* 在插入时补全 createdAt / updatedAt(已存在则不覆盖 createdAt),
|
|
5
|
+
* 更新时刷新 updatedAt。所有引擎通用,钩子内只原地修改数据、不回调引擎。
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* db.use(require('jsql-neo/lib/plugins').timestamps());
|
|
9
|
+
* db.use(require('jsql-neo/lib/plugins').timestamps({ createdField: 'created_at', updatedField: 'updated_at' }));
|
|
10
|
+
*/
|
|
11
|
+
const { definePlugin } = require('../plugin');
|
|
12
|
+
|
|
13
|
+
function createTimestamps(opts = {}) {
|
|
14
|
+
const createdField = opts.createdField || 'createdAt';
|
|
15
|
+
const updatedField = opts.updatedField || 'updatedAt';
|
|
16
|
+
const now = typeof opts.now === 'function' ? opts.now : () => new Date().toISOString();
|
|
17
|
+
|
|
18
|
+
const stampRow = (row, isInsert) => {
|
|
19
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) return;
|
|
20
|
+
const t = now();
|
|
21
|
+
if (isInsert && row[createdField] === undefined) row[createdField] = t;
|
|
22
|
+
if (row[updatedField] === undefined || opts.overwriteUpdated) row[updatedField] = t;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return definePlugin('timestamps', {
|
|
26
|
+
config: { createdField, updatedField },
|
|
27
|
+
hooks: {
|
|
28
|
+
beforeInsert(_table, rows) {
|
|
29
|
+
if (Array.isArray(rows)) rows.forEach(r => stampRow(r, true));
|
|
30
|
+
else stampRow(rows, true);
|
|
31
|
+
},
|
|
32
|
+
// 更新钩子有 [table, id, data] / [table, pairs] / [table, query, updates] 三种形态
|
|
33
|
+
beforeUpdate(_table, second, third) {
|
|
34
|
+
if (third !== undefined) { stampRow(third, false); return; }
|
|
35
|
+
if (Array.isArray(second)) {
|
|
36
|
+
for (const pair of second) if (Array.isArray(pair)) stampRow(pair[1], false);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { createTimestamps, timestamps: createTimestamps };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validation —— 基于表 schema 的插入/更新校验,跨引擎行为一致。
|
|
3
|
+
*
|
|
4
|
+
* 校验项:not null(插入)、字段类型、maxLength、min/max,以及自定义校验函数。
|
|
5
|
+
* 违反时抛出 `ER_VALIDATION: table.field ...`。
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* db.use(require('jsql-neo/lib/plugins').validation({
|
|
9
|
+
* validators: { users: { email: v => /@/.test(v) || 'invalid email' } }
|
|
10
|
+
* }));
|
|
11
|
+
*/
|
|
12
|
+
const { definePlugin } = require('../plugin');
|
|
13
|
+
|
|
14
|
+
function normalizeFieldDef(def) {
|
|
15
|
+
if (typeof def === 'string') {
|
|
16
|
+
const s = def.toLowerCase();
|
|
17
|
+
const has = re => new RegExp(re).test(s);
|
|
18
|
+
const type = s
|
|
19
|
+
.replace(/\bprimary\s+key\b/g, '')
|
|
20
|
+
.replace(/\bauto_?increment\b/g, '')
|
|
21
|
+
.replace(/\bnot\s+null\b/g, '')
|
|
22
|
+
.replace(/\bunique\b/g, '')
|
|
23
|
+
.replace(/\bdefault\s+\S+/g, '')
|
|
24
|
+
.trim();
|
|
25
|
+
return {
|
|
26
|
+
type: type || 'string',
|
|
27
|
+
required: has('\\bnot\\s+null\\b'),
|
|
28
|
+
primaryKey: has('\\bprimary\\s+key\\b'),
|
|
29
|
+
autoIncrement: has('\\bauto_?increment\\b'),
|
|
30
|
+
hasDefault: has('\\bdefault\\s+\\S+')
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (def && typeof def === 'object') {
|
|
34
|
+
return {
|
|
35
|
+
type: String(def.type || 'string').toLowerCase(),
|
|
36
|
+
required: !!(def.required || def.notNull || def.not_null),
|
|
37
|
+
nullable: def.nullable,
|
|
38
|
+
primaryKey: !!(def.primaryKey || def.primary_key),
|
|
39
|
+
autoIncrement: !!(def.autoIncrement || def.auto_increment),
|
|
40
|
+
hasDefault: def.default !== undefined,
|
|
41
|
+
maxLength: def.maxLength !== undefined ? def.maxLength : def.length,
|
|
42
|
+
min: def.min,
|
|
43
|
+
max: def.max
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { type: 'string' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const TYPE_OF = {
|
|
50
|
+
string: v => typeof v === 'string',
|
|
51
|
+
text: v => typeof v === 'string',
|
|
52
|
+
varchar: v => typeof v === 'string',
|
|
53
|
+
char: v => typeof v === 'string',
|
|
54
|
+
integer: v => Number.isInteger(v),
|
|
55
|
+
int: v => Number.isInteger(v),
|
|
56
|
+
bigint: v => typeof v === 'bigint' || Number.isInteger(v),
|
|
57
|
+
float: v => typeof v === 'number' && Number.isFinite(v),
|
|
58
|
+
double: v => typeof v === 'number' && Number.isFinite(v),
|
|
59
|
+
number: v => typeof v === 'number' && Number.isFinite(v),
|
|
60
|
+
numeric: v => typeof v === 'number' && Number.isFinite(v),
|
|
61
|
+
decimal: v => typeof v === 'number' && Number.isFinite(v),
|
|
62
|
+
boolean: v => typeof v === 'boolean',
|
|
63
|
+
bool: v => typeof v === 'boolean',
|
|
64
|
+
json: v => typeof v === 'object' && v !== null && !Array.isArray(v),
|
|
65
|
+
object: v => typeof v === 'object' && v !== null && !Array.isArray(v),
|
|
66
|
+
array: v => Array.isArray(v),
|
|
67
|
+
date: v => typeof v === 'string' || v instanceof Date,
|
|
68
|
+
datetime: v => typeof v === 'string' || v instanceof Date,
|
|
69
|
+
timestamp: v => typeof v === 'string' || v instanceof Date
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function checkValue(table, field, def, value, row, custom) {
|
|
73
|
+
const isNull = value === undefined || value === null;
|
|
74
|
+
const fail = msg => { throw new Error(`ER_VALIDATION: ${table}.${field} ${msg}`); };
|
|
75
|
+
|
|
76
|
+
if (isNull) {
|
|
77
|
+
if (def.required && !def.hasDefault && def.nullable !== true) fail('is required');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const type = def.type;
|
|
81
|
+
const checker = TYPE_OF[type];
|
|
82
|
+
if (checker && !checker(value)) fail(`expected ${type}, got ${typeof value}`);
|
|
83
|
+
if (def.maxLength !== undefined && typeof value === 'string' && value.length > def.maxLength) {
|
|
84
|
+
fail(`length > ${def.maxLength}`);
|
|
85
|
+
}
|
|
86
|
+
if (def.min !== undefined && typeof value === 'number' && value < def.min) fail(`< ${def.min}`);
|
|
87
|
+
if (def.max !== undefined && typeof value === 'number' && value > def.max) fail(`> ${def.max}`);
|
|
88
|
+
if (typeof custom === 'function') {
|
|
89
|
+
const r = custom(value, row);
|
|
90
|
+
if (r === false) fail('custom validator failed');
|
|
91
|
+
if (typeof r === 'string') fail(r);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function createValidation(opts = {}) {
|
|
96
|
+
const validators = opts.validators || {};
|
|
97
|
+
// install 时拿到插件上下文(schema 查询),供各钩子闭包使用
|
|
98
|
+
let engineCtx = null;
|
|
99
|
+
|
|
100
|
+
const validateRow = (table, row, partial) => {
|
|
101
|
+
const schema = engineCtx && engineCtx.getTableSchema(table);
|
|
102
|
+
if (!schema) return;
|
|
103
|
+
const tableValidators = validators[table] || {};
|
|
104
|
+
for (const [field, rawDef] of Object.entries(schema)) {
|
|
105
|
+
const def = normalizeFieldDef(rawDef);
|
|
106
|
+
if (partial && !(field in row)) continue;
|
|
107
|
+
checkValue(table, field, def, row[field], row, tableValidators[field] || validators[field]);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const validateMany = (table, rows, partial) => {
|
|
112
|
+
if (Array.isArray(rows)) rows.forEach(r => validateRow(table, r, partial));
|
|
113
|
+
else if (rows && typeof rows === 'object') validateRow(table, rows, partial);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return definePlugin('validation', {
|
|
117
|
+
install(engine, ctx) { engineCtx = ctx; },
|
|
118
|
+
hooks: {
|
|
119
|
+
beforeInsert(table, rows) { validateMany(table, rows, false); },
|
|
120
|
+
beforeUpdate(table, second, third) {
|
|
121
|
+
if (third !== undefined) { validateMany(table, third, true); return; }
|
|
122
|
+
if (Array.isArray(second)) for (const p of second) if (Array.isArray(p)) validateMany(table, p[1], true);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { createValidation, validation: createValidation };
|