@sitecore-feaas/clientside 0.3.0 → 0.3.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 (108) hide show
  1. package/README.md +8 -6
  2. package/build/components/FEAASComponent.js +8 -4
  3. package/build/components/FEAASComponent.js.map +1 -1
  4. package/build/components/FEAASElement.js +18 -4
  5. package/build/components/FEAASElement.js.map +1 -1
  6. package/build/dom/rendering.js +1 -1
  7. package/build/dom/rendering.js.map +1 -1
  8. package/build/headless.js +4 -5
  9. package/build/headless.js.map +1 -1
  10. package/build/ui/FEAASExternal.js +8 -0
  11. package/build/ui/FEAASExternal.js.map +1 -1
  12. package/build/ui/FEAASPicker.js +10 -1
  13. package/build/ui/FEAASPicker.js.map +1 -1
  14. package/build/ui/react.js +18 -10
  15. package/build/ui/react.js.map +1 -1
  16. package/build/{dom → utils}/data.js +3 -0
  17. package/build/utils/data.js.map +1 -0
  18. package/build/utils/datapath.js +6 -2
  19. package/build/utils/datapath.js.map +1 -1
  20. package/build/utils/settings.js +154 -52
  21. package/build/utils/settings.js.map +1 -1
  22. package/dist/browser/headless.cjs +6 -6
  23. package/dist/browser/headless.cjs.map +4 -4
  24. package/dist/browser/headless.esm.js +6 -6
  25. package/dist/browser/headless.esm.js.map +1 -1
  26. package/dist/browser/index.cjs +7 -7
  27. package/dist/browser/index.cjs.map +4 -4
  28. package/dist/browser/index.esm.js +7 -7
  29. package/dist/browser/index.esm.js.map +1 -1
  30. package/dist/browser/react.cjs +6 -6
  31. package/dist/browser/react.cjs.map +4 -4
  32. package/dist/browser/react.esm.js +6 -6
  33. package/dist/browser/react.esm.js.map +1 -1
  34. package/dist/node/headless.cjs +4 -4
  35. package/dist/node/headless.cjs.map +4 -4
  36. package/dist/node/headless.esm.js +4 -4
  37. package/dist/node/headless.esm.js.map +4 -4
  38. package/dist/node/index.cjs +6 -6
  39. package/dist/node/index.cjs.map +4 -4
  40. package/dist/node/index.esm.js +6 -6
  41. package/dist/node/index.esm.js.map +4 -4
  42. package/dist/node/react.cjs +5 -5
  43. package/dist/node/react.cjs.map +4 -4
  44. package/dist/node/react.esm.js +5 -5
  45. package/dist/node/react.esm.js.map +4 -4
  46. package/headless.cjs +4 -4
  47. package/headless.cjs.map +4 -4
  48. package/headless.js +4 -4
  49. package/headless.js.map +4 -4
  50. package/package.json +1 -1
  51. package/react.cjs +6 -6
  52. package/react.cjs.map +4 -4
  53. package/react.js +6 -6
  54. package/react.js.map +4 -4
  55. package/src/components/FEAASComponent.tsx +11 -5
  56. package/src/components/FEAASElement.tsx +18 -3
  57. package/src/dom/component.ts +2 -2
  58. package/src/dom/html.ts +2 -2
  59. package/src/dom/rendering.ts +9 -7
  60. package/src/headless.ts +5 -5
  61. package/src/ui/FEAASExternal.tsx +10 -0
  62. package/src/ui/FEAASPicker.tsx +10 -2
  63. package/src/ui/react.tsx +24 -11
  64. package/src/{dom → utils}/data.ts +3 -1
  65. package/src/utils/datapath.ts +7 -2
  66. package/src/utils/settings.ts +184 -53
  67. package/test/FEAASComponentData.test.tsx +56 -52
  68. package/test/ServerComponent.test.tsx +14 -3
  69. package/test/cdn.ts +1 -1
  70. package/test/datasettings.test.ts +248 -0
  71. package/test/{extras.ts → extras.test.ts} +1 -1
  72. package/test/fetch.ts +2 -2
  73. package/types/components/FEAASComponent.d.ts +7 -4
  74. package/types/components/FEAASComponent.d.ts.map +1 -1
  75. package/types/components/FEAASElement.d.ts +1 -0
  76. package/types/components/FEAASElement.d.ts.map +1 -1
  77. package/types/dom/component.d.ts +2 -2
  78. package/types/dom/component.d.ts.map +1 -1
  79. package/types/dom/html.d.ts +2 -2
  80. package/types/dom/html.d.ts.map +1 -1
  81. package/types/dom/rendering.d.ts +8 -7
  82. package/types/dom/rendering.d.ts.map +1 -1
  83. package/types/headless.d.ts +5 -5
  84. package/types/headless.d.ts.map +1 -1
  85. package/types/tsbuildinfo +1 -1
  86. package/types/ui/FEAASExternal.d.ts +4 -0
  87. package/types/ui/FEAASExternal.d.ts.map +1 -1
  88. package/types/ui/FEAASPicker.d.ts +3 -3
  89. package/types/ui/FEAASPicker.d.ts.map +1 -1
  90. package/types/ui/react.d.ts +7 -5
  91. package/types/ui/react.d.ts.map +1 -1
  92. package/types/ui/tsbuildinfo +1 -1
  93. package/types/{dom → utils}/data.d.ts +1 -1
  94. package/types/utils/data.d.ts.map +1 -0
  95. package/types/utils/datapath.d.ts +1 -0
  96. package/types/utils/datapath.d.ts.map +1 -1
  97. package/types/utils/settings.d.ts +85 -9
  98. package/types/utils/settings.d.ts.map +1 -1
  99. package/build/dom/data.js.map +0 -1
  100. package/build/utils/fetch.js +0 -40
  101. package/build/utils/fetch.js.map +0 -1
  102. package/src/utils/fetch.ts +0 -60
  103. package/test/cjs/tsconfig.json +0 -9
  104. package/test/fetch-data.ts +0 -55
  105. package/test/settings.test.ts +0 -78
  106. package/types/dom/data.d.ts.map +0 -1
  107. package/types/utils/fetch.d.ts +0 -9
  108. package/types/utils/fetch.d.ts.map +0 -1
@@ -2,7 +2,7 @@ import { DEFAULT_CDN_URL, FEAASCDNComponentParams, fetchAndRevalidateComponent,
2
2
  import { renderDOMContent, renderDOMElement } from '../dom/rendering.js'
3
3
  import { loadStylesheetAllowStale } from '../headless.js'
4
4
  import { ControlledPromise } from '../utils/promise.js'
5
- import { fetchData } from '../utils/fetch.js'
5
+ import { DataOptions, DataSettings } from '../utils/settings.js'
6
6
  import { FEAASElement } from './FEAASElement.js'
7
7
 
8
8
  export function FEAASComponentsProps(element: FEAASElement) {
@@ -25,7 +25,7 @@ export function FEAASComponentsProps(element: FEAASElement) {
25
25
  }
26
26
 
27
27
  return {
28
- data: data || {},
28
+ data: (data || {}) as DataOptions,
29
29
  cdn: element.getContextAttribute('cdn') ?? cdn ?? DEFAULT_CDN_URL,
30
30
  library: element.getAttribute('library') ?? library,
31
31
  component: element.getAttribute('component') ?? component,
@@ -94,12 +94,13 @@ export class FEAASComponent extends FEAASElement<FEAASComponentsProps, string> {
94
94
  version: 'responsive',
95
95
  withStylesheet: true,
96
96
  editable: false,
97
- suspended: false
97
+ suspended: false,
98
+ data: {}
98
99
  } as Partial<FEAASComponentsProps>
99
100
 
100
101
  needsRefresh: boolean
101
102
 
102
- setData(data: any) {
103
+ setData(data: DataOptions) {
103
104
  this.set({ data })
104
105
  }
105
106
 
@@ -107,6 +108,10 @@ export class FEAASComponent extends FEAASElement<FEAASComponentsProps, string> {
107
108
  return FEAASComponentsProps(this)
108
109
  }
109
110
 
111
+ isJSONAttribute(prop: keyof FEAASComponentsProps) {
112
+ return prop == 'data'
113
+ }
114
+
110
115
  attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
111
116
  if (oldValue === newValue) return
112
117
 
@@ -130,7 +135,7 @@ export class FEAASComponent extends FEAASElement<FEAASComponentsProps, string> {
130
135
 
131
136
  this.whenDataReady = this.whenDataReady.restart()
132
137
 
133
- const data = await fetchData(this.props.data)
138
+ const data = await DataSettings.fetch(this.props.data)
134
139
 
135
140
  const alreadyRendered = this.alreadyRendered
136
141
 
@@ -253,6 +258,7 @@ declare global {
253
258
  interface IntrinsicElements {
254
259
  'feaas-component': {
255
260
  data?: any
261
+ suppressHydrationWarning?: boolean
256
262
  template?: string
257
263
  instance?: string
258
264
  editable?: boolean
@@ -137,6 +137,10 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
137
137
  }
138
138
  }
139
139
 
140
+ isJSONAttribute(attribute: keyof Props) {
141
+ return false
142
+ }
143
+
140
144
  log(...args: any) {
141
145
  if (typeof process != 'undefined' && process.env.TESTING) return
142
146
  console.log(...args)
@@ -445,12 +449,23 @@ export class FEAASElement<Props = any, Payload = any> extends WebComponent {
445
449
  ...this.overrides
446
450
  }
447
451
  for (var property in this.props) {
452
+ const isJSON = this.isJSONAttribute(property)
448
453
  const value = this.props[property]
454
+ const valueAsString = isJSON ? JSON.stringify(value) : String(value)
455
+ const defaultValue = this.defaultProps[property as keyof typeof this.defaultProps]
456
+ const defaultValueAsString = isJSON ? JSON.stringify(defaultValue) : String(defaultValue)
449
457
  const attribute = property.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
450
- if (value == null || value === this.defaultProps[property as keyof typeof this.defaultProps]) {
458
+ const oldValue = this.getAttribute(attribute)
459
+ try {
460
+ var odlValueAsString: string = isJSON ? JSON.stringify(JSON.parse(oldValue)) : oldValue
461
+ } catch (e) {
462
+ var odlValueAsString: string = null
463
+ }
464
+
465
+ if (value == null || valueAsString === defaultValueAsString) {
451
466
  this.removeAttribute(attribute)
452
- } else if (typeof value == 'string' || typeof value == 'number' || typeof value == 'boolean') {
453
- if (this.getAttribute(attribute) != String(value)) this.setAttribute(attribute, String(value))
467
+ } else if (typeof value == 'string' || typeof value == 'number' || typeof value == 'boolean' || isJSON) {
468
+ if (odlValueAsString != valueAsString) this.setAttribute(attribute, valueAsString)
454
469
  }
455
470
  }
456
471
  } catch (e) {
@@ -1,8 +1,8 @@
1
1
  import { fetchAndRevalidateComponent } from '../cdn.js'
2
- import { Datascopes } from './data.js'
2
+ import { DataScopes } from '../utils/settings.js'
3
3
  import { FEAASComponentProps, renderDOMContent, renderDOMElement } from './rendering.js'
4
4
 
5
- export function renderComponentElement(content: string, datasources?: Datascopes) {
5
+ export function renderComponentElement(content: string, datasources?: DataScopes) {
6
6
  const wrapper = document.createElement('div')
7
7
  wrapper.innerHTML = content
8
8
  const fragment = document.createDocumentFragment()
package/src/dom/html.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Lightweight DOM parser and serializer to use in server environments */
2
2
 
3
- import { Datascopes } from './data.js'
3
+ import { DataScopes } from '../utils/settings.js'
4
4
  import { FEAASCustomizations, renderDOMElement } from './rendering.js'
5
5
 
6
6
  type HTMLAttribute = { name: string; value: string }
@@ -323,7 +323,7 @@ export function serializeHTML(nodes: HTMLNode[]): string {
323
323
  .join('')
324
324
  }
325
325
 
326
- export function renderHTMLContent(template: string, data?: Datascopes, config?: FEAASCustomizations) {
326
+ export function renderHTMLContent(template: string, data?: DataScopes, config?: FEAASCustomizations) {
327
327
  const newTemplate = config?.processTemplate?.(null, template, data) || template
328
328
  const nodes = parseHTML(newTemplate)
329
329
  return renderDOMElement(nodes[0] as unknown as HTMLElement, data, config).outerHTML
@@ -1,22 +1,24 @@
1
1
  import { FEAASCDNComponentProps } from '../cdn.js'
2
- import { Datascopes, queryScopes, serializeCH1RichText } from './data.js'
2
+ import { queryScopes, serializeCH1RichText } from '../utils/data.js'
3
+ import { Data, DataScopes } from '../utils/settings.js'
3
4
  import { parseHTML } from './html.js'
4
5
  import { loadScript } from './scripts.js'
5
6
  import { normalizeCollectionScope } from '../utils/datapath.js'
7
+ import { DataOptions } from '../headless.js'
6
8
 
7
9
  export interface FEAASCustomizations {
8
- processTemplate?: (element: HTMLElement, template: string, data: any) => string
9
- processData?: (element: HTMLElement, data: any) => any
10
+ processTemplate?: (element: HTMLElement, template: string, data: DataScopes) => string
11
+ processData?: (element: HTMLElement, data: DataScopes) => any
10
12
  processValue?: (element: HTMLElement, property: string, value: any) => any
11
13
  shouldRepeat?: (element: HTMLElement, value: any) => boolean
12
- onRender?: (element: HTMLElement, data: any) => void
14
+ onRender?: (element: HTMLElement, data: DataScopes) => void
13
15
  }
14
16
 
15
17
  export interface ExplicitTemplate {
16
18
  template: string
17
19
  }
18
20
 
19
- export type FEAASComponentProps<T = Datascopes> = Partial<ExplicitTemplate> & {
21
+ export type FEAASComponentProps<T = DataOptions> = Partial<ExplicitTemplate> & {
20
22
  data?: T
21
23
  } & (ExplicitTemplate | FEAASCDNComponentProps) &
22
24
  FEAASCustomizations
@@ -197,7 +199,7 @@ function isElement(node: Node): node is HTMLElement {
197
199
  return node.nodeType == 1
198
200
  }
199
201
 
200
- export function renderDOMContent(root: HTMLElement, template: string, data?: Datascopes, config?: FEAASCustomizations) {
202
+ export function renderDOMContent(root: HTMLElement, template: string, data?: Data, config?: FEAASCustomizations) {
201
203
  const newTemplate = config?.processTemplate?.(root, template, data) || template
202
204
  root.innerHTML = newTemplate
203
205
  root.classList.add('-feaas')
@@ -213,7 +215,7 @@ export function renderDOMContent(root: HTMLElement, template: string, data?: Dat
213
215
  return renderDOMElement(root, data, config)
214
216
  }
215
217
 
216
- export function renderHTMLContent(template: string, data?: Datascopes, config?: FEAASCustomizations, fakeDOM = false) {
218
+ export function renderHTMLContent(template: string, data?: Data, config?: FEAASCustomizations, fakeDOM = false) {
217
219
  const newTemplate = config?.processTemplate?.(null, template, data) || template
218
220
  if (typeof document === 'undefined' || fakeDOM) {
219
221
  const nodes = parseHTML(newTemplate)
package/src/headless.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  import FEAASReactElement from './components/FEAASReactElement.js'
7
7
 
8
8
  export * from './dom/rendering.js'
9
- export * from './dom/data.js'
9
+ export * from './utils/data.js'
10
10
  export * from './dom/scripts.js'
11
11
  export * from './dom/stylesheet.js'
12
12
  export * from './dom/component.js'
@@ -20,10 +20,10 @@ export * from './components/FEAASLoader.js'
20
20
  export * from './components/FEAASStylesheet.js'
21
21
  export * from './utils/datapath.js'
22
22
  export * as DOM from './dom/rendering.js'
23
- export * as Datascope from './dom/data.js'
24
- export * as Datapath from './utils/datapath.js'
25
- export * from './utils/fetch.js'
26
- export * as DataSettings from './utils/settings.js'
23
+ export * as DataScope from './utils/data.js'
24
+ export * as DataPath from './utils/datapath.js'
25
+ export { DataSettings, fetchData } from './utils/settings.js'
26
+ export type { Data, DataOptionsById, DataOptions, DataScopes } from './utils/settings.js'
27
27
  export * as Thumbnail from './dom/thumbnail.js'
28
28
 
29
29
  declare global {
@@ -232,6 +232,14 @@ export default class FEAASExternal extends FEAASElement<FEAASExternalProps, Reac
232
232
  props?.src || (typeof window !== 'undefined' ? window.location.search.match(/feaasSrc=([^&]+)/)?.[1] : null)
233
233
  return src ? <Component src={src} {...props} /> : null
234
234
  }
235
+
236
+ static linkComponents(components: Promise<any>[]): () => any {
237
+ return () => components.map((components) => null)
238
+ }
239
+
240
+ static getComponent(id: string) {
241
+ return FEAASExternal.registered[id]
242
+ }
235
243
  }
236
244
 
237
245
  declare global {
@@ -251,4 +259,6 @@ FEAASExternal.register('feaas-external')
251
259
  FEAASExternal.setRegistrationCallback()
252
260
 
253
261
  export const registerComponent = FEAASExternal.registerComponent
262
+ export const linkComponents = FEAASExternal.linkComponents
263
+ export const getComponent = FEAASExternal.getComponent
254
264
  export { FEAASExternal as External }
@@ -46,6 +46,12 @@ export class FEAASPicker extends FEAASElement<FEAASPickerProps, typeof FEAASPick
46
46
  const components = useLibrary('components')
47
47
  const rules = useLibrary('stylesheet.styles')
48
48
  const component = components.find((c) => c.id == context?.getAttribute('component'))
49
+ var settings = context?.getAttribute('data')
50
+ if (settings) {
51
+ try {
52
+ settings = JSON.parse(settings)
53
+ } catch (e) {}
54
+ }
49
55
  return (
50
56
  <Picker
51
57
  rules={rules}
@@ -53,9 +59,11 @@ export class FEAASPicker extends FEAASElement<FEAASPickerProps, typeof FEAASPick
53
59
  themeContext={context}
54
60
  placeholder={placeholder}
55
61
  component={component}
56
- versionId={context.getAttribute('version')}
57
- mode={mode || 'component'}
62
+ data={settings}
63
+ versionId={context?.getAttribute('version')}
64
+ mode={mode || (component ? 'component' : null)}
58
65
  setAttribute={(context, attribute, value) => {
66
+ if (attribute == 'data' && value) value = JSON.stringify(value)
59
67
  this.props.setAttribute(context, attribute, value)
60
68
  forceRender()
61
69
  }}
package/src/ui/react.tsx CHANGED
@@ -7,29 +7,34 @@ import {
7
7
  } from './index.js'
8
8
  import React from 'react'
9
9
  import { renderHTMLContent } from '../dom/html.js'
10
- import { fetchData } from '../utils/fetch.js'
10
+ import { DataSettings } from '../utils/settings.js'
11
11
 
12
12
  export { renderHTMLContent }
13
- export { External } from './FEAASExternal.js'
13
+ export { External, linkComponents, registerComponent, getComponent } from './FEAASExternal.js'
14
14
  export * from './index.js'
15
15
  export * from '../headless.js'
16
16
 
17
- export const ServerComponent = async (props: FEAASComponentProps<any>) => {
18
- const fetchedData = await fetchData(props?.data || {})
17
+ export type FEAASReactComponentProps = FEAASComponentProps & {
18
+ src?: string
19
+ preload?: boolean
20
+ lastModified?: Date | string
21
+ }
22
+ export const ServerComponent = async (props: FEAASComponentProps) => {
23
+ const fetchedData = await DataSettings.fetch(props?.data || {})
19
24
 
20
- const p = { ...props }
25
+ const p: FEAASReactComponentProps = { ...props }
21
26
 
22
- if (('src' in p || 'component' in p) && p?.template == null) {
23
- const response = await fetchComponent(p)
27
+ if (('src' in props || 'component' in props) && props?.template == null) {
28
+ const response = await fetchComponent(props)
24
29
 
25
30
  p.template = response.template
26
- // p.lastModified = response.lastModified
31
+ p.lastModified = response.lastModified
27
32
  }
28
33
 
29
- return FEAASEmbed({ ...p, data: fetchedData })
34
+ return FEAASReactComponent({ ...p, data: fetchedData })
30
35
  }
31
36
 
32
- export function FEAASEmbed({ data, ...props }: FEAASComponentProps<any> & { src?: string; preload?: boolean }) {
37
+ export function FEAASReactComponent({ data, ...props }: FEAASReactComponentProps) {
33
38
  let html = ''
34
39
  let preload = null
35
40
 
@@ -39,6 +44,13 @@ export function FEAASEmbed({ data, ...props }: FEAASComponentProps<any> & { src?
39
44
 
40
45
  if ('src' in props) {
41
46
  props = { ...parseComponentSource(props.src), ...props }
47
+ } else if (!('template' in props)) {
48
+ if (!props.version) {
49
+ props = {
50
+ ...props,
51
+ version: 'responsive'
52
+ }
53
+ }
42
54
  }
43
55
 
44
56
  return (
@@ -51,6 +63,7 @@ export function FEAASEmbed({ data, ...props }: FEAASComponentProps<any> & { src?
51
63
  )}
52
64
  <feaas-component
53
65
  {...props}
66
+ suppressHydrationWarning={true}
54
67
  template={null}
55
68
  data={typeof data == 'string' ? data : data ? JSON.stringify(data) : null}
56
69
  dangerouslySetInnerHTML={{ __html: html || '' }}
@@ -59,4 +72,4 @@ export function FEAASEmbed({ data, ...props }: FEAASComponentProps<any> & { src?
59
72
  )
60
73
  }
61
74
 
62
- export const Component = FEAASEmbed
75
+ export const Component = FEAASReactComponent
@@ -1,6 +1,8 @@
1
1
  import { JSONPath } from 'jsonpath-plus'
2
2
 
3
- export type Datascopes = Record<string, any> | any[]
3
+ export function queryObject(json: any, path: string) {
4
+ return JSONPath({ path, json })
5
+ }
4
6
 
5
7
  export function queryScopes(scopes: any, datapath: string, singleValue = false, json: any = scopes) {
6
8
  var path = datapath
@@ -20,16 +20,21 @@ export const parseRange = (qualifier: string) => {
20
20
  return bits.join(',')
21
21
  }
22
22
 
23
- export const getQualifier = (jsonpath: string) => jsonpath?.match(/(\.\*|\[[^\]]+\])$/)?.[0]
23
+ export const getQualifier = (jsonpath: string) => jsonpath?.match(/(\.\*|\[[^\]]*\])$/)?.[0]
24
24
 
25
25
  export function cleanCollectionBit(path: string) {
26
- return path.replace(/(\[[^\]\[]+\]|\.\*)$/g, '')
26
+ return path.replace(/(\.?\[[^\]\[]*\]|\.\*)$/g, '')
27
27
  }
28
28
 
29
29
  export function normalizeCollectionScope(path: string) {
30
30
  return cleanCollectionBit(path) + '.*'
31
31
  }
32
32
 
33
+ // increment numbers by given numbers, exclude second value of ranges to ensure inclusivity
34
+ export function adjustNumbers(string: string, diff: number) {
35
+ return string?.replace(/(.|^)(\d+)/g, (m, b, n) => b + (b == ':' ? n : String(parseInt(n) + diff)))
36
+ }
37
+
33
38
  export function getHumanizedLabel(path: string, maxBits = 2) {
34
39
  var bits = cleanCollectionBit(path)
35
40
  .split(/]\.|\.\*\./g)
@@ -1,69 +1,200 @@
1
- export function clean(settings: any) {
2
- let cleanSettings = {}
3
-
4
- const hasNoBody =
5
- settings?.method === 'GET' || settings?.method === 'HEAD' || Object.keys(settings?.body || {}).length === 0
6
-
7
- const hasNoHeaders =
8
- settings.headers &&
9
- (Object.keys(settings.headers).length === 0 ||
10
- (Object.keys(settings.headers).length === 1 &&
11
- (settings.headers['Content-type'] || settings.headers['Content-Type']) &&
12
- (settings.headers['Content-type'] === 'application/json' ||
13
- settings.headers['Content-Type'] === 'application/json')))
14
-
15
- const hasNoParams = settings.params || Object.keys(settings.params || {}).length === 0
16
-
17
- if (settings.method && settings.method !== 'GET') cleanSettings = { ...cleanSettings, method: settings.method }
18
- if (settings.body && !hasNoBody) cleanSettings = { ...cleanSettings, body: settings.body }
19
- cleanSettings = {
20
- ...cleanSettings,
21
- headers: hasNoHeaders ? { 'Content-type': 'application/json' } : settings.headers
22
- }
23
- if (settings.params && !hasNoParams) cleanSettings = { ...cleanSettings, params: settings.params }
1
+ import { queryObject } from './data.js'
24
2
 
25
- return { ...cleanSettings, url: settings.url || '' }
26
- }
3
+ export type DataSourceId = string
27
4
 
28
- export function toFetchArguments(settings?: any): Parameters<typeof fetch> {
29
- const normalized = normalize(settings)
5
+ // Fully resolved data - object or array
6
+ export type Data = Record<string, any> | any[]
30
7
 
31
- let body = null
8
+ // Data keyed by datasource id
9
+ export type DataScopes = Data | Record<DataSourceId, Data>
32
10
 
33
- const contentType = normalized.headers['Content-Type'] || normalized.headers['Content-type']
11
+ // Data settings is json object representing fetch call
12
+ export type DataSettings = ReturnType<typeof DataSettings>
34
13
 
35
- const isJson = contentType === 'application/json'
36
- const isForm = contentType === 'multipart/form-data'
37
- const isUrlEncoded = contentType === 'application/x-www-form-urlencoded'
14
+ // Data options/samples indexed by id
15
+ export type DataOptionsById = Record<DataSourceId, DataSettings | Data>
38
16
 
39
- if (isJson) body = JSON.stringify(normalized.body)
40
- if (isForm)
41
- body = Object.keys(normalized.body).reduce((form: any, key) => {
42
- form.append(key, normalized.body[key])
43
- return form
44
- }, new FormData())
45
- if (isUrlEncoded) body = new URLSearchParams(normalized.body)
17
+ // Data options combines of the above: data, settings, plain or keyed by datasource id
18
+ export type DataOptions = Data | DataSettings | DataOptionsById
46
19
 
47
- const params = Object.keys(normalized.params).length ? `?${new URLSearchParams(normalized.params)}` : ''
20
+ /**
21
+ * Cleans the provided settings object by removing unnecessary properties that match the defaults that will be re-added
22
+ * back through normalizations. This method is used for code generated for the user, neant to make it look neater.
23
+ *
24
+ * @param {any} settings - The settings object to be cleaned.
25
+ * @returns {Object} - The cleaned settings object.
26
+ */
27
+ export function clean(settings: any) {
28
+ const { params, method, headers, body, url } = DataSettings(settings)
29
+ var cleanSettings = { url } as Partial<DataSettings>
30
+ var cleanHeaders = { ...headers }
31
+ if (headers['Content-Type'] === 'application/json') delete cleanHeaders['Content-Type']
32
+ if (headers['Accept'] === 'application/json') delete cleanHeaders['Accept']
33
+ if (Object.keys(cleanHeaders).length > 0) {
34
+ cleanSettings.headers = cleanHeaders
35
+ }
36
+ if (method !== 'GET') {
37
+ cleanSettings.method = method
38
+ }
39
+ if (!(method === 'GET' || method === 'HEAD' || Object.keys(body).length === 0)) {
40
+ cleanSettings.body = body
41
+ }
42
+ if (Object.keys(params).length > 0) {
43
+ cleanSettings = { ...cleanSettings, params }
44
+ }
45
+ return cleanSettings
46
+ }
48
47
 
49
- let url = `${normalized.url}${params}`
48
+ /**
49
+ * Checks if the provided data is of type DataSettings by checking if all keys are recognized as settings keys.
50
+ *
51
+ * @param {any} data - The data to be checked.
52
+ * @returns {boolean} - True if the data is of type DataSettings, false otherwise.
53
+ */
54
+ export const isDataSettings = (data: any): data is DataSettings => {
55
+ if (typeof data.url != 'string') return false
56
+
57
+ for (var property in data) {
58
+ if (
59
+ property == 'body' ||
60
+ property == 'params' ||
61
+ property == 'headers' ||
62
+ property == 'url' ||
63
+ property === 'method' ||
64
+ (property == 'jsonpath' && typeof data[property] == 'string')
65
+ ) {
66
+ continue
67
+ }
68
+
69
+ return false
70
+ }
50
71
 
51
- let options: any = {}
72
+ return true
73
+ }
52
74
 
53
- if (body) options.body = body
54
- if (Object.keys(normalized.headers).length) options.headers = normalized.headers
55
- if (normalized.method !== 'GET') options.method = normalized.method
75
+ /**
76
+ * Converts settings object into fetch function arguments.
77
+ *
78
+ * @param {any} settings - The settings object.
79
+ * @returns {Array} - An array containing URL and options objects as fetch function arguments.
80
+ */
81
+ export function toFetchArguments(settings?: any): Parameters<typeof fetch> {
82
+ const { url, headers, params, method, body: payload } = DataSettings(settings)
83
+
84
+ let body = undefined
85
+
86
+ const contentType = headers['Content-Type']
87
+
88
+ if (method != 'GET' && method != 'HEAD') {
89
+ if (contentType === 'application/json') body = JSON.stringify(payload)
90
+ if (contentType === 'multipart/form-data')
91
+ body = Object.keys(payload).reduce((form, key) => {
92
+ form.append(key, (payload as any)[key])
93
+ return form
94
+ }, new FormData())
95
+ if (contentType === 'application/x-www-form-urlencoded') body = new URLSearchParams(payload)
96
+ }
97
+
98
+ const query = Object.keys(params).length ? `?${new URLSearchParams(params)}` : ''
56
99
 
57
- return [url, options]
100
+ return [
101
+ `${url}${query}`,
102
+ {
103
+ headers,
104
+ body,
105
+ method
106
+ }
107
+ ]
58
108
  }
59
109
 
60
- export function normalize(settings: any) {
110
+ /**
111
+ * Creates a DataSettings object based on the provided settings. These settings are used later to fetch data for the
112
+ * component. It's essentially fetch options with url.
113
+ *
114
+ * @param {any} settings - The settings object.
115
+ * @returns {DataSettings} - The DataSettings object.
116
+ */
117
+ export function DataSettings(settings: any) {
118
+ const headers = {} as any
119
+ for (const key in settings?.headers || {}) {
120
+ if (settings.headers.hasOwnProperty(key)) {
121
+ const headerCase = key.replace(/^[a-z]|\-[a-z]/g, (m) => m.toUpperCase())
122
+ headers[headerCase] = settings.headers[key]
123
+ }
124
+ }
125
+ if (!headers.Accept) headers['Accept'] = 'application/json'
61
126
  return {
62
- params: settings?.params || {},
63
- headers: settings?.headers || {},
64
- jsonpath: settings?.jsonpath || '$',
65
- method: settings?.method || 'GET',
66
- body: settings?.body || {},
67
- url: settings?.url || ''
127
+ params: (settings?.params || {}) as Record<string, any>,
128
+ headers: headers as Record<string, string>,
129
+ jsonpath: String(settings?.jsonpath || '$'),
130
+ method: (settings?.method || 'GET').toUpperCase() as
131
+ | 'GET'
132
+ | 'POST'
133
+ | 'OPTIONS'
134
+ | 'PUT'
135
+ | 'DELETE'
136
+ | 'UPDATE'
137
+ | 'HEAD',
138
+ body: (settings?.body || {}) as Record<string, any>,
139
+ url: String(settings?.url || '')
140
+ }
141
+ }
142
+ /**
143
+ * Fetches data based on the provided settings. It is expected that reesponse is json (forced by Accept header)
144
+ *
145
+ * @param {DataSettings} settings - The data settings.
146
+ * @returns {Promise<Data>} - A promise that resolves to the fetched data.
147
+ */
148
+ const fetchDataSettings = async (settings: DataSettings): Promise<Data> => {
149
+ try {
150
+ const [url, options] = toFetchArguments(settings)
151
+ const response = await DataSettings.fetchImplementation(url, {
152
+ ...options,
153
+ // next.js only allows lowercase method
154
+ method: options.method.toLowerCase()
155
+ })
156
+ const json = await response.json()
157
+ if (settings.jsonpath && settings.jsonpath != '$') {
158
+ return queryObject(json, settings.jsonpath)
159
+ }
160
+ return json
161
+ } catch (e) {
162
+ return {}
68
163
  }
69
164
  }
165
+ /**
166
+ * Fetches data based on the provided data options. Options is a union of different types, settngs, data or
167
+ * datasource-id keyed settings/data. If the function doesnt find settings in the object, it returns it as is. It
168
+ * supports mixing of static and fetched data. If multiple data settings are found, requests are made in parallel.
169
+ *
170
+ * @param {DataOptions} data - The data options.
171
+ * @returns {Promise<DataScopes>} - A promise that resolves to the fetched data.
172
+ */
173
+ export async function fetchData(data: DataOptions): Promise<DataScopes> {
174
+ if (!data) return data
175
+
176
+ if (isDataSettings(data)) return fetchDataSettings(data)
177
+
178
+ const keys = Object.keys(data)
179
+
180
+ const allRequests = await Promise.all(
181
+ keys
182
+ .map((key: keyof typeof data) => (isDataSettings(data[key]) ? fetchDataSettings(data[key]) : null))
183
+ .filter(Boolean)
184
+ )
185
+
186
+ return keys.reduce(
187
+ (obj, key: keyof typeof data) => ({
188
+ ...obj,
189
+ [key]: isDataSettings(data[key]) ? allRequests.shift() : data[key]
190
+ }),
191
+ {}
192
+ )
193
+ }
194
+
195
+ DataSettings.clean = clean
196
+ DataSettings.toFetchArguments = toFetchArguments
197
+ DataSettings.fetchOne = fetchDataSettings
198
+ DataSettings.fetch = fetchData
199
+ DataSettings.isDataSettings = isDataSettings
200
+ DataSettings.fetchImplementation = (...args: Parameters<typeof fetch>) => fetch(...args)