jsql-neo 6.5.1 → 6.5.3

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.3** — 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
@@ -212,17 +212,29 @@ const cli = yaggs({ pkg: require('../package.json') })
212
212
  console.log('Waiting for authorization (up to 3 minutes)…');
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
218
  console.log(`Confirmed: ${res.login} has starred ${res.repo}. Thanks!`);
218
219
  return;
219
220
  }
220
- if (res) {
221
+ if (status === 'not-starred') {
221
222
  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.`);
223
+ console.log(`Star it at ${sn.URL} then run \`jsql star\` again.`);
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
+ // 老实现这里会把 login/repo 打印成 undefined,并让用户去 star —— 误导。
229
+ console.log(`Authorization ${status === 'denied' ? 'was declined' : 'failed'}: ${res.error || 'unknown reason'}`);
230
+ console.log('The link is single-use. Run `jsql star` again to get a fresh one.');
231
+ return;
232
+ }
233
+ if (status === 'expired') {
234
+ console.log('The link expired. Run `jsql star` again to retry.');
235
+ return;
236
+ }
237
+ console.log('Timed out. Run `jsql star` again to retry.');
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.3 — 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
  });
@@ -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.3",
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 has starred vexify-org/JSQL-neo. Thanks!'],
93
+ ['done + 未 star', { state: 'done', result: { starred: false, login: 'octocat', repo: 'vexify-org/JSQL-neo' } },
94
+ 'Authorization finished, but octocat has not starred vexify-org/JSQL-neo yet.'],
95
+ ['denied', { state: 'denied', result: { error: 'access_denied' } },
96
+ 'Authorization was 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 has starred vexify-org/JSQL-neo. Thanks!'), out.trim().slice(0, 200));
115
+ ok('全程没有误判成完成', !/Authorization finished/.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); });