adminforth 1.2.98 → 1.3.1

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.
@@ -53,6 +53,43 @@ export var AdminForthResourcePages;
53
53
  AdminForthResourcePages["create"] = "create";
54
54
  AdminForthResourcePages["filter"] = "filter";
55
55
  })(AdminForthResourcePages || (AdminForthResourcePages = {}));
56
+ export class Filters {
57
+ static EQ(field, value) {
58
+ return { field, operator: AdminForthFilterOperators.EQ, value };
59
+ }
60
+ static NEQ(field, value) {
61
+ return { field, operator: AdminForthFilterOperators.NE, value };
62
+ }
63
+ static GT(field, value) {
64
+ return { field, operator: AdminForthFilterOperators.GT, value };
65
+ }
66
+ static GTE(field, value) {
67
+ return { field, operator: AdminForthFilterOperators.GTE, value };
68
+ }
69
+ static LT(field, value) {
70
+ return { field, operator: AdminForthFilterOperators.LT, value };
71
+ }
72
+ static LTE(field, value) {
73
+ return { field, operator: AdminForthFilterOperators.LTE, value };
74
+ }
75
+ static IN(field, value) {
76
+ return { field, operator: AdminForthFilterOperators.IN, value };
77
+ }
78
+ static NOT_IN(field, value) {
79
+ return { field, operator: AdminForthFilterOperators.NIN, value };
80
+ }
81
+ static LIKE(field, value) {
82
+ return { field, operator: AdminForthFilterOperators.LIKE, value };
83
+ }
84
+ }
85
+ export class Sorts {
86
+ static ASC(field) {
87
+ return { field, direction: AdminForthSortDirections.asc };
88
+ }
89
+ static DESC(field) {
90
+ return { field, direction: AdminForthSortDirections.desc };
91
+ }
92
+ }
56
93
  export var AllowedActionsEnum;
57
94
  (function (AllowedActionsEnum) {
58
95
  AllowedActionsEnum["show"] = "show";
package/index.ts CHANGED
@@ -5,12 +5,14 @@ import PostgresConnector from './dataConnectors/postgres.js';
5
5
  import SQLiteConnector from './dataConnectors/sqlite.js';
6
6
  import CodeInjector from './modules/codeInjector.js';
7
7
  import ExpressServer from './servers/express.js';
8
- import { ADMINFORTH_VERSION, listify } from './modules/utils.js';
8
+ import { ADMINFORTH_VERSION, listify, suggestIfTypo } from './modules/utils.js';
9
9
  import {
10
10
  type AdminForthConfig,
11
11
  type IAdminForth,
12
12
  type IConfigValidator,
13
- AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages, IHttpServer,
13
+ IOperationalResource,
14
+ AdminForthFilterOperators,
15
+ AdminForthDataTypes, AdminForthResourcePages, IHttpServer,
14
16
  BeforeSaveFunction,
15
17
  AfterSaveFunction,
16
18
  AdminUser,
@@ -20,11 +22,13 @@ import AdminForthPlugin from './basePlugin.js';
20
22
  import ConfigValidator from './modules/configValidator.js';
21
23
  import AdminForthRestAPI, { interpretResource } from './modules/restApi.js';
22
24
  import ClickhouseConnector from './dataConnectors/clickhouse.js';
25
+ import OperationalResource from './modules/operationalResource.js';
23
26
 
24
27
  // exports
25
28
  export * from './types/AdminForthConfig.js';
26
29
  export { interpretResource };
27
30
  export { AdminForthPlugin };
31
+ export { suggestIfTypo };
28
32
 
29
33
 
30
34
  class AdminForth implements IAdminForth {
@@ -50,7 +54,9 @@ class AdminForth implements IAdminForth {
50
54
  activatedPlugins: Array<AdminForthPlugin>;
51
55
  configValidator: IConfigValidator;
52
56
  restApi: AdminForthRestAPI;
53
-
57
+ operationalResources: {
58
+ [resourceId: string]: IOperationalResource,
59
+ }
54
60
  baseUrlSlashed: string;
55
61
 
56
62
  statuses: {
@@ -116,21 +122,24 @@ class AdminForth implements IAdminForth {
116
122
  this.config.dataSources.forEach((ds) => {
117
123
  const dbType = ds.url.split(':')[0];
118
124
  if (!this.config.databaseConnectors[dbType]) {
119
- throw new Error(`Database type ${dbType} is not supported, consider using databaseConnectors in AdminForth config`);
125
+ 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`);
120
126
  }
121
127
  this.connectors[ds.id] = new this.config.databaseConnectors[dbType]({url: ds.url});
122
128
  });
123
129
 
124
130
  await Promise.all(this.config.resources.map(async (res) => {
125
131
  if (!this.connectors[res.dataSource]) {
126
- throw new Error(`Resource '${res.table}' refers to unknown dataSource '${res.dataSource}'`);
132
+ const similar = suggestIfTypo(Object.keys(this.connectors), res.dataSource);
133
+ throw new Error(`Resource '${res.table}' refers to unknown dataSource '${res.dataSource}' ${similar
134
+ ? `. Did you mean '${similar}'?` : 'Available dataSources: '+Object.keys(this.connectors).join(', ')}`
135
+ );
127
136
  }
128
137
  const fieldTypes = await this.connectors[res.dataSource].discoverFields(res);
129
138
  if (fieldTypes !== null && !Object.keys(fieldTypes).length) {
130
139
  throw new Error(`Table '${res.table}' (In resource '${res.resourceId}') has no fields or does not exist`);
131
140
  }
132
141
  if (fieldTypes === null) {
133
- console.error(`DataSource ${res.dataSource} was not able to perform field discovery. It will not work properly`);
142
+ console.error(`⛔ DataSource ${res.dataSource} was not able to perform field discovery. It will not work properly`);
134
143
  return;
135
144
  }
136
145
  if (!res.columns) {
@@ -139,7 +148,8 @@ class AdminForth implements IAdminForth {
139
148
 
140
149
  res.columns.forEach((col, i) => {
141
150
  if (!fieldTypes[col.name] && !col.virtual) {
142
- throw new Error(`Resource '${res.table}' has no column '${col.name}'`);
151
+ const similar = suggestIfTypo(Object.keys(fieldTypes), col.name);
152
+ throw new Error(`Resource '${res.table}' has no column '${col.name}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
143
153
  }
144
154
  // first find discovered values, but allow override
145
155
  res.columns[i] = { ...fieldTypes[col.name], ...col };
@@ -156,6 +166,11 @@ class AdminForth implements IAdminForth {
156
166
 
157
167
  this.statuses.dbDiscover = 'done';
158
168
 
169
+ this.operationalResources = {};
170
+ this.config.resources.forEach((resource) => {
171
+ this.operationalResources[resource.resourceId] = new OperationalResource(this.connectors[resource.dataSource], resource);
172
+ });
173
+
159
174
  // console.log('⚙️⚙️⚙️ Database discovery done', JSON.stringify(this.config.resources, null, 2));
160
175
  }
161
176
 
@@ -164,9 +179,12 @@ class AdminForth implements IAdminForth {
164
179
  }
165
180
 
166
181
  async getUserByPk(pk: string) {
167
- const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.resourceId);
182
+ const resource = this.config.resources.find((res) => res.resourceId === this.config.auth.usersResourceId);
168
183
  if (!resource) {
169
- throw new Error('No auth resource found');
184
+ const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId), this.config.auth.usersResourceId);
185
+ throw new Error(`No resource with ${this.config.auth.usersResourceId} found. ${similar ?
186
+ `Did you mean '${similar}' in config.auth.usersResourceId?` : 'Please set correct resource in config.auth.usersResourceId'}`
187
+ );
170
188
  }
171
189
  const users = await this.connectors[resource.dataSource].getData({
172
190
  resource,
@@ -182,13 +200,6 @@ class AdminForth implements IAdminForth {
182
200
 
183
201
  async createResourceRecord({ resource, record, adminUser }: { resource: AdminForthResource, record: any, adminUser: AdminUser }) {
184
202
  for (const column of resource.columns) {
185
- if (column.fillOnCreate) {
186
- if (record[column.name] === undefined) {
187
- record[column.name] = column.fillOnCreate({
188
- initialRecord: record, adminUser
189
- });
190
- }
191
- }
192
203
  if (
193
204
  (column.required as {create?: boolean, edit?: boolean}) ?.create &&
194
205
  record[column.name] === undefined &&
@@ -231,7 +242,7 @@ class AdminForth implements IAdminForth {
231
242
  }
232
243
  const connector = this.connectors[resource.dataSource];
233
244
  process.env.HEAVY_DEBUG && console.log('🪲🪲🪲🪲 creating record createResourceRecord', record);
234
- await connector.createRecord({ resource, record });
245
+ await connector.createRecord({ resource, record, adminUser });
235
246
  // execute hook if needed
236
247
  for (const hook of listify(resource.hooks?.create?.afterSave as AfterSaveFunction[])) {
237
248
  console.log('Hook afterSave', hook);
@@ -248,6 +259,23 @@ class AdminForth implements IAdminForth {
248
259
  return { ok: true };
249
260
  }
250
261
 
262
+ resource(resourceId: string) {
263
+ if (this.statuses.dbDiscover !== 'done') {
264
+ if (this.statuses.dbDiscover === 'running') {
265
+ throw new Error('Database discovery is running. You can\'t use data API while database discovery is not finished.\n'+
266
+ 'Consider moving your code to a place where it will be executed after database discovery is already done (after await admin.discoverDatabases())');
267
+ } else {
268
+ throw new Error('Database discovery is not yet started. You can\'t use data API before database discovery is done. \n'+
269
+ 'Call admin.discoverDatabases() first and await it before using data API');
270
+ }
271
+ }
272
+ if (!this.operationalResources[resourceId]) {
273
+ const closeName = suggestIfTypo(Object.keys(this.operationalResources), resourceId);
274
+ throw new Error(`Resource with id '${resourceId}' not found${closeName ? `. Did you mean '${closeName}'?` : ''}`);
275
+ }
276
+ return this.operationalResources[resourceId];
277
+ }
278
+
251
279
  setupEndpoints(server: IHttpServer) {
252
280
  this.restApi.registerEndpoints(server);
253
281
  }
@@ -10,7 +10,7 @@ import {
10
10
 
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
 
15
15
  import crypto from 'crypto';
16
16
 
@@ -51,17 +51,6 @@ export default class ConfigValidator implements IConfigValidator {
51
51
  validateConfig() {
52
52
  const errors = [];
53
53
 
54
- if (this.config.rootUser) {
55
- if (!this.config.rootUser.username) {
56
- throw new Error('rootUser.username is required');
57
- }
58
- if (!this.config.rootUser.password) {
59
- throw new Error('rootUser.password is required');
60
- }
61
-
62
- 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');
63
- }
64
-
65
54
  if (!this.config.customization.customComponentsDir) {
66
55
  this.config.customization.customComponentsDir = './custom';
67
56
  }
@@ -74,8 +63,13 @@ export default class ConfigValidator implements IConfigValidator {
74
63
  }
75
64
 
76
65
  if (this.config.auth) {
77
- if (!this.config.auth.resourceId) {
78
- throw new Error('No config.auth.resourceId defined');
66
+ // TODO: remove in future releases
67
+ if (!this.config.auth.usersResourceId && this.config.auth.resourceId) {
68
+ this.config.auth.usersResourceId = this.config.auth.resourceId;
69
+ }
70
+
71
+ if (!this.config.auth.usersResourceId) {
72
+ throw new Error('No config.auth.usersResourceId defined');
79
73
  }
80
74
  if (!this.config.auth.passwordHashField) {
81
75
  throw new Error('No config.auth.passwordHashField defined');
@@ -86,9 +80,10 @@ export default class ConfigValidator implements IConfigValidator {
86
80
  if (this.config.auth.loginBackgroundImage) {
87
81
  errors.push(...this.checkCustomFileExists(this.config.auth.loginBackgroundImage));
88
82
  }
89
- const userResource = this.config.resources.find((res) => res.resourceId === this.config.auth.resourceId);
83
+ const userResource = this.config.resources.find((res) => res.resourceId === this.config.auth.usersResourceId);
90
84
  if (!userResource) {
91
- throw new Error(`Resource with id "${this.config.auth.resourceId}" not found`);
85
+ const similar = suggestIfTypo(this.config.resources.map((res) => res.resourceId || res.table), this.config.auth.usersResourceId);
86
+ throw new Error(`Resource with id "${this.config.auth.usersResourceId}" not found. ${similar ? `Did you mean "${similar}"?` : ''}`);
92
87
  }
93
88
 
94
89
  if (!this.config.auth.beforeLoginConfirmation) {
@@ -0,0 +1,73 @@
1
+ import { IAdminForthFilter, IAdminForthSort, IOperationalResource, IAdminForthDataSourceConnectorBase, AdminForthResource, IAdminForth } from '../types/AdminForthConfig.js';
2
+
3
+
4
+ function filtersIfFilter(filter: IAdminForthFilter | IAdminForthFilter[]): IAdminForthFilter[] {
5
+ return (Array.isArray(filter) ? filter : [filter]) as IAdminForthFilter[];
6
+ }
7
+
8
+ function sortsIfSort(sort: IAdminForthSort | IAdminForthSort[]): IAdminForthSort[] {
9
+ return (Array.isArray(sort) ? sort : [sort]) as IAdminForthSort[];
10
+ }
11
+
12
+ export default class OperationalResource implements IOperationalResource {
13
+ dataConnector: IAdminForthDataSourceConnectorBase;
14
+ resourceConfig: AdminForthResource;
15
+
16
+ constructor(dataConnector: IAdminForthDataSourceConnectorBase, resourceConfig: AdminForthResource) {
17
+ this.dataConnector = dataConnector;
18
+ this.resourceConfig = resourceConfig;
19
+ }
20
+
21
+ async get(filter: IAdminForthFilter | IAdminForthFilter[]): Promise<any | null> {
22
+ return (
23
+ await this.dataConnector.getData({
24
+ resource: this.resourceConfig,
25
+ filters: filtersIfFilter(filter),
26
+ limit: 1,
27
+ offset: 0,
28
+ sort: [],
29
+ })
30
+ ).data[0] || null;
31
+ }
32
+
33
+ async list(
34
+ filter: IAdminForthFilter | IAdminForthFilter[],
35
+ limit: number,
36
+ offset: number,
37
+ sort: IAdminForthSort | IAdminForthSort[]
38
+ ): Promise<any[]> {
39
+ const { data } = await this.dataConnector.getData({
40
+ resource: this.resourceConfig,
41
+ filters: filtersIfFilter(filter),
42
+ limit,
43
+ offset,
44
+ sort: sortsIfSort(sort),
45
+ getTotals: false,
46
+ });
47
+ return data;
48
+ }
49
+
50
+ async count(filter: IAdminForthFilter | IAdminForthFilter[]): Promise<number> {
51
+ return await this.dataConnector.getCount({
52
+ resource: this.resourceConfig,
53
+ filters: filtersIfFilter(filter),
54
+ });
55
+ }
56
+
57
+ async create(record: any): Promise<any> {
58
+ return await this.dataConnector.createRecord({ resource: this.resourceConfig, record, adminUser: null });
59
+ }
60
+
61
+ async update(primaryKey: any, record: any): Promise<any> {
62
+ return await this.dataConnector.updateRecord({
63
+ resource: this.resourceConfig,
64
+ recordId: primaryKey,
65
+ newValues: record
66
+ });
67
+ }
68
+
69
+ async delete(primaryKey: any): Promise<boolean> {
70
+ return await this.dataConnector.deleteRecord({ resource: this.resourceConfig, recordId: primaryKey });
71
+ }
72
+
73
+ }
@@ -67,73 +67,64 @@ export default class AdminForthRestAPI {
67
67
  let adminUser: AdminUser;
68
68
  let toReturn: { ok: boolean, redirectTo?: string, allowedLogin:boolean } = { ok: true, allowedLogin:true};
69
69
 
70
- let token;
71
- if (this.adminforth.config.rootUser
72
- && username === this.adminforth.config.rootUser.username
73
- && password === this.adminforth.config.rootUser.password
74
- ) {
75
- this.adminforth.auth.setAuthCookie({ response, username, pk: null });
76
- adminUser = { isRoot: true, dbUser: null, pk: null, username: this.adminforth.config.rootUser.username};
77
- } else {
78
- // get resource from db
79
- if (!this.adminforth.config.auth) {
80
- throw new Error('No config.auth defined');
81
- }
82
- const userResource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.resourceId);
83
- // if there is no passwordHashField, in columns, add it, with backendOnly and showIn: []
84
- if (!userResource.dataSourceColumns.find((col) => col.name === this.adminforth.config.auth.passwordHashField)) {
85
- userResource.dataSourceColumns.push({
86
- name: this.adminforth.config.auth.passwordHashField,
87
- backendOnly: true,
88
- showIn: [],
89
- type: AdminForthDataTypes.STRING,
90
- });
91
- console.log('Adding passwordHashField to userResource', userResource)
92
- }
93
-
94
- const userRecord = (
95
- await this.adminforth.connectors[userResource.dataSource].getData({
96
- resource: userResource,
97
- filters: [
98
- { field: this.adminforth.config.auth.usernameField, operator: AdminForthFilterOperators.EQ, value: username },
99
- ],
100
- limit: 1,
101
- offset: 0,
102
- sort: [],
103
- })
104
- ).data?.[0];
105
-
106
- if (!userRecord) {
107
- return { error: 'User not found' };
108
- }
109
-
110
- const passwordHash = userRecord[this.adminforth.config.auth.passwordHashField];
111
- const valid = await AdminForthAuth.verifyPassword(password, passwordHash);
112
- if (valid) {
113
- adminUser = {
114
- isRoot: false, dbUser: userRecord,
115
- pk: userRecord[userResource.columns.find((col) => col.primaryKey).name],
116
- username,
117
- };
118
- const beforeLoginConfirmation = this.adminforth.config.auth.beforeLoginConfirmation as (BeforeLoginConfirmationFunction[] | undefined);
119
- if (beforeLoginConfirmation?.length){
120
- for (const hook of beforeLoginConfirmation) {
121
- const resp = await hook({ adminUser, response });
122
-
123
- if (resp?.body?.redirectTo) {
124
- toReturn = {ok:resp.ok, redirectTo:resp?.body?.redirectTo, allowedLogin:resp?.body?.allowedLogin};
125
- break;
126
- }
70
+ // get resource from db
71
+ if (!this.adminforth.config.auth) {
72
+ throw new Error('No config.auth defined we need it to find user, please follow the docs');
73
+ }
74
+ const userResource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.usersResourceId);
75
+ // if there is no passwordHashField, in columns, add it, with backendOnly and showIn: []
76
+ if (!userResource.dataSourceColumns.find((col) => col.name === this.adminforth.config.auth.passwordHashField)) {
77
+ userResource.dataSourceColumns.push({
78
+ name: this.adminforth.config.auth.passwordHashField,
79
+ backendOnly: true,
80
+ showIn: [],
81
+ type: AdminForthDataTypes.STRING,
82
+ });
83
+ console.log('Adding passwordHashField to userResource', userResource)
84
+ }
85
+
86
+ const userRecord = (
87
+ await this.adminforth.connectors[userResource.dataSource].getData({
88
+ resource: userResource,
89
+ filters: [
90
+ { field: this.adminforth.config.auth.usernameField, operator: AdminForthFilterOperators.EQ, value: username },
91
+ ],
92
+ limit: 1,
93
+ offset: 0,
94
+ sort: [],
95
+ })
96
+ ).data?.[0];
97
+
98
+ if (!userRecord) {
99
+ return { error: 'User not found' };
100
+ }
101
+
102
+ const passwordHash = userRecord[this.adminforth.config.auth.passwordHashField];
103
+ const valid = await AdminForthAuth.verifyPassword(password, passwordHash);
104
+ if (valid) {
105
+ adminUser = {
106
+ dbUser: userRecord,
107
+ pk: userRecord[userResource.columns.find((col) => col.primaryKey).name],
108
+ username,
109
+ };
110
+ const beforeLoginConfirmation = this.adminforth.config.auth.beforeLoginConfirmation as (BeforeLoginConfirmationFunction[] | undefined);
111
+ if (beforeLoginConfirmation?.length){
112
+ for (const hook of beforeLoginConfirmation) {
113
+ const resp = await hook({ adminUser, response });
114
+
115
+ if (resp?.body?.redirectTo) {
116
+ toReturn = {ok:resp.ok, redirectTo:resp?.body?.redirectTo, allowedLogin:resp?.body?.allowedLogin};
117
+ break;
127
118
  }
128
119
  }
129
- if (toReturn.allowedLogin){
130
- this.adminforth.auth.setAuthCookie({ response, username, pk: userRecord[userResource.columns.find((col) => col.primaryKey).name] });
131
- }
132
- } else {
133
- return { error: INVALID_MESSAGE };
134
120
  }
135
-
121
+ if (toReturn.allowedLogin){
122
+ this.adminforth.auth.setAuthCookie({ response, username, pk: userRecord[userResource.columns.find((col) => col.primaryKey).name] });
123
+ }
124
+ } else {
125
+ return { error: INVALID_MESSAGE };
136
126
  }
127
+
137
128
 
138
129
  return toReturn;
139
130
  }
@@ -168,7 +159,7 @@ export default class AdminForthRestAPI {
168
159
  throw new Error('No config.auth defined');
169
160
  }
170
161
  const usernameField = this.adminforth.config.auth.usernameField;
171
- const resource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.resourceId);
162
+ const resource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.usersResourceId);
172
163
  const usernameColumn = resource.columns.find((col) => col.name === usernameField);
173
164
 
174
165
  return {
@@ -188,14 +179,10 @@ export default class AdminForthRestAPI {
188
179
  handler: async ({input, adminUser, cookies}) => {
189
180
  let username = ''
190
181
  let userFullName = ''
191
- if (adminUser.isRoot) {
192
- // isRoot can be in JWT token still when rootUser deleted, so we check for rootUser?"
193
- username = this.adminforth.config.rootUser?.username || 'RootUser';
194
- } else {
195
- const dbUser = adminUser.dbUser;
196
- username = dbUser[this.adminforth.config.auth.usernameField];
197
- userFullName =dbUser[this.adminforth.config.auth.userFullNameField];
198
- }
182
+
183
+ const dbUser = adminUser.dbUser;
184
+ username = dbUser[this.adminforth.config.auth.usernameField];
185
+ userFullName =dbUser[this.adminforth.config.auth.userFullNameField];
199
186
 
200
187
  const userData = {
201
188
  [this.adminforth.config.auth.usernameField]: username,
package/modules/utils.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import path from 'path';
2
2
  import { fileURLToPath } from 'url';
3
3
  import fs from 'fs';
4
+ import Fuse from 'fuse.js';
4
5
  // @ts-ignore-next-line
5
6
 
6
7
 
@@ -335,3 +336,24 @@ export function inverseRGBA(rgba) {
335
336
  return brightness > 128 ? 'rgba(0,0,0,1)' : 'rgba(255,255,255,1)';
336
337
  }
337
338
 
339
+
340
+ export function suggestIfTypo(names: string[], name: string): string {
341
+ console.log('names', names)
342
+ console.log('name', name)
343
+ if (!name) {
344
+ return null;
345
+ }
346
+ const options = {
347
+ includeScore: true, // Includes score in the results to see how close matches are
348
+ threshold: 0.3, // Defines the fuzziness (lower values mean stricter matches)
349
+ };
350
+
351
+ const fuse = new Fuse(names.filter(
352
+ (n) => !!n
353
+ ), options);
354
+ // Search for a resource
355
+ const result = fuse.search(name);
356
+ if (result.length > 0) {
357
+ return result[0].item;
358
+ }
359
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.2.98",
3
+ "version": "1.3.1",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -23,6 +23,7 @@
23
23
  "express": "^4.19.2",
24
24
  "filewatcher": "^3.0.1",
25
25
  "fs-extra": "^11.2.0",
26
+ "fuse.js": "^7.0.0",
26
27
  "jsonwebtoken": "^9.0.2",
27
28
  "mongodb": "6.6",
28
29
  "node-fetch": "^3.3.2",