@teleporthq/teleport-shared 0.43.75 → 0.44.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 (60) hide show
  1. package/__tests__/page-arrival/page-export.test.ts +109 -0
  2. package/__tests__/page-arrival/runtime.test.ts +209 -0
  3. package/__tests__/utils/table-access-trusted-reader-roles.ts +144 -0
  4. package/dist/cjs/index.d.ts +2 -1
  5. package/dist/cjs/index.d.ts.map +1 -1
  6. package/dist/cjs/index.js +3 -1
  7. package/dist/cjs/index.js.map +1 -1
  8. package/dist/cjs/page-arrival/constants.d.ts +14 -0
  9. package/dist/cjs/page-arrival/constants.d.ts.map +1 -0
  10. package/dist/cjs/page-arrival/constants.js +17 -0
  11. package/dist/cjs/page-arrival/constants.js.map +1 -0
  12. package/dist/cjs/page-arrival/index.d.ts +4 -0
  13. package/dist/cjs/page-arrival/index.d.ts.map +1 -0
  14. package/dist/cjs/page-arrival/index.js +20 -0
  15. package/dist/cjs/page-arrival/index.js.map +1 -0
  16. package/dist/cjs/page-arrival/page-export.d.ts +14 -0
  17. package/dist/cjs/page-arrival/page-export.d.ts.map +1 -0
  18. package/dist/cjs/page-arrival/page-export.js +88 -0
  19. package/dist/cjs/page-arrival/page-export.js.map +1 -0
  20. package/dist/cjs/page-arrival/runtime.d.ts +21 -0
  21. package/dist/cjs/page-arrival/runtime.d.ts.map +1 -0
  22. package/dist/cjs/page-arrival/runtime.js +26 -0
  23. package/dist/cjs/page-arrival/runtime.js.map +1 -0
  24. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  25. package/dist/cjs/utils/table-access.d.ts +17 -4
  26. package/dist/cjs/utils/table-access.d.ts.map +1 -1
  27. package/dist/cjs/utils/table-access.js +55 -14
  28. package/dist/cjs/utils/table-access.js.map +1 -1
  29. package/dist/esm/index.d.ts +2 -1
  30. package/dist/esm/index.d.ts.map +1 -1
  31. package/dist/esm/index.js +2 -1
  32. package/dist/esm/index.js.map +1 -1
  33. package/dist/esm/page-arrival/constants.d.ts +14 -0
  34. package/dist/esm/page-arrival/constants.d.ts.map +1 -0
  35. package/dist/esm/page-arrival/constants.js +14 -0
  36. package/dist/esm/page-arrival/constants.js.map +1 -0
  37. package/dist/esm/page-arrival/index.d.ts +4 -0
  38. package/dist/esm/page-arrival/index.d.ts.map +1 -0
  39. package/dist/esm/page-arrival/index.js +4 -0
  40. package/dist/esm/page-arrival/index.js.map +1 -0
  41. package/dist/esm/page-arrival/page-export.d.ts +14 -0
  42. package/dist/esm/page-arrival/page-export.d.ts.map +1 -0
  43. package/dist/esm/page-arrival/page-export.js +61 -0
  44. package/dist/esm/page-arrival/page-export.js.map +1 -0
  45. package/dist/esm/page-arrival/runtime.d.ts +21 -0
  46. package/dist/esm/page-arrival/runtime.d.ts.map +1 -0
  47. package/dist/esm/page-arrival/runtime.js +22 -0
  48. package/dist/esm/page-arrival/runtime.js.map +1 -0
  49. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  50. package/dist/esm/utils/table-access.d.ts +17 -4
  51. package/dist/esm/utils/table-access.d.ts.map +1 -1
  52. package/dist/esm/utils/table-access.js +53 -13
  53. package/dist/esm/utils/table-access.js.map +1 -1
  54. package/package.json +3 -3
  55. package/src/index.ts +2 -0
  56. package/src/page-arrival/constants.ts +15 -0
  57. package/src/page-arrival/index.ts +3 -0
  58. package/src/page-arrival/page-export.ts +58 -0
  59. package/src/page-arrival/runtime.ts +75 -0
  60. package/src/utils/table-access.ts +50 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teleporthq/teleport-shared",
3
- "version": "0.43.75",
3
+ "version": "0.44.0",
4
4
  "description": "A utility belt for the entire teleportHQ ecosystem",
5
5
  "author": "teleportHQ",
6
6
  "license": "MIT",
@@ -25,9 +25,9 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@babel/types": "^7.5.5",
28
- "@teleporthq/teleport-types": "^0.43.75",
28
+ "@teleporthq/teleport-types": "^0.44.0",
29
29
  "jss": "^10.0.0",
30
30
  "jss-preset-default": "^10.0.0"
31
31
  },
32
- "gitHead": "4bc0af8166575b4af971f39e63228ce2eefce661"
32
+ "gitHead": "3f8ab44ace8163b154799b7ea68ece45b3cf9022"
33
33
  }
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ import * as RichTextContent from './utils/rich-text-content'
31
31
  import * as RichTextContentCodegen from './utils/rich-text-content-codegen'
32
32
  import * as DataCache from './cache'
33
33
  import * as PageRevalidate from './page-revalidate'
34
+ import * as PageArrival from './page-arrival'
34
35
  import * as ScrollSceneRuntime from './scroll-scene'
35
36
  import * as MotionRuntime from './motion-runtime'
36
37
  import * as PageTransition from './page-transition'
@@ -72,6 +73,7 @@ export {
72
73
  RichTextContentCodegen,
73
74
  DataCache,
74
75
  PageRevalidate,
76
+ PageArrival,
75
77
  ScrollSceneRuntime,
76
78
  MotionRuntime,
77
79
  PageTransition,
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Where the generated page-arrival wrapper lives and what pages import it as.
3
+ *
4
+ * Lives in `teleport-shared` because two page plugins in two packages ask for
5
+ * it — the workflows plugin (page-load and element-visible triggers) and the
6
+ * state data-source plugin (state seeded from a data source) — and they must
7
+ * emit the same file under the same name.
8
+ */
9
+
10
+ /** Folder of the emitted runtime, relative to the generated project root. */
11
+ export const PAGE_ARRIVAL_DIR = ['utils']
12
+ export const PAGE_ARRIVAL_FILE_NAME = 'page-arrival'
13
+
14
+ /** The default export of the runtime, as the page module imports it. */
15
+ export const PAGE_ARRIVAL_WRAPPER = 'remountOnPageArrival'
@@ -0,0 +1,3 @@
1
+ export * from './constants'
2
+ export * from './runtime'
3
+ export * from './page-export'
@@ -0,0 +1,58 @@
1
+ import * as types from '@babel/types'
2
+ import { ComponentStructure, FileType } from '@teleporthq/teleport-types'
3
+ import { generatePageToRootPrefix } from '../utils/generic'
4
+ import { PAGE_ARRIVAL_DIR, PAGE_ARRIVAL_FILE_NAME, PAGE_ARRIVAL_WRAPPER } from './constants'
5
+ import { generatePageArrivalRuntime } from './runtime'
6
+
7
+ /** The chunk the React base-component plugin emits `export default <Page>` into. */
8
+ const EXPORT_CHUNK_NAME = 'export'
9
+
10
+ const isPageArrivalWrapper = (expression: types.Node): boolean =>
11
+ expression.type === 'CallExpression' &&
12
+ expression.callee.type === 'Identifier' &&
13
+ expression.callee.name === PAGE_ARRIVAL_WRAPPER
14
+
15
+ /**
16
+ * Makes the page start fresh on every arrival — see `generatePageArrivalRuntime`
17
+ * for why Next.js does not do it on its own.
18
+ *
19
+ * Rewrites `export default Page` into `export default remountOnPageArrival(Page)`,
20
+ * imports the wrapper and emits it once per project. Several plugins may ask for
21
+ * the same page, so a page that is already wrapped is left alone. Nothing is
22
+ * changed when the page has no plain default export to wrap, or when the
23
+ * generation has nowhere to emit the runtime — an import of a file that is never
24
+ * written would break the build.
25
+ */
26
+ export const remountPageOnArrival = (structure: ComponentStructure): void => {
27
+ const { chunks, dependencies, options, uidl } = structure
28
+ const exportChunk = chunks.find((chunk) => chunk.name === EXPORT_CHUNK_NAME)
29
+ const exportDeclaration = exportChunk?.content as types.Node | undefined
30
+
31
+ if (
32
+ !options.extractedResources ||
33
+ !exportDeclaration ||
34
+ exportDeclaration.type !== 'ExportDefaultDeclaration' ||
35
+ isPageArrivalWrapper(exportDeclaration.declaration) ||
36
+ exportDeclaration.declaration.type !== 'Identifier'
37
+ ) {
38
+ return
39
+ }
40
+
41
+ exportDeclaration.declaration = types.callExpression(types.identifier(PAGE_ARRIVAL_WRAPPER), [
42
+ exportDeclaration.declaration,
43
+ ])
44
+
45
+ const runtimePath = [...PAGE_ARRIVAL_DIR, PAGE_ARRIVAL_FILE_NAME].join('/')
46
+ const rootPrefix = generatePageToRootPrefix({
47
+ folderPath: uidl.outputOptions?.folderPath,
48
+ pagesPath: options.pagesPath,
49
+ })
50
+ dependencies[PAGE_ARRIVAL_WRAPPER] = { type: 'local', path: `${rootPrefix}${runtimePath}` }
51
+
52
+ options.extractedResources[runtimePath] = {
53
+ fileName: PAGE_ARRIVAL_FILE_NAME,
54
+ fileType: FileType.JS,
55
+ path: [...PAGE_ARRIVAL_DIR],
56
+ content: generatePageArrivalRuntime(),
57
+ }
58
+ }
@@ -0,0 +1,75 @@
1
+ import { PAGE_ARRIVAL_WRAPPER } from './constants'
2
+
3
+ /**
4
+ * `utils/page-arrival.js` — the wrapper a page's default export goes through
5
+ * when the page computes something on arrival.
6
+ *
7
+ * Next.js keeps a page mounted when a navigation lands on the page that is
8
+ * already on screen: a link to the current URL, another record of the same
9
+ * dynamic route, another query string, another language. The page receives
10
+ * the new props but keeps every `useState` seeded on its first arrival, and no
11
+ * mount effect runs again — so page-load and element-visible workflows stay
12
+ * silent and data-source states keep their old rows until a full reload.
13
+ *
14
+ * The wrapper gives the page a new key for each such arrival, which starts it
15
+ * exactly like a full load does. Only a navigation that STARTED while the page
16
+ * was mounted and ENDED on its own route counts: one that ends elsewhere
17
+ * unmounts the page anyway, and React 17 runs that unmount's effect cleanup
18
+ * after the router has already announced the completed navigation. Shallow
19
+ * navigations (state mirrored into the URL) and hash changes keep the page as
20
+ * it is — they are not arrivals.
21
+ */
22
+ export const generatePageArrivalRuntime =
23
+ (): string => `import React, { useEffect, useState } from 'react'
24
+ import Router from 'next/router'
25
+
26
+ function isShallow(options) {
27
+ return !!(options && options.shallow)
28
+ }
29
+
30
+ function usePageArrivals() {
31
+ const [arrivals, setArrivals] = useState(0)
32
+
33
+ useEffect(() => {
34
+ const route = Router.pathname
35
+ let arriving = false
36
+
37
+ function handleStart(url, options) {
38
+ arriving = !isShallow(options)
39
+ }
40
+
41
+ function handleComplete(url, options) {
42
+ const arrived = arriving && !isShallow(options) && Router.pathname === route
43
+ arriving = false
44
+ if (arrived) {
45
+ setArrivals(function (count) {
46
+ return count + 1
47
+ })
48
+ }
49
+ }
50
+
51
+ function handleError() {
52
+ arriving = false
53
+ }
54
+
55
+ Router.events.on('routeChangeStart', handleStart)
56
+ Router.events.on('routeChangeComplete', handleComplete)
57
+ Router.events.on('routeChangeError', handleError)
58
+ return function () {
59
+ Router.events.off('routeChangeStart', handleStart)
60
+ Router.events.off('routeChangeComplete', handleComplete)
61
+ Router.events.off('routeChangeError', handleError)
62
+ }
63
+ }, [])
64
+
65
+ return arrivals
66
+ }
67
+
68
+ export default function ${PAGE_ARRIVAL_WRAPPER}(Page) {
69
+ function PageArrival(props) {
70
+ return <Page key={usePageArrivals()} {...props} />
71
+ }
72
+ PageArrival.displayName = 'PageArrival(' + (Page.displayName || Page.name || 'Page') + ')'
73
+ return PageArrival
74
+ }
75
+ `
@@ -91,6 +91,19 @@ export const CUSTOMER_RECORD_TABLES: ReadonlyArray<string> = [
91
91
  'teleport_ai_chat_documents',
92
92
  'teleport_ai_chat_conversations',
93
93
  'teleport_ai_chat_messages',
94
+ // A booking carries the customer's name, email and phone and the key of
95
+ // their booking link; the capacity counters decide who gets the last place;
96
+ // the business's settings row carries the address its notices go to. The
97
+ // booking pages read them only in server segments (public columns only),
98
+ // the admin with its session.
99
+ 'teleport_bookings',
100
+ 'teleport_booking_units',
101
+ 'teleport_booking_settings',
102
+ // A calendar link holds the other platform's private calendar address and
103
+ // the key of this site's own feed; its blocks are that platform's bookings.
104
+ // Only the sync's server segments and the feed route read them.
105
+ 'teleport_booking_calendars',
106
+ 'teleport_booking_calendar_blocks',
94
107
  ]
95
108
 
96
109
  export const PROTECTED_TABLES: ReadonlyArray<string> = [...MONEY_TABLES, ...CUSTOMER_RECORD_TABLES]
@@ -183,30 +196,55 @@ const ADMIN_FOLDER_NAME = 'admin-panel'
183
196
  const ADMIN_ROUTE_PREFIX = '/admin'
184
197
 
185
198
  /**
186
- * The roles allowed to read a protected table from a browser: whoever may open
187
- * the generated admin panel, read off the protection the UIDL records for its
188
- * folder and pages. A project without an admin panel has no page that binds a
189
- * money table, so the empty list (server-side callers only) is the right answer.
199
+ * The roles that may open the generated admin panel as a whole: the roles its
200
+ * FOLDER admits, which the middleware and the workflow-route guard union into
201
+ * every page under it. A page's own roles never count — an admin page that also
202
+ * admits another role (a staff screen) must not hand that role the rest of the
203
+ * panel.
204
+ *
205
+ * A folder that records no roles (an older project, or one an owner edited)
206
+ * falls back to the roles EVERY protected admin page admits. Sorted; empty
207
+ * without an admin panel.
190
208
  */
191
- export const resolveTrustedReaderRoles = (auth?: UIDLAuthentication | null): string[] => {
192
- const roles = new Set<string>()
209
+ export const resolveAdminPanelRoles = (auth?: UIDLAuthentication | null): string[] => {
193
210
  if (!auth) {
194
211
  return []
195
212
  }
213
+ const folderRoles = new Set<string>()
196
214
  Object.values(auth.folderProtection || {}).forEach((folder) => {
197
215
  if (folder && folder.folderName === ADMIN_FOLDER_NAME) {
198
- ;(folder.allowedRoles || []).forEach((role) => roles.add(role))
216
+ ;(folder.allowedRoles || []).forEach((role) => folderRoles.add(role))
199
217
  }
200
218
  })
201
- Object.values(auth.pageProtection || {}).forEach((page) => {
219
+ if (folderRoles.size > 0) {
220
+ return Array.from(folderRoles).sort()
221
+ }
222
+
223
+ let everyPageAdmits: string[] | null = null
224
+ for (const page of Object.values(auth.pageProtection || {})) {
202
225
  const route = page && typeof page.route === 'string' ? page.route : ''
203
- if (route === ADMIN_ROUTE_PREFIX || route.startsWith(`${ADMIN_ROUTE_PREFIX}/`)) {
204
- ;(page.allowedRoles || []).forEach((role) => roles.add(role))
226
+ if (route !== ADMIN_ROUTE_PREFIX && !route.startsWith(`${ADMIN_ROUTE_PREFIX}/`)) {
227
+ continue
205
228
  }
206
- })
207
- return Array.from(roles).sort()
229
+ const pageRoles = page.allowedRoles || []
230
+ everyPageAdmits =
231
+ everyPageAdmits === null
232
+ ? Array.from(new Set(pageRoles))
233
+ : everyPageAdmits.filter((role) => pageRoles.indexOf(role) !== -1)
234
+ }
235
+ return (everyPageAdmits || []).sort()
208
236
  }
209
237
 
238
+ /**
239
+ * The roles allowed to read a protected table from a browser: the admin
240
+ * panel's (`resolveAdminPanelRoles`) — so a role that opens only some admin
241
+ * pages reads no account, email or order. A project without an admin panel
242
+ * has no page that binds a money table, so the empty list (server-side
243
+ * callers only) is the right answer.
244
+ */
245
+ export const resolveTrustedReaderRoles = (auth?: UIDLAuthentication | null): string[] =>
246
+ resolveAdminPanelRoles(auth)
247
+
210
248
  /**
211
249
  * ES5 source of the guard, inlined into a generated route. Declares:
212
250
  * - `__taIsInternalRequest(req)` — the caller presented the app secret, or