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