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.
Files changed (58) hide show
  1. package/auth.js +66 -0
  2. package/dataConnectors/mongo.js +187 -0
  3. package/dataConnectors/postgres.js +283 -0
  4. package/dataConnectors/sqlite.js +257 -0
  5. package/index.js +722 -0
  6. package/modules/codeInjector.js +313 -0
  7. package/modules/utils.js +12 -0
  8. package/package.json +23 -0
  9. package/servers/express.js +217 -0
  10. package/spa/.eslintrc.cjs +14 -0
  11. package/spa/.vscode/extensions.json +6 -0
  12. package/spa/README.md +39 -0
  13. package/spa/env.d.ts +1 -0
  14. package/spa/index.html +23 -0
  15. package/spa/package-lock.json +4152 -0
  16. package/spa/package.json +40 -0
  17. package/spa/postcss.config.js +6 -0
  18. package/spa/public/favicon.ico +0 -0
  19. package/spa/src/App.vue +172 -0
  20. package/spa/src/assets/base.css +0 -0
  21. package/spa/src/assets/logo.svg +1 -0
  22. package/spa/src/components/AcceptModal.vue +52 -0
  23. package/spa/src/components/Breadcrumbs.vue +40 -0
  24. package/spa/src/components/BreadcrumbsWithButtons.vue +26 -0
  25. package/spa/src/components/CustomDateRangePicker.vue +218 -0
  26. package/spa/src/components/Dropdown.vue +154 -0
  27. package/spa/src/components/Filters.vue +141 -0
  28. package/spa/src/components/HelloWorld.vue +17 -0
  29. package/spa/src/components/MenuLink.vue +25 -0
  30. package/spa/src/components/ResourceForm.vue +198 -0
  31. package/spa/src/components/SingleSkeletLoader.vue +13 -0
  32. package/spa/src/components/ValueRenderer.vue +44 -0
  33. package/spa/src/components/icons/IconCalendar.vue +5 -0
  34. package/spa/src/components/icons/IconCommunity.vue +7 -0
  35. package/spa/src/components/icons/IconDocumentation.vue +7 -0
  36. package/spa/src/components/icons/IconEcosystem.vue +7 -0
  37. package/spa/src/components/icons/IconSupport.vue +7 -0
  38. package/spa/src/components/icons/IconTime.vue +5 -0
  39. package/spa/src/components/icons/IconTooling.vue +19 -0
  40. package/spa/src/index.scss +26 -0
  41. package/spa/src/main.ts +18 -0
  42. package/spa/src/router/index.ts +53 -0
  43. package/spa/src/stores/core.ts +135 -0
  44. package/spa/src/stores/modal.ts +38 -0
  45. package/spa/src/utils.ts +44 -0
  46. package/spa/src/views/CreateView.vue +103 -0
  47. package/spa/src/views/EditView.vue +95 -0
  48. package/spa/src/views/HomeView.vue +8 -0
  49. package/spa/src/views/ListView.vue +466 -0
  50. package/spa/src/views/LoginView.vue +122 -0
  51. package/spa/src/views/ResourceParent.vue +18 -0
  52. package/spa/src/views/ShowView.vue +94 -0
  53. package/spa/tailwind.config.js +12 -0
  54. package/spa/tsconfig.app.json +14 -0
  55. package/spa/tsconfig.json +11 -0
  56. package/spa/tsconfig.node.json +19 -0
  57. package/spa/vite.config.ts +42 -0
  58. 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;