jsql-neo 6.5.1 → 6.5.4

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  > **One engine to rule them all** — a Rust-powered embedded database that speaks your language:
4
4
  > MySQL, PostgreSQL, MongoDB, Redis, SQL, TypeScript, **and the browser**.
5
5
 
6
- > **v6.5.1** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
6
+ > **v6.5.4** — stable release · [github.com/vexify-org/JSQL-neo](https://github.com/vexify-org/JSQL-neo) · [jsql.vexify.top](https://jsql.vexify.top)
7
7
 
8
8
  ![Engines](https://img.shields.io/badge/engines-Native%20%7C%20WASM%20%7C%20Pure%20JS-7ee787)
9
9
  ![MySQL](https://img.shields.io/badge/protocol-MySQL%20compatible-1f6feb)
package/bin/jsql CHANGED
@@ -201,28 +201,40 @@ const cli = yaggs({ pkg: require('../package.json') })
201
201
  if (argv.reset) sn.setStarred(false);
202
202
  const c = sn.readConfig();
203
203
  if (c.starred) {
204
- console.log('Already confirmed starred (config: ' + sn.configPath() + ')');
205
- console.log('Run with --reset to re-check.');
204
+ console.log(`Already confirmed starred 已确认过: ${sn.configPath()}`);
205
+ console.log('Run with --reset to re-check · 用 --reset 重新检查');
206
206
  return;
207
207
  }
208
- console.log('Opening browser to confirm your star…');
208
+ console.log('Opening browser to confirm your star… 正在打开浏览器确认');
209
209
  const nonceUrl = sn.startOauthCheck(); // 打开浏览器 + 后台轮询写回 ~/.jsqlmr
210
210
  if (!process.stdout.isTTY) console.log(' ' + nonceUrl);
211
- console.log('After you authorize, this config is updated: ' + sn.configPath());
212
- console.log('Waiting for authorization (up to 3 minutes)…');
211
+ console.log(`Authorization updates this config 授权后写入配置: ${sn.configPath()}`);
212
+ console.log('Waiting for authorization (up to 3 minutes)… 等待授权(最多 3 分钟)…');
213
213
  // startOauthCheck 的定时器是 unref 的,命令必须自己前台等,否则接不到结果
214
214
  const res = sn.waitForResult(nonceUrl, 180000);
215
- if (res && res.starred) {
215
+ const status = res && res.status;
216
+ if (status === 'starred') {
216
217
  sn.setStarred(true);
217
- console.log(`Confirmed: ${res.login} has starred ${res.repo}. Thanks!`);
218
+ console.log(`Confirmed: ${res.login} starred ${res.repo} — thanks! 已确认,谢谢!`);
218
219
  return;
219
220
  }
220
- if (res) {
221
- console.log(`Authorization finished, but ${res.login} has not starred ${res.repo} yet.`);
222
- console.log(`Star it at https://github.com/${res.repo} then run \`jsql star\` again.`);
221
+ if (status === 'not-starred') {
222
+ console.log(`Not starred yet 还没 star: ${res.login} has not starred ${res.repo}`);
223
+ console.log(`Star it at ${sn.URL}, then run \`jsql star\` again · 点星后重跑 \`jsql star\``);
223
224
  return;
224
225
  }
225
- console.log('Timed out or the link expired. Run `jsql star` again to retry.');
226
+ if (status === 'denied' || status === 'error') {
227
+ // 这不是「没 star」,是授权本身没走通(code 过期、链接被重复打开、被拒绝)
228
+ const why = status === 'denied' ? 'declined 被拒绝' : 'failed 失败';
229
+ console.log(`Authorization ${why}: ${res.error || 'unknown reason'}`);
230
+ console.log('The link is single-use — run `jsql star` again · 链接一次性,请重跑 `jsql star`');
231
+ return;
232
+ }
233
+ if (status === 'expired') {
234
+ console.log('The link expired 链接已过期 — run `jsql star` again · 请重跑');
235
+ return;
236
+ }
237
+ console.log('Timed out 已超时 — run `jsql star` again · 请重跑');
226
238
  })
227
239
  .command('bench', 'Insert + query benchmark against a data directory', (sub) => {
228
240
  sub.option('n', { type: 'number', description: 'Number of rows (default 10000)' });
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * JSQL-NEO v6.5.1 — Rust-Powered Embedded Database (WASM + HTTP)
2
+ * JSQL-NEO v6.5.4 — Rust-Powered Embedded Database (WASM + HTTP)
3
3
  *
4
4
  * @example
5
5
  * const jsql = require('jsql-neo');
package/lib/star-nag.js CHANGED
@@ -35,6 +35,24 @@ const AUTH_RESULT = AUTH_BASE + '/result.php';
35
35
  * 拿到结果写进 ~/.jsqlmr(starred: true/false)。
36
36
  * 进程提前退出就放弃这一次,下次启动会再提示(30 天节流管频率)。
37
37
  */
38
+ // result.php 的合法终态:done / denied / error。
39
+ // 只有终态才写配置——"started" 这一类中间态写进去只会污染 ~/.jsqlmr。
40
+ const TERMINAL_STATES = new Set(['done', 'denied', 'error']);
41
+
42
+ // 把 result.php 的响应收敛成规范状态。老服务端会把 started 也报成 done,
43
+ // 缺少 starred 字段 —— 一律当未完成处理,让调用方继续等。
44
+ function parseAuthResult(j) {
45
+ if (!j || typeof j !== 'object') return null;
46
+ const st = j.state;
47
+ if (!TERMINAL_STATES.has(st)) return null;
48
+ const r = j.result || {};
49
+ if (st === 'done') {
50
+ if (typeof r.starred !== 'boolean') return null;
51
+ return { status: r.starred ? 'starred' : 'not-starred', login: r.login, repo: r.repo };
52
+ }
53
+ return { status: st, error: r.error || (st === 'denied' ? 'authorization denied' : 'authorization failed') };
54
+ }
55
+
38
56
  function startOauthCheck() {
39
57
  const crypto = require('crypto');
40
58
  const nonce = crypto.randomBytes(24).toString('base64url');
@@ -61,12 +79,13 @@ function startOauthCheck() {
61
79
  if (err) return;
62
80
  let j = null;
63
81
  try { j = JSON.parse(stdout); } catch (e) { return; }
64
- if (!j || j.state !== 'done' || !j.result) return;
82
+ const res = parseAuthResult(j);
83
+ if (!res) return; // pending / started / 格式不符 —— 继续等
65
84
  clearInterval(t);
66
85
  const cfg = readConfig();
67
86
  cfg.oauthCheckedAt = Date.now();
68
- cfg.login = j.result.login || undefined;
69
- cfg.starred = !!j.result.starred;
87
+ cfg.login = res.login || undefined;
88
+ cfg.starred = res.status === 'starred';
70
89
  if (cfg.starred) cfg.starredAt = Date.now();
71
90
  writeConfig(cfg);
72
91
  });
@@ -189,7 +208,7 @@ function nag(opts = {}) {
189
208
  }
190
209
 
191
210
  out.write(`\n If JSQL-NEO saved you time, a star helps others find it: ${URL}\n`);
192
- out.write(` Run \`jsql star\` to confirm and stop seeing this (or set JSQL_NO_STAR_NAG=1 to hide)\n\n`);
211
+ out.write(` 如果帮到了你,点个 star;运行 \`jsql star\` 确认 · JSQL_NO_STAR_NAG=1 关闭提示\n\n`);
193
212
 
194
213
  // 只记录「有没有 star」,不再写 lastNagAt —— 没有节流,写它没有意义。
195
214
  return true;
@@ -214,6 +233,21 @@ function setStarred(v) {
214
233
  * @param {number} [timeoutMs] 最长等待时间,默认 180 秒
215
234
  * @returns {object|null} result 对象(含 login/starred/repo),超时或失败返回 null
216
235
  */
236
+ /**
237
+ * 前台等待授权结果(`jsql star` 用)。
238
+ *
239
+ * 返回规范化后的结果对象:
240
+ * { status: 'starred', login, repo } 已 star
241
+ * { status: 'not-starred', login, repo } 授权完成但没 star
242
+ * { status: 'denied', error } 用户在 GitHub 拒绝授权
243
+ * { status: 'error', error } 回调失败(code 过期/复用等)
244
+ * { status: 'expired' } 链接超龄被清理
245
+ * null 超时(timeoutMs 内没有终态)
246
+ *
247
+ * 老服务端会把 "started" 这类中间态也报成 done 且不带 starred 字段 ——
248
+ * parseAuthResult 一律判为未完成,继续轮询直到超时,而不是打印一段
249
+ * login/repo 全为 undefined 的假失败信息。
250
+ */
217
251
  function waitForResult(startUrl, timeoutMs = 180000) {
218
252
  const { execFileSync } = require('child_process');
219
253
  // 注意:本模块顶部有 const URL = 'https://github.com/...' 会遮蔽全局 URL 类,
@@ -232,9 +266,11 @@ function waitForResult(startUrl, timeoutMs = 180000) {
232
266
  try {
233
267
  const out = execFileSync('curl', ['-sS', '-m', '5', AUTH_RESULT + '?nonce=' + nonce],
234
268
  { stdio: ['ignore', 'pipe', 'ignore'], timeout: 8000, encoding: 'utf8' });
235
- const j = JSON.parse(out);
236
- if (j && j.state === 'done' && j.result) return j.result;
237
- if (j && j.state === 'expired') return null;
269
+ let j = null;
270
+ try { j = JSON.parse(out); } catch (e) { j = null; }
271
+ const res = parseAuthResult(j);
272
+ if (res) return res;
273
+ if (j && j.state === 'expired') return { status: 'expired' };
238
274
  } catch (e) {
239
275
  // 网络抖动 / 超时 —— 继续等
240
276
  }
@@ -243,4 +279,4 @@ function waitForResult(startUrl, timeoutMs = 180000) {
243
279
  return null;
244
280
  }
245
281
 
246
- module.exports = { nag, checkStarred, startOauthCheck, waitForResult, readConfig, setStarred, configPath, REPO, URL };
282
+ module.exports = { nag, checkStarred, startOauthCheck, waitForResult, parseAuthResult, readConfig, setStarred, configPath, REPO, URL };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsql-neo",
3
- "version": "6.5.1",
3
+ "version": "6.5.4",
4
4
  "description": "JSQL-NEO — Rust-powered embedded database with WASM, REST API, B-Tree indexes, WAL, crash recovery",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -46,7 +46,7 @@
46
46
  ],
47
47
  "scripts": {
48
48
  "postinstall": "node postinstall.js",
49
- "test": "node test/smoke.js && node test/native.test.js && node test/json-functions.test.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/query-opt.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/dml-constraints.test.js",
49
+ "test": "node test/smoke.js && node test/native.test.js && node test/json-functions.test.js && node test/sql-parser.test.js && node test/readme-audit.test.js && node test/query-opt.test.js && node test/wal-recovery.test.js && node test/mysql-compat-hijack.test.js && node test/dml-constraints.test.js && node test/star-nag.test.js",
50
50
  "test:parser": "node test/sql-parser.test.js",
51
51
  "test:btree": "node test/btree.test.js",
52
52
  "test:native": "node test/native.test.js",
@@ -59,6 +59,7 @@
59
59
  "test:index": "node test/index-ddl.test.js",
60
60
  "test:mongo": "node test/mongo-group.test.js",
61
61
  "test:multi": "node test/multiprotocol.test.js",
62
+ "test:star": "node test/star-nag.test.js",
62
63
  "test:savepoint": "node test/savepoint.test.js",
63
64
  "test:alter": "node test/alter-table.test.js",
64
65
  "test:ident": "node test/identifier-keywords.test.js",
@@ -0,0 +1,160 @@
1
+ /**
2
+ * jsql star 授权流回归测试
3
+ *
4
+ * 背景:线上 result.php 曾把「state 文件存在」一律当成授权完成,
5
+ * 而 start.php 一开始就会写入 {"state":"started"}。于是 CLI 第二轮轮询
6
+ * 就拿到 started 当 done,打印出:
7
+ * Authorization finished, but undefined has not starred undefined yet.
8
+ * Star it at https://github.com/undefined then run `jsql star` again.
9
+ * 真相是授权根本没走完。
10
+ *
11
+ * 覆盖:
12
+ * 1. parseAuthResult 对各形态响应的归一化(中间态不得当成完成)
13
+ * 2. CLI 对 starred / not-starred / denied / error 的输出
14
+ * 3. 慢用户场景:先 pending 再完成,CLI 必须继续等而不是误判
15
+ * 4. waitForResult 的超时与 expired 收敛
16
+ *
17
+ * 注意:AUTH_RESULT 是 lib/star-nag 在 require 时从 JSQL_AUTH_BASE 读的,
18
+ * 所以必须先把 env 设好再 require(waitForResult 又用同步 curl,
19
+ * 本进程内的 server 会被饿死,所以 CLI 相关用例一律起子进程)。
20
+ */
21
+ const http = require('http');
22
+ const path = require('path');
23
+ const fs = require('fs');
24
+ const os = require('os');
25
+ const { spawn } = require('child_process');
26
+
27
+ const PORT = Number(process.env.JSQL_STAR_TEST_PORT) || 34711;
28
+ process.env.JSQL_AUTH_BASE = `http://127.0.0.1:${PORT}`;
29
+
30
+ const sn = require('../lib/star-nag');
31
+
32
+ let pass = 0, fail = 0;
33
+ function ok(name, cond, extra) {
34
+ if (cond) { pass++; console.log('[OK] ' + name); }
35
+ else { fail++; console.log('[FAIL] ' + name + (extra !== undefined ? ' → ' + JSON.stringify(extra) : '')); }
36
+ }
37
+
38
+ /** 当前 result.php 要返回的内容;pollsLeft <= 0 时切到 after */
39
+ let scenario = { state: 'pending' };
40
+ let pollsLeft = 0;
41
+ let after = null;
42
+
43
+ const server = http.createServer((req, res) => {
44
+ res.writeHead(200, { 'Content-Type': 'application/json' });
45
+ if (new URL(req.url, 'http://127.0.0.1').pathname === '/result.php') {
46
+ if (pollsLeft > 0 && --pollsLeft === 0 && after) scenario = after;
47
+ res.end(JSON.stringify(scenario));
48
+ return;
49
+ }
50
+ res.end('{}');
51
+ });
52
+
53
+ function runCli(mrPath) {
54
+ return new Promise((resolve) => {
55
+ const bin = path.join(__dirname, '..', 'bin', 'jsql');
56
+ const child = spawn(process.execPath, [bin, 'star'], {
57
+ env: { ...process.env, JSQL_AUTH_BASE: `http://127.0.0.1:${PORT}`, JSQL_MR_PATH: mrPath, JSQL_NO_STAR_NAG: '1' },
58
+ stdio: ['ignore', 'pipe', 'pipe'],
59
+ });
60
+ let out = '';
61
+ child.stdout.on('data', (c) => { out += c; });
62
+ child.stderr.on('data', (c) => { out += c; });
63
+ const t = setTimeout(() => { try { child.kill(); } catch (e) {} resolve(out + '\n[TIMEOUT]'); }, 40000);
64
+ child.on('exit', () => { clearTimeout(t); resolve(out); });
65
+ });
66
+ }
67
+
68
+ async function main() {
69
+ /* ---------- 1. parseAuthResult ---------- */
70
+ console.log('--- parseAuthResult:中间态不得当成完成 ---');
71
+ {
72
+ const P = sn.parseAuthResult;
73
+ ok('done + starred=true → starred', P({ state: 'done', result: { starred: true, login: 'u', repo: 'o/r' } }).status === 'starred');
74
+ ok('done + starred=false → not-starred', P({ state: 'done', result: { starred: false, login: 'u', repo: 'o/r' } }).status === 'not-starred');
75
+ ok('started 被误报成 done → 未完成', P({ state: 'done', result: { state: 'started', at: 1 } }) === null);
76
+ ok('pending → 未完成', P({ state: 'pending' }) === null);
77
+ ok('缺 result 的 done → 未完成', P({ state: 'done' }) === null);
78
+ ok('denied → 带原因', P({ state: 'denied', result: { error: 'access_denied' } }).status === 'denied');
79
+ ok('error → 带原因', P({ state: 'error', result: { error: 'The code passed is incorrect or expired.' } }).error === 'The code passed is incorrect or expired.');
80
+ ok('空响应 → 未完成', P(null) === null && P({}) === null);
81
+ ok('未知状态 → 未完成', P({ state: 'weird' }) === null);
82
+ ok('expired 不在终态集(由 waitForResult 单独处理)', P({ state: 'expired' }) === null);
83
+ }
84
+
85
+ await new Promise((r) => server.listen(PORT, '127.0.0.1', r));
86
+ const mrDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsqlmr-'));
87
+
88
+ /* ---------- 2. CLI 各终态输出 ---------- */
89
+ console.log('\n--- 假 result.php 驱动真实 CLI ---');
90
+ const cases = [
91
+ ['done + starred', { state: 'done', result: { starred: true, login: 'octocat', repo: 'vexify-org/JSQL-neo' } },
92
+ 'Confirmed: octocat starred vexify-org/JSQL-neo'],
93
+ ['done + 未 star', { state: 'done', result: { starred: false, login: 'octocat', repo: 'vexify-org/JSQL-neo' } },
94
+ 'Not starred yet 还没 star: octocat has not starred vexify-org/JSQL-neo'],
95
+ ['denied', { state: 'denied', result: { error: 'access_denied' } },
96
+ 'Authorization declined 被拒绝: access_denied'],
97
+ ['error(code 过期/复用)', { state: 'error', result: { error: 'The code passed is incorrect or expired.' } },
98
+ 'Authorization failed 失败: The code passed is incorrect or expired.'],
99
+ ];
100
+ for (const [name, resp, expect] of cases) {
101
+ scenario = resp; pollsLeft = 0; after = null;
102
+ const out = await runCli(path.join(mrDir, name.replace(/[^a-z0-9]+/gi, '-')));
103
+ ok(`CLI 输出正确:${name}`, out.includes(expect), out.trim().slice(0, 200));
104
+ ok(`CLI 不再出现 undefined:${name}`, !/undefined/.test(out), out.trim().slice(0, 200));
105
+ }
106
+
107
+ /* ---------- 3. 慢用户 ---------- */
108
+ console.log('\n--- 慢用户:先 pending 再完成,CLI 必须继续等而不是误判 ---');
109
+ {
110
+ scenario = { state: 'pending' };
111
+ pollsLeft = 2;
112
+ after = { state: 'done', result: { starred: true, login: 'octocat', repo: 'vexify-org/JSQL-neo' } };
113
+ const out = await runCli(path.join(mrDir, 'slow'));
114
+ ok('等完 pending 后正确确认', out.includes('Confirmed: octocat starred vexify-org/JSQL-neo'), out.trim().slice(0, 200));
115
+ ok('全程没有误判成完成', !/Not starred yet/.test(out), out.trim().slice(0, 200));
116
+ ok('全程没有 undefined', !/undefined/.test(out), out.trim().slice(0, 200));
117
+ }
118
+
119
+ /* ---------- 4. waitForResult ---------- */
120
+ console.log('\n--- waitForResult:中间态与超时 ---');
121
+ {
122
+ // waitForResult 用同步 curl,会阻塞本进程事件循环 —— server 必须留在父进程,
123
+ // 轮询放到子进程里跑,否则 parent 的 server 被饿死,curl 全部 -m 超时。
124
+ const url = `http://127.0.0.1:${PORT}/start.php?nonce=abcdefghijklmnopqrst`;
125
+ const poll = (resp, timeoutMs) => new Promise((resolve) => {
126
+ scenario = resp; pollsLeft = 0; after = null;
127
+ const code = [
128
+ `const sn = require(${JSON.stringify(path.join(__dirname, '..', 'lib', 'star-nag'))});`,
129
+ `const t = Date.now();`,
130
+ `const r = sn.waitForResult(process.env.TEST_URL, ${timeoutMs});`,
131
+ `process.stdout.write(JSON.stringify({ r, ms: Date.now() - t }));`,
132
+ ].join('\n');
133
+ const c = spawn(process.execPath, ['-e', code],
134
+ { env: { ...process.env, TEST_URL: url }, stdio: ['ignore', 'pipe', 'ignore'] });
135
+ let out = '';
136
+ c.stdout.on('data', (d) => { out += d; });
137
+ c.on('exit', () => { try { resolve(JSON.parse(out)); } catch (e) { resolve({ r: null, ms: -1 }); } });
138
+ });
139
+
140
+ for (const [name, resp] of [
141
+ ['pending', { state: 'pending' }],
142
+ ['老服务端误报 started 为 done', { state: 'done', result: { state: 'started', at: 1 } }],
143
+ ]) {
144
+ const { r, ms } = await poll(resp, 3000);
145
+ ok(`waitForResult(${name}) 超时返回 null`, r === null, r);
146
+ ok(`确实等满了 3 秒`, ms >= 2800, ms);
147
+ }
148
+ {
149
+ const { r, ms } = await poll({ state: 'expired' }, 5000);
150
+ ok('waitForResult(expired) 立即收敛', !!r && r.status === 'expired', r);
151
+ ok('expired 没有拖到超时', ms >= 0 && ms < 2500, ms);
152
+ }
153
+ }
154
+
155
+ server.close();
156
+ console.log(`\nRESULT: ${pass} passed, ${fail} failed`);
157
+ if (fail > 0) process.exit(1);
158
+ }
159
+
160
+ main().catch((e) => { console.error(e); process.exit(1); });