adminforth 1.2.99 → 1.2.100
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.ts +6 -5
- package/dataConnectors/baseConnector.ts +65 -7
- package/dataConnectors/clickhouse.ts +1 -0
- package/dataConnectors/mongo.ts +3 -4
- package/dataConnectors/sqlite.ts +7 -0
- package/dist/auth.js +6 -6
- package/dist/dataConnectors/baseConnector.js +59 -10
- package/dist/dataConnectors/clickhouse.js +1 -0
- package/dist/dataConnectors/mongo.js +3 -4
- package/dist/dataConnectors/sqlite.js +10 -0
- package/dist/index.js +50 -34
- package/dist/modules/configValidator.js +25 -16
- package/dist/modules/operationalResource.js +25 -25
- package/dist/modules/restApi.js +106 -96
- package/dist/modules/utils.js +16 -0
- package/index.ts +68 -43
- package/modules/configValidator.ts +26 -21
- package/modules/operationalResource.ts +39 -45
- package/modules/restApi.ts +91 -80
- package/modules/utils.ts +20 -0
- package/package.json +3 -2
- package/spa/src/App.vue +12 -9
- package/spa/src/components/Filters.vue +1 -1
- package/spa/src/components/ResourceForm.vue +7 -4
- package/spa/src/components/ResourceListTable.vue +124 -112
- package/spa/src/components/SkeleteLoader.vue +4 -4
- package/spa/src/components/ValueRenderer.vue +1 -1
- package/spa/src/router/index.ts +0 -8
- package/spa/src/spa_types/core.ts +1 -0
- package/spa/src/stores/filters.ts +3 -2
- package/spa/src/views/ListView.vue +16 -25
- package/spa/src/views/LoginView.vue +26 -9
- package/types/AdminForthConfig.ts +54 -33
package/auth.ts
CHANGED
|
@@ -88,12 +88,13 @@ class AdminForthAuth {
|
|
|
88
88
|
console.error(`Invalid token type during verification: ${t}, must be ${mustHaveType}`);
|
|
89
89
|
return null;
|
|
90
90
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
const dbUser = await this.adminforth.getUserByPk(pk);
|
|
92
|
+
if (!dbUser) {
|
|
93
|
+
console.error(`User with pk ${pk} not found in database`);
|
|
94
|
+
// will logout user which was deleted
|
|
95
|
+
return null;
|
|
96
96
|
}
|
|
97
|
+
decoded.dbUser = dbUser;
|
|
97
98
|
return decoded;
|
|
98
99
|
}
|
|
99
100
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { get } from "http";
|
|
2
2
|
import { AdminForthResource, IAdminForthDataSourceConnectorBase, AdminForthSortDirections, AdminForthFilterOperators, AdminForthResourceColumn, IAdminForthSort, IAdminForthFilter } from "../types/AdminForthConfig.js";
|
|
3
|
+
import { suggestIfTypo } from "../modules/utils.js";
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
export default class AdminForthBaseConnector implements IAdminForthDataSourceConnectorBase {
|
|
@@ -56,14 +57,66 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
|
|
|
56
57
|
throw new Error('Method not implemented.');
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
async checkUnique(resource: AdminForthResource, column: AdminForthResourceColumn, value: any) {
|
|
61
|
+
process.env.HEAVY_DEBUG && console.log('☝️🪲🪲🪲🪲 checkUnique|||', column, value);
|
|
62
|
+
const existingRecord = await this.getData({
|
|
63
|
+
resource,
|
|
64
|
+
filters: [{ field: column.name, operator: AdminForthFilterOperators.EQ, value }],
|
|
65
|
+
limit: 1,
|
|
66
|
+
sort: [],
|
|
67
|
+
offset: 0,
|
|
68
|
+
getTotals: false
|
|
69
|
+
});
|
|
70
|
+
process.env.HEAVY_DEBUG && console.log('☝️🪲🪲🪲🪲 existingRecord|||', existingRecord);
|
|
71
|
+
|
|
72
|
+
return existingRecord.data.length > 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async createRecord({ resource, record, adminUser }: {
|
|
76
|
+
resource: AdminForthResource; record: any; adminUser: any;
|
|
77
|
+
}): Promise<{ error?: string; ok: boolean; createdRecord?: any; }> {
|
|
60
78
|
// transform value using setFieldValue and call createRecordOriginalValues
|
|
61
|
-
const
|
|
79
|
+
const filledRecord = {...record};
|
|
80
|
+
const recordWithOriginalValues = {...record};
|
|
81
|
+
|
|
62
82
|
for (const col of resource.dataSourceColumns) {
|
|
63
|
-
|
|
83
|
+
if (col.fillOnCreate) {
|
|
84
|
+
if (filledRecord[col.name] === undefined) {
|
|
85
|
+
filledRecord[col.name] = col.fillOnCreate({
|
|
86
|
+
initialRecord: record,
|
|
87
|
+
adminUser
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
recordWithOriginalValues[col.name] = this.setFieldValue(col, filledRecord[col.name]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
let error: string | null = null;
|
|
96
|
+
await Promise.all(
|
|
97
|
+
resource.dataSourceColumns.map(async (col) => {
|
|
98
|
+
|
|
99
|
+
if (col.isUnique && !col.virtual && !error) {
|
|
100
|
+
|
|
101
|
+
const exists = await this.checkUnique(resource, col, recordWithOriginalValues[col.name]);
|
|
102
|
+
if (exists) {
|
|
103
|
+
error = `Record with ${col.name} ${recordWithOriginalValues[col.name]} already exists`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 error', error);
|
|
109
|
+
if (error) {
|
|
110
|
+
return { error, ok: false };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record', recordWithOriginalValues);
|
|
114
|
+
await this.createRecordOriginalValues({ resource, record: recordWithOriginalValues });
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
ok: true,
|
|
118
|
+
createdRecord: recordWithOriginalValues,
|
|
64
119
|
}
|
|
65
|
-
process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record', newRecord);
|
|
66
|
-
return this.createRecordOriginalValues({ resource, record: newRecord });
|
|
67
120
|
}
|
|
68
121
|
|
|
69
122
|
updateRecord({ resource, recordId, newValues }: { resource: AdminForthResource; recordId: string; newValues: any; }): Promise<void> {
|
|
@@ -85,10 +138,15 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
|
|
|
85
138
|
}): Promise<{ data: any[], total: number }> {
|
|
86
139
|
if (filters) {
|
|
87
140
|
filters.map((f) => {
|
|
141
|
+
const fieldObj = resource.dataSourceColumns.find((col) => col.name == f.field);
|
|
142
|
+
if (!fieldObj) {
|
|
143
|
+
const similar = suggestIfTypo(resource.dataSourceColumns.map((col) => col.name), f.field);
|
|
144
|
+
throw new Error(`Field '${f.field}' not found in resource '${resource.resourceId}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
|
|
145
|
+
}
|
|
88
146
|
if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
|
|
89
|
-
f.value = f.value.map((val) => this.setFieldValue(
|
|
147
|
+
f.value = f.value.map((val) => this.setFieldValue(fieldObj, val));
|
|
90
148
|
} else {
|
|
91
|
-
f.value = this.setFieldValue(
|
|
149
|
+
f.value = this.setFieldValue(fieldObj, f.value);
|
|
92
150
|
}
|
|
93
151
|
});
|
|
94
152
|
}
|
|
@@ -220,6 +220,7 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
|
|
|
220
220
|
sort: { field: string, direction: AdminForthSortDirections }[],
|
|
221
221
|
filters: { field: string, operator: AdminForthFilterOperators, value: any }[],
|
|
222
222
|
}): Promise<any[]> {
|
|
223
|
+
console.log('getDataWithOriginalTypes', resource, limit, offset, sort, filters);
|
|
223
224
|
const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
|
|
224
225
|
const tableName = resource.table;
|
|
225
226
|
|
package/dataConnectors/mongo.ts
CHANGED
|
@@ -107,13 +107,12 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
|
|
|
107
107
|
return value;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
async genQuery({
|
|
111
|
-
const collection = this.db.db().collection(resource.table);
|
|
110
|
+
async genQuery({ filters }) {
|
|
112
111
|
const query = {};
|
|
113
112
|
for (const filter of filters) {
|
|
114
113
|
query[filter.field] = this.OperatorsMap[filter.operator](filter.value);
|
|
115
114
|
}
|
|
116
|
-
return
|
|
115
|
+
return query;
|
|
117
116
|
}
|
|
118
117
|
|
|
119
118
|
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }:
|
|
@@ -130,7 +129,7 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
|
|
|
130
129
|
const tableName = resource.table;
|
|
131
130
|
|
|
132
131
|
const collection = this.db.db().collection(tableName);
|
|
133
|
-
const query = this.genQuery({
|
|
132
|
+
const query = await this.genQuery({ filters });
|
|
134
133
|
|
|
135
134
|
const sortArray: any[] = sort.map((s) => {
|
|
136
135
|
return [s.field, this.SortDirectionsMap[s.direction]];
|
package/dataConnectors/sqlite.ts
CHANGED
|
@@ -47,6 +47,9 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
|
|
|
47
47
|
} else if (baseType == 'boolean') {
|
|
48
48
|
field.type = AdminForthDataTypes.BOOLEAN;
|
|
49
49
|
field._underlineType = 'boolean';
|
|
50
|
+
} else if (baseType == 'datetime') {
|
|
51
|
+
field.type = AdminForthDataTypes.DATETIME;
|
|
52
|
+
field._underlineType = 'datetime';
|
|
50
53
|
} else {
|
|
51
54
|
field.type = 'unknown'
|
|
52
55
|
}
|
|
@@ -68,6 +71,8 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
|
|
|
68
71
|
return dayjs.unix(+value).toISOString();
|
|
69
72
|
} else if (field._underlineType == 'varchar') {
|
|
70
73
|
return dayjs(value).toISOString();
|
|
74
|
+
} else if (field._underlineType == 'datetime') {
|
|
75
|
+
return dayjs(value).toISOString();
|
|
71
76
|
} else {
|
|
72
77
|
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}"`);
|
|
73
78
|
}
|
|
@@ -102,6 +107,8 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
|
|
|
102
107
|
} else if (field._underlineType == 'varchar') {
|
|
103
108
|
// value is iso string now, convert to unix timestamp
|
|
104
109
|
return dayjs(value).toISOString();
|
|
110
|
+
} else {
|
|
111
|
+
return value;
|
|
105
112
|
}
|
|
106
113
|
} else if (field.type == AdminForthDataTypes.BOOLEAN) {
|
|
107
114
|
return value ? 1 : 0;
|
package/dist/auth.js
CHANGED
|
@@ -80,13 +80,13 @@ class AdminForthAuth {
|
|
|
80
80
|
console.error(`Invalid token type during verification: ${t}, must be ${mustHaveType}`);
|
|
81
81
|
return null;
|
|
82
82
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
decoded.dbUser = dbUser;
|
|
83
|
+
const dbUser = yield this.adminforth.getUserByPk(pk);
|
|
84
|
+
if (!dbUser) {
|
|
85
|
+
console.error(`User with pk ${pk} not found in database`);
|
|
86
|
+
// will logout user which was deleted
|
|
87
|
+
return null;
|
|
89
88
|
}
|
|
89
|
+
decoded.dbUser = dbUser;
|
|
90
90
|
return decoded;
|
|
91
91
|
});
|
|
92
92
|
}
|
|
@@ -8,6 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { AdminForthFilterOperators } from "../types/AdminForthConfig.js";
|
|
11
|
+
import { suggestIfTypo } from "../modules/utils.js";
|
|
11
12
|
export default class AdminForthBaseConnector {
|
|
12
13
|
getPrimaryKey(resource) {
|
|
13
14
|
for (const col of resource.dataSourceColumns) {
|
|
@@ -49,14 +50,57 @@ export default class AdminForthBaseConnector {
|
|
|
49
50
|
createRecordOriginalValues({ resource, record }) {
|
|
50
51
|
throw new Error('Method not implemented.');
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
checkUnique(resource, column, value) {
|
|
54
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
process.env.HEAVY_DEBUG && console.log('☝️🪲🪲🪲🪲 checkUnique|||', column, value);
|
|
56
|
+
const existingRecord = yield this.getData({
|
|
57
|
+
resource,
|
|
58
|
+
filters: [{ field: column.name, operator: AdminForthFilterOperators.EQ, value }],
|
|
59
|
+
limit: 1,
|
|
60
|
+
sort: [],
|
|
61
|
+
offset: 0,
|
|
62
|
+
getTotals: false
|
|
63
|
+
});
|
|
64
|
+
process.env.HEAVY_DEBUG && console.log('☝️🪲🪲🪲🪲 existingRecord|||', existingRecord);
|
|
65
|
+
return existingRecord.data.length > 0;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
createRecord(_a) {
|
|
69
|
+
return __awaiter(this, arguments, void 0, function* ({ resource, record, adminUser }) {
|
|
70
|
+
// transform value using setFieldValue and call createRecordOriginalValues
|
|
71
|
+
const filledRecord = Object.assign({}, record);
|
|
72
|
+
const recordWithOriginalValues = Object.assign({}, record);
|
|
73
|
+
for (const col of resource.dataSourceColumns) {
|
|
74
|
+
if (col.fillOnCreate) {
|
|
75
|
+
if (filledRecord[col.name] === undefined) {
|
|
76
|
+
filledRecord[col.name] = col.fillOnCreate({
|
|
77
|
+
initialRecord: record,
|
|
78
|
+
adminUser
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
recordWithOriginalValues[col.name] = this.setFieldValue(col, filledRecord[col.name]);
|
|
83
|
+
}
|
|
84
|
+
let error = null;
|
|
85
|
+
yield Promise.all(resource.dataSourceColumns.map((col) => __awaiter(this, void 0, void 0, function* () {
|
|
86
|
+
if (col.isUnique && !col.virtual && !error) {
|
|
87
|
+
const exists = yield this.checkUnique(resource, col, recordWithOriginalValues[col.name]);
|
|
88
|
+
if (exists) {
|
|
89
|
+
error = `Record with ${col.name} ${recordWithOriginalValues[col.name]} already exists`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
})));
|
|
93
|
+
process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 error', error);
|
|
94
|
+
if (error) {
|
|
95
|
+
return { error, ok: false };
|
|
96
|
+
}
|
|
97
|
+
process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record', recordWithOriginalValues);
|
|
98
|
+
yield this.createRecordOriginalValues({ resource, record: recordWithOriginalValues });
|
|
99
|
+
return {
|
|
100
|
+
ok: true,
|
|
101
|
+
createdRecord: recordWithOriginalValues,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
60
104
|
}
|
|
61
105
|
updateRecord({ resource, recordId, newValues }) {
|
|
62
106
|
throw new Error('Method not implemented.');
|
|
@@ -68,11 +112,16 @@ export default class AdminForthBaseConnector {
|
|
|
68
112
|
return __awaiter(this, arguments, void 0, function* ({ resource, limit, offset, sort, filters, getTotals }) {
|
|
69
113
|
if (filters) {
|
|
70
114
|
filters.map((f) => {
|
|
115
|
+
const fieldObj = resource.dataSourceColumns.find((col) => col.name == f.field);
|
|
116
|
+
if (!fieldObj) {
|
|
117
|
+
const similar = suggestIfTypo(resource.dataSourceColumns.map((col) => col.name), f.field);
|
|
118
|
+
throw new Error(`Field '${f.field}' not found in resource '${resource.resourceId}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
|
|
119
|
+
}
|
|
71
120
|
if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
|
|
72
|
-
f.value = f.value.map((val) => this.setFieldValue(
|
|
121
|
+
f.value = f.value.map((val) => this.setFieldValue(fieldObj, val));
|
|
73
122
|
}
|
|
74
123
|
else {
|
|
75
|
-
f.value = this.setFieldValue(
|
|
124
|
+
f.value = this.setFieldValue(fieldObj, f.value);
|
|
76
125
|
}
|
|
77
126
|
});
|
|
78
127
|
}
|
|
@@ -217,6 +217,7 @@ class ClickhouseConnector extends AdminForthBaseConnector {
|
|
|
217
217
|
}
|
|
218
218
|
getDataWithOriginalTypes(_a) {
|
|
219
219
|
return __awaiter(this, arguments, void 0, function* ({ resource, limit, offset, sort, filters }) {
|
|
220
|
+
console.log('getDataWithOriginalTypes', resource, limit, offset, sort, filters);
|
|
220
221
|
const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
|
|
221
222
|
const tableName = resource.table;
|
|
222
223
|
const where = this.whereClause(resource, filters);
|
|
@@ -109,13 +109,12 @@ class MongoConnector extends AdminForthBaseConnector {
|
|
|
109
109
|
return value;
|
|
110
110
|
}
|
|
111
111
|
genQuery(_a) {
|
|
112
|
-
return __awaiter(this, arguments, void 0, function* ({
|
|
113
|
-
const collection = this.db.db().collection(resource.table);
|
|
112
|
+
return __awaiter(this, arguments, void 0, function* ({ filters }) {
|
|
114
113
|
const query = {};
|
|
115
114
|
for (const filter of filters) {
|
|
116
115
|
query[filter.field] = this.OperatorsMap[filter.operator](filter.value);
|
|
117
116
|
}
|
|
118
|
-
return
|
|
117
|
+
return query;
|
|
119
118
|
});
|
|
120
119
|
}
|
|
121
120
|
getDataWithOriginalTypes(_a) {
|
|
@@ -123,7 +122,7 @@ class MongoConnector extends AdminForthBaseConnector {
|
|
|
123
122
|
// const columns = resource.dataSourceColumns.filter(c=> !c.virtual).map((col) => col.name).join(', ');
|
|
124
123
|
const tableName = resource.table;
|
|
125
124
|
const collection = this.db.db().collection(tableName);
|
|
126
|
-
const query = this.genQuery({
|
|
125
|
+
const query = yield this.genQuery({ filters });
|
|
127
126
|
const sortArray = sort.map((s) => {
|
|
128
127
|
return [s.field, this.SortDirectionsMap[s.direction]];
|
|
129
128
|
});
|
|
@@ -75,6 +75,10 @@ class SQLiteConnector extends AdminForthBaseConnector {
|
|
|
75
75
|
field.type = AdminForthDataTypes.BOOLEAN;
|
|
76
76
|
field._underlineType = 'boolean';
|
|
77
77
|
}
|
|
78
|
+
else if (baseType == 'datetime') {
|
|
79
|
+
field.type = AdminForthDataTypes.DATETIME;
|
|
80
|
+
field._underlineType = 'datetime';
|
|
81
|
+
}
|
|
78
82
|
else {
|
|
79
83
|
field.type = 'unknown';
|
|
80
84
|
}
|
|
@@ -98,6 +102,9 @@ class SQLiteConnector extends AdminForthBaseConnector {
|
|
|
98
102
|
else if (field._underlineType == 'varchar') {
|
|
99
103
|
return dayjs(value).toISOString();
|
|
100
104
|
}
|
|
105
|
+
else if (field._underlineType == 'datetime') {
|
|
106
|
+
return dayjs(value).toISOString();
|
|
107
|
+
}
|
|
101
108
|
else {
|
|
102
109
|
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}"`);
|
|
103
110
|
}
|
|
@@ -134,6 +141,9 @@ class SQLiteConnector extends AdminForthBaseConnector {
|
|
|
134
141
|
// value is iso string now, convert to unix timestamp
|
|
135
142
|
return dayjs(value).toISOString();
|
|
136
143
|
}
|
|
144
|
+
else {
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
137
147
|
}
|
|
138
148
|
else if (field.type == AdminForthDataTypes.BOOLEAN) {
|
|
139
149
|
return value ? 1 : 0;
|
package/dist/index.js
CHANGED
|
@@ -19,16 +19,18 @@ import PostgresConnector from './dataConnectors/postgres.js';
|
|
|
19
19
|
import SQLiteConnector from './dataConnectors/sqlite.js';
|
|
20
20
|
import CodeInjector from './modules/codeInjector.js';
|
|
21
21
|
import ExpressServer from './servers/express.js';
|
|
22
|
-
import { ADMINFORTH_VERSION, listify } from './modules/utils.js';
|
|
22
|
+
import { ADMINFORTH_VERSION, listify, suggestIfTypo } from './modules/utils.js';
|
|
23
23
|
import { AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages, } from './types/AdminForthConfig.js';
|
|
24
24
|
import AdminForthPlugin from './basePlugin.js';
|
|
25
25
|
import ConfigValidator from './modules/configValidator.js';
|
|
26
26
|
import AdminForthRestAPI, { interpretResource } from './modules/restApi.js';
|
|
27
27
|
import ClickhouseConnector from './dataConnectors/clickhouse.js';
|
|
28
|
+
import OperationalResource from './modules/operationalResource.js';
|
|
28
29
|
// exports
|
|
29
30
|
export * from './types/AdminForthConfig.js';
|
|
30
31
|
export { interpretResource };
|
|
31
32
|
export { AdminForthPlugin };
|
|
33
|
+
export { suggestIfTypo };
|
|
32
34
|
class AdminForth {
|
|
33
35
|
constructor(config) {
|
|
34
36
|
_AdminForth_defaultConfig.set(this, {
|
|
@@ -85,20 +87,22 @@ class AdminForth {
|
|
|
85
87
|
this.config.dataSources.forEach((ds) => {
|
|
86
88
|
const dbType = ds.url.split(':')[0];
|
|
87
89
|
if (!this.config.databaseConnectors[dbType]) {
|
|
88
|
-
throw new Error(`Database type ${dbType} is not supported, consider using
|
|
90
|
+
throw new Error(`Database type '${dbType}' is not supported, consider using one of ${Object.keys(this.connectorClasses).join(', ')} or create your own data-source connector`);
|
|
89
91
|
}
|
|
90
92
|
this.connectors[ds.id] = new this.config.databaseConnectors[dbType]({ url: ds.url });
|
|
91
93
|
});
|
|
92
94
|
yield Promise.all(this.config.resources.map((res) => __awaiter(this, void 0, void 0, function* () {
|
|
93
95
|
if (!this.connectors[res.dataSource]) {
|
|
94
|
-
|
|
96
|
+
const similar = suggestIfTypo(Object.keys(this.connectors), res.dataSource);
|
|
97
|
+
throw new Error(`Resource '${res.table}' refers to unknown dataSource '${res.dataSource}' ${similar
|
|
98
|
+
? `. Did you mean '${similar}'?` : 'Available dataSources: ' + Object.keys(this.connectors).join(', ')}`);
|
|
95
99
|
}
|
|
96
100
|
const fieldTypes = yield this.connectors[res.dataSource].discoverFields(res);
|
|
97
101
|
if (fieldTypes !== null && !Object.keys(fieldTypes).length) {
|
|
98
102
|
throw new Error(`Table '${res.table}' (In resource '${res.resourceId}') has no fields or does not exist`);
|
|
99
103
|
}
|
|
100
104
|
if (fieldTypes === null) {
|
|
101
|
-
console.error(
|
|
105
|
+
console.error(`⛔ DataSource ${res.dataSource} was not able to perform field discovery. It will not work properly`);
|
|
102
106
|
return;
|
|
103
107
|
}
|
|
104
108
|
if (!res.columns) {
|
|
@@ -106,7 +110,8 @@ class AdminForth {
|
|
|
106
110
|
}
|
|
107
111
|
res.columns.forEach((col, i) => {
|
|
108
112
|
if (!fieldTypes[col.name] && !col.virtual) {
|
|
109
|
-
|
|
113
|
+
const similar = suggestIfTypo(Object.keys(fieldTypes), col.name);
|
|
114
|
+
throw new Error(`Resource '${res.table}' has no column '${col.name}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
|
|
110
115
|
}
|
|
111
116
|
// first find discovered values, but allow override
|
|
112
117
|
res.columns[i] = Object.assign(Object.assign({}, fieldTypes[col.name]), col);
|
|
@@ -118,6 +123,10 @@ class AdminForth {
|
|
|
118
123
|
}
|
|
119
124
|
})));
|
|
120
125
|
this.statuses.dbDiscover = 'done';
|
|
126
|
+
this.operationalResources = {};
|
|
127
|
+
this.config.resources.forEach((resource) => {
|
|
128
|
+
this.operationalResources[resource.resourceId] = new OperationalResource(this.connectors[resource.dataSource], resource);
|
|
129
|
+
});
|
|
121
130
|
// console.log('⚙️⚙️⚙️ Database discovery done', JSON.stringify(this.config.resources, null, 2));
|
|
122
131
|
});
|
|
123
132
|
}
|
|
@@ -128,9 +137,11 @@ class AdminForth {
|
|
|
128
137
|
}
|
|
129
138
|
getUserByPk(pk) {
|
|
130
139
|
return __awaiter(this, void 0, void 0, function* () {
|
|
131
|
-
const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.
|
|
140
|
+
const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.usersResourceId);
|
|
132
141
|
if (!resource) {
|
|
133
|
-
|
|
142
|
+
const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId), this.config.auth.usersResourceId);
|
|
143
|
+
throw new Error(`No resource with ${this.config.auth.usersResourceId} found. ${similar ?
|
|
144
|
+
`Did you mean '${similar}' in config.auth.usersResourceId?` : 'Please set correct resource in config.auth.usersResourceId'}`);
|
|
134
145
|
}
|
|
135
146
|
const users = yield this.connectors[resource.dataSource].getData({
|
|
136
147
|
resource,
|
|
@@ -148,39 +159,21 @@ class AdminForth {
|
|
|
148
159
|
return __awaiter(this, arguments, void 0, function* ({ resource, record, adminUser }) {
|
|
149
160
|
var _c, _d, _e, _f, _g;
|
|
150
161
|
for (const column of resource.columns) {
|
|
151
|
-
|
|
152
|
-
if (record[column.name] === undefined) {
|
|
153
|
-
record[column.name] = column.fillOnCreate({
|
|
154
|
-
initialRecord: record, adminUser
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
}
|
|
162
|
+
// TODO: assuming specifity for AdminForthResourcePages.create better to move it to api for this button
|
|
158
163
|
if (((_c = column.required) === null || _c === void 0 ? void 0 : _c.create) &&
|
|
159
164
|
record[column.name] === undefined &&
|
|
160
165
|
column.showIn.includes(AdminForthResourcePages.create)) {
|
|
161
|
-
return { error: `Column '${column.name}' is required
|
|
162
|
-
}
|
|
163
|
-
if (column.isUnique) {
|
|
164
|
-
const existingRecord = yield this.connectors[resource.dataSource].getData({
|
|
165
|
-
resource,
|
|
166
|
-
filters: [{ field: column.name, operator: AdminForthFilterOperators.EQ, value: record[column.name] }],
|
|
167
|
-
limit: 1,
|
|
168
|
-
sort: [],
|
|
169
|
-
offset: 0
|
|
170
|
-
});
|
|
171
|
-
if (existingRecord.data.length > 0) {
|
|
172
|
-
return { error: `Record with ${column.name} ${record[column.name]} already exists` };
|
|
173
|
-
}
|
|
166
|
+
return { error: `Column '${column.name}' is required`, ok: false };
|
|
174
167
|
}
|
|
175
168
|
}
|
|
176
169
|
// execute hook if needed
|
|
177
170
|
for (const hook of listify((_e = (_d = resource.hooks) === null || _d === void 0 ? void 0 : _d.create) === null || _e === void 0 ? void 0 : _e.beforeSave)) {
|
|
178
|
-
const resp = yield hook({ resource, record, adminUser });
|
|
171
|
+
const resp = yield hook({ recordId: undefined, resource, record, adminUser });
|
|
179
172
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
180
173
|
throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
|
|
181
174
|
}
|
|
182
175
|
if (resp.error) {
|
|
183
|
-
return { error: resp.error };
|
|
176
|
+
return { error: resp.error, ok: false };
|
|
184
177
|
}
|
|
185
178
|
}
|
|
186
179
|
// remove virtual columns from record
|
|
@@ -191,23 +184,46 @@ class AdminForth {
|
|
|
191
184
|
}
|
|
192
185
|
const connector = this.connectors[resource.dataSource];
|
|
193
186
|
process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record createResourceRecord', record);
|
|
194
|
-
yield connector.createRecord({ resource, record });
|
|
187
|
+
const { ok, error, createdRecord } = yield connector.createRecord({ resource, record, adminUser });
|
|
188
|
+
if (!ok) {
|
|
189
|
+
return { ok, error };
|
|
190
|
+
}
|
|
191
|
+
const primaryKey = record[resource.columns.find((col) => col.primaryKey).name];
|
|
195
192
|
// execute hook if needed
|
|
196
193
|
for (const hook of listify((_g = (_f = resource.hooks) === null || _f === void 0 ? void 0 : _f.create) === null || _g === void 0 ? void 0 : _g.afterSave)) {
|
|
197
194
|
console.log('Hook afterSave', hook);
|
|
198
|
-
const resp = yield hook({
|
|
195
|
+
const resp = yield hook({
|
|
196
|
+
recordId: primaryKey,
|
|
197
|
+
resource,
|
|
198
|
+
record: createdRecord,
|
|
199
|
+
adminUser
|
|
200
|
+
});
|
|
199
201
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
200
202
|
throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
|
|
201
203
|
}
|
|
202
204
|
if (resp.error) {
|
|
203
|
-
return { error: resp.error };
|
|
205
|
+
return { error: resp.error, ok: false };
|
|
204
206
|
}
|
|
205
207
|
}
|
|
206
|
-
return { ok
|
|
208
|
+
return { ok, error, createdRecord };
|
|
207
209
|
});
|
|
208
210
|
}
|
|
209
211
|
resource(resourceId) {
|
|
210
|
-
|
|
212
|
+
if (this.statuses.dbDiscover !== 'done') {
|
|
213
|
+
if (this.statuses.dbDiscover === 'running') {
|
|
214
|
+
throw new Error('Database discovery is running. You can\'t use data API while database discovery is not finished.\n' +
|
|
215
|
+
'Consider moving your code to a place where it will be executed after database discovery is already done (after await admin.discoverDatabases())');
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
throw new Error('Database discovery is not yet started. You can\'t use data API before database discovery is done. \n' +
|
|
219
|
+
'Call admin.discoverDatabases() first and await it before using data API');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!this.operationalResources[resourceId]) {
|
|
223
|
+
const closeName = suggestIfTypo(Object.keys(this.operationalResources), resourceId);
|
|
224
|
+
throw new Error(`Resource with id '${resourceId}' not found${closeName ? `. Did you mean '${closeName}'?` : ''}`);
|
|
225
|
+
}
|
|
226
|
+
return this.operationalResources[resourceId];
|
|
211
227
|
}
|
|
212
228
|
setupEndpoints(server) {
|
|
213
229
|
this.restApi.registerEndpoints(server);
|
|
@@ -10,7 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
import { AdminForthResourcePages, AllowedActionsEnum, } from "../types/AdminForthConfig.js";
|
|
11
11
|
import fs from 'fs';
|
|
12
12
|
import path from 'path';
|
|
13
|
-
import { guessLabelFromName } from './utils.js';
|
|
13
|
+
import { guessLabelFromName, suggestIfTypo } from './utils.js';
|
|
14
14
|
import crypto from 'crypto';
|
|
15
15
|
export default class ConfigValidator {
|
|
16
16
|
constructor(adminforth, config) {
|
|
@@ -47,15 +47,6 @@ export default class ConfigValidator {
|
|
|
47
47
|
validateConfig() {
|
|
48
48
|
var _a;
|
|
49
49
|
const errors = [];
|
|
50
|
-
if (this.config.rootUser) {
|
|
51
|
-
if (!this.config.rootUser.username) {
|
|
52
|
-
throw new Error('rootUser.username is required');
|
|
53
|
-
}
|
|
54
|
-
if (!this.config.rootUser.password) {
|
|
55
|
-
throw new Error('rootUser.password is required');
|
|
56
|
-
}
|
|
57
|
-
console.log('\n ☝️☝️☝️ [INSECURE ALERT] config.rootUser is set, please create a new user to login in backoffice and remove config.rootUser from config ASAP when you are in production\n');
|
|
58
|
-
}
|
|
59
50
|
if (!this.config.customization.customComponentsDir) {
|
|
60
51
|
this.config.customization.customComponentsDir = './custom';
|
|
61
52
|
}
|
|
@@ -67,8 +58,12 @@ export default class ConfigValidator {
|
|
|
67
58
|
this.config.customization.customComponentsDir = undefined;
|
|
68
59
|
}
|
|
69
60
|
if (this.config.auth) {
|
|
70
|
-
|
|
71
|
-
|
|
61
|
+
// TODO: remove in future releases
|
|
62
|
+
if (!this.config.auth.usersResourceId && this.config.auth.resourceId) {
|
|
63
|
+
this.config.auth.usersResourceId = this.config.auth.resourceId;
|
|
64
|
+
}
|
|
65
|
+
if (!this.config.auth.usersResourceId) {
|
|
66
|
+
throw new Error('No config.auth.usersResourceId defined');
|
|
72
67
|
}
|
|
73
68
|
if (!this.config.auth.passwordHashField) {
|
|
74
69
|
throw new Error('No config.auth.passwordHashField defined');
|
|
@@ -79,9 +74,10 @@ export default class ConfigValidator {
|
|
|
79
74
|
if (this.config.auth.loginBackgroundImage) {
|
|
80
75
|
errors.push(...this.checkCustomFileExists(this.config.auth.loginBackgroundImage));
|
|
81
76
|
}
|
|
82
|
-
const userResource = this.config.resources.find((res) => res.resourceId === this.config.auth.
|
|
77
|
+
const userResource = this.config.resources.find((res) => res.resourceId === this.config.auth.usersResourceId);
|
|
83
78
|
if (!userResource) {
|
|
84
|
-
|
|
79
|
+
const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId || res.table), this.config.auth.usersResourceId);
|
|
80
|
+
throw new Error(`Resource with id "${this.config.auth.usersResourceId}" not found. ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
85
81
|
}
|
|
86
82
|
if (!this.config.auth.beforeLoginConfirmation) {
|
|
87
83
|
this.config.auth.beforeLoginConfirmation = [];
|
|
@@ -126,6 +122,9 @@ export default class ConfigValidator {
|
|
|
126
122
|
if (this.config.customization.brandLogo) {
|
|
127
123
|
errors.push(...this.checkCustomFileExists(this.config.customization.brandLogo));
|
|
128
124
|
}
|
|
125
|
+
if (this.config.customization.showBrandNameInSidebar === undefined) {
|
|
126
|
+
this.config.customization.showBrandNameInSidebar = true;
|
|
127
|
+
}
|
|
129
128
|
if (this.config.customization.favicon) {
|
|
130
129
|
errors.push(...this.checkCustomFileExists(this.config.customization.favicon));
|
|
131
130
|
}
|
|
@@ -254,7 +253,12 @@ export default class ConfigValidator {
|
|
|
254
253
|
yield Promise.all(selectedIds.map((recordId) => __awaiter(this, void 0, void 0, function* () {
|
|
255
254
|
const record = yield connector.getRecordByPrimaryKey(res, recordId);
|
|
256
255
|
yield Promise.all(res.hooks.delete.beforeSave.map((hook) => __awaiter(this, void 0, void 0, function* () {
|
|
257
|
-
const resp = yield hook({
|
|
256
|
+
const resp = yield hook({
|
|
257
|
+
recordId: recordId,
|
|
258
|
+
resource: res,
|
|
259
|
+
record,
|
|
260
|
+
adminUser,
|
|
261
|
+
});
|
|
258
262
|
if (!error && resp.error) {
|
|
259
263
|
error = resp.error;
|
|
260
264
|
}
|
|
@@ -265,7 +269,12 @@ export default class ConfigValidator {
|
|
|
265
269
|
yield connector.deleteRecord({ resource: res, recordId });
|
|
266
270
|
// call afterDelete hook
|
|
267
271
|
yield Promise.all(res.hooks.delete.afterSave.map((hook) => __awaiter(this, void 0, void 0, function* () {
|
|
268
|
-
yield hook({
|
|
272
|
+
yield hook({
|
|
273
|
+
resource: res,
|
|
274
|
+
record,
|
|
275
|
+
adminUser,
|
|
276
|
+
recordId: recordId
|
|
277
|
+
});
|
|
269
278
|
})));
|
|
270
279
|
})));
|
|
271
280
|
if (error) {
|