@budibase/frontend-core 3.2.37 → 3.2.39

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 (36) hide show
  1. package/package.json +2 -2
  2. package/src/api/configs.ts +2 -2
  3. package/src/api/index.ts +4 -8
  4. package/src/api/types.ts +2 -3
  5. package/src/api/views.ts +12 -2
  6. package/src/api/viewsV2.ts +9 -4
  7. package/src/components/grid/lib/{renderers.js → renderers.ts} +14 -4
  8. package/src/components/grid/lib/{websocket.js → websocket.ts} +6 -4
  9. package/src/components/grid/stores/config.ts +1 -1
  10. package/src/components/grid/stores/datasource.ts +12 -10
  11. package/src/components/grid/stores/rows.ts +5 -4
  12. package/src/constants.ts +2 -2
  13. package/src/fetch/{CustomFetch.js → CustomFetch.ts} +24 -12
  14. package/src/fetch/{DataFetch.js → DataFetch.ts} +158 -94
  15. package/src/fetch/FieldFetch.ts +67 -0
  16. package/src/fetch/{GroupUserFetch.js → GroupUserFetch.ts} +21 -5
  17. package/src/fetch/{JSONArrayFetch.js → JSONArrayFetch.ts} +5 -3
  18. package/src/fetch/NestedProviderFetch.ts +40 -0
  19. package/src/fetch/{QueryArrayFetch.js → QueryArrayFetch.ts} +12 -6
  20. package/src/fetch/{QueryFetch.js → QueryFetch.ts} +27 -9
  21. package/src/fetch/RelationshipFetch.ts +49 -0
  22. package/src/fetch/{TableFetch.js → TableFetch.ts} +27 -5
  23. package/src/fetch/{UserFetch.js → UserFetch.ts} +31 -14
  24. package/src/fetch/ViewFetch.ts +51 -0
  25. package/src/fetch/{ViewV2Fetch.js → ViewV2Fetch.ts} +46 -24
  26. package/src/fetch/index.ts +93 -0
  27. package/src/index.ts +2 -1
  28. package/src/utils/json.d.ts +23 -0
  29. package/src/utils/utils.js +3 -0
  30. package/src/components/grid/lib/utils.js +0 -32
  31. package/src/fetch/FieldFetch.js +0 -44
  32. package/src/fetch/NestedProviderFetch.js +0 -21
  33. package/src/fetch/RelationshipFetch.js +0 -20
  34. package/src/fetch/ViewFetch.js +0 -23
  35. package/src/fetch/index.js +0 -57
  36. /package/src/components/grid/lib/{constants.js → constants.ts} +0 -0
@@ -1,25 +1,103 @@
1
- import { writable, derived, get } from "svelte/store"
1
+ import { writable, derived, get, Writable, Readable } from "svelte/store"
2
2
  import { cloneDeep } from "lodash/fp"
3
3
  import { QueryUtils } from "../utils"
4
4
  import { convertJSONSchemaToTableSchema } from "../utils/json"
5
- import { FieldType, SortOrder, SortType } from "@budibase/types"
5
+ import {
6
+ FieldType,
7
+ LegacyFilter,
8
+ Row,
9
+ SearchFilters,
10
+ SortOrder,
11
+ SortType,
12
+ TableSchema,
13
+ UISearchFilter,
14
+ } from "@budibase/types"
15
+ import { APIClient } from "../api/types"
16
+ import { DataFetchType } from "."
6
17
 
7
18
  const { buildQuery, limit: queryLimit, runQuery, sort } = QueryUtils
8
19
 
20
+ interface DataFetchStore<TDefinition, TQuery> {
21
+ rows: Row[]
22
+ info: any
23
+ schema: TableSchema | null
24
+ loading: boolean
25
+ loaded: boolean
26
+ query: TQuery
27
+ pageNumber: number
28
+ cursor: string | null
29
+ cursors: string[]
30
+ resetKey: string
31
+ error: {
32
+ message: string
33
+ status: number
34
+ } | null
35
+ definition?: TDefinition | null
36
+ }
37
+
38
+ interface DataFetchDerivedStore<TDefinition, TQuery>
39
+ extends DataFetchStore<TDefinition, TQuery> {
40
+ hasNextPage: boolean
41
+ hasPrevPage: boolean
42
+ supportsSearch: boolean
43
+ supportsSort: boolean
44
+ supportsPagination: boolean
45
+ }
46
+
47
+ export interface DataFetchParams<
48
+ TDatasource,
49
+ TQuery = SearchFilters | undefined
50
+ > {
51
+ API: APIClient
52
+ datasource: TDatasource
53
+ query: TQuery
54
+ options?: {}
55
+ }
56
+
9
57
  /**
10
58
  * Parent class which handles the implementation of fetching data from an
11
59
  * internal table or datasource plus.
12
60
  * For other types of datasource, this class is overridden and extended.
13
61
  */
14
- export default class DataFetch {
62
+ export default abstract class DataFetch<
63
+ TDatasource extends { type: DataFetchType },
64
+ TDefinition extends {
65
+ schema?: Record<string, any> | null
66
+ primaryDisplay?: string
67
+ },
68
+ TQuery extends {} = SearchFilters
69
+ > {
70
+ API: APIClient
71
+ features: {
72
+ supportsSearch: boolean
73
+ supportsSort: boolean
74
+ supportsPagination: boolean
75
+ }
76
+ options: {
77
+ datasource: TDatasource
78
+ limit: number
79
+ // Search config
80
+ filter: UISearchFilter | LegacyFilter[] | null
81
+ query: TQuery
82
+ // Sorting config
83
+ sortColumn: string | null
84
+ sortOrder: SortOrder
85
+ sortType: SortType | null
86
+ // Pagination config
87
+ paginate: boolean
88
+ // Client side feature customisation
89
+ clientSideSearching: boolean
90
+ clientSideSorting: boolean
91
+ clientSideLimiting: boolean
92
+ }
93
+ store: Writable<DataFetchStore<TDefinition, TQuery>>
94
+ derivedStore: Readable<DataFetchDerivedStore<TDefinition, TQuery>>
95
+
15
96
  /**
16
97
  * Constructs a new DataFetch instance.
17
98
  * @param opts the fetch options
18
99
  */
19
- constructor(opts) {
20
- // API client
21
- this.API = null
22
-
100
+ constructor(opts: DataFetchParams<TDatasource, TQuery>) {
23
101
  // Feature flags
24
102
  this.features = {
25
103
  supportsSearch: false,
@@ -29,12 +107,12 @@ export default class DataFetch {
29
107
 
30
108
  // Config
31
109
  this.options = {
32
- datasource: null,
110
+ datasource: opts.datasource,
33
111
  limit: 10,
34
112
 
35
113
  // Search config
36
114
  filter: null,
37
- query: null,
115
+ query: opts.query,
38
116
 
39
117
  // Sorting config
40
118
  sortColumn: null,
@@ -57,11 +135,11 @@ export default class DataFetch {
57
135
  schema: null,
58
136
  loading: false,
59
137
  loaded: false,
60
- query: null,
138
+ query: opts.query,
61
139
  pageNumber: 0,
62
140
  cursor: null,
63
141
  cursors: [],
64
- resetKey: Math.random(),
142
+ resetKey: Math.random().toString(),
65
143
  error: null,
66
144
  })
67
145
 
@@ -102,9 +180,6 @@ export default class DataFetch {
102
180
  this.store.update($store => ({ ...$store, loaded: true }))
103
181
  return
104
182
  }
105
-
106
- // Initially fetch data but don't bother waiting for the result
107
- this.getInitialData()
108
183
  }
109
184
 
110
185
  /**
@@ -118,7 +193,10 @@ export default class DataFetch {
118
193
  /**
119
194
  * Gets the default sort column for this datasource
120
195
  */
121
- getDefaultSortColumn(definition, schema) {
196
+ getDefaultSortColumn(
197
+ definition: { primaryDisplay?: string } | null,
198
+ schema: Record<string, any>
199
+ ): string | null {
122
200
  if (definition?.primaryDisplay && schema[definition.primaryDisplay]) {
123
201
  return definition.primaryDisplay
124
202
  } else {
@@ -130,13 +208,13 @@ export default class DataFetch {
130
208
  * Fetches a fresh set of data from the server, resetting pagination
131
209
  */
132
210
  async getInitialData() {
133
- const { datasource, filter, paginate } = this.options
211
+ const { filter, paginate } = this.options
134
212
 
135
213
  // Fetch datasource definition and extract sort properties if configured
136
- const definition = await this.getDefinition(datasource)
214
+ const definition = await this.getDefinition()
137
215
 
138
216
  // Determine feature flags
139
- const features = this.determineFeatureFlags(definition)
217
+ const features = await this.determineFeatureFlags()
140
218
  this.features = {
141
219
  supportsSearch: !!features?.supportsSearch,
142
220
  supportsSort: !!features?.supportsSort,
@@ -144,11 +222,11 @@ export default class DataFetch {
144
222
  }
145
223
 
146
224
  // Fetch and enrich schema
147
- let schema = this.getSchema(datasource, definition)
148
- schema = this.enrichSchema(schema)
225
+ let schema = this.getSchema(definition)
149
226
  if (!schema) {
150
227
  return
151
228
  }
229
+ schema = this.enrichSchema(schema)
152
230
 
153
231
  // If an invalid sort column is specified, delete it
154
232
  if (this.options.sortColumn && !schema[this.options.sortColumn]) {
@@ -172,20 +250,25 @@ export default class DataFetch {
172
250
  if (
173
251
  fieldSchema?.type === FieldType.NUMBER ||
174
252
  fieldSchema?.type === FieldType.BIGINT ||
175
- fieldSchema?.calculationType
253
+ ("calculationType" in fieldSchema && fieldSchema?.calculationType)
176
254
  ) {
177
255
  this.options.sortType = SortType.NUMBER
178
256
  }
257
+
179
258
  // If no sort order, default to ascending
180
259
  if (!this.options.sortOrder) {
181
260
  this.options.sortOrder = SortOrder.ASCENDING
261
+ } else {
262
+ // Ensure sortOrder matches the enum
263
+ this.options.sortOrder =
264
+ this.options.sortOrder.toLowerCase() as SortOrder
182
265
  }
183
266
  }
184
267
 
185
268
  // Build the query
186
269
  let query = this.options.query
187
270
  if (!query) {
188
- query = buildQuery(filter)
271
+ query = buildQuery(filter ?? undefined) as TQuery
189
272
  }
190
273
 
191
274
  // Update store
@@ -210,7 +293,7 @@ export default class DataFetch {
210
293
  info: page.info,
211
294
  cursors: paginate && page.hasNextPage ? [null, page.cursor] : [null],
212
295
  error: page.error,
213
- resetKey: Math.random(),
296
+ resetKey: Math.random().toString(),
214
297
  }))
215
298
  }
216
299
 
@@ -238,8 +321,8 @@ export default class DataFetch {
238
321
  }
239
322
 
240
323
  // If we don't support sorting, do a client-side sort
241
- if (!this.features.supportsSort && clientSideSorting) {
242
- rows = sort(rows, sortColumn, sortOrder, sortType)
324
+ if (!this.features.supportsSort && clientSideSorting && sortType) {
325
+ rows = sort(rows, sortColumn as any, sortOrder, sortType)
243
326
  }
244
327
 
245
328
  // If we don't support pagination, do a client-side limit
@@ -256,49 +339,28 @@ export default class DataFetch {
256
339
  }
257
340
  }
258
341
 
259
- /**
260
- * Fetches a single page of data from the remote resource.
261
- * Must be overridden by a datasource specific child class.
262
- */
263
- async getData() {
264
- return {
265
- rows: [],
266
- info: null,
267
- hasNextPage: false,
268
- cursor: null,
269
- }
270
- }
342
+ abstract getData(): Promise<{
343
+ rows: Row[]
344
+ info?: any
345
+ hasNextPage?: boolean
346
+ cursor?: any
347
+ error?: any
348
+ }>
271
349
 
272
350
  /**
273
351
  * Gets the definition for this datasource.
274
- * Defaults to fetching a table definition.
275
- * @param datasource
352
+
276
353
  * @return {object} the definition
277
354
  */
278
- async getDefinition(datasource) {
279
- if (!datasource?.tableId) {
280
- return null
281
- }
282
- try {
283
- return await this.API.fetchTableDefinition(datasource.tableId)
284
- } catch (error) {
285
- this.store.update(state => ({
286
- ...state,
287
- error,
288
- }))
289
- return null
290
- }
291
- }
355
+ abstract getDefinition(): Promise<TDefinition | null>
292
356
 
293
357
  /**
294
358
  * Gets the schema definition for a datasource.
295
- * Defaults to getting the "schema" property of the definition.
296
- * @param datasource the datasource
297
359
  * @param definition the datasource definition
298
360
  * @return {object} the schema
299
361
  */
300
- getSchema(datasource, definition) {
301
- return definition?.schema
362
+ getSchema(definition: TDefinition | null): Record<string, any> | undefined {
363
+ return definition?.schema ?? undefined
302
364
  }
303
365
 
304
366
  /**
@@ -307,53 +369,56 @@ export default class DataFetch {
307
369
  * @param schema the datasource schema
308
370
  * @return {object} the enriched datasource schema
309
371
  */
310
- enrichSchema(schema) {
311
- if (schema == null) {
312
- return null
313
- }
314
-
372
+ enrichSchema(schema: TableSchema): TableSchema {
315
373
  // Check for any JSON fields so we can add any top level properties
316
- let jsonAdditions = {}
317
- Object.keys(schema).forEach(fieldKey => {
374
+ let jsonAdditions: Record<string, { type: string; nestedJSON: true }> = {}
375
+ for (const fieldKey of Object.keys(schema)) {
318
376
  const fieldSchema = schema[fieldKey]
319
- if (fieldSchema?.type === FieldType.JSON) {
377
+ if (fieldSchema.type === FieldType.JSON) {
320
378
  const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
321
379
  squashObjects: true,
322
- })
323
- Object.keys(jsonSchema).forEach(jsonKey => {
324
- jsonAdditions[`${fieldKey}.${jsonKey}`] = {
325
- type: jsonSchema[jsonKey].type,
326
- nestedJSON: true,
380
+ }) as Record<string, { type: string }> | null // TODO: remove when convertJSONSchemaToTableSchema is typed
381
+ if (jsonSchema) {
382
+ for (const jsonKey of Object.keys(jsonSchema)) {
383
+ jsonAdditions[`${fieldKey}.${jsonKey}`] = {
384
+ type: jsonSchema[jsonKey].type,
385
+ nestedJSON: true,
386
+ }
327
387
  }
328
- })
388
+ }
329
389
  }
330
- })
331
- schema = { ...schema, ...jsonAdditions }
390
+ }
332
391
 
333
392
  // Ensure schema is in the correct structure
334
- let enrichedSchema = {}
335
- Object.entries(schema).forEach(([fieldName, fieldSchema]) => {
336
- if (typeof fieldSchema === "string") {
337
- enrichedSchema[fieldName] = {
338
- type: fieldSchema,
339
- name: fieldName,
340
- }
341
- } else {
342
- enrichedSchema[fieldName] = {
343
- ...fieldSchema,
344
- name: fieldName,
393
+ let enrichedSchema: TableSchema = {}
394
+ Object.entries({ ...schema, ...jsonAdditions }).forEach(
395
+ ([fieldName, fieldSchema]) => {
396
+ if (typeof fieldSchema === "string") {
397
+ enrichedSchema[fieldName] = {
398
+ type: fieldSchema,
399
+ name: fieldName,
400
+ }
401
+ } else {
402
+ enrichedSchema[fieldName] = {
403
+ ...fieldSchema,
404
+ type: fieldSchema.type as any, // TODO: check type union definition conflicts
405
+ name: fieldName,
406
+ }
345
407
  }
346
408
  }
347
- })
409
+ )
348
410
 
349
411
  return enrichedSchema
350
412
  }
351
413
 
352
414
  /**
353
- * Determine the feature flag for this datasource definition
354
- * @param definition
415
+ * Determine the feature flag for this datasource
355
416
  */
356
- determineFeatureFlags(_definition) {
417
+ async determineFeatureFlags(): Promise<{
418
+ supportsPagination: boolean
419
+ supportsSearch?: boolean
420
+ supportsSort?: boolean
421
+ }> {
357
422
  return {
358
423
  supportsSearch: false,
359
424
  supportsSort: false,
@@ -365,12 +430,11 @@ export default class DataFetch {
365
430
  * Resets the data set and updates options
366
431
  * @param newOptions any new options
367
432
  */
368
- async update(newOptions) {
433
+ async update(newOptions: any) {
369
434
  // Check if any settings have actually changed
370
435
  let refresh = false
371
- const entries = Object.entries(newOptions || {})
372
- for (let [key, value] of entries) {
373
- const oldVal = this.options[key] == null ? null : this.options[key]
436
+ for (const [key, value] of Object.entries(newOptions || {})) {
437
+ const oldVal = this.options[key as keyof typeof this.options] ?? null
374
438
  const newVal = value == null ? null : value
375
439
  if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
376
440
  refresh = true
@@ -437,7 +501,7 @@ export default class DataFetch {
437
501
  * @param state the current store state
438
502
  * @return {boolean} whether there is a next page of data or not
439
503
  */
440
- hasNextPage(state) {
504
+ private hasNextPage(state: DataFetchStore<TDefinition, TQuery>): boolean {
441
505
  return state.cursors[state.pageNumber + 1] != null
442
506
  }
443
507
 
@@ -447,7 +511,7 @@ export default class DataFetch {
447
511
  * @param state the current store state
448
512
  * @return {boolean} whether there is a previous page of data or not
449
513
  */
450
- hasPrevPage(state) {
514
+ private hasPrevPage(state: { pageNumber: number }): boolean {
451
515
  return state.pageNumber > 0
452
516
  }
453
517
 
@@ -0,0 +1,67 @@
1
+ import { Row } from "@budibase/types"
2
+ import DataFetch from "./DataFetch"
3
+
4
+ type Types = "field" | "queryarray" | "jsonarray"
5
+
6
+ export interface FieldDatasource<TType extends Types> {
7
+ type: TType
8
+ tableId: string
9
+ fieldType: "attachment" | "array"
10
+ value: string[] | Row[]
11
+ }
12
+
13
+ export interface FieldDefinition {
14
+ schema?: Record<string, { type: string }> | null
15
+ }
16
+
17
+ function isArrayOfStrings(value: string[] | Row[]): value is string[] {
18
+ return Array.isArray(value) && !!value[0] && typeof value[0] !== "object"
19
+ }
20
+
21
+ export default class FieldFetch<TType extends Types> extends DataFetch<
22
+ FieldDatasource<TType>,
23
+ FieldDefinition
24
+ > {
25
+ async getDefinition(): Promise<FieldDefinition | null> {
26
+ const { datasource } = this.options
27
+
28
+ // Field sources have their schema statically defined
29
+ let schema
30
+ if (datasource.fieldType === "attachment") {
31
+ schema = {
32
+ url: {
33
+ type: "string",
34
+ },
35
+ name: {
36
+ type: "string",
37
+ },
38
+ }
39
+ } else if (datasource.fieldType === "array") {
40
+ schema = {
41
+ value: {
42
+ type: "string",
43
+ },
44
+ }
45
+ }
46
+ return { schema }
47
+ }
48
+
49
+ async getData() {
50
+ const { datasource } = this.options
51
+
52
+ // These sources will be available directly from context
53
+ const data = datasource?.value || []
54
+ let rows: Row[]
55
+ if (isArrayOfStrings(data)) {
56
+ rows = data.map(value => ({ value }))
57
+ } else {
58
+ rows = data
59
+ }
60
+
61
+ return {
62
+ rows: rows || [],
63
+ hasNextPage: false,
64
+ cursor: null,
65
+ }
66
+ }
67
+ }
@@ -1,18 +1,33 @@
1
1
  import { get } from "svelte/store"
2
- import DataFetch from "./DataFetch.js"
2
+ import DataFetch, { DataFetchParams } from "./DataFetch"
3
3
  import { TableNames } from "../constants"
4
4
 
5
- export default class GroupUserFetch extends DataFetch {
6
- constructor(opts) {
5
+ interface GroupUserQuery {
6
+ groupId: string
7
+ emailSearch: string
8
+ }
9
+
10
+ interface GroupUserDatasource {
11
+ type: "groupUser"
12
+ tableId: TableNames.USERS
13
+ }
14
+
15
+ export default class GroupUserFetch extends DataFetch<
16
+ GroupUserDatasource,
17
+ {},
18
+ GroupUserQuery
19
+ > {
20
+ constructor(opts: DataFetchParams<GroupUserDatasource, GroupUserQuery>) {
7
21
  super({
8
22
  ...opts,
9
23
  datasource: {
24
+ type: "groupUser",
10
25
  tableId: TableNames.USERS,
11
26
  },
12
27
  })
13
28
  }
14
29
 
15
- determineFeatureFlags() {
30
+ async determineFeatureFlags() {
16
31
  return {
17
32
  supportsSearch: true,
18
33
  supportsSort: false,
@@ -28,11 +43,12 @@ export default class GroupUserFetch extends DataFetch {
28
43
 
29
44
  async getData() {
30
45
  const { query, cursor } = get(this.store)
46
+
31
47
  try {
32
48
  const res = await this.API.getGroupUsers({
33
49
  id: query.groupId,
34
50
  emailSearch: query.emailSearch,
35
- bookmark: cursor,
51
+ bookmark: cursor ?? undefined,
36
52
  })
37
53
 
38
54
  return {
@@ -1,8 +1,10 @@
1
- import FieldFetch from "./FieldFetch.js"
1
+ import FieldFetch from "./FieldFetch"
2
2
  import { getJSONArrayDatasourceSchema } from "../utils/json"
3
3
 
4
- export default class JSONArrayFetch extends FieldFetch {
5
- async getDefinition(datasource) {
4
+ export default class JSONArrayFetch extends FieldFetch<"jsonarray"> {
5
+ async getDefinition() {
6
+ const { datasource } = this.options
7
+
6
8
  // JSON arrays need their table definitions fetched.
7
9
  // We can then extract their schema as a subset of the table schema.
8
10
  try {
@@ -0,0 +1,40 @@
1
+ import { Row, TableSchema } from "@budibase/types"
2
+ import DataFetch from "./DataFetch"
3
+
4
+ interface NestedProviderDatasource {
5
+ type: "provider"
6
+ value?: {
7
+ schema: TableSchema
8
+ primaryDisplay: string
9
+ rows: Row[]
10
+ }
11
+ }
12
+
13
+ interface NestedProviderDefinition {
14
+ schema?: TableSchema
15
+ primaryDisplay?: string
16
+ }
17
+ export default class NestedProviderFetch extends DataFetch<
18
+ NestedProviderDatasource,
19
+ NestedProviderDefinition
20
+ > {
21
+ async getDefinition() {
22
+ const { datasource } = this.options
23
+
24
+ // Nested providers should already have exposed their own schema
25
+ return {
26
+ schema: datasource?.value?.schema,
27
+ primaryDisplay: datasource?.value?.primaryDisplay,
28
+ }
29
+ }
30
+
31
+ async getData() {
32
+ const { datasource } = this.options
33
+ // Pull the rows from the existing data provider
34
+ return {
35
+ rows: datasource?.value?.rows || [],
36
+ hasNextPage: false,
37
+ cursor: null,
38
+ }
39
+ }
40
+ }
@@ -1,11 +1,13 @@
1
- import FieldFetch from "./FieldFetch.js"
1
+ import FieldFetch from "./FieldFetch"
2
2
  import {
3
3
  getJSONArrayDatasourceSchema,
4
4
  generateQueryArraySchemas,
5
5
  } from "../utils/json"
6
6
 
7
- export default class QueryArrayFetch extends FieldFetch {
8
- async getDefinition(datasource) {
7
+ export default class QueryArrayFetch extends FieldFetch<"queryarray"> {
8
+ async getDefinition() {
9
+ const { datasource } = this.options
10
+
9
11
  if (!datasource?.tableId) {
10
12
  return null
11
13
  }
@@ -14,10 +16,14 @@ export default class QueryArrayFetch extends FieldFetch {
14
16
  try {
15
17
  const table = await this.API.fetchQueryDefinition(datasource.tableId)
16
18
  const schema = generateQueryArraySchemas(
17
- table?.schema,
18
- table?.nestedSchemaFields
19
+ table.schema,
20
+ table.nestedSchemaFields
19
21
  )
20
- return { schema: getJSONArrayDatasourceSchema(schema, datasource) }
22
+ const result = {
23
+ schema: getJSONArrayDatasourceSchema(schema, datasource),
24
+ }
25
+
26
+ return result
21
27
  } catch (error) {
22
28
  return null
23
29
  }