@nocobase/plugin-data-visualization 0.10.1-alpha.1

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 (108) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +88 -0
  3. package/client.d.ts +3 -0
  4. package/client.js +65 -0
  5. package/lib/client/Settings.d.ts +2 -0
  6. package/lib/client/Settings.js +81 -0
  7. package/lib/client/block/ChartBlock.d.ts +2 -0
  8. package/lib/client/block/ChartBlock.js +73 -0
  9. package/lib/client/block/ChartBlockDesigner.d.ts +2 -0
  10. package/lib/client/block/ChartBlockDesigner.js +35 -0
  11. package/lib/client/block/ChartBlockInitializer.d.ts +6 -0
  12. package/lib/client/block/ChartBlockInitializer.js +114 -0
  13. package/lib/client/block/ChartConfigure.d.ts +33 -0
  14. package/lib/client/block/ChartConfigure.js +501 -0
  15. package/lib/client/block/formatters.d.ts +15 -0
  16. package/lib/client/block/formatters.js +58 -0
  17. package/lib/client/block/index.d.ts +4 -0
  18. package/lib/client/block/index.js +49 -0
  19. package/lib/client/block/schemas/configure.d.ts +4 -0
  20. package/lib/client/block/schemas/configure.js +492 -0
  21. package/lib/client/block/transformers.d.ts +6 -0
  22. package/lib/client/block/transformers.js +69 -0
  23. package/lib/client/hooks.d.ts +312 -0
  24. package/lib/client/hooks.js +275 -0
  25. package/lib/client/index.d.ts +5 -0
  26. package/lib/client/index.js +70 -0
  27. package/lib/client/locale/en-US.d.ts +23 -0
  28. package/lib/client/locale/en-US.js +29 -0
  29. package/lib/client/locale/index.d.ts +3 -0
  30. package/lib/client/locale/index.js +39 -0
  31. package/lib/client/locale/ja-JP.d.ts +2 -0
  32. package/lib/client/locale/ja-JP.js +8 -0
  33. package/lib/client/locale/pt-BR.d.ts +23 -0
  34. package/lib/client/locale/pt-BR.js +29 -0
  35. package/lib/client/locale/ru-RU.d.ts +2 -0
  36. package/lib/client/locale/ru-RU.js +8 -0
  37. package/lib/client/locale/tr-TR.d.ts +2 -0
  38. package/lib/client/locale/tr-TR.js +8 -0
  39. package/lib/client/locale/zh-CN.d.ts +70 -0
  40. package/lib/client/locale/zh-CN.js +76 -0
  41. package/lib/client/renderer/ChartLibrary.d.ts +71 -0
  42. package/lib/client/renderer/ChartLibrary.js +140 -0
  43. package/lib/client/renderer/ChartRenderer.d.ts +7 -0
  44. package/lib/client/renderer/ChartRenderer.js +258 -0
  45. package/lib/client/renderer/ChartRendererProvider.d.ts +43 -0
  46. package/lib/client/renderer/ChartRendererProvider.js +38 -0
  47. package/lib/client/renderer/index.d.ts +4 -0
  48. package/lib/client/renderer/index.js +49 -0
  49. package/lib/client/renderer/library/AntdLibrary.d.ts +2 -0
  50. package/lib/client/renderer/library/AntdLibrary.js +123 -0
  51. package/lib/client/renderer/library/G2PlotLibrary.d.ts +2 -0
  52. package/lib/client/renderer/library/G2PlotLibrary.js +288 -0
  53. package/lib/client/renderer/library/index.d.ts +3 -0
  54. package/lib/client/renderer/library/index.js +15 -0
  55. package/lib/client/utils.d.ts +96 -0
  56. package/lib/client/utils.js +137 -0
  57. package/lib/index.d.ts +1 -0
  58. package/lib/index.js +13 -0
  59. package/lib/server/actions/formatter.d.ts +3 -0
  60. package/lib/server/actions/formatter.js +44 -0
  61. package/lib/server/actions/query.d.ts +86 -0
  62. package/lib/server/actions/query.js +326 -0
  63. package/lib/server/index.d.ts +1 -0
  64. package/lib/server/index.js +13 -0
  65. package/lib/server/plugin.d.ts +13 -0
  66. package/lib/server/plugin.js +64 -0
  67. package/package.json +23 -0
  68. package/server.d.ts +3 -0
  69. package/server.js +65 -0
  70. package/src/client/Settings.tsx +43 -0
  71. package/src/client/__tests__/chart-configure.test.tsx +14 -0
  72. package/src/client/__tests__/chart-library.test.ts +78 -0
  73. package/src/client/__tests__/chart-renderer.test.tsx +30 -0
  74. package/src/client/__tests__/hooks.test.ts +261 -0
  75. package/src/client/block/ChartBlock.tsx +22 -0
  76. package/src/client/block/ChartBlockDesigner.tsx +19 -0
  77. package/src/client/block/ChartBlockInitializer.tsx +83 -0
  78. package/src/client/block/ChartConfigure.tsx +450 -0
  79. package/src/client/block/formatters.ts +70 -0
  80. package/src/client/block/index.ts +4 -0
  81. package/src/client/block/schemas/configure.ts +474 -0
  82. package/src/client/block/transformers.ts +52 -0
  83. package/src/client/hooks.ts +239 -0
  84. package/src/client/index.tsx +41 -0
  85. package/src/client/locale/en-US.ts +23 -0
  86. package/src/client/locale/index.ts +19 -0
  87. package/src/client/locale/ja-JP.ts +1 -0
  88. package/src/client/locale/pt-BR.ts +23 -0
  89. package/src/client/locale/ru-RU.ts +1 -0
  90. package/src/client/locale/tr-TR.ts +1 -0
  91. package/src/client/locale/zh-CN.ts +71 -0
  92. package/src/client/renderer/ChartLibrary.tsx +178 -0
  93. package/src/client/renderer/ChartRenderer.tsx +201 -0
  94. package/src/client/renderer/ChartRendererProvider.tsx +58 -0
  95. package/src/client/renderer/index.ts +4 -0
  96. package/src/client/renderer/library/AntdLibrary.tsx +94 -0
  97. package/src/client/renderer/library/G2PlotLibrary.tsx +236 -0
  98. package/src/client/renderer/library/index.tsx +4 -0
  99. package/src/client/utils.ts +102 -0
  100. package/src/index.ts +1 -0
  101. package/src/server/__tests__/api.test.ts +105 -0
  102. package/src/server/__tests__/formatter.test.ts +49 -0
  103. package/src/server/__tests__/query.test.ts +220 -0
  104. package/src/server/actions/formatter.ts +49 -0
  105. package/src/server/actions/query.ts +285 -0
  106. package/src/server/collections/.gitkeep +0 -0
  107. package/src/server/index.ts +1 -0
  108. package/src/server/plugin.ts +37 -0
@@ -0,0 +1,105 @@
1
+ import { Database } from '@nocobase/database';
2
+ import { MockServer, mockServer } from '@nocobase/test';
3
+ import { queryData } from '../actions/query';
4
+ import ChartsV2Plugin from '../plugin';
5
+
6
+ describe('api', () => {
7
+ let app: MockServer;
8
+ let db: Database;
9
+
10
+ beforeAll(async () => {
11
+ app = mockServer({
12
+ acl: true,
13
+ plugins: ['users', 'auth'],
14
+ });
15
+ app.plugin(ChartsV2Plugin);
16
+ await app.loadAndInstall({ clean: true });
17
+ db = app.db;
18
+
19
+ db.collection({
20
+ name: 'chart_test',
21
+ fields: [
22
+ {
23
+ type: 'double',
24
+ name: 'price',
25
+ },
26
+ {
27
+ type: 'bigInt',
28
+ name: 'count',
29
+ },
30
+ {
31
+ type: 'string',
32
+ name: 'title',
33
+ },
34
+ {
35
+ type: 'date',
36
+ name: 'createdAt',
37
+ },
38
+ ],
39
+ });
40
+ await db.sync();
41
+ const repo = db.getRepository('chart_test');
42
+ await repo.create({
43
+ values: [
44
+ { price: 1, count: 1, title: 'title1' },
45
+ { price: 2, count: 2, title: 'title2' },
46
+ ],
47
+ });
48
+ });
49
+
50
+ afterAll(async () => {
51
+ await db.close();
52
+ });
53
+
54
+ test('query', () => {
55
+ expect.assertions(1);
56
+ return expect(
57
+ queryData({ db } as any, {
58
+ collection: 'chart_test',
59
+ measures: [
60
+ {
61
+ field: ['price'],
62
+ alias: 'Price',
63
+ },
64
+ {
65
+ field: ['count'],
66
+ alias: 'Count',
67
+ },
68
+ ],
69
+ dimensions: [
70
+ {
71
+ field: ['title'],
72
+ alias: 'Title',
73
+ },
74
+ ],
75
+ }),
76
+ ).resolves.toBeDefined();
77
+ });
78
+
79
+ test('query with sort', () => {
80
+ expect.assertions(1);
81
+ return expect(
82
+ queryData({ db } as any, {
83
+ collection: 'chart_test',
84
+ measures: [
85
+ {
86
+ field: ['price'],
87
+ aggregation: 'sum',
88
+ alias: 'Price',
89
+ },
90
+ ],
91
+ dimensions: [
92
+ {
93
+ field: ['title'],
94
+ alias: 'Title',
95
+ },
96
+ {
97
+ field: ['createdAt'],
98
+ format: 'YYYY',
99
+ },
100
+ ],
101
+ orders: [{ field: 'createdAt', order: 'asc' }],
102
+ }),
103
+ ).resolves.toBeDefined();
104
+ });
105
+ });
@@ -0,0 +1,49 @@
1
+ import { dateFormatFn } from '../actions/formatter';
2
+
3
+ describe('formatter', () => {
4
+ const field = 'field';
5
+ const format = 'YYYY-MM-DD hh:mm:ss';
6
+ describe('dateFormatFn', () => {
7
+ it('should return correct format for sqlite', () => {
8
+ const sequelize = {
9
+ fn: jest.fn().mockImplementation((fn: string, format: string, field: string) => ({
10
+ fn,
11
+ format,
12
+ field,
13
+ })),
14
+ col: jest.fn().mockImplementation((field: string) => field),
15
+ };
16
+ const dialect = 'sqlite';
17
+ const result = dateFormatFn(sequelize, dialect, field, format);
18
+ expect(result.format).toEqual('%Y-%m-%d %H:%M:%S');
19
+ });
20
+
21
+ it('should return correct format for mysql', () => {
22
+ const sequelize = {
23
+ fn: jest.fn().mockImplementation((fn: string, field: string, format: string) => ({
24
+ fn,
25
+ format,
26
+ field,
27
+ })),
28
+ col: jest.fn().mockImplementation((field: string) => field),
29
+ };
30
+ const dialect = 'mysql';
31
+ const result = dateFormatFn(sequelize, dialect, field, format);
32
+ expect(result.format).toEqual('%Y-%m-%d %H:%i:%S');
33
+ });
34
+
35
+ it('should return correct format for postgres', () => {
36
+ const sequelize = {
37
+ fn: jest.fn().mockImplementation((fn: string, field: string, format: string) => ({
38
+ fn,
39
+ format,
40
+ field,
41
+ })),
42
+ col: jest.fn().mockImplementation((field: string) => field),
43
+ };
44
+ const dialect = 'postgres';
45
+ const result = dateFormatFn(sequelize, dialect, field, format);
46
+ expect(result.format).toEqual('YYYY-MM-DD HH24:MI:SS');
47
+ });
48
+ });
49
+ });
@@ -0,0 +1,220 @@
1
+ import { MockServer, mockServer } from '@nocobase/test';
2
+ import * as formatter from '../actions/formatter';
3
+ import { cacheWrap, parseBuilder, parseFieldAndAssociations } from '../actions/query';
4
+
5
+ describe('query', () => {
6
+ describe('parseBuilder', () => {
7
+ const sequelize = {
8
+ fn: jest.fn().mockImplementation((fn: string, field: string) => [fn, field]),
9
+ col: jest.fn().mockImplementation((field: string) => field),
10
+ };
11
+ let ctx: any;
12
+ let app: MockServer;
13
+
14
+ beforeAll(() => {
15
+ app = mockServer();
16
+ app.db.collection({
17
+ name: 'orders',
18
+ fields: [
19
+ {
20
+ name: 'id',
21
+ type: 'bigInt',
22
+ },
23
+ {
24
+ name: 'price',
25
+ type: 'double',
26
+ },
27
+ {
28
+ name: 'createdAt',
29
+ type: 'date',
30
+ },
31
+ {
32
+ type: 'belongsTo',
33
+ name: 'user',
34
+ target: 'users',
35
+ targetKey: 'id',
36
+ foreignKey: 'userId',
37
+ },
38
+ ],
39
+ });
40
+ app.db.collection({
41
+ name: 'users',
42
+ fields: [
43
+ {
44
+ name: 'id',
45
+ type: 'bigInt',
46
+ },
47
+ {
48
+ name: 'name',
49
+ type: 'string',
50
+ },
51
+ ],
52
+ });
53
+ ctx = {
54
+ db: {
55
+ sequelize,
56
+ getRepository: (name: string) => app.db.getRepository(name),
57
+ getModel: (name: string) => app.db.getModel(name),
58
+ getCollection: (name: string) => app.db.getCollection(name),
59
+ options: {
60
+ underscored: true,
61
+ },
62
+ },
63
+ };
64
+ });
65
+
66
+ it('should parse field and associations', () => {
67
+ const associations = parseFieldAndAssociations(ctx, {
68
+ collection: 'orders',
69
+ measures: [{ field: ['price'], aggregation: 'sum', alias: 'price' }],
70
+ dimensions: [{ field: ['createdAt'] }, { field: ['user', 'name'] }],
71
+ });
72
+ expect(associations).toMatchObject({
73
+ measures: [{ field: 'orders.price', aggregation: 'sum', alias: 'price', type: 'double' }],
74
+ dimensions: [
75
+ { field: 'orders.created_at', alias: 'createdAt', type: 'date' },
76
+ { field: 'user.name', alias: 'user.name' },
77
+ ],
78
+ include: [{ association: 'user' }],
79
+ });
80
+ });
81
+
82
+ it('should parse measures', () => {
83
+ const measures1 = [
84
+ {
85
+ field: ['price'],
86
+ },
87
+ ];
88
+ const { queryParams: result1 } = parseBuilder(ctx, { collection: 'orders', measures: measures1 });
89
+ expect(result1.attributes).toEqual([['orders.price', 'price']]);
90
+
91
+ const measures2 = [
92
+ {
93
+ field: ['price'],
94
+ aggregation: 'sum',
95
+ alias: 'price-alias',
96
+ },
97
+ ];
98
+ const { queryParams: result2 } = parseBuilder(ctx, { collection: 'orders', measures: measures2 });
99
+ expect(result2.attributes).toEqual([[['sum', 'orders.price'], 'price-alias']]);
100
+ });
101
+
102
+ it('should parse dimensions', () => {
103
+ jest.spyOn(formatter, 'formatter').mockReturnValue('formatted-field');
104
+ const dimensions = [
105
+ {
106
+ field: ['createdAt'],
107
+ format: 'YYYY-MM-DD',
108
+ alias: 'Created at',
109
+ },
110
+ ];
111
+ const { queryParams: result } = parseBuilder(ctx, { collection: 'orders', dimensions });
112
+ expect(result.attributes).toEqual([['formatted-field', 'Created at']]);
113
+ expect(result.group).toEqual([]);
114
+
115
+ const measures = [
116
+ {
117
+ field: ['field'],
118
+ aggregation: 'sum',
119
+ },
120
+ ];
121
+ const { queryParams: result2 } = parseBuilder(ctx, { collection: 'orders', measures, dimensions });
122
+ expect(result2.group).toEqual(['formatted-field']);
123
+ });
124
+
125
+ it('should parse filter', () => {
126
+ const filter = {
127
+ createdAt: {
128
+ $gt: '2020-01-01',
129
+ },
130
+ };
131
+ const { queryParams: result } = parseBuilder(ctx, { collection: 'orders', filter });
132
+ expect(result.where.createdAt).toBeDefined();
133
+ });
134
+ });
135
+
136
+ describe('cacheWrap', () => {
137
+ const key = 'test-key';
138
+ const value = 'test-val';
139
+ class MockCache {
140
+ map: Map<string, any> = new Map();
141
+ async func() {
142
+ return value;
143
+ }
144
+
145
+ get(key: string) {
146
+ return this.map.get(key);
147
+ }
148
+ set(key: string, value: any) {
149
+ this.map.set(key, value);
150
+ }
151
+ }
152
+ let cache: any;
153
+ let query: () => Promise<any>;
154
+
155
+ beforeEach(() => {
156
+ cache = new MockCache();
157
+ });
158
+
159
+ it('should use cache', async () => {
160
+ query = async () =>
161
+ await cacheWrap(cache, {
162
+ key,
163
+ func: cache.func,
164
+ useCache: true,
165
+ refresh: false,
166
+ });
167
+
168
+ const spy = jest.spyOn(cache, 'func');
169
+ expect(cache.get(key)).toBeUndefined();
170
+ const result = await query();
171
+ expect(cache.func).toBeCalled();
172
+ expect(result).toEqual(value);
173
+ expect(cache.get(key)).toEqual(value);
174
+
175
+ spy.mockReset();
176
+ const result2 = await query();
177
+ expect(result2).toEqual(value);
178
+ expect(cache.func).not.toBeCalled();
179
+ });
180
+
181
+ it('should not use cache', async () => {
182
+ query = async () =>
183
+ await cacheWrap(cache, {
184
+ key,
185
+ func: cache.func,
186
+ useCache: false,
187
+ refresh: false,
188
+ });
189
+
190
+ cache.set(key, value);
191
+ expect(cache.get(key)).toBeDefined();
192
+ jest.spyOn(cache, 'func');
193
+ const result = await query();
194
+ expect(cache.func).toBeCalled();
195
+ expect(result).toEqual(value);
196
+ });
197
+
198
+ it('should refresh', async () => {
199
+ query = async () =>
200
+ await cacheWrap(cache, {
201
+ key,
202
+ func: cache.func,
203
+ useCache: true,
204
+ refresh: true,
205
+ });
206
+
207
+ const spy = jest.spyOn(cache, 'func');
208
+ expect(cache.get(key)).toBeUndefined();
209
+ const result = await query();
210
+ expect(cache.func).toBeCalled();
211
+ expect(result).toEqual(value);
212
+ expect(cache.get(key)).toEqual(value);
213
+
214
+ spy.mockClear();
215
+ const result2 = await query();
216
+ expect(cache.func).toBeCalled();
217
+ expect(result2).toEqual(value);
218
+ });
219
+ });
220
+ });
@@ -0,0 +1,49 @@
1
+ export const dateFormatFn = (sequelize: any, dialect: string, field: string, format: string) => {
2
+ switch (dialect) {
3
+ case 'sqlite':
4
+ format = format
5
+ .replace(/YYYY/g, '%Y')
6
+ .replace(/MM/g, '%m')
7
+ .replace(/DD/g, '%d')
8
+ .replace(/hh/g, '%H')
9
+ .replace(/mm/g, '%M')
10
+ .replace(/ss/g, '%S');
11
+ return sequelize.fn('strftime', format, sequelize.col(field));
12
+ case 'mysql':
13
+ format = format
14
+ .replace(/YYYY/g, '%Y')
15
+ .replace(/MM/g, '%m')
16
+ .replace(/DD/g, '%d')
17
+ .replace(/hh/g, '%H')
18
+ .replace(/mm/g, '%i')
19
+ .replace(/ss/g, '%S');
20
+ return sequelize.fn('date_format', sequelize.col(field), format);
21
+ case 'postgres':
22
+ format = format.replace(/hh/g, 'HH24').replace(/mm/g, 'MI').replace(/ss/g, 'SS');
23
+ return sequelize.fn('to_char', sequelize.col(field), format);
24
+ default:
25
+ return field;
26
+ }
27
+ };
28
+
29
+ export const formatFn = (sequelize: any, dialect: string, field: string, format: string) => {
30
+ switch (dialect) {
31
+ case 'sqlite':
32
+ case 'postgres':
33
+ return sequelize.fn('format', format, sequelize.col(field));
34
+ default:
35
+ return field;
36
+ }
37
+ };
38
+
39
+ export const formatter = (sequelize: any, type: string, field: string, format: string) => {
40
+ const dialect = sequelize.getDialect();
41
+ switch (type) {
42
+ case 'date':
43
+ case 'datetime':
44
+ case 'time':
45
+ return dateFormatFn(sequelize, dialect, field, format);
46
+ default:
47
+ return formatFn(sequelize, dialect, field, format);
48
+ }
49
+ };
@@ -0,0 +1,285 @@
1
+ import { Context, Next } from '@nocobase/actions';
2
+ import { Cache } from '@nocobase/cache';
3
+ import { FilterParser, snakeCase } from '@nocobase/database';
4
+ import ChartsV2Plugin from '../plugin';
5
+ import { formatter } from './formatter';
6
+
7
+ type MeasureProps = {
8
+ field: string | string[];
9
+ type?: string;
10
+ aggregation?: string;
11
+ alias?: string;
12
+ };
13
+
14
+ type DimensionProps = {
15
+ field: string | string[];
16
+ type?: string;
17
+ alias?: string;
18
+ format?: string;
19
+ };
20
+
21
+ type OrderProps = {
22
+ field: string | string[];
23
+ alias?: string;
24
+ order?: 'asc' | 'desc';
25
+ };
26
+
27
+ type QueryParams = Partial<{
28
+ uid: string;
29
+ collection: string;
30
+ measures: MeasureProps[];
31
+ dimensions: DimensionProps[];
32
+ orders: OrderProps[];
33
+ filter: any;
34
+ limit: number;
35
+ sql: {
36
+ fields?: string;
37
+ clauses?: string;
38
+ };
39
+ cache: {
40
+ enabled: boolean;
41
+ ttl: number;
42
+ };
43
+ // Get the latest data from the database
44
+ refresh: boolean;
45
+ }>;
46
+
47
+ export const parseFieldAndAssociations = (ctx: Context, params: QueryParams) => {
48
+ const { collection: collectionName, measures, dimensions, orders, filter } = params;
49
+ const collection = ctx.db.getCollection(collectionName);
50
+ const fields = collection.fields;
51
+ const underscored = ctx.db.options.underscored;
52
+ const models: {
53
+ [target: string]: {
54
+ type: string;
55
+ };
56
+ } = {};
57
+ const parseField = (selected: { field: string | string[]; alias?: string }) => {
58
+ let target: string;
59
+ let name: string;
60
+ if (!Array.isArray(selected.field)) {
61
+ name = selected.field;
62
+ } else if (selected.field.length === 1) {
63
+ name = selected.field[0];
64
+ } else if (selected.field.length > 1) {
65
+ [target, name] = selected.field;
66
+ }
67
+ let field = underscored ? snakeCase(name) : name;
68
+ let type = fields.get(name)?.type;
69
+ if (target) {
70
+ field = `${target}.${field}`;
71
+ name = `${target}.${name}`;
72
+ type = fields.get(target)?.type;
73
+ if (!models[target]) {
74
+ models[target] = { type };
75
+ }
76
+ } else {
77
+ field = `${collectionName}.${field}`;
78
+ }
79
+ return {
80
+ ...selected,
81
+ field,
82
+ name,
83
+ type,
84
+ alias: selected.alias || name,
85
+ };
86
+ };
87
+
88
+ const parsedMeasures = measures?.map(parseField) || [];
89
+ const parsedDimensions = dimensions?.map(parseField) || [];
90
+ const parsedOrders = orders?.map(parseField) || [];
91
+ const include = Object.entries(models).map(([target, { type }]) => ({
92
+ association: target,
93
+ attributes: [],
94
+ ...(type === 'belongsToMany' ? { through: { attributes: [] } } : {}),
95
+ }));
96
+
97
+ const filterParser = new FilterParser(filter, {
98
+ collection,
99
+ });
100
+ const { where, include: filterInclude } = filterParser.toSequelizeParams();
101
+ const parsedFilterInclude = filterInclude?.map((item) => {
102
+ if (fields.get(item.association)?.type === 'belongsToMany') {
103
+ item.through = { attributes: [] };
104
+ }
105
+ return item;
106
+ });
107
+
108
+ return {
109
+ where,
110
+ measures: parsedMeasures,
111
+ dimensions: parsedDimensions,
112
+ orders: parsedOrders,
113
+ include: [...include, ...(parsedFilterInclude || [])],
114
+ };
115
+ };
116
+
117
+ export const parseBuilder = (ctx: Context, builder: QueryParams) => {
118
+ const { sequelize } = ctx.db;
119
+ const { limit } = builder;
120
+ const { measures, dimensions, orders, include, where } = parseFieldAndAssociations(ctx, builder);
121
+ const attributes = [];
122
+ const group = [];
123
+ const order = [];
124
+ const fieldMap = {};
125
+ let hasAgg = false;
126
+
127
+ measures.forEach((measure: MeasureProps & { field: string }) => {
128
+ const { field, aggregation, alias } = measure;
129
+ const attribute = [];
130
+ const col = sequelize.col(field);
131
+ if (aggregation) {
132
+ hasAgg = true;
133
+ attribute.push(sequelize.fn(aggregation, col));
134
+ } else {
135
+ attribute.push(col);
136
+ }
137
+ if (alias) {
138
+ attribute.push(alias);
139
+ }
140
+ attributes.push(attribute.length > 1 ? attribute : attribute[0]);
141
+ fieldMap[alias || field] = measure;
142
+ });
143
+
144
+ dimensions.forEach((dimension: DimensionProps & { field: string }) => {
145
+ const { field, format, alias, type } = dimension;
146
+ const attribute = [];
147
+ const col = sequelize.col(field);
148
+ if (format) {
149
+ attribute.push(formatter(sequelize, type, field, format));
150
+ } else {
151
+ attribute.push(col);
152
+ }
153
+ if (alias) {
154
+ attribute.push(alias);
155
+ }
156
+ attributes.push(attribute.length > 1 ? attribute : attribute[0]);
157
+ if (hasAgg) {
158
+ group.push(attribute[0]);
159
+ }
160
+ fieldMap[alias || field] = dimension;
161
+ });
162
+
163
+ orders.forEach((item: OrderProps) => {
164
+ const name = hasAgg ? sequelize.literal(`"${item.alias}"`) : sequelize.col(item.field as string);
165
+ order.push([name, item.order || 'ASC']);
166
+ });
167
+
168
+ return {
169
+ queryParams: {
170
+ where,
171
+ attributes,
172
+ include,
173
+ group,
174
+ order,
175
+ limit: limit > 2000 ? 2000 : limit,
176
+ raw: true,
177
+ },
178
+ fieldMap,
179
+ };
180
+ };
181
+
182
+ export const processData = (ctx: Context, data: any[], fieldMap: { [source: string]: { type?: string } }) => {
183
+ const { sequelize } = ctx.db;
184
+ const dialect = sequelize.getDialect();
185
+ switch (dialect) {
186
+ case 'postgres':
187
+ // https://github.com/sequelize/sequelize/issues/4550
188
+ return data.map((record) => {
189
+ const result = {};
190
+ Object.entries(record).forEach(([key, value]) => {
191
+ const { type } = fieldMap[key] || {};
192
+ switch (type) {
193
+ case 'bigInt':
194
+ case 'integer':
195
+ case 'float':
196
+ case 'double':
197
+ value = Number(value);
198
+ break;
199
+ }
200
+ result[key] = value;
201
+ });
202
+ return result;
203
+ });
204
+ default:
205
+ return data;
206
+ }
207
+ };
208
+
209
+ export const queryData = async (ctx: Context, builder: QueryParams) => {
210
+ const { collection, measures, dimensions, orders, filter, limit, sql } = builder;
211
+ const model = ctx.db.getModel(collection);
212
+ const { queryParams, fieldMap } = parseBuilder(ctx, { collection, measures, dimensions, orders, filter, limit });
213
+ const data = await model.findAll(queryParams);
214
+ return processData(ctx, data, fieldMap);
215
+ // if (!sql) {
216
+ // return await repository.find(parseBuilder(ctx, { collection, measures, dimensions, orders, filter, limit }));
217
+ // }
218
+
219
+ // const statement = `SELECT ${sql.fields} FROM ${collection} ${sql.clauses}`;
220
+ // const [data] = await ctx.db.sequelize.query(statement);
221
+ // return data;
222
+ };
223
+
224
+ export const cacheWrap = async (
225
+ cache: Cache,
226
+ options: {
227
+ func: () => Promise<any>;
228
+ key: string;
229
+ ttl?: number;
230
+ useCache?: boolean;
231
+ refresh?: boolean;
232
+ },
233
+ ) => {
234
+ const { func, key, ttl, useCache, refresh } = options;
235
+ if (useCache && !refresh) {
236
+ const data = await cache.get(key);
237
+ if (data) {
238
+ return data;
239
+ }
240
+ }
241
+ const data = await func();
242
+ if (useCache) {
243
+ await cache.set(key, data, ttl);
244
+ }
245
+ return data;
246
+ };
247
+
248
+ export const query = async (ctx: Context, next: Next) => {
249
+ const {
250
+ uid,
251
+ collection,
252
+ measures,
253
+ dimensions,
254
+ orders,
255
+ filter,
256
+ limit,
257
+ sql,
258
+ cache: cacheConfig,
259
+ refresh,
260
+ } = ctx.action.params.values as QueryParams;
261
+ const roleName = ctx.state.currentRole || 'anonymous';
262
+ const can = ctx.app.acl.can({ role: roleName, resource: collection, action: 'list' });
263
+ if (!can && roleName !== 'root') {
264
+ ctx.throw(403, 'No permissions');
265
+ }
266
+
267
+ const plugin = ctx.app.getPlugin('data-visualization') as ChartsV2Plugin;
268
+ const cache = plugin.cache;
269
+ const useCache = cacheConfig?.enabled && uid;
270
+
271
+ try {
272
+ ctx.body = await cacheWrap(cache, {
273
+ func: async () => await queryData(ctx, { collection, measures, dimensions, orders, filter, limit, sql }),
274
+ key: uid,
275
+ ttl: cacheConfig?.ttl || 30,
276
+ useCache: useCache ? true : false,
277
+ refresh,
278
+ });
279
+ } catch (err) {
280
+ ctx.app.logger.error('charts query: ', err);
281
+ ctx.throw(500, err);
282
+ }
283
+
284
+ await next();
285
+ };
File without changes
@@ -0,0 +1 @@
1
+ export { default } from './plugin';