@nordcraft/core 1.0.0

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 (119) hide show
  1. package/README.md +5 -0
  2. package/dist/api/LegacyToddleApi.d.ts +34 -0
  3. package/dist/api/LegacyToddleApi.js +178 -0
  4. package/dist/api/LegacyToddleApi.js.map +1 -0
  5. package/dist/api/ToddleApiV2.d.ts +77 -0
  6. package/dist/api/ToddleApiV2.js +271 -0
  7. package/dist/api/ToddleApiV2.js.map +1 -0
  8. package/dist/api/api.d.ts +49 -0
  9. package/dist/api/api.js +276 -0
  10. package/dist/api/api.js.map +1 -0
  11. package/dist/api/apiTypes.d.ts +125 -0
  12. package/dist/api/apiTypes.js +11 -0
  13. package/dist/api/apiTypes.js.map +1 -0
  14. package/dist/api/headers.d.ts +10 -0
  15. package/dist/api/headers.js +36 -0
  16. package/dist/api/headers.js.map +1 -0
  17. package/dist/api/template.d.ts +5 -0
  18. package/dist/api/template.js +7 -0
  19. package/dist/api/template.js.map +1 -0
  20. package/dist/component/ToddleComponent.d.ts +45 -0
  21. package/dist/component/ToddleComponent.js +373 -0
  22. package/dist/component/ToddleComponent.js.map +1 -0
  23. package/dist/component/actionUtils.d.ts +2 -0
  24. package/dist/component/actionUtils.js +56 -0
  25. package/dist/component/actionUtils.js.map +1 -0
  26. package/dist/component/component.types.d.ts +313 -0
  27. package/dist/component/component.types.js +9 -0
  28. package/dist/component/component.types.js.map +1 -0
  29. package/dist/component/isPageComponent.d.ts +2 -0
  30. package/dist/component/isPageComponent.js +3 -0
  31. package/dist/component/isPageComponent.js.map +1 -0
  32. package/dist/formula/ToddleFormula.d.ts +15 -0
  33. package/dist/formula/ToddleFormula.js +20 -0
  34. package/dist/formula/ToddleFormula.js.map +1 -0
  35. package/dist/formula/formula.d.ts +103 -0
  36. package/dist/formula/formula.js +186 -0
  37. package/dist/formula/formula.js.map +1 -0
  38. package/dist/formula/formulaTypes.d.ts +30 -0
  39. package/dist/formula/formulaTypes.js +2 -0
  40. package/dist/formula/formulaTypes.js.map +1 -0
  41. package/dist/formula/formulaUtils.d.ts +18 -0
  42. package/dist/formula/formulaUtils.js +240 -0
  43. package/dist/formula/formulaUtils.js.map +1 -0
  44. package/dist/styling/className.d.ts +2 -0
  45. package/dist/styling/className.js +52 -0
  46. package/dist/styling/className.js.map +1 -0
  47. package/dist/styling/style.css.d.ts +5 -0
  48. package/dist/styling/style.css.js +242 -0
  49. package/dist/styling/style.css.js.map +1 -0
  50. package/dist/styling/theme.const.d.ts +3 -0
  51. package/dist/styling/theme.const.js +381 -0
  52. package/dist/styling/theme.const.js.map +1 -0
  53. package/dist/styling/theme.d.ts +72 -0
  54. package/dist/styling/theme.js +200 -0
  55. package/dist/styling/theme.js.map +1 -0
  56. package/dist/styling/variantSelector.d.ts +123 -0
  57. package/dist/styling/variantSelector.js +25 -0
  58. package/dist/styling/variantSelector.js.map +1 -0
  59. package/dist/types.d.ts +97 -0
  60. package/dist/types.js +2 -0
  61. package/dist/types.js.map +1 -0
  62. package/dist/utils/collections.d.ts +21 -0
  63. package/dist/utils/collections.js +75 -0
  64. package/dist/utils/collections.js.map +1 -0
  65. package/dist/utils/customElements.d.ts +6 -0
  66. package/dist/utils/customElements.js +15 -0
  67. package/dist/utils/customElements.js.map +1 -0
  68. package/dist/utils/hash.d.ts +1 -0
  69. package/dist/utils/hash.js +17 -0
  70. package/dist/utils/hash.js.map +1 -0
  71. package/dist/utils/json.d.ts +5 -0
  72. package/dist/utils/json.js +16 -0
  73. package/dist/utils/json.js.map +1 -0
  74. package/dist/utils/sha1.d.ts +2 -0
  75. package/dist/utils/sha1.js +13 -0
  76. package/dist/utils/sha1.js.map +1 -0
  77. package/dist/utils/url.d.ts +5 -0
  78. package/dist/utils/url.js +27 -0
  79. package/dist/utils/url.js.map +1 -0
  80. package/dist/utils/util.d.ts +3 -0
  81. package/dist/utils/util.js +4 -0
  82. package/dist/utils/util.js.map +1 -0
  83. package/package.json +18 -0
  84. package/src/api/LegacyToddleApi.ts +205 -0
  85. package/src/api/ToddleApiV2.ts +331 -0
  86. package/src/api/api.test.ts +319 -0
  87. package/src/api/api.ts +414 -0
  88. package/src/api/apiTypes.ts +145 -0
  89. package/src/api/headers.ts +41 -0
  90. package/src/api/template.ts +10 -0
  91. package/src/component/ToddleComponent.actionReferences.test.ts +75 -0
  92. package/src/component/ToddleComponent.formulasInComponent.test.ts +234 -0
  93. package/src/component/ToddleComponent.ts +470 -0
  94. package/src/component/actionUtils.ts +61 -0
  95. package/src/component/component.types.ts +362 -0
  96. package/src/component/isPageComponent.ts +6 -0
  97. package/src/formula/ToddleFormula.ts +30 -0
  98. package/src/formula/formula.ts +355 -0
  99. package/src/formula/formulaTypes.ts +45 -0
  100. package/src/formula/formulaUtils.ts +287 -0
  101. package/src/styling/className.test.ts +19 -0
  102. package/src/styling/className.ts +73 -0
  103. package/src/styling/style.css.ts +309 -0
  104. package/src/styling/theme.const.ts +390 -0
  105. package/src/styling/theme.ts +339 -0
  106. package/src/styling/variantSelector.ts +168 -0
  107. package/src/types.ts +158 -0
  108. package/src/utils/collections.test.ts +57 -0
  109. package/src/utils/collections.ts +122 -0
  110. package/src/utils/customElements.test.ts +40 -0
  111. package/src/utils/customElements.ts +16 -0
  112. package/src/utils/hash.test.ts +32 -0
  113. package/src/utils/hash.ts +18 -0
  114. package/src/utils/json.ts +18 -0
  115. package/src/utils/sha1.test.ts +50 -0
  116. package/src/utils/sha1.ts +17 -0
  117. package/src/utils/url.test.ts +17 -0
  118. package/src/utils/url.ts +33 -0
  119. package/src/utils/util.ts +8 -0
package/src/api/api.ts ADDED
@@ -0,0 +1,414 @@
1
+ import type { Formula, FormulaContext } from '../formula/formula'
2
+ import { applyFormula } from '../formula/formula'
3
+ import { omitKeys, sortObjectEntries } from '../utils/collections'
4
+ import { hash } from '../utils/hash'
5
+ import { isDefined, isObject, toBoolean } from '../utils/util'
6
+ import type {
7
+ ApiBase,
8
+ ApiPerformance,
9
+ ApiRequest,
10
+ ComponentAPI,
11
+ LegacyComponentAPI,
12
+ ToddleRequestInit,
13
+ } from './apiTypes'
14
+ import { ApiMethod } from './apiTypes'
15
+ import { LegacyToddleApi } from './LegacyToddleApi'
16
+ import { ToddleApiV2 } from './ToddleApiV2'
17
+
18
+ export const NON_BODY_RESPONSE_CODES = [101, 204, 205, 304]
19
+
20
+ export const isLegacyApi = <Handler>(
21
+ api: ComponentAPI | LegacyToddleApi<Handler> | ToddleApiV2<Handler>,
22
+ ): api is LegacyComponentAPI | LegacyToddleApi<Handler> =>
23
+ api instanceof LegacyToddleApi ? true : !('version' in api)
24
+
25
+ export const createApiRequest = <Handler>({
26
+ api,
27
+ formulaContext,
28
+ baseUrl,
29
+ defaultHeaders,
30
+ }: {
31
+ api: ApiRequest | ToddleApiV2<Handler>
32
+ formulaContext: FormulaContext
33
+ baseUrl?: string
34
+ defaultHeaders: Headers | undefined
35
+ }) => {
36
+ const url = getUrl(api, formulaContext, baseUrl)
37
+ const requestSettings = getRequestSettings({
38
+ api,
39
+ formulaContext,
40
+ defaultHeaders,
41
+ })
42
+
43
+ return { url, requestSettings }
44
+ }
45
+
46
+ export const getUrl = (
47
+ api: ApiBase,
48
+ formulaContext: FormulaContext,
49
+ baseUrl?: string,
50
+ ): URL => {
51
+ let urlPathname = ''
52
+ let urlQueryParams = new URLSearchParams()
53
+ let parsedUrl: URL | undefined
54
+ const url = applyFormula(api.url, formulaContext)
55
+ if (['string', 'number'].includes(typeof url)) {
56
+ const urlInput = typeof url === 'number' ? String(url) : url
57
+ try {
58
+ // Try to parse the URL to extract potential path and query parameters
59
+ parsedUrl = new URL(urlInput, baseUrl)
60
+ urlPathname = parsedUrl.pathname
61
+ urlQueryParams = parsedUrl.searchParams
62
+ // eslint-disable-next-line no-empty
63
+ } catch {}
64
+ }
65
+ const pathParams = getRequestPath(api.path, formulaContext)
66
+ // Combine potential path parameters from the url declaration with the actual path parameters
67
+ const path = `${urlPathname}${pathParams.length > 0 && !urlPathname.endsWith('/') ? '/' : ''}${pathParams}`
68
+ // Combine potential query parameters from the url declaration with the actual query parameters
69
+ const queryParams = new URLSearchParams([
70
+ ...urlQueryParams,
71
+ ...getRequestQueryParams(api.queryParams, formulaContext),
72
+ ])
73
+ const queryString =
74
+ [...queryParams.entries()].length > 0 ? `?${queryParams.toString()}` : ''
75
+ if (parsedUrl) {
76
+ const combinedUrl = new URL(parsedUrl.origin, baseUrl)
77
+ combinedUrl.pathname = path
78
+ combinedUrl.search = queryParams.toString()
79
+ return combinedUrl
80
+ } else {
81
+ return new URL(`${path}${queryString}`, baseUrl)
82
+ }
83
+ }
84
+
85
+ const HttpMethodsWithAllowedBody: ApiMethod[] = [
86
+ ApiMethod.POST,
87
+ ApiMethod.DELETE,
88
+ ApiMethod.PUT,
89
+ ApiMethod.PATCH,
90
+ ApiMethod.OPTIONS,
91
+ ]
92
+
93
+ export const applyAbortSignal = (
94
+ api: ApiRequest,
95
+ requestSettings: RequestInit,
96
+ formulaContext: FormulaContext,
97
+ ) => {
98
+ if (api.timeout) {
99
+ const timeout = applyFormula(api.timeout.formula, formulaContext)
100
+ if (typeof timeout === 'number' && !Number.isNaN(timeout) && timeout > 0) {
101
+ requestSettings.signal = AbortSignal.timeout(timeout)
102
+ }
103
+ }
104
+ }
105
+
106
+ const getRequestSettings = ({
107
+ api,
108
+ formulaContext,
109
+ defaultHeaders,
110
+ }: {
111
+ api: ApiRequest
112
+ formulaContext: FormulaContext
113
+ defaultHeaders: Headers | undefined
114
+ }): ToddleRequestInit => {
115
+ const method = Object.values(ApiMethod).includes(api.method as ApiMethod)
116
+ ? (api.method as ApiMethod)
117
+ : ApiMethod.GET
118
+ const headers = getRequestHeaders({
119
+ apiHeaders: api.headers,
120
+ formulaContext,
121
+ defaultHeaders,
122
+ })
123
+ const body = getRequestBody({ api, formulaContext, headers, method })
124
+ if (headers.get('content-type') === 'multipart/form-data') {
125
+ headers.delete('content-type')
126
+ }
127
+
128
+ const requestSettings: ToddleRequestInit = {
129
+ method,
130
+ headers,
131
+ body,
132
+ }
133
+
134
+ applyAbortSignal(api, requestSettings, formulaContext)
135
+
136
+ return requestSettings
137
+ }
138
+
139
+ export const getRequestPath = (
140
+ path: ApiRequest['path'],
141
+ formulaContext: FormulaContext,
142
+ ): string =>
143
+ sortObjectEntries(path ?? {}, ([_, p]) => p.index)
144
+ .map(([_, p]) => applyFormula(p.formula, formulaContext))
145
+ .join('/')
146
+
147
+ export const getRequestQueryParams = (
148
+ params: ApiRequest['queryParams'],
149
+ formulaContext: FormulaContext,
150
+ ): URLSearchParams => {
151
+ const queryParams = new URLSearchParams()
152
+ Object.entries(params ?? {}).forEach(([key, param]) => {
153
+ const enabled = isDefined(param.enabled)
154
+ ? applyFormula(param.enabled, formulaContext)
155
+ : true
156
+ if (!enabled) {
157
+ return
158
+ }
159
+
160
+ const value = applyFormula(param.formula, formulaContext)
161
+ if (!isDefined(value)) {
162
+ // Ignore null/undefined values
163
+ return
164
+ }
165
+ if (Array.isArray(value)) {
166
+ // Support encoding 1-dimensional arrays
167
+ value.forEach((v) => queryParams.append(key, String(v)))
168
+ } else if (isObject(value)) {
169
+ // Support encoding (nested) objects, but cast any non-object to a String
170
+ const encodeObject = (obj: Record<string, any>, prefix: string) => {
171
+ Object.entries(obj).forEach(([key, val]) => {
172
+ if (!Array.isArray(val) && isObject(val)) {
173
+ return encodeObject(val, `${prefix}[${key}]`)
174
+ } else {
175
+ queryParams.set(`${prefix}[${key}]`, String(val))
176
+ }
177
+ })
178
+ }
179
+ encodeObject(value, key)
180
+ } else {
181
+ queryParams.set(key, String(value))
182
+ }
183
+ })
184
+ return queryParams
185
+ }
186
+
187
+ export const getRequestHeaders = ({
188
+ apiHeaders,
189
+ formulaContext,
190
+ defaultHeaders,
191
+ }: {
192
+ apiHeaders: ApiRequest['headers']
193
+ formulaContext: FormulaContext
194
+ defaultHeaders: Headers | undefined
195
+ }) => {
196
+ const headers = new Headers(defaultHeaders)
197
+ Object.entries(apiHeaders ?? {}).forEach(([key, param]) => {
198
+ const enabled = isDefined(param.enabled)
199
+ ? applyFormula(param.enabled, formulaContext)
200
+ : true
201
+ if (enabled) {
202
+ const value = applyFormula(param.formula, formulaContext)
203
+ if (isDefined(value)) {
204
+ try {
205
+ headers.set(
206
+ key.trim(),
207
+ (typeof value === 'string' ? value : String(value)).trim(),
208
+ )
209
+ // eslint-disable-next-line no-empty
210
+ } catch {}
211
+ }
212
+ }
213
+ })
214
+ return headers
215
+ }
216
+
217
+ export const getBaseUrl = ({
218
+ origin,
219
+ url,
220
+ }: {
221
+ origin: string
222
+ url?: string
223
+ }) =>
224
+ !isDefined(url) || url === '' || url.startsWith('/') ? origin + url : url
225
+
226
+ /**
227
+ * Calculate the hash of a Request object based on its properties
228
+ */
229
+ export const requestHash = (url: URL, request: RequestInit) =>
230
+ hash(
231
+ JSON.stringify({
232
+ url: url.href,
233
+ method: request.method,
234
+ headers: omitKeys(
235
+ Object.fromEntries(Object.entries(request.headers ?? {})),
236
+ ['host', 'cookie'],
237
+ ),
238
+ body: request.body ?? null,
239
+ }),
240
+ )
241
+
242
+ export const isApiError = ({
243
+ apiName,
244
+ response,
245
+ formulaContext,
246
+ errorFormula,
247
+ performance,
248
+ }: {
249
+ apiName: string
250
+ response: {
251
+ ok: boolean
252
+ status?: number
253
+ headers?: Record<string, string> | null
254
+ body: unknown
255
+ }
256
+ formulaContext: FormulaContext
257
+ performance: ApiPerformance
258
+ errorFormula?: { formula: Formula } | null
259
+ }) => {
260
+ const errorFormulaRes = errorFormula
261
+ ? applyFormula(errorFormula.formula, {
262
+ component: formulaContext.component,
263
+ package: formulaContext.package,
264
+ toddle: formulaContext.toddle,
265
+ data: {
266
+ Attributes: {},
267
+ Args: formulaContext.data.Args,
268
+ Apis: {
269
+ // The errorFormula will only have access to the data of the current API
270
+ [apiName]: {
271
+ isLoading: false,
272
+ data: response.body,
273
+ error: null,
274
+ response: {
275
+ status: response.status,
276
+ headers: response.headers,
277
+ performance,
278
+ },
279
+ },
280
+ },
281
+ },
282
+ env: formulaContext.env,
283
+ })
284
+ : null
285
+
286
+ if (errorFormulaRes === null || errorFormulaRes === undefined) {
287
+ return !response.ok
288
+ }
289
+ return toBoolean(errorFormulaRes)
290
+ }
291
+
292
+ const getRequestBody = ({
293
+ api,
294
+ formulaContext,
295
+ headers,
296
+ method,
297
+ }: {
298
+ api: ApiRequest
299
+ formulaContext: FormulaContext
300
+ headers: Headers
301
+ method: ApiMethod
302
+ }): FormData | string | undefined => {
303
+ if (!api.body || !HttpMethodsWithAllowedBody.includes(method)) {
304
+ return
305
+ }
306
+
307
+ const body = applyFormula(api.body, formulaContext)
308
+ if (!body) {
309
+ return
310
+ }
311
+ switch (headers.get('content-type')) {
312
+ case 'application/x-www-form-urlencoded': {
313
+ if (typeof body === 'object' && body !== null) {
314
+ return Object.entries(body)
315
+ .map(([key, value]) => {
316
+ if (Array.isArray(value)) {
317
+ return value
318
+ .map(
319
+ (v) => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`,
320
+ )
321
+ .join('&')
322
+ } else {
323
+ return `${encodeURIComponent(key)}=${encodeURIComponent(
324
+ String(value),
325
+ )}`
326
+ }
327
+ })
328
+ .join('&')
329
+ }
330
+ return ''
331
+ }
332
+ case 'multipart/form-data': {
333
+ const formData = new FormData()
334
+ if (typeof body === 'object' && body !== null) {
335
+ Object.entries(body).forEach(([key, value]) => {
336
+ formData.set(key, value as string | Blob)
337
+ })
338
+ }
339
+ return formData
340
+ }
341
+ case 'text/plain':
342
+ return String(body)
343
+ default:
344
+ return JSON.stringify(body)
345
+ }
346
+ }
347
+
348
+ export const createApiEvent = (
349
+ eventName: 'message' | 'success' | 'failed',
350
+ detail: any,
351
+ ) => {
352
+ return new CustomEvent(eventName, {
353
+ detail,
354
+ })
355
+ }
356
+
357
+ const compareApiDependencies = <Handler>(
358
+ a: LegacyToddleApi<Handler> | ToddleApiV2<Handler>,
359
+ b: LegacyToddleApi<Handler> | ToddleApiV2<Handler>,
360
+ ) => {
361
+ const isADependentOnB = a.apiReferences.has(b.name)
362
+ const isBDependentOnA = b.apiReferences.has(a.name)
363
+ if (isADependentOnB === isBDependentOnA) {
364
+ return 0
365
+ }
366
+ // 1 means A goes last - hence B is evaluated before A
367
+ return isADependentOnB ? 1 : -1
368
+ }
369
+
370
+ export const sortApiObjects = <Handler>(
371
+ apis: Array<[string, ComponentAPI]>,
372
+ ) => {
373
+ const apiMap = new Map<
374
+ string,
375
+ LegacyToddleApi<Handler> | ToddleApiV2<Handler>
376
+ >()
377
+ const getApi = (apiObj: ComponentAPI, key: string) => {
378
+ let api = apiMap.get(key)
379
+ if (!api) {
380
+ api =
381
+ apiObj.version === 2
382
+ ? new ToddleApiV2<Handler>(
383
+ apiObj,
384
+ key,
385
+ // global formulas are not required for sorting
386
+ {
387
+ formulas: {},
388
+ packages: {},
389
+ },
390
+ )
391
+ : new LegacyToddleApi<Handler>(
392
+ apiObj,
393
+ key,
394
+ // global formulas are not required for sorting
395
+ {
396
+ formulas: {},
397
+ packages: {},
398
+ },
399
+ )
400
+ apiMap.set(key, api)
401
+ }
402
+ return api
403
+ }
404
+
405
+ return [...apis].sort(([aKey, aObj], [bKey, bObj]) => {
406
+ const a = getApi(aObj, aKey)
407
+ const b = getApi(bObj, bKey)
408
+ return compareApiDependencies(a, b)
409
+ })
410
+ }
411
+
412
+ export const sortApiEntries = <Handler>(
413
+ apis: Array<[string, LegacyToddleApi<Handler> | ToddleApiV2<Handler>]>,
414
+ ) => [...apis].sort(([_, a], [__, b]) => compareApiDependencies(a, b))
@@ -0,0 +1,145 @@
1
+ import type { EventModel } from '../component/component.types'
2
+ import type { Formula } from '../formula/formula'
3
+ import type { ToddleMetadata } from '../types'
4
+
5
+ export type ComponentAPI = LegacyComponentAPI | ApiRequest
6
+
7
+ export interface LegacyComponentAPI {
8
+ name: string
9
+ type: 'REST'
10
+ autoFetch?: Formula | null
11
+ url?: Formula
12
+ path?: { formula: Formula }[]
13
+ proxy?: boolean
14
+ queryParams?: Record<string, { name: string; formula: Formula }>
15
+ headers?: Record<string, Formula> | Formula
16
+ method?: 'GET' | 'POST' | 'DELETE' | 'PUT'
17
+ body?: Formula
18
+ auth?: {
19
+ type: 'Bearer id_token' | 'Bearer access_token'
20
+ }
21
+ throttle?: number | null
22
+ debounce?: number | null
23
+ onCompleted: EventModel | null
24
+ onFailed: EventModel | null
25
+ version?: never
26
+ }
27
+
28
+ export interface LegacyApiStatus {
29
+ data: unknown
30
+ isLoading: boolean
31
+ error: unknown
32
+ response?: never
33
+ }
34
+
35
+ export enum ApiMethod {
36
+ GET = 'GET',
37
+ POST = 'POST',
38
+ DELETE = 'DELETE',
39
+ PUT = 'PUT',
40
+ PATCH = 'PATCH',
41
+ HEAD = 'HEAD',
42
+ OPTIONS = 'OPTIONS',
43
+ }
44
+
45
+ export type RedirectStatusCode = 300 | 301 | 302 | 303 | 304 | 307 | 308
46
+
47
+ export type ApiParserMode =
48
+ | 'auto'
49
+ | 'text'
50
+ | 'json'
51
+ | 'event-stream'
52
+ | 'json-stream'
53
+ | 'blob'
54
+
55
+ export interface ApiBase extends ToddleMetadata {
56
+ url?: Formula
57
+ path?: Record<string, { formula: Formula; index: number }>
58
+ queryParams?: Record<
59
+ string,
60
+ // The enabled formula is used to determine if the query parameter should be included in the request or not
61
+ { formula: Formula; enabled?: Formula | null }
62
+ >
63
+ }
64
+
65
+ export interface ApiRequest extends ApiBase {
66
+ version: 2
67
+ name: string
68
+ type: 'http' | 'ws' // The structure for web sockets might look different
69
+ autoFetch?: Formula | null
70
+ headers?: Record<string, { formula: Formula; enabled?: Formula | null }>
71
+ method?: ApiMethod
72
+ body?: Formula
73
+ // inputs for an API request
74
+ inputs: Record<string, { formula: Formula | null }>
75
+ service?: string | null
76
+ servicePath?: string | null
77
+ server?: {
78
+ // We should only accept server side proxy requests if proxy is defined
79
+ proxy?: {
80
+ enabled: { formula: Formula }
81
+ } | null
82
+ ssr?: {
83
+ // We should only fetch a request server side during SSR if this is true
84
+ // it should probably be true by default for autofetch APIs
85
+ // During SSR we will only fetch autoFetch requests
86
+ enabled?: { formula: Formula } | null
87
+ }
88
+ }
89
+ client?: {
90
+ debounce?: { formula: Formula } | null
91
+ onCompleted?: EventModel | null
92
+ onFailed?: EventModel | null
93
+ onMessage?: EventModel | null
94
+ // parserMode is used to determine how the response should be handled
95
+ // auto: The response will be handled based on the content type of the response
96
+ // stream: The response will be handled as a stream
97
+ parserMode: ApiParserMode
98
+ }
99
+ // Shared logic for client/server 👇
100
+ // The user could distinguish using an environment
101
+ // variable e.g. IS_SERVER when they declare the formula
102
+
103
+ // Rules for redirecting the user to a different page
104
+ // These rules will run both on server+client - mostly used for 401 response -> 302 redirect
105
+ // We allow multiple rules since it makes it easier to setup multiple conditions/redirect locations
106
+ redirectRules?: Record<
107
+ string,
108
+ {
109
+ // The formula will receive the response from the server including a status code
110
+ // A redirect response will be returned if the formula returns a valid url
111
+ formula: Formula
112
+ // The status code used in the redirect response. Only relevant server side
113
+ statusCode?: RedirectStatusCode | null
114
+ index: number
115
+ }
116
+ > | null
117
+ // Formula for determining whether the response is an error or not. Receives the API response + status code/headers as input
118
+ // The response is considered an error if the formula returns true
119
+ // Default behavior is to check if the status code is 400 or higher
120
+ isError?: { formula: Formula } | null
121
+ // Formula for determining when the request should time out
122
+ timeout?: { formula: Formula } | null
123
+ }
124
+
125
+ export interface ApiStatus {
126
+ data: unknown
127
+ isLoading: boolean
128
+ error: unknown
129
+ response?: {
130
+ status?: number
131
+ headers?: Record<string, string> | null
132
+ performance?: ApiPerformance
133
+ debug?: null | unknown
134
+ }
135
+ }
136
+
137
+ export interface ApiPerformance {
138
+ requestStart: number | null
139
+ responseStart: number | null
140
+ responseEnd: number | null
141
+ }
142
+
143
+ export interface ToddleRequestInit extends RequestInit {
144
+ headers: Headers
145
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Checks if a header is a json (content-type) header
3
+ * Also supports edge cases like application/vnd.api+json and application/vnd.contentful.delivery.v1+json
4
+ * See https://jsonapi.org/#mime-types
5
+ */
6
+ export const isJsonHeader = (header?: string | null) => {
7
+ if (typeof header !== 'string') {
8
+ return false
9
+ }
10
+ return /^application\/(json|.*\+json)/.test(header)
11
+ }
12
+
13
+ export const isTextHeader = (header?: string | null) => {
14
+ if (typeof header !== 'string') {
15
+ return false
16
+ }
17
+ return /^(text\/|application\/x-www-form-urlencoded|application\/(xml|.*\+xml))/.test(
18
+ header,
19
+ )
20
+ }
21
+
22
+ export const isEventStreamHeader = (header?: string | null) => {
23
+ if (typeof header !== 'string') {
24
+ return false
25
+ }
26
+ return /^text\/event-stream/.test(header)
27
+ }
28
+
29
+ export const isJsonStreamHeader = (header?: string | null) => {
30
+ if (typeof header !== 'string') {
31
+ return false
32
+ }
33
+ return /^(application\/stream\+json|application\/x-ndjson)/.test(header)
34
+ }
35
+
36
+ export const isImageHeader = (header?: string | null) => {
37
+ if (typeof header !== 'string') {
38
+ return false
39
+ }
40
+ return /^image\//.test(header)
41
+ }
@@ -0,0 +1,10 @@
1
+ export const STRING_TEMPLATE = (
2
+ type: keyof typeof templateTypes,
3
+ name: string,
4
+ ) => {
5
+ return `{{ ${templateTypes[type]}.${name} }}`
6
+ }
7
+
8
+ const templateTypes = {
9
+ cookies: 'cookies',
10
+ }
@@ -0,0 +1,75 @@
1
+ import { ToddleComponent } from './ToddleComponent'
2
+
3
+ describe('ToddleComponent.actionReferences', () => {
4
+ test('it return custom actions used in the component', () => {
5
+ const demo = new ToddleComponent({
6
+ component: {
7
+ name: 'demo',
8
+ apis: {},
9
+ attributes: {},
10
+ nodes: {},
11
+ variables: {},
12
+ workflows: {
13
+ '7XLoA3': {
14
+ name: 'my-workflow',
15
+ actions: [
16
+ {
17
+ type: 'Custom',
18
+ name: 'MyCustomAction',
19
+ },
20
+ {
21
+ name: 'MyLegacyCustomAction',
22
+ },
23
+ ],
24
+ parameters: [],
25
+ },
26
+ },
27
+ },
28
+ getComponent: () => undefined,
29
+ packageName: 'demo',
30
+ globalFormulas: { formulas: {}, packages: {} },
31
+ })
32
+ const actions = Array.from(demo.actionReferences)
33
+ expect(actions).toEqual(['MyCustomAction', 'MyLegacyCustomAction'])
34
+ })
35
+ test('it should not include non-custom actions', () => {
36
+ const demo = new ToddleComponent({
37
+ component: {
38
+ name: 'demo',
39
+ apis: {},
40
+ attributes: {},
41
+ nodes: {},
42
+ variables: {},
43
+ workflows: {
44
+ '7XLoA3': {
45
+ name: 'my-workflow',
46
+ actions: [
47
+ {
48
+ type: 'SetVariable',
49
+ data: {
50
+ type: 'value',
51
+ value: 'Hello World',
52
+ },
53
+ variable: 'my-variable',
54
+ },
55
+ {
56
+ type: 'TriggerEvent',
57
+ event: 'my-event',
58
+ data: {
59
+ type: 'value',
60
+ value: 'Hello World',
61
+ },
62
+ },
63
+ ],
64
+ parameters: [],
65
+ },
66
+ },
67
+ },
68
+ getComponent: () => undefined,
69
+ packageName: 'demo',
70
+ globalFormulas: { formulas: {}, packages: {} },
71
+ })
72
+ const actions = Array.from(demo.actionReferences)
73
+ expect(actions).toEqual([])
74
+ })
75
+ })