adminforth 1.2.28 → 1.2.29
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/dataConnectors/baseConnector.ts +15 -6
- package/dataConnectors/clickhouse.ts +67 -39
- package/dataConnectors/mongo.ts +6 -22
- package/dataConnectors/postgres.ts +6 -19
- package/dataConnectors/sqlite.ts +6 -25
- package/dist/dataConnectors/baseConnector.js +13 -3
- package/dist/dataConnectors/clickhouse.js +51 -32
- package/dist/dataConnectors/mongo.js +5 -23
- package/dist/dataConnectors/postgres.js +5 -18
- package/dist/dataConnectors/sqlite.js +6 -18
- package/dist/spa/spa/src/components/ResourceForm.vue +7 -5
- package/dist/spa/spa/src/views/CreateView.vue +1 -0
- package/package.json +3 -3
- package/spa/src/components/ResourceForm.vue +7 -5
- package/spa/src/views/CreateView.vue +1 -0
- package/types/AdminForthConfig.ts +7 -3
|
@@ -10,16 +10,25 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
getRecordByPrimaryKeyWithOriginalTypes(resource: AdminForthResource, id: string): Promise<any> {
|
|
14
|
-
|
|
13
|
+
async getRecordByPrimaryKeyWithOriginalTypes(resource: AdminForthResource, id: string): Promise<any> {
|
|
14
|
+
const data = await this.getDataWithOriginalTypes({
|
|
15
|
+
resource,
|
|
16
|
+
limit: 1,
|
|
17
|
+
offset: 0,
|
|
18
|
+
sort: [],
|
|
19
|
+
filters: [{ field: this.getPrimaryKey(resource), operator: AdminForthFilterOperators.EQ, value: id }],
|
|
20
|
+
getTotals: false
|
|
21
|
+
});
|
|
22
|
+
return data.data.length > 0 ? data.data[0] : null;
|
|
15
23
|
}
|
|
16
24
|
|
|
17
|
-
getDataWithOriginalTypes({ resource, limit, offset, sort, filters }: {
|
|
25
|
+
getDataWithOriginalTypes({ resource, limit, offset, sort, filters, getTotals }: {
|
|
18
26
|
resource: AdminForthResource,
|
|
19
27
|
limit: number,
|
|
20
28
|
offset: number,
|
|
21
29
|
sort: { field: string, direction: AdminForthSortDirections }[],
|
|
22
|
-
filters: { field: string, operator: AdminForthFilterOperators, value: any }[]
|
|
30
|
+
filters: { field: string, operator: AdminForthFilterOperators, value: any }[],
|
|
31
|
+
getTotals?: boolean
|
|
23
32
|
}): Promise<{ data: any[], total: number }> {
|
|
24
33
|
throw new Error('Method not implemented.');
|
|
25
34
|
}
|
|
@@ -68,7 +77,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
|
|
|
68
77
|
limit: number,
|
|
69
78
|
offset: number,
|
|
70
79
|
sort: { field: string, direction: AdminForthSortDirections }[],
|
|
71
|
-
filters: { field: string, operator: AdminForthFilterOperators, value: any }[]
|
|
80
|
+
filters: { field: string, operator: AdminForthFilterOperators, value: any }[]
|
|
72
81
|
}): Promise<{ data: any[], total: number }> {
|
|
73
82
|
if (filters) {
|
|
74
83
|
filters.map((f) => {
|
|
@@ -80,7 +89,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
|
|
|
80
89
|
});
|
|
81
90
|
}
|
|
82
91
|
|
|
83
|
-
const d = await this.getDataWithOriginalTypes({ resource, limit, offset, sort, filters });
|
|
92
|
+
const d = await this.getDataWithOriginalTypes({ resource, limit, offset, sort, filters, getTotals: true });
|
|
84
93
|
// call getFieldValue for each field
|
|
85
94
|
d.data.map((record) => {
|
|
86
95
|
for (const col of resource.dataSourceColumns) {
|
|
@@ -2,7 +2,9 @@ import betterSqlite3 from 'better-sqlite3';
|
|
|
2
2
|
import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirections, IAdminForthDataSourceConnector, AdminForthResource, AdminForthResourceColumn } from '../types/AdminForthConfig.js';
|
|
3
3
|
import AdminForthBaseConnector from './baseConnector.js';
|
|
4
4
|
import dayjs from 'dayjs';
|
|
5
|
-
import { createClient } from '@clickhouse/client' // or '@clickhouse/client-web'
|
|
5
|
+
import { createClient, ClickHouseLogLevel } from '@clickhouse/client' // or '@clickhouse/client-web'
|
|
6
|
+
import { isObject } from 'util';
|
|
7
|
+
import { date } from 'zod';
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
|
|
@@ -21,6 +23,18 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
|
|
|
21
23
|
// create connection here
|
|
22
24
|
this.client = createClient({
|
|
23
25
|
url: url.replace('clickhouse://', 'http://'),
|
|
26
|
+
clickhouse_settings: {
|
|
27
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
28
|
+
date_time_input_format: 'best_effort',
|
|
29
|
+
|
|
30
|
+
// Recommended for cluster usage to avoid situations where a query processing error occurred after the response code,
|
|
31
|
+
// and HTTP headers were already sent to the client.
|
|
32
|
+
// See https://clickhouse.com/docs/en/interfaces/http/#response-buffering
|
|
33
|
+
wait_end_of_query: 1,
|
|
34
|
+
},
|
|
35
|
+
// log:{
|
|
36
|
+
// level: ClickHouseLogLevel.TRACE,
|
|
37
|
+
// }
|
|
24
38
|
});
|
|
25
39
|
}
|
|
26
40
|
|
|
@@ -83,7 +97,8 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
|
|
|
83
97
|
} else if (field._underlineType.startsWith('DateTime')
|
|
84
98
|
|| field._underlineType.startsWith('String')
|
|
85
99
|
|| field._underlineType.startsWith('FixedString')) {
|
|
86
|
-
|
|
100
|
+
const v = dayjs(value).toISOString();
|
|
101
|
+
return v;
|
|
87
102
|
} else {
|
|
88
103
|
throw new Error(`AdminForth does not support row type: ${field._underlineType} for timestamps, use VARCHAR (with iso strings) or TIMESTAMP/INT (with unix timestamps). Issue in field "${field.name}"`);
|
|
89
104
|
}
|
|
@@ -104,21 +119,6 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
|
|
|
104
119
|
return value;
|
|
105
120
|
}
|
|
106
121
|
|
|
107
|
-
async getRecordByPrimaryKeyWithOriginalTypes(resource: AdminForthResource, key: any): Promise<any> {
|
|
108
|
-
const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
|
|
109
|
-
const tableName = resource.table;
|
|
110
|
-
const stmt = await this.client.query({
|
|
111
|
-
query: `SELECT ${columns} FROM ${tableName} WHERE ${this.getPrimaryKey(resource)} = ?`,
|
|
112
|
-
format: 'JSONEachRow',
|
|
113
|
-
query_params: [key],
|
|
114
|
-
});
|
|
115
|
-
const row = await stmt.json();
|
|
116
|
-
if (!row.length) {
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
return row[0];
|
|
120
|
-
}
|
|
121
|
-
|
|
122
122
|
setFieldValue(field: AdminForthResourceColumn, value: any): any {
|
|
123
123
|
if (field.type == AdminForthDataTypes.DATETIME) {
|
|
124
124
|
if (!value) {
|
|
@@ -131,7 +131,8 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
|
|
|
131
131
|
|| field._underlineType.startsWith('String')
|
|
132
132
|
|| field._underlineType.startsWith('FixedString')) {
|
|
133
133
|
// value is iso string now, convert to unix timestamp
|
|
134
|
-
|
|
134
|
+
const iso = dayjs(value).toISOString();
|
|
135
|
+
return iso;
|
|
135
136
|
}
|
|
136
137
|
} else if (field.type == AdminForthDataTypes.BOOLEAN) {
|
|
137
138
|
return value ? 1 : 0;
|
|
@@ -166,18 +167,20 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
|
|
|
166
167
|
};
|
|
167
168
|
|
|
168
169
|
|
|
169
|
-
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }: {
|
|
170
|
+
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters, getTotals }: {
|
|
170
171
|
resource: AdminForthResource,
|
|
171
172
|
limit: number,
|
|
172
173
|
offset: number,
|
|
173
174
|
sort: { field: string, direction: AdminForthSortDirections }[],
|
|
174
|
-
filters: { field: string, operator: AdminForthFilterOperators, value: any }[]
|
|
175
|
+
filters: { field: string, operator: AdminForthFilterOperators, value: any }[],
|
|
176
|
+
getTotals?: boolean,
|
|
175
177
|
}): Promise<{ data: any[], total: number }> {
|
|
176
178
|
const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
|
|
177
179
|
const tableName = resource.table;
|
|
178
180
|
|
|
179
181
|
const where = filters.length ? `WHERE ${filters.map((f, i) => {
|
|
180
|
-
|
|
182
|
+
const column = resource.dataSourceColumns.find((col) => col.name == f.field);
|
|
183
|
+
let placeholder = `{f${i}:${column._underlineType}}`;
|
|
181
184
|
let field = f.field;
|
|
182
185
|
let operator = this.OperatorsMap[f.operator];
|
|
183
186
|
if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
|
|
@@ -208,7 +211,6 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
|
|
|
208
211
|
|
|
209
212
|
|
|
210
213
|
const q = `SELECT ${columns} FROM ${tableName} ${where} ${orderBy} LIMIT {limit:Int} OFFSET {offset:Int}`;
|
|
211
|
-
|
|
212
214
|
const d = {
|
|
213
215
|
...params,
|
|
214
216
|
limit,
|
|
@@ -227,15 +229,17 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
|
|
|
227
229
|
}
|
|
228
230
|
const rows = await stmt.json();
|
|
229
231
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
await countQ.json()
|
|
238
|
-
|
|
232
|
+
let total = 0;
|
|
233
|
+
if (getTotals) {
|
|
234
|
+
const countQ = await this.client.query({
|
|
235
|
+
query: `SELECT COUNT(*) FROM ${tableName} ${where}`,
|
|
236
|
+
format: 'JSONEachRow',
|
|
237
|
+
query_params: d,
|
|
238
|
+
});
|
|
239
|
+
const countResp = await countQ.json()
|
|
240
|
+
console.log('🪲 Clickhouse Query count', countResp);
|
|
241
|
+
total = countResp[0]['COUNT()'];
|
|
242
|
+
}
|
|
239
243
|
|
|
240
244
|
return {
|
|
241
245
|
data: rows.map((row) => {
|
|
@@ -261,17 +265,34 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
|
|
|
261
265
|
}))
|
|
262
266
|
return result;
|
|
263
267
|
}
|
|
264
|
-
|
|
265
268
|
async createRecordOriginalValues({ resource, record }: { resource: AdminForthResource, record: any }) {
|
|
266
269
|
const tableName = resource.table;
|
|
267
270
|
const columns = Object.keys(record);
|
|
271
|
+
|
|
272
|
+
// const q = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${columns.map((colName) => {
|
|
273
|
+
// const colType = resource.dataSourceColumns.find((col) => col.name == colName)._underlineType;
|
|
274
|
+
// if (colType.startsWith('Int') || colType.startsWith('UInt') || colType.startsWith('Float')) {
|
|
275
|
+
// return record[colName];
|
|
276
|
+
// } else {
|
|
277
|
+
// return "'"+record[colName]+"'"
|
|
278
|
+
// }
|
|
279
|
+
// }).join(', ') })`;
|
|
280
|
+
// console.log('🪲 Clickhouse Query', q) ;
|
|
281
|
+
// await this.client.command({
|
|
282
|
+
// query: q,
|
|
283
|
+
|
|
284
|
+
// });
|
|
285
|
+
|
|
286
|
+
console.log('🪲 Clickhouse Insert', record);
|
|
287
|
+
|
|
268
288
|
await this.client.insert({
|
|
289
|
+
database: this.dbName,
|
|
269
290
|
table: tableName,
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
291
|
+
columns: columns,
|
|
292
|
+
values: [Object.values(record)],
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
console.log('🪲 Clickhouse Query done');
|
|
275
296
|
|
|
276
297
|
}
|
|
277
298
|
|
|
@@ -286,8 +307,15 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
|
|
|
286
307
|
}
|
|
287
308
|
|
|
288
309
|
async deleteRecord({ resource, recordId }: { resource: AdminForthResource, recordId: any }) {
|
|
289
|
-
|
|
290
|
-
|
|
310
|
+
const pkColumn = resource.dataSourceColumns.find((col) => col.primaryKey);
|
|
311
|
+
await this.client.command(
|
|
312
|
+
{
|
|
313
|
+
query: `ALTER TABLE ${this.dbName}.${resource.table} DELETE WHERE ${
|
|
314
|
+
pkColumn.name
|
|
315
|
+
} = {recordId:${pkColumn._underlineType}}`,
|
|
316
|
+
query_params: { recordId },
|
|
317
|
+
}
|
|
318
|
+
);
|
|
291
319
|
}
|
|
292
320
|
|
|
293
321
|
close() {
|
package/dataConnectors/mongo.ts
CHANGED
|
@@ -83,28 +83,9 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
return value;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async getRecordByPrimaryKeyWithOriginalTypes(resource, key) {
|
|
89
|
-
const tableName = resource.table;
|
|
90
|
-
const collection = this.db.db().collection(tableName);
|
|
91
|
-
const row = await collection.findOne({ [this.getPrimaryKey(resource)]: key });
|
|
92
|
-
if (!row) {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
const newRow = {};
|
|
96
|
-
for (const [key_1, value] of Object.entries(row)) {
|
|
97
|
-
const dbKey = resource.dataSourceColumns.find((col) => col.name == key_1);
|
|
98
|
-
if (!dbKey) {
|
|
99
|
-
continue; // should I continue or throw an error?
|
|
100
|
-
throw new Error(`Resource '${resource.table}' has no column '${key_1}' defined`);
|
|
101
|
-
}
|
|
102
|
-
newRow[key_1] = value;
|
|
103
|
-
}
|
|
104
|
-
console.log('newRow', newRow);
|
|
105
|
-
return newRow;
|
|
106
86
|
}
|
|
107
87
|
|
|
88
|
+
|
|
108
89
|
setFieldValue(field, value) {
|
|
109
90
|
if (field.type == AdminForthDataTypes.DATETIME) {
|
|
110
91
|
if (!value) {
|
|
@@ -123,7 +104,7 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
|
|
|
123
104
|
return value;
|
|
124
105
|
}
|
|
125
106
|
|
|
126
|
-
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }) {
|
|
107
|
+
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters, getTotals }) {
|
|
127
108
|
// const columns = resource.dataSourceColumns.filter(c=> !c.virtual).map((col) => col.name).join(', ');
|
|
128
109
|
const tableName = resource.table;
|
|
129
110
|
|
|
@@ -132,7 +113,10 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
|
|
|
132
113
|
for (const filter of filters) {
|
|
133
114
|
query[filter.field] = this.OperatorsMap[filter.operator](filter.value);
|
|
134
115
|
}
|
|
135
|
-
|
|
116
|
+
let total = 0;
|
|
117
|
+
if (getTotals) {
|
|
118
|
+
total = await collection.countDocuments(query);
|
|
119
|
+
}
|
|
136
120
|
const result = await collection.find(query)
|
|
137
121
|
.sort(sort)
|
|
138
122
|
.skip(offset)
|
|
@@ -164,24 +164,9 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
return value;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
async getRecordByPrimaryKeyWithOriginalTypes(resource, key) {
|
|
171
|
-
const tableName = resource.table;
|
|
172
|
-
const columns = resource.dataSourceColumns.map((col) => `"${col.name}"`).join(', ');
|
|
173
|
-
const stmt = await this.db.query(`SELECT ${columns} FROM ${tableName} WHERE ${this.getPrimaryKey(resource)} = $1`, [key]);
|
|
174
|
-
const row = stmt.rows[0];
|
|
175
|
-
if (!row) {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
const newRow = {};
|
|
179
|
-
for (const [key_1, value] of Object.entries(row)) {
|
|
180
|
-
newRow[key_1] = value;
|
|
181
|
-
}
|
|
182
|
-
return newRow;
|
|
183
167
|
}
|
|
184
168
|
|
|
169
|
+
|
|
185
170
|
setFieldValue(field, value) {
|
|
186
171
|
if (field.type == AdminForthDataTypes.DATETIME) {
|
|
187
172
|
if (!value) {
|
|
@@ -198,7 +183,7 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
|
|
|
198
183
|
return value;
|
|
199
184
|
}
|
|
200
185
|
|
|
201
|
-
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }) {
|
|
186
|
+
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters, getTotals }) {
|
|
202
187
|
const columns = resource.dataSourceColumns.map((col) => `"${col.name}"`).join(', ');
|
|
203
188
|
const tableName = resource.table;
|
|
204
189
|
|
|
@@ -246,8 +231,10 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
|
|
|
246
231
|
}
|
|
247
232
|
const stmt = await this.db.query(selectQuery, d);
|
|
248
233
|
const rows = stmt.rows;
|
|
249
|
-
|
|
250
|
-
|
|
234
|
+
let total = 0;
|
|
235
|
+
if (getTotals) {
|
|
236
|
+
total = (await this.db.query(`SELECT COUNT(*) FROM ${tableName} ${where}`, filterValues)).rows[0].count;
|
|
237
|
+
}
|
|
251
238
|
return {
|
|
252
239
|
data: rows.map((row) => {
|
|
253
240
|
const newRow = {};
|
package/dataConnectors/sqlite.ts
CHANGED
|
@@ -91,21 +91,6 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
|
|
|
91
91
|
return value;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
async getRecordByPrimaryKeyWithOriginalTypes(resource: AdminForthResource, key: any): Promise<any> {
|
|
95
|
-
const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
|
|
96
|
-
const tableName = resource.table;
|
|
97
|
-
const stmt = this.db.prepare(`SELECT ${columns} FROM ${tableName} WHERE ${this.getPrimaryKey(resource)} = ?`);
|
|
98
|
-
const row = stmt.get(key);
|
|
99
|
-
if (!row) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
const newRow = {};
|
|
103
|
-
for (const [key, value] of Object.entries(row)) {
|
|
104
|
-
newRow[key] = value;
|
|
105
|
-
}
|
|
106
|
-
return newRow;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
94
|
setFieldValue(field: AdminForthResourceColumn, value: any): any {
|
|
110
95
|
if (field.type == AdminForthDataTypes.DATETIME) {
|
|
111
96
|
if (!value) {
|
|
@@ -151,13 +136,7 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
|
|
|
151
136
|
};
|
|
152
137
|
|
|
153
138
|
|
|
154
|
-
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }: {
|
|
155
|
-
resource: AdminForthResource,
|
|
156
|
-
limit: number,
|
|
157
|
-
offset: number,
|
|
158
|
-
sort: { field: string, direction: AdminForthSortDirections }[],
|
|
159
|
-
filters: { field: string, operator: AdminForthFilterOperators, value: any }[]
|
|
160
|
-
}): Promise<{ data: any[], total: number }> {
|
|
139
|
+
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters, getTotals }): Promise<{ data: any[], total: number }> {
|
|
161
140
|
const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
|
|
162
141
|
const tableName = resource.table;
|
|
163
142
|
|
|
@@ -203,9 +182,11 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
|
|
|
203
182
|
}
|
|
204
183
|
const rows = await stmt.all(d);
|
|
205
184
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
185
|
+
let total = 0;
|
|
186
|
+
if (getTotals) {
|
|
187
|
+
const totalStmt = this.db.prepare(`SELECT COUNT(*) FROM ${tableName} ${where}`);
|
|
188
|
+
total = totalStmt.get([...filterValues])['COUNT(*)'];
|
|
189
|
+
}
|
|
209
190
|
|
|
210
191
|
return {
|
|
211
192
|
data: rows.map((row) => {
|
|
@@ -17,9 +17,19 @@ export default class AdminForthBaseConnector {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
getRecordByPrimaryKeyWithOriginalTypes(resource, id) {
|
|
20
|
-
|
|
20
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
21
|
+
const data = yield this.getDataWithOriginalTypes({
|
|
22
|
+
resource,
|
|
23
|
+
limit: 1,
|
|
24
|
+
offset: 0,
|
|
25
|
+
sort: [],
|
|
26
|
+
filters: [{ field: this.getPrimaryKey(resource), operator: AdminForthFilterOperators.EQ, value: id }],
|
|
27
|
+
getTotals: false
|
|
28
|
+
});
|
|
29
|
+
return data.data.length > 0 ? data.data[0] : null;
|
|
30
|
+
});
|
|
21
31
|
}
|
|
22
|
-
getDataWithOriginalTypes({ resource, limit, offset, sort, filters }) {
|
|
32
|
+
getDataWithOriginalTypes({ resource, limit, offset, sort, filters, getTotals }) {
|
|
23
33
|
throw new Error('Method not implemented.');
|
|
24
34
|
}
|
|
25
35
|
discoverFields(resource) {
|
|
@@ -64,7 +74,7 @@ export default class AdminForthBaseConnector {
|
|
|
64
74
|
}
|
|
65
75
|
});
|
|
66
76
|
}
|
|
67
|
-
const d = yield this.getDataWithOriginalTypes({ resource, limit, offset, sort, filters });
|
|
77
|
+
const d = yield this.getDataWithOriginalTypes({ resource, limit, offset, sort, filters, getTotals: true });
|
|
68
78
|
// call getFieldValue for each field
|
|
69
79
|
d.data.map((record) => {
|
|
70
80
|
for (const col of resource.dataSourceColumns) {
|
|
@@ -38,6 +38,17 @@ class ClickhouseConnector extends AdminForthBaseConnector {
|
|
|
38
38
|
// create connection here
|
|
39
39
|
this.client = createClient({
|
|
40
40
|
url: url.replace('clickhouse://', 'http://'),
|
|
41
|
+
clickhouse_settings: {
|
|
42
|
+
// Allows to insert serialized JS Dates (such as '2023-12-06T10:54:48.000Z')
|
|
43
|
+
date_time_input_format: 'best_effort',
|
|
44
|
+
// Recommended for cluster usage to avoid situations where a query processing error occurred after the response code,
|
|
45
|
+
// and HTTP headers were already sent to the client.
|
|
46
|
+
// See https://clickhouse.com/docs/en/interfaces/http/#response-buffering
|
|
47
|
+
wait_end_of_query: 1,
|
|
48
|
+
},
|
|
49
|
+
// log:{
|
|
50
|
+
// level: ClickHouseLogLevel.TRACE,
|
|
51
|
+
// }
|
|
41
52
|
});
|
|
42
53
|
}
|
|
43
54
|
discoverFields(resource) {
|
|
@@ -108,7 +119,8 @@ class ClickhouseConnector extends AdminForthBaseConnector {
|
|
|
108
119
|
else if (field._underlineType.startsWith('DateTime')
|
|
109
120
|
|| field._underlineType.startsWith('String')
|
|
110
121
|
|| field._underlineType.startsWith('FixedString')) {
|
|
111
|
-
|
|
122
|
+
const v = dayjs(value).toISOString();
|
|
123
|
+
return v;
|
|
112
124
|
}
|
|
113
125
|
else {
|
|
114
126
|
throw new Error(`AdminForth does not support row type: ${field._underlineType} for timestamps, use VARCHAR (with iso strings) or TIMESTAMP/INT (with unix timestamps). Issue in field "${field.name}"`);
|
|
@@ -133,22 +145,6 @@ class ClickhouseConnector extends AdminForthBaseConnector {
|
|
|
133
145
|
}
|
|
134
146
|
return value;
|
|
135
147
|
}
|
|
136
|
-
getRecordByPrimaryKeyWithOriginalTypes(resource, key) {
|
|
137
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
138
|
-
const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
|
|
139
|
-
const tableName = resource.table;
|
|
140
|
-
const stmt = yield this.client.query({
|
|
141
|
-
query: `SELECT ${columns} FROM ${tableName} WHERE ${this.getPrimaryKey(resource)} = ?`,
|
|
142
|
-
format: 'JSONEachRow',
|
|
143
|
-
query_params: [key],
|
|
144
|
-
});
|
|
145
|
-
const row = yield stmt.json();
|
|
146
|
-
if (!row.length) {
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
return row[0];
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
148
|
setFieldValue(field, value) {
|
|
153
149
|
if (field.type == AdminForthDataTypes.DATETIME) {
|
|
154
150
|
if (!value) {
|
|
@@ -162,7 +158,8 @@ class ClickhouseConnector extends AdminForthBaseConnector {
|
|
|
162
158
|
|| field._underlineType.startsWith('String')
|
|
163
159
|
|| field._underlineType.startsWith('FixedString')) {
|
|
164
160
|
// value is iso string now, convert to unix timestamp
|
|
165
|
-
|
|
161
|
+
const iso = dayjs(value).toISOString();
|
|
162
|
+
return iso;
|
|
166
163
|
}
|
|
167
164
|
}
|
|
168
165
|
else if (field.type == AdminForthDataTypes.BOOLEAN) {
|
|
@@ -180,11 +177,12 @@ class ClickhouseConnector extends AdminForthBaseConnector {
|
|
|
180
177
|
return value;
|
|
181
178
|
}
|
|
182
179
|
getDataWithOriginalTypes(_a) {
|
|
183
|
-
return __awaiter(this, arguments, void 0, function* ({ resource, limit, offset, sort, filters }) {
|
|
180
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, limit, offset, sort, filters, getTotals }) {
|
|
184
181
|
const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
|
|
185
182
|
const tableName = resource.table;
|
|
186
183
|
const where = filters.length ? `WHERE ${filters.map((f, i) => {
|
|
187
|
-
|
|
184
|
+
const column = resource.dataSourceColumns.find((col) => col.name == f.field);
|
|
185
|
+
let placeholder = `{f${i}:${column._underlineType}}`;
|
|
188
186
|
let field = f.field;
|
|
189
187
|
let operator = this.OperatorsMap[f.operator];
|
|
190
188
|
if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
|
|
@@ -222,12 +220,17 @@ class ClickhouseConnector extends AdminForthBaseConnector {
|
|
|
222
220
|
console.log('🪲 Clickhouse Query', q, 'params:', d);
|
|
223
221
|
}
|
|
224
222
|
const rows = yield stmt.json();
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
223
|
+
let total = 0;
|
|
224
|
+
if (getTotals) {
|
|
225
|
+
const countQ = yield this.client.query({
|
|
226
|
+
query: `SELECT COUNT(*) FROM ${tableName} ${where}`,
|
|
227
|
+
format: 'JSONEachRow',
|
|
228
|
+
query_params: d,
|
|
229
|
+
});
|
|
230
|
+
const countResp = yield countQ.json();
|
|
231
|
+
console.log('🪲 Clickhouse Query count', countResp);
|
|
232
|
+
total = countResp[0]['COUNT()'];
|
|
233
|
+
}
|
|
231
234
|
return {
|
|
232
235
|
data: rows.map((row) => {
|
|
233
236
|
const newRow = {};
|
|
@@ -258,13 +261,26 @@ class ClickhouseConnector extends AdminForthBaseConnector {
|
|
|
258
261
|
return __awaiter(this, arguments, void 0, function* ({ resource, record }) {
|
|
259
262
|
const tableName = resource.table;
|
|
260
263
|
const columns = Object.keys(record);
|
|
264
|
+
// const q = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${columns.map((colName) => {
|
|
265
|
+
// const colType = resource.dataSourceColumns.find((col) => col.name == colName)._underlineType;
|
|
266
|
+
// if (colType.startsWith('Int') || colType.startsWith('UInt') || colType.startsWith('Float')) {
|
|
267
|
+
// return record[colName];
|
|
268
|
+
// } else {
|
|
269
|
+
// return "'"+record[colName]+"'"
|
|
270
|
+
// }
|
|
271
|
+
// }).join(', ') })`;
|
|
272
|
+
// console.log('🪲 Clickhouse Query', q) ;
|
|
273
|
+
// await this.client.command({
|
|
274
|
+
// query: q,
|
|
275
|
+
// });
|
|
276
|
+
console.log('🪲 Clickhouse Insert', record);
|
|
261
277
|
yield this.client.insert({
|
|
278
|
+
database: this.dbName,
|
|
262
279
|
table: tableName,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}),
|
|
266
|
-
format: 'JSONEachRow',
|
|
280
|
+
columns: columns,
|
|
281
|
+
values: [Object.values(record)],
|
|
267
282
|
});
|
|
283
|
+
console.log('🪲 Clickhouse Query done');
|
|
268
284
|
});
|
|
269
285
|
}
|
|
270
286
|
updateRecord(_a) {
|
|
@@ -279,8 +295,11 @@ class ClickhouseConnector extends AdminForthBaseConnector {
|
|
|
279
295
|
}
|
|
280
296
|
deleteRecord(_a) {
|
|
281
297
|
return __awaiter(this, arguments, void 0, function* ({ resource, recordId }) {
|
|
282
|
-
|
|
283
|
-
|
|
298
|
+
const pkColumn = resource.dataSourceColumns.find((col) => col.primaryKey);
|
|
299
|
+
yield this.client.command({
|
|
300
|
+
query: `ALTER TABLE ${this.dbName}.${resource.table} DELETE WHERE ${pkColumn.name} = {recordId:${pkColumn._underlineType}}`,
|
|
301
|
+
query_params: { recordId },
|
|
302
|
+
});
|
|
284
303
|
});
|
|
285
304
|
}
|
|
286
305
|
close() {
|
|
@@ -86,27 +86,6 @@ class MongoConnector extends AdminForthBaseConnector {
|
|
|
86
86
|
}
|
|
87
87
|
return value;
|
|
88
88
|
}
|
|
89
|
-
getRecordByPrimaryKeyWithOriginalTypes(resource, key) {
|
|
90
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
91
|
-
const tableName = resource.table;
|
|
92
|
-
const collection = this.db.db().collection(tableName);
|
|
93
|
-
const row = yield collection.findOne({ [this.getPrimaryKey(resource)]: key });
|
|
94
|
-
if (!row) {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
const newRow = {};
|
|
98
|
-
for (const [key_1, value] of Object.entries(row)) {
|
|
99
|
-
const dbKey = resource.dataSourceColumns.find((col) => col.name == key_1);
|
|
100
|
-
if (!dbKey) {
|
|
101
|
-
continue; // should I continue or throw an error?
|
|
102
|
-
throw new Error(`Resource '${resource.table}' has no column '${key_1}' defined`);
|
|
103
|
-
}
|
|
104
|
-
newRow[key_1] = value;
|
|
105
|
-
}
|
|
106
|
-
console.log('newRow', newRow);
|
|
107
|
-
return newRow;
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
89
|
setFieldValue(field, value) {
|
|
111
90
|
if (field.type == AdminForthDataTypes.DATETIME) {
|
|
112
91
|
if (!value) {
|
|
@@ -127,7 +106,7 @@ class MongoConnector extends AdminForthBaseConnector {
|
|
|
127
106
|
return value;
|
|
128
107
|
}
|
|
129
108
|
getDataWithOriginalTypes(_a) {
|
|
130
|
-
return __awaiter(this, arguments, void 0, function* ({ resource, limit, offset, sort, filters }) {
|
|
109
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, limit, offset, sort, filters, getTotals }) {
|
|
131
110
|
// const columns = resource.dataSourceColumns.filter(c=> !c.virtual).map((col) => col.name).join(', ');
|
|
132
111
|
const tableName = resource.table;
|
|
133
112
|
const collection = this.db.db().collection(tableName);
|
|
@@ -135,7 +114,10 @@ class MongoConnector extends AdminForthBaseConnector {
|
|
|
135
114
|
for (const filter of filters) {
|
|
136
115
|
query[filter.field] = this.OperatorsMap[filter.operator](filter.value);
|
|
137
116
|
}
|
|
138
|
-
|
|
117
|
+
let total = 0;
|
|
118
|
+
if (getTotals) {
|
|
119
|
+
total = yield collection.countDocuments(query);
|
|
120
|
+
}
|
|
139
121
|
const result = yield collection.find(query)
|
|
140
122
|
.sort(sort)
|
|
141
123
|
.skip(offset)
|
|
@@ -167,22 +167,6 @@ class PostgresConnector extends AdminForthBaseConnector {
|
|
|
167
167
|
}
|
|
168
168
|
return value;
|
|
169
169
|
}
|
|
170
|
-
getRecordByPrimaryKeyWithOriginalTypes(resource, key) {
|
|
171
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
172
|
-
const tableName = resource.table;
|
|
173
|
-
const columns = resource.dataSourceColumns.map((col) => `"${col.name}"`).join(', ');
|
|
174
|
-
const stmt = yield this.db.query(`SELECT ${columns} FROM ${tableName} WHERE ${this.getPrimaryKey(resource)} = $1`, [key]);
|
|
175
|
-
const row = stmt.rows[0];
|
|
176
|
-
if (!row) {
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
const newRow = {};
|
|
180
|
-
for (const [key_1, value] of Object.entries(row)) {
|
|
181
|
-
newRow[key_1] = value;
|
|
182
|
-
}
|
|
183
|
-
return newRow;
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
170
|
setFieldValue(field, value) {
|
|
187
171
|
if (field.type == AdminForthDataTypes.DATETIME) {
|
|
188
172
|
if (!value) {
|
|
@@ -201,7 +185,7 @@ class PostgresConnector extends AdminForthBaseConnector {
|
|
|
201
185
|
return value;
|
|
202
186
|
}
|
|
203
187
|
getDataWithOriginalTypes(_a) {
|
|
204
|
-
return __awaiter(this, arguments, void 0, function* ({ resource, limit, offset, sort, filters }) {
|
|
188
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, limit, offset, sort, filters, getTotals }) {
|
|
205
189
|
const columns = resource.dataSourceColumns.map((col) => `"${col.name}"`).join(', ');
|
|
206
190
|
const tableName = resource.table;
|
|
207
191
|
let totalCounter = 1;
|
|
@@ -248,7 +232,10 @@ class PostgresConnector extends AdminForthBaseConnector {
|
|
|
248
232
|
}
|
|
249
233
|
const stmt = yield this.db.query(selectQuery, d);
|
|
250
234
|
const rows = stmt.rows;
|
|
251
|
-
|
|
235
|
+
let total = 0;
|
|
236
|
+
if (getTotals) {
|
|
237
|
+
total = (yield this.db.query(`SELECT COUNT(*) FROM ${tableName} ${where}`, filterValues)).rows[0].count;
|
|
238
|
+
}
|
|
252
239
|
return {
|
|
253
240
|
data: rows.map((row) => {
|
|
254
241
|
const newRow = {};
|
|
@@ -121,22 +121,6 @@ class SQLiteConnector extends AdminForthBaseConnector {
|
|
|
121
121
|
}
|
|
122
122
|
return value;
|
|
123
123
|
}
|
|
124
|
-
getRecordByPrimaryKeyWithOriginalTypes(resource, key) {
|
|
125
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
126
|
-
const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
|
|
127
|
-
const tableName = resource.table;
|
|
128
|
-
const stmt = this.db.prepare(`SELECT ${columns} FROM ${tableName} WHERE ${this.getPrimaryKey(resource)} = ?`);
|
|
129
|
-
const row = stmt.get(key);
|
|
130
|
-
if (!row) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
const newRow = {};
|
|
134
|
-
for (const [key, value] of Object.entries(row)) {
|
|
135
|
-
newRow[key] = value;
|
|
136
|
-
}
|
|
137
|
-
return newRow;
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
124
|
setFieldValue(field, value) {
|
|
141
125
|
if (field.type == AdminForthDataTypes.DATETIME) {
|
|
142
126
|
if (!value) {
|
|
@@ -166,7 +150,7 @@ class SQLiteConnector extends AdminForthBaseConnector {
|
|
|
166
150
|
return value;
|
|
167
151
|
}
|
|
168
152
|
getDataWithOriginalTypes(_a) {
|
|
169
|
-
return __awaiter(this, arguments, void 0, function* ({ resource, limit, offset, sort, filters }) {
|
|
153
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, limit, offset, sort, filters, getTotals }) {
|
|
170
154
|
const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
|
|
171
155
|
const tableName = resource.table;
|
|
172
156
|
const where = filters.length ? `WHERE ${filters.map((f, i) => {
|
|
@@ -205,7 +189,11 @@ class SQLiteConnector extends AdminForthBaseConnector {
|
|
|
205
189
|
console.log('🪲 SQLITE Query', q, 'params:', d);
|
|
206
190
|
}
|
|
207
191
|
const rows = yield stmt.all(d);
|
|
208
|
-
|
|
192
|
+
let total = 0;
|
|
193
|
+
if (getTotals) {
|
|
194
|
+
const totalStmt = this.db.prepare(`SELECT COUNT(*) FROM ${tableName} ${where}`);
|
|
195
|
+
total = totalStmt.get([...filterValues])['COUNT(*)'];
|
|
196
|
+
}
|
|
209
197
|
return {
|
|
210
198
|
data: rows.map((row) => {
|
|
211
199
|
const newRow = {};
|
|
@@ -232,8 +232,12 @@ const columnError = (column) => {
|
|
|
232
232
|
|
|
233
233
|
|
|
234
234
|
const setCurrentValue = (key, value) => {
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
const col = props.resource.columns.find((column) => column.name === key);
|
|
236
|
+
if (['integer', 'float'].includes(col.type) && (value || value === 0)) {
|
|
237
|
+
currentValues.value[key] = +value;
|
|
238
|
+
} else {
|
|
239
|
+
currentValues.value[key] = value;
|
|
240
|
+
}
|
|
237
241
|
emit('update:record', currentValues.value);
|
|
238
242
|
};
|
|
239
243
|
|
|
@@ -242,8 +246,7 @@ onMounted(() => {
|
|
|
242
246
|
currentValues.value[key] = props.record[key];
|
|
243
247
|
});
|
|
244
248
|
initFlowbite();
|
|
245
|
-
|
|
246
|
-
|
|
249
|
+
emit('update:isValid', isValid.value);
|
|
247
250
|
});
|
|
248
251
|
|
|
249
252
|
const columnOptions = computedAsync(async () => {
|
|
@@ -267,7 +270,6 @@ const columnOptions = computedAsync(async () => {
|
|
|
267
270
|
|
|
268
271
|
}, {});
|
|
269
272
|
|
|
270
|
-
const coreStore = useCoreStore();
|
|
271
273
|
|
|
272
274
|
const editableColumns = computed(() => {
|
|
273
275
|
const mode = props.record ? 'edit' : 'create';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adminforth",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.29",
|
|
4
4
|
"description": "OpenSource Vue3 powered forth-generation admin panel",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"license": "ISC",
|
|
16
16
|
"type": "module",
|
|
17
17
|
"dependencies": {
|
|
18
|
+
"@clickhouse/client": "^1.4.0",
|
|
18
19
|
"better-sqlite3": "^10.0.0",
|
|
19
20
|
"crypto": "^1.0.1",
|
|
20
21
|
"dayjs": "^1.11.11",
|
|
@@ -25,8 +26,7 @@
|
|
|
25
26
|
"mongodb": "6.6",
|
|
26
27
|
"node-fetch": "^3.3.2",
|
|
27
28
|
"pg": "^8.11.5",
|
|
28
|
-
"uuid": "^9.0.1"
|
|
29
|
-
"@clickhouse/client": "^1.4.0"
|
|
29
|
+
"uuid": "^9.0.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^20.14.2",
|
|
@@ -232,8 +232,12 @@ const columnError = (column) => {
|
|
|
232
232
|
|
|
233
233
|
|
|
234
234
|
const setCurrentValue = (key, value) => {
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
const col = props.resource.columns.find((column) => column.name === key);
|
|
236
|
+
if (['integer', 'float'].includes(col.type) && (value || value === 0)) {
|
|
237
|
+
currentValues.value[key] = +value;
|
|
238
|
+
} else {
|
|
239
|
+
currentValues.value[key] = value;
|
|
240
|
+
}
|
|
237
241
|
emit('update:record', currentValues.value);
|
|
238
242
|
};
|
|
239
243
|
|
|
@@ -242,8 +246,7 @@ onMounted(() => {
|
|
|
242
246
|
currentValues.value[key] = props.record[key];
|
|
243
247
|
});
|
|
244
248
|
initFlowbite();
|
|
245
|
-
|
|
246
|
-
|
|
249
|
+
emit('update:isValid', isValid.value);
|
|
247
250
|
});
|
|
248
251
|
|
|
249
252
|
const columnOptions = computedAsync(async () => {
|
|
@@ -267,7 +270,6 @@ const columnOptions = computedAsync(async () => {
|
|
|
267
270
|
|
|
268
271
|
}, {});
|
|
269
272
|
|
|
270
|
-
const coreStore = useCoreStore();
|
|
271
273
|
|
|
272
274
|
const editableColumns = computed(() => {
|
|
273
275
|
const mode = props.record ? 'edit' : 'create';
|
|
@@ -93,7 +93,10 @@ export interface IExpressHttpServer extends IHttpServer {
|
|
|
93
93
|
export interface IAdminForthDataSourceConnector {
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
|
-
*
|
|
96
|
+
* Optional.
|
|
97
|
+
* You an redefine this function to define how one record should be fetched from database.
|
|
98
|
+
* You you will not redefine it, AdminForth will use {@link IAdminForthDataSourceConnector.getData} with limit 1 and offset 0 and
|
|
99
|
+
* filter by primary key.
|
|
97
100
|
*/
|
|
98
101
|
getRecordByPrimaryKeyWithOriginalTypes(resource: AdminForthResource, recordId: string): Promise<any>;
|
|
99
102
|
|
|
@@ -142,12 +145,13 @@ export interface IAdminForthDataSourceConnector {
|
|
|
142
145
|
*
|
|
143
146
|
* Fields are returned from db "as is" then {@link AdminForthBaseConnector.getData} will transform each field using {@link IAdminForthDataSourceConnector.getFieldValue}
|
|
144
147
|
*/
|
|
145
|
-
getDataWithOriginalTypes({ resource, limit, offset, sort, filters }: {
|
|
148
|
+
getDataWithOriginalTypes({ resource, limit, offset, sort, filters, getTotals }: {
|
|
146
149
|
resource: AdminForthResource,
|
|
147
150
|
limit: number,
|
|
148
151
|
offset: number,
|
|
149
152
|
sort: { field: string, direction: AdminForthSortDirections }[],
|
|
150
|
-
filters: { field: string, operator: AdminForthFilterOperators, value: any }[]
|
|
153
|
+
filters: { field: string, operator: AdminForthFilterOperators, value: any }[],
|
|
154
|
+
getTotals?: boolean
|
|
151
155
|
}): Promise<{data: Array<any>, total: number}>;
|
|
152
156
|
|
|
153
157
|
|