adminforth 1.2.25 → 1.2.27
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/clickhouse.ts +298 -0
- package/dist/dataConnectors/clickhouse.js +290 -0
- package/dist/index.js +2 -0
- package/dist/modules/restApi.js +2 -1
- package/index.ts +2 -0
- package/modules/restApi.ts +2 -1
- package/package.json +3 -2
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import betterSqlite3 from 'better-sqlite3';
|
|
2
|
+
import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirections, IAdminForthDataSourceConnector, AdminForthResource, AdminForthResourceColumn } from '../types/AdminForthConfig.js';
|
|
3
|
+
import AdminForthBaseConnector from './baseConnector.js';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import { createClient } from '@clickhouse/client' // or '@clickhouse/client-web'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForthDataSourceConnector {
|
|
10
|
+
|
|
11
|
+
client: any;
|
|
12
|
+
dbName: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* url: http[s]://[username:password@]hostname:port[/database][?param1=value1¶m2=value2]
|
|
16
|
+
* @param param0
|
|
17
|
+
*/
|
|
18
|
+
constructor({ url }: { url: string }) {
|
|
19
|
+
super();
|
|
20
|
+
this.dbName = new URL(url).pathname.replace('/', '');
|
|
21
|
+
// create connection here
|
|
22
|
+
this.client = createClient({
|
|
23
|
+
url: url.replace('clickhouse://', 'http://'),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async discoverFields(resource: AdminForthResource): Promise<{[key: string]: AdminForthResourceColumn}> {
|
|
28
|
+
const tableName = resource.table;
|
|
29
|
+
const q = await this.client.query({
|
|
30
|
+
query: `SELECT * FROM system.columns WHERE table = '${tableName}' and database = '${this.dbName}'`,
|
|
31
|
+
format: 'JSONEachRow',
|
|
32
|
+
});
|
|
33
|
+
const rows = await q.json();
|
|
34
|
+
|
|
35
|
+
const fieldTypes = {};
|
|
36
|
+
rows.forEach((row) => {
|
|
37
|
+
const field: any = {};
|
|
38
|
+
const baseType = row.type;
|
|
39
|
+
if (baseType.startsWith('Int') || baseType.startsWith('UInt')) {
|
|
40
|
+
field.type = AdminForthDataTypes.INTEGER;
|
|
41
|
+
} else if (baseType === 'FixedString' || baseType === 'String') {
|
|
42
|
+
field.type = AdminForthDataTypes.STRING;
|
|
43
|
+
// TODO
|
|
44
|
+
// const length = baseType.match(/\d+/g);
|
|
45
|
+
// field.maxLength = length ? parseInt(length[0]) : null;
|
|
46
|
+
} else if (baseType == 'UUID') {
|
|
47
|
+
field.type = AdminForthDataTypes.STRING;
|
|
48
|
+
} else if (baseType.startsWith('Decimal')) {
|
|
49
|
+
field.type = AdminForthDataTypes.DECIMAL;
|
|
50
|
+
// const [precision, scale] = baseType.match(/\d+/g);
|
|
51
|
+
// TODO
|
|
52
|
+
// field.precision = parseInt(precision);
|
|
53
|
+
// field.scale = parseInt(scale);
|
|
54
|
+
} else if (baseType.startsWith('Float')) {
|
|
55
|
+
field.type = AdminForthDataTypes.FLOAT;
|
|
56
|
+
} else if (baseType == 'DateTime64' || baseType == 'DateTime') {
|
|
57
|
+
field.type = AdminForthDataTypes.DATETIME;
|
|
58
|
+
} else if (baseType == 'Date' || baseType == 'Date64') {
|
|
59
|
+
field.type = AdminForthDataTypes.DATE;
|
|
60
|
+
} else if (baseType == 'Boolean') {
|
|
61
|
+
field.type = AdminForthDataTypes.BOOLEAN;
|
|
62
|
+
field._underlineType = 'boolean';
|
|
63
|
+
} else {
|
|
64
|
+
field.type = 'unknown'
|
|
65
|
+
}
|
|
66
|
+
field._underlineType = baseType;
|
|
67
|
+
field._baseTypeDebug = baseType;
|
|
68
|
+
field.required = row.notnull == 1;
|
|
69
|
+
field.primaryKey = row.pk == 1;
|
|
70
|
+
field.default = row.dflt_value;
|
|
71
|
+
fieldTypes[row.name] = field
|
|
72
|
+
});
|
|
73
|
+
return fieldTypes;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getFieldValue(field: AdminForthResourceColumn, value: any): any {
|
|
77
|
+
if (field.type == AdminForthDataTypes.DATETIME) {
|
|
78
|
+
if (!value) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (field._underlineType.startsWith('Int') || field._underlineType.startsWith('UInt')) {
|
|
82
|
+
return dayjs.unix(+value).toISOString();
|
|
83
|
+
} else if (field._underlineType.startsWith('DateTime')
|
|
84
|
+
|| field._underlineType.startsWith('String')
|
|
85
|
+
|| field._underlineType.startsWith('FixedString')) {
|
|
86
|
+
return dayjs(value).toISOString();
|
|
87
|
+
} else {
|
|
88
|
+
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
|
+
}
|
|
90
|
+
} else if (field.type == AdminForthDataTypes.DATE) {
|
|
91
|
+
if (!value) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return dayjs(value).toISOString().split('T')[0];
|
|
95
|
+
} else if (field.type == AdminForthDataTypes.BOOLEAN) {
|
|
96
|
+
return !!value;
|
|
97
|
+
} else if (field.type == AdminForthDataTypes.JSON) {
|
|
98
|
+
if (field._underlineType.startsWith('String') || field._underlineType.startsWith('FixedString')) {
|
|
99
|
+
return JSON.parse(value);
|
|
100
|
+
} else {
|
|
101
|
+
console.error(`AdminForth: JSON field is not a string but ${field._underlineType}, this is not supported yet`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
|
|
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
|
+
setFieldValue(field: AdminForthResourceColumn, value: any): any {
|
|
123
|
+
if (field.type == AdminForthDataTypes.DATETIME) {
|
|
124
|
+
if (!value) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
if (field._underlineType.startsWith('Int') || field._underlineType.startsWith('UInt')) {
|
|
128
|
+
// value is iso string now, convert to unix timestamp
|
|
129
|
+
return dayjs(value).unix();
|
|
130
|
+
} else if (field._underlineType.startsWith('DateTime')
|
|
131
|
+
|| field._underlineType.startsWith('String')
|
|
132
|
+
|| field._underlineType.startsWith('FixedString')) {
|
|
133
|
+
// value is iso string now, convert to unix timestamp
|
|
134
|
+
return dayjs(value).toISOString();
|
|
135
|
+
}
|
|
136
|
+
} else if (field.type == AdminForthDataTypes.BOOLEAN) {
|
|
137
|
+
return value ? 1 : 0;
|
|
138
|
+
} else if (field.type == AdminForthDataTypes.JSON) {
|
|
139
|
+
// check underline type is text or string
|
|
140
|
+
if (field._underlineType.startsWith('String') || field._underlineType.startsWith('FixedString')) {
|
|
141
|
+
return JSON.stringify(value);
|
|
142
|
+
} else {
|
|
143
|
+
console.error(`AdminForth: JSON field is not a string/text but ${field._underlineType}, this is not supported yet`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
OperatorsMap = {
|
|
151
|
+
[AdminForthFilterOperators.EQ]: '=',
|
|
152
|
+
[AdminForthFilterOperators.NE]: '!=',
|
|
153
|
+
[AdminForthFilterOperators.GT]: '>',
|
|
154
|
+
[AdminForthFilterOperators.LT]: '<',
|
|
155
|
+
[AdminForthFilterOperators.GTE]: '>=',
|
|
156
|
+
[AdminForthFilterOperators.LTE]: '<=',
|
|
157
|
+
[AdminForthFilterOperators.LIKE]: 'LIKE',
|
|
158
|
+
[AdminForthFilterOperators.ILIKE]: 'ILIKE',
|
|
159
|
+
[AdminForthFilterOperators.IN]: 'IN',
|
|
160
|
+
[AdminForthFilterOperators.NIN]: 'NOT IN',
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
SortDirectionsMap = {
|
|
164
|
+
[AdminForthSortDirections.asc]: 'ASC',
|
|
165
|
+
[AdminForthSortDirections.desc]: 'DESC',
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }: {
|
|
170
|
+
resource: AdminForthResource,
|
|
171
|
+
limit: number,
|
|
172
|
+
offset: number,
|
|
173
|
+
sort: { field: string, direction: AdminForthSortDirections }[],
|
|
174
|
+
filters: { field: string, operator: AdminForthFilterOperators, value: any }[]
|
|
175
|
+
}): Promise<{ data: any[], total: number }> {
|
|
176
|
+
const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
|
|
177
|
+
const tableName = resource.table;
|
|
178
|
+
|
|
179
|
+
const where = filters.length ? `WHERE ${filters.map((f, i) => {
|
|
180
|
+
let placeholder = `{f${i}}`;
|
|
181
|
+
let field = f.field;
|
|
182
|
+
let operator = this.OperatorsMap[f.operator];
|
|
183
|
+
if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
|
|
184
|
+
placeholder = `(${f.value.map((_, j) => `p${i}_${j}`).join(', ')})`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return `${field} ${operator} ${placeholder}`
|
|
188
|
+
}).join(' AND ')}` : '';
|
|
189
|
+
|
|
190
|
+
const params = {};
|
|
191
|
+
|
|
192
|
+
filters.length ? filters.forEach((f, i) => {
|
|
193
|
+
// for arrays do set in map
|
|
194
|
+
const v = f.value;
|
|
195
|
+
|
|
196
|
+
if (f.operator == AdminForthFilterOperators.LIKE || f.operator == AdminForthFilterOperators.ILIKE) {
|
|
197
|
+
params[`f${i}`] = `%${v}%`;
|
|
198
|
+
} else if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
|
|
199
|
+
v.forEach((_, j) => {
|
|
200
|
+
params[`p${i}_${j}`] = v[j];
|
|
201
|
+
});
|
|
202
|
+
} else {
|
|
203
|
+
params[`f${i}`] = v;
|
|
204
|
+
}
|
|
205
|
+
}) : [];
|
|
206
|
+
|
|
207
|
+
const orderBy = sort.length ? `ORDER BY ${sort.map((s) => `${s.field} ${this.SortDirectionsMap[s.direction]}`).join(', ')}` : '';
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
const q = `SELECT ${columns} FROM ${tableName} ${where} ${orderBy} LIMIT {limit:Int} OFFSET {offset:Int}`;
|
|
211
|
+
|
|
212
|
+
const d = {
|
|
213
|
+
...params,
|
|
214
|
+
limit,
|
|
215
|
+
offset,
|
|
216
|
+
};
|
|
217
|
+
console.log('🪲 Clickhouse Query', q, 'params:', d);
|
|
218
|
+
|
|
219
|
+
const stmt = await this.client.query({
|
|
220
|
+
query: q,
|
|
221
|
+
format: 'JSONEachRow',
|
|
222
|
+
query_params: d,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (process.env.HEAVY_DEBUG) {
|
|
226
|
+
console.log('🪲 Clickhouse Query', q, 'params:', d);
|
|
227
|
+
}
|
|
228
|
+
const rows = await stmt.json();
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
const countQ = await this.client.query({
|
|
232
|
+
query: `SELECT COUNT(*) FROM ${tableName} ${where}`,
|
|
233
|
+
format: 'JSONEachRow',
|
|
234
|
+
query_params: d,
|
|
235
|
+
});
|
|
236
|
+
const total = (
|
|
237
|
+
await countQ.json()
|
|
238
|
+
)['COUNT(*)'];
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
data: rows.map((row) => {
|
|
242
|
+
const newRow = {};
|
|
243
|
+
for (const [key, value] of Object.entries(row)) {
|
|
244
|
+
newRow[key] = value;
|
|
245
|
+
}
|
|
246
|
+
return newRow;
|
|
247
|
+
}),
|
|
248
|
+
total,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async getMinMaxForColumnsWithOriginalTypes({ resource, columns }: { resource: AdminForthResource, columns: AdminForthResourceColumn[] }): Promise<{ [key: string]: { min: any, max: any } }> {
|
|
253
|
+
const tableName = resource.table;
|
|
254
|
+
const result = {};
|
|
255
|
+
await Promise.all(columns.map(async (col) => {
|
|
256
|
+
// const stmt = await this.db.prepare(`SELECT MIN(${col.name}) as min, MAX(${col.name}) as max FROM ${tableName}`);
|
|
257
|
+
// const { min, max } = stmt.get();
|
|
258
|
+
// result[col.name] = {
|
|
259
|
+
// min, max,
|
|
260
|
+
// };
|
|
261
|
+
}))
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async createRecordOriginalValues({ resource, record }: { resource: AdminForthResource, record: any }) {
|
|
266
|
+
const tableName = resource.table;
|
|
267
|
+
const columns = Object.keys(record);
|
|
268
|
+
await this.client.insert({
|
|
269
|
+
table: tableName,
|
|
270
|
+
values: columns.map((colName) => {
|
|
271
|
+
return {[colName]: record[colName]};
|
|
272
|
+
}),
|
|
273
|
+
format: 'JSONEachRow',
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async updateRecord({ resource, recordId, newValues }: { resource: AdminForthResource, recordId: any, newValues: any }) {
|
|
279
|
+
const columnsWithPlaceholders = Object.keys(newValues).map((col) => `${col} = ?`);
|
|
280
|
+
const values = [...Object.values(newValues), recordId];
|
|
281
|
+
|
|
282
|
+
// const q = this.db.prepare(
|
|
283
|
+
// `UPDATE ${resource.table} SET ${columnsWithPlaceholders} WHERE ${this.getPrimaryKey(resource)} = ?`
|
|
284
|
+
// )
|
|
285
|
+
// await q.run(values);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async deleteRecord({ resource, recordId }: { resource: AdminForthResource, recordId: any }) {
|
|
289
|
+
// const q = this.db.prepare(`DELETE FROM ${resource.table} WHERE ${this.getPrimaryKey(resource)} = ?`);
|
|
290
|
+
// await q.run(recordId);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
close() {
|
|
294
|
+
this.client.disconnect();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export default ClickhouseConnector;
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirections } from '../types/AdminForthConfig.js';
|
|
11
|
+
import AdminForthBaseConnector from './baseConnector.js';
|
|
12
|
+
import dayjs from 'dayjs';
|
|
13
|
+
import { createClient } from '@clickhouse/client'; // or '@clickhouse/client-web'
|
|
14
|
+
class ClickhouseConnector extends AdminForthBaseConnector {
|
|
15
|
+
/**
|
|
16
|
+
* url: http[s]://[username:password@]hostname:port[/database][?param1=value1¶m2=value2]
|
|
17
|
+
* @param param0
|
|
18
|
+
*/
|
|
19
|
+
constructor({ url }) {
|
|
20
|
+
super();
|
|
21
|
+
this.OperatorsMap = {
|
|
22
|
+
[AdminForthFilterOperators.EQ]: '=',
|
|
23
|
+
[AdminForthFilterOperators.NE]: '!=',
|
|
24
|
+
[AdminForthFilterOperators.GT]: '>',
|
|
25
|
+
[AdminForthFilterOperators.LT]: '<',
|
|
26
|
+
[AdminForthFilterOperators.GTE]: '>=',
|
|
27
|
+
[AdminForthFilterOperators.LTE]: '<=',
|
|
28
|
+
[AdminForthFilterOperators.LIKE]: 'LIKE',
|
|
29
|
+
[AdminForthFilterOperators.ILIKE]: 'ILIKE',
|
|
30
|
+
[AdminForthFilterOperators.IN]: 'IN',
|
|
31
|
+
[AdminForthFilterOperators.NIN]: 'NOT IN',
|
|
32
|
+
};
|
|
33
|
+
this.SortDirectionsMap = {
|
|
34
|
+
[AdminForthSortDirections.asc]: 'ASC',
|
|
35
|
+
[AdminForthSortDirections.desc]: 'DESC',
|
|
36
|
+
};
|
|
37
|
+
this.dbName = new URL(url).pathname.replace('/', '');
|
|
38
|
+
// create connection here
|
|
39
|
+
this.client = createClient({
|
|
40
|
+
url: url.replace('clickhouse://', 'http://'),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
discoverFields(resource) {
|
|
44
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
45
|
+
const tableName = resource.table;
|
|
46
|
+
const q = yield this.client.query({
|
|
47
|
+
query: `SELECT * FROM system.columns WHERE table = '${tableName}' and database = '${this.dbName}'`,
|
|
48
|
+
format: 'JSONEachRow',
|
|
49
|
+
});
|
|
50
|
+
const rows = yield q.json();
|
|
51
|
+
const fieldTypes = {};
|
|
52
|
+
rows.forEach((row) => {
|
|
53
|
+
const field = {};
|
|
54
|
+
const baseType = row.type;
|
|
55
|
+
if (baseType.startsWith('Int') || baseType.startsWith('UInt')) {
|
|
56
|
+
field.type = AdminForthDataTypes.INTEGER;
|
|
57
|
+
}
|
|
58
|
+
else if (baseType === 'FixedString' || baseType === 'String') {
|
|
59
|
+
field.type = AdminForthDataTypes.STRING;
|
|
60
|
+
// TODO
|
|
61
|
+
// const length = baseType.match(/\d+/g);
|
|
62
|
+
// field.maxLength = length ? parseInt(length[0]) : null;
|
|
63
|
+
}
|
|
64
|
+
else if (baseType == 'UUID') {
|
|
65
|
+
field.type = AdminForthDataTypes.STRING;
|
|
66
|
+
}
|
|
67
|
+
else if (baseType.startsWith('Decimal')) {
|
|
68
|
+
field.type = AdminForthDataTypes.DECIMAL;
|
|
69
|
+
// const [precision, scale] = baseType.match(/\d+/g);
|
|
70
|
+
// TODO
|
|
71
|
+
// field.precision = parseInt(precision);
|
|
72
|
+
// field.scale = parseInt(scale);
|
|
73
|
+
}
|
|
74
|
+
else if (baseType.startsWith('Float')) {
|
|
75
|
+
field.type = AdminForthDataTypes.FLOAT;
|
|
76
|
+
}
|
|
77
|
+
else if (baseType == 'DateTime64' || baseType == 'DateTime') {
|
|
78
|
+
field.type = AdminForthDataTypes.DATETIME;
|
|
79
|
+
}
|
|
80
|
+
else if (baseType == 'Date' || baseType == 'Date64') {
|
|
81
|
+
field.type = AdminForthDataTypes.DATE;
|
|
82
|
+
}
|
|
83
|
+
else if (baseType == 'Boolean') {
|
|
84
|
+
field.type = AdminForthDataTypes.BOOLEAN;
|
|
85
|
+
field._underlineType = 'boolean';
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
field.type = 'unknown';
|
|
89
|
+
}
|
|
90
|
+
field._underlineType = baseType;
|
|
91
|
+
field._baseTypeDebug = baseType;
|
|
92
|
+
field.required = row.notnull == 1;
|
|
93
|
+
field.primaryKey = row.pk == 1;
|
|
94
|
+
field.default = row.dflt_value;
|
|
95
|
+
fieldTypes[row.name] = field;
|
|
96
|
+
});
|
|
97
|
+
return fieldTypes;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
getFieldValue(field, value) {
|
|
101
|
+
if (field.type == AdminForthDataTypes.DATETIME) {
|
|
102
|
+
if (!value) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
if (field._underlineType.startsWith('Int') || field._underlineType.startsWith('UInt')) {
|
|
106
|
+
return dayjs.unix(+value).toISOString();
|
|
107
|
+
}
|
|
108
|
+
else if (field._underlineType.startsWith('DateTime')
|
|
109
|
+
|| field._underlineType.startsWith('String')
|
|
110
|
+
|| field._underlineType.startsWith('FixedString')) {
|
|
111
|
+
return dayjs(value).toISOString();
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
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}"`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (field.type == AdminForthDataTypes.DATE) {
|
|
118
|
+
if (!value) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return dayjs(value).toISOString().split('T')[0];
|
|
122
|
+
}
|
|
123
|
+
else if (field.type == AdminForthDataTypes.BOOLEAN) {
|
|
124
|
+
return !!value;
|
|
125
|
+
}
|
|
126
|
+
else if (field.type == AdminForthDataTypes.JSON) {
|
|
127
|
+
if (field._underlineType.startsWith('String') || field._underlineType.startsWith('FixedString')) {
|
|
128
|
+
return JSON.parse(value);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.error(`AdminForth: JSON field is not a string but ${field._underlineType}, this is not supported yet`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
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
|
+
setFieldValue(field, value) {
|
|
153
|
+
if (field.type == AdminForthDataTypes.DATETIME) {
|
|
154
|
+
if (!value) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
if (field._underlineType.startsWith('Int') || field._underlineType.startsWith('UInt')) {
|
|
158
|
+
// value is iso string now, convert to unix timestamp
|
|
159
|
+
return dayjs(value).unix();
|
|
160
|
+
}
|
|
161
|
+
else if (field._underlineType.startsWith('DateTime')
|
|
162
|
+
|| field._underlineType.startsWith('String')
|
|
163
|
+
|| field._underlineType.startsWith('FixedString')) {
|
|
164
|
+
// value is iso string now, convert to unix timestamp
|
|
165
|
+
return dayjs(value).toISOString();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (field.type == AdminForthDataTypes.BOOLEAN) {
|
|
169
|
+
return value ? 1 : 0;
|
|
170
|
+
}
|
|
171
|
+
else if (field.type == AdminForthDataTypes.JSON) {
|
|
172
|
+
// check underline type is text or string
|
|
173
|
+
if (field._underlineType.startsWith('String') || field._underlineType.startsWith('FixedString')) {
|
|
174
|
+
return JSON.stringify(value);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
console.error(`AdminForth: JSON field is not a string/text but ${field._underlineType}, this is not supported yet`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
getDataWithOriginalTypes(_a) {
|
|
183
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, limit, offset, sort, filters }) {
|
|
184
|
+
const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
|
|
185
|
+
const tableName = resource.table;
|
|
186
|
+
const where = filters.length ? `WHERE ${filters.map((f, i) => {
|
|
187
|
+
let placeholder = `{f${i}}`;
|
|
188
|
+
let field = f.field;
|
|
189
|
+
let operator = this.OperatorsMap[f.operator];
|
|
190
|
+
if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
|
|
191
|
+
placeholder = `(${f.value.map((_, j) => `p${i}_${j}`).join(', ')})`;
|
|
192
|
+
}
|
|
193
|
+
return `${field} ${operator} ${placeholder}`;
|
|
194
|
+
}).join(' AND ')}` : '';
|
|
195
|
+
const params = {};
|
|
196
|
+
filters.length ? filters.forEach((f, i) => {
|
|
197
|
+
// for arrays do set in map
|
|
198
|
+
const v = f.value;
|
|
199
|
+
if (f.operator == AdminForthFilterOperators.LIKE || f.operator == AdminForthFilterOperators.ILIKE) {
|
|
200
|
+
params[`f${i}`] = `%${v}%`;
|
|
201
|
+
}
|
|
202
|
+
else if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
|
|
203
|
+
v.forEach((_, j) => {
|
|
204
|
+
params[`p${i}_${j}`] = v[j];
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
params[`f${i}`] = v;
|
|
209
|
+
}
|
|
210
|
+
}) : [];
|
|
211
|
+
const orderBy = sort.length ? `ORDER BY ${sort.map((s) => `${s.field} ${this.SortDirectionsMap[s.direction]}`).join(', ')}` : '';
|
|
212
|
+
const q = `SELECT ${columns} FROM ${tableName} ${where} ${orderBy} LIMIT {limit:Int} OFFSET {offset:Int}`;
|
|
213
|
+
const d = Object.assign(Object.assign({}, params), { limit,
|
|
214
|
+
offset });
|
|
215
|
+
console.log('🪲 Clickhouse Query', q, 'params:', d);
|
|
216
|
+
const stmt = yield this.client.query({
|
|
217
|
+
query: q,
|
|
218
|
+
format: 'JSONEachRow',
|
|
219
|
+
query_params: d,
|
|
220
|
+
});
|
|
221
|
+
if (process.env.HEAVY_DEBUG) {
|
|
222
|
+
console.log('🪲 Clickhouse Query', q, 'params:', d);
|
|
223
|
+
}
|
|
224
|
+
const rows = yield stmt.json();
|
|
225
|
+
const countQ = yield this.client.query({
|
|
226
|
+
query: `SELECT COUNT(*) FROM ${tableName} ${where}`,
|
|
227
|
+
format: 'JSONEachRow',
|
|
228
|
+
query_params: d,
|
|
229
|
+
});
|
|
230
|
+
const total = (yield countQ.json())['COUNT(*)'];
|
|
231
|
+
return {
|
|
232
|
+
data: rows.map((row) => {
|
|
233
|
+
const newRow = {};
|
|
234
|
+
for (const [key, value] of Object.entries(row)) {
|
|
235
|
+
newRow[key] = value;
|
|
236
|
+
}
|
|
237
|
+
return newRow;
|
|
238
|
+
}),
|
|
239
|
+
total,
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
getMinMaxForColumnsWithOriginalTypes(_a) {
|
|
244
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, columns }) {
|
|
245
|
+
const tableName = resource.table;
|
|
246
|
+
const result = {};
|
|
247
|
+
yield Promise.all(columns.map((col) => __awaiter(this, void 0, void 0, function* () {
|
|
248
|
+
// const stmt = await this.db.prepare(`SELECT MIN(${col.name}) as min, MAX(${col.name}) as max FROM ${tableName}`);
|
|
249
|
+
// const { min, max } = stmt.get();
|
|
250
|
+
// result[col.name] = {
|
|
251
|
+
// min, max,
|
|
252
|
+
// };
|
|
253
|
+
})));
|
|
254
|
+
return result;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
createRecordOriginalValues(_a) {
|
|
258
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, record }) {
|
|
259
|
+
const tableName = resource.table;
|
|
260
|
+
const columns = Object.keys(record);
|
|
261
|
+
yield this.client.insert({
|
|
262
|
+
table: tableName,
|
|
263
|
+
values: columns.map((colName) => {
|
|
264
|
+
return { [colName]: record[colName] };
|
|
265
|
+
}),
|
|
266
|
+
format: 'JSONEachRow',
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
updateRecord(_a) {
|
|
271
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, recordId, newValues }) {
|
|
272
|
+
const columnsWithPlaceholders = Object.keys(newValues).map((col) => `${col} = ?`);
|
|
273
|
+
const values = [...Object.values(newValues), recordId];
|
|
274
|
+
// const q = this.db.prepare(
|
|
275
|
+
// `UPDATE ${resource.table} SET ${columnsWithPlaceholders} WHERE ${this.getPrimaryKey(resource)} = ?`
|
|
276
|
+
// )
|
|
277
|
+
// await q.run(values);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
deleteRecord(_a) {
|
|
281
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, recordId }) {
|
|
282
|
+
// const q = this.db.prepare(`DELETE FROM ${resource.table} WHERE ${this.getPrimaryKey(resource)} = ?`);
|
|
283
|
+
// await q.run(recordId);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
close() {
|
|
287
|
+
this.client.disconnect();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
export default ClickhouseConnector;
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages
|
|
|
24
24
|
import AdminForthPlugin from './basePlugin.js';
|
|
25
25
|
import ConfigValidator from './modules/configValidator.js';
|
|
26
26
|
import AdminForthRestAPI from './modules/restApi.js';
|
|
27
|
+
import ClickhouseConnector from './dataConnectors/clickhouse.js';
|
|
27
28
|
// exports
|
|
28
29
|
export * from './types/AdminForthConfig.js';
|
|
29
30
|
export { AdminForthPlugin };
|
|
@@ -65,6 +66,7 @@ class AdminForth {
|
|
|
65
66
|
'sqlite': SQLiteConnector,
|
|
66
67
|
'postgres': PostgresConnector,
|
|
67
68
|
'mongodb': MongoConnector,
|
|
69
|
+
'clickhouse': ClickhouseConnector,
|
|
68
70
|
};
|
|
69
71
|
if (!this.config.databaseConnectors) {
|
|
70
72
|
this.config.databaseConnectors = Object.assign({}, this.connectorClasses);
|
package/dist/modules/restApi.js
CHANGED
|
@@ -201,7 +201,7 @@ export default class AdminForthRestAPI {
|
|
|
201
201
|
return __awaiter(this, void 0, void 0, function* () {
|
|
202
202
|
var _a;
|
|
203
203
|
if (process.env.HEAVY_DEBUG) {
|
|
204
|
-
console.log('🪲Interpreting resource', resource.resourceId, source);
|
|
204
|
+
console.log('🪲Interpreting resource', resource.resourceId, source, 'adminUser', adminUser);
|
|
205
205
|
}
|
|
206
206
|
const allowedActions = {};
|
|
207
207
|
yield Promise.all(Object.entries(((_a = resource.options) === null || _a === void 0 ? void 0 : _a.allowedActions) || {}).map((_b) => __awaiter(this, [_b], void 0, function* ([key, value]) {
|
|
@@ -609,6 +609,7 @@ export default class AdminForthRestAPI {
|
|
|
609
609
|
path: '/start_bulk_action',
|
|
610
610
|
handler: (_11) => __awaiter(this, [_11], void 0, function* ({ body }) {
|
|
611
611
|
const { resourceId, actionId, recordIds, adminUser } = body;
|
|
612
|
+
process.env.HEAVY_DEBUG && console.log('🪲 starting bulk action', body);
|
|
612
613
|
const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
|
|
613
614
|
if (!resource) {
|
|
614
615
|
return { error: `Resource '${resourceId}' not found` };
|
package/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import AdminForthPlugin from './basePlugin.js';
|
|
20
20
|
import ConfigValidator from './modules/configValidator.js';
|
|
21
21
|
import AdminForthRestAPI from './modules/restApi.js';
|
|
22
|
+
import ClickhouseConnector from './dataConnectors/clickhouse.js';
|
|
22
23
|
|
|
23
24
|
// exports
|
|
24
25
|
export * from './types/AdminForthConfig.js';
|
|
@@ -92,6 +93,7 @@ class AdminForth implements IAdminForth {
|
|
|
92
93
|
'sqlite': SQLiteConnector,
|
|
93
94
|
'postgres': PostgresConnector,
|
|
94
95
|
'mongodb': MongoConnector,
|
|
96
|
+
'clickhouse': ClickhouseConnector,
|
|
95
97
|
};
|
|
96
98
|
if (!this.config.databaseConnectors) {
|
|
97
99
|
this.config.databaseConnectors = {...this.connectorClasses};
|
package/modules/restApi.ts
CHANGED
|
@@ -226,7 +226,7 @@ export default class AdminForthRestAPI {
|
|
|
226
226
|
|
|
227
227
|
async function interpretResource(adminUser: AdminUser, resource: AdminForthResource, meta: any, source: ActionCheckSource): Promise<{allowedActions: AllowedActionsResolved}> {
|
|
228
228
|
if (process.env.HEAVY_DEBUG) {
|
|
229
|
-
console.log('🪲Interpreting resource', resource.resourceId, source);
|
|
229
|
+
console.log('🪲Interpreting resource', resource.resourceId, source, 'adminUser', adminUser);
|
|
230
230
|
}
|
|
231
231
|
const allowedActions = {};
|
|
232
232
|
|
|
@@ -697,6 +697,7 @@ export default class AdminForthRestAPI {
|
|
|
697
697
|
path: '/start_bulk_action',
|
|
698
698
|
handler: async ({ body }) => {
|
|
699
699
|
const { resourceId, actionId, recordIds, adminUser } = body;
|
|
700
|
+
process.env.HEAVY_DEBUG && console.log('🪲 starting bulk action', body);
|
|
700
701
|
const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
|
|
701
702
|
if (!resource) {
|
|
702
703
|
return { error: `Resource '${resourceId}' not found` };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adminforth",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.27",
|
|
4
4
|
"description": "OpenSource Vue3 powered forth-generation admin panel",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"mongodb": "6.6",
|
|
26
26
|
"node-fetch": "^3.3.2",
|
|
27
27
|
"pg": "^8.11.5",
|
|
28
|
-
"uuid": "^9.0.1"
|
|
28
|
+
"uuid": "^9.0.1",
|
|
29
|
+
"@clickhouse/client": "^1.4.0"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@types/node": "^20.14.2",
|