adminforth 1.3.54-next.9 → 1.3.55-next.2

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 (224) hide show
  1. package/dist/auth.d.ts +31 -0
  2. package/dist/auth.d.ts.map +1 -0
  3. package/dist/auth.js +42 -56
  4. package/dist/auth.js.map +1 -0
  5. package/dist/basePlugin.d.ts +18 -0
  6. package/dist/basePlugin.d.ts.map +1 -0
  7. package/dist/basePlugin.js +1 -0
  8. package/dist/basePlugin.js.map +1 -0
  9. package/dist/dataConnectors/baseConnector.d.ts +94 -0
  10. package/dist/dataConnectors/baseConnector.d.ts.map +1 -0
  11. package/dist/dataConnectors/baseConnector.js +108 -122
  12. package/dist/dataConnectors/baseConnector.js.map +1 -0
  13. package/dist/dataConnectors/clickhouse.d.ts +92 -0
  14. package/dist/dataConnectors/clickhouse.d.ts.map +1 -0
  15. package/dist/dataConnectors/clickhouse.js +132 -149
  16. package/dist/dataConnectors/clickhouse.js.map +1 -0
  17. package/dist/dataConnectors/mongo.d.ts +93 -0
  18. package/dist/dataConnectors/mongo.d.ts.map +1 -0
  19. package/dist/dataConnectors/mongo.js +75 -101
  20. package/dist/dataConnectors/mongo.js.map +1 -0
  21. package/dist/dataConnectors/postgres.d.ts +71 -0
  22. package/dist/dataConnectors/postgres.d.ts.map +1 -0
  23. package/dist/dataConnectors/postgres.js +124 -143
  24. package/dist/dataConnectors/postgres.js.map +1 -0
  25. package/dist/dataConnectors/sqlite.d.ts +67 -0
  26. package/dist/dataConnectors/sqlite.d.ts.map +1 -0
  27. package/dist/dataConnectors/sqlite.js +113 -130
  28. package/dist/dataConnectors/sqlite.js.map +1 -0
  29. package/dist/index.d.ts +92 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +197 -217
  32. package/dist/index.js.map +1 -0
  33. package/dist/modules/codeInjector.d.ts +35 -0
  34. package/dist/modules/codeInjector.d.ts.map +1 -0
  35. package/dist/modules/codeInjector.js +480 -486
  36. package/dist/modules/codeInjector.js.map +1 -0
  37. package/dist/modules/configValidator.d.ts +12 -0
  38. package/dist/modules/configValidator.d.ts.map +1 -0
  39. package/dist/modules/configValidator.js +31 -22
  40. package/dist/modules/configValidator.js.map +1 -0
  41. package/dist/modules/operationalResource.d.ts +17 -0
  42. package/dist/modules/operationalResource.d.ts.map +1 -0
  43. package/dist/modules/operationalResource.js +50 -70
  44. package/dist/modules/operationalResource.js.map +1 -0
  45. package/dist/modules/restApi.d.ts +10 -0
  46. package/dist/modules/restApi.d.ts.map +1 -0
  47. package/dist/modules/restApi.js +104 -116
  48. package/dist/modules/restApi.js.map +1 -0
  49. package/dist/modules/styleGenerator.d.ts +9 -0
  50. package/dist/modules/styleGenerator.d.ts.map +1 -0
  51. package/dist/modules/styleGenerator.js +1 -0
  52. package/dist/modules/styleGenerator.js.map +1 -0
  53. package/dist/modules/styles.d.ts +96 -0
  54. package/dist/modules/styles.d.ts.map +1 -0
  55. package/dist/modules/styles.js +1 -0
  56. package/dist/modules/styles.js.map +1 -0
  57. package/dist/modules/utils.d.ts +39 -0
  58. package/dist/modules/utils.d.ts.map +1 -0
  59. package/dist/modules/utils.js +1 -0
  60. package/dist/modules/utils.js.map +1 -0
  61. package/dist/plugins/audit-log/types.d.ts +35 -0
  62. package/dist/plugins/audit-log/types.d.ts.map +1 -0
  63. package/dist/plugins/audit-log/types.js +2 -0
  64. package/dist/plugins/audit-log/types.js.map +1 -0
  65. package/dist/plugins/chat-gpt/types.d.ts +82 -0
  66. package/dist/plugins/chat-gpt/types.d.ts.map +1 -0
  67. package/dist/plugins/chat-gpt/types.js +2 -0
  68. package/dist/plugins/chat-gpt/types.js.map +1 -0
  69. package/dist/plugins/email-password-reset/types.d.ts +28 -0
  70. package/dist/plugins/email-password-reset/types.d.ts.map +1 -0
  71. package/dist/plugins/email-password-reset/types.js +2 -0
  72. package/dist/plugins/email-password-reset/types.js.map +1 -0
  73. package/dist/plugins/foreign-inline-list/types.d.ts +19 -0
  74. package/dist/plugins/foreign-inline-list/types.d.ts.map +1 -0
  75. package/dist/plugins/foreign-inline-list/types.js +2 -0
  76. package/dist/plugins/foreign-inline-list/types.js.map +1 -0
  77. package/dist/plugins/import-export/types.d.ts +3 -0
  78. package/dist/plugins/import-export/types.d.ts.map +1 -0
  79. package/dist/plugins/import-export/types.js +2 -0
  80. package/dist/plugins/import-export/types.js.map +1 -0
  81. package/dist/plugins/rich-editor/custom/async-queue.d.ts +8 -0
  82. package/dist/plugins/rich-editor/custom/async-queue.d.ts.map +1 -0
  83. package/dist/plugins/rich-editor/custom/async-queue.js +29 -0
  84. package/dist/plugins/rich-editor/custom/async-queue.js.map +1 -0
  85. package/dist/plugins/rich-editor/dist/custom/async-queue.d.ts +8 -0
  86. package/dist/plugins/rich-editor/dist/custom/async-queue.d.ts.map +1 -0
  87. package/dist/plugins/rich-editor/dist/custom/async-queue.js +29 -0
  88. package/dist/plugins/rich-editor/dist/custom/async-queue.js.map +1 -0
  89. package/dist/plugins/rich-editor/types.d.ts +153 -0
  90. package/dist/plugins/rich-editor/types.d.ts.map +1 -0
  91. package/dist/plugins/rich-editor/types.js +16 -0
  92. package/dist/plugins/rich-editor/types.js.map +1 -0
  93. package/dist/plugins/two-factors-auth/types.d.ts +18 -0
  94. package/dist/plugins/two-factors-auth/types.d.ts.map +1 -0
  95. package/dist/plugins/two-factors-auth/types.js +2 -0
  96. package/dist/plugins/two-factors-auth/types.js.map +1 -0
  97. package/dist/plugins/upload/types.d.ts +132 -0
  98. package/dist/plugins/upload/types.d.ts.map +1 -0
  99. package/dist/plugins/upload/types.js +2 -0
  100. package/dist/plugins/upload/types.js.map +1 -0
  101. package/dist/servers/express.d.ts +18 -0
  102. package/dist/servers/express.d.ts.map +1 -0
  103. package/dist/servers/express.js +30 -42
  104. package/dist/servers/express.js.map +1 -0
  105. package/dist/spa/index.html +2 -2
  106. package/dist/spa/package-lock.json +87 -1
  107. package/dist/spa/package.json +4 -1
  108. package/dist/spa/src/App.vue +154 -50
  109. package/dist/spa/src/components/AcceptModal.vue +1 -1
  110. package/dist/spa/src/components/Breadcrumbs.vue +1 -1
  111. package/dist/spa/src/components/CustomDatePicker.vue +1 -1
  112. package/dist/spa/src/components/CustomDateRangePicker.vue +1 -1
  113. package/dist/spa/src/components/CustomRangePicker.vue +9 -5
  114. package/dist/spa/src/components/Dropdown.vue +4 -4
  115. package/dist/spa/src/components/Filters.vue +2 -2
  116. package/dist/spa/src/components/MenuLink.vue +3 -0
  117. package/dist/spa/src/components/ResourceForm.vue +67 -36
  118. package/dist/spa/src/components/ResourceListTable.vue +216 -144
  119. package/dist/spa/src/components/SkeleteLoader.vue +4 -4
  120. package/dist/spa/src/components/Toast.vue +3 -2
  121. package/dist/spa/src/components/ValueRenderer.vue +81 -6
  122. package/dist/spa/src/composables/useFrontendApi.ts +1 -1
  123. package/dist/spa/src/composables/useStores.ts +18 -14
  124. package/dist/spa/src/index.scss +4 -0
  125. package/{spa → dist/spa}/src/renderers/CompactUUID.vue +4 -4
  126. package/{spa → dist/spa}/src/renderers/CountryFlag.vue +2 -2
  127. package/dist/spa/src/router/index.ts +4 -8
  128. package/dist/spa/src/spa_types/core.ts +2 -0
  129. package/dist/spa/src/stores/core.ts +6 -2
  130. package/dist/spa/src/stores/filters.ts +15 -10
  131. package/dist/spa/src/stores/toast.ts +22 -6
  132. package/dist/spa/src/types/AdminForthConfig.ts +340 -55
  133. package/dist/spa/src/types/FrontendAPI.ts +52 -30
  134. package/dist/spa/src/utils.ts +59 -2
  135. package/dist/spa/src/views/CreateView.vue +15 -4
  136. package/dist/spa/src/views/EditView.vue +20 -7
  137. package/dist/spa/src/views/ListView.vue +132 -38
  138. package/dist/spa/src/views/LoginView.vue +50 -18
  139. package/dist/spa/src/views/ShowView.vue +25 -15
  140. package/dist/types/AdminForthConfig.d.ts +1619 -0
  141. package/dist/types/AdminForthConfig.d.ts.map +1 -0
  142. package/dist/types/AdminForthConfig.js +1 -0
  143. package/dist/types/AdminForthConfig.js.map +1 -0
  144. package/{types/FrontendAPI.ts → dist/types/FrontendAPI.d.ts} +27 -52
  145. package/dist/types/FrontendAPI.d.ts.map +1 -0
  146. package/dist/types/FrontendAPI.js +1 -0
  147. package/dist/types/FrontendAPI.js.map +1 -0
  148. package/package.json +16 -6
  149. package/auth.ts +0 -140
  150. package/basePlugin.ts +0 -70
  151. package/dataConnectors/baseConnector.ts +0 -216
  152. package/dataConnectors/clickhouse.ts +0 -338
  153. package/dataConnectors/mongo.ts +0 -202
  154. package/dataConnectors/postgres.ts +0 -306
  155. package/dataConnectors/sqlite.ts +0 -254
  156. package/index.ts +0 -428
  157. package/modules/codeInjector.ts +0 -736
  158. package/modules/configValidator.ts +0 -571
  159. package/modules/operationalResource.ts +0 -98
  160. package/modules/restApi.ts +0 -718
  161. package/modules/styleGenerator.ts +0 -55
  162. package/modules/styles.ts +0 -126
  163. package/modules/utils.ts +0 -472
  164. package/servers/express.ts +0 -259
  165. package/spa/.eslintrc.cjs +0 -14
  166. package/spa/README.md +0 -39
  167. package/spa/env.d.ts +0 -1
  168. package/spa/index.html +0 -23
  169. package/spa/package-lock.json +0 -4602
  170. package/spa/package.json +0 -51
  171. package/spa/postcss.config.js +0 -6
  172. package/spa/public/assets/favicon.png +0 -0
  173. package/spa/src/App.vue +0 -418
  174. package/spa/src/assets/base.css +0 -2
  175. package/spa/src/assets/logo.svg +0 -19
  176. package/spa/src/components/AcceptModal.vue +0 -45
  177. package/spa/src/components/Breadcrumbs.vue +0 -41
  178. package/spa/src/components/BreadcrumbsWithButtons.vue +0 -26
  179. package/spa/src/components/CustomDatePicker.vue +0 -176
  180. package/spa/src/components/CustomDateRangePicker.vue +0 -218
  181. package/spa/src/components/CustomRangePicker.vue +0 -156
  182. package/spa/src/components/Dropdown.vue +0 -168
  183. package/spa/src/components/Filters.vue +0 -222
  184. package/spa/src/components/HelloWorld.vue +0 -17
  185. package/spa/src/components/MenuLink.vue +0 -27
  186. package/spa/src/components/ResourceForm.vue +0 -290
  187. package/spa/src/components/ResourceListTable.vue +0 -466
  188. package/spa/src/components/SingleSkeletLoader.vue +0 -13
  189. package/spa/src/components/SkeleteLoader.vue +0 -23
  190. package/spa/src/components/Toast.vue +0 -78
  191. package/spa/src/components/ValueRenderer.vue +0 -114
  192. package/spa/src/components/icons/IconCalendar.vue +0 -5
  193. package/spa/src/components/icons/IconCommunity.vue +0 -7
  194. package/spa/src/components/icons/IconDocumentation.vue +0 -7
  195. package/spa/src/components/icons/IconEcosystem.vue +0 -7
  196. package/spa/src/components/icons/IconSupport.vue +0 -7
  197. package/spa/src/components/icons/IconTime.vue +0 -5
  198. package/spa/src/components/icons/IconTooling.vue +0 -19
  199. package/spa/src/composables/useFrontendApi.ts +0 -26
  200. package/spa/src/composables/useStores.ts +0 -131
  201. package/spa/src/index.scss +0 -31
  202. package/spa/src/main.ts +0 -18
  203. package/spa/src/router/index.ts +0 -59
  204. package/spa/src/spa_types/core.ts +0 -53
  205. package/spa/src/stores/core.ts +0 -148
  206. package/spa/src/stores/filters.ts +0 -27
  207. package/spa/src/stores/modal.ts +0 -48
  208. package/spa/src/stores/toast.ts +0 -31
  209. package/spa/src/stores/user.ts +0 -72
  210. package/spa/src/utils.ts +0 -160
  211. package/spa/src/views/CreateView.vue +0 -167
  212. package/spa/src/views/EditView.vue +0 -170
  213. package/spa/src/views/ListView.vue +0 -352
  214. package/spa/src/views/LoginView.vue +0 -192
  215. package/spa/src/views/ResourceParent.vue +0 -17
  216. package/spa/src/views/ShowView.vue +0 -186
  217. package/spa/tailwind.config.js +0 -17
  218. package/spa/tsconfig.app.json +0 -14
  219. package/spa/tsconfig.json +0 -11
  220. package/spa/tsconfig.node.json +0 -19
  221. package/spa/vite.config.ts +0 -56
  222. package/tsconfig.json +0 -112
  223. package/types/AdminForthConfig.ts +0 -1762
  224. /package/{spa → dist/spa}/src/components/ThreeDotsMenu.vue +0 -0
@@ -1,571 +0,0 @@
1
- import {
2
- AdminForthConfig,
3
- AdminForthResource,
4
- IAdminForth, IConfigValidator,
5
- AdminForthComponentDeclaration ,
6
- AdminForthResourcePages, AllowedActionsEnum,
7
- type AdminForthComponentDeclarationFull,
8
- type AfterSaveFunction,
9
- AdminForthBulkAction,
10
- } from "../types/AdminForthConfig.js";
11
-
12
- import fs from 'fs';
13
- import path from 'path';
14
- import { guessLabelFromName, suggestIfTypo } from './utils.js';
15
-
16
- import crypto from 'crypto';
17
-
18
-
19
-
20
-
21
- export default class ConfigValidator implements IConfigValidator {
22
-
23
- constructor(private adminforth: IAdminForth, private config: AdminForthConfig) {
24
- this.adminforth = adminforth;
25
- this.config = config;
26
- }
27
-
28
- validateAndListifyInjection(obj, key, errors) {
29
- if (!Array.isArray(obj[key])) {
30
- // not array
31
- obj[key] = [obj[key]];
32
- }
33
- obj[key].forEach((target, i) => {
34
- obj[key][i] = this.validateComponent(target, errors);
35
- });
36
- }
37
-
38
- checkCustomFileExists(filePath: string): Array<string> {
39
- if (filePath.startsWith('@@/')) {
40
- const checkPath = path.join(this.config.customization.customComponentsDir, filePath.replace('@@/', ''));
41
- if (!fs.existsSync(checkPath)) {
42
- return [`File file ${filePath} does not exist in ${this.config.customization.customComponentsDir}`];
43
- }
44
- }
45
- return [];
46
- }
47
-
48
- validateComponent(component: AdminForthComponentDeclaration, errors: Array<string>): AdminForthComponentDeclaration {
49
-
50
- if (!component) {
51
- return component;
52
- }
53
- let obj: AdminForthComponentDeclarationFull;
54
- if (typeof component === 'string') {
55
- obj = { file: component, meta: {} };
56
- } else {
57
- obj = component;
58
- }
59
-
60
- let ignoreExistsCheck = false;
61
- if (
62
- this.adminforth.codeInjector.allComponentNames.hasOwnProperty(
63
- (component as AdminForthComponentDeclarationFull).file)
64
- ) {
65
- // not obvious, but if we are in this if, it means that this is plugin component
66
- // if component is plugin component, we don't need to check if it exists in users folder
67
- ignoreExistsCheck = true;
68
- }
69
-
70
-
71
- if (!ignoreExistsCheck) {
72
- errors.push(...this.checkCustomFileExists(obj.file));
73
- }
74
-
75
- return obj;
76
- }
77
-
78
- validateConfig() {
79
- const errors = [];
80
-
81
- if (!this.config.customization.customComponentsDir) {
82
- this.config.customization.customComponentsDir = './custom';
83
- }
84
-
85
- try {
86
- // check customComponentsDir exists
87
- fs.accessSync(this.config.customization.customComponentsDir, fs.constants.R_OK);
88
- } catch (e) {
89
- this.config.customization.customComponentsDir = undefined;
90
- }
91
-
92
- if (!this.config.customization) {
93
- this.config.customization = {};
94
- }
95
-
96
- if (!this.config.customization.customComponentsDir) {
97
- this.config.customization.customComponentsDir = './custom';
98
- }
99
-
100
- try {
101
- // check customComponentsDir exists
102
- fs.accessSync(this.config.customization.customComponentsDir, fs.constants.R_OK);
103
- } catch (e) {
104
- this.config.customization.customComponentsDir = undefined;
105
- }
106
-
107
- if (this.config.customization.customPages) {
108
- this.config.customization.customPages.forEach((page, i) => {
109
- this.validateComponent(page.component, errors);
110
- });
111
- } else {
112
- this.config.customization.customPages = [];
113
- }
114
- if (!this.config.baseUrl) {
115
- this.config.baseUrl = '';
116
- }
117
- if (!this.config.baseUrl.endsWith('/')) {
118
- this.adminforth.baseUrlSlashed = this.config.baseUrl + '/';
119
- } else {
120
- this.adminforth.baseUrlSlashed = this.config.baseUrl;
121
- }
122
- if (this.config?.customization.brandName === undefined) {
123
- this.config.customization.brandName = 'AdminForth';
124
- }
125
- if (this.config.customization.loginPageInjections === undefined) {
126
- this.config.customization.loginPageInjections = {};
127
- }
128
- if (this.config.customization.globalInjections === undefined) {
129
- this.config.customization.globalInjections = {};
130
- }
131
- if (this.config.customization.loginPageInjections.underInputs === undefined) {
132
- this.config.customization.loginPageInjections.underInputs = [];
133
- }
134
- if (this.config.customization.brandLogo) {
135
- errors.push(...this.checkCustomFileExists(this.config.customization.brandLogo));
136
- }
137
- if (this.config.customization.showBrandNameInSidebar === undefined) {
138
- this.config.customization.showBrandNameInSidebar = true;
139
- }
140
- if (this.config.customization.favicon) {
141
- errors.push(...this.checkCustomFileExists(this.config.customization.favicon));
142
- }
143
-
144
- if (!this.config.customization.datesFormat) {
145
- this.config.customization.datesFormat = 'MMM D, YYYY';
146
- }
147
-
148
- if (!this.config.customization.timeFormat) {
149
- this.config.customization.timeFormat = 'HH:mm:ss';
150
- }
151
-
152
- if (this.config.resources) {
153
- this.config.resources.forEach((res: AdminForthResource) => {
154
- if (!res.table) {
155
- errors.push(`Resource "${res.dataSource}" is missing table`);
156
- }
157
- // if recordLabel is not callable, throw error
158
- if (res.recordLabel && typeof res.recordLabel !== 'function') {
159
- errors.push(`Resource "${res.dataSource}" recordLabel is not a function`);
160
- }
161
- if (!res.recordLabel) {
162
- res.recordLabel = (item) => {
163
- const pkVal = item[res.columns.find((col) => col.primaryKey).name];
164
- return `${res.label} ${pkVal}`;
165
- }
166
- }
167
-
168
-
169
- res.resourceId = res.resourceId || res.table;
170
- // as fallback value, capitalize and then replace _ with space
171
- res.label = res.label || res.resourceId.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
172
- if (!res.dataSource) {
173
- errors.push(`Resource "${res.resourceId}" is missing dataSource`);
174
- }
175
- if (!res.columns) {
176
- res.columns = [];
177
- }
178
- res.columns.forEach((col) => {
179
- col.label = col.label || guessLabelFromName(col.name);
180
- //define default sortable
181
- if (!Object.keys(col).includes('sortable')) { col.sortable = true; }
182
- if (col.showIn && !Array.isArray(col.showIn)) {
183
- errors.push(`Resource "${res.resourceId}" column "${col.name}" showIn must be an array`);
184
- }
185
-
186
- // check col.required is string or object
187
- if (col.required && !((typeof col.required === 'boolean') || (typeof col.required === 'object'))) {
188
- errors.push(`Resource "${res.resourceId}" column "${col.name}" required must be a string or object`);
189
- }
190
-
191
- // if it is object check the keys are one of ['create', 'edit']
192
- if (typeof col.required === 'object') {
193
- const wrongRequiredOn = Object.keys(col.required).find((c) => !['create', 'edit'].includes(c));
194
- if (wrongRequiredOn) {
195
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid required value "${wrongRequiredOn}", allowed keys are 'create', 'edit']`);
196
- }
197
- }
198
-
199
- // same for editingNote
200
- if (col.editingNote && !((typeof col.editingNote === 'string') || (typeof col.editingNote === 'object'))) {
201
- errors.push(`Resource "${res.resourceId}" column "${col.name}" editingNote must be a string or object`);
202
- }
203
- if (typeof col.editingNote === 'object') {
204
- const wrongEditingNoteOn = Object.keys(col.editingNote).find((c) => !['create', 'edit'].includes(c));
205
- if (wrongEditingNoteOn) {
206
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid editingNote value "${wrongEditingNoteOn}", allowed keys are 'create', 'edit']`);
207
- }
208
- }
209
-
210
- const wrongShowIn = col.showIn && col.showIn.find((c) => AdminForthResourcePages[c] === undefined);
211
- if (wrongShowIn) {
212
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid showIn value "${wrongShowIn}", allowed values are ${Object.keys(AdminForthResourcePages).join(', ')}`);
213
- }
214
- col.showIn = col.showIn || Object.values(AdminForthResourcePages);
215
-
216
- if (col.foreignResource) {
217
-
218
- if (!col.foreignResource.resourceId) {
219
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource without resourceId`);
220
- }
221
- const resource = this.config.resources.find((r) => r.resourceId === col.foreignResource.resourceId);
222
- if (!resource) {
223
- const similar = suggestIfTypo(this.config.resources.map((r) => r.resourceId), col.foreignResource.resourceId);
224
- errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource resourceId which is not in resources: "${col.foreignResource.resourceId}".
225
- ${similar ? `Did you mean "${similar}" instead of "${col.foreignResource.resourceId}"?` : ''}`);
226
- }
227
- const befHook = col.foreignResource.hooks?.dropdownList?.beforeDatasourceRequest;
228
- if (befHook) {
229
- if (!Array.isArray(befHook)) {
230
- col.foreignResource.hooks.dropdownList.beforeDatasourceRequest = [befHook];
231
- }
232
- }
233
- const aftHook = col.foreignResource.hooks?.dropdownList?.afterDatasourceResponse;
234
- if (aftHook) {
235
- if (!Array.isArray(aftHook)) {
236
- col.foreignResource.hooks.dropdownList.afterDatasourceResponse = [aftHook];
237
- }
238
- }
239
- }
240
- })
241
-
242
- if (!res.options) {
243
- res.options = { bulkActions: [], allowedActions: {} };
244
- }
245
-
246
- if (!res.options.allowedActions) {
247
- res.options.allowedActions = {
248
- all: true,
249
- };
250
- }
251
-
252
-
253
- if (Object.keys(res.options.allowedActions).includes('all')) {
254
- if (Object.keys(res.options.allowedActions).length > 1) {
255
- errors.push(`Resource "${res.resourceId}" allowedActions cannot have "all" and other keys at same time: ${Object.keys(res.options.allowedActions).join(', ')}`);
256
- }
257
- for (const key of Object.keys(AllowedActionsEnum)) {
258
- if (key !== 'all') {
259
- res.options.allowedActions[key] = res.options.allowedActions.all;
260
- }
261
- }
262
- delete res.options.allowedActions.all;
263
- } else {
264
- // by default allow all actions
265
- for (const key of Object.keys(AllowedActionsEnum)) {
266
- if (!Object.keys(res.options.allowedActions).includes(key)) {
267
- res.options.allowedActions[key] = true;
268
- }
269
- }
270
- }
271
-
272
-
273
- //check if resource has bulkActions
274
- let bulkActions: AdminForthBulkAction[] = res?.options?.bulkActions || [];
275
-
276
- if (!Array.isArray(bulkActions)) {
277
- errors.push(`Resource "${res.resourceId}" bulkActions must be an array`);
278
- bulkActions = [];
279
- }
280
-
281
- if (!bulkActions.find((action) => action.label === 'Delete checked')) {
282
- bulkActions.push({
283
- label: `Delete checked`,
284
- state: 'danger',
285
- icon: 'flowbite:trash-bin-outline',
286
- confirm: 'Are you sure you want to delete selected items?',
287
- allowed: async ({ resource, adminUser, allowedActions }) => { return allowedActions.delete },
288
- action: async ({ selectedIds, adminUser }) => {
289
- const connector = this.adminforth.connectors[res.dataSource];
290
-
291
- // for now if at least one error, stop and return error
292
- let error = null;
293
-
294
- await Promise.all(
295
- selectedIds.map(async (recordId) => {
296
- const record = await connector.getRecordByPrimaryKey(res, recordId);
297
-
298
- await Promise.all(
299
- (res.hooks.delete.beforeSave as AfterSaveFunction[]).map(
300
- async (hook) => {
301
- const resp = await hook({
302
- recordId: recordId,
303
- resource: res,
304
- record,
305
- adminUser,
306
- });
307
- if (!error && resp.error) {
308
- error = resp.error;
309
- }
310
- }
311
- )
312
- )
313
-
314
- if (error) {
315
- return;
316
- }
317
-
318
- await connector.deleteRecord({ resource: res, recordId });
319
- // call afterDelete hook
320
- await Promise.all(
321
- (res.hooks.delete.afterSave as AfterSaveFunction[]).map(
322
- async (hook) => {
323
- await hook({
324
- resource: res,
325
- record,
326
- adminUser,
327
- recordId: recordId
328
- });
329
- }
330
- )
331
- )
332
-
333
- })
334
- );
335
-
336
- if (error) {
337
- return { error, ok: false };
338
- }
339
- return { ok: true, successMessage: `${selectedIds.length} item${selectedIds.length > 1 ? 's' : ''} deleted` };
340
- }
341
- });
342
- }
343
-
344
- bulkActions.map((action) => {
345
- if (!action.id) {
346
- action.id = crypto.createHash('sha256').update(
347
- action.label,
348
- ).digest('hex');
349
- }
350
- });
351
- res.options.bulkActions = bulkActions;
352
-
353
- // if pageInjection is a string, make array with one element. Also check file exists
354
- const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems', 'customActionIcons'];
355
- const possiblePages = ['list', 'show', 'create', 'edit'];
356
-
357
-
358
-
359
-
360
- if (res.options.pageInjections) {
361
-
362
- Object.entries(res.options.pageInjections).map(([key, value]) => {
363
- if (!possiblePages.includes(key)) {
364
- const similar = suggestIfTypo(possiblePages, key);
365
- errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${key}", allowed keys are ${possiblePages.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
366
- }
367
-
368
- Object.entries(value).map(([injection, target]) => {
369
- if (possibleInjections.includes(injection)) {
370
- this.validateAndListifyInjection(res.options.pageInjections[key], injection, errors);
371
- } else {
372
- const similar = suggestIfTypo(possibleInjections, injection);
373
- errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')} ${similar ? `Did you mean "${similar}"?` : ''}`);
374
- }
375
- });
376
-
377
- })
378
-
379
- }
380
-
381
- // transform all hooks Functions to array of functions
382
- if (!res.hooks) {
383
- res.hooks = {};
384
- }
385
- for (const hookName of ['show', 'list']) {
386
- if (!res.hooks[hookName]) {
387
- res.hooks[hookName] = {};
388
- }
389
- if (!res.hooks[hookName].beforeDatasourceRequest) {
390
- res.hooks[hookName].beforeDatasourceRequest = [];
391
- }
392
-
393
- if (!Array.isArray(res.hooks[hookName].beforeDatasourceRequest)) {
394
- res.hooks[hookName].beforeDatasourceRequest = [res.hooks[hookName].beforeDatasourceRequest];
395
- }
396
-
397
- if (!res.hooks[hookName].afterDatasourceResponse) {
398
- res.hooks[hookName].afterDatasourceResponse = [];
399
- }
400
-
401
- if (!Array.isArray(res.hooks[hookName].afterDatasourceResponse)) {
402
- res.hooks[hookName].afterDatasourceResponse = [res.hooks[hookName].afterDatasourceResponse];
403
- }
404
- }
405
- for (const hookName of ['create', 'edit', 'delete']) {
406
- if (!res.hooks[hookName]) {
407
- res.hooks[hookName] = {};
408
- }
409
- if (!res.hooks[hookName].beforeSave) {
410
- res.hooks[hookName].beforeSave = [];
411
- }
412
- if (!Array.isArray(res.hooks[hookName].beforeSave)) {
413
- res.hooks[hookName].beforeSave = [res.hooks[hookName].beforeSave];
414
- }
415
- if (!res.hooks[hookName].afterSave) {
416
- res.hooks[hookName].afterSave = [];
417
- }
418
- if (!Array.isArray(res.hooks[hookName].afterSave)) {
419
- res.hooks[hookName].afterSave = [res.hooks[hookName].afterSave];
420
- }
421
- }
422
- });
423
-
424
- if (this.config.customization.globalInjections) {
425
- const ALLOWED_GLOBAL_INJECTIONS = ['userMenu', 'header', 'sidebar',]
426
- Object.keys(this.config.customization.globalInjections).forEach((injection) => {
427
- if (ALLOWED_GLOBAL_INJECTIONS.includes(injection)) {
428
- this.validateAndListifyInjection(this.config.customization.globalInjections, injection, errors);
429
- } else {
430
- const similar = suggestIfTypo(ALLOWED_GLOBAL_INJECTIONS, injection);
431
- errors.push(`Global injection key "${injection}" is not allowed. Allowed keys are ${ALLOWED_GLOBAL_INJECTIONS.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
432
- }
433
- });
434
- }
435
-
436
- if (!this.config.menu) {
437
- errors.push('No config.menu defined');
438
- }
439
-
440
- // check if there is only one homepage: true in menu, recursivly
441
- let homepages = 0;
442
- const browseMenu = (menu) => {
443
- menu.forEach((item) => {
444
- if (item.component && item.resourceId) {
445
- errors.push(`Menu item cannot have both component and resourceId: ${JSON.stringify(item)}`);
446
- }
447
- if (item.component && !item.path) {
448
- errors.push(`Menu item with component must have path : ${JSON.stringify(item)}`);
449
- }
450
-
451
- if (item.type === 'resource' && !item.resourceId) {
452
- errors.push(`Menu item with type 'resource' must have resourceId : ${JSON.stringify(item)}`);
453
- }
454
-
455
- if (item.resourceId && !this.config.resources.find((res) => res.resourceId === item.resourceId)) {
456
- const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId), item.resourceId);
457
- errors.push(`Menu item with type 'resourceId' has resourceId which is not in resources: "${JSON.stringify(item)}".
458
- ${similar ? `Did you mean "${similar}" instead of "${item.resourceId}"?` : ''}`);
459
- }
460
-
461
- if (item.type === 'component' && !item.component) {
462
- errors.push(`Menu item with type 'component' must have component : ${JSON.stringify(item)}`);
463
- }
464
-
465
- // make sure component starts with @@
466
- if (item.component) {
467
- if (!item.component.startsWith('@@')) {
468
- errors.push(`Menu item component must start with @@ : ${JSON.stringify(item)}`);
469
- }
470
-
471
- const path = item.component.replace('@@', this.config.customization.customComponentsDir);
472
- if (!fs.existsSync(path)) {
473
- errors.push(`Menu item component "${item.component.replace('@@', '')}" does not exist in "${this.config.customization.customComponentsDir}"`);
474
- }
475
- }
476
-
477
- if (item.homepage) {
478
- homepages++;
479
- if (homepages > 1) {
480
- errors.push('There must be only one homepage: true in menu, found second one in ' + JSON.stringify(item));
481
- }
482
- }
483
- if (item.children) {
484
- browseMenu(item.children);
485
- }
486
- });
487
- };
488
- browseMenu(this.config.menu);
489
-
490
- }
491
-
492
- if (this.config.auth) {
493
- // TODO: remove in future releases
494
- if (!this.config.auth.usersResourceId && this.config.auth.resourceId) {
495
- this.config.auth.usersResourceId = this.config.auth.resourceId;
496
- }
497
-
498
- if (!this.config.auth.usersResourceId) {
499
- throw new Error('No config.auth.usersResourceId defined');
500
- }
501
- if (!this.config.auth.passwordHashField) {
502
- throw new Error('No config.auth.passwordHashField defined');
503
- }
504
- if (!this.config.auth.usernameField) {
505
- throw new Error('No config.auth.usernameField defined');
506
- }
507
- if (this.config.auth.loginBackgroundImage) {
508
- errors.push(...this.checkCustomFileExists(this.config.auth.loginBackgroundImage));
509
- }
510
- const userResource = this.config.resources.find((res) => res.resourceId === this.config.auth.usersResourceId);
511
- if (!userResource) {
512
- const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId ), this.config.auth.usersResourceId);
513
- throw new Error(`Resource with id "${this.config.auth.usersResourceId}" not found. ${similar ? `Did you mean "${similar}"?` : ''}`);
514
- }
515
-
516
- if (!this.config.auth.beforeLoginConfirmation) {
517
- this.config.auth.beforeLoginConfirmation = [];
518
- }
519
- }
520
-
521
- // check for duplicate resourceIds and show which ones are duplicated
522
- const resourceIds = this.config.resources.map((res) => res.resourceId);
523
- const uniqueResourceIds = new Set(resourceIds);
524
- if (uniqueResourceIds.size != resourceIds.length) {
525
- const duplicates = resourceIds.filter((item, index) => resourceIds.indexOf(item) != index);
526
- errors.push(`Duplicate fields "resourceId" or "table": ${duplicates.join(', ')}`);
527
- }
528
-
529
- //add ids for onSelectedAllActions for each resource
530
- if (errors.length > 0) {
531
- throw new Error(`Invalid AdminForth config: ${errors.join(', ')}`);
532
- }
533
-
534
- // check is all custom components files exists
535
- for (const resource of this.config.resources) {
536
- for (const column of resource.columns) {
537
- if (column.components) {
538
-
539
- for (const [key, comp] of Object.entries(column.components as Record<string, AdminForthComponentDeclarationFull>)) {
540
-
541
- column.components[key] = this.validateComponent(comp, errors);
542
- }
543
- }
544
- }
545
- }
546
- }
547
-
548
- postProcessAfterDiscover(resource: AdminForthResource) {
549
- resource.columns.forEach((column) => {
550
- // if db/user says column is required in boolean, expand
551
- if (typeof column.required === 'boolean') {
552
- column.required = { create: column.required, edit: column.required };
553
- }
554
-
555
- if (!column.required) {
556
- column.required = { create: false, edit: false };
557
- }
558
-
559
- // same for editingNote
560
- if (typeof column.editingNote === 'string') {
561
- column.editingNote = { create: column.editingNote, edit: column.editingNote };
562
- }
563
- })
564
- resource.dataSourceColumns = resource.columns.filter((col) => !col.virtual);
565
- (resource.plugins || []).forEach((plugin) => {
566
- if (plugin.validateConfigAfterDiscover) {
567
- plugin.validateConfigAfterDiscover(this.adminforth, resource);
568
- }
569
- });
570
- }
571
- }
@@ -1,98 +0,0 @@
1
- import { IAdminForthFilter, IAdminForthSort, IOperationalResource, IAdminForthDataSourceConnectorBase, AdminForthResource } from '../types/AdminForthConfig.js';
2
-
3
-
4
- function filtersIfFilter(filter: IAdminForthFilter | IAdminForthFilter[] | undefined): IAdminForthFilter[] {
5
- if (!filter) {
6
- return [];
7
- }
8
- return (Array.isArray(filter) ? filter : [filter]) as IAdminForthFilter[];
9
- }
10
-
11
- function sortsIfSort(sort: IAdminForthSort | IAdminForthSort[]): IAdminForthSort[] {
12
- return (Array.isArray(sort) ? sort : [sort]) as IAdminForthSort[];
13
- }
14
-
15
- export default class OperationalResource implements IOperationalResource {
16
- dataConnector: IAdminForthDataSourceConnectorBase;
17
- resourceConfig: AdminForthResource;
18
-
19
- constructor(dataConnector: IAdminForthDataSourceConnectorBase, resourceConfig: AdminForthResource) {
20
- this.dataConnector = dataConnector;
21
- this.resourceConfig = resourceConfig;
22
- }
23
-
24
- async get(filter: IAdminForthFilter | IAdminForthFilter[]): Promise<any | null> {
25
- return (
26
- await this.dataConnector.getData({
27
- resource: this.resourceConfig,
28
- filters: filtersIfFilter(filter),
29
- limit: 1,
30
- offset: 0,
31
- sort: [],
32
- })
33
- ).data[0] || null;
34
- }
35
-
36
- async list(
37
- filter: IAdminForthFilter | IAdminForthFilter[],
38
- limit: number | null = null,
39
- offset: number | null = null,
40
- sort: IAdminForthSort | IAdminForthSort[] = []
41
- ): Promise<any[]> {
42
- // check if type of limit and offset is number
43
- if (limit !== null && typeof limit !== 'number') {
44
- throw new Error('Limit must be a number');
45
- }
46
- if (offset !== null && typeof offset !== 'number') {
47
- throw new Error('Offset must be a number');
48
- }
49
-
50
- let appliedLimit = limit;
51
- if (limit === null) {
52
- appliedLimit = 1000000000;
53
- }
54
- let appliedOffset = offset;
55
- if (offset === null) {
56
- appliedOffset = 0;
57
- }
58
-
59
- const { data } = await this.dataConnector.getData({
60
- resource: this.resourceConfig,
61
- filters: filtersIfFilter(filter),
62
- limit: appliedLimit,
63
- offset: appliedOffset,
64
- sort: sortsIfSort(sort),
65
- getTotals: false,
66
- });
67
- return data;
68
- }
69
-
70
- async count(filter: IAdminForthFilter | IAdminForthFilter[] | undefined): Promise<number> {
71
- return await this.dataConnector.getCount({
72
- resource: this.resourceConfig,
73
- filters: filtersIfFilter(filter),
74
- });
75
- }
76
-
77
- async create(recordValues: any): Promise<{ ok: boolean; createdRecord: any; error?: string; }> {
78
- const { ok, createdRecord, error } = await this.dataConnector.createRecord({
79
- resource: this.resourceConfig,
80
- record: recordValues,
81
- adminUser: null
82
- });
83
- return { ok, createdRecord, error };
84
- }
85
-
86
- async update(primaryKey: any, record: any): Promise<any> {
87
- return await this.dataConnector.updateRecord({
88
- resource: this.resourceConfig,
89
- recordId: primaryKey,
90
- newValues: record
91
- });
92
- }
93
-
94
- async delete(primaryKey: any): Promise<boolean> {
95
- return await this.dataConnector.deleteRecord({ resource: this.resourceConfig, recordId: primaryKey });
96
- }
97
-
98
- }