adminforth 1.3.54-next.3 → 1.3.54-next.30

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 (135) hide show
  1. package/dist/auth.js +42 -56
  2. package/dist/auth.js.map +1 -0
  3. package/dist/basePlugin.js +1 -0
  4. package/dist/basePlugin.js.map +1 -0
  5. package/dist/dataConnectors/baseConnector.js +108 -122
  6. package/dist/dataConnectors/baseConnector.js.map +1 -0
  7. package/dist/dataConnectors/clickhouse.js +132 -150
  8. package/dist/dataConnectors/clickhouse.js.map +1 -0
  9. package/dist/dataConnectors/mongo.js +75 -101
  10. package/dist/dataConnectors/mongo.js.map +1 -0
  11. package/dist/dataConnectors/postgres.js +124 -143
  12. package/dist/dataConnectors/postgres.js.map +1 -0
  13. package/dist/dataConnectors/sqlite.js +113 -130
  14. package/dist/dataConnectors/sqlite.js.map +1 -0
  15. package/dist/index.js +197 -217
  16. package/dist/index.js.map +1 -0
  17. package/dist/modules/codeInjector.js +480 -486
  18. package/dist/modules/codeInjector.js.map +1 -0
  19. package/dist/modules/configValidator.js +31 -22
  20. package/dist/modules/configValidator.js.map +1 -0
  21. package/dist/modules/operationalResource.js +50 -70
  22. package/dist/modules/operationalResource.js.map +1 -0
  23. package/dist/modules/restApi.js +104 -116
  24. package/dist/modules/restApi.js.map +1 -0
  25. package/dist/modules/styleGenerator.js +1 -0
  26. package/dist/modules/styleGenerator.js.map +1 -0
  27. package/dist/modules/styles.js +1 -0
  28. package/dist/modules/styles.js.map +1 -0
  29. package/dist/modules/utils.js +1 -0
  30. package/dist/modules/utils.js.map +1 -0
  31. package/dist/plugins/audit-log/types.js +2 -0
  32. package/dist/plugins/audit-log/types.js.map +1 -0
  33. package/dist/plugins/chat-gpt/types.js +2 -0
  34. package/dist/plugins/chat-gpt/types.js.map +1 -0
  35. package/dist/plugins/email-password-reset/types.js +2 -0
  36. package/dist/plugins/email-password-reset/types.js.map +1 -0
  37. package/dist/plugins/foreign-inline-list/types.js +2 -0
  38. package/dist/plugins/foreign-inline-list/types.js.map +1 -0
  39. package/dist/plugins/import-export/types.js +2 -0
  40. package/dist/plugins/import-export/types.js.map +1 -0
  41. package/dist/plugins/rich-editor/custom/async-queue.js +29 -0
  42. package/dist/plugins/rich-editor/custom/async-queue.js.map +1 -0
  43. package/dist/plugins/rich-editor/dist/async-queue.js +41 -0
  44. package/dist/plugins/rich-editor/dist/custom/async-queue.js +29 -0
  45. package/dist/plugins/rich-editor/dist/custom/async-queue.js.map +1 -0
  46. package/dist/plugins/rich-editor/types.js +16 -0
  47. package/dist/plugins/rich-editor/types.js.map +1 -0
  48. package/dist/plugins/two-factors-auth/types.js +2 -0
  49. package/dist/plugins/two-factors-auth/types.js.map +1 -0
  50. package/dist/plugins/upload/types.js +2 -0
  51. package/dist/plugins/upload/types.js.map +1 -0
  52. package/dist/servers/express.js +30 -42
  53. package/dist/servers/express.js.map +1 -0
  54. package/dist/types/AdminForthConfig.js +1 -0
  55. package/dist/types/AdminForthConfig.js.map +1 -0
  56. package/dist/types/FrontendAPI.js +1 -0
  57. package/dist/types/FrontendAPI.js.map +1 -0
  58. package/package.json +7 -4
  59. package/auth.ts +0 -140
  60. package/basePlugin.ts +0 -70
  61. package/dataConnectors/baseConnector.ts +0 -216
  62. package/dataConnectors/clickhouse.ts +0 -341
  63. package/dataConnectors/mongo.ts +0 -202
  64. package/dataConnectors/postgres.ts +0 -306
  65. package/dataConnectors/sqlite.ts +0 -254
  66. package/index.ts +0 -428
  67. package/modules/codeInjector.ts +0 -736
  68. package/modules/configValidator.ts +0 -571
  69. package/modules/operationalResource.ts +0 -98
  70. package/modules/restApi.ts +0 -718
  71. package/modules/styleGenerator.ts +0 -55
  72. package/modules/styles.ts +0 -126
  73. package/modules/utils.ts +0 -472
  74. package/servers/express.ts +0 -259
  75. package/spa/.eslintrc.cjs +0 -14
  76. package/spa/README.md +0 -39
  77. package/spa/env.d.ts +0 -1
  78. package/spa/index.html +0 -23
  79. package/spa/package-lock.json +0 -4573
  80. package/spa/package.json +0 -49
  81. package/spa/postcss.config.js +0 -6
  82. package/spa/public/assets/favicon.png +0 -0
  83. package/spa/src/App.vue +0 -418
  84. package/spa/src/assets/base.css +0 -2
  85. package/spa/src/assets/logo.svg +0 -19
  86. package/spa/src/components/AcceptModal.vue +0 -45
  87. package/spa/src/components/Breadcrumbs.vue +0 -41
  88. package/spa/src/components/BreadcrumbsWithButtons.vue +0 -26
  89. package/spa/src/components/CustomDatePicker.vue +0 -176
  90. package/spa/src/components/CustomDateRangePicker.vue +0 -218
  91. package/spa/src/components/CustomRangePicker.vue +0 -156
  92. package/spa/src/components/Dropdown.vue +0 -168
  93. package/spa/src/components/Filters.vue +0 -222
  94. package/spa/src/components/HelloWorld.vue +0 -17
  95. package/spa/src/components/MenuLink.vue +0 -27
  96. package/spa/src/components/ResourceForm.vue +0 -290
  97. package/spa/src/components/ResourceListTable.vue +0 -460
  98. package/spa/src/components/SingleSkeletLoader.vue +0 -13
  99. package/spa/src/components/SkeleteLoader.vue +0 -23
  100. package/spa/src/components/ThreeDotsMenu.vue +0 -43
  101. package/spa/src/components/Toast.vue +0 -78
  102. package/spa/src/components/ValueRenderer.vue +0 -114
  103. package/spa/src/components/icons/IconCalendar.vue +0 -5
  104. package/spa/src/components/icons/IconCommunity.vue +0 -7
  105. package/spa/src/components/icons/IconDocumentation.vue +0 -7
  106. package/spa/src/components/icons/IconEcosystem.vue +0 -7
  107. package/spa/src/components/icons/IconSupport.vue +0 -7
  108. package/spa/src/components/icons/IconTime.vue +0 -5
  109. package/spa/src/components/icons/IconTooling.vue +0 -19
  110. package/spa/src/composables/useFrontendApi.ts +0 -26
  111. package/spa/src/composables/useStores.ts +0 -131
  112. package/spa/src/index.scss +0 -31
  113. package/spa/src/main.ts +0 -18
  114. package/spa/src/router/index.ts +0 -59
  115. package/spa/src/spa_types/core.ts +0 -53
  116. package/spa/src/stores/core.ts +0 -148
  117. package/spa/src/stores/filters.ts +0 -27
  118. package/spa/src/stores/modal.ts +0 -48
  119. package/spa/src/stores/toast.ts +0 -31
  120. package/spa/src/stores/user.ts +0 -72
  121. package/spa/src/utils.ts +0 -149
  122. package/spa/src/views/CreateView.vue +0 -167
  123. package/spa/src/views/EditView.vue +0 -170
  124. package/spa/src/views/ListView.vue +0 -279
  125. package/spa/src/views/LoginView.vue +0 -192
  126. package/spa/src/views/ResourceParent.vue +0 -17
  127. package/spa/src/views/ShowView.vue +0 -186
  128. package/spa/tailwind.config.js +0 -17
  129. package/spa/tsconfig.app.json +0 -14
  130. package/spa/tsconfig.json +0 -11
  131. package/spa/tsconfig.node.json +0 -19
  132. package/spa/vite.config.ts +0 -56
  133. package/tsconfig.json +0 -112
  134. package/types/AdminForthConfig.ts +0 -1762
  135. package/types/FrontendAPI.ts +0 -143
@@ -1,254 +0,0 @@
1
- import betterSqlite3 from 'better-sqlite3';
2
- import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirections, IAdminForthDataSourceConnector, AdminForthResource, AdminForthResourceColumn } from '../types/AdminForthConfig.js';
3
- import AdminForthBaseConnector from './baseConnector.js';
4
- import dayjs from 'dayjs';
5
-
6
- class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthDataSourceConnector {
7
-
8
- db: any;
9
-
10
- constructor({ url }: { url: string }) {
11
- super();
12
- // create connection here
13
- this.db = betterSqlite3(url.replace('sqlite://', ''));
14
- }
15
-
16
- async discoverFields(resource: AdminForthResource): Promise<{[key: string]: AdminForthResourceColumn}> {
17
- const tableName = resource.table;
18
- const stmt = this.db.prepare(`PRAGMA table_info(${tableName})`);
19
- const rows = await stmt.all();
20
- const fieldTypes = {};
21
- rows.forEach((row) => {
22
- const field: any = {};
23
- const baseType = row.type.toLowerCase();
24
- if (baseType == 'int') {
25
- field.type = AdminForthDataTypes.INTEGER;
26
- field._underlineType = 'int';
27
- } else if (baseType.includes('varchar(')) {
28
- field.type = AdminForthDataTypes.STRING;
29
- field._underlineType = 'varchar';
30
- const length = baseType.match(/\d+/);
31
- field.maxLength = length ? parseInt(length[0]) : null;
32
- } else if (baseType == 'text') {
33
- field.type = AdminForthDataTypes.TEXT;
34
- field._underlineType = 'text';
35
- } else if (baseType.includes('decimal(')) {
36
- field.type = AdminForthDataTypes.DECIMAL;
37
- field._underlineType = 'decimal';
38
- const [precision, scale] = baseType.match(/\d+/g);
39
- field.precision = parseInt(precision);
40
- field.scale = parseInt(scale);
41
- } else if (baseType == 'real') {
42
- field.type = AdminForthDataTypes.FLOAT; //8-byte IEEE floating point number. It
43
- field._underlineType = 'real';
44
- } else if (baseType == 'timestamp') {
45
- field.type = AdminForthDataTypes.DATETIME;
46
- field._underlineType = 'timestamp';
47
- } else if (baseType == 'boolean') {
48
- field.type = AdminForthDataTypes.BOOLEAN;
49
- field._underlineType = 'boolean';
50
- } else if (baseType == 'datetime') {
51
- field.type = AdminForthDataTypes.DATETIME;
52
- field._underlineType = 'datetime';
53
- } else {
54
- field.type = 'unknown'
55
- }
56
- field._baseTypeDebug = baseType;
57
- field.required = row.notnull == 1;
58
- field.primaryKey = row.pk == 1;
59
- field.default = row.dflt_value;
60
- fieldTypes[row.name] = field
61
- });
62
- return fieldTypes;
63
- }
64
-
65
- getFieldValue(field: AdminForthResourceColumn, value: any): any {
66
- if (field.type == AdminForthDataTypes.DATETIME) {
67
- if (!value) {
68
- return null;
69
- }
70
- if (field._underlineType == 'timestamp' || field._underlineType == 'int') {
71
- return dayjs.unix(+value).toISOString();
72
- } else if (field._underlineType == 'varchar') {
73
- return dayjs(value).toISOString();
74
- } else if (field._underlineType == 'datetime') {
75
- return dayjs(value).toISOString();
76
- } else {
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}"`);
78
- }
79
-
80
- } else if (field.type == AdminForthDataTypes.DATE) {
81
- if (!value) {
82
- return null;
83
- }
84
- return dayjs(value).toISOString().split('T')[0];
85
-
86
- } else if (field.type == AdminForthDataTypes.BOOLEAN) {
87
- return !!value;
88
- } else if (field.type == AdminForthDataTypes.JSON) {
89
- if (field._underlineType == 'text' || field._underlineType == 'varchar') {
90
- return JSON.parse(value);
91
- } else {
92
- console.error(`AdminForth: JSON field is not a string/text but ${field._underlineType}, this is not supported yet`);
93
- }
94
- }
95
-
96
- return value;
97
- }
98
-
99
- setFieldValue(field: AdminForthResourceColumn, value: any): any {
100
- if (field.type == AdminForthDataTypes.DATETIME) {
101
- if (!value) {
102
- return null;
103
- }
104
- if (field._underlineType == 'timestamp' || field._underlineType == 'int') {
105
- // value is iso string now, convert to unix timestamp
106
- return dayjs(value).unix();
107
- } else if (field._underlineType == 'varchar') {
108
- // value is iso string now, convert to unix timestamp
109
- return dayjs(value).toISOString();
110
- } else {
111
- return value;
112
- }
113
- } else if (field.type == AdminForthDataTypes.BOOLEAN) {
114
- return value ? 1 : 0;
115
- } else if (field.type == AdminForthDataTypes.JSON) {
116
- // check underline type is text or string
117
- if (field._underlineType == 'text' || field._underlineType == 'varchar') {
118
- return JSON.stringify(value);
119
- } else {
120
- console.error(`AdminForth: JSON field is not a string/text but ${field._underlineType}, this is not supported yet`);
121
- }
122
- }
123
-
124
- return value;
125
- }
126
-
127
- OperatorsMap = {
128
- [AdminForthFilterOperators.EQ]: '=',
129
- [AdminForthFilterOperators.NE]: '!=',
130
- [AdminForthFilterOperators.GT]: '>',
131
- [AdminForthFilterOperators.LT]: '<',
132
- [AdminForthFilterOperators.GTE]: '>=',
133
- [AdminForthFilterOperators.LTE]: '<=',
134
- [AdminForthFilterOperators.LIKE]: 'LIKE',
135
- [AdminForthFilterOperators.ILIKE]: 'ILIKE',
136
- [AdminForthFilterOperators.IN]: 'IN',
137
- [AdminForthFilterOperators.NIN]: 'NOT IN',
138
- };
139
-
140
- SortDirectionsMap = {
141
- [AdminForthSortDirections.asc]: 'ASC',
142
- [AdminForthSortDirections.desc]: 'DESC',
143
- };
144
-
145
- whereClause(filters) {
146
- return filters.length ? `WHERE ${filters.map((f, i) => {
147
- let placeholder = '?';
148
- let field = f.field;
149
- let operator = this.OperatorsMap[f.operator];
150
- if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
151
- placeholder = `(${f.value.map(() => '?').join(', ')})`;
152
- } else if (f.operator == AdminForthFilterOperators.ILIKE) {
153
- placeholder = `LOWER(?)`;
154
- field = `LOWER(${f.field})`;
155
- operator = 'LIKE';
156
- }
157
-
158
- return `${field} ${operator} ${placeholder}`
159
- }).join(' AND ')}` : '';
160
- }
161
- whereParams(filters) {
162
- return filters.reduce((acc, f) => {
163
- if (f.operator == AdminForthFilterOperators.LIKE || f.operator == AdminForthFilterOperators.ILIKE) {
164
- acc.push(`%${f.value}%`);
165
- } else if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
166
- acc.push(...f.value);
167
- } else {
168
- acc.push(f.value);
169
- }
170
- return acc;
171
- }, []);
172
- }
173
-
174
- async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }): Promise<any[]> {
175
- const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
176
- const tableName = resource.table;
177
-
178
- const where = this.whereClause(filters);
179
-
180
- const filterValues = this.whereParams(filters);
181
-
182
- const orderBy = sort.length ? `ORDER BY ${sort.map((s) => `${s.field} ${this.SortDirectionsMap[s.direction]}`).join(', ')}` : '';
183
-
184
- const q = `SELECT ${columns} FROM ${tableName} ${where} ${orderBy} LIMIT ? OFFSET ?`;
185
- const stmt = this.db.prepare(q);
186
- const d = [...filterValues, limit, offset];
187
-
188
- if (process.env.HEAVY_DEBUG) {
189
- console.log('🪲📜 SQLITE Q', q, 'params:', d);
190
- }
191
- const rows = await stmt.all(d);
192
-
193
- return rows.map((row) => {
194
- const newRow = {};
195
- for (const [key, value] of Object.entries(row)) {
196
- newRow[key] = value;
197
- }
198
- return newRow;
199
- })
200
- }
201
-
202
- async getCount({ resource, filters }) {
203
- const tableName = resource.table;
204
- const where = this.whereClause(filters);
205
- const filterValues = this.whereParams(filters);
206
- const totalStmt = this.db.prepare(`SELECT COUNT(*) FROM ${tableName} ${where}`);
207
- return totalStmt.get([...filterValues])['COUNT(*)'];
208
- }
209
-
210
- async getMinMaxForColumnsWithOriginalTypes({ resource, columns }: { resource: AdminForthResource, columns: AdminForthResourceColumn[] }): Promise<{ [key: string]: { min: any, max: any } }> {
211
- const tableName = resource.table;
212
- const result = {};
213
- await Promise.all(columns.map(async (col) => {
214
- const stmt = await this.db.prepare(`SELECT MIN(${col.name}) as min, MAX(${col.name}) as max FROM ${tableName}`);
215
- const { min, max } = stmt.get();
216
- result[col.name] = {
217
- min, max,
218
- };
219
- }))
220
- return result;
221
- }
222
-
223
- async createRecordOriginalValues({ resource, record }: { resource: AdminForthResource, record: any }) {
224
- const tableName = resource.table;
225
- const columns = Object.keys(record);
226
- const placeholders = columns.map(() => '?').join(', ');
227
- const values = columns.map((colName) => record[colName]);
228
- const q = this.db.prepare(`INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`)
229
- await q.run(values);
230
- }
231
-
232
- async updateRecordOriginalValues({ resource, recordId, newValues }: { resource: AdminForthResource, recordId: any, newValues: any }) {
233
- const columnsWithPlaceholders = Object.keys(newValues).map((col) => `${col} = ?`);
234
- const values = [...Object.values(newValues), recordId];
235
-
236
- const q = this.db.prepare(
237
- `UPDATE ${resource.table} SET ${columnsWithPlaceholders} WHERE ${this.getPrimaryKey(resource)} = ?`
238
- )
239
- process.env.HEAVY_DEBUG && console.log('🪲 SQLITE Query', `UPDATE ${resource.table} SET ${columnsWithPlaceholders} WHERE ${this.getPrimaryKey(resource)} = ?`, 'params:', values);
240
- await q.run(values);
241
- }
242
-
243
- async deleteRecord({ resource, recordId }: { resource: AdminForthResource, recordId: any }): Promise<boolean> {
244
- const q = this.db.prepare(`DELETE FROM ${resource.table} WHERE ${this.getPrimaryKey(resource)} = ?`);
245
- const res = await q.run(recordId);
246
- return res.changes > 0;
247
- }
248
-
249
- close() {
250
- this.db.close();
251
- }
252
- }
253
-
254
- export default SQLiteConnector;
package/index.ts DELETED
@@ -1,428 +0,0 @@
1
-
2
- import AdminForthAuth from './auth.js';
3
- import MongoConnector from './dataConnectors/mongo.js';
4
- import PostgresConnector from './dataConnectors/postgres.js';
5
- import SQLiteConnector from './dataConnectors/sqlite.js';
6
- import CodeInjector from './modules/codeInjector.js';
7
- import ExpressServer from './servers/express.js';
8
- import { ADMINFORTH_VERSION, listify, suggestIfTypo, RateLimiter, getClinetIp } from './modules/utils.js';
9
- import {
10
- type AdminForthConfig,
11
- type IAdminForth,
12
- type IConfigValidator,
13
- IOperationalResource,
14
- AdminForthFilterOperators,
15
- AdminForthDataTypes, IHttpServer,
16
- BeforeSaveFunction,
17
- AfterSaveFunction,
18
- AdminUser,
19
- AdminForthResource,
20
- IAdminForthDataSourceConnectorBase,
21
- } from './types/AdminForthConfig.js';
22
- import AdminForthPlugin from './basePlugin.js';
23
- import ConfigValidator from './modules/configValidator.js';
24
- import AdminForthRestAPI, { interpretResource } from './modules/restApi.js';
25
- import ClickhouseConnector from './dataConnectors/clickhouse.js';
26
- import OperationalResource from './modules/operationalResource.js';
27
-
28
- // exports
29
- export * from './types/AdminForthConfig.js';
30
- export { interpretResource };
31
- export { AdminForthPlugin };
32
- export { suggestIfTypo, RateLimiter, getClinetIp };
33
-
34
-
35
- class AdminForth implements IAdminForth {
36
- static Types = AdminForthDataTypes;
37
-
38
- static Utils = {
39
- generatePasswordHash: async (password) => {
40
- return await AdminForthAuth.generatePasswordHash(password);
41
- },
42
-
43
- PASSWORD_VALIDATORS: {
44
- UP_LOW_NUM_SPECIAL: {
45
- regExp: '^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[!@#\\$%\\^&\\*\\(\\)\\-_=\\+\\[\\]\\{\\}\\|;:\',\\.<>\\/\\?]).+$',
46
- message: 'Password must include at least one uppercase letter, one lowercase letter, one number, and one special character'
47
- },
48
- UP_LOW_NUM: {
49
- regExp: '^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]).+$',
50
- message: 'Password must include at least one uppercase letter, one lowercase letter, and one number'
51
- },
52
- },
53
- EMAIL_VALIDATOR: {
54
- regExp: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$',
55
- message: 'Email is not valid, must be in format example@test.com',
56
- }
57
- }
58
-
59
- #defaultConfig = {
60
- deleteConfirmation: true,
61
- }
62
-
63
- config: AdminForthConfig;
64
- express: ExpressServer;
65
- auth: AdminForthAuth;
66
- codeInjector: CodeInjector;
67
- connectors: {
68
- [dataSourceId: string]: IAdminForthDataSourceConnectorBase,
69
- };
70
- connectorClasses: any;
71
- runningHotReload: boolean;
72
- activatedPlugins: Array<AdminForthPlugin>;
73
- configValidator: IConfigValidator;
74
- restApi: AdminForthRestAPI;
75
- operationalResources: {
76
- [resourceId: string]: IOperationalResource,
77
- }
78
- baseUrlSlashed: string;
79
-
80
- statuses: {
81
- dbDiscover: 'running' | 'done',
82
- }
83
-
84
- constructor(config: AdminForthConfig) {
85
- this.config = {...this.#defaultConfig,...config};
86
- this.codeInjector = new CodeInjector(this);
87
- this.configValidator = new ConfigValidator(this, this.config);
88
- this.restApi = new AdminForthRestAPI(this);
89
- this.activatedPlugins = [];
90
-
91
- this.configValidator.validateConfig();
92
- this.activatePlugins();
93
- this.configValidator.validateConfig(); // revalidate after plugins
94
-
95
- this.express = new ExpressServer(this);
96
- this.auth = new AdminForthAuth(this);
97
- this.connectors = {};
98
- this.statuses = {
99
- dbDiscover: 'running',
100
- };
101
-
102
- console.log(`🚀 AdminForth v${ADMINFORTH_VERSION} starting up`)
103
- }
104
-
105
- activatePlugins() {
106
- process.env.HEAVY_DEBUG && console.log('🔌🔌🔌 Activating plugins');
107
- const allPluginInstances = [];
108
- for (let resource of this.config.resources) {
109
- for (let pluginInstance of resource.plugins || []) {
110
- allPluginInstances.push({pi: pluginInstance, resource});
111
- }
112
- }
113
- allPluginInstances.sort(({pi: a}, {pi: b}) => a.activationOrder - b.activationOrder);
114
- allPluginInstances.forEach(
115
- ({pi: pluginInstance, resource}) => {
116
- pluginInstance.modifyResourceConfig(this, resource);
117
- const plugin = this.activatedPlugins.find((p) => p.pluginInstanceId === pluginInstance.pluginInstanceId);
118
- if (plugin) {
119
- process.env.HEAVY_DEBUG && console.log(`Current plugin pluginInstance.pluginInstanceId ${pluginInstance.pluginInstanceId}`);
120
-
121
- throw new Error(`Attempt to activate Plugin ${pluginInstance.constructor.name} second time for same resource, but plugin does not support it.
122
- To support multiple plugin instance pre one resource, plugin should return unique string values for each installation from instanceUniqueRepresentation`);
123
- }
124
- this.activatedPlugins.push(pluginInstance);
125
- }
126
- );
127
- }
128
-
129
- getPluginsByClassName<T>(className: string): T[] {
130
- const plugins = this.activatedPlugins.filter((plugin) => plugin.className === className);
131
- return plugins as T[];
132
- }
133
-
134
- getPluginByClassName<T>(className: string): T {
135
- const plugins = this.getPluginsByClassName(className);
136
- if (plugins.length > 1) {
137
- throw new Error(`Multiple plugins with className ${className} found. Use getPluginsByClassName instead`);
138
- }
139
- if (plugins.length === 0) {
140
- const similar = suggestIfTypo(this.activatedPlugins.map((p) => p.className), className);
141
- throw new Error(`Plugin with className ${className} not found. ${similar ? `Did you mean ${similar}?` : ''}`);
142
- }
143
- return plugins[0] as T;
144
- }
145
-
146
- async discoverDatabases() {
147
- this.statuses.dbDiscover = 'running';
148
- this.connectorClasses = {
149
- 'sqlite': SQLiteConnector,
150
- 'postgres': PostgresConnector,
151
- 'mongodb': MongoConnector,
152
- 'clickhouse': ClickhouseConnector,
153
- };
154
- if (!this.config.databaseConnectors) {
155
- this.config.databaseConnectors = {...this.connectorClasses};
156
- }
157
- this.config.dataSources.forEach((ds) => {
158
- const dbType = ds.url.split(':')[0];
159
- if (!this.config.databaseConnectors[dbType]) {
160
- 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`);
161
- }
162
- this.connectors[ds.id] = new this.config.databaseConnectors[dbType]({url: ds.url});
163
- });
164
-
165
- await Promise.all(this.config.resources.map(async (res) => {
166
- if (!this.connectors[res.dataSource]) {
167
- const similar = suggestIfTypo(Object.keys(this.connectors), res.dataSource);
168
- throw new Error(`Resource '${res.table}' refers to unknown dataSource '${res.dataSource}' ${similar
169
- ? `. Did you mean '${similar}'?` : 'Available dataSources: '+Object.keys(this.connectors).join(', ')}`
170
- );
171
- }
172
- const fieldTypes = await this.connectors[res.dataSource].discoverFields(res);
173
- if (fieldTypes !== null && !Object.keys(fieldTypes).length) {
174
- throw new Error(`Table '${res.table}' (In resource '${res.resourceId}') has no fields or does not exist`);
175
- }
176
- if (fieldTypes === null) {
177
- console.error(`⛔ DataSource ${res.dataSource} was not able to perform field discovery. It will not work properly`);
178
- return;
179
- }
180
- if (!res.columns) {
181
- res.columns = Object.keys(fieldTypes).map((name) => ({ name }));
182
- }
183
-
184
- res.columns.forEach((col, i) => {
185
- if (!fieldTypes[col.name] && !col.virtual) {
186
- const similar = suggestIfTypo(Object.keys(fieldTypes), col.name);
187
- throw new Error(`Resource '${res.table}' has no column '${col.name}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
188
- }
189
- // first find discovered values, but allow override
190
- res.columns[i] = { ...fieldTypes[col.name], ...col };
191
- });
192
-
193
- this.configValidator.postProcessAfterDiscover(res);
194
-
195
- // check if primaryKey column is present
196
- if (!res.columns.some((col) => col.primaryKey)) {
197
- throw new Error(`Resource '${res.table}' has no column defined or auto-discovered. Please set 'primaryKey: true' in a columns which has unique value for each record and index`);
198
- }
199
-
200
- }));
201
-
202
- this.statuses.dbDiscover = 'done';
203
-
204
- this.operationalResources = {};
205
- this.config.resources.forEach((resource) => {
206
- this.operationalResources[resource.resourceId] = new OperationalResource(this.connectors[resource.dataSource], resource);
207
- });
208
-
209
- // console.log('⚙️⚙️⚙️ Database discovery done', JSON.stringify(this.config.resources, null, 2));
210
- }
211
-
212
- async bundleNow({ hotReload=false }) {
213
- await this.codeInjector.bundleNow({ hotReload });
214
- }
215
-
216
- async getUserByPk(pk: string) {
217
- // if database discovery is not done, throw
218
- if (this.statuses.dbDiscover !== 'done') {
219
- if (this.statuses.dbDiscover === 'running') {
220
- throw new Error('Database discovery is running. You can\'t use data API while database discovery is not finished.\n'+
221
- 'Consider moving your code to a place where it will be executed after database discovery is already done (after await admin.discoverDatabases())');
222
- }
223
- throw new Error('Database discovery is not yet started. You can\'t use data API before database discovery is done. \n'+
224
- 'Call admin.discoverDatabases() first and await it before using data API');
225
- }
226
-
227
- const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.usersResourceId);
228
- if (!resource) {
229
- const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId), this.config.auth.usersResourceId);
230
- throw new Error(`No resource with ${this.config.auth.usersResourceId} found. ${similar ?
231
- `Did you mean '${similar}' in config.auth.usersResourceId?` : 'Please set correct resource in config.auth.usersResourceId'}`
232
- );
233
- }
234
- const users = await this.connectors[resource.dataSource].getData({
235
- resource,
236
- filters: [
237
- { field: resource.columns.find((col) => col.primaryKey).name, operator: AdminForthFilterOperators.EQ, value: pk },
238
- ],
239
- limit: 1,
240
- offset: 0,
241
- sort: [],
242
- });
243
- return users.data[0] || null;
244
- }
245
-
246
- async createResourceRecord(
247
- { resource, record, adminUser }:
248
- { resource: AdminForthResource, record: any, adminUser: AdminUser }
249
- ): Promise<{ error?: string, createdRecord?: any }> {
250
-
251
- // execute hook if needed
252
- for (const hook of listify(resource.hooks?.create?.beforeSave as BeforeSaveFunction[])) {
253
- const resp = await hook({ recordId: undefined, resource, record, adminUser });
254
- if (!resp || (!resp.ok && !resp.error)) {
255
- throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
256
- }
257
-
258
- if (resp.error) {
259
- return { error: resp.error };
260
- }
261
- }
262
-
263
- // remove virtual columns from record
264
- for (const column of resource.columns.filter((col) => col.virtual)) {
265
- if (record[column.name]) {
266
- delete record[column.name];
267
- }
268
- }
269
- const connector = this.connectors[resource.dataSource];
270
- process.env.HEAVY_DEBUG && console.log('🪲🆕 creating record createResourceRecord', record);
271
- const { error, createdRecord } = await connector.createRecord({ resource, record, adminUser });
272
- if ( error ) {
273
- return { error };
274
- }
275
-
276
- const primaryKey = record[resource.columns.find((col) => col.primaryKey).name];
277
-
278
- // execute hook if needed
279
- for (const hook of listify(resource.hooks?.create?.afterSave as AfterSaveFunction[])) {
280
- process.env.HEAVY_DEBUG && console.log('🪲 Hook afterSave', hook);
281
- const resp = await hook({
282
- recordId: primaryKey,
283
- resource,
284
- record: createdRecord,
285
- adminUser
286
- });
287
-
288
- if (!resp || (!resp.ok && !resp.error)) {
289
- throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
290
- }
291
-
292
- if (resp.error) {
293
- return { error: resp.error };
294
- }
295
- }
296
-
297
- return { error, createdRecord };
298
- }
299
-
300
- /**
301
- * record is partial record with only changed fields
302
- */
303
- async updateResourceRecord(
304
- { resource, recordId, record, oldRecord, adminUser }:
305
- { resource: AdminForthResource, recordId: any, record: any, oldRecord: any, adminUser: AdminUser }
306
- ): Promise<{ error?: string }> {
307
-
308
- // execute hook if needed
309
- for (const hook of listify(resource.hooks?.edit?.beforeSave as BeforeSaveFunction[])) {
310
- const resp = await hook({
311
- recordId,
312
- resource,
313
- record,
314
- oldRecord,
315
- adminUser
316
- });
317
- if (!resp || (!resp.ok && !resp.error)) {
318
- throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
319
- }
320
- if (resp.error) {
321
- return { error: resp.error };
322
- }
323
- }
324
- const newValues = {};
325
- const connector = this.connectors[resource.dataSource];
326
-
327
- for (const recordField in record) {
328
- if (record[recordField] !== oldRecord[recordField]) {
329
- // leave only changed fields to reduce data transfer/modifications in db
330
- const column = resource.columns.find((col) => col.name === recordField);
331
- if (!column || !column.virtual) {
332
- // exclude virtual columns
333
- newValues[recordField] = record[recordField];
334
- }
335
- }
336
- }
337
-
338
- if (Object.keys(newValues).length > 0) {
339
- await connector.updateRecord({ resource, recordId, newValues });
340
- }
341
-
342
- // execute hook if needed
343
- for (const hook of listify(resource.hooks?.edit?.afterSave as AfterSaveFunction[])) {
344
- const resp = await hook({
345
- resource,
346
- record,
347
- adminUser,
348
- oldRecord,
349
- recordId,
350
- });
351
- if (!resp || (!resp.ok && !resp.error)) {
352
- throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
353
- }
354
- if (resp.error) {
355
- return { error: resp.error };
356
- }
357
- }
358
-
359
- return { error: null };
360
- }
361
-
362
- async deleteResourceRecord(
363
- { resource, recordId, adminUser, record }:
364
- { resource: AdminForthResource, recordId: any, adminUser: AdminUser, record: any }
365
- ): Promise<{ error?: string }> {
366
- // execute hook if needed
367
- for (const hook of listify(resource.hooks?.delete?.beforeSave as BeforeSaveFunction[])) {
368
- const resp = await hook({
369
- resource,
370
- record,
371
- adminUser,
372
- recordId,
373
- });
374
- if (!resp || (!resp.ok && !resp.error)) {
375
- throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
376
- }
377
-
378
- if (resp.error) {
379
- return { error: resp.error };
380
- }
381
- }
382
-
383
- const connector = this.connectors[resource.dataSource];
384
- await connector.deleteRecord({ resource, recordId});
385
-
386
- // execute hook if needed
387
- for (const hook of listify(resource.hooks?.delete?.afterSave as BeforeSaveFunction[])) {
388
- const resp = await hook({
389
- resource,
390
- record,
391
- adminUser,
392
- recordId,
393
- });
394
- if (!resp || (!resp.ok && !resp.error)) {
395
- throw new Error(`Hook afterSave must return object with {ok: true} or { error: 'Error' } `);
396
- }
397
-
398
- if (resp.error) {
399
- return { error: resp.error };
400
- }
401
- }
402
-
403
- return { error: null };
404
- }
405
-
406
- resource(resourceId: string): IOperationalResource {
407
- if (this.statuses.dbDiscover !== 'done') {
408
- if (this.statuses.dbDiscover === 'running') {
409
- throw new Error('Database discovery is running. You can\'t use data API while database discovery is not finished.\n'+
410
- 'Consider moving your code to a place where it will be executed after database discovery is already done (after await admin.discoverDatabases())');
411
- } else {
412
- throw new Error('Database discovery is not yet started. You can\'t use data API before database discovery is done. \n'+
413
- 'Call admin.discoverDatabases() first and await it before using data API');
414
- }
415
- }
416
- if (!this.operationalResources[resourceId]) {
417
- const closeName = suggestIfTypo(Object.keys(this.operationalResources), resourceId);
418
- throw new Error(`Resource with id '${resourceId}' not found${closeName ? `. Did you mean '${closeName}'?` : ''}`);
419
- }
420
- return this.operationalResources[resourceId];
421
- }
422
-
423
- setupEndpoints(server: IHttpServer) {
424
- this.restApi.registerEndpoints(server);
425
- }
426
- }
427
-
428
- export default AdminForth;