adminforth 1.0.17 → 1.0.24
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/{mongo.js → mongo.ts} +15 -10
- package/dataConnectors/{postgres.js → postgres.ts} +6 -2
- package/dataConnectors/{sqlite.js → sqlite.ts} +6 -2
- package/dist/auth.js +68 -0
- package/dist/dataConnectors/mongo.js +204 -0
- package/dist/dataConnectors/postgres.js +298 -0
- package/dist/dataConnectors/sqlite.js +261 -0
- package/dist/index.js +693 -0
- package/dist/modules/codeInjector.js +337 -0
- package/dist/modules/utils.js +12 -0
- package/dist/servers/express.js +210 -0
- package/dist/spa/src/main.js +16 -0
- package/dist/spa/src/router/index.js +79 -0
- package/dist/spa/src/stores/core.js +154 -0
- package/dist/spa/src/stores/modal.js +35 -0
- package/dist/spa/src/utils.js +59 -0
- package/dist/spa/vite.config.js +44 -0
- package/dist/spa_tmp/src/custom/custom/vueUses.js +10 -0
- package/dist/spa_tmp/src/main.js +29 -0
- package/dist/spa_tmp/src/router/index.js +83 -0
- package/dist/spa_tmp/src/stores/core.js +150 -0
- package/dist/spa_tmp/src/stores/modal.js +35 -0
- package/dist/spa_tmp/src/utils.js +59 -0
- package/dist/spa_tmp/vite.config.js +43 -0
- package/dist/types.js +30 -0
- package/{index.js → index.ts} +115 -6
- package/modules/{codeInjector.js → codeInjector.ts} +17 -13
- package/package.json +9 -3
- package/servers/{express.js → express.ts} +8 -2
- package/spa/package.json +2 -2
- package/spa/src/views/ListView.vue +1 -1
- package/tsconfig.json +112 -0
- package/{types.js → types.ts} +2 -0
- /package/{auth.js → auth.ts} +0 -0
- /package/modules/{utils.js → utils.ts} +0 -0
|
@@ -4,12 +4,10 @@ import { AdminForthFilterOperators, AdminForthSortDirections, AdminForthTypes }
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class MongoConnector {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
};
|
|
12
|
-
|
|
7
|
+
db: any;
|
|
8
|
+
|
|
9
|
+
constructor({ url }) {
|
|
10
|
+
this.db = new MongoClient(url);
|
|
13
11
|
(async () => {
|
|
14
12
|
try {
|
|
15
13
|
await this.db.connect();
|
|
@@ -21,8 +19,6 @@ class MongoConnector {
|
|
|
21
19
|
console.error('ERROR: Failed to connect to Mongo', e);
|
|
22
20
|
}
|
|
23
21
|
})();
|
|
24
|
-
|
|
25
|
-
this.fieldtypesByTable = fieldtypesByTable;
|
|
26
22
|
}
|
|
27
23
|
|
|
28
24
|
OperatorsMap = {
|
|
@@ -43,8 +39,17 @@ class MongoConnector {
|
|
|
43
39
|
[AdminForthSortDirections.DESC]: -1,
|
|
44
40
|
};
|
|
45
41
|
|
|
46
|
-
async discoverFields(
|
|
47
|
-
return
|
|
42
|
+
async discoverFields(resource) {
|
|
43
|
+
return resource.columns.reduce((acc, col) => {
|
|
44
|
+
acc[col.name] = {
|
|
45
|
+
name: col.name,
|
|
46
|
+
type: col.type,
|
|
47
|
+
primaryKey: col.primaryKey,
|
|
48
|
+
virtual: col.virtual,
|
|
49
|
+
_underlineType: col._underlineType,
|
|
50
|
+
};
|
|
51
|
+
return acc;
|
|
52
|
+
}, {});
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
getPrimaryKey(resource) {
|
|
@@ -5,6 +5,9 @@ const { Client } = pkg;
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class PostgresConnector {
|
|
8
|
+
|
|
9
|
+
db: any;
|
|
10
|
+
|
|
8
11
|
constructor({ url }) {
|
|
9
12
|
this.db = new Client({
|
|
10
13
|
connectionString: url
|
|
@@ -37,7 +40,8 @@ class PostgresConnector {
|
|
|
37
40
|
[AdminForthSortDirections.DESC]: 'DESC',
|
|
38
41
|
};
|
|
39
42
|
|
|
40
|
-
async discoverFields(
|
|
43
|
+
async discoverFields(resource) {
|
|
44
|
+
const tableName = resource.table;
|
|
41
45
|
const stmt = await this.db.query(`
|
|
42
46
|
SELECT
|
|
43
47
|
a.attname AS name,
|
|
@@ -65,7 +69,7 @@ class PostgresConnector {
|
|
|
65
69
|
const fieldTypes = {};
|
|
66
70
|
|
|
67
71
|
rows.forEach((row) => {
|
|
68
|
-
const field = {};
|
|
72
|
+
const field: any = {};
|
|
69
73
|
const baseType = row.type.toLowerCase();
|
|
70
74
|
if (baseType == 'int') {
|
|
71
75
|
field.type = AdminForthTypes.INTEGER;
|
|
@@ -3,18 +3,22 @@ import { AdminForthTypes, AdminForthFilterOperators, AdminForthSortDirections }
|
|
|
3
3
|
import dayjs from 'dayjs';
|
|
4
4
|
|
|
5
5
|
class SQLiteConnector {
|
|
6
|
+
|
|
7
|
+
db: any;
|
|
8
|
+
|
|
6
9
|
constructor({ url }) {
|
|
7
10
|
// create connection here
|
|
8
11
|
|
|
9
12
|
this.db = betterSqlite3(url.replace('sqlite://', ''));
|
|
10
13
|
}
|
|
11
14
|
|
|
12
|
-
async discoverFields(
|
|
15
|
+
async discoverFields(resource) {
|
|
16
|
+
const tableName = resource.table;
|
|
13
17
|
const stmt = this.db.prepare(`PRAGMA table_info(${tableName})`);
|
|
14
18
|
const rows = await stmt.all();
|
|
15
19
|
const fieldTypes = {};
|
|
16
20
|
rows.forEach((row) => {
|
|
17
|
-
const field = {};
|
|
21
|
+
const field: any = {};
|
|
18
22
|
const baseType = row.type.toLowerCase();
|
|
19
23
|
if (baseType == 'int') {
|
|
20
24
|
field.type = AdminForthTypes.INTEGER;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
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 jwt from 'jsonwebtoken';
|
|
11
|
+
import crypto from 'crypto';
|
|
12
|
+
// Function to generate a password hash using PBKDF2
|
|
13
|
+
function calcPasswordHash(password, salt, iterations = 100000, keyLength = 64, digest = 'sha512') {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
crypto.pbkdf2(password, salt, iterations, keyLength, digest, (err, derivedKey) => {
|
|
16
|
+
if (err)
|
|
17
|
+
reject(err);
|
|
18
|
+
resolve(derivedKey.toString('hex'));
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// Function to generate a random salt
|
|
23
|
+
function generateSalt(length = 16) {
|
|
24
|
+
return crypto.randomBytes(length).toString('hex');
|
|
25
|
+
}
|
|
26
|
+
class AdminForthAuth {
|
|
27
|
+
issueJWT(payload) {
|
|
28
|
+
// read ADMINFORH_SECRET from environment if not drop error
|
|
29
|
+
const secret = process.env.ADMINFORTH_SECRET;
|
|
30
|
+
if (!secret) {
|
|
31
|
+
throw new Error('ADMINFORTH_SECRET environment not set');
|
|
32
|
+
}
|
|
33
|
+
// issue JWT token
|
|
34
|
+
const expiresIn = process.env.ADMINFORTH_AUTH_EXPIRESIN || '24h';
|
|
35
|
+
return jwt.sign(payload, secret, { expiresIn });
|
|
36
|
+
}
|
|
37
|
+
verify(jwtToken) {
|
|
38
|
+
// read ADMINFORH_SECRET from environment if not drop error
|
|
39
|
+
const secret = process.env.ADMINFORTH_SECRET;
|
|
40
|
+
if (!secret) {
|
|
41
|
+
throw new Error('ADMINFORTH_SECRET environment not set');
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
// verify JWT token
|
|
45
|
+
const decoded = jwt.verify(jwtToken, secret);
|
|
46
|
+
return decoded;
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.error('Failed to verify JWT token', err);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
static generatePasswordHash(password) {
|
|
54
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
const salt = generateSalt();
|
|
56
|
+
const hashedPassword = yield calcPasswordHash(password, salt);
|
|
57
|
+
return `${salt}:${hashedPassword}`;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
static verifyPassword(password, hashedPassword) {
|
|
61
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
const [salt, hash] = hashedPassword.split(':');
|
|
63
|
+
const newHash = yield calcPasswordHash(password, salt);
|
|
64
|
+
return newHash === hash;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export default AdminForthAuth;
|
|
@@ -0,0 +1,204 @@
|
|
|
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 dayjs from 'dayjs';
|
|
11
|
+
import { MongoClient } from 'mongodb';
|
|
12
|
+
import { AdminForthFilterOperators, AdminForthSortDirections, AdminForthTypes } from '../types.js';
|
|
13
|
+
class MongoConnector {
|
|
14
|
+
constructor({ url }) {
|
|
15
|
+
this.OperatorsMap = {
|
|
16
|
+
[AdminForthFilterOperators.EQ]: (value) => value,
|
|
17
|
+
[AdminForthFilterOperators.NE]: (value) => ({ $ne: value }),
|
|
18
|
+
[AdminForthFilterOperators.GT]: (value) => ({ $gt: value }),
|
|
19
|
+
[AdminForthFilterOperators.LT]: (value) => ({ $lt: value }),
|
|
20
|
+
[AdminForthFilterOperators.GTE]: (value) => ({ $gte: value }),
|
|
21
|
+
[AdminForthFilterOperators.LTE]: (value) => ({ $lte: value }),
|
|
22
|
+
[AdminForthFilterOperators.LIKE]: (value) => ({ $regex: value }),
|
|
23
|
+
[AdminForthFilterOperators.ILIKE]: (value) => ({ $regex: value, $options: 'i' }),
|
|
24
|
+
[AdminForthFilterOperators.IN]: (value) => ({ $in: value }),
|
|
25
|
+
[AdminForthFilterOperators.NIN]: (value) => ({ $nin: value }),
|
|
26
|
+
};
|
|
27
|
+
this.SortDirectionsMap = {
|
|
28
|
+
[AdminForthSortDirections.ASC]: 1,
|
|
29
|
+
[AdminForthSortDirections.DESC]: -1,
|
|
30
|
+
};
|
|
31
|
+
this.db = new MongoClient(url);
|
|
32
|
+
(() => __awaiter(this, void 0, void 0, function* () {
|
|
33
|
+
try {
|
|
34
|
+
yield this.db.connect();
|
|
35
|
+
this.db.on('error', (err) => {
|
|
36
|
+
console.log('Mongo error: ', err.message);
|
|
37
|
+
});
|
|
38
|
+
console.log('Connected to Mongo');
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
console.error('ERROR: Failed to connect to Mongo', e);
|
|
42
|
+
}
|
|
43
|
+
}))();
|
|
44
|
+
}
|
|
45
|
+
discoverFields(resource) {
|
|
46
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
47
|
+
return resource.columns.reduce((acc, col) => {
|
|
48
|
+
acc[col.name] = {
|
|
49
|
+
name: col.name,
|
|
50
|
+
type: col.type,
|
|
51
|
+
primaryKey: col.primaryKey,
|
|
52
|
+
virtual: col.virtual,
|
|
53
|
+
_underlineType: col._underlineType,
|
|
54
|
+
};
|
|
55
|
+
return acc;
|
|
56
|
+
}, {});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
getPrimaryKey(resource) {
|
|
60
|
+
for (const col of resource.dataSourceColumns) {
|
|
61
|
+
if (col.primaryKey) {
|
|
62
|
+
return col.name;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
getFieldValue(field, value) {
|
|
67
|
+
if (field.type == AdminForthTypes.DATETIME) {
|
|
68
|
+
if (!value) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (field._underlineType == 'timestamp' || field._underlineType == 'int') {
|
|
72
|
+
return dayjs.unix(+value).toISOString();
|
|
73
|
+
}
|
|
74
|
+
else if (field._underlineType == 'varchar') {
|
|
75
|
+
return dayjs.unix(+value).toISOString();
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
throw new Error(`AdminForth does not support row type: ${field._underlineType} for timestamps, use VARCHAR (with iso strings) or TIMESTAMP/INT (with unix timestamps)`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (field.type == AdminForthTypes.BOOLEAN) {
|
|
82
|
+
return !!value;
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
getRecordByPrimaryKey(resource, key) {
|
|
87
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
88
|
+
const tableName = resource.table;
|
|
89
|
+
const collection = this.db.db().collection(tableName);
|
|
90
|
+
const row = yield collection.findOne({ [this.getPrimaryKey(resource)]: key });
|
|
91
|
+
if (!row) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const newRow = {};
|
|
95
|
+
for (const [key_1, value] of Object.entries(row)) {
|
|
96
|
+
const dbKey = resource.dataSourceColumns.find((col) => col.name == key_1);
|
|
97
|
+
if (!dbKey) {
|
|
98
|
+
continue; // should I continue or throw an error?
|
|
99
|
+
throw new Error(`Resource '${resource.table}' has no column '${key_1}' defined`);
|
|
100
|
+
}
|
|
101
|
+
newRow[key_1] = this.getFieldValue(dbKey, value);
|
|
102
|
+
}
|
|
103
|
+
console.log('newRow', newRow);
|
|
104
|
+
return newRow;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
setFieldValue(field, value) {
|
|
108
|
+
if (field.type == AdminForthTypes.DATETIME) {
|
|
109
|
+
if (!value) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
if (field._underlineType == 'timestamp' || field._underlineType == 'int') {
|
|
113
|
+
// value is iso string now, convert to unix timestamp
|
|
114
|
+
return dayjs(value).unix();
|
|
115
|
+
}
|
|
116
|
+
else if (field._underlineType == 'varchar') {
|
|
117
|
+
// value is iso string now, convert to unix timestamp
|
|
118
|
+
return dayjs(value).toISOString();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (field.type == AdminForthTypes.BOOLEAN) {
|
|
122
|
+
return value ? 1 : 0;
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
getData(_a) {
|
|
127
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, limit, offset, sort, filters }) {
|
|
128
|
+
// const columns = resource.dataSourceColumns.filter(c=> !c.virtual).map((col) => col.name).join(', ');
|
|
129
|
+
const tableName = resource.table;
|
|
130
|
+
for (const filter of filters) {
|
|
131
|
+
if (!this.OperatorsMap[filter.operator]) {
|
|
132
|
+
throw new Error(`Operator ${filter.operator} is not allowed`);
|
|
133
|
+
}
|
|
134
|
+
if (!resource.dataSourceColumns.some((col) => col.name == filter.field)) {
|
|
135
|
+
throw new Error(`Field ${filter.field} is not in resource ${resource.resourceId}. Available fields: ${resource.dataSourceColumns.map((col) => col.name).join(', ')}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const collection = this.db.db().collection(tableName);
|
|
139
|
+
const query = {};
|
|
140
|
+
for (const filter of filters) {
|
|
141
|
+
query[filter.field] = this.OperatorsMap[filter.operator](filter.value);
|
|
142
|
+
}
|
|
143
|
+
const total = yield collection.countDocuments(query);
|
|
144
|
+
const result = yield collection.find(query)
|
|
145
|
+
.sort(sort)
|
|
146
|
+
.skip(offset)
|
|
147
|
+
.limit(limit)
|
|
148
|
+
.toArray();
|
|
149
|
+
return { data: result, total };
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
getMinMaxForColumns(_a) {
|
|
153
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, columns }) {
|
|
154
|
+
const tableName = resource.table;
|
|
155
|
+
const collection = this.db.db().collection(tableName);
|
|
156
|
+
const result = {};
|
|
157
|
+
for (const column of columns) {
|
|
158
|
+
result[column] = yield collection
|
|
159
|
+
.aggregate([
|
|
160
|
+
{ $group: { _id: null, min: { $min: `$${column}` }, max: { $max: `$${column}` } } },
|
|
161
|
+
])
|
|
162
|
+
.toArray();
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
createRecord(_a) {
|
|
168
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, record }) {
|
|
169
|
+
const tableName = resource.table;
|
|
170
|
+
const collection = this.db.db().collection(tableName);
|
|
171
|
+
const columns = Object.keys(record);
|
|
172
|
+
const newRecord = {};
|
|
173
|
+
for (const colName of columns) {
|
|
174
|
+
const col = resource.dataSourceColumns.find((col) => col.name == colName);
|
|
175
|
+
if (col) {
|
|
176
|
+
newRecord[colName] = this.setFieldValue(col, record[colName]);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
newRecord[colName] = record[colName];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
yield collection.insertOne(newRecord);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
updateRecord(_a) {
|
|
186
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, recordId, record, newValues }) {
|
|
187
|
+
const collection = this.db.db().collection(resource.table);
|
|
188
|
+
yield collection.updateOne({ [this.getPrimaryKey(resource)]: recordId }, { $set: newValues });
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
deleteRecord(_a) {
|
|
192
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, recordId }) {
|
|
193
|
+
const primaryKey = this.getPrimaryKey(resource);
|
|
194
|
+
const collection = this.db.db().collection(resource.table);
|
|
195
|
+
yield collection.deleteOne({ [primaryKey]: recordId });
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
close() {
|
|
199
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
200
|
+
yield this.db.end();
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
export default MongoConnector;
|
|
@@ -0,0 +1,298 @@
|
|
|
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 dayjs from 'dayjs';
|
|
11
|
+
import pkg from 'pg';
|
|
12
|
+
import { AdminForthFilterOperators, AdminForthSortDirections, AdminForthTypes } from '../types.js';
|
|
13
|
+
const { Client } = pkg;
|
|
14
|
+
class PostgresConnector {
|
|
15
|
+
constructor({ url }) {
|
|
16
|
+
this.OperatorsMap = {
|
|
17
|
+
[AdminForthFilterOperators.EQ]: '=',
|
|
18
|
+
[AdminForthFilterOperators.NE]: '!=',
|
|
19
|
+
[AdminForthFilterOperators.GT]: '>',
|
|
20
|
+
[AdminForthFilterOperators.LT]: '<',
|
|
21
|
+
[AdminForthFilterOperators.GTE]: '>=',
|
|
22
|
+
[AdminForthFilterOperators.LTE]: '<=',
|
|
23
|
+
[AdminForthFilterOperators.LIKE]: 'LIKE',
|
|
24
|
+
[AdminForthFilterOperators.ILIKE]: 'ILIKE',
|
|
25
|
+
[AdminForthFilterOperators.IN]: 'IN',
|
|
26
|
+
[AdminForthFilterOperators.NIN]: 'NOT IN',
|
|
27
|
+
};
|
|
28
|
+
this.SortDirectionsMap = {
|
|
29
|
+
[AdminForthSortDirections.ASC]: 'ASC',
|
|
30
|
+
[AdminForthSortDirections.DESC]: 'DESC',
|
|
31
|
+
};
|
|
32
|
+
this.db = new Client({
|
|
33
|
+
connectionString: url
|
|
34
|
+
});
|
|
35
|
+
(() => __awaiter(this, void 0, void 0, function* () {
|
|
36
|
+
yield this.db.connect();
|
|
37
|
+
this.db.on('error', (err) => {
|
|
38
|
+
console.log('Postgres error: ', err.message, err.stack);
|
|
39
|
+
this.db.end();
|
|
40
|
+
this.db = new PostgresConnector({ url }).db;
|
|
41
|
+
});
|
|
42
|
+
}))();
|
|
43
|
+
}
|
|
44
|
+
discoverFields(resource) {
|
|
45
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
46
|
+
const tableName = resource.table;
|
|
47
|
+
const stmt = yield this.db.query(`
|
|
48
|
+
SELECT
|
|
49
|
+
a.attname AS name,
|
|
50
|
+
pg_catalog.format_type(a.atttypid, a.atttypmod) AS type,
|
|
51
|
+
a.attnotnull AS notnull,
|
|
52
|
+
COALESCE(pg_get_expr(d.adbin, d.adrelid), '') AS dflt_value,
|
|
53
|
+
CASE
|
|
54
|
+
WHEN ct.contype = 'p' THEN 1
|
|
55
|
+
ELSE 0
|
|
56
|
+
END AS pk
|
|
57
|
+
FROM
|
|
58
|
+
pg_catalog.pg_attribute a
|
|
59
|
+
LEFT JOIN pg_catalog.pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
|
60
|
+
LEFT JOIN pg_catalog.pg_constraint ct ON a.attnum = ANY (ct.conkey) AND a.attrelid = ct.conrelid
|
|
61
|
+
LEFT JOIN pg_catalog.pg_class c ON a.attrelid = c.oid
|
|
62
|
+
LEFT JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid
|
|
63
|
+
WHERE
|
|
64
|
+
c.relname = $1
|
|
65
|
+
AND a.attnum > 0
|
|
66
|
+
AND NOT a.attisdropped
|
|
67
|
+
ORDER BY
|
|
68
|
+
a.attnum;
|
|
69
|
+
`, [tableName]);
|
|
70
|
+
const rows = stmt.rows;
|
|
71
|
+
const fieldTypes = {};
|
|
72
|
+
rows.forEach((row) => {
|
|
73
|
+
const field = {};
|
|
74
|
+
const baseType = row.type.toLowerCase();
|
|
75
|
+
if (baseType == 'int') {
|
|
76
|
+
field.type = AdminForthTypes.INTEGER;
|
|
77
|
+
field._underlineType = 'int';
|
|
78
|
+
}
|
|
79
|
+
else if (baseType.includes('float') || baseType.includes('double')) {
|
|
80
|
+
field.type = AdminForthTypes.FLOAT;
|
|
81
|
+
field._underlineType = 'float';
|
|
82
|
+
}
|
|
83
|
+
else if (baseType.includes('bool')) {
|
|
84
|
+
field.type = AdminForthTypes.BOOLEAN;
|
|
85
|
+
field._underlineType = 'bool';
|
|
86
|
+
}
|
|
87
|
+
else if (baseType.includes('character varying')) {
|
|
88
|
+
field.type = AdminForthTypes.STRING;
|
|
89
|
+
field._underlineType = 'varchar';
|
|
90
|
+
const length = baseType.match(/\d+/);
|
|
91
|
+
field.maxLength = length ? parseInt(length[0]) : null;
|
|
92
|
+
}
|
|
93
|
+
else if (baseType == 'text') {
|
|
94
|
+
field.type = AdminForthTypes.TEXT;
|
|
95
|
+
field._underlineType = 'text';
|
|
96
|
+
}
|
|
97
|
+
else if (baseType.includes('decimal(')) {
|
|
98
|
+
field.type = AdminForthTypes.DECIMAL;
|
|
99
|
+
field._underlineType = 'decimal';
|
|
100
|
+
const [precision, scale] = baseType.match(/\d+/g);
|
|
101
|
+
field.precision = parseInt(precision);
|
|
102
|
+
field.scale = parseInt(scale);
|
|
103
|
+
}
|
|
104
|
+
else if (baseType == 'real') {
|
|
105
|
+
field.type = AdminForthTypes.FLOAT;
|
|
106
|
+
field._underlineType = 'real';
|
|
107
|
+
}
|
|
108
|
+
else if (baseType.includes('date') || baseType.includes('time')) {
|
|
109
|
+
field.type = AdminForthTypes.DATETIME;
|
|
110
|
+
field._underlineType = 'timestamp';
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
field.type = 'unknown';
|
|
114
|
+
}
|
|
115
|
+
field._baseTypeDebug = baseType;
|
|
116
|
+
field.primaryKey = row.pk == 1;
|
|
117
|
+
field.default = row.dflt_value;
|
|
118
|
+
field.required = row.notnull && !row.dflt_value;
|
|
119
|
+
fieldTypes[row.name] = field;
|
|
120
|
+
});
|
|
121
|
+
return fieldTypes;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
getFieldValue(field, value) {
|
|
125
|
+
if (field.type == AdminForthTypes.DATETIME) {
|
|
126
|
+
if (!value) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
if (field._underlineType == 'timestamp' || field._underlineType == 'int') {
|
|
130
|
+
return dayjs.unix(+value).toISOString();
|
|
131
|
+
}
|
|
132
|
+
else if (field._underlineType == 'varchar') {
|
|
133
|
+
return dayjs.unix(+value).toISOString();
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
throw new Error(`AdminForth does not support row type: ${field._underlineType} for timestamps, use VARCHAR (with iso strings) or TIMESTAMP/INT (with unix timestamps)`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
getPrimaryKey(resource) {
|
|
142
|
+
for (const col of resource.dataSourceColumns) {
|
|
143
|
+
if (col.primaryKey) {
|
|
144
|
+
return col.name;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
getRecordByPrimaryKey(resource, key) {
|
|
149
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
150
|
+
const tableName = resource.table;
|
|
151
|
+
const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
|
|
152
|
+
const stmt = yield this.db.query(`SELECT ${columns} FROM ${tableName} WHERE ${this.getPrimaryKey(resource)} = $1`, [key]);
|
|
153
|
+
const row = stmt.rows[0];
|
|
154
|
+
if (!row) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const newRow = {};
|
|
158
|
+
for (const [key_1, value] of Object.entries(row)) {
|
|
159
|
+
newRow[key_1] = this.getFieldValue(resource.dataSourceColumns.find((col_1) => col_1.name == key_1), value);
|
|
160
|
+
}
|
|
161
|
+
return newRow;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
setFieldValue(field, value) {
|
|
165
|
+
if (field.type == AdminForthTypes.DATETIME) {
|
|
166
|
+
if (!value) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
if (field._underlineType == 'timestamp' || field._underlineType == 'int') {
|
|
170
|
+
// value is iso string now, convert to unix timestamp
|
|
171
|
+
return dayjs(value).unix();
|
|
172
|
+
}
|
|
173
|
+
else if (field._underlineType == 'varchar') {
|
|
174
|
+
// value is iso string now, convert to unix timestamp
|
|
175
|
+
return dayjs(value).toISOString();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (field.type == AdminForthTypes.BOOLEAN) {
|
|
179
|
+
return value ? 1 : 0;
|
|
180
|
+
}
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
getData(_a) {
|
|
184
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, limit, offset, sort, filters }) {
|
|
185
|
+
const columns = resource.dataSourceColumns.filter(c => !c.virtual).map((col) => col.name).join(', ');
|
|
186
|
+
const tableName = resource.table;
|
|
187
|
+
for (const filter of filters) {
|
|
188
|
+
if (!this.OperatorsMap[filter.operator]) {
|
|
189
|
+
throw new Error(`Operator ${filter.operator} is not allowed`);
|
|
190
|
+
}
|
|
191
|
+
if (!resource.dataSourceColumns.some((col) => col.name == filter.field)) {
|
|
192
|
+
throw new Error(`Field ${filter.field} is not in resource ${resource.resourceId}. Available fields: ${resource.dataSourceColumns.map((col) => col.name).join(', ')}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
let totalCounter = 1;
|
|
196
|
+
const where = filters.length ? `WHERE ${filters.map((f, i) => {
|
|
197
|
+
let placeholder = '$' + (totalCounter);
|
|
198
|
+
let field = f.field;
|
|
199
|
+
let operator = this.OperatorsMap[f.operator];
|
|
200
|
+
if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
|
|
201
|
+
placeholder = `(${f.value.map((_, i) => `$${totalCounter + i}`).join(', ')})`;
|
|
202
|
+
totalCounter += f.value.length;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
totalCounter += 1;
|
|
206
|
+
}
|
|
207
|
+
return `${field} ${operator} ${placeholder}`;
|
|
208
|
+
}).join(' AND ')}` : '';
|
|
209
|
+
const filterValues = [];
|
|
210
|
+
filters.length ? filters.forEach((f) => {
|
|
211
|
+
// for arrays do set in map
|
|
212
|
+
let v;
|
|
213
|
+
if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
|
|
214
|
+
v = f.value.map((val) => this.setFieldValue(resource.dataSourceColumns.find((col) => col.name == f.field), val));
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
v = this.setFieldValue(resource.dataSourceColumns.find((col) => col.name == f.field), f.value);
|
|
218
|
+
}
|
|
219
|
+
if (f.operator == AdminForthFilterOperators.LIKE || f.operator == AdminForthFilterOperators.ILIKE) {
|
|
220
|
+
filterValues.push(`%${v}%`);
|
|
221
|
+
}
|
|
222
|
+
else if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
|
|
223
|
+
filterValues.push(...v);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
filterValues.push(v);
|
|
227
|
+
}
|
|
228
|
+
}) : [];
|
|
229
|
+
const limitOffset = `LIMIT $${totalCounter} OFFSET $${totalCounter + 1}`;
|
|
230
|
+
const d = [...filterValues, limit, offset];
|
|
231
|
+
const orderBy = sort.length ? `ORDER BY ${sort.map((s) => `${s.field} ${this.SortDirectionsMap[s.direction]}`).join(', ')}` : '';
|
|
232
|
+
const stmt = yield this.db.query(`SELECT ${columns} FROM ${tableName} ${where} ${orderBy} ${limitOffset}`, d);
|
|
233
|
+
const rows = stmt.rows;
|
|
234
|
+
const total = (yield this.db.query(`SELECT COUNT(*) FROM ${tableName} ${where}`, filterValues)).rows[0].count;
|
|
235
|
+
// run all fields via getFieldValue
|
|
236
|
+
return {
|
|
237
|
+
data: rows.map((row) => {
|
|
238
|
+
const newRow = {};
|
|
239
|
+
for (const [key, value] of Object.entries(row)) {
|
|
240
|
+
newRow[key] = this.getFieldValue(resource.dataSourceColumns.find((col) => col.name == key), value);
|
|
241
|
+
}
|
|
242
|
+
return newRow;
|
|
243
|
+
}),
|
|
244
|
+
total,
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
getMinMaxForColumns(_a) {
|
|
249
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, columns }) {
|
|
250
|
+
const tableName = resource.table;
|
|
251
|
+
const result = {};
|
|
252
|
+
yield Promise.all(columns.map((col) => __awaiter(this, void 0, void 0, function* () {
|
|
253
|
+
const stmt = yield this.db.query(`SELECT MIN(${col.name}) as min, MAX(${col.name}) as max FROM ${tableName}`);
|
|
254
|
+
const { min, max } = stmt.rows[0];
|
|
255
|
+
result[col.name] = {
|
|
256
|
+
min: this.getFieldValue(col, min),
|
|
257
|
+
max: this.getFieldValue(col, max),
|
|
258
|
+
};
|
|
259
|
+
})));
|
|
260
|
+
return result;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
createRecord(_a) {
|
|
264
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, record }) {
|
|
265
|
+
const tableName = resource.table;
|
|
266
|
+
const columns = Object.keys(record);
|
|
267
|
+
const placeholders = columns.map((_, i) => `$${i + 1}`).join(', ');
|
|
268
|
+
const values = columns.map((colName) => {
|
|
269
|
+
const col = resource.dataSourceColumns.find((col) => col.name == colName);
|
|
270
|
+
if (col) {
|
|
271
|
+
return this.setFieldValue(col, record[colName]);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
return record[colName];
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
yield this.db.query(`INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`, values);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
updateRecord(_a) {
|
|
281
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, recordId, record, newValues }) {
|
|
282
|
+
const values = [...Object.values(newValues), recordId];
|
|
283
|
+
const columnsWithPlaceholders = Object.keys(newValues).map((col, i) => `${col} = $${i + 1}`).join(', ');
|
|
284
|
+
yield this.db.query(`UPDATE ${resource.table} SET ${columnsWithPlaceholders} WHERE ${this.getPrimaryKey(resource)} = $${values.length}`, values);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
deleteRecord(_a) {
|
|
288
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, recordId }) {
|
|
289
|
+
yield this.db.query(`DELETE FROM ${resource.table} WHERE ${this.getPrimaryKey(resource)} = $1`, [recordId]);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
close() {
|
|
293
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
294
|
+
yield this.db.end();
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
export default PostgresConnector;
|