@finebi/finebi-cli 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +10 -0
  2. package/package.json +4 -3
  3. package/skills/finebi-cli-agent/SKILL.md +8 -4
  4. package/skills/finebi-cli-agent/references/command-workflows.md +7 -3
  5. package/skills/finebi-cli-agent/references/dashboard-data-result.md +191 -191
  6. package/skills/finebi-cli-agent/references/dashboard-dsl-api.md +8 -1018
  7. package/skills/finebi-cli-agent/references/dashboard-dsl-core.md +194 -0
  8. package/skills/finebi-cli-agent/references/dashboard-dsl-examples.md +378 -0
  9. package/skills/finebi-cli-agent/references/dashboard-dsl-items.md +456 -0
  10. package/skills/finebi-cli-agent/references/mapping-rules.md +14 -14
  11. package/skills/finebi-cli-agent/references/scenario-workflows.md +1 -1
  12. package/skills/finebi-cli-agent/references/web-app.md +13 -0
  13. package/skills/finebi-cli-agent/references/widget-dsl-api-examples.md +3 -4
  14. package/skills/finebi-cli-agent/references/widget-dsl-api.md +10 -1374
  15. package/skills/finebi-cli-agent/references/widget-dsl-contexts.md +142 -0
  16. package/skills/finebi-cli-agent/references/widget-dsl-core.md +374 -0
  17. package/skills/finebi-cli-agent/references/widget-dsl-encodings.md +457 -0
  18. package/skills/finebi-cli-agent/references/widget-dsl-styles.md +413 -0
  19. package/skills/finebi-cli-agent/scripts/finebi-cli/package.json +1 -1
  20. package/skills/finebi-cli-agent/scripts/finebi-cli/src/app.js +189 -0
  21. package/skills/finebi-cli-agent/scripts/finebi-cli/src/config.js +27 -27
  22. package/skills/finebi-cli-agent/scripts/finebi-cli/src/help.js +78 -55
  23. package/skills/finebi-cli-agent/scripts/finebi-cli/src/index.js +5 -0
  24. package/skills/finebi-cli-agent/scripts/finebi-cli/src/install-skill.js +89 -89
  25. package/skills/finebi-cli-agent/scripts/finebi-cli/src/profiles.js +158 -158
  26. package/skills/finebi-cli-agent/scripts/finebi-cli/src/skill-files.js +94 -94
  27. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/AGENTS.md +27 -0
  28. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/README.md +30 -0
  29. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/auth-and-deploy.md +17 -0
  30. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/data-contract.md +21 -0
  31. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/design.md +13 -0
  32. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/docs/filters.md +89 -0
  33. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/index.html +2 -0
  34. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/package-lock.json +2191 -0
  35. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/package.json +15 -0
  36. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/public/app-config.json +11 -0
  37. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/scripts/dev-proxy.mjs +56 -0
  38. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/App.tsx +31 -0
  39. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Chart.tsx +31 -0
  40. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/DataTable.tsx +14 -0
  41. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/FilterBar.tsx +42 -0
  42. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/Widget.tsx +28 -0
  43. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/components/calendar.ts +14 -0
  44. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/client.ts +37 -0
  45. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.d.ts +1 -0
  46. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/date.js +5 -0
  47. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.d.ts +4 -0
  48. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/filters.js +106 -0
  49. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/normalize.ts +77 -0
  50. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/finebi/types.ts +18 -0
  51. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/main.tsx +5 -0
  52. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/services/view-data.ts +18 -0
  53. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/src/styles.css +8 -0
  54. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/calendar.test.mjs +25 -0
  55. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/proxy.test.mjs +84 -0
  56. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tests/runtime.test.mjs +136 -0
  57. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/tsconfig.json +4 -0
  58. package/skills/finebi-cli-agent/scripts/finebi-cli/templates/web-app/vite.config.ts +13 -0
  59. package/skills/finebi-cli-agent/scripts/finebi-cli-runner.mjs +4 -4
  60. package/src/app.js +189 -0
  61. package/src/config.js +27 -27
  62. package/src/help.js +78 -55
  63. package/src/index.js +5 -0
  64. package/src/install-skill.js +89 -89
  65. package/src/profiles.js +158 -158
  66. package/src/skill-files.js +94 -94
  67. package/templates/web-app/AGENTS.md +27 -0
  68. package/templates/web-app/README.md +30 -0
  69. package/templates/web-app/docs/auth-and-deploy.md +17 -0
  70. package/templates/web-app/docs/data-contract.md +21 -0
  71. package/templates/web-app/docs/design.md +13 -0
  72. package/templates/web-app/docs/filters.md +89 -0
  73. package/templates/web-app/index.html +2 -0
  74. package/templates/web-app/package-lock.json +2191 -0
  75. package/templates/web-app/package.json +15 -0
  76. package/templates/web-app/public/app-config.json +11 -0
  77. package/templates/web-app/scripts/dev-proxy.mjs +56 -0
  78. package/templates/web-app/src/App.tsx +31 -0
  79. package/templates/web-app/src/components/Chart.tsx +31 -0
  80. package/templates/web-app/src/components/DataTable.tsx +14 -0
  81. package/templates/web-app/src/components/FilterBar.tsx +42 -0
  82. package/templates/web-app/src/components/Widget.tsx +28 -0
  83. package/templates/web-app/src/components/calendar.ts +14 -0
  84. package/templates/web-app/src/finebi/client.ts +37 -0
  85. package/templates/web-app/src/finebi/date.d.ts +1 -0
  86. package/templates/web-app/src/finebi/date.js +5 -0
  87. package/templates/web-app/src/finebi/filters.d.ts +4 -0
  88. package/templates/web-app/src/finebi/filters.js +106 -0
  89. package/templates/web-app/src/finebi/normalize.ts +77 -0
  90. package/templates/web-app/src/finebi/types.ts +18 -0
  91. package/templates/web-app/src/main.tsx +5 -0
  92. package/templates/web-app/src/services/view-data.ts +18 -0
  93. package/templates/web-app/src/styles.css +8 -0
  94. package/templates/web-app/tests/calendar.test.mjs +25 -0
  95. package/templates/web-app/tests/proxy.test.mjs +84 -0
  96. package/templates/web-app/tests/runtime.test.mjs +136 -0
  97. package/templates/web-app/tsconfig.json +4 -0
  98. package/templates/web-app/vite.config.ts +13 -0
@@ -0,0 +1,84 @@
1
+ import assert from 'node:assert/strict';
2
+ import {test} from 'node:test';
3
+ import {createServer} from 'node:http';
4
+ import {createDevMiddleware} from '../scripts/dev-proxy.mjs';
5
+
6
+ test('development identity stays on server and cross-site or out-of-scope requests are rejected', async () => {
7
+ const upstream = [];
8
+ const middleware = createDevMiddleware({basePath:'/webroot/decision',dashboardId:'r',widgets:[{entryId:'w'}],maxRows:1000}, {FINEBI_DEV_BASE_URL:'http://bi.example/webroot/decision',FINEBI_DEV_USERNAME:'tester',FINEBI_DEV_PASSWORD:'dev-secret'}, async (url,init) => {
9
+ upstream.push({url,init});
10
+ return new Response(JSON.stringify(url.endsWith('/login') ? {data:{accessToken:'session-secret'}} : {success:true,data:{w:{previewResult:{resultType:'DETAIL'}}}}));
11
+ });
12
+ const server=createServer((req,res)=>middleware(req,res,()=>{res.writeHead(404);res.end();}));
13
+ await new Promise(r=>server.listen(0,'127.0.0.1',r));
14
+ const base=`http://127.0.0.1:${server.address().port}`;
15
+ const path='/webroot/decision/v5/api/dashboard/report/r/widgets/query';
16
+ const request=(body,headers={})=>fetch(base+path,{method:'POST',headers:{'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest',...headers},body:JSON.stringify(body)});
17
+ try {
18
+ const valid={widgetIds:['w'],pageLimit:1000};
19
+ assert.equal((await request(valid,{Origin:'https://evil.example'})).status,403);
20
+ assert.equal((await request({...valid,widgetIds:['other']})).status,400);
21
+ assert.equal((await request({...valid,pageLimit:10001})).status,400);
22
+ assert.equal((await request({...valid,refer:{'未配置的控件':['x']}})).status,400);
23
+ assert.equal(upstream.length,0);
24
+ const result=await request(valid,{Origin:base});
25
+ assert.equal(result.status,200);
26
+ assert.doesNotMatch(await result.text(),/secret/);
27
+ assert.equal(upstream.length,2);
28
+ assert.equal(upstream[1].init.headers.Cookie,'fine_auth_token=session-secret');
29
+ assert.equal(upstream[1].init.redirect,'manual');
30
+ assert.equal((await fetch(base+'/webroot/decision/login',{method:'POST'})).status,403);
31
+ assert.equal((await request(valid)).status,200);
32
+ assert.equal(upstream.length,3,'reuse only the in-memory development session');
33
+ } finally {await new Promise(r=>server.close(r));}
34
+ });
35
+ test('HTML login response clears expired development session and explicit retry logs in again', async()=>{
36
+ let logins=0, queries=0;
37
+ const middleware=createDevMiddleware({basePath:'/webroot/decision',dashboardId:'r',widgets:[{entryId:'w'}],filters:[],maxRows:1000},{FINEBI_DEV_BASE_URL:'http://bi.example/webroot/decision',FINEBI_DEV_USERNAME:'tester',FINEBI_DEV_PASSWORD:'secret'},async url=>{
38
+ if(url.endsWith('/login')){logins++;return new Response(JSON.stringify({data:{accessToken:'token-'+logins}}));}
39
+ if(++queries===1)return new Response('<html>登录</html>',{headers:{'content-type':'text/html'}});
40
+ return new Response(JSON.stringify({success:true,data:{w:{previewResult:{resultType:'DETAIL'}}}}));
41
+ });
42
+ const server=createServer((req,res)=>middleware(req,res,()=>{}));await new Promise(r=>server.listen(0,'127.0.0.1',r));
43
+ const send=()=>fetch(`http://127.0.0.1:${server.address().port}/webroot/decision/v5/api/dashboard/report/r/widgets/query`,{method:'POST',headers:{'X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({widgetIds:['w'],pageLimit:10})});
44
+ try{const expired=await send();assert.equal(expired.status,401);assert.equal((await expired.json()).errorCode,'AUTH_REQUIRED');assert.equal((await send()).status,200);assert.equal(logins,2);}finally{await new Promise(r=>server.close(r));}
45
+ });
46
+ test('missing credentials gives an explicit auth error without upstream access',async()=>{
47
+ const middleware=createDevMiddleware({basePath:'/webroot/decision',dashboardId:'r',widgets:[{entryId:'w'}],maxRows:1000},{},()=>{throw Error('must not fetch');});
48
+ const server=createServer((req,res)=>middleware(req,res,()=>{}));
49
+ await new Promise(r=>server.listen(0,'127.0.0.1',r));
50
+ try {
51
+ const r=await fetch(`http://127.0.0.1:${server.address().port}/webroot/decision/v5/api/dashboard/report/r/widgets/query`,{method:'POST',headers:{'X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({widgetIds:['w'],pageLimit:10})});
52
+ assert.equal(r.status,401);assert.equal((await r.json()).errorCode,'AUTH_REQUIRED');
53
+ }finally{await new Promise(r=>server.close(r));}
54
+ });
55
+ test('date proxy accepts real dates without an option allowlist and rejects malformed overrides',async()=>{
56
+ const calls=[];
57
+ const middleware=createDevMiddleware({basePath:'/bi',dashboardId:'r',widgets:[{entryId:'w'}],maxRows:1000,filters:[{name:'日期',label:'日期',type:'date'},{name:'产品',label:'产品',options:[{label:'橡皮',value:'橡皮'}]}]},
58
+ {FINEBI_DEV_BASE_URL:'http://bi.example/bi',FINEBI_DEV_TOKEN:'secret'},async (_url,init)=>{calls.push(JSON.parse(init.body));return new Response(JSON.stringify({success:true,data:{}}));});
59
+ const server=createServer((req,res)=>middleware(req,res,()=>{}));await new Promise(r=>server.listen(0,'127.0.0.1',r));
60
+ const send=refer=>fetch(`http://127.0.0.1:${server.address().port}/bi/v5/api/dashboard/report/r/widgets/query`,{method:'POST',headers:{'X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({widgetIds:['w'],pageLimit:1000,refer})});
61
+ try {
62
+ for(const refer of [{'日期':['2023-02-29']},{'日期':['2024-02-30']},{'日期':['0000-01-01']},{'日期':['2024-2-01']},{'日期':['']},{'日期':['2024-01-01','2024-02-01']},{'日期':[42]},{'未知':['2024-01-01']},{'产品':['未知']}]) assert.equal((await send(refer)).status,400);
63
+ assert.equal(calls.length,0);
64
+ const refer={'日期':['2024-02-29'],'产品':['橡皮']};assert.equal((await send(refer)).status,200);assert.deepEqual(calls[0].refer,refer);
65
+ assert.equal((await send({})).status,200);assert.deepEqual(calls[1].refer,{});
66
+ }finally{await new Promise(r=>server.close(r));}
67
+ });
68
+
69
+ test('proxy applies the same typed contract to ranges, multi-select, null and unsupported raw values',async()=>{
70
+ const calls=[];
71
+ const middleware=createDevMiddleware({basePath:'/bi',dashboardId:'r',widgets:[{entryId:'w'}],maxRows:1000,filters:[
72
+ {name:'产品',label:'产品',type:'multi-select',allowNull:true,options:[{label:'A',value:'A'},{label:'B',value:'B'}]},
73
+ {name:'日期',label:'日期',type:'date-range'}, {name:'金额',label:'金额',type:'number'},
74
+ ]},{FINEBI_DEV_BASE_URL:'http://bi.example/bi',FINEBI_DEV_TOKEN:'secret'},async (_url,init)=>{calls.push(JSON.parse(init.body));return new Response(JSON.stringify({success:true,data:{}}));});
75
+ const server=createServer((req,res)=>middleware(req,res,()=>{}));await new Promise(r=>server.listen(0,'127.0.0.1',r));
76
+ const send=refer=>fetch(`http://127.0.0.1:${server.address().port}/bi/v5/api/dashboard/report/r/widgets/query`,{method:'POST',headers:{'X-Requested-With':'XMLHttpRequest'},body:JSON.stringify({widgetIds:['w'],pageLimit:10,refer})});
77
+ try {
78
+ for(const refer of [{'产品':['A','B']},{'产品':['A,C']},{'产品':['A,A']},{'产品':['A,']},{'日期_s':['2024-03-01'],'日期_e':['2024-02-29']},{'日期':['2024-01-01']},{'金额':['10,20']},null]) assert.equal((await send(refer)).status,400);
79
+ assert.equal(calls.length,0);
80
+ for(const refer of [{'产品':['A,B'],'日期_s':['2024-02-29'],'金额':['-0.25']},{'产品':['null']},{}]) {
81
+ assert.equal((await send(refer)).status,200);assert.deepEqual(calls.at(-1).refer,refer);
82
+ }
83
+ }finally{await new Promise(r=>server.close(r));}
84
+ });
@@ -0,0 +1,136 @@
1
+ import assert from 'node:assert/strict';
2
+ import {after, test} from 'node:test';
3
+ import {createServer} from 'vite';
4
+ const server = await createServer({configFile: false, cacheDir: 'temp/vite-tests', server: {middlewareMode: true, hmr: false}});
5
+ after(() => server.close());
6
+ const {normalize} = await server.ssrLoadModule('/src/finebi/normalize.ts');
7
+ const {queryWidgets, FineBIError} = await server.ssrLoadModule('/src/finebi/client.ts');
8
+ const {chartValues, selectDataset} = await server.ssrLoadModule('/src/services/view-data.ts');
9
+ const detail = () => ({previewResult: {resultType: 'DETAIL', header: [
10
+ {dId: 'd', text: '重复名'}, {dId: 'n', text: '重复名'}, {dId: 't', text: '日期'}
11
+ ], items: [[{value: 'A', text: 'A', authority: true}, {value: '1234.50', formatText: '1,234.50元', authority: true}, {value: '1349020800000', formatText: '2012-10-01', authority: true}]], row: 1000, size: 1, hasNext: true}, widgetBean: {dimensions: {d: {type: 1}, n: {type: 2}, t: {type: 3}}}});
12
+ test('detail preserves field identity, display, decimal and date values with honest range', () => {
13
+ const r = normalize(detail());
14
+ assert.equal(r.schemaVersion, '1');
15
+ assert.equal(r.normalization, 'COMPLETE');
16
+ assert.deepEqual(r.datasets[0].columns.map(c => c.key), ['c0','c1','c2']);
17
+ assert.deepEqual(r.datasets[0].rows[0].cells.map(c => c.value), ['A','1234.50','1349020800000']);
18
+ assert.equal(r.datasets[0].rows[0].cells[1].display, '1,234.50元');
19
+ assert.equal(r.range.total.value, 1000);
20
+ assert.ok(r.warnings.includes('NEXT_PAGE_UNSUPPORTED'));
21
+ });
22
+ test('restricted, display-only, missing, unknown and invalid dates cannot become computable values', () => {
23
+ const source = detail();
24
+ source.previewResult.items = [[{value: 'secret', text: '***', authority: false}, {value: null, text: '20%', authority: true}, {value: '9223372036854775808', authority: true}], []];
25
+ const r = normalize(source);
26
+ assert.equal(r.normalization, 'PARTIAL');
27
+ const cells = r.datasets[0].rows[0].cells;
28
+ assert.equal(cells[0].state, 'RESTRICTED');
29
+ assert.equal(cells[0].value, null);
30
+ assert.equal(cells[1].state, 'UNAVAILABLE');
31
+ assert.equal(cells[2].reason, 'INVALID_TIMESTAMP_VALUE');
32
+ delete source.widgetBean;
33
+ assert.equal(normalize(source).datasets[0].rows[0].cells[1].value, null);
34
+ });
35
+ test('full-looking page does not discard native hasNext; empty is different from unsupported', () => {
36
+ const source = detail(); source.previewResult.row = 1;
37
+ assert.ok(normalize(source).warnings.includes('NEXT_PAGE_UNSUPPORTED'));
38
+ source.previewResult.items = []; source.previewResult.row = 0; source.previewResult.hasNext = false;
39
+ assert.equal(normalize(source).datasets[0].returnedRowCount, 0);
40
+ assert.equal(normalize({previewResult: {resultType: 'BICROSS'}}).normalization, 'UNSUPPORTED');
41
+ });
42
+ test('chart permission placeholders and all supported detail date metadata preserve their semantics', () => {
43
+ const r=normalize({previewResult:{resultType:'CHART',geoms:[{dataModel:{fields:[{id:'n',name:'指标',fieldType:'MEASURE'}],colData:[['***']],rowCount:1}}]}});
44
+ assert.equal(r.datasets[0].rows[0].cells[0].state,'RESTRICTED');
45
+ for(const type of [3,13,15,20]) {const s=detail();s.widgetBean.dimensions.t.type=type;assert.equal(normalize(s).datasets[0].columns[2].type,'DATE');}
46
+ });
47
+ test('chart preserves multiple models, rejects formatted numeric strings and mismatched columns', () => {
48
+ const model = {fields: [{id:'x',name:'地区',fieldType:'DIMENSION'}, {id:'y',name:'金额',fieldType:'MEASURE'}], colData: [['East','West'], [12,'1,234']],rowCount:2};
49
+ const r = normalize({previewResult:{resultType:'CHART',geoms:[{dataModel:model},{dataModel:{...model,colData:[['A'],[]],rowCount:1}}]}});
50
+ assert.equal(r.datasets.length,2);
51
+ assert.equal(r.datasets[0].rows[0].cells[1].value,'12');
52
+ assert.equal(r.datasets[0].rows[1].cells[1].state,'UNAVAILABLE');
53
+ assert.equal(r.normalization,'PARTIAL');
54
+ });
55
+ const config = {schemaVersion:'1',basePath:'/webroot/decision',dashboardId:'report',maxRows:1000,widgets:[{entryId:'table',type:'table',name:'明细'}],filters:[{name:'地区',label:'地区',options:[{label:'西部',value:'West'}]}]};
56
+ test('query sends scoped refer, cookie credentials and bounded pageLimit without browser token storage', async () => {
57
+ let sent;
58
+ const r = await queryWidgets(config, {'地区':'West'}, undefined, async (url, init) => {
59
+ sent = {url, ...init};
60
+ return new Response(JSON.stringify({success:true,data:{table:detail()}}));
61
+ });
62
+ assert.equal(sent.credentials,'include');
63
+ assert.equal(sent.headers['X-Requested-With'],'XMLHttpRequest');
64
+ assert.deepEqual(JSON.parse(sent.body).refer, {'地区':['West']});
65
+ assert.equal(JSON.parse(sent.body).pageLimit,1000);
66
+ assert.equal(sent.headers.Authorization,undefined);
67
+ assert.equal(r.table.datasets[0].returnedRowCount,1);
68
+ await assert.rejects(queryWidgets({...config,maxRows:10001},{},undefined,()=>{throw new Error('must not fetch');}), /maxRows/);
69
+ await assert.rejects(queryWidgets(config,{'未知':'x'}), /筛选/);
70
+ });
71
+ test('auth expiry, permission denial, login HTML and missing widget stay distinct from empty data', async () => {
72
+ for (const [response, code] of [[new Response('',{status:302,headers:{Location:'/login'}}),'AUTH_REQUIRED'],[new Response('',{status:401}),'AUTH_REQUIRED'],[new Response('',{status:403}),'PERMISSION_DENIED'],[new Response(JSON.stringify({errorCode:'TIME_OUT'})),'AUTH_REQUIRED'],[new Response('<html>login</html>',{headers:{'content-type':'text/html'}}),'AUTH_REQUIRED'],[new Response(JSON.stringify({success:true,data:{}})),'INVALID_RESPONSE']]) {
73
+ await assert.rejects(queryWidgets(config,{},undefined,async()=>response), e=>e instanceof FineBIError && e.code===code);
74
+ }
75
+ });
76
+ test('view binding requires exact fields and an explicit model for multi-model results', () => {
77
+ const source = {previewResult:{resultType:'CHART',geoms:[{dataModel:{fields:[{id:'x',name:'地区',fieldType:'DIMENSION'},{id:'y',name:'金额',fieldType:'MEASURE'}],colData:[['East','West'],[12,5]],rowCount:2}}]}};
78
+ const r=normalize(source);
79
+ assert.deepEqual(chartValues(selectDataset(r),{categoryFieldId:'x',valueFieldId:'y'}),[{name:'East',value:12},{name:'West',value:5}]);
80
+ assert.throws(()=>chartValues(selectDataset(r),{categoryFieldId:'x',valueFieldId:'missing'}),/字段/);
81
+ r.datasets.push({...r.datasets[0],id:'model1'});
82
+ assert.throws(()=>selectDataset(r),/模型/);
83
+ assert.equal(selectDataset(r,'model1').id,'model1');
84
+ });
85
+ test('date filters accept calendar dates without options and omit cleared dates', async () => {
86
+ const dateConfig = {...config, filters:[{name:'日期',label:'日期',type:'date'},...config.filters]};
87
+ let sent;
88
+ const fetchImpl=async (_url,init)=>{sent=JSON.parse(init.body);return new Response(JSON.stringify({success:true,data:{table:detail()}}));};
89
+ for (const value of ['2012-10-02','2024-02-29','0001-01-01','9999-12-31']) {
90
+ await queryWidgets(dateConfig,{'日期':value},undefined,fetchImpl);
91
+ assert.deepEqual(sent.refer,{'日期':[value]});
92
+ }
93
+ await queryWidgets(dateConfig,{'日期':''},undefined,fetchImpl);
94
+ assert.deepEqual(sent.refer,{});
95
+ for (const value of ['2023-02-29','2024-02-30','2024-04-31','0000-01-01','10000-01-01','2024-13-01','2024-1-01','2024-01-01T00:00:00Z']) {
96
+ await assert.rejects(queryWidgets(dateConfig,{'日期':value},undefined,()=>{assert.fail('invalid date must not be sent');}),/筛选/);
97
+ }
98
+ for (const filter of [{name:'日期',label:'日期',type:'date',options:[]},{name:'日期',label:'日期',type:'unknown'}]) {
99
+ await assert.rejects(queryWidgets({...config,filters:[filter]},{},undefined,()=>{assert.fail('invalid configuration must not be sent');}),/筛选/);
100
+ }
101
+ });
102
+
103
+ test('typed filters encode native multi-value, null and open date ranges without guessing', async () => {
104
+ const filters=[
105
+ {name:'文本',label:'文本',type:'text',allowNull:true},
106
+ {name:'多选',label:'多选',type:'multi-select',options:[{label:'东',value:'East'},{label:'西',value:'West'}]},
107
+ {name:'数值',label:'数值',type:'number'},
108
+ {name:'区间',label:'区间',type:'date-range'},
109
+ {name:'月份',label:'月份',type:'month-range'},
110
+ {name:'年',label:'年',type:'year'}, {name:'季',label:'季',type:'quarter'},
111
+ {name:'标签',label:'标签',type:'time-label',options:[{label:'本月',value:'本月,动态'}]},
112
+ ];
113
+ let sent;
114
+ const send=values=>queryWidgets({...config,filters},values,undefined,async (_url,init)=>{
115
+ sent=JSON.parse(init.body);return new Response(JSON.stringify({success:true,data:{table:detail()}}));
116
+ });
117
+ await send({'文本':' North ','多选':['East','West'],'数值':'12345678901234567890.25','区间':{start:'2024-02-29',end:'2024-03-01'},'月份':{start:'2024-01',end:''},'年':'2024','季':'2024-3','标签':'本月,动态'});
118
+ assert.deepEqual(sent.refer,{'文本':[' North '],'多选':['East,West'],'数值':['12345678901234567890.25'],'区间_s':['2024-02-29'],'区间_e':['2024-03-01'],'月份_s':['2024-01'],'年':['2024'],'季':['2024-3'],'标签':['本月,动态']});
119
+ await send({'文本':null,'多选':[],'区间':{start:'',end:''}});
120
+ assert.deepEqual(sent.refer,{'文本':['null']});
121
+ await send({}); assert.deepEqual(sent.refer,{});
122
+ for(const values of [{'文本':'A,B'},{'文本':'null'},{'多选':['East,West']},{'多选':['East','East']},{'多选':[null]},{'数值':'1e3'},{'区间':{start:'2024-03-02',end:'2024-03-01'}},{'区间':{start:'2024-02-30',end:''}},{'月份':{start:'2024-13',end:''}},{'年':'0000'},{'季':'2024-5'},{'标签':'未知'},{'区间':null},{'未知':''}]) {
123
+ await assert.rejects(send(values),/筛选/);
124
+ }
125
+ });
126
+
127
+ test('filter configuration rejects ambiguous native keys and unrepresentable choices before fetching',async()=>{
128
+ for(const filters of [
129
+ [{name:'范围',label:'范围',type:'date-range'},{name:'范围_s',label:'起始',type:'date'}],
130
+ [{name:'重复',label:'A',type:'text'},{name:'重复',label:'B',type:'date'}],
131
+ [{name:'x',label:'x',options:[{label:'逗号',value:'A,B'}]}],
132
+ [{name:'x',label:'x',options:[{label:'字面空值',value:'null'}]}],
133
+ [{name:'x',label:'x',type:'number-range'}],
134
+ [{name:'x',label:'x',type:'date',allowNull:true}],
135
+ ]) await assert.rejects(queryWidgets({...config,filters},{},undefined,()=>assert.fail('invalid config must not fetch')),/筛选/);
136
+ });
@@ -0,0 +1,4 @@
1
+ {
2
+ "compilerOptions": {"target":"ES2020","lib":["ES2020","DOM","DOM.Iterable"],"module":"ESNext","moduleResolution":"Bundler","jsx":"react-jsx","strict":true,"skipLibCheck":true,"esModuleInterop":true,"resolveJsonModule":true,"isolatedModules":true,"noEmit":true},
3
+ "include":["src","vite.config.ts"]
4
+ }
@@ -0,0 +1,13 @@
1
+ import {readFileSync} from 'node:fs';
2
+ import {defineConfig, loadEnv} from 'vite';
3
+ // @ts-expect-error The development-only Node module is tested directly with node:test.
4
+ import {createDevMiddleware} from './scripts/dev-proxy.mjs';
5
+ export default defineConfig(({mode}) => ({
6
+ base: './',
7
+ server: {host:'127.0.0.1',port:4173,strictPort:true},
8
+ plugins:[{name:'finebi-development-session',apply:'serve',configureServer(server) {
9
+ const config=JSON.parse(readFileSync(new URL('./public/app-config.json',import.meta.url),'utf8'));
10
+ const env={...loadEnv(mode,process.cwd(),'FINEBI_DEV_'),...process.env};
11
+ server.middlewares.use(createDevMiddleware(config,env));
12
+ }}],
13
+ }));