diff --git a/packages/client/src/components/app/Link.svelte b/packages/client/src/components/app/Link.svelte
index 79bf296208..6cabcec7df 100644
--- a/packages/client/src/components/app/Link.svelte
+++ b/packages/client/src/components/app/Link.svelte
@@ -1,7 +1,8 @@
diff --git a/packages/client/src/sdk.js b/packages/client/src/sdk.js
index 264cc85626..c9ff1eba36 100644
--- a/packages/client/src/sdk.js
+++ b/packages/client/src/sdk.js
@@ -12,6 +12,7 @@ import {
environmentStore,
sidePanelStore,
dndIsDragging,
+ confirmationStore,
} from "stores"
import { styleable } from "utils/styleable"
import { linkable } from "utils/linkable"
@@ -35,6 +36,7 @@ export default {
sidePanelStore,
dndIsDragging,
currentRole,
+ confirmationStore,
styleable,
linkable,
getAction,
diff --git a/packages/client/src/utils/domDebounce.js b/packages/client/src/utils/domDebounce.js
index b7fc017247..b15d2698b4 100644
--- a/packages/client/src/utils/domDebounce.js
+++ b/packages/client/src/utils/domDebounce.js
@@ -1,12 +1,14 @@
-export const domDebounce = callback => {
+export const domDebounce = (callback, extractParams = x => x) => {
let active = false
- return e => {
+ let lastParams
+ return (...params) => {
+ lastParams = extractParams(...params)
if (!active) {
- window.requestAnimationFrame(() => {
- callback(e)
+ active = true
+ requestAnimationFrame(() => {
+ callback(lastParams)
active = false
})
- active = true
}
}
}
diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json
index 4a6004b0ed..70cec3f9e0 100644
--- a/packages/frontend-core/package.json
+++ b/packages/frontend-core/package.json
@@ -1,14 +1,16 @@
{
"name": "@budibase/frontend-core",
- "version": "2.5.5-alpha.0",
+ "version": "2.5.6-alpha.32",
"description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase",
"license": "MPL-2.0",
"svelte": "src/index.js",
"dependencies": {
- "@budibase/bbui": "2.5.5-alpha.0",
- "@budibase/shared-core": "2.5.5-alpha.0",
+ "@budibase/bbui": "2.5.6-alpha.32",
+ "@budibase/shared-core": "2.5.6-alpha.32",
+ "dayjs": "^1.11.7",
"lodash": "^4.17.21",
+ "socket.io-client": "^4.6.1",
"svelte": "^3.46.2"
}
}
diff --git a/packages/frontend-core/src/api/other.js b/packages/frontend-core/src/api/other.js
index ac4b481395..3d171eaab4 100644
--- a/packages/frontend-core/src/api/other.js
+++ b/packages/frontend-core/src/api/other.js
@@ -1,13 +1,4 @@
export const buildOtherEndpoints = API => ({
- /**
- * TODO: find out what this is
- */
- checkImportComplete: async () => {
- return await API.get({
- url: "/api/cloud/import/complete",
- })
- },
-
/**
* Gets the current environment details.
*/
diff --git a/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte
new file mode 100644
index 0000000000..4d830723c2
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/cells/AttachmentCell.svelte
@@ -0,0 +1,154 @@
+
+
+
+ {#each value || [] as attachment}
+ {#if isImage(attachment.extension)}
+
+ {:else}
+
+ {attachment.extension}
+
+ {/if}
+ {/each}
+
+
+{#if isOpen}
+
+ onChange(e.detail)}
+ {processFiles}
+ {deleteAttachments}
+ {handleFileTooLarge}
+ />
+
+{/if}
+
+
diff --git a/packages/frontend-core/src/components/grid/cells/BooleanCell.svelte b/packages/frontend-core/src/components/grid/cells/BooleanCell.svelte
new file mode 100644
index 0000000000..52aecb07a7
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/cells/BooleanCell.svelte
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
diff --git a/packages/frontend-core/src/components/grid/cells/DataCell.svelte b/packages/frontend-core/src/components/grid/cells/DataCell.svelte
new file mode 100644
index 0000000000..5a2e02340f
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/cells/DataCell.svelte
@@ -0,0 +1,88 @@
+
+
+
focusedCellId.set(cellId)}
+ on:contextmenu={e => menu.actions.open(cellId, e)}
+ width={column.width}
+>
+
+
diff --git a/packages/frontend-core/src/components/grid/cells/DateCell.svelte b/packages/frontend-core/src/components/grid/cells/DateCell.svelte
new file mode 100644
index 0000000000..0112bcda15
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/cells/DateCell.svelte
@@ -0,0 +1,77 @@
+
+
+
+
+ {#if value}
+ {dayjs(timeOnly ? time : value).format(format)}
+ {/if}
+
+ {#if editable}
+
+ {/if}
+
+
+{#if editable}
+
+ onChange(e.detail)}
+ appendTo={document.documentElement}
+ enableTime={!dateOnly}
+ {timeOnly}
+ time24hr
+ ignoreTimezones={schema.ignoreTimezones}
+ />
+
+{/if}
+
+
diff --git a/packages/frontend-core/src/components/grid/cells/FormulaCell.svelte b/packages/frontend-core/src/components/grid/cells/FormulaCell.svelte
new file mode 100644
index 0000000000..b4db795e44
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/cells/FormulaCell.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/packages/frontend-core/src/components/grid/cells/GridCell.svelte b/packages/frontend-core/src/components/grid/cells/GridCell.svelte
new file mode 100644
index 0000000000..dfc53f6f0c
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/cells/GridCell.svelte
@@ -0,0 +1,162 @@
+
+
+
+ {#if error}
+
+ {error}
+
+ {/if}
+
+ {#if selectedUser && !focused}
+
+ {selectedUser.label}
+
+ {/if}
+
+
+
diff --git a/packages/frontend-core/src/components/grid/cells/GutterCell.svelte b/packages/frontend-core/src/components/grid/cells/GutterCell.svelte
new file mode 100644
index 0000000000..d9fd09fb6c
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/cells/GutterCell.svelte
@@ -0,0 +1,127 @@
+
+
+
+
+ {#if $$slots.default}
+
+ {:else}
+
+
+
+ {#if !disableNumber}
+
+ {row.__idx + 1}
+
+ {/if}
+ {/if}
+ {#if $config.allowExpandRows}
+
+
+
+ {/if}
+
+
+
+
diff --git a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte
new file mode 100644
index 0000000000..165711c51f
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte
@@ -0,0 +1,246 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-core/src/components/grid/cells/JSONCell.svelte b/packages/frontend-core/src/components/grid/cells/JSONCell.svelte
new file mode 100644
index 0000000000..30803fd862
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/cells/JSONCell.svelte
@@ -0,0 +1,36 @@
+
+
+
diff --git a/packages/frontend-core/src/components/grid/cells/LongFormCell.svelte b/packages/frontend-core/src/components/grid/cells/LongFormCell.svelte
new file mode 100644
index 0000000000..00e12dc6a3
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/cells/LongFormCell.svelte
@@ -0,0 +1,120 @@
+
+
+{#if isOpen}
+
+{:else}
+
+{/if}
+
+
diff --git a/packages/frontend-core/src/components/grid/cells/MultiSelectCell.svelte b/packages/frontend-core/src/components/grid/cells/MultiSelectCell.svelte
new file mode 100644
index 0000000000..403aeaf8d8
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/cells/MultiSelectCell.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/packages/frontend-core/src/components/grid/cells/NumberCell.svelte b/packages/frontend-core/src/components/grid/cells/NumberCell.svelte
new file mode 100644
index 0000000000..00c101026e
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/cells/NumberCell.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/packages/frontend-core/src/components/grid/cells/OptionsCell.svelte b/packages/frontend-core/src/components/grid/cells/OptionsCell.svelte
new file mode 100644
index 0000000000..f3b6b9b59d
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/cells/OptionsCell.svelte
@@ -0,0 +1,240 @@
+
+
+
+
+ {#each values as val}
+ {@const color = getOptionColor(val)}
+ {#if color}
+
+
+ {val}
+
+
+ {:else}
+
+ {val || ""}
+
+ {/if}
+ {/each}
+
+ {#if editable}
+
+
+
+ {/if}
+ {#if isOpen}
+
e.stopPropagation()}
+ >
+ {#each options as option, idx}
+ {@const color = getOptionColor(option)}
+
toggleOption(option)}
+ class:focused={focusedOptionIdx === idx}
+ on:mouseenter={() => (focusedOptionIdx = idx)}
+ >
+
+ {option}
+
+ {#if values.includes(option)}
+
+ {/if}
+
+ {/each}
+
+ {/if}
+
+
+
diff --git a/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte b/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte
new file mode 100644
index 0000000000..90182e9983
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte
@@ -0,0 +1,504 @@
+
+
+
+
+
+
+
(focused ? e.stopPropagation() : null)}>
+ {#each value || [] as relationship, idx}
+ {#if relationship.primaryDisplay}
+
+ showRelationship(relationship._id)
+ : null}
+ >
+ {readable(relationship.primaryDisplay)}
+
+ {#if editable}
+ toggleRow(relationship)}
+ />
+ {/if}
+
+ {/if}
+ {/each}
+ {#if editable}
+
+
+
+ {/if}
+
+ {#if value?.length}
+
+ {value?.length || 0}
+
+ {/if}
+
+
+ {#if isOpen}
+
+
+
+
+ {#if searching}
+
+ {:else if searchResults?.length}
+
+ {#each searchResults as row, idx}
+
toggleRow(row)}
+ class:candidate={idx === candidateIndex}
+ on:mouseenter={() => (candidateIndex = idx)}
+ >
+
+
+ {readable(row.primaryDisplay)}
+
+
+ {#if isRowSelected(row)}
+
+ {/if}
+
+ {/each}
+
+ {/if}
+
+ {/if}
+
+
+
diff --git a/packages/frontend-core/src/components/grid/cells/TextCell.svelte b/packages/frontend-core/src/components/grid/cells/TextCell.svelte
new file mode 100644
index 0000000000..533b030b5c
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/cells/TextCell.svelte
@@ -0,0 +1,111 @@
+
+
+{#if editable}
+
(active = true)}
+ on:blur={() => (active = false)}
+ {type}
+ value={value || ""}
+ on:change={handleChange}
+ spellcheck="false"
+ />
+{:else}
+
+{/if}
+
+
diff --git a/packages/frontend-core/src/components/grid/controls/AddColumnButton.svelte b/packages/frontend-core/src/components/grid/controls/AddColumnButton.svelte
new file mode 100644
index 0000000000..6ad241eb65
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/controls/AddColumnButton.svelte
@@ -0,0 +1,16 @@
+
+
+
dispatch("add-column")}
+ disabled={!$config.allowAddColumns}
+>
+ Add column
+
diff --git a/packages/frontend-core/src/components/grid/controls/AddRowButton.svelte b/packages/frontend-core/src/components/grid/controls/AddRowButton.svelte
new file mode 100644
index 0000000000..71062d6a1a
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/controls/AddRowButton.svelte
@@ -0,0 +1,18 @@
+
+
+
dispatch("add-row-inline")}
+ disabled={!loaded ||
+ !$config.allowAddRows ||
+ (!$columns.length && !$stickyColumn)}
+>
+ Add row
+
diff --git a/packages/frontend-core/src/components/grid/controls/BetaButton.svelte b/packages/frontend-core/src/components/grid/controls/BetaButton.svelte
new file mode 100644
index 0000000000..e5069388e7
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/controls/BetaButton.svelte
@@ -0,0 +1,46 @@
+
+
+
+
+ Enjoying the Grid?
+
+
+
+
diff --git a/packages/frontend-core/src/components/grid/controls/ColumnWidthButton.svelte b/packages/frontend-core/src/components/grid/controls/ColumnWidthButton.svelte
new file mode 100644
index 0000000000..754aebbb51
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/controls/ColumnWidthButton.svelte
@@ -0,0 +1,91 @@
+
+
+
+
(open = !open)}
+ selected={open}
+ disabled={!allCols.length}
+ >
+ Width
+
+
+
+
+
+ {#each sizeOptions as option}
+
changeColumnWidth(option.size)}
+ selected={option.selected}
+ >
+ {option.label}
+
+ {/each}
+ {#if custom}
+
Custom
+ {/if}
+
+
+
+
diff --git a/packages/frontend-core/src/components/grid/controls/DeleteButton.svelte b/packages/frontend-core/src/components/grid/controls/DeleteButton.svelte
new file mode 100644
index 0000000000..8ca5f0920d
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/controls/DeleteButton.svelte
@@ -0,0 +1,66 @@
+
+
+{#if selectedRowCount}
+
+
+
+{/if}
+
+
+
+ Are you sure you want to delete {selectedRowCount}
+ row{selectedRowCount === 1 ? "" : "s"}?
+
+
+
+
diff --git a/packages/frontend-core/src/components/grid/controls/HideColumnsButton.svelte b/packages/frontend-core/src/components/grid/controls/HideColumnsButton.svelte
new file mode 100644
index 0000000000..30904fbebc
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/controls/HideColumnsButton.svelte
@@ -0,0 +1,95 @@
+
+
+
+
(open = !open)}
+ selected={open || anyHidden}
+ disabled={!$columns.length}
+ >
+ Columns
+
+
+
+
+
+
+ {#if $stickyColumn}
+
+ {$stickyColumn.label}
+ {/if}
+ {#each $columns as column}
+ toggleVisibility(column, e.detail)}
+ />
+ {column.label}
+ {/each}
+
+
+
+
+
+
diff --git a/packages/frontend-core/src/components/grid/controls/RowHeightButton.svelte b/packages/frontend-core/src/components/grid/controls/RowHeightButton.svelte
new file mode 100644
index 0000000000..e9045ac5c7
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/controls/RowHeightButton.svelte
@@ -0,0 +1,70 @@
+
+
+
+
(open = !open)}
+ selected={open}
+ >
+ Height
+
+
+
+
+
+ {#each sizeOptions as option}
+
changeRowHeight(option.size)}
+ >
+ {option.label}
+
+ {/each}
+
+
+
+
diff --git a/packages/frontend-core/src/components/grid/controls/SortButton.svelte b/packages/frontend-core/src/components/grid/controls/SortButton.svelte
new file mode 100644
index 0000000000..26eba050bf
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/controls/SortButton.svelte
@@ -0,0 +1,116 @@
+
+
+
+
(open = !open)}
+ selected={open}
+ disabled={!columnOptions.length}
+ >
+ Sort
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-core/src/components/grid/index.js b/packages/frontend-core/src/components/grid/index.js
new file mode 100644
index 0000000000..25747ec142
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/index.js
@@ -0,0 +1 @@
+export { default as Grid } from "./layout/Grid.svelte"
diff --git a/packages/frontend-core/src/components/grid/layout/Avatar.svelte b/packages/frontend-core/src/components/grid/layout/Avatar.svelte
new file mode 100644
index 0000000000..e3eec9f830
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/layout/Avatar.svelte
@@ -0,0 +1,24 @@
+
+
+
+ {user.email[0]}
+
+
+
diff --git a/packages/frontend-core/src/components/grid/layout/Grid.svelte b/packages/frontend-core/src/components/grid/layout/Grid.svelte
new file mode 100644
index 0000000000..36a3802164
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/layout/Grid.svelte
@@ -0,0 +1,254 @@
+
+
+
+
+ {#if $loaded}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/if}
+ {#if $loading}
+
+ {/if}
+
+
+
+
diff --git a/packages/frontend-core/src/components/grid/layout/GridBody.svelte b/packages/frontend-core/src/components/grid/layout/GridBody.svelte
new file mode 100644
index 0000000000..67f5f03898
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/layout/GridBody.svelte
@@ -0,0 +1,73 @@
+
+
+
+
+ {#each $renderedRows as row, idx}
+ = $rowVerticalInversionIndex} />
+ {/each}
+ {#if $config.allowAddRows && $renderedColumns.length}
+ ($hoveredRowId = BlankRowID)}
+ on:mouseleave={() => ($hoveredRowId = null)}
+ on:click={() => dispatch("add-row-inline")}
+ />
+ {/if}
+
+
+
+
diff --git a/packages/frontend-core/src/components/grid/layout/GridRow.svelte b/packages/frontend-core/src/components/grid/layout/GridRow.svelte
new file mode 100644
index 0000000000..eb1fd8b96a
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/layout/GridRow.svelte
@@ -0,0 +1,57 @@
+
+
+ ($hoveredRowId = row._id)}
+ on:mouseleave={() => ($hoveredRowId = null)}
+>
+ {#each $renderedColumns as column, columnIdx (column.name)}
+ {@const cellId = `${row._id}-${column.name}`}
+ = $columnHorizontalInversionIndex}
+ highlighted={rowHovered || rowFocused || reorderSource === column.name}
+ selected={rowSelected}
+ rowIdx={idx}
+ focused={$focusedCellId === cellId}
+ selectedUser={$selectedCellMap[cellId]}
+ width={column.width}
+ />
+ {/each}
+
+
+
diff --git a/packages/frontend-core/src/components/grid/layout/GridScrollWrapper.svelte b/packages/frontend-core/src/components/grid/layout/GridScrollWrapper.svelte
new file mode 100644
index 0000000000..04f0960057
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/layout/GridScrollWrapper.svelte
@@ -0,0 +1,81 @@
+
+
+ ($focusedCellId = null)}
+>
+
+
+
+
+
+
diff --git a/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte b/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte
new file mode 100644
index 0000000000..2ec186a4d6
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/layout/HeaderRow.svelte
@@ -0,0 +1,62 @@
+
+
+
+
+
diff --git a/packages/frontend-core/src/components/grid/layout/NewRow.svelte b/packages/frontend-core/src/components/grid/layout/NewRow.svelte
new file mode 100644
index 0000000000..54fef78301
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/layout/NewRow.svelte
@@ -0,0 +1,265 @@
+
+
+
+{#if isAdding}
+ 0}
+ style="--offset:{offset}px; --sticky-width:{width}px;"
+ >
+
+
+
+
+
+
+ {#if $stickyColumn}
+ {@const cellId = `${NewRowID}-${$stickyColumn.name}`}
+
+ {/if}
+
+
+
+
+ {#each $renderedColumns as column, columnIdx}
+ {@const cellId = `new-${column.name}`}
+ {#key cellId}
+ = $columnHorizontalInversionIndex}
+ {invertY}
+ />
+ {/key}
+ {/each}
+
+
+
+
+
+
+
+
+{/if}
+
+
diff --git a/packages/frontend-core/src/components/grid/layout/RowCount.svelte b/packages/frontend-core/src/components/grid/layout/RowCount.svelte
new file mode 100644
index 0000000000..6115b1f567
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/layout/RowCount.svelte
@@ -0,0 +1,9 @@
+
+
+
+ {$rows.length} row{$rows.length === 1 ? "" : "s"}
+
diff --git a/packages/frontend-core/src/components/grid/layout/StickyColumn.svelte b/packages/frontend-core/src/components/grid/layout/StickyColumn.svelte
new file mode 100644
index 0000000000..6f10c30695
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/layout/StickyColumn.svelte
@@ -0,0 +1,171 @@
+
+
+ 0}
+>
+
+
+
($hoveredRowId = null)}>
+
+ {#each $renderedRows as row, idx}
+ {@const rowSelected = !!$selectedRows[row._id]}
+ {@const rowHovered = $hoveredRowId === row._id}
+ {@const rowFocused = $focusedRow?._id === row._id}
+ {@const cellId = `${row._id}-${$stickyColumn?.name}`}
+ ($hoveredRowId = row._id)}
+ on:mouseleave={() => ($hoveredRowId = null)}
+ >
+
+ {#if $stickyColumn}
+
+ {/if}
+
+ {/each}
+ {#if $config.allowAddRows && ($renderedColumns.length || $stickyColumn)}
+ ($hoveredRowId = BlankRowID)}
+ on:mouseleave={() => ($hoveredRowId = null)}
+ on:click={() => dispatch("add-row-inline")}
+ >
+
+
+
+ {#if $stickyColumn}
+
+ {/if}
+
+ {/if}
+
+
+
+
+
diff --git a/packages/frontend-core/src/components/grid/layout/UserAvatars.svelte b/packages/frontend-core/src/components/grid/layout/UserAvatars.svelte
new file mode 100644
index 0000000000..be33b6713d
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/layout/UserAvatars.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {#each $users as user}
+
+ {/each}
+
+
+
diff --git a/packages/frontend-core/src/components/grid/lib/constants.js b/packages/frontend-core/src/components/grid/lib/constants.js
new file mode 100644
index 0000000000..a7209d6ea4
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/lib/constants.js
@@ -0,0 +1,14 @@
+export const Padding = 128
+export const MaxCellRenderHeight = 252
+export const MaxCellRenderWidthOverflow = 200
+export const ScrollBarSize = 8
+export const GutterWidth = 72
+export const DefaultColumnWidth = 200
+export const MinColumnWidth = 80
+export const SmallRowHeight = 36
+export const MediumRowHeight = 64
+export const LargeRowHeight = 92
+export const DefaultRowHeight = SmallRowHeight
+export const NewRowID = "new"
+export const BlankRowID = "blank"
+export const RowPageSize = 100
diff --git a/packages/frontend-core/src/components/grid/lib/events.js b/packages/frontend-core/src/components/grid/lib/events.js
new file mode 100644
index 0000000000..1c486858b2
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/lib/events.js
@@ -0,0 +1,29 @@
+import { createEventDispatcher } from "svelte"
+
+export const createEventManagers = () => {
+ const svelteDispatch = createEventDispatcher()
+ let subscribers = {}
+
+ // Dispatches an event, notifying subscribers and also emitting a normal
+ // svelte event
+ const dispatch = (event, payload) => {
+ svelteDispatch(event, payload)
+ const subs = subscribers[event] || []
+ for (let i = 0; i < subs.length; i++) {
+ subs[i](payload)
+ }
+ }
+
+ // Subscribes to events
+ const subscribe = (event, callback) => {
+ const subs = subscribers[event] || []
+ subscribers[event] = [...subs, callback]
+
+ // Return unsubscribe function
+ return () => {
+ subscribers[event] = subscribers[event].filter(cb => cb !== callback)
+ }
+ }
+
+ return { dispatch, subscribe }
+}
diff --git a/packages/frontend-core/src/components/grid/lib/renderers.js b/packages/frontend-core/src/components/grid/lib/renderers.js
new file mode 100644
index 0000000000..104984ca4e
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/lib/renderers.js
@@ -0,0 +1,29 @@
+import OptionsCell from "../cells/OptionsCell.svelte"
+import DateCell from "../cells/DateCell.svelte"
+import MultiSelectCell from "../cells/MultiSelectCell.svelte"
+import NumberCell from "../cells/NumberCell.svelte"
+import RelationshipCell from "../cells/RelationshipCell.svelte"
+import TextCell from "../cells/TextCell.svelte"
+import LongFormCell from "../cells/LongFormCell.svelte"
+import BooleanCell from "../cells/BooleanCell.svelte"
+import FormulaCell from "../cells/FormulaCell.svelte"
+import JSONCell from "../cells/JSONCell.svelte"
+import AttachmentCell from "../cells/AttachmentCell.svelte"
+
+const TypeComponentMap = {
+ text: TextCell,
+ options: OptionsCell,
+ datetime: DateCell,
+ barcodeqr: TextCell,
+ longform: LongFormCell,
+ array: MultiSelectCell,
+ number: NumberCell,
+ boolean: BooleanCell,
+ attachment: AttachmentCell,
+ link: RelationshipCell,
+ formula: FormulaCell,
+ json: JSONCell,
+}
+export const getCellRenderer = column => {
+ return TypeComponentMap[column?.schema?.type] || TextCell
+}
diff --git a/packages/frontend-core/src/components/grid/lib/utils.js b/packages/frontend-core/src/components/grid/lib/utils.js
new file mode 100644
index 0000000000..d830d96dba
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/lib/utils.js
@@ -0,0 +1,26 @@
+export const getColor = (idx, opacity = 0.3) => {
+ if (idx == null || idx === -1) {
+ return null
+ }
+ return `hsla(${((idx + 1) * 222) % 360}, 90%, 75%, ${opacity})`
+}
+
+const TypeIconMap = {
+ text: "Text",
+ options: "Dropdown",
+ datetime: "Date",
+ barcodeqr: "Camera",
+ longform: "TextAlignLeft",
+ array: "Dropdown",
+ number: "123",
+ boolean: "Boolean",
+ attachment: "AppleFiles",
+ link: "DataCorrelated",
+ formula: "Calculator",
+ json: "Brackets",
+}
+
+export const getColumnIcon = column => {
+ const type = column.schema.type
+ return TypeIconMap[type] || "Text"
+}
diff --git a/packages/frontend-core/src/components/grid/lib/websocket.js b/packages/frontend-core/src/components/grid/lib/websocket.js
new file mode 100644
index 0000000000..af2e247f98
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/lib/websocket.js
@@ -0,0 +1,63 @@
+import { get } from "svelte/store"
+import { io } from "socket.io-client"
+
+export const createWebsocket = context => {
+ const { rows, tableId, users, userId, focusedCellId } = context
+
+ // Determine connection info
+ const tls = location.protocol === "https:"
+ const proto = tls ? "wss:" : "ws:"
+ const host = location.hostname
+ const port = location.port || (tls ? 443 : 80)
+ const socket = io(`${proto}//${host}:${port}`, {
+ path: "/socket/grid",
+ // Cap reconnection attempts to 3 (total of 15 seconds before giving up)
+ reconnectionAttempts: 3,
+ // Delay reconnection attempt by 5 seconds
+ reconnectionDelay: 5000,
+ reconnectionDelayMax: 5000,
+ // Timeout after 4 seconds so we never stack requests
+ timeout: 4000,
+ })
+
+ const connectToTable = tableId => {
+ if (!socket.connected) {
+ return
+ }
+ // Identify which table we are editing
+ socket.emit("select-table", tableId, response => {
+ // handle initial connection info
+ users.set(response.users)
+ userId.set(response.id)
+ })
+ }
+
+ // Event handlers
+ socket.on("connect", () => {
+ connectToTable(get(tableId))
+ })
+ socket.on("row-update", data => {
+ if (data.id) {
+ rows.actions.refreshRow(data.id)
+ }
+ })
+ socket.on("user-update", user => {
+ users.actions.updateUser(user)
+ })
+ socket.on("user-disconnect", user => {
+ users.actions.removeUser(user)
+ })
+ socket.on("connect_error", err => {
+ console.log("Failed to connect to grid websocket:", err.message)
+ })
+
+ // Change websocket connection when table changes
+ tableId.subscribe(connectToTable)
+
+ // Notify selected cell changes
+ focusedCellId.subscribe($focusedCellId => {
+ socket.emit("select-cell", $focusedCellId)
+ })
+
+ return () => socket?.disconnect()
+}
diff --git a/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte b/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte
new file mode 100644
index 0000000000..e0e842dc16
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte
@@ -0,0 +1,223 @@
+
diff --git a/packages/frontend-core/src/components/grid/overlays/MenuOverlay.svelte b/packages/frontend-core/src/components/grid/overlays/MenuOverlay.svelte
new file mode 100644
index 0000000000..89e4d3503b
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/overlays/MenuOverlay.svelte
@@ -0,0 +1,96 @@
+
+
+{#if $menu.visible}
+
+{/if}
+
+
diff --git a/packages/frontend-core/src/components/grid/overlays/ReorderOverlay.svelte b/packages/frontend-core/src/components/grid/overlays/ReorderOverlay.svelte
new file mode 100644
index 0000000000..f684f2bc9b
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/overlays/ReorderOverlay.svelte
@@ -0,0 +1,63 @@
+
+
+{#if visible}
+
+{/if}
+
+
diff --git a/packages/frontend-core/src/components/grid/overlays/ResizeOverlay.svelte b/packages/frontend-core/src/components/grid/overlays/ResizeOverlay.svelte
new file mode 100644
index 0000000000..42fb796ac9
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/overlays/ResizeOverlay.svelte
@@ -0,0 +1,70 @@
+
+
+{#if !$isReordering}
+ {#if $stickyColumn}
+ resize.actions.startResizing($stickyColumn, e)}
+ on:dblclick={() => resize.actions.resetSize($stickyColumn)}
+ style="left:{GutterWidth + $stickyColumn.width}px;"
+ >
+
+
+ {/if}
+ {#each $renderedColumns as column}
+ resize.actions.startResizing(column, e)}
+ on:dblclick={() => resize.actions.resetSize(column)}
+ style={getStyle(column, offset, $scrollLeft)}
+ >
+
+
+ {/each}
+{/if}
+
+
diff --git a/packages/frontend-core/src/components/grid/overlays/ScrollOverlay.svelte b/packages/frontend-core/src/components/grid/overlays/ScrollOverlay.svelte
new file mode 100644
index 0000000000..f92ae1b26b
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/overlays/ScrollOverlay.svelte
@@ -0,0 +1,121 @@
+
+
+{#if $showVScrollbar}
+
+{/if}
+{#if $showHScrollbar}
+
+{/if}
+
+
diff --git a/packages/frontend-core/src/components/grid/stores/bounds.js b/packages/frontend-core/src/components/grid/stores/bounds.js
new file mode 100644
index 0000000000..c0939f7389
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/stores/bounds.js
@@ -0,0 +1,16 @@
+import { derived, writable } from "svelte/store"
+
+export const createStores = () => {
+ const bounds = writable({
+ left: 0,
+ top: 0,
+ width: 0,
+ height: 0,
+ })
+
+ // Derive height and width as primitives to avoid wasted computation
+ const width = derived(bounds, $bounds => $bounds.width, 0)
+ const height = derived(bounds, $bounds => $bounds.height, 0)
+
+ return { bounds, height, width }
+}
diff --git a/packages/frontend-core/src/components/grid/stores/clipboard.js b/packages/frontend-core/src/components/grid/stores/clipboard.js
new file mode 100644
index 0000000000..cf59dafc54
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/stores/clipboard.js
@@ -0,0 +1,34 @@
+import { writable, get } from "svelte/store"
+
+export const createStores = () => {
+ const copiedCell = writable(null)
+
+ return {
+ copiedCell,
+ }
+}
+
+export const deriveStores = context => {
+ const { copiedCell, focusedCellAPI } = context
+
+ const copy = () => {
+ copiedCell.set(get(focusedCellAPI)?.getValue())
+ }
+
+ const paste = () => {
+ const $copiedCell = get(copiedCell)
+ const $focusedCellAPI = get(focusedCellAPI)
+ if ($copiedCell != null && $focusedCellAPI) {
+ $focusedCellAPI.setValue($copiedCell)
+ }
+ }
+
+ return {
+ clipboard: {
+ actions: {
+ copy,
+ paste,
+ },
+ },
+ }
+}
diff --git a/packages/frontend-core/src/components/grid/stores/columns.js b/packages/frontend-core/src/components/grid/stores/columns.js
new file mode 100644
index 0000000000..e953977487
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/stores/columns.js
@@ -0,0 +1,206 @@
+import { derived, get, writable } from "svelte/store"
+import { cloneDeep } from "lodash/fp"
+import { GutterWidth, DefaultColumnWidth } from "../lib/constants"
+
+export const createStores = () => {
+ const columns = writable([])
+ const stickyColumn = writable(null)
+
+ // Derive an enriched version of columns with left offsets and indexes
+ // automatically calculated
+ const enrichedColumns = derived(
+ columns,
+ $columns => {
+ let offset = 0
+ return $columns.map(column => {
+ const enriched = {
+ ...column,
+ left: offset,
+ }
+ if (column.visible) {
+ offset += column.width
+ }
+ return enriched
+ })
+ },
+ []
+ )
+
+ // Derived list of columns which have not been explicitly hidden
+ const visibleColumns = derived(
+ enrichedColumns,
+ $columns => {
+ return $columns.filter(col => col.visible)
+ },
+ []
+ )
+
+ return {
+ columns: {
+ ...columns,
+ subscribe: enrichedColumns.subscribe,
+ },
+ stickyColumn,
+ visibleColumns,
+ }
+}
+
+export const deriveStores = context => {
+ const { table, columns, stickyColumn, API, dispatch } = context
+
+ // Updates the tables primary display column
+ const changePrimaryDisplay = async column => {
+ return await saveTable({
+ ...get(table),
+ primaryDisplay: column,
+ })
+ }
+
+ // Persists column changes by saving metadata against table schema
+ const saveChanges = async () => {
+ const $columns = get(columns)
+ const $table = get(table)
+ const $stickyColumn = get(stickyColumn)
+ const newSchema = cloneDeep($table.schema)
+
+ // Build new updated table schema
+ Object.keys(newSchema).forEach(column => {
+ // Respect order specified by columns
+ const index = $columns.findIndex(x => x.name === column)
+ if (index !== -1) {
+ newSchema[column].order = index
+ } else {
+ delete newSchema[column].order
+ }
+
+ // Copy over metadata
+ if (column === $stickyColumn?.name) {
+ newSchema[column].visible = true
+ newSchema[column].width = $stickyColumn.width || DefaultColumnWidth
+ } else {
+ newSchema[column].visible = $columns[index]?.visible ?? true
+ newSchema[column].width = $columns[index]?.width || DefaultColumnWidth
+ }
+ })
+
+ await saveTable({ ...$table, schema: newSchema })
+ }
+
+ const saveTable = async newTable => {
+ // Update local state
+ table.set(newTable)
+
+ // Broadcast event so that we can keep sync with external state
+ // (e.g. data section which maintains a list of table definitions)
+ dispatch("updatetable", newTable)
+
+ // Update server
+ await API.saveTable(newTable)
+ }
+
+ return {
+ columns: {
+ ...columns,
+ actions: {
+ saveChanges,
+ saveTable,
+ changePrimaryDisplay,
+ },
+ },
+ }
+}
+
+export const initialise = context => {
+ const { table, columns, stickyColumn, schemaOverrides } = context
+
+ const schema = derived(
+ [table, schemaOverrides],
+ ([$table, $schemaOverrides]) => {
+ let newSchema = $table?.schema
+ if (!newSchema) {
+ return null
+ }
+ Object.keys($schemaOverrides || {}).forEach(field => {
+ if (newSchema[field]) {
+ newSchema[field] = {
+ ...newSchema[field],
+ ...$schemaOverrides[field],
+ }
+ }
+ })
+ return newSchema
+ }
+ )
+
+ // Merge new schema fields with existing schema in order to preserve widths
+ schema.subscribe($schema => {
+ if (!$schema) {
+ columns.set([])
+ stickyColumn.set(null)
+ return
+ }
+ const $table = get(table)
+
+ // Find primary display
+ let primaryDisplay
+ if ($table.primaryDisplay && $schema[$table.primaryDisplay]) {
+ primaryDisplay = $table.primaryDisplay
+ }
+
+ // Get field list
+ let fields = []
+ Object.keys($schema).forEach(field => {
+ if (field !== primaryDisplay) {
+ fields.push(field)
+ }
+ })
+
+ // Update columns, removing extraneous columns and adding missing ones
+ columns.set(
+ fields
+ .map(field => ({
+ name: field,
+ label: $schema[field].name || field,
+ schema: $schema[field],
+ width: $schema[field].width || DefaultColumnWidth,
+ visible: $schema[field].visible ?? true,
+ order: $schema[field].order,
+ }))
+ .sort((a, b) => {
+ // Sort by order first
+ const orderA = a.order
+ const orderB = b.order
+ if (orderA != null && orderB != null) {
+ return orderA < orderB ? -1 : 1
+ } else if (orderA != null) {
+ return -1
+ } else if (orderB != null) {
+ return 1
+ }
+
+ // Then sort by auto columns
+ const autoColA = a.schema?.autocolumn
+ const autoColB = b.schema?.autocolumn
+ if (autoColA === autoColB) {
+ return 0
+ }
+ return autoColA ? 1 : -1
+ })
+ )
+
+ // Update sticky column
+ if (!primaryDisplay) {
+ stickyColumn.set(null)
+ return
+ }
+ stickyColumn.set({
+ name: primaryDisplay,
+ label: $schema[primaryDisplay].name || primaryDisplay,
+ schema: $schema[primaryDisplay],
+ width: $schema[primaryDisplay].width || DefaultColumnWidth,
+ visible: true,
+ order: 0,
+ left: GutterWidth,
+ })
+ })
+}
diff --git a/packages/frontend-core/src/components/grid/stores/index.js b/packages/frontend-core/src/components/grid/stores/index.js
new file mode 100644
index 0000000000..aa552a9ffe
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/stores/index.js
@@ -0,0 +1,48 @@
+import * as Bounds from "./bounds"
+import * as Columns from "./columns"
+import * as Menu from "./menu"
+import * as Pagination from "./pagination"
+import * as Reorder from "./reorder"
+import * as Resize from "./resize"
+import * as Rows from "./rows"
+import * as Scroll from "./scroll"
+import * as UI from "./ui"
+import * as Users from "./users"
+import * as Validation from "./validation"
+import * as Viewport from "./viewport"
+import * as Clipboard from "./clipboard"
+
+const DependencyOrderedStores = [
+ Bounds,
+ Scroll,
+ Rows,
+ Columns,
+ UI,
+ Validation,
+ Resize,
+ Viewport,
+ Reorder,
+ Users,
+ Menu,
+ Pagination,
+ Clipboard,
+]
+
+export const attachStores = context => {
+ // Atomic store creation
+ for (let store of DependencyOrderedStores) {
+ context = { ...context, ...store.createStores?.(context) }
+ }
+
+ // Derived store creation
+ for (let store of DependencyOrderedStores) {
+ context = { ...context, ...store.deriveStores?.(context) }
+ }
+
+ // Initialise any store logic
+ for (let store of DependencyOrderedStores) {
+ store.initialise?.(context)
+ }
+
+ return context
+}
diff --git a/packages/frontend-core/src/components/grid/stores/menu.js b/packages/frontend-core/src/components/grid/stores/menu.js
new file mode 100644
index 0000000000..3f34c0a266
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/stores/menu.js
@@ -0,0 +1,49 @@
+import { writable, get } from "svelte/store"
+import { GutterWidth } from "../lib/constants"
+
+export const createStores = () => {
+ const menu = writable({
+ x: 0,
+ y: 0,
+ visible: false,
+ selectedRow: null,
+ })
+ return {
+ menu,
+ }
+}
+
+export const deriveStores = context => {
+ const { menu, bounds, focusedCellId, stickyColumn, rowHeight } = context
+
+ const open = (cellId, e) => {
+ const $bounds = get(bounds)
+ const $stickyColumn = get(stickyColumn)
+ const $rowHeight = get(rowHeight)
+ e.preventDefault()
+ focusedCellId.set(cellId)
+ menu.set({
+ left:
+ e.clientX - $bounds.left + GutterWidth + ($stickyColumn?.width || 0),
+ top: e.clientY - $bounds.top + $rowHeight,
+ visible: true,
+ })
+ }
+
+ const close = () => {
+ menu.update(state => ({
+ ...state,
+ visible: false,
+ }))
+ }
+
+ return {
+ menu: {
+ ...menu,
+ actions: {
+ open,
+ close,
+ },
+ },
+ }
+}
diff --git a/packages/frontend-core/src/components/grid/stores/pagination.js b/packages/frontend-core/src/components/grid/stores/pagination.js
new file mode 100644
index 0000000000..c6a856e229
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/stores/pagination.js
@@ -0,0 +1,24 @@
+import { derived } from "svelte/store"
+
+export const initialise = context => {
+ const { scrolledRowCount, rows, visualRowCapacity } = context
+
+ // Derive how many rows we have in total
+ const rowCount = derived(rows, $rows => $rows.length, 0)
+
+ // Derive how many rows we have available to scroll
+ const remainingRows = derived(
+ [scrolledRowCount, rowCount, visualRowCapacity],
+ ([$scrolledRowCount, $rowCount, $visualRowCapacity]) => {
+ return Math.max(0, $rowCount - $scrolledRowCount - $visualRowCapacity)
+ },
+ 100
+ )
+
+ // Fetch next page when fewer than 25 remaining rows to scroll
+ remainingRows.subscribe(remaining => {
+ if (remaining < 25) {
+ rows.actions.loadNextPage()
+ }
+ })
+}
diff --git a/packages/frontend-core/src/components/grid/stores/reorder.js b/packages/frontend-core/src/components/grid/stores/reorder.js
new file mode 100644
index 0000000000..de343987db
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/stores/reorder.js
@@ -0,0 +1,155 @@
+import { get, writable, derived } from "svelte/store"
+
+const reorderInitialState = {
+ sourceColumn: null,
+ targetColumn: null,
+ breakpoints: [],
+ initialMouseX: null,
+ scrollLeft: 0,
+ gridLeft: 0,
+}
+
+export const createStores = () => {
+ const reorder = writable(reorderInitialState)
+ const isReordering = derived(
+ reorder,
+ $reorder => !!$reorder.sourceColumn,
+ false
+ )
+ return {
+ reorder,
+ isReordering,
+ }
+}
+
+export const deriveStores = context => {
+ const { reorder, columns, visibleColumns, scroll, bounds, stickyColumn, ui } =
+ context
+
+ // Callback when dragging on a colum header and starting reordering
+ const startReordering = (column, e) => {
+ const $visibleColumns = get(visibleColumns)
+ const $bounds = get(bounds)
+ const $scroll = get(scroll)
+ const $stickyColumn = get(stickyColumn)
+ ui.actions.blur()
+
+ // Generate new breakpoints for the current columns
+ let breakpoints = $visibleColumns.map(col => ({
+ x: col.left + col.width,
+ column: col.name,
+ }))
+ if ($stickyColumn) {
+ breakpoints.unshift({
+ x: 0,
+ column: $stickyColumn.name,
+ })
+ }
+
+ // Update state
+ reorder.set({
+ sourceColumn: column,
+ targetColumn: null,
+ breakpoints,
+ initialMouseX: e.clientX,
+ scrollLeft: $scroll.left,
+ gridLeft: $bounds.left,
+ })
+
+ // Add listeners to handle mouse movement
+ document.addEventListener("mousemove", onReorderMouseMove)
+ document.addEventListener("mouseup", stopReordering)
+
+ // Trigger a move event immediately so ensure a candidate column is chosen
+ onReorderMouseMove(e)
+ }
+
+ // Callback when moving the mouse when reordering columns
+ const onReorderMouseMove = e => {
+ const $reorder = get(reorder)
+
+ // Compute the closest breakpoint to the current position
+ let targetColumn
+ let minDistance = Number.MAX_SAFE_INTEGER
+ const mouseX = e.clientX - $reorder.gridLeft + $reorder.scrollLeft
+ $reorder.breakpoints.forEach(point => {
+ const distance = Math.abs(point.x - mouseX)
+ if (distance < minDistance) {
+ minDistance = distance
+ targetColumn = point.column
+ }
+ })
+
+ if (targetColumn !== $reorder.targetColumn) {
+ reorder.update(state => ({
+ ...state,
+ targetColumn,
+ }))
+ }
+ }
+
+ // Callback when stopping reordering columns
+ const stopReordering = async () => {
+ // Swap position of columns
+ let { sourceColumn, targetColumn } = get(reorder)
+ moveColumn(sourceColumn, targetColumn)
+
+ // Reset state
+ reorder.set(reorderInitialState)
+
+ // Remove event handlers
+ document.removeEventListener("mousemove", onReorderMouseMove)
+ document.removeEventListener("mouseup", stopReordering)
+
+ // Save column changes
+ await columns.actions.saveChanges()
+ }
+
+ // Moves a column after another columns.
+ // An undefined target column will move the source to index 0.
+ const moveColumn = (sourceColumn, targetColumn) => {
+ let $columns = get(columns)
+ let sourceIdx = $columns.findIndex(x => x.name === sourceColumn)
+ let targetIdx = $columns.findIndex(x => x.name === targetColumn)
+ targetIdx++
+ columns.update(state => {
+ const removed = state.splice(sourceIdx, 1)
+ if (--targetIdx < sourceIdx) {
+ targetIdx++
+ }
+ state.splice(targetIdx, 0, removed[0])
+ return state.slice()
+ })
+ }
+
+ // Moves a column one place left (as appears visually)
+ const moveColumnLeft = async column => {
+ const $visibleColumns = get(visibleColumns)
+ const sourceIdx = $visibleColumns.findIndex(x => x.name === column)
+ moveColumn(column, $visibleColumns[sourceIdx - 2]?.name)
+ await columns.actions.saveChanges()
+ }
+
+ // Moves a column one place right (as appears visually)
+ const moveColumnRight = async column => {
+ const $visibleColumns = get(visibleColumns)
+ const sourceIdx = $visibleColumns.findIndex(x => x.name === column)
+ if (sourceIdx === $visibleColumns.length - 1) {
+ return
+ }
+ moveColumn(column, $visibleColumns[sourceIdx + 1]?.name)
+ await columns.actions.saveChanges()
+ }
+
+ return {
+ reorder: {
+ ...reorder,
+ actions: {
+ startReordering,
+ stopReordering,
+ moveColumnLeft,
+ moveColumnRight,
+ },
+ },
+ }
+}
diff --git a/packages/frontend-core/src/components/grid/stores/resize.js b/packages/frontend-core/src/components/grid/stores/resize.js
new file mode 100644
index 0000000000..d007e70c4e
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/stores/resize.js
@@ -0,0 +1,124 @@
+import { writable, get, derived } from "svelte/store"
+import { MinColumnWidth, DefaultColumnWidth } from "../lib/constants"
+
+const initialState = {
+ initialMouseX: null,
+ initialWidth: null,
+ column: null,
+ columnIdx: null,
+ width: 0,
+ left: 0,
+}
+
+export const createStores = () => {
+ const resize = writable(initialState)
+ const isResizing = derived(resize, $resize => $resize.column != null, false)
+ return {
+ resize,
+ isResizing,
+ }
+}
+
+export const deriveStores = context => {
+ const { resize, columns, stickyColumn, ui } = context
+
+ // Starts resizing a certain column
+ const startResizing = (column, e) => {
+ // Prevent propagation to stop reordering triggering
+ e.stopPropagation()
+ ui.actions.blur()
+
+ // Find and cache index
+ let columnIdx = get(columns).findIndex(col => col.name === column.name)
+ if (columnIdx === -1) {
+ columnIdx = "sticky"
+ }
+
+ // Set initial store state
+ resize.set({
+ width: column.width,
+ left: column.left,
+ initialWidth: column.width,
+ initialMouseX: e.clientX,
+ column: column.name,
+ columnIdx,
+ })
+
+ // Add mouse event listeners to handle resizing
+ document.addEventListener("mousemove", onResizeMouseMove)
+ document.addEventListener("mouseup", stopResizing)
+ }
+
+ // Handler for moving the mouse to resize columns
+ const onResizeMouseMove = e => {
+ const { initialMouseX, initialWidth, width, columnIdx } = get(resize)
+ const dx = e.clientX - initialMouseX
+ const newWidth = Math.round(Math.max(MinColumnWidth, initialWidth + dx))
+
+ // Ignore small changes
+ if (Math.abs(width - newWidth) < 5) {
+ return
+ }
+
+ // Update column state
+ if (columnIdx === "sticky") {
+ stickyColumn.update(state => ({
+ ...state,
+ width: newWidth,
+ }))
+ } else {
+ columns.update(state => {
+ state[columnIdx].width = newWidth
+ return [...state]
+ })
+ }
+
+ // Update state
+ resize.update(state => ({
+ ...state,
+ width: newWidth,
+ }))
+ }
+
+ // Stop resizing any columns
+ const stopResizing = async () => {
+ // Reset state
+ const $resize = get(resize)
+ resize.set(initialState)
+ document.removeEventListener("mousemove", onResizeMouseMove)
+ document.removeEventListener("mouseup", stopResizing)
+
+ // Persist width if it changed
+ if ($resize.width !== $resize.initialWidth) {
+ await columns.actions.saveChanges()
+ }
+ }
+
+ // Resets a column size back to default
+ const resetSize = async column => {
+ const $stickyColumn = get(stickyColumn)
+ if (column.name === $stickyColumn?.name) {
+ stickyColumn.update(state => ({
+ ...state,
+ width: DefaultColumnWidth,
+ }))
+ } else {
+ columns.update(state => {
+ const columnIdx = state.findIndex(x => x.name === column.name)
+ state[columnIdx].width = DefaultColumnWidth
+ return [...state]
+ })
+ }
+ await columns.actions.saveChanges()
+ }
+
+ return {
+ resize: {
+ ...resize,
+ actions: {
+ startResizing,
+ resetSize,
+ },
+ },
+ }
+}
diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js
new file mode 100644
index 0000000000..694b66629b
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/stores/rows.js
@@ -0,0 +1,467 @@
+import { writable, derived, get } from "svelte/store"
+import { fetchData } from "../../../fetch/fetchData"
+import { notifications } from "@budibase/bbui"
+import { NewRowID, RowPageSize } from "../lib/constants"
+
+const initialSortState = {
+ column: null,
+ order: "ascending",
+}
+
+export const createStores = () => {
+ const rows = writable([])
+ const table = writable(null)
+ const filter = writable([])
+ const loading = writable(false)
+ const loaded = writable(false)
+ const sort = writable(initialSortState)
+ const rowChangeCache = writable({})
+ const inProgressChanges = writable({})
+ const hasNextPage = writable(false)
+
+ // Generate a lookup map to quick find a row by ID
+ const rowLookupMap = derived(
+ rows,
+ $rows => {
+ let map = {}
+ for (let i = 0; i < $rows.length; i++) {
+ map[$rows[i]._id] = i
+ }
+ return map
+ },
+ {}
+ )
+
+ // Mark loaded as true if we've ever stopped loading
+ let hasStartedLoading = false
+ loading.subscribe($loading => {
+ if ($loading) {
+ hasStartedLoading = true
+ } else if (hasStartedLoading) {
+ loaded.set(true)
+ }
+ })
+
+ return {
+ rows,
+ rowLookupMap,
+ table,
+ filter,
+ loaded,
+ loading,
+ sort,
+ rowChangeCache,
+ inProgressChanges,
+ hasNextPage,
+ }
+}
+
+export const deriveStores = context => {
+ const {
+ rows,
+ rowLookupMap,
+ table,
+ filter,
+ loading,
+ sort,
+ tableId,
+ API,
+ scroll,
+ validation,
+ focusedCellId,
+ columns,
+ rowChangeCache,
+ inProgressChanges,
+ previousFocusedRowId,
+ hasNextPage,
+ } = context
+ const instanceLoaded = writable(false)
+ const fetch = writable(null)
+
+ // Local cache of row IDs to speed up checking if a row exists
+ let rowCacheMap = {}
+
+ // Enrich rows with an index property and any pending changes
+ const enrichedRows = derived(
+ [rows, rowChangeCache],
+ ([$rows, $rowChangeCache]) => {
+ return $rows.map((row, idx) => ({
+ ...row,
+ ...$rowChangeCache[row._id],
+ __idx: idx,
+ }))
+ },
+ []
+ )
+
+ // Reset everything when table ID changes
+ let unsubscribe = null
+ let lastResetKey = null
+ tableId.subscribe($tableId => {
+ // Unsub from previous fetch if one exists
+ unsubscribe?.()
+ fetch.set(null)
+ instanceLoaded.set(false)
+ loading.set(true)
+
+ // Reset state
+ filter.set([])
+
+ // Create new fetch model
+ const newFetch = fetchData({
+ API,
+ datasource: {
+ type: "table",
+ tableId: $tableId,
+ },
+ options: {
+ filter: [],
+ sortColumn: initialSortState.column,
+ sortOrder: initialSortState.order,
+ limit: RowPageSize,
+ paginate: true,
+ },
+ })
+
+ // Subscribe to changes of this fetch model
+ unsubscribe = newFetch.subscribe($fetch => {
+ if ($fetch.loaded && !$fetch.loading) {
+ hasNextPage.set($fetch.hasNextPage)
+ const $instanceLoaded = get(instanceLoaded)
+ const resetRows = $fetch.resetKey !== lastResetKey
+ lastResetKey = $fetch.resetKey
+
+ // Reset state properties when dataset changes
+ if (!$instanceLoaded || resetRows) {
+ table.set($fetch.definition)
+ sort.set({
+ column: $fetch.sortColumn,
+ order: $fetch.sortOrder,
+ })
+ }
+
+ // Reset scroll state when data changes
+ if (!$instanceLoaded) {
+ // Reset both top and left for a new table ID
+ instanceLoaded.set(true)
+ scroll.set({ top: 0, left: 0 })
+ } else if (resetRows) {
+ // Only reset top scroll position when resetting rows
+ scroll.update(state => ({ ...state, top: 0 }))
+ }
+
+ // Process new rows
+ handleNewRows($fetch.rows, resetRows)
+
+ // Notify that we're loaded
+ loading.set(false)
+ }
+ })
+
+ fetch.set(newFetch)
+ })
+
+ // Update fetch when filter or sort config changes
+ filter.subscribe($filter => {
+ get(fetch)?.update({
+ filter: $filter,
+ })
+ })
+ sort.subscribe($sort => {
+ get(fetch)?.update({
+ sortOrder: $sort.order,
+ sortColumn: $sort.column,
+ })
+ })
+
+ // Gets a row by ID
+ const getRow = id => {
+ const index = get(rowLookupMap)[id]
+ return index >= 0 ? get(rows)[index] : null
+ }
+
+ // Handles validation errors from the rows API and updates local validation
+ // state, storing error messages against relevant cells
+ const handleValidationError = (rowId, error) => {
+ if (error?.json?.validationErrors) {
+ // Normal validation error
+ const keys = Object.keys(error.json.validationErrors)
+ const $columns = get(columns)
+ for (let column of keys) {
+ validation.actions.setError(
+ `${rowId}-${column}`,
+ `${column} ${error.json.validationErrors[column]}`
+ )
+
+ // Ensure the column is visible
+ const index = $columns.findIndex(x => x.name === column)
+ if (index !== -1 && !$columns[index].visible) {
+ columns.update(state => {
+ state[index].visible = true
+ return state.slice()
+ })
+ }
+ }
+ // Focus the first cell with an error
+ focusedCellId.set(`${rowId}-${keys[0]}`)
+ } else {
+ // Some other error - just update the current cell
+ validation.actions.setError(get(focusedCellId), error?.message || "Error")
+ }
+ }
+
+ // Adds a new row
+ const addRow = async (row, idx, bubble = false) => {
+ try {
+ // Create row
+ let newRow = await API.saveRow({ ...row, tableId: get(tableId) })
+ newRow = await fetchRow(newRow._id)
+
+ // Update state
+ if (idx != null) {
+ rowCacheMap[newRow._id] = true
+ rows.update(state => {
+ state.splice(idx, 0, newRow)
+ return state.slice()
+ })
+ } else {
+ handleNewRows([newRow])
+ }
+
+ // Refresh row to ensure data is in the correct format
+ notifications.success("Row created successfully")
+ return newRow
+ } catch (error) {
+ if (bubble) {
+ throw error
+ } else {
+ handleValidationError(NewRowID, error)
+ }
+ }
+ }
+
+ // Duplicates a row, inserting the duplicate row after the existing one
+ const duplicateRow = async row => {
+ let clone = { ...row }
+ delete clone._id
+ delete clone._rev
+ delete clone.__idx
+ try {
+ return await addRow(clone, row.__idx + 1, true)
+ } catch (error) {
+ handleValidationError(row._id, error)
+ }
+ }
+
+ // Fetches a row by ID using the search endpoint
+ const fetchRow = async id => {
+ const res = await API.searchTable({
+ tableId: get(tableId),
+ limit: 1,
+ query: {
+ equal: {
+ _id: id,
+ },
+ },
+ paginate: false,
+ })
+ return res?.rows?.[0]
+ }
+
+ // Refreshes a specific row, handling updates, addition or deletion
+ const refreshRow = async id => {
+ // Fetch row from the server again
+ const newRow = await fetchRow(id)
+
+ // Get index of row to check if it exists
+ const $rows = get(rows)
+ const $rowLookupMap = get(rowLookupMap)
+ const index = $rowLookupMap[id]
+
+ // Process as either an update, addition or deletion
+ if (newRow) {
+ if (index != null) {
+ // An existing row was updated
+ rows.update(state => {
+ state[index] = { ...newRow }
+ return state
+ })
+ } else {
+ // A new row was created
+ handleNewRows([newRow])
+ }
+ } else if (index != null) {
+ // A row was removed
+ handleRemoveRows([$rows[index]])
+ }
+ }
+
+ // Refreshes all data
+ const refreshData = () => {
+ get(fetch)?.getInitialData()
+ }
+
+ // Patches a row with some changes
+ const updateRow = async (rowId, changes) => {
+ const $rows = get(rows)
+ const $rowLookupMap = get(rowLookupMap)
+ const index = $rowLookupMap[rowId]
+ const row = $rows[index]
+ if (index == null || !Object.keys(changes || {}).length) {
+ return
+ }
+
+ // Abandon if no changes
+ let same = true
+ for (let column of Object.keys(changes)) {
+ if (row[column] !== changes[column]) {
+ same = false
+ break
+ }
+ }
+ if (same) {
+ return
+ }
+
+ // Immediately update state so that the change is reflected
+ rowChangeCache.update(state => ({
+ ...state,
+ [rowId]: {
+ ...state[rowId],
+ ...changes,
+ },
+ }))
+
+ // Save change
+ try {
+ inProgressChanges.update(state => ({
+ ...state,
+ [rowId]: true,
+ }))
+ const newRow = { ...row, ...get(rowChangeCache)[rowId] }
+ const saved = await API.saveRow(newRow)
+
+ // Update state after a successful change
+ rows.update(state => {
+ state[index] = {
+ ...newRow,
+ _rev: saved._rev,
+ }
+ return state.slice()
+ })
+ rowChangeCache.update(state => {
+ delete state[rowId]
+ return state
+ })
+ } catch (error) {
+ handleValidationError(rowId, error)
+ }
+ inProgressChanges.update(state => ({
+ ...state,
+ [rowId]: false,
+ }))
+ }
+
+ // Updates a value of a row
+ const updateValue = async (rowId, column, value) => {
+ return await updateRow(rowId, { [column]: value })
+ }
+
+ // Deletes an array of rows
+ const deleteRows = async rowsToDelete => {
+ if (!rowsToDelete?.length) {
+ return
+ }
+
+ // Actually delete rows
+ rowsToDelete.forEach(row => {
+ delete row.__idx
+ })
+ await API.deleteRows({
+ tableId: get(tableId),
+ rows: rowsToDelete,
+ })
+
+ // Update state
+ handleRemoveRows(rowsToDelete)
+ }
+
+ // Local handler to process new rows inside the fetch, and append any new
+ // rows to state that we haven't encountered before
+ const handleNewRows = (newRows, resetRows) => {
+ if (resetRows) {
+ rowCacheMap = {}
+ }
+ let rowsToAppend = []
+ let newRow
+ for (let i = 0; i < newRows.length; i++) {
+ newRow = newRows[i]
+ if (!rowCacheMap[newRow._id]) {
+ rowCacheMap[newRow._id] = true
+ rowsToAppend.push(newRow)
+ }
+ }
+ if (resetRows) {
+ rows.set(rowsToAppend)
+ } else if (rowsToAppend.length) {
+ rows.update(state => [...state, ...rowsToAppend])
+ }
+ }
+
+ // Local handler to remove rows from state
+ const handleRemoveRows = rowsToRemove => {
+ const deletedIds = rowsToRemove.map(row => row._id)
+
+ // We deliberately do not remove IDs from the cache map as the data may
+ // still exist inside the fetch, but we don't want to add it again
+ rows.update(state => {
+ return state.filter(row => !deletedIds.includes(row._id))
+ })
+ }
+
+ // Loads the next page of data if available
+ const loadNextPage = () => {
+ get(fetch)?.nextPage()
+ }
+
+ // Refreshes the schema of the data fetch subscription
+ const refreshTableDefinition = async () => {
+ const definition = await API.fetchTableDefinition(get(tableId))
+ table.set(definition)
+ }
+
+ // Checks if we have a row with a certain ID
+ const hasRow = id => {
+ return get(rowLookupMap)[id] != null
+ }
+
+ // Wipe the row change cache when changing row
+ previousFocusedRowId.subscribe(id => {
+ if (id && !get(inProgressChanges)[id]) {
+ rowChangeCache.update(state => {
+ delete state[id]
+ return state
+ })
+ }
+ })
+
+ return {
+ enrichedRows,
+ rows: {
+ ...rows,
+ actions: {
+ addRow,
+ duplicateRow,
+ getRow,
+ updateValue,
+ updateRow,
+ deleteRows,
+ hasRow,
+ loadNextPage,
+ refreshRow,
+ refreshData,
+ refreshTableDefinition,
+ },
+ },
+ }
+}
diff --git a/packages/frontend-core/src/components/grid/stores/scroll.js b/packages/frontend-core/src/components/grid/stores/scroll.js
new file mode 100644
index 0000000000..c1ca19633a
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/stores/scroll.js
@@ -0,0 +1,201 @@
+import { writable, derived, get } from "svelte/store"
+import { tick } from "svelte"
+import { Padding, GutterWidth } from "../lib/constants"
+
+export const createStores = () => {
+ const scroll = writable({
+ left: 0,
+ top: 0,
+ })
+
+ // Derive height and width as primitives to avoid wasted computation
+ const scrollTop = derived(scroll, $scroll => $scroll.top, 0)
+ const scrollLeft = derived(scroll, $scroll => $scroll.left, 0)
+
+ return {
+ scroll,
+ scrollTop,
+ scrollLeft,
+ }
+}
+
+export const deriveStores = context => {
+ const { rows, visibleColumns, stickyColumn, rowHeight, width, height } =
+ context
+
+ // Memoize store primitives
+ const stickyColumnWidth = derived(stickyColumn, $col => $col?.width || 0, 0)
+
+ // Derive vertical limits
+ const contentHeight = derived(
+ [rows, rowHeight],
+ ([$rows, $rowHeight]) => ($rows.length + 1) * $rowHeight + Padding,
+ 0
+ )
+ const maxScrollTop = derived(
+ [height, contentHeight],
+ ([$height, $contentHeight]) => Math.max($contentHeight - $height, 0),
+ 0
+ )
+
+ // Derive horizontal limits
+ const contentWidth = derived(
+ [visibleColumns, stickyColumnWidth],
+ ([$visibleColumns, $stickyColumnWidth]) => {
+ let width = GutterWidth + Padding + $stickyColumnWidth
+ $visibleColumns.forEach(col => {
+ width += col.width
+ })
+ return width
+ },
+ 0
+ )
+ const screenWidth = derived(
+ [width, stickyColumnWidth],
+ ([$width, $stickyColumnWidth]) => $width + GutterWidth + $stickyColumnWidth,
+ 0
+ )
+ const maxScrollLeft = derived(
+ [contentWidth, screenWidth],
+ ([$contentWidth, $screenWidth]) => {
+ return Math.max($contentWidth - $screenWidth, 0)
+ },
+ 0
+ )
+
+ // Derive whether to show scrollbars or not
+ const showVScrollbar = derived(
+ [contentHeight, height],
+ ([$contentHeight, $height]) => {
+ return $contentHeight > $height
+ }
+ )
+ const showHScrollbar = derived(
+ [contentWidth, screenWidth],
+ ([$contentWidth, $screenWidth]) => {
+ return $contentWidth > $screenWidth
+ }
+ )
+
+ return {
+ contentHeight,
+ contentWidth,
+ screenWidth,
+ maxScrollTop,
+ maxScrollLeft,
+ showHScrollbar,
+ showVScrollbar,
+ }
+}
+
+export const initialise = context => {
+ const {
+ focusedCellId,
+ focusedRow,
+ scroll,
+ bounds,
+ rowHeight,
+ visibleColumns,
+ scrollTop,
+ maxScrollTop,
+ scrollLeft,
+ maxScrollLeft,
+ } = context
+
+ // Ensure scroll state never goes invalid, which can happen when changing
+ // rows or tables
+ const overscrollTop = derived(
+ [scrollTop, maxScrollTop],
+ ([$scrollTop, $maxScrollTop]) => $scrollTop > $maxScrollTop,
+ false
+ )
+ const overscrollLeft = derived(
+ [scrollLeft, maxScrollLeft],
+ ([$scrollLeft, $maxScrollLeft]) => $scrollLeft > $maxScrollLeft,
+ false
+ )
+ overscrollTop.subscribe(overscroll => {
+ if (overscroll) {
+ scroll.update(state => ({
+ ...state,
+ top: get(maxScrollTop),
+ }))
+ }
+ })
+ overscrollLeft.subscribe(overscroll => {
+ if (overscroll) {
+ scroll.update(state => ({
+ ...state,
+ left: get(maxScrollLeft),
+ }))
+ }
+ })
+
+ // Ensure the selected cell is visible
+ focusedCellId.subscribe(async $focusedCellId => {
+ await tick()
+ const $focusedRow = get(focusedRow)
+ const $scroll = get(scroll)
+ const $bounds = get(bounds)
+ const $rowHeight = get(rowHeight)
+ const verticalOffset = 60
+
+ // Ensure vertical position is viewable
+ if ($focusedRow) {
+ // Ensure row is not below bottom of screen
+ const rowYPos = $focusedRow.__idx * $rowHeight
+ const bottomCutoff =
+ $scroll.top + $bounds.height - $rowHeight - verticalOffset
+ let delta = rowYPos - bottomCutoff
+ if (delta > 0) {
+ scroll.update(state => ({
+ ...state,
+ top: state.top + delta,
+ }))
+ }
+
+ // Ensure row is not above top of screen
+ else {
+ const delta = $scroll.top - rowYPos + verticalOffset
+ if (delta > 0) {
+ scroll.update(state => ({
+ ...state,
+ top: Math.max(0, state.top - delta),
+ }))
+ }
+ }
+ }
+
+ // Ensure horizontal position is viewable
+ // Check horizontal position of columns next
+ const $visibleColumns = get(visibleColumns)
+ const columnName = $focusedCellId?.split("-")[1]
+ const column = $visibleColumns.find(col => col.name === columnName)
+ const horizontalOffset = 50
+ if (!column) {
+ return
+ }
+
+ // Ensure column is not cutoff on left edge
+ let delta = $scroll.left - column.left + horizontalOffset
+ if (delta > 0) {
+ scroll.update(state => ({
+ ...state,
+ left: Math.max(0, state.left - delta),
+ }))
+ }
+
+ // Ensure column is not cutoff on right edge
+ else {
+ const rightEdge = column.left + column.width
+ const rightBound = $bounds.width + $scroll.left - horizontalOffset
+ delta = rightEdge - rightBound
+ if (delta > 0) {
+ scroll.update(state => ({
+ ...state,
+ left: state.left + delta,
+ }))
+ }
+ }
+ })
+}
diff --git a/packages/frontend-core/src/components/grid/stores/ui.js b/packages/frontend-core/src/components/grid/stores/ui.js
new file mode 100644
index 0000000000..0636400d59
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/stores/ui.js
@@ -0,0 +1,159 @@
+import { writable, get, derived } from "svelte/store"
+import { tick } from "svelte"
+import {
+ DefaultRowHeight,
+ LargeRowHeight,
+ MediumRowHeight,
+ NewRowID,
+} from "../lib/constants"
+
+export const createStores = () => {
+ const focusedCellId = writable(null)
+ const focusedCellAPI = writable(null)
+ const selectedRows = writable({})
+ const hoveredRowId = writable(null)
+ const rowHeight = writable(DefaultRowHeight)
+ const previousFocusedRowId = writable(null)
+
+ // Derive the current focused row ID
+ const focusedRowId = derived(
+ focusedCellId,
+ $focusedCellId => {
+ return $focusedCellId?.split("-")[0]
+ },
+ null
+ )
+
+ return {
+ focusedCellId,
+ focusedCellAPI,
+ focusedRowId,
+ previousFocusedRowId,
+ selectedRows,
+ hoveredRowId,
+ rowHeight,
+ }
+}
+
+export const deriveStores = context => {
+ const {
+ focusedCellId,
+ selectedRows,
+ hoveredRowId,
+ enrichedRows,
+ rowLookupMap,
+ rowHeight,
+ } = context
+
+ // Derive the row that contains the selected cell
+ const focusedRow = derived(
+ [focusedCellId, rowLookupMap, enrichedRows],
+ ([$focusedCellId, $rowLookupMap, $enrichedRows]) => {
+ const rowId = $focusedCellId?.split("-")[0]
+
+ // Edge case for new rows
+ if (rowId === NewRowID) {
+ return { _id: NewRowID }
+ }
+
+ // All normal rows
+ const index = $rowLookupMap[rowId]
+ return $enrichedRows[index]
+ },
+ null
+ )
+
+ // Callback when leaving the grid, deselecting all focussed or selected items
+ const blur = () => {
+ focusedCellId.set(null)
+ selectedRows.set({})
+ hoveredRowId.set(null)
+ }
+
+ const contentLines = derived(rowHeight, $rowHeight => {
+ if ($rowHeight === LargeRowHeight) {
+ return 3
+ } else if ($rowHeight === MediumRowHeight) {
+ return 2
+ }
+ return 1
+ })
+
+ return {
+ focusedRow,
+ contentLines,
+ ui: {
+ actions: {
+ blur,
+ },
+ },
+ }
+}
+
+export const initialise = context => {
+ const {
+ focusedRowId,
+ previousFocusedRowId,
+ rows,
+ focusedCellId,
+ selectedRows,
+ hoveredRowId,
+ table,
+ rowHeight,
+ } = context
+
+ // Ensure we clear invalid rows from state if they disappear
+ rows.subscribe(async () => {
+ // We tick here to ensure other derived stores have properly updated.
+ // We depend on the row lookup map which is a derived store,
+ await tick()
+ const $focusedCellId = get(focusedCellId)
+ const $selectedRows = get(selectedRows)
+ const $hoveredRowId = get(hoveredRowId)
+ const hasRow = rows.actions.hasRow
+
+ // Check selected cell
+ const selectedRowId = $focusedCellId?.split("-")[0]
+ if (selectedRowId && !hasRow(selectedRowId)) {
+ focusedCellId.set(null)
+ }
+
+ // Check hovered row
+ if ($hoveredRowId && !hasRow($hoveredRowId)) {
+ hoveredRowId.set(null)
+ }
+
+ // Check selected rows
+ let newSelectedRows = { ...$selectedRows }
+ let selectedRowsNeedsUpdate = false
+ const selectedIds = Object.keys($selectedRows)
+ for (let i = 0; i < selectedIds.length; i++) {
+ if (!hasRow(selectedIds[i])) {
+ delete newSelectedRows[selectedIds[i]]
+ selectedRowsNeedsUpdate = true
+ }
+ }
+ if (selectedRowsNeedsUpdate) {
+ selectedRows.set(newSelectedRows)
+ }
+ })
+
+ // Remember the last focused row ID so that we can store the previous one
+ let lastFocusedRowId = null
+ focusedRowId.subscribe(id => {
+ previousFocusedRowId.set(lastFocusedRowId)
+ lastFocusedRowId = id
+ })
+
+ // Remove hovered row when a cell is selected
+ focusedCellId.subscribe(cell => {
+ if (cell && get(hoveredRowId)) {
+ hoveredRowId.set(null)
+ }
+ })
+
+ // Pull row height from table
+ table.subscribe($table => {
+ rowHeight.set($table?.rowHeight || DefaultRowHeight)
+ })
+}
diff --git a/packages/frontend-core/src/components/grid/stores/users.js b/packages/frontend-core/src/components/grid/stores/users.js
new file mode 100644
index 0000000000..3a6ec5fb21
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/stores/users.js
@@ -0,0 +1,115 @@
+import { writable, get, derived } from "svelte/store"
+
+export const createStores = () => {
+ const users = writable([])
+ const userId = writable(null)
+
+ // Enrich users with unique colours
+ const enrichedUsers = derived(
+ [users, userId],
+ ([$users, $userId]) => {
+ return (
+ $users
+ .slice()
+ // Place current user first
+ .sort((a, b) => {
+ if (a.id === $userId) {
+ return -1
+ } else if (b.id === $userId) {
+ return 1
+ } else {
+ return 0
+ }
+ })
+ // Enrich users with colors
+ .map((user, idx) => {
+ // Generate random colour hue
+ let hue = 1
+ for (let i = 0; i < user.email.length && i < 5; i++) {
+ hue *= user.email.charCodeAt(i + 1)
+ hue /= 17
+ }
+ hue = hue % 360
+ const color =
+ idx === 0
+ ? "var(--spectrum-global-color-blue-400)"
+ : `hsl(${hue}, 50%, 40%)`
+
+ // Generate friendly label
+ let label = user.email
+ if (user.firstName) {
+ label = user.firstName
+ if (user.lastName) {
+ label += ` ${user.lastName}`
+ }
+ }
+
+ return {
+ ...user,
+ color,
+ label,
+ }
+ })
+ )
+ },
+ []
+ )
+
+ return {
+ users: {
+ ...users,
+ subscribe: enrichedUsers.subscribe,
+ },
+ userId,
+ }
+}
+
+export const deriveStores = context => {
+ const { users, userId } = context
+
+ // Generate a lookup map of cell ID to the user that has it selected, to make
+ // lookups inside cells extremely fast
+ const selectedCellMap = derived(
+ [users, userId],
+ ([$enrichedUsers, $userId]) => {
+ let map = {}
+ $enrichedUsers.forEach(user => {
+ if (user.focusedCellId && user.id !== $userId) {
+ map[user.focusedCellId] = user
+ }
+ })
+ return map
+ },
+ {}
+ )
+
+ const updateUser = user => {
+ const $users = get(users)
+ const index = $users.findIndex(x => x.id === user.id)
+ if (index === -1) {
+ users.set([...$users, user])
+ } else {
+ users.update(state => {
+ state[index] = user
+ return state.slice()
+ })
+ }
+ }
+
+ const removeUser = user => {
+ users.update(state => {
+ return state.filter(x => x.id !== user.id)
+ })
+ }
+
+ return {
+ users: {
+ ...users,
+ actions: {
+ updateUser,
+ removeUser,
+ },
+ },
+ selectedCellMap,
+ }
+}
diff --git a/packages/frontend-core/src/components/grid/stores/validation.js b/packages/frontend-core/src/components/grid/stores/validation.js
new file mode 100644
index 0000000000..9c3927f9c9
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/stores/validation.js
@@ -0,0 +1,45 @@
+import { writable, get } from "svelte/store"
+
+export const createStores = () => {
+ const validation = writable({})
+
+ const setError = (cellId, error) => {
+ if (!cellId) {
+ return
+ }
+ validation.update(state => ({
+ ...state,
+ [cellId]: error,
+ }))
+ }
+
+ return {
+ validation: {
+ ...validation,
+ actions: {
+ setError,
+ },
+ },
+ }
+}
+
+export const initialise = context => {
+ const { validation, previousFocusedRowId, columns, stickyColumn } = context
+
+ // Remove validation errors from previous focused row
+ previousFocusedRowId.subscribe(id => {
+ if (id) {
+ const $columns = get(columns)
+ const $stickyColumn = get(stickyColumn)
+ validation.update(state => {
+ $columns.forEach(column => {
+ state[`${id}-${column.name}`] = null
+ })
+ if ($stickyColumn) {
+ state[`${id}-${$stickyColumn.name}`] = null
+ }
+ return state
+ })
+ }
+ })
+}
diff --git a/packages/frontend-core/src/components/grid/stores/viewport.js b/packages/frontend-core/src/components/grid/stores/viewport.js
new file mode 100644
index 0000000000..bda56dc34f
--- /dev/null
+++ b/packages/frontend-core/src/components/grid/stores/viewport.js
@@ -0,0 +1,145 @@
+import { derived, get } from "svelte/store"
+import {
+ MaxCellRenderHeight,
+ MaxCellRenderWidthOverflow,
+ MinColumnWidth,
+ ScrollBarSize,
+} from "../lib/constants"
+
+export const deriveStores = context => {
+ const {
+ rowHeight,
+ visibleColumns,
+ enrichedRows,
+ scrollTop,
+ scrollLeft,
+ width,
+ height,
+ } = context
+
+ // Derive visible rows
+ // Split into multiple stores containing primitives to optimise invalidation
+ // as much as possible
+ const scrolledRowCount = derived(
+ [scrollTop, rowHeight],
+ ([$scrollTop, $rowHeight]) => {
+ return Math.floor($scrollTop / $rowHeight)
+ },
+ 0
+ )
+ const visualRowCapacity = derived(
+ [height, rowHeight],
+ ([$height, $rowHeight]) => {
+ return Math.ceil($height / $rowHeight) + 1
+ },
+ 0
+ )
+ const renderedRows = derived(
+ [enrichedRows, scrolledRowCount, visualRowCapacity],
+ ([$enrichedRows, $scrolledRowCount, $visualRowCapacity]) => {
+ return $enrichedRows.slice(
+ $scrolledRowCount,
+ $scrolledRowCount + $visualRowCapacity
+ )
+ },
+ []
+ )
+
+ // Derive visible columns
+ const scrollLeftRounded = derived(scrollLeft, $scrollLeft => {
+ const interval = MinColumnWidth
+ return Math.round($scrollLeft / interval) * interval
+ })
+ const renderedColumns = derived(
+ [visibleColumns, scrollLeftRounded, width],
+ ([$visibleColumns, $scrollLeft, $width], set) => {
+ if (!$visibleColumns.length) {
+ set([])
+ return
+ }
+ let startColIdx = 0
+ let rightEdge = $visibleColumns[0].width
+ while (
+ rightEdge < $scrollLeft &&
+ startColIdx < $visibleColumns.length - 1
+ ) {
+ startColIdx++
+ rightEdge += $visibleColumns[startColIdx].width
+ }
+ let endColIdx = startColIdx + 1
+ let leftEdge = rightEdge
+ while (
+ leftEdge < $width + $scrollLeft &&
+ endColIdx < $visibleColumns.length
+ ) {
+ leftEdge += $visibleColumns[endColIdx].width
+ endColIdx++
+ }
+ // Render an additional column on either side to account for
+ // debounce column updates based on scroll position
+ const next = $visibleColumns.slice(
+ Math.max(0, startColIdx - 1),
+ endColIdx + 1
+ )
+ const current = get(renderedColumns)
+ if (JSON.stringify(next) !== JSON.stringify(current)) {
+ set(next)
+ }
+ }
+ )
+
+ const hiddenColumnsWidth = derived(
+ [renderedColumns, visibleColumns],
+ ([$renderedColumns, $visibleColumns]) => {
+ const idx = $visibleColumns.findIndex(
+ col => col.name === $renderedColumns[0]?.name
+ )
+ let width = 0
+ if (idx > 0) {
+ for (let i = 0; i < idx; i++) {
+ width += $visibleColumns[i].width
+ }
+ }
+ return width
+ },
+ 0
+ )
+
+ // Determine the row index at which we should start vertically inverting cell
+ // dropdowns
+ const rowVerticalInversionIndex = derived(
+ [visualRowCapacity, rowHeight],
+ ([$visualRowCapacity, $rowHeight]) => {
+ return (
+ $visualRowCapacity - Math.ceil(MaxCellRenderHeight / $rowHeight) - 2
+ )
+ }
+ )
+
+ // Determine the column index at which we should start horizontally inverting
+ // cell dropdowns
+ const columnHorizontalInversionIndex = derived(
+ [renderedColumns, scrollLeft, width],
+ ([$renderedColumns, $scrollLeft, $width]) => {
+ const cutoff = $width + $scrollLeft - ScrollBarSize * 3
+ let inversionIdx = $renderedColumns.length
+ for (let i = $renderedColumns.length - 1; i >= 0; i--, inversionIdx--) {
+ const rightEdge = $renderedColumns[i].left + $renderedColumns[i].width
+ if (rightEdge + MaxCellRenderWidthOverflow < cutoff) {
+ break
+ }
+ }
+ return inversionIdx
+ }
+ )
+
+ return {
+ scrolledRowCount,
+ visualRowCapacity,
+ renderedRows,
+ renderedColumns,
+ hiddenColumnsWidth,
+ rowVerticalInversionIndex,
+ columnHorizontalInversionIndex,
+ }
+}
diff --git a/packages/frontend-core/src/components/index.js b/packages/frontend-core/src/components/index.js
index 7ca21c4ff9..2b7033ad8f 100644
--- a/packages/frontend-core/src/components/index.js
+++ b/packages/frontend-core/src/components/index.js
@@ -1,2 +1,3 @@
export { default as SplitPage } from "./SplitPage.svelte"
export { default as TestimonialPage } from "./TestimonialPage.svelte"
+export { Grid } from "./grid"
diff --git a/packages/frontend-core/src/fetch/DataFetch.js b/packages/frontend-core/src/fetch/DataFetch.js
index 9b687e9844..f68b37dcca 100644
--- a/packages/frontend-core/src/fetch/DataFetch.js
+++ b/packages/frontend-core/src/fetch/DataFetch.js
@@ -21,11 +21,11 @@ export default class DataFetch {
this.API = null
// Feature flags
- this.featureStore = writable({
+ this.features = {
supportsSearch: false,
supportsSort: false,
supportsPagination: false,
- })
+ }
// Config
this.options = {
@@ -56,6 +56,7 @@ export default class DataFetch {
pageNumber: 0,
cursor: null,
cursors: [],
+ resetKey: Math.random(),
})
// Merge options with their default values
@@ -81,17 +82,14 @@ export default class DataFetch {
this.prevPage = this.prevPage.bind(this)
// Derive certain properties to return
- this.derivedStore = derived(
- [this.store, this.featureStore],
- ([$store, $featureStore]) => {
- return {
- ...$store,
- ...$featureStore,
- hasNextPage: this.hasNextPage($store),
- hasPrevPage: this.hasPrevPage($store),
- }
+ this.derivedStore = derived(this.store, $store => {
+ return {
+ ...$store,
+ ...this.features,
+ hasNextPage: this.hasNextPage($store),
+ hasPrevPage: this.hasPrevPage($store),
}
- )
+ })
// Mark as loaded if we have no datasource
if (!this.options.datasource) {
@@ -120,11 +118,11 @@ export default class DataFetch {
// Fetch datasource definition and determine feature flags
const definition = await this.getDefinition(datasource)
const features = this.determineFeatureFlags(definition)
- this.featureStore.set({
+ this.features = {
supportsSearch: !!features?.supportsSearch,
supportsSort: !!features?.supportsSort,
supportsPagination: paginate && !!features?.supportsPagination,
- })
+ }
// Fetch and enrich schema
let schema = this.getSchema(datasource, definition)
@@ -138,11 +136,17 @@ export default class DataFetch {
this.options.sortOrder = "ascending"
}
- // If no sort column, use the first field in the schema
+ // If no sort column, use the primary display and fallback to first column
if (!this.options.sortColumn) {
- this.options.sortColumn = Object.keys(schema)[0]
+ let newSortColumn
+ if (definition?.primaryDisplay && schema[definition.primaryDisplay]) {
+ newSortColumn = definition.primaryDisplay
+ } else {
+ newSortColumn = Object.keys(schema)[0]
+ }
+ this.options.sortColumn = newSortColumn
}
- const { sortColumn } = this.options
+ const { sortOrder, sortColumn } = this.options
// Determine what sort type to use
let sortType = "string"
@@ -167,6 +171,8 @@ export default class DataFetch {
loading: true,
cursors: [],
cursor: null,
+ sortOrder,
+ sortColumn,
}))
// Actually fetch data
@@ -180,6 +186,7 @@ export default class DataFetch {
info: page.info,
cursors: paginate && page.hasNextPage ? [null, page.cursor] : [null],
error: page.error,
+ resetKey: Math.random(),
}))
}
@@ -189,23 +196,22 @@ export default class DataFetch {
async getPage() {
const { sortColumn, sortOrder, sortType, limit } = this.options
const { query } = get(this.store)
- const features = get(this.featureStore)
// Get the actual data
let { rows, info, hasNextPage, cursor, error } = await this.getData()
// If we don't support searching, do a client search
- if (!features.supportsSearch) {
+ if (!this.features.supportsSearch) {
rows = runLuceneQuery(rows, query)
}
// If we don't support sorting, do a client-side sort
- if (!features.supportsSort) {
+ if (!this.features.supportsSort) {
rows = luceneSort(rows, sortColumn, sortOrder, sortType)
}
// If we don't support pagination, do a client-side limit
- if (!features.supportsPagination) {
+ if (!this.features.supportsPagination) {
rows = luceneLimit(rows, limit)
}
diff --git a/packages/frontend-core/src/fetch/QueryFetch.js b/packages/frontend-core/src/fetch/QueryFetch.js
index 65f0a4f130..456abaec79 100644
--- a/packages/frontend-core/src/fetch/QueryFetch.js
+++ b/packages/frontend-core/src/fetch/QueryFetch.js
@@ -31,7 +31,7 @@ export default class QueryFetch extends DataFetch {
async getData() {
const { datasource, limit, paginate } = this.options
- const { supportsPagination } = get(this.featureStore)
+ const { supportsPagination } = this.features
const { cursor, definition } = get(this.store)
const type = definition?.fields?.pagination?.type
diff --git a/packages/frontend-core/src/themes/midnight.css b/packages/frontend-core/src/themes/midnight.css
index f3763819d4..e311452262 100644
--- a/packages/frontend-core/src/themes/midnight.css
+++ b/packages/frontend-core/src/themes/midnight.css
@@ -13,6 +13,9 @@
--spectrum-global-color-gray-800: hsl(var(--hue), var(--sat), 85%);
--spectrum-global-color-gray-900: hsl(var(--hue), var(--sat), 95%);
+ /* Custom additions */
--modal-background: var(--spectrum-global-color-gray-50);
+ --drop-shadow: rgba(0, 0, 0, 0.25) !important;
+ --spectrum-global-color-blue-100: rgba(35, 40, 50) !important;
}
diff --git a/packages/frontend-core/src/themes/nord.css b/packages/frontend-core/src/themes/nord.css
index 11c7a3aea1..d47dbe8aa8 100644
--- a/packages/frontend-core/src/themes/nord.css
+++ b/packages/frontend-core/src/themes/nord.css
@@ -46,5 +46,8 @@
--spectrum-alias-highlight-active: rgba(169, 177, 193, 0.1);
--spectrum-alias-background-color-hover-overlay: rgba(169, 177, 193, 0.1);
+ /* Custom additions */
--modal-background: var(--spectrum-global-color-gray-50);
+ --drop-shadow: rgba(0, 0, 0, 0.15) !important;
+ --spectrum-global-color-blue-100: rgb(56, 65, 84) !important;
}
diff --git a/packages/frontend-core/src/utils/utils.js b/packages/frontend-core/src/utils/utils.js
index d4cf579af1..46d8395f77 100644
--- a/packages/frontend-core/src/utils/utils.js
+++ b/packages/frontend-core/src/utils/utils.js
@@ -86,3 +86,23 @@ export const throttle = (callback, minDelay = 1000) => {
}
return invoke
}
+
+/**
+ * Utility to debounce DOM activities using requestAnimationFrame
+ * @param callback the function to run
+ * @returns {Function}
+ */
+export const domDebounce = callback => {
+ let active = false
+ let lastParams
+ return (...params) => {
+ lastParams = params
+ if (!active) {
+ active = true
+ requestAnimationFrame(() => {
+ callback(...lastParams)
+ active = false
+ })
+ }
+ }
+}
diff --git a/packages/sdk/package.json b/packages/sdk/package.json
index ba187295d0..c38dedec7d 100644
--- a/packages/sdk/package.json
+++ b/packages/sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@budibase/sdk",
- "version": "2.5.5-alpha.0",
+ "version": "2.5.6-alpha.32",
"description": "Budibase Public API SDK",
"author": "Budibase",
"license": "MPL-2.0",
diff --git a/packages/server/package.json b/packages/server/package.json
index 65e79ba7b7..1bc50f61d6 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
- "version": "2.5.5-alpha.0",
+ "version": "2.5.6-alpha.32",
"description": "Budibase Web Server",
"main": "src/index.ts",
"repository": {
@@ -45,12 +45,12 @@
"license": "GPL-3.0",
"dependencies": {
"@apidevtools/swagger-parser": "10.0.3",
- "@budibase/backend-core": "2.5.5-alpha.0",
- "@budibase/client": "2.5.5-alpha.0",
- "@budibase/pro": "2.5.5-alpha.0",
- "@budibase/shared-core": "2.5.5-alpha.0",
- "@budibase/string-templates": "2.5.5-alpha.0",
- "@budibase/types": "2.5.5-alpha.0",
+ "@budibase/backend-core": "2.5.6-alpha.32",
+ "@budibase/client": "2.5.6-alpha.32",
+ "@budibase/pro": "2.5.6-alpha.32",
+ "@budibase/shared-core": "2.5.6-alpha.32",
+ "@budibase/string-templates": "2.5.6-alpha.32",
+ "@budibase/types": "2.5.6-alpha.32",
"@bull-board/api": "3.7.0",
"@bull-board/koa": "3.9.4",
"@elastic/elasticsearch": "7.10.0",
@@ -65,6 +65,7 @@
"bull": "4.10.1",
"chmodr": "1.2.0",
"chokidar": "3.5.3",
+ "cookies": "0.8.0",
"csvtojson": "2.0.10",
"curlconverter": "3.21.0",
"dd-trace": "3.13.2",
@@ -108,18 +109,17 @@
"redis": "4",
"server-destroy": "1.0.1",
"snowflake-promise": "^4.5.0",
- "socket.io": "^4.5.1",
+ "socket.io": "4.6.1",
"svelte": "3.49.0",
"swagger-parser": "10.0.3",
"tar": "6.1.11",
"to-json-schema": "0.2.5",
"uuid": "3.3.2",
"validate.js": "0.13.1",
- "vm2": "3.9.16",
+ "vm2": "3.9.17",
"worker-farm": "1.7.0",
"xml2js": "0.5.0",
- "yargs": "13.2.4",
- "zlib": "1.0.5"
+ "yargs": "13.2.4"
},
"devDependencies": {
"@babel/core": "7.17.4",
diff --git a/packages/server/scripts/integrations/mssql/data/entrypoint.sh b/packages/server/scripts/integrations/mssql/data/entrypoint.sh
index 04780d085e..ffe8d2cd5d 100644
--- a/packages/server/scripts/integrations/mssql/data/entrypoint.sh
+++ b/packages/server/scripts/integrations/mssql/data/entrypoint.sh
@@ -11,6 +11,7 @@ if [ "$1" = '/opt/mssql/bin/sqlservr' ]; then
echo "RUNNING BUDIBASE SETUP"
+ cat setup.sql
#run the setup script to create the DB and the schema in the DB
/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P Passw0rd -i setup.sql
diff --git a/packages/server/scripts/integrations/mssql/data/setup.sql b/packages/server/scripts/integrations/mssql/data/setup.sql
index 91c53cced7..a33a99cea5 100644
--- a/packages/server/scripts/integrations/mssql/data/setup.sql
+++ b/packages/server/scripts/integrations/mssql/data/setup.sql
@@ -34,7 +34,7 @@ GO
CREATE TABLE people
(
name varchar(30) NOT NULL,
- age varchar(20),
+ age int default 20 NOT NULL,
CONSTRAINT pk_people PRIMARY KEY NONCLUSTERED (name, age)
);
@@ -50,22 +50,22 @@ VALUES
('Processing', 1);
INSERT INTO people (name, age)
-VALUES ('Bob', '30'),
- ('Bert', '10'),
- ('Jack', '12'),
- ('Mike', '31'),
- ('Dave', '44'),
- ('Jim', '43'),
- ('Kerry', '32'),
- ('Julie', '12'),
- ('Kim', '55'),
- ('Andy', '33'),
- ('John', '22'),
- ('Ruth', '66'),
- ('Robert', '88'),
- ('Bobert', '99'),
- ('Jan', '22'),
- ('Megan', '11');
+VALUES ('Bob', 30),
+ ('Bert', 10),
+ ('Jack', 12),
+ ('Mike', 31),
+ ('Dave', 44),
+ ('Jim', 43),
+ ('Kerry', 32),
+ ('Julie', 12),
+ ('Kim', 55),
+ ('Andy', 33),
+ ('John', 22),
+ ('Ruth', 66),
+ ('Robert', 88),
+ ('Bobert', 99),
+ ('Jan', 22),
+ ('Megan', 11);
IF OBJECT_ID ('Chains.sizes', 'U') IS NOT NULL
diff --git a/packages/server/scripts/integrations/mysql/init.sql b/packages/server/scripts/integrations/mysql/init.sql
index 15269f2f41..ae5cd07788 100644
--- a/packages/server/scripts/integrations/mysql/init.sql
+++ b/packages/server/scripts/integrations/mysql/init.sql
@@ -3,7 +3,7 @@ USE main;
CREATE TABLE Persons (
PersonID int NOT NULL AUTO_INCREMENT,
CreatedAt datetime,
- Age float,
+ Age float DEFAULT 20 NOT NULL,
LastName varchar(255),
FirstName varchar(255),
Address varchar(255),
diff --git a/packages/server/scripts/integrations/postgres/init.sql b/packages/server/scripts/integrations/postgres/init.sql
index 78af4c20b9..057944101a 100644
--- a/packages/server/scripts/integrations/postgres/init.sql
+++ b/packages/server/scripts/integrations/postgres/init.sql
@@ -8,6 +8,7 @@ CREATE TABLE Persons (
FirstName varchar(255),
Address varchar(255),
City varchar(255) DEFAULT 'Belfast',
+ Age INTEGER DEFAULT 20 NOT NULL,
Type person_job
);
CREATE TABLE Tasks (
diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts
index 02404fdd24..ff0dbe015b 100644
--- a/packages/server/src/api/controllers/application.ts
+++ b/packages/server/src/api/controllers/application.ts
@@ -1,5 +1,4 @@
import env from "../../environment"
-import packageJson from "../../../package.json"
import {
createLinkView,
createRoutingView,
@@ -24,6 +23,7 @@ import {
migrations,
objectStore,
ErrorCode,
+ env as envCore,
} from "@budibase/backend-core"
import { USERS_TABLE_SCHEMA } from "../../constants"
import { buildDefaultDocs } from "../../db/defaultData/datasource_bb_default"
@@ -223,7 +223,7 @@ export async function fetchAppPackage(ctx: UserCtx) {
)
ctx.body = {
- application,
+ application: { ...application, upgradableVersion: envCore.VERSION },
screens,
layouts,
clientLibPath,
@@ -264,7 +264,7 @@ async function performAppCreate(ctx: UserCtx) {
_rev: undefined,
appId,
type: "app",
- version: packageJson.version,
+ version: envCore.VERSION,
componentLibraries: ["@budibase/standard-components"],
name: name,
url: url,
@@ -433,7 +433,7 @@ export async function updateClient(ctx: UserCtx) {
}
// Update versions in app package
- const updatedToVersion = packageJson.version
+ const updatedToVersion = envCore.VERSION
const appPackageUpdates = {
version: updatedToVersion,
revertableVersion: currentVersion,
diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts
index 8ccd3fb3d0..ae49cd83f1 100644
--- a/packages/server/src/api/controllers/automation.ts
+++ b/packages/server/src/api/controllers/automation.ts
@@ -16,9 +16,15 @@ import { setTestFlag, clearTestFlag } from "../../utilities/redis"
import { context, cache, events } from "@budibase/backend-core"
import { automations } from "@budibase/pro"
import { Automation, BBContext } from "@budibase/types"
+import { getActionDefinitions as actionDefs } from "../../automations/actions"
-const ACTION_DEFS = removeDeprecated(actions.ACTION_DEFINITIONS)
-const TRIGGER_DEFS = removeDeprecated(triggers.TRIGGER_DEFINITIONS)
+async function getActionDefinitions() {
+ return removeDeprecated(await actionDefs())
+}
+
+function getTriggerDefinitions() {
+ return removeDeprecated(triggers.TRIGGER_DEFINITIONS)
+}
/*************************
* *
@@ -228,17 +234,17 @@ export async function clearLogError(ctx: BBContext) {
}
export async function getActionList(ctx: BBContext) {
- ctx.body = ACTION_DEFS
+ ctx.body = await getActionDefinitions()
}
export async function getTriggerList(ctx: BBContext) {
- ctx.body = TRIGGER_DEFS
+ ctx.body = getTriggerDefinitions()
}
export async function getDefinitionList(ctx: BBContext) {
ctx.body = {
- trigger: TRIGGER_DEFS,
- action: ACTION_DEFS,
+ trigger: getTriggerDefinitions(),
+ action: await getActionDefinitions(),
}
}
diff --git a/packages/server/src/api/controllers/cloud.ts b/packages/server/src/api/controllers/cloud.ts
deleted file mode 100644
index 7be00e3a1d..0000000000
--- a/packages/server/src/api/controllers/cloud.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import env from "../../environment"
-import { db as dbCore, tenancy } from "@budibase/backend-core"
-import { streamFile } from "../../utilities/fileSystem"
-import { stringToReadStream } from "../../utilities"
-import { getDocParams, DocumentType, isDevAppID } from "../../db/utils"
-import { create } from "./application"
-import { join } from "path"
-import sdk from "../../sdk"
-import { App, Ctx, Database } from "@budibase/types"
-
-async function createApp(appName: string, appDirectory: string) {
- const ctx = {
- request: {
- body: {
- useTemplate: true,
- name: appName,
- },
- files: {
- templateFile: {
- path: appDirectory,
- },
- },
- },
- }
- // @ts-ignore
- return create(ctx)
-}
-
-async function getAllDocType(db: Database, docType: string) {
- const response = await db.allDocs(
- getDocParams(docType, null, {
- include_docs: true,
- })
- )
- return response.rows.map(row => row.doc)
-}
-
-export async function exportApps(ctx: Ctx) {
- if (env.SELF_HOSTED || !env.MULTI_TENANCY) {
- ctx.throw(400, "Exporting only allowed in multi-tenant cloud environments.")
- }
- const apps = (await dbCore.getAllApps({ all: true })) as App[]
- const globalDBString = await sdk.backups.exportDB(dbCore.getGlobalDBName(), {
- filter: (doc: any) => !doc._id.startsWith(DocumentType.USER),
- })
- // only export the dev apps as they will be the latest, the user can republish the apps
- // in their self-hosted environment
- let appMetadata = apps
- .filter((app: App) => isDevAppID(app.appId || app._id))
- .map((app: App) => ({ appId: (app.appId || app._id)!, name: app.name }))
- const tmpPath = await sdk.backups.exportMultipleApps(
- appMetadata,
- globalDBString
- )
- const filename = `cloud-export-${new Date().getTime()}.tar.gz`
- ctx.attachment(filename)
- ctx.body = streamFile(tmpPath)
-}
-
-async function checkHasBeenImported() {
- if (!env.SELF_HOSTED) {
- return true
- }
- const apps = await dbCore.getAllApps({ all: true })
- return apps.length !== 0
-}
-
-export async function hasBeenImported(ctx: Ctx) {
- ctx.body = {
- imported: await checkHasBeenImported(),
- }
-}
-
-export async function importApps(ctx: Ctx) {
- if (!env.SELF_HOSTED) {
- ctx.throw(400, "Importing only allowed in self hosted environments.")
- }
- const beenImported = await checkHasBeenImported()
- if (beenImported || !ctx.request.files || !ctx.request.files.importFile) {
- ctx.throw(
- 400,
- "Import file is required and environment must be fresh to import apps."
- )
- }
- const file = ctx.request.files.importFile as any
- if (Array.isArray(file)) {
- ctx.throw(400, "Single file is required")
- }
- if (file.type !== "application/gzip" && file.type !== "application/x-gzip") {
- ctx.throw(400, "Import file must be a gzipped tarball.")
- }
-
- // initially get all the app databases out of the tarball
- const tmpPath = sdk.backups.untarFile(file)
- const globalDbImport = sdk.backups.getGlobalDBFile(tmpPath)
- const appNames = sdk.backups.getListOfAppsInMulti(tmpPath)
-
- const globalDb = tenancy.getGlobalDB()
- // load the global db first
- await globalDb.load(stringToReadStream(globalDbImport))
- for (let appName of appNames) {
- await createApp(appName, join(tmpPath, appName))
- }
-
- // if there are any users make sure to remove them
- let users = await getAllDocType(globalDb, DocumentType.USER)
- let userDeletionPromises = []
- for (let user of users) {
- userDeletionPromises.push(globalDb.remove(user._id, user._rev))
- }
- if (userDeletionPromises.length > 0) {
- await Promise.all(userDeletionPromises)
- }
-
- await globalDb.bulkDocs(users)
- ctx.body = {
- message: "Apps successfully imported.",
- }
-}
diff --git a/packages/server/src/api/controllers/dev.ts b/packages/server/src/api/controllers/dev.ts
index 9dbbe90555..e80d9d7ea1 100644
--- a/packages/server/src/api/controllers/dev.ts
+++ b/packages/server/src/api/controllers/dev.ts
@@ -4,7 +4,7 @@ import { checkSlashesInUrl } from "../../utilities"
import { request } from "../../utilities/workerRequests"
import { clearLock as redisClearLock } from "../../utilities/redis"
import { DocumentType } from "../../db/utils"
-import { context } from "@budibase/backend-core"
+import { context, env as envCore } from "@budibase/backend-core"
import { events, db as dbCore, cache } from "@budibase/backend-core"
async function redirect(ctx: any, method: string, path: string = "global") {
@@ -121,7 +121,7 @@ export async function revert(ctx: any) {
}
export async function getBudibaseVersion(ctx: any) {
- const version = require("../../../package.json").version
+ const version = envCore.VERSION
ctx.body = {
version,
}
diff --git a/packages/server/src/api/controllers/plugin/index.ts b/packages/server/src/api/controllers/plugin/index.ts
index faecbc1fd8..31a3468178 100644
--- a/packages/server/src/api/controllers/plugin/index.ts
+++ b/packages/server/src/api/controllers/plugin/index.ts
@@ -1,31 +1,11 @@
-import { npmUpload, urlUpload, githubUpload, fileUpload } from "./uploaders"
-import {
- plugins as pluginCore,
- db as dbCore,
- tenancy,
- objectStore,
-} from "@budibase/backend-core"
-import { PluginType, FileType, PluginSource, Plugin } from "@budibase/types"
+import { npmUpload, urlUpload, githubUpload } from "./uploaders"
+import { plugins as pluginCore } from "@budibase/backend-core"
+import { PluginType, FileType, PluginSource } from "@budibase/types"
import env from "../../../environment"
-import { ClientAppSocket } from "../../../websocket"
+import { clientAppSocket } from "../../../websockets"
+import sdk from "../../../sdk"
import { sdk as pro } from "@budibase/pro"
-export async function getPlugins(type?: PluginType) {
- const db = tenancy.getGlobalDB()
- const response = await db.allDocs(
- dbCore.getPluginParams(null, {
- include_docs: true,
- })
- )
- let plugins = response.rows.map((row: any) => row.doc) as Plugin[]
- plugins = objectStore.enrichPluginURLs(plugins)
- if (type) {
- return plugins.filter((plugin: Plugin) => plugin.schema?.type === type)
- } else {
- return plugins
- }
-}
-
export async function upload(ctx: any) {
const plugins: FileType[] =
ctx.request.files.file.length > 1
@@ -35,7 +15,7 @@ export async function upload(ctx: any) {
let docs = []
// can do single or multiple plugins
for (let plugin of plugins) {
- const doc = await processUploadedPlugin(plugin, PluginSource.FILE)
+ const doc = await sdk.plugins.processUploaded(plugin, PluginSource.FILE)
docs.push(doc)
}
ctx.body = {
@@ -91,7 +71,7 @@ export async function create(ctx: any) {
const doc = await pro.plugins.storePlugin(metadata, directory, source)
- ClientAppSocket.emit("plugins-update", { name, hash: doc.hash })
+ clientAppSocket.emit("plugins-update", { name, hash: doc.hash })
ctx.body = {
message: "Plugin uploaded successfully",
plugins: [doc],
@@ -105,7 +85,7 @@ export async function create(ctx: any) {
}
export async function fetch(ctx: any) {
- ctx.body = await getPlugins()
+ ctx.body = await sdk.plugins.fetch()
}
export async function destroy(ctx: any) {
@@ -119,20 +99,3 @@ export async function destroy(ctx: any) {
ctx.throw(400, err.message)
}
}
-
-export async function processUploadedPlugin(
- plugin: FileType,
- source?: PluginSource
-) {
- const { metadata, directory } = await fileUpload(plugin)
- pluginCore.validate(metadata?.schema)
-
- // Only allow components in cloud
- if (!env.SELF_HOSTED && metadata?.schema?.type !== PluginType.COMPONENT) {
- throw new Error("Only component plugins are supported outside of self-host")
- }
-
- const doc = await pro.plugins.storePlugin(metadata, directory, source)
- ClientAppSocket.emit("plugin-update", { name: doc.name, hash: doc.hash })
- return doc
-}
diff --git a/packages/server/src/api/controllers/query/index.ts b/packages/server/src/api/controllers/query/index.ts
index 91e7f01afa..356a2eb5c0 100644
--- a/packages/server/src/api/controllers/query/index.ts
+++ b/packages/server/src/api/controllers/query/index.ts
@@ -245,7 +245,7 @@ async function execute(
}
const runFn = () => Runner.run(inputs)
- const { rows, pagination, extra } = await quotas.addQuery(runFn, {
+ const { rows, pagination, extra, info } = await quotas.addQuery(runFn, {
datasourceId: datasource._id,
})
// remove the raw from execution incase transformer being used to hide data
@@ -255,7 +255,7 @@ async function execute(
if (opts && opts.rowsOnly) {
ctx.body = rows
} else {
- ctx.body = { data: rows, pagination, ...extra }
+ ctx.body = { data: rows, pagination, ...extra, ...info }
}
} catch (err) {
ctx.throw(400, err)
diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts
index 44b38bbeac..b13ec22cfd 100644
--- a/packages/server/src/api/controllers/row/ExternalRequest.ts
+++ b/packages/server/src/api/controllers/row/ExternalRequest.ts
@@ -1,31 +1,32 @@
import {
+ Datasource,
+ FieldSchema,
+ FieldType,
FilterType,
IncludeRelationship,
Operation,
PaginationJson,
RelationshipsJson,
+ RelationshipTypes,
+ Row,
SearchFilters,
SortJson,
- Datasource,
- FieldSchema,
- Row,
- Table,
- RelationshipTypes,
- FieldType,
SortType,
+ Table,
} from "@budibase/types"
import {
+ breakExternalTableId,
breakRowIdField,
+ convertRowId,
generateRowIdField,
isRowId,
- convertRowId,
+ isSQL,
} from "../../../integrations/utils"
import { getDatasourceAndQuery } from "./utils"
import { FieldTypes } from "../../../constants"
-import { breakExternalTableId, isSQL } from "../../../integrations/utils"
import { processObjectSync } from "@budibase/string-templates"
import { cloneDeep } from "lodash/fp"
-import { processFormulas, processDates } from "../../../utilities/rowProcessor"
+import { processDates, processFormulas } from "../../../utilities/rowProcessor"
import { db as dbCore } from "@budibase/backend-core"
import sdk from "../../../sdk"
@@ -382,10 +383,18 @@ export class ExternalRequest {
}
const display = linkedTable.primaryDisplay
for (let key of Object.keys(row[relationship.column])) {
- const related: Row = row[relationship.column][key]
+ let relatedRow: Row = row[relationship.column][key]
+ // add this row as context for the relationship
+ for (let col of Object.values(linkedTable.schema)) {
+ if (col.type === FieldType.LINK && col.tableId === table._id) {
+ relatedRow[col.name] = [row]
+ }
+ }
+ relatedRow = processFormulas(linkedTable, relatedRow)
+ const relatedDisplay = display ? relatedRow[display] : undefined
row[relationship.column][key] = {
- primaryDisplay: display ? related[display] : undefined,
- _id: related._id,
+ primaryDisplay: relatedDisplay || "Invalid display column",
+ _id: relatedRow._id,
}
}
}
diff --git a/packages/server/src/api/controllers/row/internal.ts b/packages/server/src/api/controllers/row/internal.ts
index 039f03c015..a0bebc2490 100644
--- a/packages/server/src/api/controllers/row/internal.ts
+++ b/packages/server/src/api/controllers/row/internal.ts
@@ -30,7 +30,6 @@ import { finaliseRow, updateRelatedFormula } from "./staticFormula"
import { csv, json, jsonWithSchema, Format } from "../view/exporters"
import { apiFileReturn } from "../../../utilities/fileSystem"
import {
- Ctx,
UserCtx,
Database,
LinkDocumentValue,
@@ -72,7 +71,7 @@ async function getView(db: Database, viewName: string) {
return viewInfo
}
-async function getRawTableData(ctx: Ctx, db: Database, tableId: string) {
+async function getRawTableData(ctx: UserCtx, db: Database, tableId: string) {
let rows
if (tableId === InternalTables.USER_METADATA) {
await userController.fetchMetadata(ctx)
@@ -188,7 +187,7 @@ export async function save(ctx: UserCtx) {
})
}
-export async function fetchView(ctx: Ctx) {
+export async function fetchView(ctx: UserCtx) {
const viewName = decodeURIComponent(ctx.params.viewName)
// if this is a table view being looked for just transfer to that
@@ -255,7 +254,7 @@ export async function fetchView(ctx: Ctx) {
return rows
}
-export async function fetch(ctx: Ctx) {
+export async function fetch(ctx: UserCtx) {
const db = context.getAppDB()
const tableId = ctx.params.tableId
@@ -264,7 +263,7 @@ export async function fetch(ctx: Ctx) {
return outputProcessing(table, rows)
}
-export async function find(ctx: Ctx) {
+export async function find(ctx: UserCtx) {
const db = dbCore.getDB(ctx.appId)
const table = await db.get(ctx.params.tableId)
let row = await utils.findRow(ctx, ctx.params.tableId, ctx.params.rowId)
@@ -272,7 +271,7 @@ export async function find(ctx: Ctx) {
return row
}
-export async function destroy(ctx: Ctx) {
+export async function destroy(ctx: UserCtx) {
const db = context.getAppDB()
const { _id } = ctx.request.body
let row = await db.get(_id)
@@ -308,7 +307,7 @@ export async function destroy(ctx: Ctx) {
return { response, row }
}
-export async function bulkDestroy(ctx: Ctx) {
+export async function bulkDestroy(ctx: UserCtx) {
const db = context.getAppDB()
const tableId = ctx.params.tableId
const table = await db.get(tableId)
@@ -347,7 +346,7 @@ export async function bulkDestroy(ctx: Ctx) {
return { response: { ok: true }, rows: processedRows }
}
-export async function search(ctx: Ctx) {
+export async function search(ctx: UserCtx) {
// Fetch the whole table when running in cypress, as search doesn't work
if (!env.COUCH_DB_URL && env.isCypress()) {
return { rows: await fetch(ctx) }
@@ -387,7 +386,7 @@ export async function search(ctx: Ctx) {
return response
}
-export async function exportRows(ctx: Ctx) {
+export async function exportRows(ctx: UserCtx) {
const db = context.getAppDB()
const table = await db.get(ctx.params.tableId)
const rowIds = ctx.request.body.rows
@@ -439,7 +438,7 @@ export async function exportRows(ctx: Ctx) {
}
}
-export async function fetchEnrichedRow(ctx: Ctx) {
+export async function fetchEnrichedRow(ctx: UserCtx) {
const db = context.getAppDB()
const tableId = ctx.params.tableId
const rowId = ctx.params.rowId
diff --git a/packages/server/src/api/controllers/row/utils.ts b/packages/server/src/api/controllers/row/utils.ts
index 2cf3b5472f..e96a4fe6ee 100644
--- a/packages/server/src/api/controllers/row/utils.ts
+++ b/packages/server/src/api/controllers/row/utils.ts
@@ -5,7 +5,7 @@ import { context } from "@budibase/backend-core"
import { makeExternalQuery } from "../../../integrations/base/query"
import { Row, Table } from "@budibase/types"
import { Format } from "../view/exporters"
-import { Ctx } from "@budibase/types"
+import { UserCtx } from "@budibase/types"
import sdk from "../../../sdk"
const validateJs = require("validate.js")
const { cloneDeep } = require("lodash/fp")
@@ -26,7 +26,7 @@ export async function getDatasourceAndQuery(json: any) {
return makeExternalQuery(datasource, json)
}
-export async function findRow(ctx: Ctx, tableId: string, rowId: string) {
+export async function findRow(ctx: UserCtx, tableId: string, rowId: string) {
const db = context.getAppDB()
let row
// TODO remove special user case in future
diff --git a/packages/server/src/api/controllers/table/utils.ts b/packages/server/src/api/controllers/table/utils.ts
index cf1883c083..1a3eda683b 100644
--- a/packages/server/src/api/controllers/table/utils.ts
+++ b/packages/server/src/api/controllers/table/utils.ts
@@ -110,21 +110,28 @@ export function importToRows(
table: Table,
user: ContextUser | null = null
) {
+ let originalTable = table
let finalData: any = []
for (let i = 0; i < data.length; i++) {
let row = data[i]
row._id = generateRowID(table._id!)
row.tableId = table._id
+
+ // We use a reference to table here and update it after input processing,
+ // so that we can auto increment auto IDs in imported data properly
const processed = inputProcessing(user, table, row, {
noAutoRelationships: true,
})
row = processed.row
table = processed.table
- for (const [fieldName, schema] of Object.entries(table.schema)) {
- // check whether the options need to be updated for inclusion as part of the data import
+ // However here we must reference the original table, as we want to mutate
+ // the real schema of the table passed in, not the clone used for
+ // incrementing auto IDs
+ for (const [fieldName, schema] of Object.entries(originalTable.schema)) {
if (
- schema.type === FieldTypes.OPTIONS &&
+ (schema.type === FieldTypes.OPTIONS ||
+ schema.type === FieldTypes.ARRAY) &&
row[fieldName] &&
(!schema.constraints!.inclusion ||
schema.constraints!.inclusion.indexOf(row[fieldName]) === -1)
diff --git a/packages/server/src/api/controllers/user.ts b/packages/server/src/api/controllers/user.ts
index 1ae1a68824..b66f11bc1c 100644
--- a/packages/server/src/api/controllers/user.ts
+++ b/packages/server/src/api/controllers/user.ts
@@ -1,98 +1,12 @@
import { generateUserMetadataID, generateUserFlagID } from "../../db/utils"
import { InternalTables } from "../../db/utils"
-import { getGlobalUsers, getRawGlobalUser } from "../../utilities/global"
+import { getGlobalUsers } from "../../utilities/global"
import { getFullUser } from "../../utilities/users"
-import {
- context,
- roles as rolesCore,
- db as dbCore,
-} from "@budibase/backend-core"
-import { BBContext, Ctx, SyncUserRequest, User } from "@budibase/types"
+import { context } from "@budibase/backend-core"
+import { UserCtx } from "@budibase/types"
import sdk from "../../sdk"
-export async function syncUser(ctx: Ctx) {
- let deleting = false,
- user: User | any
- const userId = ctx.params.id
-
- const previousUser = ctx.request.body?.previousUser
-
- try {
- user = (await getRawGlobalUser(userId)) as User
- } catch (err: any) {
- if (err && err.status === 404) {
- user = {}
- deleting = true
- } else {
- throw err
- }
- }
-
- let previousApps = previousUser
- ? Object.keys(previousUser.roles).map(appId => appId)
- : []
-
- const roles = deleting ? {} : user.roles
- // remove props which aren't useful to metadata
- delete user.password
- delete user.forceResetPassword
- delete user.roles
- // run through all production appIDs in the users roles
- let prodAppIds
- // if they are a builder then get all production app IDs
- if ((user.builder && user.builder.global) || deleting) {
- prodAppIds = await dbCore.getProdAppIDs()
- } else {
- prodAppIds = Object.entries(roles)
- .filter(entry => entry[1] !== rolesCore.BUILTIN_ROLE_IDS.PUBLIC)
- .map(([appId]) => appId)
- }
- for (let prodAppId of new Set([...prodAppIds, ...previousApps])) {
- const roleId = roles[prodAppId]
- const deleteFromApp = !roleId
- const devAppId = dbCore.getDevelopmentAppID(prodAppId)
- for (let appId of [prodAppId, devAppId]) {
- if (!(await dbCore.dbExists(appId))) {
- continue
- }
- await context.doInAppContext(appId, async () => {
- const db = context.getAppDB()
- const metadataId = generateUserMetadataID(userId)
- let metadata
- try {
- metadata = await db.get(metadataId)
- } catch (err) {
- if (deleteFromApp) {
- return
- }
- metadata = {
- tableId: InternalTables.USER_METADATA,
- }
- }
-
- if (deleteFromApp) {
- await db.remove(metadata)
- return
- }
-
- // assign the roleId for the metadata doc
- if (roleId) {
- metadata.roleId = roleId
- }
- let combined = sdk.users.combineMetadataAndUser(user, metadata)
- // if its null then there was no updates required
- if (combined) {
- await db.put(combined)
- }
- })
- }
- }
- ctx.body = {
- message: "User synced.",
- }
-}
-
-export async function fetchMetadata(ctx: BBContext) {
+export async function fetchMetadata(ctx: UserCtx) {
const global = await getGlobalUsers()
const metadata = await sdk.users.rawUserMetadata()
const users = []
@@ -111,7 +25,7 @@ export async function fetchMetadata(ctx: BBContext) {
ctx.body = users
}
-export async function updateSelfMetadata(ctx: BBContext) {
+export async function updateSelfMetadata(ctx: UserCtx) {
// overwrite the ID with current users
ctx.request.body._id = ctx.user?._id
// make sure no stale rev
@@ -121,7 +35,7 @@ export async function updateSelfMetadata(ctx: BBContext) {
await updateMetadata(ctx)
}
-export async function updateMetadata(ctx: BBContext) {
+export async function updateMetadata(ctx: UserCtx) {
const db = context.getAppDB()
const user = ctx.request.body
// this isn't applicable to the user
@@ -133,7 +47,7 @@ export async function updateMetadata(ctx: BBContext) {
ctx.body = await db.put(metadata)
}
-export async function destroyMetadata(ctx: BBContext) {
+export async function destroyMetadata(ctx: UserCtx) {
const db = context.getAppDB()
try {
const dbUser = await db.get(ctx.params.id)
@@ -146,11 +60,11 @@ export async function destroyMetadata(ctx: BBContext) {
}
}
-export async function findMetadata(ctx: BBContext) {
+export async function findMetadata(ctx: UserCtx) {
ctx.body = await getFullUser(ctx, ctx.params.id)
}
-export async function setFlag(ctx: BBContext) {
+export async function setFlag(ctx: UserCtx) {
const userId = ctx.user?._id
const { flag, value } = ctx.request.body
if (!flag) {
@@ -169,7 +83,7 @@ export async function setFlag(ctx: BBContext) {
ctx.body = { message: "Flag set successfully" }
}
-export async function getFlags(ctx: BBContext) {
+export async function getFlags(ctx: UserCtx) {
const userId = ctx.user?._id
const docId = generateUserFlagID(userId!)
const db = context.getAppDB()
diff --git a/packages/server/src/api/index.ts b/packages/server/src/api/index.ts
index 656596a198..b4ee0faa7d 100644
--- a/packages/server/src/api/index.ts
+++ b/packages/server/src/api/index.ts
@@ -1,10 +1,8 @@
import Router from "@koa/router"
-import { auth, middleware } from "@budibase/backend-core"
+import { auth, middleware, env as envCore } from "@budibase/backend-core"
import currentApp from "../middleware/currentapp"
import zlib from "zlib"
import { mainRoutes, staticRoutes, publicRoutes } from "./routes"
-import pkg from "../../package.json"
-import env from "../environment"
import { middleware as pro } from "@budibase/pro"
export { shutdown } from "./routes/public"
const compress = require("koa-compress")
@@ -12,7 +10,7 @@ const compress = require("koa-compress")
export const router: Router = new Router()
router.get("/health", ctx => (ctx.status = 200))
-router.get("/version", ctx => (ctx.body = pkg.version))
+router.get("/version", ctx => (ctx.body = envCore.VERSION))
router.use(middleware.errorHandling)
diff --git a/packages/server/src/api/routes/cloud.ts b/packages/server/src/api/routes/cloud.ts
deleted file mode 100644
index 308ee260c1..0000000000
--- a/packages/server/src/api/routes/cloud.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import Router from "@koa/router"
-import * as controller from "../controllers/cloud"
-import authorized from "../../middleware/authorized"
-import { permissions } from "@budibase/backend-core"
-
-const router: Router = new Router()
-
-router
- .get(
- "/api/cloud/export",
- authorized(permissions.BUILDER),
- controller.exportApps
- )
- // has to be public, only run if apps don't exist
- .post("/api/cloud/import", controller.importApps)
- .get("/api/cloud/import/complete", controller.hasBeenImported)
-
-export default router
diff --git a/packages/server/src/api/routes/index.ts b/packages/server/src/api/routes/index.ts
index e15e8aa92b..f5374465d0 100644
--- a/packages/server/src/api/routes/index.ts
+++ b/packages/server/src/api/routes/index.ts
@@ -22,7 +22,6 @@ import queryRoutes from "./query"
import backupRoutes from "./backup"
import metadataRoutes from "./metadata"
import devRoutes from "./dev"
-import cloudRoutes from "./cloud"
import migrationRoutes from "./migrations"
import pluginRoutes from "./plugin"
import opsRoutes from "./ops"
@@ -60,7 +59,6 @@ export const mainRoutes: Router[] = [
queryRoutes,
metadataRoutes,
devRoutes,
- cloudRoutes,
rowRoutes,
migrationRoutes,
pluginRoutes,
diff --git a/packages/server/src/api/routes/tests/automation.spec.js b/packages/server/src/api/routes/tests/automation.spec.js
index 47daa8975b..9efc2b0d7e 100644
--- a/packages/server/src/api/routes/tests/automation.spec.js
+++ b/packages/server/src/api/routes/tests/automation.spec.js
@@ -7,7 +7,7 @@ const {
const setup = require("./utilities")
const { basicAutomation, newAutomation, automationTrigger, automationStep } = setup.structures
const MAX_RETRIES = 4
-const { TRIGGER_DEFINITIONS, ACTION_DEFINITIONS } = require("../../../automations")
+const { TRIGGER_DEFINITIONS, BUILTIN_ACTION_DEFINITIONS } = require("../../../automations")
const { events } = require("@budibase/backend-core")
@@ -55,7 +55,7 @@ describe("/automations", () => {
.expect('Content-Type', /json/)
.expect(200)
- let definitionsLength = Object.keys(ACTION_DEFINITIONS).length
+ let definitionsLength = Object.keys(BUILTIN_ACTION_DEFINITIONS).length
definitionsLength-- // OUTGOING_WEBHOOK is deprecated
expect(Object.keys(res.body.action).length).toBeGreaterThanOrEqual(definitionsLength)
diff --git a/packages/server/src/api/routes/tests/cloud.spec.ts b/packages/server/src/api/routes/tests/cloud.spec.ts
deleted file mode 100644
index aad1214a31..0000000000
--- a/packages/server/src/api/routes/tests/cloud.spec.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { App } from "@budibase/types"
-
-jest.setTimeout(30000)
-
-import { AppStatus } from "../../../db/utils"
-
-import * as setup from "./utilities"
-
-import { wipeDb } from "./utilities/TestFunctions"
-import { tenancy } from "@budibase/backend-core"
-
-describe("/cloud", () => {
- let request = setup.getRequest()!
- let config = setup.getConfig()
-
- afterAll(setup.afterAll)
-
- beforeAll(async () => {
- // Importing is only allowed in self hosted environments
- await config.init()
- config.modeSelf()
- })
-
- describe("import", () => {
- it("should be able to import apps", async () => {
- // first we need to delete any existing apps on the system so it looks clean otherwise the
- // import will not run
- await wipeDb()
-
- // Perform the import
- const res = await request
- .post(`/api/cloud/import`)
- .set(config.publicHeaders())
- .attach("importFile", "src/api/routes/tests/data/export-test.tar.gz")
- .expect(200)
- expect(res.body.message).toEqual("Apps successfully imported.")
-
- // get a count of apps after the import
- const postImportApps = await request
- .get(`/api/applications?status=${AppStatus.ALL}`)
- .set(config.publicHeaders())
- .expect("Content-Type", /json/)
- .expect(200)
-
- const apps = postImportApps.body as App[]
- // There are two apps in the file that was imported so check for this
- expect(apps.length).toEqual(2)
- // The new tenant id was assigned to the imported apps
- expect(tenancy.getTenantIDFromAppID(apps[0].appId)).toBe(
- config.getTenantId()
- )
- })
- })
-})
diff --git a/packages/server/src/api/routes/tests/user.spec.js b/packages/server/src/api/routes/tests/user.spec.js
index 0a2b02364a..e8ffd8df2b 100644
--- a/packages/server/src/api/routes/tests/user.spec.js
+++ b/packages/server/src/api/routes/tests/user.spec.js
@@ -205,41 +205,4 @@ describe("/users", () => {
expect(res.body.message).toEqual("Flag set successfully")
})
})
-
- describe("syncUser", () => {
- it("should sync the user", async () => {
- let user = await config.createUser()
- await config.createApp("New App")
- let res = await request
- .post(`/api/users/metadata/sync/${user._id}`)
- .set(config.defaultHeaders())
- .expect(200)
- .expect("Content-Type", /json/)
- expect(res.body.message).toEqual("User synced.")
- })
-
- it("should sync the user when a previous user is specified", async () => {
- const app1 = await config.createApp("App 1")
- const app2 = await config.createApp("App 2")
-
- let user = await config.createUser({
- builder: false,
- admin: true,
- roles: { [app1.appId]: "ADMIN" },
- })
- let res = await request
- .post(`/api/users/metadata/sync/${user._id}`)
- .set(config.defaultHeaders())
- .send({
- previousUser: {
- ...user,
- roles: { ...user.roles, [app2.appId]: "BASIC" },
- },
- })
- .expect(200)
- .expect("Content-Type", /json/)
-
- expect(res.body.message).toEqual("User synced.")
- })
- })
})
diff --git a/packages/server/src/api/routes/user.ts b/packages/server/src/api/routes/user.ts
index 14deb111e6..24f33140a6 100644
--- a/packages/server/src/api/routes/user.ts
+++ b/packages/server/src/api/routes/user.ts
@@ -32,11 +32,6 @@ router
authorized(PermissionType.USER, PermissionLevel.WRITE),
controller.destroyMetadata
)
- .post(
- "/api/users/metadata/sync/:id",
- authorized(PermissionType.USER, PermissionLevel.WRITE),
- controller.syncUser
- )
.post(
"/api/users/flags",
authorized(PermissionType.USER, PermissionLevel.WRITE),
diff --git a/packages/server/src/app.ts b/packages/server/src/app.ts
index 230904f405..f8f82f9fdc 100644
--- a/packages/server/src/app.ts
+++ b/packages/server/src/app.ts
@@ -15,8 +15,8 @@ import * as api from "./api"
import * as automations from "./automations"
import { Thread } from "./threads"
import * as redis from "./utilities/redis"
+import { initialise as initialiseWebsockets } from "./websockets"
import { events, logging, middleware, timers } from "@budibase/backend-core"
-import { initialise as initialiseWebsockets } from "./websocket"
import { startup } from "./startup"
const Sentry = require("@sentry/node")
const destroyable = require("server-destroy")
@@ -61,7 +61,7 @@ if (env.isProd()) {
const server = http.createServer(app.callback())
destroyable(server)
-initialiseWebsockets(server)
+initialiseWebsockets(app, server)
let shuttingDown = false,
errCode = 0
diff --git a/packages/server/src/automations/actions.ts b/packages/server/src/automations/actions.ts
index 456399bc68..2a6b760725 100644
--- a/packages/server/src/automations/actions.ts
+++ b/packages/server/src/automations/actions.ts
@@ -15,7 +15,14 @@ import * as delay from "./steps/delay"
import * as queryRow from "./steps/queryRows"
import * as loop from "./steps/loop"
import env from "../environment"
-import { AutomationStepSchema, AutomationStepInput } from "@budibase/types"
+import {
+ AutomationStepSchema,
+ AutomationStepInput,
+ PluginType,
+ AutomationStep,
+} from "@budibase/types"
+import sdk from "../sdk"
+import { getAutomationPlugin } from "../utilities/fileSystem"
const ACTION_IMPLS: Record<
string,
@@ -38,25 +45,26 @@ const ACTION_IMPLS: Record<
zapier: zapier.run,
integromat: integromat.run,
}
-export const ACTION_DEFINITIONS: Record = {
- SEND_EMAIL_SMTP: sendSmtpEmail.definition,
- CREATE_ROW: createRow.definition,
- UPDATE_ROW: updateRow.definition,
- DELETE_ROW: deleteRow.definition,
- OUTGOING_WEBHOOK: outgoingWebhook.definition,
- EXECUTE_SCRIPT: executeScript.definition,
- EXECUTE_QUERY: executeQuery.definition,
- SERVER_LOG: serverLog.definition,
- DELAY: delay.definition,
- FILTER: filter.definition,
- QUERY_ROWS: queryRow.definition,
- LOOP: loop.definition,
- // these used to be lowercase step IDs, maintain for backwards compat
- discord: discord.definition,
- slack: slack.definition,
- zapier: zapier.definition,
- integromat: integromat.definition,
-}
+export const BUILTIN_ACTION_DEFINITIONS: Record =
+ {
+ SEND_EMAIL_SMTP: sendSmtpEmail.definition,
+ CREATE_ROW: createRow.definition,
+ UPDATE_ROW: updateRow.definition,
+ DELETE_ROW: deleteRow.definition,
+ OUTGOING_WEBHOOK: outgoingWebhook.definition,
+ EXECUTE_SCRIPT: executeScript.definition,
+ EXECUTE_QUERY: executeQuery.definition,
+ SERVER_LOG: serverLog.definition,
+ DELAY: delay.definition,
+ FILTER: filter.definition,
+ QUERY_ROWS: queryRow.definition,
+ LOOP: loop.definition,
+ // these used to be lowercase step IDs, maintain for backwards compat
+ discord: discord.definition,
+ slack: slack.definition,
+ zapier: zapier.definition,
+ integromat: integromat.definition,
+ }
// don't add the bash script/definitions unless in self host
// the fact this isn't included in any definitions means it cannot be
@@ -66,12 +74,36 @@ if (env.SELF_HOSTED) {
// @ts-ignore
ACTION_IMPLS["EXECUTE_BASH"] = bash.run
// @ts-ignore
- ACTION_DEFINITIONS["EXECUTE_BASH"] = bash.definition
+ BUILTIN_ACTION_DEFINITIONS["EXECUTE_BASH"] = bash.definition
+}
+
+export async function getActionDefinitions() {
+ const actionDefinitions = BUILTIN_ACTION_DEFINITIONS
+ if (env.SELF_HOSTED) {
+ const plugins = await sdk.plugins.fetch(PluginType.AUTOMATION)
+ for (let plugin of plugins) {
+ const schema = plugin.schema.schema as AutomationStep
+ actionDefinitions[schema.stepId] = {
+ ...schema,
+ custom: true,
+ }
+ }
+ }
+ return actionDefinitions
}
/* istanbul ignore next */
-export async function getAction(actionName: string) {
- if (ACTION_IMPLS[actionName] != null) {
- return ACTION_IMPLS[actionName]
+export async function getAction(stepId: string) {
+ if (ACTION_IMPLS[stepId] != null) {
+ return ACTION_IMPLS[stepId]
+ }
+ // must be a plugin
+ if (env.SELF_HOSTED) {
+ const plugins = await sdk.plugins.fetch(PluginType.AUTOMATION)
+ const found = plugins.find(plugin => plugin.schema.schema.stepId === stepId)
+ if (!found) {
+ throw new Error(`Unable to find action implementation for "${stepId}"`)
+ }
+ return (await getAutomationPlugin(found)).action
}
}
diff --git a/packages/server/src/automations/index.ts b/packages/server/src/automations/index.ts
index e46500d33e..9bbab95a27 100644
--- a/packages/server/src/automations/index.ts
+++ b/packages/server/src/automations/index.ts
@@ -6,7 +6,7 @@ import BullQueue from "bull"
export { automationQueue } from "./bullboard"
export { shutdown } from "./bullboard"
export { TRIGGER_DEFINITIONS } from "./triggers"
-export { ACTION_DEFINITIONS } from "./actions"
+export { BUILTIN_ACTION_DEFINITIONS, getActionDefinitions } from "./actions"
/**
* This module is built purely to kick off the worker farm and manage the inputs/outputs
diff --git a/packages/server/src/automations/steps/bash.ts b/packages/server/src/automations/steps/bash.ts
index e6deb8c38f..820d0329db 100644
--- a/packages/server/src/automations/steps/bash.ts
+++ b/packages/server/src/automations/steps/bash.ts
@@ -4,8 +4,11 @@ import * as automationUtils from "../automationUtils"
import environment from "../../environment"
import {
AutomationActionStepId,
- AutomationStepSchema,
+ AutomationCustomIOType,
+ AutomationIOType,
AutomationStepInput,
+ AutomationStepSchema,
+ AutomationStepType,
} from "@budibase/types"
export const definition: AutomationStepSchema = {
@@ -13,7 +16,7 @@ export const definition: AutomationStepSchema = {
tagline: "Execute a bash command",
icon: "JourneyEvent",
description: "Run a bash script",
- type: "ACTION",
+ type: AutomationStepType.ACTION,
internal: true,
stepId: AutomationActionStepId.EXECUTE_BASH,
inputs: {},
@@ -21,8 +24,8 @@ export const definition: AutomationStepSchema = {
inputs: {
properties: {
code: {
- type: "string",
- customType: "code",
+ type: AutomationIOType.STRING,
+ customType: AutomationCustomIOType.CODE,
title: "Code",
},
},
@@ -31,16 +34,16 @@ export const definition: AutomationStepSchema = {
outputs: {
properties: {
stdout: {
- type: "string",
+ type: AutomationIOType.STRING,
description: "Standard output of your bash command or script",
},
success: {
- type: "boolean",
+ type: AutomationIOType.BOOLEAN,
description: "Whether the command was successful",
},
},
+ required: ["stdout"],
},
- required: ["stdout"],
},
}
diff --git a/packages/server/src/automations/steps/createRow.ts b/packages/server/src/automations/steps/createRow.ts
index d529127360..dac929f1ee 100644
--- a/packages/server/src/automations/steps/createRow.ts
+++ b/packages/server/src/automations/steps/createRow.ts
@@ -3,8 +3,11 @@ import { cleanUpRow, getError } from "../automationUtils"
import { buildCtx } from "./utils"
import {
AutomationActionStepId,
- AutomationStepSchema,
+ AutomationCustomIOType,
+ AutomationIOType,
AutomationStepInput,
+ AutomationStepSchema,
+ AutomationStepType,
} from "@budibase/types"
export const definition: AutomationStepSchema = {
@@ -12,7 +15,7 @@ export const definition: AutomationStepSchema = {
tagline: "Create a {{inputs.enriched.table.name}} row",
icon: "TableRowAddBottom",
description: "Add a row to your database",
- type: "ACTION",
+ type: AutomationStepType.ACTION,
internal: true,
stepId: AutomationActionStepId.CREATE_ROW,
inputs: {},
@@ -20,14 +23,14 @@ export const definition: AutomationStepSchema = {
inputs: {
properties: {
row: {
- type: "object",
+ type: AutomationIOType.OBJECT,
properties: {
tableId: {
- type: "string",
- customType: "table",
+ type: AutomationIOType.STRING,
+ customType: AutomationCustomIOType.TABLE,
},
},
- customType: "row",
+ customType: AutomationCustomIOType.ROW,
title: "Table",
required: ["tableId"],
},
@@ -37,24 +40,24 @@ export const definition: AutomationStepSchema = {
outputs: {
properties: {
row: {
- type: "object",
- customType: "row",
+ type: AutomationIOType.OBJECT,
+ customType: AutomationCustomIOType.ROW,
description: "The new row",
},
response: {
- type: "object",
+ type: AutomationIOType.OBJECT,
description: "The response from the table",
},
success: {
- type: "boolean",
+ type: AutomationIOType.BOOLEAN,
description: "Whether the row creation was successful",
},
id: {
- type: "string",
+ type: AutomationIOType.STRING,
description: "The identifier of the new row",
},
revision: {
- type: "string",
+ type: AutomationIOType.STRING,
description: "The revision of the new row",
},
},
diff --git a/packages/server/src/automations/steps/delay.ts b/packages/server/src/automations/steps/delay.ts
index 58ca383ac1..9cf578805c 100644
--- a/packages/server/src/automations/steps/delay.ts
+++ b/packages/server/src/automations/steps/delay.ts
@@ -1,8 +1,10 @@
import { wait } from "../../utilities"
import {
AutomationActionStepId,
- AutomationStepSchema,
+ AutomationIOType,
AutomationStepInput,
+ AutomationStepSchema,
+ AutomationStepType,
} from "@budibase/types"
export const definition: AutomationStepSchema = {
@@ -17,7 +19,7 @@ export const definition: AutomationStepSchema = {
inputs: {
properties: {
time: {
- type: "number",
+ type: AutomationIOType.NUMBER,
title: "Delay in milliseconds",
},
},
@@ -26,14 +28,14 @@ export const definition: AutomationStepSchema = {
outputs: {
properties: {
success: {
- type: "boolean",
+ type: AutomationIOType.BOOLEAN,
description: "Whether the delay was successful",
},
},
required: ["success"],
},
},
- type: "LOGIC",
+ type: AutomationStepType.LOGIC,
}
export async function run({ inputs }: AutomationStepInput) {
diff --git a/packages/server/src/automations/steps/deleteRow.ts b/packages/server/src/automations/steps/deleteRow.ts
index 540d95b94d..86c7703491 100644
--- a/packages/server/src/automations/steps/deleteRow.ts
+++ b/packages/server/src/automations/steps/deleteRow.ts
@@ -3,8 +3,11 @@ import { buildCtx } from "./utils"
import { getError } from "../automationUtils"
import {
AutomationActionStepId,
- AutomationStepSchema,
AutomationStepInput,
+ AutomationStepSchema,
+ AutomationStepType,
+ AutomationIOType,
+ AutomationCustomIOType,
} from "@budibase/types"
export const definition: AutomationStepSchema = {
@@ -12,7 +15,7 @@ export const definition: AutomationStepSchema = {
icon: "TableRowRemoveCenter",
name: "Delete Row",
tagline: "Delete a {{inputs.enriched.table.name}} row",
- type: "ACTION",
+ type: AutomationStepType.ACTION,
stepId: AutomationActionStepId.DELETE_ROW,
internal: true,
inputs: {},
@@ -20,12 +23,12 @@ export const definition: AutomationStepSchema = {
inputs: {
properties: {
tableId: {
- type: "string",
- customType: "table",
+ type: AutomationIOType.STRING,
+ customType: AutomationCustomIOType.TABLE,
title: "Table",
},
id: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Row ID",
},
},
@@ -34,16 +37,16 @@ export const definition: AutomationStepSchema = {
outputs: {
properties: {
row: {
- type: "object",
- customType: "row",
+ type: AutomationIOType.OBJECT,
+ customType: AutomationCustomIOType.ROW,
description: "The deleted row",
},
response: {
- type: "object",
+ type: AutomationIOType.OBJECT,
description: "The response from the table",
},
success: {
- type: "boolean",
+ type: AutomationIOType.BOOLEAN,
description: "Whether the deletion was successful",
},
},
diff --git a/packages/server/src/automations/steps/discord.ts b/packages/server/src/automations/steps/discord.ts
index 5d7487ed3b..c46220c3b2 100644
--- a/packages/server/src/automations/steps/discord.ts
+++ b/packages/server/src/automations/steps/discord.ts
@@ -4,6 +4,8 @@ import {
AutomationActionStepId,
AutomationStepSchema,
AutomationStepInput,
+ AutomationStepType,
+ AutomationIOType,
} from "@budibase/types"
const DEFAULT_USERNAME = "Budibase Automate"
@@ -15,26 +17,26 @@ export const definition: AutomationStepSchema = {
description: "Send a message to a Discord server",
icon: "ri-discord-line",
stepId: AutomationActionStepId.discord,
- type: "ACTION",
+ type: AutomationStepType.ACTION,
internal: false,
inputs: {},
schema: {
inputs: {
properties: {
url: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Discord Webhook URL",
},
username: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Bot Name",
},
avatar_url: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Bot Avatar URL",
},
content: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Message",
},
},
@@ -43,15 +45,15 @@ export const definition: AutomationStepSchema = {
outputs: {
properties: {
httpStatus: {
- type: "number",
+ type: AutomationIOType.NUMBER,
description: "The HTTP status code of the request",
},
response: {
- type: "string",
+ type: AutomationIOType.STRING,
description: "The response from the Discord Webhook",
},
success: {
- type: "boolean",
+ type: AutomationIOType.BOOLEAN,
description: "Whether the message sent successfully",
},
},
diff --git a/packages/server/src/automations/steps/executeQuery.ts b/packages/server/src/automations/steps/executeQuery.ts
index 72fb69b96c..4969400552 100644
--- a/packages/server/src/automations/steps/executeQuery.ts
+++ b/packages/server/src/automations/steps/executeQuery.ts
@@ -3,8 +3,11 @@ import { buildCtx } from "./utils"
import * as automationUtils from "../automationUtils"
import {
AutomationActionStepId,
- AutomationStepSchema,
+ AutomationCustomIOType,
+ AutomationIOType,
AutomationStepInput,
+ AutomationStepSchema,
+ AutomationStepType,
} from "@budibase/types"
export const definition: AutomationStepSchema = {
@@ -12,7 +15,7 @@ export const definition: AutomationStepSchema = {
tagline: "Execute Data Connector",
icon: "Data",
description: "Execute a query in an external data connector",
- type: "ACTION",
+ type: AutomationStepType.ACTION,
stepId: AutomationActionStepId.EXECUTE_QUERY,
internal: true,
inputs: {},
@@ -20,14 +23,14 @@ export const definition: AutomationStepSchema = {
inputs: {
properties: {
query: {
- type: "object",
+ type: AutomationIOType.OBJECT,
properties: {
queryId: {
- type: "string",
- customType: "query",
+ type: AutomationIOType.STRING,
+ customType: AutomationCustomIOType.QUERY,
},
},
- customType: "queryParams",
+ customType: AutomationCustomIOType.QUERY_PARAMS,
title: "Parameters",
required: ["queryId"],
},
@@ -37,21 +40,21 @@ export const definition: AutomationStepSchema = {
outputs: {
properties: {
response: {
- type: "object",
+ type: AutomationIOType.OBJECT,
description: "The response from the datasource execution",
},
info: {
- type: "object",
+ type: AutomationIOType.OBJECT,
description:
"Some query types may return extra data, like headers from a REST query",
},
success: {
- type: "boolean",
+ type: AutomationIOType.BOOLEAN,
description: "Whether the action was successful",
},
},
+ required: ["response", "success"],
},
- required: ["response", "success"],
},
}
diff --git a/packages/server/src/automations/steps/executeScript.ts b/packages/server/src/automations/steps/executeScript.ts
index 84bdb0e2d5..9bd0eaed18 100644
--- a/packages/server/src/automations/steps/executeScript.ts
+++ b/packages/server/src/automations/steps/executeScript.ts
@@ -3,8 +3,11 @@ import { buildCtx } from "./utils"
import * as automationUtils from "../automationUtils"
import {
AutomationActionStepId,
- AutomationStepSchema,
+ AutomationCustomIOType,
+ AutomationIOType,
AutomationStepInput,
+ AutomationStepSchema,
+ AutomationStepType,
} from "@budibase/types"
export const definition: AutomationStepSchema = {
@@ -12,7 +15,7 @@ export const definition: AutomationStepSchema = {
tagline: "Execute JavaScript Code",
icon: "Code",
description: "Run a piece of JavaScript code in your automation",
- type: "ACTION",
+ type: AutomationStepType.ACTION,
internal: true,
stepId: AutomationActionStepId.EXECUTE_SCRIPT,
inputs: {},
@@ -20,8 +23,8 @@ export const definition: AutomationStepSchema = {
inputs: {
properties: {
code: {
- type: "string",
- customType: "code",
+ type: AutomationIOType.STRING,
+ customType: AutomationCustomIOType.CODE,
title: "Code",
},
},
@@ -30,16 +33,16 @@ export const definition: AutomationStepSchema = {
outputs: {
properties: {
value: {
- type: "string",
+ type: AutomationIOType.STRING,
description: "The result of the return statement",
},
success: {
- type: "boolean",
+ type: AutomationIOType.BOOLEAN,
description: "Whether the action was successful",
},
},
+ required: ["success"],
},
- required: ["success"],
},
}
diff --git a/packages/server/src/automations/steps/filter.ts b/packages/server/src/automations/steps/filter.ts
index 18914ddca6..c7ab4210ec 100644
--- a/packages/server/src/automations/steps/filter.ts
+++ b/packages/server/src/automations/steps/filter.ts
@@ -2,6 +2,8 @@ import {
AutomationActionStepId,
AutomationStepSchema,
AutomationStepInput,
+ AutomationStepType,
+ AutomationIOType,
} from "@budibase/types"
export const FilterConditions = {
@@ -24,7 +26,7 @@ export const definition: AutomationStepSchema = {
icon: "Branch2",
description:
"Conditionally halt automations which do not meet certain conditions",
- type: "LOGIC",
+ type: AutomationStepType.LOGIC,
internal: true,
stepId: AutomationActionStepId.FILTER,
inputs: {
@@ -34,17 +36,17 @@ export const definition: AutomationStepSchema = {
inputs: {
properties: {
field: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Reference Value",
},
condition: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Condition",
enum: Object.values(FilterConditions),
pretty: Object.values(PrettyFilterConditions),
},
value: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Comparison Value",
},
},
@@ -53,11 +55,11 @@ export const definition: AutomationStepSchema = {
outputs: {
properties: {
success: {
- type: "boolean",
+ type: AutomationIOType.BOOLEAN,
description: "Whether the action was successful",
},
result: {
- type: "boolean",
+ type: AutomationIOType.BOOLEAN,
description: "Whether the logic block passed",
},
},
diff --git a/packages/server/src/automations/steps/integromat.ts b/packages/server/src/automations/steps/integromat.ts
index 811c0a3d91..d7c78a6dd8 100644
--- a/packages/server/src/automations/steps/integromat.ts
+++ b/packages/server/src/automations/steps/integromat.ts
@@ -4,6 +4,8 @@ import {
AutomationActionStepId,
AutomationStepSchema,
AutomationStepInput,
+ AutomationStepType,
+ AutomationIOType,
} from "@budibase/types"
export const definition: AutomationStepSchema = {
@@ -13,34 +15,34 @@ export const definition: AutomationStepSchema = {
"Performs a webhook call to Integromat and gets the response (if configured)",
icon: "ri-shut-down-line",
stepId: AutomationActionStepId.integromat,
- type: "ACTION",
+ type: AutomationStepType.ACTION,
internal: false,
inputs: {},
schema: {
inputs: {
properties: {
url: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Webhook URL",
},
value1: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Input Value 1",
},
value2: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Input Value 2",
},
value3: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Input Value 3",
},
value4: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Input Value 4",
},
value5: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Input Value 5",
},
},
@@ -49,15 +51,15 @@ export const definition: AutomationStepSchema = {
outputs: {
properties: {
success: {
- type: "boolean",
+ type: AutomationIOType.BOOLEAN,
description: "Whether call was successful",
},
httpStatus: {
- type: "number",
+ type: AutomationIOType.NUMBER,
description: "The HTTP status code returned",
},
response: {
- type: "object",
+ type: AutomationIOType.OBJECT,
description: "The webhook response - this can have properties",
},
},
diff --git a/packages/server/src/automations/steps/loop.ts b/packages/server/src/automations/steps/loop.ts
index 72087ae357..fdbec83f2e 100644
--- a/packages/server/src/automations/steps/loop.ts
+++ b/packages/server/src/automations/steps/loop.ts
@@ -1,4 +1,10 @@
-import { AutomationActionStepId, AutomationStepSchema } from "@budibase/types"
+import {
+ AutomationActionStepId,
+ AutomationCustomIOType,
+ AutomationIOType,
+ AutomationStepSchema,
+ AutomationStepType,
+} from "@budibase/types"
export const definition: AutomationStepSchema = {
name: "Looping",
@@ -12,19 +18,19 @@ export const definition: AutomationStepSchema = {
inputs: {
properties: {
option: {
- customType: "loopOption",
+ customType: AutomationCustomIOType.LOOP_OPTION,
title: "Input type",
},
binding: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Binding / Value",
},
iterations: {
- type: "number",
+ type: AutomationIOType.NUMBER,
title: "Max loop iterations",
},
failure: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Failure Condition",
},
},
@@ -33,20 +39,20 @@ export const definition: AutomationStepSchema = {
outputs: {
properties: {
items: {
- customType: "item",
+ customType: AutomationCustomIOType.ITEM,
description: "The item currently being executed",
},
success: {
- type: "boolean",
+ type: AutomationIOType.BOOLEAN,
description: "Whether the message loop was successfully",
},
iterations: {
- type: "number",
- descriptions: "The amount of times the block ran",
+ type: AutomationIOType.NUMBER,
+ description: "The amount of times the block ran",
},
},
required: ["success", "items", "iterations"],
},
},
- type: "LOGIC",
+ type: AutomationStepType.LOGIC,
}
diff --git a/packages/server/src/automations/steps/outgoingWebhook.ts b/packages/server/src/automations/steps/outgoingWebhook.ts
index ea1ffeb339..f174935195 100644
--- a/packages/server/src/automations/steps/outgoingWebhook.ts
+++ b/packages/server/src/automations/steps/outgoingWebhook.ts
@@ -3,8 +3,11 @@ import { getFetchResponse } from "./utils"
import * as automationUtils from "../automationUtils"
import {
AutomationActionStepId,
- AutomationStepSchema,
+ AutomationCustomIOType,
+ AutomationIOType,
AutomationStepInput,
+ AutomationStepSchema,
+ AutomationStepType,
} from "@budibase/types"
enum RequestType {
@@ -27,7 +30,7 @@ export const definition: AutomationStepSchema = {
tagline: "Send a {{inputs.requestMethod}} request",
icon: "Send",
description: "Send a request of specified method to a URL",
- type: "ACTION",
+ type: AutomationStepType.ACTION,
internal: true,
stepId: AutomationActionStepId.OUTGOING_WEBHOOK,
inputs: {
@@ -40,23 +43,23 @@ export const definition: AutomationStepSchema = {
inputs: {
properties: {
requestMethod: {
- type: "string",
+ type: AutomationIOType.STRING,
enum: Object.values(RequestType),
title: "Request method",
},
url: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "URL",
},
requestBody: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "JSON Body",
- customType: "wide",
+ customType: AutomationCustomIOType.WIDE,
},
headers: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Headers",
- customType: "wide",
+ customType: AutomationCustomIOType.WIDE,
},
},
required: ["requestMethod", "url"],
@@ -64,15 +67,15 @@ export const definition: AutomationStepSchema = {
outputs: {
properties: {
response: {
- type: "object",
+ type: AutomationIOType.OBJECT,
description: "The response from the webhook",
},
httpStatus: {
- type: "number",
+ type: AutomationIOType.NUMBER,
description: "The HTTP status code returned",
},
success: {
- type: "boolean",
+ type: AutomationIOType.BOOLEAN,
description: "Whether the action was successful",
},
},
diff --git a/packages/server/src/automations/steps/queryRows.ts b/packages/server/src/automations/steps/queryRows.ts
index 6de518e931..1d7e8a419e 100644
--- a/packages/server/src/automations/steps/queryRows.ts
+++ b/packages/server/src/automations/steps/queryRows.ts
@@ -5,8 +5,11 @@ import { buildCtx } from "./utils"
import * as automationUtils from "../automationUtils"
import {
AutomationActionStepId,
- AutomationStepSchema,
+ AutomationCustomIOType,
+ AutomationIOType,
AutomationStepInput,
+ AutomationStepSchema,
+ AutomationStepType,
SearchFilters,
Table,
} from "@budibase/types"
@@ -36,7 +39,7 @@ export const definition: AutomationStepSchema = {
icon: "Search",
name: "Query rows",
tagline: "Query rows from {{inputs.enriched.table.name}} table",
- type: "ACTION",
+ type: AutomationStepType.ACTION,
stepId: AutomationActionStepId.QUERY_ROWS,
internal: true,
inputs: {},
@@ -44,35 +47,35 @@ export const definition: AutomationStepSchema = {
inputs: {
properties: {
tableId: {
- type: "string",
- customType: "table",
+ type: AutomationIOType.STRING,
+ customType: AutomationCustomIOType.TABLE,
title: "Table",
},
filters: {
- type: "object",
- customType: "filters",
+ type: AutomationIOType.OBJECT,
+ customType: AutomationCustomIOType.FILTERS,
title: "Filtering",
},
sortColumn: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Sort Column",
- customType: "column",
+ customType: AutomationCustomIOType.COLUMN,
},
sortOrder: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Sort Order",
enum: Object.values(SortOrder),
pretty: Object.values(SortOrderPretty),
},
limit: {
- type: "number",
+ type: AutomationIOType.NUMBER,
title: "Limit",
- customType: "queryLimit",
+ customType: AutomationCustomIOType.QUERY_LIMIT,
},
onEmptyFilter: {
pretty: Object.values(EmptyFilterOptionPretty),
enum: Object.values(EmptyFilterOption),
- type: "string",
+ type: AutomationIOType.STRING,
title: "When Filter Empty",
},
},
@@ -81,12 +84,12 @@ export const definition: AutomationStepSchema = {
outputs: {
properties: {
rows: {
- type: "array",
- customType: "rows",
+ type: AutomationIOType.ARRAY,
+ customType: AutomationCustomIOType.ROWS,
description: "The rows that were found",
},
success: {
- type: "boolean",
+ type: AutomationIOType.BOOLEAN,
description: "Whether the query was successful",
},
},
diff --git a/packages/server/src/automations/steps/sendSmtpEmail.ts b/packages/server/src/automations/steps/sendSmtpEmail.ts
index 67516c803d..c4af9aeaa9 100644
--- a/packages/server/src/automations/steps/sendSmtpEmail.ts
+++ b/packages/server/src/automations/steps/sendSmtpEmail.ts
@@ -4,6 +4,8 @@ import {
AutomationActionStepId,
AutomationStepSchema,
AutomationStepInput,
+ AutomationStepType,
+ AutomationIOType,
} from "@budibase/types"
export const definition: AutomationStepSchema = {
@@ -11,7 +13,7 @@ export const definition: AutomationStepSchema = {
tagline: "Send SMTP email to {{inputs.to}}",
icon: "Email",
name: "Send Email (SMTP)",
- type: "ACTION",
+ type: AutomationStepType.ACTION,
internal: true,
stepId: AutomationActionStepId.SEND_EMAIL_SMTP,
inputs: {},
@@ -19,27 +21,27 @@ export const definition: AutomationStepSchema = {
inputs: {
properties: {
to: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Send To",
},
from: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Send From",
},
cc: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "CC",
},
bcc: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "BCC",
},
subject: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Email Subject",
},
contents: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "HTML Contents",
},
},
@@ -48,11 +50,11 @@ export const definition: AutomationStepSchema = {
outputs: {
properties: {
success: {
- type: "boolean",
+ type: AutomationIOType.BOOLEAN,
description: "Whether the email was sent",
},
response: {
- type: "object",
+ type: AutomationIOType.OBJECT,
description: "A response from the email client, this may be an error",
},
},
diff --git a/packages/server/src/automations/steps/serverLog.ts b/packages/server/src/automations/steps/serverLog.ts
index bb2f49ede8..382f7d4efc 100644
--- a/packages/server/src/automations/steps/serverLog.ts
+++ b/packages/server/src/automations/steps/serverLog.ts
@@ -2,6 +2,8 @@ import {
AutomationActionStepId,
AutomationStepSchema,
AutomationStepInput,
+ AutomationStepType,
+ AutomationIOType,
} from "@budibase/types"
/**
@@ -15,7 +17,7 @@ export const definition: AutomationStepSchema = {
tagline: "Console log a value in the backend",
icon: "Monitoring",
description: "Logs the given text to the server (using console.log)",
- type: "ACTION",
+ type: AutomationStepType.ACTION,
internal: true,
stepId: AutomationActionStepId.SERVER_LOG,
inputs: {
@@ -25,7 +27,7 @@ export const definition: AutomationStepSchema = {
inputs: {
properties: {
text: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Log",
},
},
@@ -34,11 +36,11 @@ export const definition: AutomationStepSchema = {
outputs: {
properties: {
success: {
- type: "boolean",
+ type: AutomationIOType.BOOLEAN,
description: "Whether the action was successful",
},
message: {
- type: "string",
+ type: AutomationIOType.STRING,
description: "What was output",
},
},
diff --git a/packages/server/src/automations/steps/slack.ts b/packages/server/src/automations/steps/slack.ts
index 0c9320a699..21ee6ed742 100644
--- a/packages/server/src/automations/steps/slack.ts
+++ b/packages/server/src/automations/steps/slack.ts
@@ -4,6 +4,8 @@ import {
AutomationActionStepId,
AutomationStepSchema,
AutomationStepInput,
+ AutomationStepType,
+ AutomationIOType,
} from "@budibase/types"
export const definition: AutomationStepSchema = {
@@ -12,18 +14,18 @@ export const definition: AutomationStepSchema = {
description: "Send a message to Slack",
icon: "ri-slack-line",
stepId: AutomationActionStepId.slack,
- type: "ACTION",
+ type: AutomationStepType.ACTION,
internal: false,
inputs: {},
schema: {
inputs: {
properties: {
url: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Incoming Webhook URL",
},
text: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Message",
},
},
@@ -32,15 +34,15 @@ export const definition: AutomationStepSchema = {
outputs: {
properties: {
httpStatus: {
- type: "number",
+ type: AutomationIOType.NUMBER,
description: "The HTTP status code of the request",
},
success: {
- type: "boolean",
+ type: AutomationIOType.BOOLEAN,
description: "Whether the message sent successfully",
},
response: {
- type: "string",
+ type: AutomationIOType.STRING,
description: "The response from the Slack Webhook",
},
},
diff --git a/packages/server/src/automations/steps/updateRow.ts b/packages/server/src/automations/steps/updateRow.ts
index 9e905a0706..96ab1d1642 100644
--- a/packages/server/src/automations/steps/updateRow.ts
+++ b/packages/server/src/automations/steps/updateRow.ts
@@ -3,8 +3,11 @@ import * as automationUtils from "../automationUtils"
import { buildCtx } from "./utils"
import {
AutomationActionStepId,
- AutomationStepSchema,
+ AutomationCustomIOType,
+ AutomationIOType,
AutomationStepInput,
+ AutomationStepSchema,
+ AutomationStepType,
} from "@budibase/types"
export const definition: AutomationStepSchema = {
@@ -12,7 +15,7 @@ export const definition: AutomationStepSchema = {
tagline: "Update a {{inputs.enriched.table.name}} row",
icon: "Refresh",
description: "Update a row in your database",
- type: "ACTION",
+ type: AutomationStepType.ACTION,
internal: true,
stepId: AutomationActionStepId.UPDATE_ROW,
inputs: {},
@@ -20,16 +23,16 @@ export const definition: AutomationStepSchema = {
inputs: {
properties: {
meta: {
- type: "object",
+ type: AutomationIOType.OBJECT,
title: "Field settings",
},
row: {
- type: "object",
- customType: "row",
+ type: AutomationIOType.OBJECT,
+ customType: AutomationCustomIOType.ROW,
title: "Table",
},
rowId: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Row ID",
},
},
@@ -38,24 +41,24 @@ export const definition: AutomationStepSchema = {
outputs: {
properties: {
row: {
- type: "object",
- customType: "row",
+ type: AutomationIOType.OBJECT,
+ customType: AutomationCustomIOType.ROW,
description: "The updated row",
},
response: {
- type: "object",
+ type: AutomationIOType.OBJECT,
description: "The response from the table",
},
success: {
- type: "boolean",
+ type: AutomationIOType.BOOLEAN,
description: "Whether the action was successful",
},
id: {
- type: "string",
+ type: AutomationIOType.STRING,
description: "The identifier of the updated row",
},
revision: {
- type: "string",
+ type: AutomationIOType.STRING,
description: "The revision of the updated row",
},
},
diff --git a/packages/server/src/automations/steps/zapier.ts b/packages/server/src/automations/steps/zapier.ts
index 90068e685d..75a21deaae 100644
--- a/packages/server/src/automations/steps/zapier.ts
+++ b/packages/server/src/automations/steps/zapier.ts
@@ -4,12 +4,14 @@ import {
AutomationActionStepId,
AutomationStepSchema,
AutomationStepInput,
+ AutomationStepType,
+ AutomationIOType,
} from "@budibase/types"
export const definition: AutomationStepSchema = {
name: "Zapier Webhook",
stepId: AutomationActionStepId.zapier,
- type: "ACTION",
+ type: AutomationStepType.ACTION,
internal: false,
description: "Trigger a Zapier Zap via webhooks",
tagline: "Trigger a Zapier Zap",
@@ -19,27 +21,27 @@ export const definition: AutomationStepSchema = {
inputs: {
properties: {
url: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Webhook URL",
},
value1: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Payload Value 1",
},
value2: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Payload Value 2",
},
value3: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Payload Value 3",
},
value4: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Payload Value 4",
},
value5: {
- type: "string",
+ type: AutomationIOType.STRING,
title: "Payload Value 5",
},
},
@@ -48,11 +50,11 @@ export const definition: AutomationStepSchema = {
outputs: {
properties: {
httpStatus: {
- type: "number",
+ type: AutomationIOType.NUMBER,
description: "The HTTP status code of the request",
},
response: {
- type: "string",
+ type: AutomationIOType.STRING,
description: "The response from Zapier",
},
},
diff --git a/packages/server/src/automations/tests/utilities/index.ts b/packages/server/src/automations/tests/utilities/index.ts
index 3c990a38d0..9ba4f950f3 100644
--- a/packages/server/src/automations/tests/utilities/index.ts
+++ b/packages/server/src/automations/tests/utilities/index.ts
@@ -1,6 +1,6 @@
import TestConfig from "../../../tests/utilities/TestConfiguration"
import { context } from "@budibase/backend-core"
-import { ACTION_DEFINITIONS, getAction } from "../../actions"
+import { BUILTIN_ACTION_DEFINITIONS, getAction } from "../../actions"
import emitter from "../../../events/index"
import env from "../../../environment"
@@ -57,4 +57,4 @@ export async function runStep(stepId: string, inputs: any, stepContext?: any) {
}
export const apiKey = "test"
-export const actions = ACTION_DEFINITIONS
+export const actions = BUILTIN_ACTION_DEFINITIONS
diff --git a/packages/server/src/automations/triggerInfo/app.ts b/packages/server/src/automations/triggerInfo/app.ts
index fca9acaef8..abc1463f1a 100644
--- a/packages/server/src/automations/triggerInfo/app.ts
+++ b/packages/server/src/automations/triggerInfo/app.ts
@@ -1,4 +1,7 @@
import {
+ AutomationCustomIOType,
+ AutomationIOType,
+ AutomationStepType,
AutomationTriggerSchema,
AutomationTriggerStepId,
} from "@budibase/types"
@@ -15,8 +18,8 @@ export const definition: AutomationTriggerSchema = {
inputs: {
properties: {
fields: {
- type: "object",
- customType: "triggerSchema",
+ type: AutomationIOType.OBJECT,
+ customType: AutomationCustomIOType.TRIGGER_SCHEMA,
title: "Fields",
},
},
@@ -25,13 +28,13 @@ export const definition: AutomationTriggerSchema = {
outputs: {
properties: {
fields: {
- type: "object",
+ type: AutomationIOType.OBJECT,
description: "Fields submitted from the app frontend",
- customType: "triggerSchema",
+ customType: AutomationCustomIOType.TRIGGER_SCHEMA,
},
},
required: ["fields"],
},
},
- type: "TRIGGER",
+ type: AutomationStepType.TRIGGER,
}
diff --git a/packages/server/src/automations/triggerInfo/cron.ts b/packages/server/src/automations/triggerInfo/cron.ts
index 91b41f7243..1c47aeaeec 100644
--- a/packages/server/src/automations/triggerInfo/cron.ts
+++ b/packages/server/src/automations/triggerInfo/cron.ts
@@ -1,4 +1,7 @@
import {
+ AutomationCustomIOType,
+ AutomationIOType,
+ AutomationStepType,
AutomationTriggerSchema,
AutomationTriggerStepId,
} from "@budibase/types"
@@ -15,8 +18,8 @@ export const definition: AutomationTriggerSchema = {
inputs: {
properties: {
cron: {
- type: "string",
- customType: "cron",
+ type: AutomationIOType.STRING,
+ customType: AutomationCustomIOType.CRON,
title: "Expression",
},
},
@@ -25,12 +28,12 @@ export const definition: AutomationTriggerSchema = {
outputs: {
properties: {
timestamp: {
- type: "number",
+ type: AutomationIOType.NUMBER,
description: "Timestamp the cron was executed",
},
},
required: ["timestamp"],
},
},
- type: "TRIGGER",
+ type: AutomationStepType.TRIGGER,
}
diff --git a/packages/server/src/automations/triggerInfo/rowDeleted.ts b/packages/server/src/automations/triggerInfo/rowDeleted.ts
index de4a1b0412..e1014f3dbc 100644
--- a/packages/server/src/automations/triggerInfo/rowDeleted.ts
+++ b/packages/server/src/automations/triggerInfo/rowDeleted.ts
@@ -1,4 +1,7 @@
import {
+ AutomationCustomIOType,
+ AutomationIOType,
+ AutomationStepType,
AutomationTriggerSchema,
AutomationTriggerStepId,
} from "@budibase/types"
@@ -15,8 +18,8 @@ export const definition: AutomationTriggerSchema = {
inputs: {
properties: {
tableId: {
- type: "string",
- customType: "table",
+ type: AutomationIOType.STRING,
+ customType: AutomationCustomIOType.TABLE,
title: "Table",
},
},
@@ -25,13 +28,13 @@ export const definition: AutomationTriggerSchema = {
outputs: {
properties: {
row: {
- type: "object",
- customType: "row",
+ type: AutomationIOType.OBJECT,
+ customType: AutomationCustomIOType.ROW,
description: "The row that was deleted",
},
},
required: ["row"],
},
},
- type: "TRIGGER",
+ type: AutomationStepType.TRIGGER,
}
diff --git a/packages/server/src/automations/triggerInfo/rowSaved.ts b/packages/server/src/automations/triggerInfo/rowSaved.ts
index c1dde25eef..faa32ef96e 100644
--- a/packages/server/src/automations/triggerInfo/rowSaved.ts
+++ b/packages/server/src/automations/triggerInfo/rowSaved.ts
@@ -1,4 +1,7 @@
import {
+ AutomationCustomIOType,
+ AutomationIOType,
+ AutomationStepType,
AutomationTriggerSchema,
AutomationTriggerStepId,
} from "@budibase/types"
@@ -15,8 +18,8 @@ export const definition: AutomationTriggerSchema = {
inputs: {
properties: {
tableId: {
- type: "string",
- customType: "table",
+ type: AutomationIOType.STRING,
+ customType: AutomationCustomIOType.TABLE,
title: "Table",
},
},
@@ -25,21 +28,21 @@ export const definition: AutomationTriggerSchema = {
outputs: {
properties: {
row: {
- type: "object",
- customType: "row",
+ type: AutomationIOType.OBJECT,
+ customType: AutomationCustomIOType.ROW,
description: "The new row that was created",
},
id: {
- type: "string",
+ type: AutomationIOType.STRING,
description: "Row ID - can be used for updating",
},
revision: {
- type: "string",
+ type: AutomationIOType.STRING,
description: "Revision of row",
},
},
required: ["row", "id"],
},
},
- type: "TRIGGER",
+ type: AutomationStepType.TRIGGER,
}
diff --git a/packages/server/src/automations/triggerInfo/rowUpdated.ts b/packages/server/src/automations/triggerInfo/rowUpdated.ts
index 1bc8811d54..5e60015808 100644
--- a/packages/server/src/automations/triggerInfo/rowUpdated.ts
+++ b/packages/server/src/automations/triggerInfo/rowUpdated.ts
@@ -1,4 +1,7 @@
import {
+ AutomationCustomIOType,
+ AutomationIOType,
+ AutomationStepType,
AutomationTriggerSchema,
AutomationTriggerStepId,
} from "@budibase/types"
@@ -15,8 +18,8 @@ export const definition: AutomationTriggerSchema = {
inputs: {
properties: {
tableId: {
- type: "string",
- customType: "table",
+ type: AutomationIOType.STRING,
+ customType: AutomationCustomIOType.TABLE,
title: "Table",
},
},
@@ -25,21 +28,21 @@ export const definition: AutomationTriggerSchema = {
outputs: {
properties: {
row: {
- type: "object",
- customType: "row",
+ type: AutomationIOType.OBJECT,
+ customType: AutomationCustomIOType.ROW,
description: "The row that was updated",
},
id: {
- type: "string",
+ type: AutomationIOType.STRING,
description: "Row ID - can be used for updating",
},
revision: {
- type: "string",
+ type: AutomationIOType.STRING,
description: "Revision of row",
},
},
required: ["row", "id"],
},
},
- type: "TRIGGER",
+ type: AutomationStepType.TRIGGER,
}
diff --git a/packages/server/src/automations/triggerInfo/webhook.ts b/packages/server/src/automations/triggerInfo/webhook.ts
index 906967a02a..310dd66b41 100644
--- a/packages/server/src/automations/triggerInfo/webhook.ts
+++ b/packages/server/src/automations/triggerInfo/webhook.ts
@@ -1,4 +1,7 @@
import {
+ AutomationCustomIOType,
+ AutomationIOType,
+ AutomationStepType,
AutomationTriggerSchema,
AutomationTriggerStepId,
} from "@budibase/types"
@@ -15,13 +18,13 @@ export const definition: AutomationTriggerSchema = {
inputs: {
properties: {
schemaUrl: {
- type: "string",
- customType: "webhookUrl",
+ type: AutomationIOType.STRING,
+ customType: AutomationCustomIOType.WEBHOOK_URL,
title: "Schema URL",
},
triggerUrl: {
- type: "string",
- customType: "webhookUrl",
+ type: AutomationIOType.STRING,
+ customType: AutomationCustomIOType.WEBHOOK_URL,
title: "Trigger URL",
},
},
@@ -30,12 +33,12 @@ export const definition: AutomationTriggerSchema = {
outputs: {
properties: {
body: {
- type: "object",
+ type: AutomationIOType.OBJECT,
description: "Body of the request which hit the webhook",
},
},
required: ["body"],
},
},
- type: "TRIGGER",
+ type: AutomationStepType.TRIGGER,
}
diff --git a/packages/server/src/events/docUpdates/index.ts b/packages/server/src/events/docUpdates/index.ts
new file mode 100644
index 0000000000..fa7a623108
--- /dev/null
+++ b/packages/server/src/events/docUpdates/index.ts
@@ -0,0 +1 @@
+export * from "./processors"
diff --git a/packages/server/src/events/docUpdates/processors.ts b/packages/server/src/events/docUpdates/processors.ts
new file mode 100644
index 0000000000..53036970e0
--- /dev/null
+++ b/packages/server/src/events/docUpdates/processors.ts
@@ -0,0 +1,14 @@
+import userGroupProcessor from "./syncUsers"
+import { docUpdates } from "@budibase/backend-core"
+
+export type UpdateCallback = (docId: string) => void
+let started = false
+
+export function init(updateCb?: UpdateCallback) {
+ if (started) {
+ return
+ }
+ const processors = [userGroupProcessor(updateCb)]
+ docUpdates.init(processors)
+ started = true
+}
diff --git a/packages/server/src/events/docUpdates/syncUsers.ts b/packages/server/src/events/docUpdates/syncUsers.ts
new file mode 100644
index 0000000000..7957178168
--- /dev/null
+++ b/packages/server/src/events/docUpdates/syncUsers.ts
@@ -0,0 +1,35 @@
+import { constants, logging } from "@budibase/backend-core"
+import { sdk as proSdk } from "@budibase/pro"
+import { DocUpdateEvent, UserGroupSyncEvents } from "@budibase/types"
+import { syncUsersToAllApps } from "../../sdk/app/applications/sync"
+import { UpdateCallback } from "./processors"
+
+export default function process(updateCb?: UpdateCallback) {
+ const processor = async (update: DocUpdateEvent) => {
+ try {
+ const docId = update.id
+ const isGroup = docId.startsWith(constants.DocumentType.GROUP)
+ let userIds: string[]
+ if (isGroup) {
+ const group = await proSdk.groups.get(docId)
+ userIds = group.users?.map(user => user._id) || []
+ } else {
+ userIds = [docId]
+ }
+ if (userIds.length > 0) {
+ await syncUsersToAllApps(userIds)
+ }
+ if (updateCb) {
+ updateCb(docId)
+ }
+ } catch (err: any) {
+ // if something not found - no changes to perform
+ if (err?.status === 404) {
+ return
+ } else {
+ logging.logAlert("Failed to perform user/group app sync", err)
+ }
+ }
+ }
+ return { events: UserGroupSyncEvents, processor }
+}
diff --git a/packages/server/src/events/index.ts b/packages/server/src/events/index.ts
index fad7bcaa9a..23c3f3e512 100644
--- a/packages/server/src/events/index.ts
+++ b/packages/server/src/events/index.ts
@@ -2,4 +2,5 @@ import BudibaseEmitter from "./BudibaseEmitter"
const emitter = new BudibaseEmitter()
+export { init } from "./docUpdates"
export default emitter
diff --git a/packages/server/src/integration-test/postgres.spec.ts b/packages/server/src/integration-test/postgres.spec.ts
index fab1fcf618..36285e2831 100644
--- a/packages/server/src/integration-test/postgres.spec.ts
+++ b/packages/server/src/integration-test/postgres.spec.ts
@@ -921,6 +921,7 @@ describe("row api - postgres", () => {
[m2mFieldName]: [
{
_id: row._id,
+ primaryDisplay: "Invalid display column",
},
],
})
@@ -929,6 +930,7 @@ describe("row api - postgres", () => {
[m2mFieldName]: [
{
_id: row._id,
+ primaryDisplay: "Invalid display column",
},
],
})
diff --git a/packages/server/src/integrations/base/sqlTable.ts b/packages/server/src/integrations/base/sqlTable.ts
index 58b2b7a3d1..3bdfb76c02 100644
--- a/packages/server/src/integrations/base/sqlTable.ts
+++ b/packages/server/src/integrations/base/sqlTable.ts
@@ -79,10 +79,17 @@ function generateSchema(
if (!relatedTable) {
throw "Referenced table doesn't exist"
}
- schema.integer(column.foreignKey).unsigned()
+ const relatedPrimary = relatedTable.primary[0]
+ const externalType = relatedTable.schema[relatedPrimary].externalType
+ if (externalType) {
+ schema.specificType(column.foreignKey, externalType)
+ } else {
+ schema.integer(column.foreignKey).unsigned()
+ }
+
schema
.foreign(column.foreignKey)
- .references(`${tableName}.${relatedTable.primary[0]}`)
+ .references(`${tableName}.${relatedPrimary}`)
}
break
}
diff --git a/packages/server/src/integrations/index.ts b/packages/server/src/integrations/index.ts
index edbce6db0a..f3285e441f 100644
--- a/packages/server/src/integrations/index.ts
+++ b/packages/server/src/integrations/index.ts
@@ -14,11 +14,11 @@ import firebase from "./firebase"
import redis from "./redis"
import snowflake from "./snowflake"
import oracle from "./oracle"
-import { getPlugins } from "../api/controllers/plugin"
import { SourceName, Integration, PluginType } from "@budibase/types"
import { getDatasourcePlugin } from "../utilities/fileSystem"
import env from "../environment"
import { cloneDeep } from "lodash"
+import sdk from "../sdk"
const DEFINITIONS: { [key: string]: Integration } = {
[SourceName.POSTGRES]: postgres.schema,
@@ -79,7 +79,7 @@ export async function getDefinition(source: SourceName): Promise {
export async function getDefinitions() {
const pluginSchemas: { [key: string]: Integration } = {}
if (env.SELF_HOSTED) {
- const plugins = await getPlugins(PluginType.DATASOURCE)
+ const plugins = await sdk.plugins.fetch(PluginType.DATASOURCE)
// extract the actual schema from each custom
for (let plugin of plugins) {
const sourceId = plugin.name
@@ -103,7 +103,7 @@ export async function getIntegration(integration: string) {
return INTEGRATIONS[integration]
}
if (env.SELF_HOSTED) {
- const plugins = await getPlugins(PluginType.DATASOURCE)
+ const plugins = await sdk.plugins.fetch(PluginType.DATASOURCE)
for (let plugin of plugins) {
if (plugin.name === integration) {
// need to use commonJS require due to its dynamic runtime nature
diff --git a/packages/server/src/integrations/microsoftSqlServer.ts b/packages/server/src/integrations/microsoftSqlServer.ts
index 2da190df5e..eb87c1ccf1 100644
--- a/packages/server/src/integrations/microsoftSqlServer.ts
+++ b/packages/server/src/integrations/microsoftSqlServer.ts
@@ -243,11 +243,14 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
if (typeof name !== "string") {
continue
}
+ const hasDefault = def.COLUMN_DEFAULT
+ const isAuto = !!autoColumns.find(col => col === name)
+ const required = !!requiredColumns.find(col => col === name)
schema[name] = {
- autocolumn: !!autoColumns.find(col => col === name),
+ autocolumn: isAuto,
name: name,
constraints: {
- presence: requiredColumns.find(col => col === name),
+ presence: required && !isAuto && !hasDefault,
},
...convertSqlType(def.DATA_TYPE),
externalType: def.DATA_TYPE,
diff --git a/packages/server/src/integrations/mysql.ts b/packages/server/src/integrations/mysql.ts
index 080939593f..8d984ed402 100644
--- a/packages/server/src/integrations/mysql.ts
+++ b/packages/server/src/integrations/mysql.ts
@@ -229,13 +229,15 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
if (column.Key === "PRI" && primaryKeys.indexOf(column.Key) === -1) {
primaryKeys.push(columnName)
}
- const constraints = {
- presence: column.Null !== "YES",
- }
+ const hasDefault = column.Default != null
const isAuto: boolean =
typeof column.Extra === "string" &&
(column.Extra === "auto_increment" ||
column.Extra.toLowerCase().includes("generated"))
+ const required = column.Null !== "YES"
+ const constraints = {
+ presence: required && !isAuto && !hasDefault,
+ }
schema[columnName] = {
name: columnName,
autocolumn: isAuto,
diff --git a/packages/server/src/integrations/postgres.ts b/packages/server/src/integrations/postgres.ts
index eba0d3691d..c981c3acc5 100644
--- a/packages/server/src/integrations/postgres.ts
+++ b/packages/server/src/integrations/postgres.ts
@@ -262,15 +262,17 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
column.identity_start ||
column.identity_increment
)
- const constraints = {
- presence: column.is_nullable === "NO",
- }
- const hasDefault =
+ const hasDefault = column.column_default != null
+ const hasNextVal =
typeof column.column_default === "string" &&
column.column_default.startsWith("nextval")
const isGenerated =
column.is_generated && column.is_generated !== "NEVER"
- const isAuto: boolean = hasDefault || identity || isGenerated
+ const isAuto: boolean = hasNextVal || identity || isGenerated
+ const required = column.is_nullable === "NO"
+ const constraints = {
+ presence: required && !hasDefault && !isGenerated,
+ }
tables[tableName].schema[columnName] = {
autocolumn: isAuto,
name: columnName,
diff --git a/packages/server/src/migrations/functions/syncQuotas.ts b/packages/server/src/migrations/functions/syncQuotas.ts
index 85aa7bb337..67f38ba929 100644
--- a/packages/server/src/migrations/functions/syncQuotas.ts
+++ b/packages/server/src/migrations/functions/syncQuotas.ts
@@ -2,6 +2,7 @@ import { runQuotaMigration } from "./usageQuotas"
import * as syncApps from "./usageQuotas/syncApps"
import * as syncRows from "./usageQuotas/syncRows"
import * as syncPlugins from "./usageQuotas/syncPlugins"
+import * as syncUsers from "./usageQuotas/syncUsers"
/**
* Synchronise quotas to the state of the db.
@@ -11,5 +12,6 @@ export const run = async () => {
await syncApps.run()
await syncRows.run()
await syncPlugins.run()
+ await syncUsers.run()
})
}
diff --git a/packages/server/src/migrations/functions/tests/syncQuotas.spec.js b/packages/server/src/migrations/functions/tests/syncQuotas.spec.js
deleted file mode 100644
index 71f6554b84..0000000000
--- a/packages/server/src/migrations/functions/tests/syncQuotas.spec.js
+++ /dev/null
@@ -1,26 +0,0 @@
-const syncApps = jest.fn()
-const syncRows = jest.fn()
-const syncPlugins = jest.fn()
-jest.mock("../usageQuotas/syncApps", () => ({ run: syncApps }) )
-jest.mock("../usageQuotas/syncRows", () => ({ run: syncRows }) )
-jest.mock("../usageQuotas/syncPlugins", () => ({ run: syncPlugins }) )
-
-const TestConfig = require("../../../tests/utilities/TestConfiguration")
-const migration = require("../syncQuotas")
-
-describe("run", () => {
- let config = new TestConfig(false)
-
- beforeAll(async () => {
- await config.init()
- })
-
- afterAll(config.end)
-
- it("run", async () => {
- await migration.run()
- expect(syncApps).toHaveBeenCalledTimes(1)
- expect(syncRows).toHaveBeenCalledTimes(1)
- expect(syncPlugins).toHaveBeenCalledTimes(1)
- })
-})
diff --git a/packages/server/src/migrations/functions/usageQuotas/syncApps.ts b/packages/server/src/migrations/functions/usageQuotas/syncApps.ts
index 39b8efe335..80ed1953d3 100644
--- a/packages/server/src/migrations/functions/usageQuotas/syncApps.ts
+++ b/packages/server/src/migrations/functions/usageQuotas/syncApps.ts
@@ -1,4 +1,4 @@
-import { tenancy, db as dbCore } from "@budibase/backend-core"
+import { db as dbCore } from "@budibase/backend-core"
import { quotas } from "@budibase/pro"
import { QuotaUsageType, StaticQuotaName } from "@budibase/types"
@@ -8,7 +8,6 @@ export const run = async () => {
const appCount = devApps ? devApps.length : 0
// sync app count
- const tenantId = tenancy.getTenantId()
console.log(`Syncing app count: ${appCount}`)
await quotas.setUsage(appCount, StaticQuotaName.APPS, QuotaUsageType.STATIC)
}
diff --git a/packages/server/src/migrations/functions/usageQuotas/syncUsers.ts b/packages/server/src/migrations/functions/usageQuotas/syncUsers.ts
new file mode 100644
index 0000000000..c9913dced8
--- /dev/null
+++ b/packages/server/src/migrations/functions/usageQuotas/syncUsers.ts
@@ -0,0 +1,9 @@
+import { users } from "@budibase/backend-core"
+import { quotas } from "@budibase/pro"
+import { QuotaUsageType, StaticQuotaName } from "@budibase/types"
+
+export const run = async () => {
+ const userCount = await users.getUserCount()
+ console.log(`Syncing user count: ${userCount}`)
+ await quotas.setUsage(userCount, StaticQuotaName.USERS, QuotaUsageType.STATIC)
+}
diff --git a/packages/server/src/migrations/functions/usageQuotas/tests/syncUsers.spec.ts b/packages/server/src/migrations/functions/usageQuotas/tests/syncUsers.spec.ts
new file mode 100644
index 0000000000..f7500c8b4d
--- /dev/null
+++ b/packages/server/src/migrations/functions/usageQuotas/tests/syncUsers.spec.ts
@@ -0,0 +1,26 @@
+import TestConfig from "../../../../tests/utilities/TestConfiguration"
+import * as syncUsers from "../syncUsers"
+import { quotas } from "@budibase/pro"
+
+describe("syncUsers", () => {
+ let config = new TestConfig(false)
+
+ beforeEach(async () => {
+ await config.init()
+ })
+
+ afterAll(config.end)
+
+ it("syncs users", async () => {
+ return config.doInContext(null, async () => {
+ await config.createUser()
+
+ await syncUsers.run()
+
+ const usageDoc = await quotas.getQuotaUsage()
+ // default + additional user
+ const userCount = 2
+ expect(usageDoc.usageQuota.users).toBe(userCount)
+ })
+ })
+})
diff --git a/packages/server/src/migrations/index.ts b/packages/server/src/migrations/index.ts
index a66d793142..61ae75adf1 100644
--- a/packages/server/src/migrations/index.ts
+++ b/packages/server/src/migrations/index.ts
@@ -11,6 +11,7 @@ import env from "../environment"
// migration functions
import * as userEmailViewCasing from "./functions/userEmailViewCasing"
import * as syncQuotas from "./functions/syncQuotas"
+import * as syncUsers from "./functions/usageQuotas/syncUsers"
import * as appUrls from "./functions/appUrls"
import * as tableSettings from "./functions/tableSettings"
import * as backfill from "./functions/backfill"
diff --git a/packages/server/src/sdk/app/applications/sync.ts b/packages/server/src/sdk/app/applications/sync.ts
index 6fb3576ae6..d0a1d78428 100644
--- a/packages/server/src/sdk/app/applications/sync.ts
+++ b/packages/server/src/sdk/app/applications/sync.ts
@@ -1,6 +1,117 @@
import env from "../../../environment"
-import { db as dbCore, context } from "@budibase/backend-core"
+import {
+ db as dbCore,
+ context,
+ docUpdates,
+ constants,
+ logging,
+ roles,
+} from "@budibase/backend-core"
+import { User, ContextUser, UserGroup } from "@budibase/types"
+import { sdk as proSdk } from "@budibase/pro"
import sdk from "../../"
+import { getGlobalUsers, processUser } from "../../../utilities/global"
+import { generateUserMetadataID, InternalTables } from "../../../db/utils"
+
+type DeletedUser = { _id: string; deleted: boolean }
+
+async function syncUsersToApp(
+ appId: string,
+ users: (User | DeletedUser)[],
+ groups: UserGroup[]
+) {
+ if (!(await dbCore.dbExists(appId))) {
+ return
+ }
+ await context.doInAppContext(appId, async () => {
+ const db = context.getAppDB()
+ for (let user of users) {
+ let ctxUser = user as ContextUser
+ let deletedUser = false
+ const metadataId = generateUserMetadataID(user._id!)
+ if ((user as DeletedUser).deleted) {
+ deletedUser = true
+ }
+
+ // make sure role is correct
+ if (!deletedUser) {
+ ctxUser = await processUser(ctxUser, { appId, groups })
+ }
+ let roleId = ctxUser.roleId
+ if (roleId === roles.BUILTIN_ROLE_IDS.PUBLIC) {
+ roleId = undefined
+ }
+
+ let metadata
+ try {
+ metadata = await db.get(metadataId)
+ } catch (err: any) {
+ if (err.status !== 404) {
+ throw err
+ }
+ // no metadata and user is to be deleted, can skip
+ // no role - user isn't in app anyway
+ if (!roleId) {
+ continue
+ } else if (!deletedUser) {
+ // doesn't exist yet, creating it
+ metadata = {
+ tableId: InternalTables.USER_METADATA,
+ }
+ }
+ }
+
+ // the user doesn't exist, or doesn't have a role anymore
+ // get rid of their metadata
+ if (deletedUser || !roleId) {
+ await db.remove(metadata)
+ continue
+ }
+
+ // assign the roleId for the metadata doc
+ if (roleId) {
+ metadata.roleId = roleId
+ }
+
+ let combined = sdk.users.combineMetadataAndUser(ctxUser, metadata)
+ // if no combined returned, there are no updates to make
+ if (combined) {
+ await db.put(combined)
+ }
+ }
+ })
+}
+
+export async function syncUsersToAllApps(userIds: string[]) {
+ // list of users, if one has been deleted it will be undefined in array
+ const users = (await getGlobalUsers(userIds, {
+ noProcessing: true,
+ })) as User[]
+ const groups = await proSdk.groups.fetch()
+ const finalUsers: (User | DeletedUser)[] = []
+ for (let userId of userIds) {
+ const user = users.find(user => user._id === userId)
+ if (!user) {
+ finalUsers.push({ _id: userId, deleted: true })
+ } else {
+ finalUsers.push(user)
+ }
+ }
+ const devAppIds = await dbCore.getDevAppIDs()
+ let promises = []
+ for (let devAppId of devAppIds) {
+ const prodAppId = dbCore.getProdAppID(devAppId)
+ for (let appId of [prodAppId, devAppId]) {
+ promises.push(syncUsersToApp(appId, finalUsers, groups))
+ }
+ }
+ const resp = await Promise.allSettled(promises)
+ const failed = resp.filter(promise => promise.status === "rejected")
+ if (failed.length > 0) {
+ const reasons = failed.map(fail => (fail as PromiseRejectedResult).reason)
+ logging.logAlert("Failed to sync users to apps", reasons)
+ }
+}
export async function syncApp(
appId: string,
@@ -23,32 +134,28 @@ export async function syncApp(
// specific case, want to make sure setup is skipped
const prodDb = context.getProdAppDB({ skip_setup: true })
const exists = await prodDb.exists()
- if (!exists) {
- // the database doesn't exist. Don't replicate
- return {
- message: "App sync not required, app not deployed.",
- }
- }
- const replication = new dbCore.Replication({
- source: prodAppId,
- target: appId,
- })
let error
- try {
- const replOpts = replication.appReplicateOpts()
- if (opts?.automationOnly) {
- replOpts.filter = (doc: any) =>
- doc._id.startsWith(dbCore.DocumentType.AUTOMATION)
+ if (exists) {
+ const replication = new dbCore.Replication({
+ source: prodAppId,
+ target: appId,
+ })
+ try {
+ const replOpts = replication.appReplicateOpts()
+ if (opts?.automationOnly) {
+ replOpts.filter = (doc: any) =>
+ doc._id.startsWith(dbCore.DocumentType.AUTOMATION)
+ }
+ await replication.replicate(replOpts)
+ } catch (err) {
+ error = err
+ } finally {
+ await replication.close()
}
- await replication.replicate(replOpts)
- } catch (err) {
- error = err
- } finally {
- await replication.close()
}
- // sync the users
+ // sync the users - kept for safe keeping
await sdk.users.syncGlobalUsers()
if (error) {
diff --git a/packages/server/src/sdk/app/applications/tests/sync.spec.ts b/packages/server/src/sdk/app/applications/tests/sync.spec.ts
new file mode 100644
index 0000000000..8609e59a2f
--- /dev/null
+++ b/packages/server/src/sdk/app/applications/tests/sync.spec.ts
@@ -0,0 +1,137 @@
+import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
+import { events, context, roles, constants } from "@budibase/backend-core"
+import { init } from "../../../../events"
+import { rawUserMetadata } from "../../../users/utils"
+import EventEmitter from "events"
+import { UserGroup, UserMetadata, UserRoles, User } from "@budibase/types"
+
+const config = new TestConfiguration()
+let app, group: UserGroup, groupUser: User
+const ROLE_ID = roles.BUILTIN_ROLE_IDS.BASIC
+
+const emitter = new EventEmitter()
+
+function updateCb(docId: string) {
+ const isGroup = docId.startsWith(constants.DocumentType.GROUP)
+ if (isGroup) {
+ emitter.emit("update-group")
+ } else {
+ emitter.emit("update-user")
+ }
+}
+
+init(updateCb)
+
+function waitForUpdate(opts: { group?: boolean }) {
+ return new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ reject()
+ }, 5000)
+ const event = opts?.group ? "update-group" : "update-user"
+ emitter.on(event, () => {
+ clearTimeout(timeout)
+ resolve()
+ })
+ })
+}
+
+beforeAll(async () => {
+ app = await config.init("syncApp")
+})
+
+async function createUser(email: string, roles: UserRoles, builder?: boolean) {
+ const user = await config.createUser({
+ email,
+ roles,
+ builder: builder || false,
+ admin: false,
+ })
+ await context.doInContext(config.appId!, async () => {
+ await events.user.created(user)
+ })
+ return user
+}
+
+async function removeUserRole(user: User) {
+ const final = await config.globalUser({
+ ...user,
+ id: user._id,
+ roles: {},
+ builder: false,
+ admin: false,
+ })
+ await context.doInContext(config.appId!, async () => {
+ await events.user.updated(final)
+ })
+}
+
+async function createGroupAndUser(email: string) {
+ groupUser = await config.createUser({
+ email,
+ roles: {},
+ builder: false,
+ admin: false,
+ })
+ group = await config.createGroup()
+ await config.addUserToGroup(group._id!, groupUser._id!)
+}
+
+async function removeUserFromGroup() {
+ await config.removeUserFromGroup(group._id!, groupUser._id!)
+ return context.doInContext(config.appId!, async () => {
+ await events.user.updated(groupUser)
+ })
+}
+
+async function getUserMetadata(): Promise {
+ return context.doInContext(config.appId!, async () => {
+ return await rawUserMetadata()
+ })
+}
+
+function buildRoles() {
+ return { [config.prodAppId!]: ROLE_ID }
+}
+
+describe("app user/group sync", () => {
+ const groupEmail = "test2@test.com",
+ normalEmail = "test@test.com"
+ async function checkEmail(
+ email: string,
+ opts?: { group?: boolean; notFound?: boolean }
+ ) {
+ await waitForUpdate(opts || {})
+ const metadata = await getUserMetadata()
+ const found = metadata.find(data => data.email === email)
+ if (opts?.notFound) {
+ expect(found).toBeUndefined()
+ } else {
+ expect(found).toBeDefined()
+ }
+ }
+
+ it("should be able to sync a new user, add then remove", async () => {
+ const user = await createUser(normalEmail, buildRoles())
+ await checkEmail(normalEmail)
+ await removeUserRole(user)
+ await checkEmail(normalEmail, { notFound: true })
+ })
+
+ it("should be able to sync a group", async () => {
+ await createGroupAndUser(groupEmail)
+ await checkEmail(groupEmail, { group: true })
+ })
+
+ it("should be able to remove user from group", async () => {
+ if (!group) {
+ await createGroupAndUser(groupEmail)
+ }
+ await removeUserFromGroup()
+ await checkEmail(groupEmail, { notFound: true })
+ })
+
+ it("should be able to handle builder users", async () => {
+ await createUser("test3@test.com", {}, true)
+ await checkEmail("test3@test.com")
+ })
+})
diff --git a/packages/server/src/sdk/app/backups/exports.ts b/packages/server/src/sdk/app/backups/exports.ts
index c10f106451..27a907fceb 100644
--- a/packages/server/src/sdk/app/backups/exports.ts
+++ b/packages/server/src/sdk/app/backups/exports.ts
@@ -148,41 +148,6 @@ export async function exportApp(appId: string, config?: ExportOpts) {
}
}
-/**
- * Export all apps + global DB (if supplied) to a single tarball, this includes
- * the attachments for each app as well.
- * @param {object[]} appMetadata The IDs and names of apps to export.
- * @param {string} globalDbContents The contents of the global DB to export as well.
- * @return {string} The path to the tarball.
- */
-export async function exportMultipleApps(
- appMetadata: { appId: string; name: string }[],
- globalDbContents?: string
-) {
- const tmpPath = join(budibaseTempDir(), uuid())
- fs.mkdirSync(tmpPath)
- let exportPromises: Promise[] = []
- // export each app to a directory, then move it into the complete export
- const exportAndMove = async (appId: string, appName: string) => {
- const path = await exportApp(appId)
- await fs.promises.rename(path, join(tmpPath, appName))
- }
- for (let metadata of appMetadata) {
- exportPromises.push(exportAndMove(metadata.appId, metadata.name))
- }
- // wait for all exports to finish
- await Promise.all(exportPromises)
- // add the global DB contents
- if (globalDbContents) {
- fs.writeFileSync(join(tmpPath, GLOBAL_DB_EXPORT_FILE), globalDbContents)
- }
- const appNames = appMetadata.map(metadata => metadata.name)
- const tarPath = tarFilesToTmp(tmpPath, [...appNames, GLOBAL_DB_EXPORT_FILE])
- // clear up the tmp path now tarball generated
- fs.rmSync(tmpPath, { recursive: true, force: true })
- return tarPath
-}
-
/**
* Streams a backup of the database state for an app
* @param {string} appId The ID of the app which is to be backed up.
diff --git a/packages/server/src/sdk/index.ts b/packages/server/src/sdk/index.ts
index 294c99a12c..1bf7d89604 100644
--- a/packages/server/src/sdk/index.ts
+++ b/packages/server/src/sdk/index.ts
@@ -6,6 +6,7 @@ import { default as datasources } from "./app/datasources"
import { default as queries } from "./app/queries"
import { default as rows } from "./app/rows"
import { default as users } from "./users"
+import { default as plugins } from "./plugins"
const sdk = {
backups,
@@ -16,6 +17,7 @@ const sdk = {
users,
datasources,
queries,
+ plugins,
}
// default export for TS
diff --git a/packages/server/src/sdk/plugins/index.ts b/packages/server/src/sdk/plugins/index.ts
new file mode 100644
index 0000000000..863cfa0db6
--- /dev/null
+++ b/packages/server/src/sdk/plugins/index.ts
@@ -0,0 +1,5 @@
+import * as plugins from "./plugins"
+
+export default {
+ ...plugins,
+}
diff --git a/packages/server/src/sdk/plugins/plugins.ts b/packages/server/src/sdk/plugins/plugins.ts
new file mode 100644
index 0000000000..02000410bf
--- /dev/null
+++ b/packages/server/src/sdk/plugins/plugins.ts
@@ -0,0 +1,41 @@
+import { FileType, Plugin, PluginSource, PluginType } from "@budibase/types"
+import {
+ db as dbCore,
+ objectStore,
+ plugins as pluginCore,
+ tenancy,
+} from "@budibase/backend-core"
+import { fileUpload } from "../../api/controllers/plugin/file"
+import env from "../../environment"
+import { clientAppSocket } from "../../websockets"
+import { sdk as pro } from "@budibase/pro"
+
+export async function fetch(type?: PluginType) {
+ const db = tenancy.getGlobalDB()
+ const response = await db.allDocs(
+ dbCore.getPluginParams(null, {
+ include_docs: true,
+ })
+ )
+ let plugins = response.rows.map((row: any) => row.doc) as Plugin[]
+ plugins = objectStore.enrichPluginURLs(plugins)
+ if (type) {
+ return plugins.filter((plugin: Plugin) => plugin.schema?.type === type)
+ } else {
+ return plugins
+ }
+}
+
+export async function processUploaded(plugin: FileType, source?: PluginSource) {
+ const { metadata, directory } = await fileUpload(plugin)
+ pluginCore.validate(metadata?.schema)
+
+ // Only allow components in cloud
+ if (!env.SELF_HOSTED && metadata?.schema?.type !== PluginType.COMPONENT) {
+ throw new Error("Only component plugins are supported outside of self-host")
+ }
+
+ const doc = await pro.plugins.storePlugin(metadata, directory, source)
+ clientAppSocket.emit("plugin-update", { name: doc.name, hash: doc.hash })
+ return doc
+}
diff --git a/packages/server/src/sdk/users/tests/utils.spec.ts b/packages/server/src/sdk/users/tests/utils.spec.ts
index 11c2c53643..5c6777df59 100644
--- a/packages/server/src/sdk/users/tests/utils.spec.ts
+++ b/packages/server/src/sdk/users/tests/utils.spec.ts
@@ -121,38 +121,7 @@ describe("syncGlobalUsers", () => {
await syncGlobalUsers()
const metadata = await rawUserMetadata()
- expect(metadata).toHaveLength(1)
- })
- })
- })
-
- it("app users are removed when app is removed from user group", async () => {
- await config.doInTenant(async () => {
- const group = await proSdk.groups.save(structures.userGroups.userGroup())
- const user1 = await config.createUser({ admin: false, builder: false })
- const user2 = await config.createUser({ admin: false, builder: false })
- await proSdk.groups.updateGroupApps(group.id, {
- appsToAdd: [
- { appId: config.prodAppId!, roleId: roles.BUILTIN_ROLE_IDS.BASIC },
- ],
- })
- await proSdk.groups.addUsers(group.id, [user1._id, user2._id])
-
- await config.doInContext(config.appId, async () => {
- await syncGlobalUsers()
- expect(await rawUserMetadata()).toHaveLength(3)
-
- await proSdk.groups.removeUsers(group.id, [user1._id])
- await syncGlobalUsers()
-
- const metadata = await rawUserMetadata()
- expect(metadata).toHaveLength(2)
-
- expect(metadata).not.toContainEqual(
- expect.objectContaining({
- _id: db.generateUserMetadataID(user1._id),
- })
- )
+ expect(metadata).toHaveLength(0)
})
})
})
diff --git a/packages/server/src/sdk/users/utils.ts b/packages/server/src/sdk/users/utils.ts
index 9b9ea04c56..7f7f0b4809 100644
--- a/packages/server/src/sdk/users/utils.ts
+++ b/packages/server/src/sdk/users/utils.ts
@@ -1,12 +1,13 @@
import { getGlobalUsers } from "../../utilities/global"
import { context, roles as rolesCore } from "@budibase/backend-core"
import {
+ getGlobalIDFromUserMetadataID,
generateUserMetadataID,
getUserMetadataParams,
InternalTables,
} from "../../db/utils"
import { isEqual } from "lodash"
-import { ContextUser, UserMetadata } from "@budibase/types"
+import { ContextUser, UserMetadata, User } from "@budibase/types"
export function combineMetadataAndUser(
user: ContextUser,
@@ -37,6 +38,10 @@ export function combineMetadataAndUser(
if (found) {
newDoc._rev = found._rev
}
+ // clear fields that shouldn't be in metadata
+ delete newDoc.password
+ delete newDoc.forceResetPassword
+ delete newDoc.roles
if (found == null || !isEqual(newDoc, found)) {
return {
...found,
@@ -60,10 +65,9 @@ export async function rawUserMetadata() {
export async function syncGlobalUsers() {
// sync user metadata
const db = context.getAppDB()
- const [users, metadata] = await Promise.all([
- getGlobalUsers(),
- rawUserMetadata(),
- ])
+ const resp = await Promise.all([getGlobalUsers(), rawUserMetadata()])
+ const users = resp[0] as User[]
+ const metadata = resp[1] as UserMetadata[]
const toWrite = []
for (let user of users) {
const combined = combineMetadataAndUser(user, metadata)
@@ -71,5 +75,19 @@ export async function syncGlobalUsers() {
toWrite.push(combined)
}
}
+ let foundEmails: string[] = []
+ for (let data of metadata) {
+ if (!data._id) {
+ continue
+ }
+ const alreadyExisting = data.email && foundEmails.indexOf(data.email) !== -1
+ const globalId = getGlobalIDFromUserMetadataID(data._id)
+ if (!users.find(user => user._id === globalId) || alreadyExisting) {
+ toWrite.push({ ...data, _deleted: true })
+ }
+ if (data.email) {
+ foundEmails.push(data.email)
+ }
+ }
await db.bulkDocs(toWrite)
}
diff --git a/packages/server/src/startup.ts b/packages/server/src/startup.ts
index 95d0715185..2fd59b7a8e 100644
--- a/packages/server/src/startup.ts
+++ b/packages/server/src/startup.ts
@@ -10,7 +10,7 @@ import fs from "fs"
import { watch } from "./watch"
import * as automations from "./automations"
import * as fileSystem from "./utilities/fileSystem"
-import eventEmitter from "./events"
+import { default as eventEmitter, init as eventInit } from "./events"
import * as migrations from "./migrations"
import * as bullboard from "./automations/bullboard"
import * as pro from "@budibase/pro"
@@ -63,6 +63,7 @@ export async function startup(app?: any, server?: any) {
eventEmitter.emitPort(env.PORT)
fileSystem.init()
await redis.init()
+ eventInit()
// run migrations on startup if not done via http
// not recommended in a clustered environment
diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts
index ca48dd7f86..f5887e6558 100644
--- a/packages/server/src/tests/utilities/TestConfiguration.ts
+++ b/packages/server/src/tests/utilities/TestConfiguration.ts
@@ -49,6 +49,7 @@ import {
SearchFilters,
UserRoles,
} from "@budibase/types"
+import { BUILTIN_ROLE_IDS } from "@budibase/backend-core/src/security/roles"
type DefaultUserValues = {
globalUserId: string
@@ -306,6 +307,33 @@ class TestConfiguration {
}
}
+ async createGroup(roleId: string = BUILTIN_ROLE_IDS.BASIC) {
+ return context.doInTenant(this.tenantId!, async () => {
+ const baseGroup = structures.userGroups.userGroup()
+ baseGroup.roles = {
+ [this.prodAppId]: roleId,
+ }
+ const { id, rev } = await pro.sdk.groups.save(baseGroup)
+ return {
+ _id: id,
+ _rev: rev,
+ ...baseGroup,
+ }
+ })
+ }
+
+ async addUserToGroup(groupId: string, userId: string) {
+ return context.doInTenant(this.tenantId!, async () => {
+ await pro.sdk.groups.addUsers(groupId, [userId])
+ })
+ }
+
+ async removeUserFromGroup(groupId: string, userId: string) {
+ return context.doInTenant(this.tenantId!, async () => {
+ await pro.sdk.groups.removeUsers(groupId, [userId])
+ })
+ }
+
async login({ roleId, userId, builder, prodApp = false }: any = {}) {
const appId = prodApp ? this.prodAppId : this.appId
return context.doInAppContext(appId, async () => {
diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts
index 207863a6aa..e4e03fb85d 100644
--- a/packages/server/src/tests/utilities/structures.ts
+++ b/packages/server/src/tests/utilities/structures.ts
@@ -1,18 +1,22 @@
-import { permissions, roles } from "@budibase/backend-core"
+import { permissions, roles, utils } from "@budibase/backend-core"
import { createHomeScreen } from "../../constants/screens"
import { EMPTY_LAYOUT } from "../../constants/layouts"
import { cloneDeep } from "lodash/fp"
-import { ACTION_DEFINITIONS, TRIGGER_DEFINITIONS } from "../../automations"
+import {
+ BUILTIN_ACTION_DEFINITIONS,
+ TRIGGER_DEFINITIONS,
+} from "../../automations"
import {
Automation,
AutomationActionStepId,
+ AutomationStep,
+ AutomationStepType,
+ AutomationTrigger,
AutomationTriggerStepId,
Datasource,
SourceName,
} from "@budibase/types"
-const { v4: uuidv4 } = require("uuid")
-
export function basicTable() {
return {
name: "TestTable",
@@ -71,19 +75,19 @@ export function view(tableId: string) {
}
export function automationStep(
- actionDefinition = ACTION_DEFINITIONS.CREATE_ROW
-) {
+ actionDefinition = BUILTIN_ACTION_DEFINITIONS.CREATE_ROW
+): AutomationStep {
return {
- id: uuidv4(),
+ id: utils.newid(),
...actionDefinition,
}
}
export function automationTrigger(
triggerDefinition = TRIGGER_DEFINITIONS.ROW_SAVED
-) {
+): AutomationTrigger {
return {
- id: uuidv4(),
+ id: utils.newid(),
...triggerDefinition,
}
}
@@ -106,7 +110,7 @@ export function newAutomation({ steps, trigger }: any = {}) {
return automation
}
-export function basicAutomation(appId?: string) {
+export function basicAutomation(appId?: string): Automation {
return {
name: "My Automation",
screenId: "kasdkfldsafkl",
@@ -119,18 +123,22 @@ export function basicAutomation(appId?: string) {
tagline: "test",
icon: "test",
description: "test",
- type: "trigger",
+ type: AutomationStepType.TRIGGER,
id: "test",
inputs: {},
schema: {
- inputs: {},
- outputs: {},
+ inputs: {
+ properties: {},
+ },
+ outputs: {
+ properties: {},
+ },
},
},
steps: [],
},
type: "automation",
- appId,
+ appId: appId!,
}
}
@@ -154,7 +162,7 @@ export function loopAutomation(tableId: string, loopOpts?: any): Automation {
inputs: {
tableId,
},
- schema: ACTION_DEFINITIONS.QUERY_ROWS.schema,
+ schema: BUILTIN_ACTION_DEFINITIONS.QUERY_ROWS.schema,
},
{
id: "c",
@@ -163,7 +171,7 @@ export function loopAutomation(tableId: string, loopOpts?: any): Automation {
internal: true,
inputs: loopOpts,
blockToLoop: "d",
- schema: ACTION_DEFINITIONS.LOOP.schema,
+ schema: BUILTIN_ACTION_DEFINITIONS.LOOP.schema,
},
{
id: "d",
@@ -173,7 +181,7 @@ export function loopAutomation(tableId: string, loopOpts?: any): Automation {
inputs: {
text: "log statement",
},
- schema: ACTION_DEFINITIONS.SERVER_LOG.schema,
+ schema: BUILTIN_ACTION_DEFINITIONS.SERVER_LOG.schema,
},
],
trigger: {
diff --git a/packages/server/src/threads/automation.ts b/packages/server/src/threads/automation.ts
index c19b05a8a7..adc1a83af0 100644
--- a/packages/server/src/threads/automation.ts
+++ b/packages/server/src/threads/automation.ts
@@ -27,8 +27,8 @@ import { processObject } from "@budibase/string-templates"
import { cloneDeep } from "lodash/fp"
import * as sdkUtils from "../sdk/utils"
import env from "../environment"
-const FILTER_STEP_ID = actions.ACTION_DEFINITIONS.FILTER.stepId
-const LOOP_STEP_ID = actions.ACTION_DEFINITIONS.LOOP.stepId
+const FILTER_STEP_ID = actions.BUILTIN_ACTION_DEFINITIONS.FILTER.stepId
+const LOOP_STEP_ID = actions.BUILTIN_ACTION_DEFINITIONS.LOOP.stepId
const CRON_STEP_ID = triggerDefs.CRON.stepId
const STOPPED_STATUS = { success: true, status: AutomationStatus.STOPPED }
diff --git a/packages/server/src/utilities/fileSystem/plugin.ts b/packages/server/src/utilities/fileSystem/plugin.ts
index 0bc2b7de44..3e1e9bef4d 100644
--- a/packages/server/src/utilities/fileSystem/plugin.ts
+++ b/packages/server/src/utilities/fileSystem/plugin.ts
@@ -5,6 +5,7 @@ import { join } from "path"
import { objectStore } from "@budibase/backend-core"
const DATASOURCE_PATH = join(budibaseTempDir(), "datasource")
+const AUTOMATION_PATH = join(budibaseTempDir(), "automation")
export const getPluginMetadata = async (path: string) => {
let metadata: any = {}
@@ -33,12 +34,12 @@ export const getPluginMetadata = async (path: string) => {
return { metadata, directory: path }
}
-export const getDatasourcePlugin = async (plugin: Plugin) => {
+async function getPluginImpl(path: string, plugin: Plugin) {
const hash = plugin.schema?.hash
- if (!fs.existsSync(DATASOURCE_PATH)) {
- fs.mkdirSync(DATASOURCE_PATH)
+ if (!fs.existsSync(path)) {
+ fs.mkdirSync(path)
}
- const filename = join(DATASOURCE_PATH, plugin.name)
+ const filename = join(path, plugin.name)
const metadataName = `${filename}.bbmetadata`
if (fs.existsSync(filename)) {
const currentHash = fs.readFileSync(metadataName, "utf8")
@@ -62,3 +63,11 @@ export const getDatasourcePlugin = async (plugin: Plugin) => {
return require(filename)
}
+
+export const getDatasourcePlugin = async (plugin: Plugin) => {
+ return getPluginImpl(DATASOURCE_PATH, plugin)
+}
+
+export const getAutomationPlugin = async (plugin: Plugin) => {
+ return getPluginImpl(AUTOMATION_PATH, plugin)
+}
diff --git a/packages/server/src/utilities/global.ts b/packages/server/src/utilities/global.ts
index a75fcc0b30..21e86a28b9 100644
--- a/packages/server/src/utilities/global.ts
+++ b/packages/server/src/utilities/global.ts
@@ -9,6 +9,7 @@ import {
import env from "../environment"
import { groups } from "@budibase/pro"
import { UserCtx, ContextUser, User, UserGroup } from "@budibase/types"
+import { global } from "yargs"
export function updateAppRole(
user: ContextUser,
@@ -16,7 +17,7 @@ export function updateAppRole(
) {
appId = appId || context.getAppId()
- if (!user || !user.roles) {
+ if (!user || (!user.roles && !user.userGroups)) {
return user
}
// if in an multi-tenancy environment make sure roles are never updated
@@ -27,7 +28,7 @@ export function updateAppRole(
return user
}
// always use the deployed app
- if (appId) {
+ if (appId && user.roles) {
user.roleId = user.roles[dbCore.getProdAppID(appId)]
}
// if a role wasn't found then either set as admin (builder) or public (everyone else)
@@ -60,7 +61,7 @@ async function checkGroupRoles(
return user
}
-async function processUser(
+export async function processUser(
user: ContextUser,
opts: { appId?: string; groups?: UserGroup[] } = {}
) {
@@ -94,16 +95,15 @@ export async function getGlobalUser(userId: string) {
return processUser(user, { appId })
}
-export async function getGlobalUsers(users?: ContextUser[]) {
+export async function getGlobalUsers(
+ userIds?: string[],
+ opts?: { noProcessing?: boolean }
+) {
const appId = context.getAppId()
const db = tenancy.getGlobalDB()
- const allGroups = await groups.fetch()
let globalUsers
- if (users) {
- const globalIds = users.map(user =>
- getGlobalIDFromUserMetadataID(user._id!)
- )
- globalUsers = (await db.allDocs(getMultiIDParams(globalIds))).rows.map(
+ if (userIds) {
+ globalUsers = (await db.allDocs(getMultiIDParams(userIds))).rows.map(
row => row.doc
)
} else {
@@ -126,15 +126,20 @@ export async function getGlobalUsers(users?: ContextUser[]) {
return globalUsers
}
- // pass in the groups, meaning we don't actually need to retrieve them for
- // each user individually
- return Promise.all(
- globalUsers.map(user => processUser(user, { groups: allGroups }))
- )
+ if (opts?.noProcessing) {
+ return globalUsers
+ } else {
+ // pass in the groups, meaning we don't actually need to retrieve them for
+ // each user individually
+ const allGroups = await groups.fetch()
+ return Promise.all(
+ globalUsers.map(user => processUser(user, { groups: allGroups }))
+ )
+ }
}
export async function getGlobalUsersFromMetadata(users: ContextUser[]) {
- const globalUsers = await getGlobalUsers(users)
+ const globalUsers = await getGlobalUsers(users.map(user => user._id!))
return users.map(user => {
const globalUser = globalUsers.find(
globalUser => globalUser && user._id?.includes(globalUser._id)
diff --git a/packages/server/src/utilities/rowProcessor/utils.ts b/packages/server/src/utilities/rowProcessor/utils.ts
index c22bc59419..66ca969663 100644
--- a/packages/server/src/utilities/rowProcessor/utils.ts
+++ b/packages/server/src/utilities/rowProcessor/utils.ts
@@ -1,11 +1,11 @@
import {
- FieldTypes,
- FormulaTypes,
AutoFieldDefaultNames,
AutoFieldSubTypes,
+ FieldTypes,
+ FormulaTypes,
} from "../../constants"
import { processStringSync } from "@budibase/string-templates"
-import { FieldSchema, Table, Row } from "@budibase/types"
+import { FieldSchema, FieldType, Row, Table } from "@budibase/types"
/**
* If the subtype has been lost for any reason this works out what
@@ -50,6 +50,7 @@ export function processFormulas(
const isStatic = schema.formulaType === FormulaTypes.STATIC
if (
schema.type !== FieldTypes.FORMULA ||
+ schema.formula == null ||
(dynamic && isStatic) ||
(!dynamic && !isStatic)
) {
@@ -57,13 +58,11 @@ export function processFormulas(
}
// iterate through rows and process formula
for (let i = 0; i < rowArray.length; i++) {
- if (schema.formula) {
- let row = rowArray[i]
- let context = contextRows ? contextRows[i] : row
- rowArray[i] = {
- ...row,
- [column]: processStringSync(schema.formula, context),
- }
+ let row = rowArray[i]
+ let context = contextRows ? contextRows[i] : row
+ rowArray[i] = {
+ ...row,
+ [column]: processStringSync(schema.formula, context),
}
}
}
diff --git a/packages/server/src/watch.ts b/packages/server/src/watch.ts
index 09c7312e3b..8f3fe22023 100644
--- a/packages/server/src/watch.ts
+++ b/packages/server/src/watch.ts
@@ -3,7 +3,7 @@ import env from "./environment"
import chokidar from "chokidar"
import fs from "fs"
import { constants, tenancy } from "@budibase/backend-core"
-import { processUploadedPlugin } from "./api/controllers/plugin"
+import pluginsSdk from "./sdk/plugins"
export function watch() {
const watchPath = path.join(env.PLUGINS_DIR, "./**/*.tar.gz")
@@ -27,7 +27,7 @@ export function watch() {
const split = path.split("/")
const name = split[split.length - 1]
console.log("Importing plugin:", path)
- await processUploadedPlugin({ name, path })
+ await pluginsSdk.processUploaded({ name, path })
} catch (err: any) {
const message = err?.message ? err?.message : err
console.error("Failed to import plugin:", message)
diff --git a/packages/server/src/websocket.ts b/packages/server/src/websocket.ts
deleted file mode 100644
index d6d91b0ca0..0000000000
--- a/packages/server/src/websocket.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Server } from "socket.io"
-import http from "http"
-
-class Websocket {
- socketServer: Server
-
- constructor(server: http.Server, path: string) {
- this.socketServer = new Server(server, {
- path,
- })
- }
-
- // Emit an event to all sockets
- emit(event: string, payload: any) {
- this.socketServer.sockets.emit(event, payload)
- }
-}
-
-// Likely to be more socket instances in future
-let ClientAppSocket: Websocket
-
-export const initialise = (server: http.Server) => {
- ClientAppSocket = new Websocket(server, "/socket/client")
-}
-
-export { ClientAppSocket }
diff --git a/packages/server/src/websockets/client.ts b/packages/server/src/websockets/client.ts
new file mode 100644
index 0000000000..d59325f66e
--- /dev/null
+++ b/packages/server/src/websockets/client.ts
@@ -0,0 +1,11 @@
+import Socket from "./websocket"
+import authorized from "../middleware/authorized"
+import http from "http"
+import Koa from "koa"
+import { permissions } from "@budibase/backend-core"
+
+export default class ClientAppWebsocket extends Socket {
+ constructor(app: Koa, server: http.Server) {
+ super(app, server, "/socket/client", [authorized(permissions.BUILDER)])
+ }
+}
diff --git a/packages/server/src/websockets/grid.ts b/packages/server/src/websockets/grid.ts
new file mode 100644
index 0000000000..886784cd2c
--- /dev/null
+++ b/packages/server/src/websockets/grid.ts
@@ -0,0 +1,55 @@
+import authorized from "../middleware/authorized"
+import Socket from "./websocket"
+import { permissions } from "@budibase/backend-core"
+import http from "http"
+import Koa from "koa"
+
+export default class GridSocket extends Socket {
+ constructor(app: Koa, server: http.Server) {
+ super(app, server, "/socket/grid", [authorized(permissions.BUILDER)])
+
+ this.io.on("connection", socket => {
+ const user = socket.data.user
+ console.log(`Spreadsheet user connected: ${user?.id}`)
+
+ // Socket state
+ let currentRoom: string
+
+ // Initial identification of connected spreadsheet
+ socket.on("select-table", async (tableId, callback) => {
+ // Leave current room
+ if (currentRoom) {
+ socket.to(currentRoom).emit("user-disconnect", socket.data.user)
+ socket.leave(currentRoom)
+ }
+
+ // Join new room
+ currentRoom = tableId
+ socket.join(currentRoom)
+ socket.to(currentRoom).emit("user-update", socket.data.user)
+
+ // Reply with all users in current room
+ const sockets = await this.io.in(currentRoom).fetchSockets()
+ callback({
+ users: sockets.map(socket => socket.data.user),
+ id: user.id,
+ })
+ })
+
+ // Handle users selecting a new cell
+ socket.on("select-cell", cellId => {
+ socket.data.user.selectedCellId = cellId
+ if (currentRoom) {
+ socket.to(currentRoom).emit("user-update", socket.data.user)
+ }
+ })
+
+ // Disconnection cleanup
+ socket.on("disconnect", () => {
+ if (currentRoom) {
+ socket.to(currentRoom).emit("user-disconnect", socket.data.user)
+ }
+ })
+ })
+ }
+}
diff --git a/packages/server/src/websockets/index.ts b/packages/server/src/websockets/index.ts
new file mode 100644
index 0000000000..60cdfb8aed
--- /dev/null
+++ b/packages/server/src/websockets/index.ts
@@ -0,0 +1,14 @@
+import http from "http"
+import Koa from "koa"
+import GridSocket from "./grid"
+import ClientAppSocket from "./client"
+
+let clientAppSocket: ClientAppSocket
+let gridSocket: GridSocket
+
+export const initialise = (app: Koa, server: http.Server) => {
+ clientAppSocket = new ClientAppSocket(app, server)
+ gridSocket = new GridSocket(app, server)
+}
+
+export { clientAppSocket, gridSocket }
diff --git a/packages/server/src/websockets/websocket.ts b/packages/server/src/websockets/websocket.ts
new file mode 100644
index 0000000000..1b34168f14
--- /dev/null
+++ b/packages/server/src/websockets/websocket.ts
@@ -0,0 +1,83 @@
+import { Server } from "socket.io"
+import http from "http"
+import Koa from "koa"
+import Cookies from "cookies"
+import { userAgent } from "koa-useragent"
+import { auth } from "@budibase/backend-core"
+import currentApp from "../middleware/currentapp"
+
+export default class Socket {
+ io: Server
+
+ constructor(
+ app: Koa,
+ server: http.Server,
+ path: string,
+ additionalMiddlewares?: any[]
+ ) {
+ this.io = new Server(server, {
+ path,
+ })
+
+ // Attach default middlewares
+ const authenticate = auth.buildAuthMiddleware([], {
+ publicAllowed: true,
+ })
+ const middlewares = [
+ userAgent,
+ authenticate,
+ currentApp,
+ ...(additionalMiddlewares || []),
+ ]
+
+ // Apply middlewares
+ this.io.use(async (socket, next) => {
+ // Build fake koa context
+ const res = new http.ServerResponse(socket.request)
+ const ctx: any = {
+ ...app.createContext(socket.request, res),
+
+ // Additional overrides needed to make our middlewares work with this
+ // fake koa context
+ cookies: new Cookies(socket.request, res),
+ get: (field: string) => socket.request.headers[field],
+ throw: (code: number, message: string) => {
+ throw new Error(message)
+ },
+
+ // Needed for koa-useragent middleware
+ headers: socket.request.headers,
+ header: socket.request.headers,
+
+ // We don't really care about the path since it will never contain
+ // an app ID
+ path: "/socket",
+ }
+
+ // Run all koa middlewares
+ try {
+ for (let [idx, middleware] of middlewares.entries()) {
+ await middleware(ctx, () => {
+ if (idx === middlewares.length - 1) {
+ // Middlewares are finished.
+ // Extract some data from our enriched koa context to persist
+ // as metadata for the socket
+ socket.data.user = {
+ id: ctx.user._id,
+ email: ctx.user.email,
+ }
+ next()
+ }
+ })
+ }
+ } catch (error: any) {
+ next(error)
+ }
+ })
+ }
+
+ // Emit an event to all sockets
+ emit(event: string, payload: any) {
+ this.io.sockets.emit(event, payload)
+ }
+}
diff --git a/packages/server/tsconfig.build.json b/packages/server/tsconfig.build.json
index 9289b6f9da..136de776dd 100644
--- a/packages/server/tsconfig.build.json
+++ b/packages/server/tsconfig.build.json
@@ -8,13 +8,11 @@
"esModuleInterop": true,
"resolveJsonModule": true,
"incremental": true,
- "types": [ "node", "jest" ],
- "outDir": "dist",
+ "types": ["node", "jest"],
+ "outDir": "dist/src",
"skipLibCheck": true
},
- "include": [
- "src/**/*"
- ],
+ "include": ["src/**/*"],
"exclude": [
"node_modules",
"dist",
diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json
index 5f4ee1a701..e98a7cc9ff 100644
--- a/packages/server/tsconfig.json
+++ b/packages/server/tsconfig.json
@@ -5,6 +5,7 @@
"declaration": true,
"sourceMap": true,
"baseUrl": ".",
+ "outDir": "dist",
"paths": {
"@budibase/types": ["../types/src"],
"@budibase/backend-core": ["../backend-core/src"],
@@ -23,6 +24,6 @@
{ "path": "../shared-core" },
{ "path": "../../../budibase-pro/packages/pro" }
],
- "include": ["src/**/*", "specs", "package.json"],
+ "include": ["src/**/*", "specs"],
"exclude": ["node_modules", "dist"]
}
diff --git a/packages/shared-core/package.json b/packages/shared-core/package.json
index c97a166216..9b9eb21db1 100644
--- a/packages/shared-core/package.json
+++ b/packages/shared-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@budibase/shared-core",
- "version": "2.5.5-alpha.0",
+ "version": "2.5.6-alpha.32",
"description": "Shared data utils",
"main": "dist/cjs/src/index.js",
"types": "dist/mjs/src/index.d.ts",
@@ -20,7 +20,7 @@
"dev:builder": "yarn prebuild && concurrently \"tsc -p tsconfig.build.json --watch\" \"tsc -p tsconfig-cjs.build.json --watch\""
},
"dependencies": {
- "@budibase/types": "2.5.5-alpha.0"
+ "@budibase/types": "2.5.6-alpha.32"
},
"devDependencies": {
"concurrently": "^7.6.0",
diff --git a/packages/shared-core/src/filters.ts b/packages/shared-core/src/filters.ts
index 34186cf554..20d79195f1 100644
--- a/packages/shared-core/src/filters.ts
+++ b/packages/shared-core/src/filters.ts
@@ -54,13 +54,8 @@ export const getValidOperatorsForType = (
ops = stringOps.concat([Op.MoreThan, Op.LessThan])
}
- // Filter out "like" for internal tables
- const externalTable = datasource?.tableId?.includes("datasource_plus")
- if (datasource?.type === "table" && !externalTable) {
- ops = ops.filter(x => x !== Op.Like)
- }
-
// Only allow equal/not equal for _id in SQL tables
+ const externalTable = datasource?.tableId?.includes("datasource_plus")
if (field === "_id" && externalTable) {
ops = [Op.Equals, Op.NotEquals, Op.In]
}
diff --git a/packages/shared-core/tsconfig-base.build.json b/packages/shared-core/tsconfig-base.build.json
index 9715eb06cf..6930e3cb99 100644
--- a/packages/shared-core/tsconfig-base.build.json
+++ b/packages/shared-core/tsconfig-base.build.json
@@ -14,7 +14,7 @@
"outDir": "dist",
"skipLibCheck": true
},
- "include": ["**/*.js", "**/*.ts", "package.json"],
+ "include": ["**/*.js", "**/*.ts"],
"exclude": [
"node_modules",
"dist",
diff --git a/packages/shared-core/tsconfig-cjs.build.json b/packages/shared-core/tsconfig-cjs.build.json
index 9b479b7b34..45e81e575c 100644
--- a/packages/shared-core/tsconfig-cjs.build.json
+++ b/packages/shared-core/tsconfig-cjs.build.json
@@ -2,7 +2,7 @@
"extends": "./tsconfig-base.build.json",
"compilerOptions": {
"module": "commonjs",
- "outDir": "dist/cjs",
+ "outDir": "dist/cjs/src",
"target": "es2015"
}
}
diff --git a/packages/shared-core/tsconfig.build.json b/packages/shared-core/tsconfig.build.json
index 4d3c9fc2e0..2a00a44dfe 100644
--- a/packages/shared-core/tsconfig.build.json
+++ b/packages/shared-core/tsconfig.build.json
@@ -2,7 +2,7 @@
"extends": "./tsconfig-base.build.json",
"compilerOptions": {
"module": "esnext",
- "outDir": "dist/mjs",
+ "outDir": "dist/mjs/src",
"target": "esnext"
}
}
diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json
index 1e266fe96a..8ac9d477b9 100644
--- a/packages/string-templates/package.json
+++ b/packages/string-templates/package.json
@@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
- "version": "2.5.5-alpha.0",
+ "version": "2.5.6-alpha.32",
"description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs",
"module": "dist/bundle.mjs",
diff --git a/packages/types/package.json b/packages/types/package.json
index d79739bc90..3a4bcd9efa 100644
--- a/packages/types/package.json
+++ b/packages/types/package.json
@@ -1,6 +1,6 @@
{
"name": "@budibase/types",
- "version": "2.5.5-alpha.0",
+ "version": "2.5.6-alpha.32",
"description": "Budibase types",
"main": "dist/cjs/index.js",
"types": "dist/mjs/index.d.ts",
@@ -25,6 +25,7 @@
"@types/koa": "2.13.4",
"@types/node": "14.18.20",
"@types/pouchdb": "6.4.0",
+ "@types/redlock": "4.0.3",
"concurrently": "^7.6.0",
"koa-body": "4.2.0",
"rimraf": "3.0.2",
diff --git a/packages/types/src/api/account/license.ts b/packages/types/src/api/account/license.ts
index 40ee79c3e3..a867358559 100644
--- a/packages/types/src/api/account/license.ts
+++ b/packages/types/src/api/account/license.ts
@@ -1,7 +1,15 @@
-import { QuotaUsage } from "../../documents"
+import { LicenseOverrides, QuotaUsage } from "../../documents"
+import { PlanType } from "../../sdk"
export interface GetLicenseRequest {
- quotaUsage: QuotaUsage
+ // All fields should be optional to cater for
+ // historical versions of budibase
+ quotaUsage?: QuotaUsage
+ install: {
+ id: string
+ tenantId: string
+ version: string
+ }
}
export interface QuotaTriggeredRequest {
@@ -9,3 +17,12 @@ export interface QuotaTriggeredRequest {
name: string
resetDate?: string
}
+
+export interface LicenseActivateRequest {
+ installVersion?: string
+}
+
+export interface UpdateLicenseRequest {
+ planType?: PlanType
+ overrides?: LicenseOverrides
+}
diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts
index 63d6be6fa0..619362805a 100644
--- a/packages/types/src/api/web/user.ts
+++ b/packages/types/src/api/web/user.ts
@@ -22,15 +22,19 @@ export interface BulkUserRequest {
}
}
+export interface BulkUserCreated {
+ successful: UserDetails[]
+ unsuccessful: { email: string; reason: string }[]
+}
+
+export interface BulkUserDeleted {
+ successful: UserDetails[]
+ unsuccessful: { _id: string; email: string; reason: string }[]
+}
+
export interface BulkUserResponse {
- created?: {
- successful: UserDetails[]
- unsuccessful: { email: string; reason: string }[]
- }
- deleted?: {
- successful: UserDetails[]
- unsuccessful: { _id: string; email: string; reason: string }[]
- }
+ created?: BulkUserCreated
+ deleted?: BulkUserDeleted
message?: string
}
diff --git a/packages/types/src/documents/account/account.ts b/packages/types/src/documents/account/account.ts
index e335f014ce..8678085df0 100644
--- a/packages/types/src/documents/account/account.ts
+++ b/packages/types/src/documents/account/account.ts
@@ -1,14 +1,6 @@
-import {
- Feature,
- Hosting,
- License,
- MonthlyQuotaName,
- PlanType,
- PriceDuration,
- Quotas,
- StaticQuotaName,
-} from "../../sdk"
-import { MonthlyUsage, QuotaUsage, StaticUsage } from "../global"
+import { Feature, Hosting, License, PlanType, Quotas } from "../../sdk"
+import { DeepPartial } from "../../shared"
+import { QuotaUsage } from "../global"
export interface CreateAccount {
email: string
@@ -34,7 +26,7 @@ export const isCreatePasswordAccount = (
export interface LicenseOverrides {
features?: Feature[]
- quotas?: Quotas
+ quotas?: DeepPartial
}
export interface Account extends CreateAccount {
@@ -49,6 +41,9 @@ export interface Account extends CreateAccount {
planType?: PlanType
planTier?: number
license?: License
+ installId?: string
+ installTenantId?: string
+ installVersion?: string
stripeCustomerId?: string
licenseKey?: string
licenseKeyActivatedAt?: number
diff --git a/packages/types/src/documents/app/automation.ts b/packages/types/src/documents/app/automation.ts
index 0c431b1f93..aa600c6375 100644
--- a/packages/types/src/documents/app/automation.ts
+++ b/packages/types/src/documents/app/automation.ts
@@ -1,6 +1,32 @@
import { Document } from "../document"
import { EventEmitter } from "events"
+export enum AutomationIOType {
+ OBJECT = "object",
+ STRING = "string",
+ BOOLEAN = "boolean",
+ NUMBER = "number",
+ ARRAY = "array",
+}
+
+export enum AutomationCustomIOType {
+ TABLE = "table",
+ ROW = "row",
+ ROWS = "rows",
+ WIDE = "wide",
+ QUERY = "query",
+ QUERY_PARAMS = "queryParams",
+ QUERY_LIMIT = "queryLimit",
+ LOOP_OPTION = "loopOption",
+ ITEM = "item",
+ CODE = "code",
+ FILTERS = "filters",
+ COLUMN = "column",
+ TRIGGER_SCHEMA = "triggerSchema",
+ CRON = "cron",
+ WEBHOOK_URL = "webhookUrl",
+}
+
export enum AutomationTriggerStepId {
ROW_SAVED = "ROW_SAVED",
ROW_UPDATED = "ROW_UPDATED",
@@ -10,6 +36,12 @@ export enum AutomationTriggerStepId {
CRON = "CRON",
}
+export enum AutomationStepType {
+ LOGIC = "LOGIC",
+ ACTION = "ACTION",
+ TRIGGER = "TRIGGER",
+}
+
export enum AutomationActionStepId {
SEND_EMAIL_SMTP = "SEND_EMAIL_SMTP",
CREATE_ROW = "CREATE_ROW",
@@ -31,14 +63,43 @@ export enum AutomationActionStepId {
integromat = "integromat",
}
+export const AutomationStepIdArray = [
+ ...Object.values(AutomationActionStepId),
+ ...Object.values(AutomationTriggerStepId),
+]
+
export interface Automation extends Document {
definition: {
steps: AutomationStep[]
trigger: AutomationTrigger
}
+ screenId?: string
+ uiTree?: any
appId: string
live?: boolean
name: string
+ internal?: boolean
+ type?: string
+}
+
+interface BaseIOStructure {
+ type?: AutomationIOType
+ customType?: AutomationCustomIOType
+ title?: string
+ description?: string
+ enum?: string[]
+ pretty?: string[]
+ properties?: {
+ [key: string]: BaseIOStructure
+ }
+ required?: string[]
+}
+
+interface InputOutputBlock {
+ properties: {
+ [key: string]: BaseIOStructure
+ }
+ required?: string[]
}
export interface AutomationStepSchema {
@@ -46,7 +107,7 @@ export interface AutomationStepSchema {
tagline: string
icon: string
description: string
- type: string
+ type: AutomationStepType
internal?: boolean
deprecated?: boolean
stepId: AutomationTriggerStepId | AutomationActionStepId
@@ -55,14 +116,10 @@ export interface AutomationStepSchema {
[key: string]: any
}
schema: {
- inputs: {
- [key: string]: any
- }
- outputs: {
- [key: string]: any
- }
- required?: string[]
+ inputs: InputOutputBlock
+ outputs: InputOutputBlock
}
+ custom?: boolean
}
export interface AutomationStep extends AutomationStepSchema {
diff --git a/packages/types/src/documents/app/table.ts b/packages/types/src/documents/app/table.ts
index 929409d0e9..849009098b 100644
--- a/packages/types/src/documents/app/table.ts
+++ b/packages/types/src/documents/app/table.ts
@@ -31,6 +31,8 @@ export interface FieldSchema {
timeOnly?: boolean
lastID?: number
useRichText?: boolean | null
+ order?: number
+ width?: number
meta?: {
toTable: string
toKey: string
@@ -77,6 +79,7 @@ export interface Table extends Document {
indexes?: { [key: string]: any }
rows?: { [key: string]: any }
created?: boolean
+ rowHeight?: number
}
export interface TableRequest extends Table {
diff --git a/packages/types/src/documents/app/user.ts b/packages/types/src/documents/app/user.ts
index b5f31ca349..4defd4a414 100644
--- a/packages/types/src/documents/app/user.ts
+++ b/packages/types/src/documents/app/user.ts
@@ -2,4 +2,5 @@ import { Document } from "../document"
export interface UserMetadata extends Document {
roleId: string
+ email?: string
}
diff --git a/packages/types/src/documents/global/plugin.ts b/packages/types/src/documents/global/plugin.ts
index 17a8426d85..dd96bc20db 100644
--- a/packages/types/src/documents/global/plugin.ts
+++ b/packages/types/src/documents/global/plugin.ts
@@ -3,6 +3,7 @@ import { Document } from "../document"
export enum PluginType {
DATASOURCE = "datasource",
COMPONENT = "component",
+ AUTOMATION = "automation",
}
export enum PluginSource {
diff --git a/packages/types/src/documents/global/quotas.ts b/packages/types/src/documents/global/quotas.ts
index 84e5af3996..61410f7435 100644
--- a/packages/types/src/documents/global/quotas.ts
+++ b/packages/types/src/documents/global/quotas.ts
@@ -31,6 +31,7 @@ export type QuotaTriggers = {
export interface StaticUsage {
[StaticQuotaName.APPS]: number
[StaticQuotaName.PLUGINS]: number
+ [StaticQuotaName.USERS]: number
[StaticQuotaName.USER_GROUPS]: number
[StaticQuotaName.ROWS]: number
triggers: {
diff --git a/packages/types/src/sdk/events/event.ts b/packages/types/src/sdk/events/event.ts
index 0d59576435..c4990f869b 100644
--- a/packages/types/src/sdk/events/event.ts
+++ b/packages/types/src/sdk/events/event.ts
@@ -1,4 +1,5 @@
import { Hosting } from "../hosting"
+import { Group, Identity } from "./identification"
export enum Event {
// USER
@@ -186,6 +187,24 @@ export enum Event {
AUDIT_LOGS_DOWNLOADED = "audit_log:downloaded",
}
+export const UserGroupSyncEvents: Event[] = [
+ Event.USER_CREATED,
+ Event.USER_UPDATED,
+ Event.USER_DELETED,
+ Event.USER_PERMISSION_ADMIN_ASSIGNED,
+ Event.USER_PERMISSION_ADMIN_REMOVED,
+ Event.USER_PERMISSION_BUILDER_ASSIGNED,
+ Event.USER_PERMISSION_BUILDER_REMOVED,
+ Event.USER_GROUP_CREATED,
+ Event.USER_GROUP_UPDATED,
+ Event.USER_GROUP_DELETED,
+ Event.USER_GROUP_USERS_ADDED,
+ Event.USER_GROUP_USERS_REMOVED,
+ Event.USER_GROUP_PERMISSIONS_EDITED,
+]
+
+export const AsyncEvents: Event[] = [...UserGroupSyncEvents]
+
// all events that are not audited have been added to this record as undefined, this means
// that Typescript can protect us against new events being added and auditing of those
// events not being considered. This might be a little ugly, but provides a level of
@@ -383,3 +402,21 @@ export interface BaseEvent {
}
export type TableExportFormat = "json" | "csv"
+
+export type DocUpdateEvent = {
+ id: string
+ tenantId: string
+ appId?: string
+}
+
+export interface EventProcessor {
+ processEvent(
+ event: Event,
+ identity: Identity,
+ properties: any,
+ timestamp?: string | number
+ ): Promise
+ identify?(identity: Identity, timestamp?: string | number): Promise
+ identifyGroup?(group: Group, timestamp?: string | number): Promise
+ shutdown?(): void
+}
diff --git a/packages/types/src/sdk/events/identification.ts b/packages/types/src/sdk/events/identification.ts
index 627254882e..7c7a2be8e0 100644
--- a/packages/types/src/sdk/events/identification.ts
+++ b/packages/types/src/sdk/events/identification.ts
@@ -46,6 +46,8 @@ export interface Identity {
environment: string
installationId?: string
tenantId?: string
+ // usable - no unique format
+ realTenantId?: string
hostInfo?: HostInfo
}
diff --git a/packages/types/src/sdk/licensing/billing.ts b/packages/types/src/sdk/licensing/billing.ts
index d4365525db..35f366c811 100644
--- a/packages/types/src/sdk/licensing/billing.ts
+++ b/packages/types/src/sdk/licensing/billing.ts
@@ -7,6 +7,7 @@ export interface Customer {
export interface Subscription {
amount: number
+ currency: string
quantity: number
duration: PriceDuration
cancelAt: number | null | undefined
diff --git a/packages/types/src/sdk/licensing/feature.ts b/packages/types/src/sdk/licensing/feature.ts
index 8274a95d34..97fbd36224 100644
--- a/packages/types/src/sdk/licensing/feature.ts
+++ b/packages/types/src/sdk/licensing/feature.ts
@@ -1,3 +1,5 @@
+import { PlanType } from "./plan"
+
export enum Feature {
USER_GROUPS = "userGroups",
APP_BACKUPS = "appBackups",
@@ -7,3 +9,5 @@ export enum Feature {
BRANDING = "branding",
SCIM = "scim",
}
+
+export type PlanFeatures = { [key in PlanType]: Feature[] | undefined }
diff --git a/packages/types/src/sdk/licensing/license.ts b/packages/types/src/sdk/licensing/license.ts
index d19a3a617d..b287e67adf 100644
--- a/packages/types/src/sdk/licensing/license.ts
+++ b/packages/types/src/sdk/licensing/license.ts
@@ -1,8 +1,9 @@
-import { AccountPlan, Quotas, Feature, Billing } from "."
+import { PurchasedPlan, Quotas, Feature, Billing } from "."
export interface License {
features: Feature[]
quotas: Quotas
- plan: AccountPlan
+ plan: PurchasedPlan
billing?: Billing
+ testClockId?: string
}
diff --git a/packages/types/src/sdk/licensing/plan.ts b/packages/types/src/sdk/licensing/plan.ts
index f46432b109..360d7d08e5 100644
--- a/packages/types/src/sdk/licensing/plan.ts
+++ b/packages/types/src/sdk/licensing/plan.ts
@@ -1,12 +1,10 @@
-export interface AccountPlan {
- type: PlanType
- price?: Price
-}
-
export enum PlanType {
FREE = "free",
+ /** @deprecated */
PRO = "pro",
+ /** @deprecated */
TEAM = "team",
+ PREMIUM = "premium",
BUSINESS = "business",
ENTERPRISE = "enterprise",
}
@@ -16,12 +14,36 @@ export enum PriceDuration {
YEARLY = "yearly",
}
-export interface Price {
+export interface AvailablePlan {
+ type: PlanType
+ maxUsers: number
+ minUsers: number
+ prices: AvailablePrice[]
+}
+
+export interface AvailablePrice {
amount: number
amountMonthly: number
currency: string
duration: PriceDuration
priceId: string
- dayPasses: number
+}
+
+export enum PlanModel {
+ PER_USER = "perUser",
+ DAY_PASS = "dayPass",
+}
+
+export interface PurchasedPlan {
+ type: PlanType
+ model: PlanModel
+ usesInvoicing: boolean
+ minUsers: number
+ price?: PurchasedPrice
+}
+
+export interface PurchasedPrice extends AvailablePrice {
+ dayPasses: number | undefined
+ /** @deprecated - now at the plan level via model */
isPerUser: boolean
}
diff --git a/packages/types/src/sdk/licensing/quota.ts b/packages/types/src/sdk/licensing/quota.ts
index 86a092c6f5..ea51f7a490 100644
--- a/packages/types/src/sdk/licensing/quota.ts
+++ b/packages/types/src/sdk/licensing/quota.ts
@@ -13,6 +13,7 @@ export enum QuotaType {
export enum StaticQuotaName {
ROWS = "rows",
APPS = "apps",
+ USERS = "users",
USER_GROUPS = "userGroups",
PLUGINS = "plugins",
}
@@ -54,14 +55,14 @@ export const isConstantQuota = (
return quotaType === QuotaType.CONSTANT
}
-export type PlanQuotas = {
- [PlanType.FREE]: Quotas
- [PlanType.PRO]: Quotas
- [PlanType.TEAM]: Quotas
- [PlanType.BUSINESS]: Quotas
- [PlanType.ENTERPRISE]: Quotas
+export interface Minimums {
+ users: number
}
+export type PlanMinimums = { [key in PlanType]: Minimums }
+
+export type PlanQuotas = { [key in PlanType]: Quotas | undefined }
+
export type MonthlyQuotas = {
[MonthlyQuotaName.QUERIES]: Quota
[MonthlyQuotaName.AUTOMATIONS]: Quota
@@ -71,6 +72,7 @@ export type MonthlyQuotas = {
export type StaticQuotas = {
[StaticQuotaName.ROWS]: Quota
[StaticQuotaName.APPS]: Quota
+ [StaticQuotaName.USERS]: Quota
[StaticQuotaName.USER_GROUPS]: Quota
[StaticQuotaName.PLUGINS]: Quota
}
@@ -99,4 +101,5 @@ export interface Quota {
* which can have subsequent effects such as sending emails to users.
*/
triggers: number[]
+ startDate?: number
}
diff --git a/packages/types/src/sdk/locks.ts b/packages/types/src/sdk/locks.ts
index 3f6f03b811..6147308f7d 100644
--- a/packages/types/src/sdk/locks.ts
+++ b/packages/types/src/sdk/locks.ts
@@ -1,3 +1,5 @@
+import Redlock from "redlock"
+
export enum LockType {
/**
* If this lock is already held the attempted operation will not be performed.
@@ -6,6 +8,7 @@ export enum LockType {
TRY_ONCE = "try_once",
DEFAULT = "default",
DELAY_500 = "delay_500",
+ CUSTOM = "custom",
}
export enum LockName {
@@ -14,6 +17,7 @@ export enum LockName {
SYNC_ACCOUNT_LICENSE = "sync_account_license",
UPDATE_TENANTS_DOC = "update_tenants_doc",
PERSIST_WRITETHROUGH = "persist_writethrough",
+ QUOTA_USAGE_EVENT = "quota_usage_event",
}
export interface LockOptions {
@@ -21,6 +25,11 @@ export interface LockOptions {
* The lock type determines which client to use
*/
type: LockType
+ /**
+ * The custom options to use when creating the redlock instance
+ * type must be set to custom for the options to be applied
+ */
+ customOptions?: Redlock.Options
/**
* The name for the lock
*/
diff --git a/packages/types/src/shared/index.ts b/packages/types/src/shared/index.ts
new file mode 100644
index 0000000000..f8f5c5cc25
--- /dev/null
+++ b/packages/types/src/shared/index.ts
@@ -0,0 +1 @@
+export * from "./typeUtils"
diff --git a/packages/types/src/shared/typeUtils.ts b/packages/types/src/shared/typeUtils.ts
new file mode 100644
index 0000000000..71fadfc7aa
--- /dev/null
+++ b/packages/types/src/shared/typeUtils.ts
@@ -0,0 +1,3 @@
+export type DeepPartial = {
+ [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]
+}
diff --git a/packages/worker/package.json b/packages/worker/package.json
index efefa541a9..77594a9896 100644
--- a/packages/worker/package.json
+++ b/packages/worker/package.json
@@ -1,7 +1,7 @@
{
"name": "@budibase/worker",
"email": "hi@budibase.com",
- "version": "2.5.5-alpha.0",
+ "version": "2.5.6-alpha.32",
"description": "Budibase background service",
"main": "src/index.ts",
"repository": {
@@ -37,10 +37,10 @@
"author": "Budibase",
"license": "GPL-3.0",
"dependencies": {
- "@budibase/backend-core": "2.5.5-alpha.0",
- "@budibase/pro": "2.5.5-alpha.0",
- "@budibase/string-templates": "2.5.5-alpha.0",
- "@budibase/types": "2.5.5-alpha.0",
+ "@budibase/backend-core": "2.5.6-alpha.32",
+ "@budibase/pro": "2.5.6-alpha.32",
+ "@budibase/string-templates": "2.5.6-alpha.32",
+ "@budibase/types": "2.5.6-alpha.32",
"@koa/router": "8.0.8",
"@sentry/node": "6.17.7",
"@techpass/passport-openidconnect": "0.3.2",
diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts
index 1b063599ab..8db2d29691 100644
--- a/packages/worker/src/api/controllers/global/users.ts
+++ b/packages/worker/src/api/controllers/global/users.ts
@@ -329,6 +329,7 @@ export const checkInvite = async (ctx: any) => {
try {
invite = await checkInviteCode(code, false)
} catch (e) {
+ console.warn("Error getting invite from code", e)
ctx.throw(400, "There was a problem with the invite")
}
ctx.body = {
@@ -415,8 +416,8 @@ export const inviteAccept = async (
})
ctx.body = {
- _id: user._id,
- _rev: user._rev,
+ _id: user._id!,
+ _rev: user._rev!,
email: user.email,
}
} catch (err: any) {
diff --git a/packages/worker/src/api/controllers/system/accounts.ts b/packages/worker/src/api/controllers/system/accounts.ts
index 10b809c0b5..7e6d1b51b8 100644
--- a/packages/worker/src/api/controllers/system/accounts.ts
+++ b/packages/worker/src/api/controllers/system/accounts.ts
@@ -1,7 +1,7 @@
-import { Account, AccountMetadata } from "@budibase/types"
+import { Account, AccountMetadata, Ctx } from "@budibase/types"
import * as accounts from "../../../sdk/accounts"
-export const save = async (ctx: any) => {
+export const save = async (ctx: Ctx) => {
const account = ctx.request.body as Account
let metadata: AccountMetadata = {
_id: accounts.metadata.formatAccountMetadataId(account.accountId),
diff --git a/packages/worker/src/api/index.ts b/packages/worker/src/api/index.ts
index b390d36bb8..2a3133af3f 100644
--- a/packages/worker/src/api/index.ts
+++ b/packages/worker/src/api/index.ts
@@ -1,6 +1,6 @@
import Router from "@koa/router"
const compress = require("koa-compress")
-const zlib = require("zlib")
+import zlib from "zlib"
import { routes } from "./routes"
import { middleware as pro } from "@budibase/pro"
import { auth, middleware } from "@budibase/backend-core"
diff --git a/packages/worker/src/api/routes/global/tests/scim.spec.ts b/packages/worker/src/api/routes/global/tests/scim.spec.ts
index 3352951152..db47493685 100644
--- a/packages/worker/src/api/routes/global/tests/scim.spec.ts
+++ b/packages/worker/src/api/routes/global/tests/scim.spec.ts
@@ -585,6 +585,59 @@ describe("scim", () => {
totalResults: groupCount,
})
})
+
+ it("can fetch groups using displayName filters", async () => {
+ const groupToFetch = _.sample(groups)
+ const response = await getScimGroups({
+ params: { filter: `displayName eq "${groupToFetch!.displayName}"` },
+ })
+
+ expect(response).toEqual({
+ Resources: [groupToFetch],
+ itemsPerPage: 1,
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
+ startIndex: 1,
+ totalResults: 1,
+ })
+ })
+
+ it("can fetch groups excluding members", async () => {
+ const response = await getScimGroups({
+ params: { excludedAttributes: "members" },
+ })
+
+ expect(response).toEqual({
+ Resources: expect.arrayContaining(
+ groups.map(g => {
+ const { members, ...groupData } = g
+ return groupData
+ })
+ ),
+ itemsPerPage: 25,
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
+ startIndex: 1,
+ totalResults: groupCount,
+ })
+ })
+
+ it("can fetch groups excluding multiple fields", async () => {
+ const response = await getScimGroups({
+ params: { excludedAttributes: "members,displayName" },
+ })
+
+ expect(response).toEqual({
+ Resources: expect.arrayContaining(
+ groups.map(g => {
+ const { members, displayName, ...groupData } = g
+ return groupData
+ })
+ ),
+ itemsPerPage: 25,
+ schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
+ startIndex: 1,
+ totalResults: groupCount,
+ })
+ })
})
})
@@ -662,6 +715,16 @@ describe("scim", () => {
status: 404,
})
})
+
+ it("should allow excluding members", async () => {
+ const response = await findScimGroup(group.id, {
+ qs: "excludedAttributes=members",
+ })
+
+ const { members, ...expectedResponse } = group
+
+ expect(response).toEqual(expectedResponse)
+ })
})
describe("DELETE /api/global/scim/v2/groups/:id", () => {
diff --git a/packages/worker/src/constants/templates/core.hbs b/packages/worker/src/constants/templates/core.hbs
index b7f9658d8c..0b8b8cbde7 100644
--- a/packages/worker/src/constants/templates/core.hbs
+++ b/packages/worker/src/constants/templates/core.hbs
@@ -16,7 +16,8 @@
cellspacing="0"
>
{
+ await validateUniqueUser(email, tenantId)
- let builtUser = await buildUser(user, opts, tenantId, dbUser)
- // don't allow a user to update its own roles/perms
- if (opts.currentUserId && opts.currentUserId === dbUser?._id) {
- builtUser.builder = dbUser.builder
- builtUser.admin = dbUser.admin
- builtUser.roles = dbUser.roles
- }
+ let builtUser = await buildUser(user, opts, tenantId, dbUser)
+ // don't allow a user to update its own roles/perms
+ if (opts.currentUserId && opts.currentUserId === dbUser?._id) {
+ builtUser.builder = dbUser.builder
+ builtUser.admin = dbUser.admin
+ builtUser.roles = dbUser.roles
+ }
- if (!dbUser && roles?.length) {
- builtUser.roles = { ...roles }
- }
+ if (!dbUser && roles?.length) {
+ builtUser.roles = { ...roles }
+ }
- // make sure we set the _id field for a new user
- // Also if this is a new user, associate groups with them
- let groupPromises = []
- if (!_id) {
- _id = builtUser._id!
+ // make sure we set the _id field for a new user
+ // Also if this is a new user, associate groups with them
+ let groupPromises = []
+ if (!_id) {
+ _id = builtUser._id!
- if (userGroups.length > 0) {
- for (let groupId of userGroups) {
- groupPromises.push(pro.groups.addUsers(groupId, [_id]))
+ if (userGroups.length > 0) {
+ for (let groupId of userGroups) {
+ groupPromises.push(pro.groups.addUsers(groupId, [_id]))
+ }
}
}
- }
- try {
- // save the user to db
- let response = await db.put(builtUser)
- builtUser._rev = response.rev
+ try {
+ // save the user to db
+ let response = await db.put(builtUser)
+ builtUser._rev = response.rev
- await eventHelpers.handleSaveEvents(builtUser, dbUser)
- await platform.users.addUser(tenantId, builtUser._id!, builtUser.email)
- await cache.user.invalidateUser(response.id)
+ await eventHelpers.handleSaveEvents(builtUser, dbUser)
+ await platform.users.addUser(tenantId, builtUser._id!, builtUser.email)
+ await cache.user.invalidateUser(response.id)
- // let server know to sync user
- await apps.syncUserInApps(_id, dbUser)
+ await Promise.all(groupPromises)
- await Promise.all(groupPromises)
-
- // finally returned the saved user from the db
- return db.get(builtUser._id!)
- } catch (err: any) {
- if (err.status === 409) {
- throw "User exists already"
- } else {
- throw err
+ // finally returned the saved user from the db
+ return db.get(builtUser._id!)
+ } catch (err: any) {
+ if (err.status === 409) {
+ throw "User exists already"
+ } else {
+ throw err
+ }
}
- }
+ })
}
const getExistingTenantUsers = async (emails: string[]): Promise => {
@@ -379,7 +380,7 @@ const searchExistingEmails = async (emails: string[]) => {
export const bulkCreate = async (
newUsersRequested: User[],
groups: string[]
-): Promise => {
+): Promise => {
const tenantId = tenancy.getTenantId()
let usersToSave: any[] = []
@@ -407,55 +408,56 @@ export const bulkCreate = async (
}
const account = await accountSdk.api.getAccountByTenantId(tenantId)
- // create the promises array that will be called by bulkDocs
- newUsers.forEach((user: any) => {
- usersToSave.push(
- buildUser(
- user,
- {
- hashPassword: true,
- requirePassword: user.requirePassword,
- },
- tenantId,
- undefined, // no dbUser
- account
+ return pro.quotas.addUsers(newUsers.length, async () => {
+ // create the promises array that will be called by bulkDocs
+ newUsers.forEach((user: any) => {
+ usersToSave.push(
+ buildUser(
+ user,
+ {
+ hashPassword: true,
+ requirePassword: user.requirePassword,
+ },
+ tenantId,
+ undefined, // no dbUser
+ account
+ )
)
- )
- })
+ })
- const usersToBulkSave = await Promise.all(usersToSave)
- await usersCore.bulkUpdateGlobalUsers(usersToBulkSave)
+ const usersToBulkSave = await Promise.all(usersToSave)
+ await usersCore.bulkUpdateGlobalUsers(usersToBulkSave)
- // Post-processing of bulk added users, e.g. events and cache operations
- for (const user of usersToBulkSave) {
- // TODO: Refactor to bulk insert users into the info db
- // instead of relying on looping tenant creation
- await platform.users.addUser(tenantId, user._id, user.email)
- await eventHelpers.handleSaveEvents(user, undefined)
- await apps.syncUserInApps(user._id)
- }
+ // Post-processing of bulk added users, e.g. events and cache operations
+ for (const user of usersToBulkSave) {
+ // TODO: Refactor to bulk insert users into the info db
+ // instead of relying on looping tenant creation
+ await platform.users.addUser(tenantId, user._id, user.email)
+ await eventHelpers.handleSaveEvents(user, undefined)
+ }
+
+ const saved = usersToBulkSave.map(user => {
+ return {
+ _id: user._id,
+ email: user.email,
+ }
+ })
+
+ // now update the groups
+ if (Array.isArray(saved) && groups) {
+ const groupPromises = []
+ const createdUserIds = saved.map(user => user._id)
+ for (let groupId of groups) {
+ groupPromises.push(pro.groups.addUsers(groupId, createdUserIds))
+ }
+ await Promise.all(groupPromises)
+ }
- const saved = usersToBulkSave.map(user => {
return {
- _id: user._id,
- email: user.email,
+ successful: saved,
+ unsuccessful,
}
})
-
- // now update the groups
- if (Array.isArray(saved) && groups) {
- const groupPromises = []
- const createdUserIds = saved.map(user => user._id)
- for (let groupId of groups) {
- groupPromises.push(pro.groups.addUsers(groupId, createdUserIds))
- }
- await Promise.all(groupPromises)
- }
-
- return {
- successful: saved,
- unsuccessful,
- }
}
/**
@@ -480,10 +482,10 @@ const getAccountHolderFromUserIds = async (
export const bulkDelete = async (
userIds: string[]
-): Promise => {
+): Promise => {
const db = tenancy.getGlobalDB()
- const response: BulkUserResponse["deleted"] = {
+ const response: BulkUserDeleted = {
successful: [],
unsuccessful: [],
}
@@ -517,6 +519,8 @@ export const bulkDelete = async (
_deleted: true,
}))
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
+
+ await pro.quotas.removeUsers(toDelete.length)
for (let user of usersToDelete) {
await bulkDeleteProcessing(user)
}
@@ -546,6 +550,8 @@ export const bulkDelete = async (
return response
}
+// TODO: The single delete should re-use the bulk delete with a single
+// user so that we don't need to duplicate logic
export const destroy = async (id: string) => {
const db = tenancy.getGlobalDB()
const dbUser = (await db.get(id)) as User
@@ -568,11 +574,10 @@ export const destroy = async (id: string) => {
await db.remove(userId, dbUser._rev)
+ await pro.quotas.removeUsers(1)
await eventHelpers.handleDeleteEvents(dbUser)
await cache.user.invalidateUser(userId)
await sessions.invalidateSessions(userId, { reason: "deletion" })
- // let server know to sync user
- await apps.syncUserInApps(userId, dbUser)
}
const bulkDeleteProcessing = async (dbUser: User) => {
@@ -581,8 +586,6 @@ const bulkDeleteProcessing = async (dbUser: User) => {
await eventHelpers.handleDeleteEvents(dbUser)
await cache.user.invalidateUser(userId)
await sessions.invalidateSessions(userId, { reason: "bulk-deletion" })
- // let server know to sync user
- await apps.syncUserInApps(userId, dbUser)
}
export const invite = async (
diff --git a/packages/worker/src/tests/api/scim/groups.ts b/packages/worker/src/tests/api/scim/groups.ts
index 96ff9aeb67..d2df007614 100644
--- a/packages/worker/src/tests/api/scim/groups.ts
+++ b/packages/worker/src/tests/api/scim/groups.ts
@@ -18,6 +18,7 @@ export class ScimGroupsAPI extends ScimTestAPI {
startIndex?: number
pageSize?: number
filter?: string
+ excludedAttributes?: string
}
}
) => {
@@ -32,6 +33,9 @@ export class ScimGroupsAPI extends ScimTestAPI {
if (params?.filter) {
url += `filter=${params.filter}&`
}
+ if (params?.excludedAttributes) {
+ url += `excludedAttributes=${params.excludedAttributes}&`
+ }
const res = await this.call(url, "get", requestSettings)
return res.body as ScimGroupListResponse
}
@@ -54,9 +58,12 @@ export class ScimGroupsAPI extends ScimTestAPI {
return res.body as ScimGroupResponse
}
- find = async (id: string, requestSettings?: Partial) => {
+ find = async (
+ id: string,
+ requestSettings?: Partial & { qs?: string }
+ ) => {
const res = await this.call(
- `/api/global/scim/v2/groups/${id}`,
+ `/api/global/scim/v2/groups/${id}?${requestSettings?.qs}`,
"get",
requestSettings
)
diff --git a/packages/worker/src/utilities/appService.ts b/packages/worker/src/utilities/appService.ts
deleted file mode 100644
index 8f411d58fa..0000000000
--- a/packages/worker/src/utilities/appService.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import fetch from "node-fetch"
-import {
- constants,
- tenancy,
- logging,
- env as coreEnv,
-} from "@budibase/backend-core"
-import { checkSlashesInUrl } from "../utilities"
-import env from "../environment"
-import { SyncUserRequest, User } from "@budibase/types"
-
-async function makeAppRequest(url: string, method: string, body: any) {
- if (env.isTest()) {
- return
- }
- const request: any = { headers: {} }
- request.headers[constants.Header.API_KEY] = coreEnv.INTERNAL_API_KEY
- if (tenancy.isTenantIdSet()) {
- request.headers[constants.Header.TENANT_ID] = tenancy.getTenantId()
- }
- if (body) {
- request.headers["Content-Type"] = "application/json"
- request.body = JSON.stringify(body)
- }
- request.method = method
-
- // add x-budibase-correlation-id header
- logging.correlation.setHeader(request.headers)
-
- return fetch(checkSlashesInUrl(env.APPS_URL + url), request)
-}
-
-export async function syncUserInApps(userId: string, previousUser?: User) {
- const body: SyncUserRequest = {
- previousUser,
- }
-
- const response = await makeAppRequest(
- `/api/users/metadata/sync/${userId}`,
- "POST",
- body
- )
- if (response && response.status !== 200) {
- throw "Unable to sync user."
- }
-}
diff --git a/packages/worker/src/utilities/email.ts b/packages/worker/src/utilities/email.ts
index fcd022cd92..e47d352f87 100644
--- a/packages/worker/src/utilities/email.ts
+++ b/packages/worker/src/utilities/email.ts
@@ -62,8 +62,8 @@ function createSMTPTransport(config?: SMTPInnerConfig) {
host: "smtp.ethereal.email",
secure: false,
auth: {
- user: "wyatt.zulauf29@ethereal.email",
- pass: "tEwDtHBWWxusVWAPfa",
+ user: "seamus99@ethereal.email",
+ pass: "5ghVteZAqj6jkKJF9R",
},
}
}
diff --git a/packages/worker/tsconfig.json b/packages/worker/tsconfig.json
index 3c8500d248..0a5063574a 100644
--- a/packages/worker/tsconfig.json
+++ b/packages/worker/tsconfig.json
@@ -21,11 +21,6 @@
{ "path": "../backend-core" },
{ "path": "../../../budibase-pro/packages/pro" }
],
- "include": [
- "src/**/*",
- "package.json"
- ],
- "exclude": [
- "dist"
- ]
-}
\ No newline at end of file
+ "include": ["src/**/*"],
+ "exclude": ["dist"]
+}
diff --git a/qa-core/scripts/createEnv.js b/qa-core/scripts/createEnv.js
index cc21282ca9..e5ab783735 100644
--- a/qa-core/scripts/createEnv.js
+++ b/qa-core/scripts/createEnv.js
@@ -8,8 +8,10 @@ function init() {
const envFileJson = {
BUDIBASE_URL: "http://localhost:10000",
ACCOUNT_PORTAL_URL: "http://localhost:10001",
+ ACCOUNT_PORTAL_API_KEY: "budibase",
BB_ADMIN_USER_EMAIL: "admin",
BB_ADMIN_USER_PASSWORD: "admin",
+ LOG_LEVEL: "info",
}
let envFile = ""
Object.keys(envFileJson).forEach(key => {
diff --git a/qa-core/src/account-api/api/AccountInternalAPIClient.ts b/qa-core/src/account-api/api/AccountInternalAPIClient.ts
index 7438059a8c..df58ab0ce3 100644
--- a/qa-core/src/account-api/api/AccountInternalAPIClient.ts
+++ b/qa-core/src/account-api/api/AccountInternalAPIClient.ts
@@ -8,6 +8,7 @@ interface ApiOptions {
method?: APIMethod
body?: object
headers?: HeadersInit | undefined
+ internal?: boolean
}
export default class AccountInternalAPIClient {
@@ -18,6 +19,9 @@ export default class AccountInternalAPIClient {
if (!env.ACCOUNT_PORTAL_URL) {
throw new Error("Must set ACCOUNT_PORTAL_URL env var")
}
+ if (!env.ACCOUNT_PORTAL_API_KEY) {
+ throw new Error("Must set ACCOUNT_PORTAL_API_KEY env var")
+ }
this.host = `${env.ACCOUNT_PORTAL_URL}`
this.state = state
}
@@ -39,6 +43,13 @@ export default class AccountInternalAPIClient {
credentials: "include",
}
+ if (options.internal) {
+ requestOptions.headers = {
+ ...requestOptions.headers,
+ ...{ "x-budibase-api-key": env.ACCOUNT_PORTAL_API_KEY },
+ }
+ }
+
// @ts-ignore
const response = await fetch(`${this.host}${url}`, requestOptions)
@@ -50,15 +61,20 @@ export default class AccountInternalAPIClient {
body = await response.text()
}
- const message = `${method} ${url} - ${response.status}
- Response body: ${JSON.stringify(body)}
- Request body: ${requestOptions.body}`
+ const data = {
+ request: requestOptions.body,
+ response: body,
+ }
+ const message = `${method} ${url} - ${response.status}`
if (response.status > 499) {
- console.error(message)
+ console.error(message, data)
} else if (response.status >= 400) {
- console.warn(message)
+ console.warn(message, data)
+ } else {
+ console.debug(message, data)
}
+
return [response, body]
}
diff --git a/qa-core/src/account-api/api/apis/LicenseAPI.ts b/qa-core/src/account-api/api/apis/LicenseAPI.ts
index 080c22a4ff..f726eb5682 100644
--- a/qa-core/src/account-api/api/apis/LicenseAPI.ts
+++ b/qa-core/src/account-api/api/apis/LicenseAPI.ts
@@ -1,4 +1,6 @@
import AccountInternalAPIClient from "../AccountInternalAPIClient"
+import { Account, UpdateLicenseRequest } from "@budibase/types"
+import { Response } from "node-fetch"
export default class LicenseAPI {
client: AccountInternalAPIClient
@@ -6,4 +8,18 @@ export default class LicenseAPI {
constructor(client: AccountInternalAPIClient) {
this.client = client
}
+
+ async updateLicense(
+ accountId: string,
+ body: UpdateLicenseRequest
+ ): Promise<[Response, Account]> {
+ const [response, json] = await this.client.put(
+ `/api/accounts/${accountId}/license`,
+ {
+ body,
+ internal: true,
+ }
+ )
+ return [response, json]
+ }
}
diff --git a/qa-core/src/environment.ts b/qa-core/src/environment.ts
index 262f5505df..0257b10831 100644
--- a/qa-core/src/environment.ts
+++ b/qa-core/src/environment.ts
@@ -11,6 +11,7 @@ if (!LOADED) {
const env = {
BUDIBASE_URL: process.env.BUDIBASE_URL,
ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL,
+ ACCOUNT_PORTAL_API_KEY: process.env.ACCOUNT_PORTAL_API_KEY,
BB_ADMIN_USER_EMAIL: process.env.BB_ADMIN_USER_EMAIL,
BB_ADMIN_USER_PASSWORD: process.env.BB_ADMIN_USER_PASSWORD,
POSTGRES_HOST: process.env.POSTGRES_HOST,
diff --git a/qa-core/src/internal-api/api/BudibaseInternalAPIClient.ts b/qa-core/src/internal-api/api/BudibaseInternalAPIClient.ts
index ab6c5f065e..a6921a75b2 100644
--- a/qa-core/src/internal-api/api/BudibaseInternalAPIClient.ts
+++ b/qa-core/src/internal-api/api/BudibaseInternalAPIClient.ts
@@ -18,7 +18,6 @@ class BudibaseInternalAPIClient {
if (!env.BUDIBASE_URL) {
throw new Error("Must set BUDIBASE_URL env var")
}
- this.host = `${env.ACCOUNT_PORTAL_URL}/api`
this.host = `${env.BUDIBASE_URL}/api`
this.state = state
}
@@ -53,14 +52,18 @@ class BudibaseInternalAPIClient {
body = await response.text()
}
- const message = `${method} ${url} - ${response.status}
- Response body: ${JSON.stringify(body)}
- Request body: ${requestOptions.body}`
+ const data = {
+ request: requestOptions.body,
+ response: body,
+ }
+ const message = `${method} ${url} - ${response.status}`
if (response.status > 499) {
- console.error(message)
+ console.error(message, data)
} else if (response.status >= 400) {
- console.warn(message)
+ console.warn(message, data)
+ } else {
+ console.debug(message, data)
}
return [response, body]
diff --git a/qa-core/src/internal-api/tests/applications/publish.spec.ts b/qa-core/src/internal-api/tests/applications/publish.spec.ts
index 3d5fa7c598..e614a0f2a4 100644
--- a/qa-core/src/internal-api/tests/applications/publish.spec.ts
+++ b/qa-core/src/internal-api/tests/applications/publish.spec.ts
@@ -36,7 +36,7 @@ describe("Internal API - Application creation, update, publish and delete", () =
const [syncResponse, sync] = await config.api.apps.sync(app.appId!)
expect(sync).toEqual({
- message: "App sync not required, app not deployed.",
+ message: "App sync completed successfully.",
})
})
diff --git a/qa-core/src/jest/globalSetup.ts b/qa-core/src/jest/globalSetup.ts
index a25c483173..e222e7c043 100644
--- a/qa-core/src/jest/globalSetup.ts
+++ b/qa-core/src/jest/globalSetup.ts
@@ -1,8 +1,8 @@
+import { DEFAULT_TENANT_ID, logging } from "@budibase/backend-core"
import { AccountInternalAPI } from "../account-api"
import * as fixtures from "../internal-api/fixtures"
import { BudibaseInternalAPI } from "../internal-api"
-import { DEFAULT_TENANT_ID, logging } from "@budibase/backend-core"
-import { CreateAccountRequest } from "@budibase/types"
+import { CreateAccountRequest, Feature } from "@budibase/types"
import env from "../environment"
import { APIRequestOpts } from "../types"
@@ -22,10 +22,35 @@ async function createAccount() {
const account = fixtures.accounts.generateAccount()
await accountsApi.accounts.validateEmail(account.email, API_OPTS)
await accountsApi.accounts.validateTenantId(account.tenantId, API_OPTS)
- await accountsApi.accounts.create(account, API_OPTS)
+ const [res, newAccount] = await accountsApi.accounts.create(account, API_OPTS)
+ await updateLicense(newAccount.accountId)
return account
}
+const UNLIMITED = { value: -1 }
+
+async function updateLicense(accountId: string) {
+ await accountsApi.licenses.updateLicense(accountId, {
+ overrides: {
+ // add all features
+ features: Object.values(Feature),
+ quotas: {
+ usage: {
+ monthly: {
+ automations: UNLIMITED,
+ },
+ static: {
+ rows: UNLIMITED,
+ users: UNLIMITED,
+ userGroups: UNLIMITED,
+ plugins: UNLIMITED,
+ },
+ },
+ },
+ },
+ })
+}
+
async function loginAsAdmin() {
const username = env.BB_ADMIN_USER_EMAIL!
const password = env.BB_ADMIN_USER_PASSWORD!
diff --git a/qa-core/src/jest/jestSetup.ts b/qa-core/src/jest/jestSetup.ts
index 7f0aeddaa3..5c86d5fd24 100644
--- a/qa-core/src/jest/jestSetup.ts
+++ b/qa-core/src/jest/jestSetup.ts
@@ -1 +1,4 @@
+import { logging } from "@budibase/backend-core"
+logging.LOG_CONTEXT = false
+
jest.setTimeout(60000)
diff --git a/qa-core/src/public-api/api/BudibasePublicAPIClient.ts b/qa-core/src/public-api/api/BudibasePublicAPIClient.ts
index d5fd6b9ef4..f4fe978812 100644
--- a/qa-core/src/public-api/api/BudibasePublicAPIClient.ts
+++ b/qa-core/src/public-api/api/BudibasePublicAPIClient.ts
@@ -51,14 +51,18 @@ class BudibasePublicAPIClient {
body = await response.text()
}
- const message = `${method} ${url} - ${response.status}
- Response body: ${JSON.stringify(body)}
- Request body: ${requestOptions.body}`
+ const data = {
+ request: requestOptions.body,
+ response: body,
+ }
+ const message = `${method} ${url} - ${response.status}`
if (response.status > 499) {
- console.error(message)
+ console.error(message, data)
} else if (response.status >= 400) {
- console.warn(message)
+ console.warn(message, data)
+ } else {
+ console.debug(message, data)
}
return [response, body]
diff --git a/qa-core/src/shared/BudibaseTestConfiguration.ts b/qa-core/src/shared/BudibaseTestConfiguration.ts
index 6adc1bf5c4..12a0f16138 100644
--- a/qa-core/src/shared/BudibaseTestConfiguration.ts
+++ b/qa-core/src/shared/BudibaseTestConfiguration.ts
@@ -3,9 +3,6 @@ import { AccountInternalAPI } from "../account-api"
import { CreateAppRequest, State } from "../types"
import * as fixtures from "../internal-api/fixtures"
-// TEMP
-import setup from "../jest/globalSetup"
-
export default class BudibaseTestConfiguration {
// apis
internalApi: BudibaseInternalAPI
@@ -23,11 +20,6 @@ export default class BudibaseTestConfiguration {
// LIFECYCLE
async beforeAll() {
- // TEMP - move back to single tenant when we integrate licensing with
- // the test run - need to use multiple tenants in cloud to get around
- // app limit restrictions
- await setup()
-
// @ts-ignore
this.state.tenantId = global.qa.tenantId
// @ts-ignore
diff --git a/qa-core/tsconfig.json b/qa-core/tsconfig.json
index 8cd0c30d46..227f60bd6f 100644
--- a/qa-core/tsconfig.json
+++ b/qa-core/tsconfig.json
@@ -16,7 +16,7 @@
"@budibase/types": ["../packages/types/src"],
"@budibase/backend-core": ["../packages/backend-core/src"],
"@budibase/backend-core/*": ["../packages/backend-core/*"],
- "@budibase/server/*": ["../packages/server/src/*"],
+ "@budibase/server/*": ["../packages/server/src/*"]
}
},
"ts-node": {
@@ -24,14 +24,8 @@
},
"references": [
{ "path": "../packages/types" },
- { "path": "../packages/backend-core" },
+ { "path": "../packages/backend-core" }
],
- "include": [
- "src/**/*",
- "package.json"
- ],
- "exclude": [
- "node_modules",
- "dist"
- ]
-}
\ No newline at end of file
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/scripts/link-dependencies.sh b/scripts/link-dependencies.sh
index 31d99fda3c..a3a75e879d 100755
--- a/scripts/link-dependencies.sh
+++ b/scripts/link-dependencies.sh
@@ -88,6 +88,6 @@ if [ -d "../account-portal" ]; then
echo "Linking bbui to account-portal"
yarn link "@budibase/bbui"
- echo "Linking frontend-core to account-portal"
- yarn link "@budibase/frontend-core"
+ echo "Linking frontend-core to account-portal"
+ yarn link "@budibase/frontend-core"
fi
diff --git a/yarn.lock b/yarn.lock
index c84f28b254..404952f815 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1486,14 +1486,15 @@
pouchdb-promise "^6.0.4"
through2 "^2.0.0"
-"@budibase/pro@2.5.4":
- version "2.5.4"
- resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.5.4.tgz#9368117a41b276ec97d3994e3ec67d9f2570a5bc"
- integrity sha512-xPNVlRFTcjpWn+oZCvrfgDd9SoslkUgJsS2Bnff+qDoWcTFz30KoOyZPAkSwXxX3Y8FmZRO9csl0AZa9TsUs7A==
+"@budibase/pro@2.5.6-alpha.32":
+ version "2.5.6-alpha.32"
+ resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.5.6-alpha.32.tgz#6d52da231efcacbb163a5af0cfe8284199ff659e"
+ integrity sha512-XzM55VcxpxTXCxYhLUOYvYqlCAPRPASJBbNonuRV6qUtXZxE5xSSEDnHQehhD8TrNP3QQ94DlCJ5DQCm/f3yJQ==
dependencies:
- "@budibase/backend-core" "2.5.4"
- "@budibase/string-templates" "2.3.20"
- "@budibase/types" "2.5.4"
+ "@budibase/backend-core" "2.5.6-alpha.32"
+ "@budibase/shared-core" "2.4.44-alpha.1"
+ "@budibase/string-templates" "2.4.44-alpha.1"
+ "@budibase/types" "2.5.6-alpha.32"
"@koa/router" "8.0.8"
bull "4.10.1"
joi "17.6.0"
@@ -1501,6 +1502,15 @@
lru-cache "^7.14.1"
memorystream "^0.3.1"
node-fetch "^2.6.1"
+ scim-patch "^0.7.0"
+ scim2-parse-filter "^0.2.8"
+
+"@budibase/shared-core@2.4.44-alpha.1":
+ version "2.4.44-alpha.1"
+ resolved "https://registry.yarnpkg.com/@budibase/shared-core/-/shared-core-2.4.44-alpha.1.tgz#3d499e40e7e6c646e13a87cd08e01ba116c2ff1d"
+ integrity sha512-cN8LaDczijtsfWUYiXC4sg9Z+US4020i3Mb8TwCbf8TQyA1b06U5PwPCp+GHVA/wDFqfwcpcE1GXf8GwVuYs7A==
+ dependencies:
+ "@budibase/types" "2.4.44-alpha.1"
"@budibase/standard-components@^0.9.139":
version "0.9.139"
@@ -1520,10 +1530,10 @@
svelte-apexcharts "^1.0.2"
svelte-flatpickr "^3.1.0"
-"@budibase/string-templates@2.3.20":
- version "2.3.20"
- resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-2.3.20.tgz#35f74b6f515e8127cc375ee0a4679b0a7c117588"
- integrity sha512-wMKau3IzVF6M+dRu99aKV1yMdrrK5lghVm9qYtR1B163SMbHEwC8JmZFGPLIi1XsG0T+vw+xfcemfJ2zcATWwg==
+"@budibase/string-templates@2.4.44-alpha.1":
+ version "2.4.44-alpha.1"
+ resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-2.4.44-alpha.1.tgz#6c2aee594d16eac1f173c509e087a817dd3172f0"
+ integrity sha512-4gC2+0kccK0ilLnd0i/dmJzC0Ur7UgSAmV6zbzDDYNL4spU0qSy5VhBh7E3qKieg5RKMMzbpXLMWERpoPLlnqA==
dependencies:
"@budibase/handlebars-helpers" "^0.11.8"
dayjs "^1.10.4"
@@ -1532,6 +1542,11 @@
lodash "^4.17.20"
vm2 "^3.9.4"
+"@budibase/types@2.4.44-alpha.1":
+ version "2.4.44-alpha.1"
+ resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.4.44-alpha.1.tgz#1679657aa180d9c59afa1dffa611bff0638bd933"
+ integrity sha512-Sq+8HfM75EBMoOvKYFwELdlxmVN6wNZMofDjT/2G+9aF+Zfe5Tzw69C+unmdBgcGGjGCHEYWSz4mF0v8FPAGbg==
+
"@bull-board/api@3.7.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@bull-board/api/-/api-3.7.0.tgz#231f687187c0cb34e0b97f463917b6aaeb4ef6af"
@@ -1728,6 +1743,11 @@
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz#cf91e86df127aa3d141744edafcba0abdc577d23"
integrity sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==
+"@esbuild/android-arm64@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.17.tgz#164b054d58551f8856285f386e1a8f45d9ba3a31"
+ integrity sha512-jaJ5IlmaDLFPNttv0ofcwy/cfeY4bh/n705Tgh+eLObbGtQBK3EPAu+CzL95JVE4nFAliyrnEu0d32Q5foavqg==
+
"@esbuild/android-arm@0.15.18":
version "0.15.18"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.18.tgz#266d40b8fdcf87962df8af05b76219bc786b4f80"
@@ -1738,46 +1758,91 @@
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.16.17.tgz#025b6246d3f68b7bbaa97069144fb5fb70f2fff2"
integrity sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==
+"@esbuild/android-arm@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.17.tgz#1b3b5a702a69b88deef342a7a80df4c894e4f065"
+ integrity sha512-E6VAZwN7diCa3labs0GYvhEPL2M94WLF8A+czO8hfjREXxba8Ng7nM5VxV+9ihNXIY1iQO1XxUU4P7hbqbICxg==
+
"@esbuild/android-x64@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.16.17.tgz#c820e0fef982f99a85c4b8bfdd582835f04cd96e"
integrity sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==
+"@esbuild/android-x64@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.17.tgz#6781527e3c4ea4de532b149d18a2167f06783e7f"
+ integrity sha512-446zpfJ3nioMC7ASvJB1pszHVskkw4u/9Eu8s5yvvsSDTzYh4p4ZIRj0DznSl3FBF0Z/mZfrKXTtt0QCoFmoHA==
+
"@esbuild/darwin-arm64@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz#edef4487af6b21afabba7be5132c26d22379b220"
integrity sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==
+"@esbuild/darwin-arm64@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.17.tgz#c5961ef4d3c1cc80dafe905cc145b5a71d2ac196"
+ integrity sha512-m/gwyiBwH3jqfUabtq3GH31otL/0sE0l34XKpSIqR7NjQ/XHQ3lpmQHLHbG8AHTGCw8Ao059GvV08MS0bhFIJQ==
+
"@esbuild/darwin-x64@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz#42829168730071c41ef0d028d8319eea0e2904b4"
integrity sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==
+"@esbuild/darwin-x64@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.17.tgz#b81f3259cc349691f67ae30f7b333a53899b3c20"
+ integrity sha512-4utIrsX9IykrqYaXR8ob9Ha2hAY2qLc6ohJ8c0CN1DR8yWeMrTgYFjgdeQ9LIoTOfLetXjuCu5TRPHT9yKYJVg==
+
"@esbuild/freebsd-arm64@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz#1f4af488bfc7e9ced04207034d398e793b570a27"
integrity sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==
+"@esbuild/freebsd-arm64@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.17.tgz#db846ad16cf916fd3acdda79b85ea867cb100e87"
+ integrity sha512-4PxjQII/9ppOrpEwzQ1b0pXCsFLqy77i0GaHodrmzH9zq2/NEhHMAMJkJ635Ns4fyJPFOlHMz4AsklIyRqFZWA==
+
"@esbuild/freebsd-x64@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz#636306f19e9bc981e06aa1d777302dad8fddaf72"
integrity sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==
+"@esbuild/freebsd-x64@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.17.tgz#4dd99acbaaba00949d509e7c144b1b6ef9e1815b"
+ integrity sha512-lQRS+4sW5S3P1sv0z2Ym807qMDfkmdhUYX30GRBURtLTrJOPDpoU0kI6pVz1hz3U0+YQ0tXGS9YWveQjUewAJw==
+
"@esbuild/linux-arm64@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz#a003f7ff237c501e095d4f3a09e58fc7b25a4aca"
integrity sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==
+"@esbuild/linux-arm64@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.17.tgz#7f9274140b2bb9f4230dbbfdf5dc2761215e30f6"
+ integrity sha512-2+pwLx0whKY1/Vqt8lyzStyda1v0qjJ5INWIe+d8+1onqQxHLLi3yr5bAa4gvbzhZqBztifYEu8hh1La5+7sUw==
+
"@esbuild/linux-arm@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz#b591e6a59d9c4fe0eeadd4874b157ab78cf5f196"
integrity sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==
+"@esbuild/linux-arm@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.17.tgz#5c8e44c2af056bb2147cf9ad13840220bcb8948b"
+ integrity sha512-biDs7bjGdOdcmIk6xU426VgdRUpGg39Yz6sT9Xp23aq+IEHDb/u5cbmu/pAANpDB4rZpY/2USPhCA+w9t3roQg==
+
"@esbuild/linux-ia32@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz#24333a11027ef46a18f57019450a5188918e2a54"
integrity sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==
+"@esbuild/linux-ia32@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.17.tgz#18a6b3798658be7f46e9873fa0c8d4bec54c9212"
+ integrity sha512-IBTTv8X60dYo6P2t23sSUYym8fGfMAiuv7PzJ+0LcdAndZRzvke+wTVxJeCq4WgjppkOpndL04gMZIFvwoU34Q==
+
"@esbuild/linux-loong64@0.15.18":
version "0.15.18"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz#128b76ecb9be48b60cf5cfc1c63a4f00691a3239"
@@ -1788,61 +1853,121 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz#d5ad459d41ed42bbd4d005256b31882ec52227d8"
integrity sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==
+"@esbuild/linux-loong64@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.17.tgz#a8d93514a47f7b4232716c9f02aeb630bae24c40"
+ integrity sha512-WVMBtcDpATjaGfWfp6u9dANIqmU9r37SY8wgAivuKmgKHE+bWSuv0qXEFt/p3qXQYxJIGXQQv6hHcm7iWhWjiw==
+
"@esbuild/linux-mips64el@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz#4e5967a665c38360b0a8205594377d4dcf9c3726"
integrity sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==
+"@esbuild/linux-mips64el@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.17.tgz#4784efb1c3f0eac8133695fa89253d558149ee1b"
+ integrity sha512-2kYCGh8589ZYnY031FgMLy0kmE4VoGdvfJkxLdxP4HJvWNXpyLhjOvxVsYjYZ6awqY4bgLR9tpdYyStgZZhi2A==
+
"@esbuild/linux-ppc64@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz#206443a02eb568f9fdf0b438fbd47d26e735afc8"
integrity sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==
+"@esbuild/linux-ppc64@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.17.tgz#ef6558ec5e5dd9dc16886343e0ccdb0699d70d3c"
+ integrity sha512-KIdG5jdAEeAKogfyMTcszRxy3OPbZhq0PPsW4iKKcdlbk3YE4miKznxV2YOSmiK/hfOZ+lqHri3v8eecT2ATwQ==
+
"@esbuild/linux-riscv64@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz#c351e433d009bf256e798ad048152c8d76da2fc9"
integrity sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==
+"@esbuild/linux-riscv64@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.17.tgz#13a87fdbcb462c46809c9d16bcf79817ecf9ce6f"
+ integrity sha512-Cj6uWLBR5LWhcD/2Lkfg2NrkVsNb2sFM5aVEfumKB2vYetkA/9Uyc1jVoxLZ0a38sUhFk4JOVKH0aVdPbjZQeA==
+
"@esbuild/linux-s390x@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz#661f271e5d59615b84b6801d1c2123ad13d9bd87"
integrity sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==
+"@esbuild/linux-s390x@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.17.tgz#83cb16d1d3ac0dca803b3f031ba3dc13f1ec7ade"
+ integrity sha512-lK+SffWIr0XsFf7E0srBjhpkdFVJf3HEgXCwzkm69kNbRar8MhezFpkIwpk0qo2IOQL4JE4mJPJI8AbRPLbuOQ==
+
"@esbuild/linux-x64@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz#e4ba18e8b149a89c982351443a377c723762b85f"
integrity sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==
+"@esbuild/linux-x64@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.17.tgz#7bc400568690b688e20a0c94b2faabdd89ae1a79"
+ integrity sha512-XcSGTQcWFQS2jx3lZtQi7cQmDYLrpLRyz1Ns1DzZCtn898cWfm5Icx/DEWNcTU+T+tyPV89RQtDnI7qL2PObPg==
+
"@esbuild/netbsd-x64@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz#7d4f4041e30c5c07dd24ffa295c73f06038ec775"
integrity sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==
+"@esbuild/netbsd-x64@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.17.tgz#1b5dcfbc4bfba80e67a11e9148de836af5b58b6c"
+ integrity sha512-RNLCDmLP5kCWAJR+ItLM3cHxzXRTe4N00TQyQiimq+lyqVqZWGPAvcyfUBM0isE79eEZhIuGN09rAz8EL5KdLA==
+
"@esbuild/openbsd-x64@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz#970fa7f8470681f3e6b1db0cc421a4af8060ec35"
integrity sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==
+"@esbuild/openbsd-x64@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.17.tgz#e275098902291149a5dcd012c9ea0796d6b7adff"
+ integrity sha512-PAXswI5+cQq3Pann7FNdcpSUrhrql3wKjj3gVkmuz6OHhqqYxKvi6GgRBoaHjaG22HV/ZZEgF9TlS+9ftHVigA==
+
"@esbuild/sunos-x64@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz#abc60e7c4abf8b89fb7a4fe69a1484132238022c"
integrity sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==
+"@esbuild/sunos-x64@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.17.tgz#10603474866f64986c0370a2d4fe5a2bb7fee4f5"
+ integrity sha512-V63egsWKnx/4V0FMYkr9NXWrKTB5qFftKGKuZKFIrAkO/7EWLFnbBZNM1CvJ6Sis+XBdPws2YQSHF1Gqf1oj/Q==
+
"@esbuild/win32-arm64@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz#7b0ff9e8c3265537a7a7b1fd9a24e7bd39fcd87a"
integrity sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==
+"@esbuild/win32-arm64@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.17.tgz#521a6d97ee0f96b7c435930353cc4e93078f0b54"
+ integrity sha512-YtUXLdVnd6YBSYlZODjWzH+KzbaubV0YVd6UxSfoFfa5PtNJNaW+1i+Hcmjpg2nEe0YXUCNF5bkKy1NnBv1y7Q==
+
"@esbuild/win32-ia32@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz#e90fe5267d71a7b7567afdc403dfd198c292eb09"
integrity sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==
+"@esbuild/win32-ia32@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.17.tgz#56f88462ebe82dad829dc2303175c0e0ccd8e38e"
+ integrity sha512-yczSLRbDdReCO74Yfc5tKG0izzm+lPMYyO1fFTcn0QNwnKmc3K+HdxZWLGKg4pZVte7XVgcFku7TIZNbWEJdeQ==
+
"@esbuild/win32-x64@0.16.17":
version "0.16.17"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz#c5a1a4bfe1b57f0c3e61b29883525c6da3e5c091"
integrity sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==
+"@esbuild/win32-x64@0.17.17":
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.17.tgz#2b577b976e6844106715bbe0cdc57cd1528063f9"
+ integrity sha512-FNZw7H3aqhF9OyRQbDDnzUApDXfC1N6fgBhkqEO2jvYCJ+DxMTfZVqg3AX0R1khg1wHTBRD5SdcibSJ+XF6bFg==
+
"@eslint/eslintrc@^0.4.3":
version "0.4.3"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
@@ -4553,7 +4678,7 @@
lodash "^4.17.15"
redent "^3.0.0"
-"@testing-library/svelte@^3.0.0":
+"@testing-library/svelte@^3.2.2":
version "3.2.2"
resolved "https://registry.yarnpkg.com/@testing-library/svelte/-/svelte-3.2.2.tgz#aa8ca6bde3136593c5f8b4554e3634d7adc9bdc5"
integrity sha512-IKwZgqbekC3LpoRhSwhd0JswRGxKdAGkf39UiDXTywK61YyLXbCYoR831e/UUC6EeNW4hiHPY+2WuovxOgI5sw==
@@ -4701,6 +4826,18 @@
resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==
+"@types/chai-subset@^1.3.3":
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94"
+ integrity sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==
+ dependencies:
+ "@types/chai" "*"
+
+"@types/chai@*", "@types/chai@^4.3.4":
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.4.tgz#e913e8175db8307d78b4e8fa690408ba6b65dee4"
+ integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==
+
"@types/chance@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.1.3.tgz#d19fe9391288d60fdccd87632bfc9ab2b4523fea"
@@ -5574,6 +5711,41 @@
"@typescript-eslint/types" "5.53.0"
eslint-visitor-keys "^3.3.0"
+"@vitest/expect@0.29.8":
+ version "0.29.8"
+ resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.29.8.tgz#6ecdd031b4ea8414717d10b65ccd800908384612"
+ integrity sha512-xlcVXn5I5oTq6NiZSY3ykyWixBxr5mG8HYtjvpgg6KaqHm0mvhX18xuwl5YGxIRNt/A5jidd7CWcNHrSvgaQqQ==
+ dependencies:
+ "@vitest/spy" "0.29.8"
+ "@vitest/utils" "0.29.8"
+ chai "^4.3.7"
+
+"@vitest/runner@0.29.8":
+ version "0.29.8"
+ resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.29.8.tgz#ede8a7be8a074ea1180bc1d1595bd879ed15971c"
+ integrity sha512-FzdhnRDwEr/A3Oo1jtIk/B952BBvP32n1ObMEb23oEJNO+qO5cBet6M2XWIDQmA7BDKGKvmhUf2naXyp/2JEwQ==
+ dependencies:
+ "@vitest/utils" "0.29.8"
+ p-limit "^4.0.0"
+ pathe "^1.1.0"
+
+"@vitest/spy@0.29.8":
+ version "0.29.8"
+ resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.29.8.tgz#2e0c3b30e04d317b2197e3356234448aa432e131"
+ integrity sha512-VdjBe9w34vOMl5I5mYEzNX8inTxrZ+tYUVk9jxaZJmHFwmDFC/GV3KBFTA/JKswr3XHvZL+FE/yq5EVhb6pSAw==
+ dependencies:
+ tinyspy "^1.0.2"
+
+"@vitest/utils@0.29.8":
+ version "0.29.8"
+ resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.29.8.tgz#423da85fd0c6633f3ab496cf7d2fc0119b850df8"
+ integrity sha512-qGzuf3vrTbnoY+RjjVVIBYfuWMjn3UMUqyQtdGNZ6ZIIyte7B37exj6LaVkrZiUTvzSadVvO/tJm8AEgbGCBPg==
+ dependencies:
+ cli-truncate "^3.1.0"
+ diff "^5.1.0"
+ loupe "^2.3.6"
+ pretty-format "^27.5.1"
+
"@webassemblyjs/ast@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
@@ -5760,7 +5932,7 @@ a-sync-waterfall@^1.0.0:
resolved "https://registry.yarnpkg.com/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz#75b6b6aa72598b497a125e7a2770f14f4c8a1fa7"
integrity sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==
-abab@^2.0.3, abab@^2.0.5:
+abab@^2.0.3, abab@^2.0.5, abab@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
@@ -5839,6 +6011,14 @@ acorn-globals@^6.0.0:
acorn "^7.1.1"
acorn-walk "^7.1.1"
+acorn-globals@^7.0.0:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-7.0.1.tgz#0dbf05c44fa7c94332914c02066d5beff62c40c3"
+ integrity sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==
+ dependencies:
+ acorn "^8.1.0"
+ acorn-walk "^8.0.2"
+
acorn-import-assertions@^1.7.6:
version "1.8.0"
resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9"
@@ -5854,7 +6034,7 @@ acorn-walk@^7.1.1:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
-acorn-walk@^8.1.1, acorn-walk@^8.2.0:
+acorn-walk@^8.0.2, acorn-walk@^8.1.1, acorn-walk@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
@@ -5874,7 +6054,7 @@ acorn@^7.1.1, acorn@^7.3.1, acorn@^7.4.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
-acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0, acorn@^8.7.1:
+acorn@^8.1.0, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2:
version "8.8.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
@@ -6019,6 +6199,11 @@ ansi-regex@^5.0.0, ansi-regex@^5.0.1:
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+ansi-regex@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a"
+ integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==
+
ansi-styles@^3.2.0, ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@@ -6038,6 +6223,11 @@ ansi-styles@^5.0.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
+ansi-styles@^6.0.0:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
+ integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
+
any-base@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/any-base/-/any-base-1.1.0.tgz#ae101a62bc08a597b4c9ab5b7089d456630549fe"
@@ -6336,6 +6526,11 @@ assert-plus@1.0.0, assert-plus@^1.0.0:
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==
+assertion-error@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
+ integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==
+
assign-symbols@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
@@ -7254,6 +7449,11 @@ bytes@3.1.2, bytes@^3.0.0:
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
+cac@^6.7.14:
+ version "6.7.14"
+ resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959"
+ integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==
+
cacache@^16.0.0, cacache@^16.1.0:
version "16.1.3"
resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.1.3.tgz#a02b9f34ecfaf9a78c9f4bc16fceb94d5d67a38e"
@@ -7433,6 +7633,19 @@ caseless@~0.12.0:
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==
+chai@^4.3.7:
+ version "4.3.7"
+ resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.7.tgz#ec63f6df01829088e8bf55fca839bcd464a8ec51"
+ integrity sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==
+ dependencies:
+ assertion-error "^1.1.0"
+ check-error "^1.0.2"
+ deep-eql "^4.1.2"
+ get-func-name "^2.0.0"
+ loupe "^2.3.1"
+ pathval "^1.1.1"
+ type-detect "^4.0.5"
+
chalk@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
@@ -7491,6 +7704,11 @@ cheap-watch@^1.0.2:
resolved "https://registry.yarnpkg.com/cheap-watch/-/cheap-watch-1.0.4.tgz#0bcb4a3a8fbd9d5327936493f6b56baa668d8fef"
integrity sha512-QR/9FrtRL5fjfUJBhAKCdi0lSRQ3rVRRum3GF9wDKp2TJbEIMGhUEr2yU8lORzm9Isdjx7/k9S0DFDx+z5VGtw==
+check-error@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
+ integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==
+
check-more-types@2.24.0, check-more-types@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
@@ -7620,6 +7838,14 @@ cli-truncate@^2.1.0:
slice-ansi "^3.0.0"
string-width "^4.2.0"
+cli-truncate@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-3.1.0.tgz#3f23ab12535e3d73e839bb43e73c9de487db1389"
+ integrity sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==
+ dependencies:
+ slice-ansi "^5.0.0"
+ string-width "^5.0.0"
+
cli-width@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
@@ -8201,7 +8427,7 @@ cookiejar@^2.1.2, cookiejar@^2.1.3:
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b"
integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==
-cookies@~0.8.0:
+cookies@0.8.0, cookies@~0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90"
integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==
@@ -8512,6 +8738,13 @@ cssstyle@^2.3.0:
dependencies:
cssom "~0.3.6"
+cssstyle@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-3.0.0.tgz#17ca9c87d26eac764bb8cfd00583cff21ce0277a"
+ integrity sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==
+ dependencies:
+ rrweb-cssom "^0.6.0"
+
csvtojson@2.0.10:
version "2.0.10"
resolved "https://registry.yarnpkg.com/csvtojson/-/csvtojson-2.0.10.tgz#11e7242cc630da54efce7958a45f443210357574"
@@ -8637,6 +8870,15 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
+data-urls@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-4.0.0.tgz#333a454eca6f9a5b7b0f1013ff89074c3f522dd4"
+ integrity sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==
+ dependencies:
+ abab "^2.0.6"
+ whatwg-mimetype "^3.0.0"
+ whatwg-url "^12.0.0"
+
date-fns@^2.29.1:
version "2.29.3"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
@@ -8657,7 +8899,7 @@ dateformat@^4.5.1, dateformat@^4.6.3:
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
-dayjs@^1.10.4, dayjs@^1.10.5, dayjs@^1.11.2:
+dayjs@^1.10.4, dayjs@^1.10.5, dayjs@^1.11.2, dayjs@^1.11.7:
version "1.11.7"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2"
integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==
@@ -8741,7 +8983,7 @@ decamelize@^1.1.0, decamelize@^1.2.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
-decimal.js@^10.2.1:
+decimal.js@^10.2.1, decimal.js@^10.4.3:
version "10.4.3"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==
@@ -8830,6 +9072,13 @@ dedent@0.7.0, dedent@^0.7.0:
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==
+deep-eql@^4.1.2:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d"
+ integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==
+ dependencies:
+ type-detect "^4.0.0"
+
deep-equal@^2.0.5:
version "2.2.0"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.0.tgz#5caeace9c781028b9ff459f33b779346637c43e6"
@@ -9268,7 +9517,7 @@ diff@^4.0.1:
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
-diff@^5.0.0:
+diff@^5.0.0, diff@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
@@ -9383,6 +9632,13 @@ domexception@^2.0.1:
dependencies:
webidl-conversions "^5.0.0"
+domexception@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673"
+ integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==
+ dependencies:
+ webidl-conversions "^7.0.0"
+
domhandler@^4.2.0, domhandler@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c"
@@ -9496,6 +9752,11 @@ duplexify@^4.0.0, duplexify@^4.1.2:
readable-stream "^3.1.1"
stream-shift "^1.0.0"
+eastasianwidth@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
+ integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
+
easymde@^2.16.1:
version "2.18.0"
resolved "https://registry.yarnpkg.com/easymde/-/easymde-2.18.0.tgz#ff1397d07329b1a7b9187d2d0c20766fa16b3b1b"
@@ -9648,6 +9909,11 @@ emoji-regex@^8.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+emoji-regex@^9.2.2:
+ version "9.2.2"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
+ integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
+
emojis-list@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
@@ -10084,6 +10350,34 @@ esbuild@^0.16.17:
"@esbuild/win32-ia32" "0.16.17"
"@esbuild/win32-x64" "0.16.17"
+esbuild@^0.17.5:
+ version "0.17.17"
+ resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.17.tgz#fa906ab11b11d2ed4700f494f4f764229b25c916"
+ integrity sha512-/jUywtAymR8jR4qsa2RujlAF7Krpt5VWi72Q2yuLD4e/hvtNcFQ0I1j8m/bxq238pf3/0KO5yuXNpuLx8BE1KA==
+ optionalDependencies:
+ "@esbuild/android-arm" "0.17.17"
+ "@esbuild/android-arm64" "0.17.17"
+ "@esbuild/android-x64" "0.17.17"
+ "@esbuild/darwin-arm64" "0.17.17"
+ "@esbuild/darwin-x64" "0.17.17"
+ "@esbuild/freebsd-arm64" "0.17.17"
+ "@esbuild/freebsd-x64" "0.17.17"
+ "@esbuild/linux-arm" "0.17.17"
+ "@esbuild/linux-arm64" "0.17.17"
+ "@esbuild/linux-ia32" "0.17.17"
+ "@esbuild/linux-loong64" "0.17.17"
+ "@esbuild/linux-mips64el" "0.17.17"
+ "@esbuild/linux-ppc64" "0.17.17"
+ "@esbuild/linux-riscv64" "0.17.17"
+ "@esbuild/linux-s390x" "0.17.17"
+ "@esbuild/linux-x64" "0.17.17"
+ "@esbuild/netbsd-x64" "0.17.17"
+ "@esbuild/openbsd-x64" "0.17.17"
+ "@esbuild/sunos-x64" "0.17.17"
+ "@esbuild/win32-arm64" "0.17.17"
+ "@esbuild/win32-ia32" "0.17.17"
+ "@esbuild/win32-x64" "0.17.17"
+
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -10835,7 +11129,7 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
-fast-redact@^3.0.0:
+fast-redact@^3.0.0, fast-redact@^3.1.1:
version "3.1.2"
resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa"
integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==
@@ -11580,6 +11874,11 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+get-func-name@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
+ integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==
+
get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f"
@@ -12413,6 +12712,13 @@ html-encoding-sniffer@^2.0.1:
dependencies:
whatwg-encoding "^1.0.5"
+html-encoding-sniffer@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9"
+ integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==
+ dependencies:
+ whatwg-encoding "^2.0.0"
+
html-escaper@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
@@ -12549,7 +12855,7 @@ http2-wrapper@^1.0.0-beta.5.2:
quick-lru "^5.1.1"
resolve-alpn "^1.0.0"
-https-proxy-agent@5, https-proxy-agent@^5.0.0:
+https-proxy-agent@5, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
@@ -12586,6 +12892,13 @@ iconv-lite@0.4.24, iconv-lite@^0.4.15, iconv-lite@^0.4.24, iconv-lite@^0.4.5:
dependencies:
safer-buffer ">= 2.1.2 < 3"
+iconv-lite@0.6.3, iconv-lite@^0.6.2, iconv-lite@^0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
+ integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
iconv-lite@^0.5.0:
version "0.5.2"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.5.2.tgz#af6d628dccfb463b7364d97f715e4b74b8c8c2b8"
@@ -12593,13 +12906,6 @@ iconv-lite@^0.5.0:
dependencies:
safer-buffer ">= 2.1.2 < 3"
-iconv-lite@^0.6.2, iconv-lite@^0.6.3:
- version "0.6.3"
- resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
- integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
- dependencies:
- safer-buffer ">= 2.1.2 < 3.0.0"
-
icss-replace-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
@@ -13194,6 +13500,11 @@ is-fullwidth-code-point@^3.0.0:
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+is-fullwidth-code-point@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88"
+ integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==
+
is-function@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.2.tgz#4f097f30abf6efadac9833b17ca5dc03f8144e08"
@@ -14957,6 +15268,38 @@ jsdom@^16.0.1, jsdom@^16.4.0:
ws "^7.4.6"
xml-name-validator "^3.0.0"
+jsdom@^21.1.1:
+ version "21.1.1"
+ resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-21.1.1.tgz#ab796361e3f6c01bcfaeda1fea3c06197ac9d8ae"
+ integrity sha512-Jjgdmw48RKcdAIQyUD1UdBh2ecH7VqwaXPN3ehoZN6MqgVbMn+lRm1aAT1AsdJRAJpwfa4IpwgzySn61h2qu3w==
+ dependencies:
+ abab "^2.0.6"
+ acorn "^8.8.2"
+ acorn-globals "^7.0.0"
+ cssstyle "^3.0.0"
+ data-urls "^4.0.0"
+ decimal.js "^10.4.3"
+ domexception "^4.0.0"
+ escodegen "^2.0.0"
+ form-data "^4.0.0"
+ html-encoding-sniffer "^3.0.0"
+ http-proxy-agent "^5.0.0"
+ https-proxy-agent "^5.0.1"
+ is-potential-custom-element-name "^1.0.1"
+ nwsapi "^2.2.2"
+ parse5 "^7.1.2"
+ rrweb-cssom "^0.6.0"
+ saxes "^6.0.0"
+ symbol-tree "^3.2.4"
+ tough-cookie "^4.1.2"
+ w3c-xmlserializer "^4.0.0"
+ webidl-conversions "^7.0.0"
+ whatwg-encoding "^2.0.0"
+ whatwg-mimetype "^3.0.0"
+ whatwg-url "^12.0.1"
+ ws "^8.13.0"
+ xml-name-validator "^4.0.0"
+
jsesc@^2.5.1:
version "2.5.2"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
@@ -15940,6 +16283,11 @@ loader-utils@^3.2.0:
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576"
integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==
+local-pkg@^0.4.2:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.4.3.tgz#0ff361ab3ae7f1c19113d9bb97b98b905dbc4963"
+ integrity sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==
+
locate-path@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@@ -16214,6 +16562,13 @@ loose-envify@^1.4.0:
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
+loupe@^2.3.1, loupe@^2.3.6:
+ version "2.3.6"
+ resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.6.tgz#76e4af498103c532d1ecc9be102036a21f787b53"
+ integrity sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==
+ dependencies:
+ get-func-name "^2.0.0"
+
lowercase-keys@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
@@ -16874,6 +17229,16 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+mlly@^1.1.0, mlly@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.2.0.tgz#f0f6c2fc8d2d12ea6907cd869066689b5031b613"
+ integrity sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==
+ dependencies:
+ acorn "^8.8.2"
+ pathe "^1.1.0"
+ pkg-types "^1.0.2"
+ ufo "^1.1.1"
+
mochawesome-merge@^4.2.1:
version "4.3.0"
resolved "https://registry.yarnpkg.com/mochawesome-merge/-/mochawesome-merge-4.3.0.tgz#7cc5d5730c7d8d1b034c8d8fecf3cd36e0010297"
@@ -17126,6 +17491,11 @@ nanoid@^3.3.4:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
+nanoid@^3.3.6:
+ version "3.3.6"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
+ integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
+
nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@@ -17742,7 +18112,7 @@ nunjucks@^3.2.3:
asap "^2.0.3"
commander "^5.1.0"
-nwsapi@^2.2.0:
+nwsapi@^2.2.0, nwsapi@^2.2.2:
version "2.2.4"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.4.tgz#fd59d5e904e8e1f03c25a7d5a15cfa16c714a1e5"
integrity sha512-NHj4rzRo0tQdijE9ZqAx6kYDcoRwYwSYzCA8MY3JzfxlrvEU0jhnhJT9BhqhJs7I/dKcrDm6TyulaRqZPIhN5g==
@@ -18217,6 +18587,13 @@ p-limit@^3.0.2, p-limit@^3.1.0:
dependencies:
yocto-queue "^0.1.0"
+p-limit@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644"
+ integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==
+ dependencies:
+ yocto-queue "^1.0.0"
+
p-locate@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
@@ -18508,6 +18885,13 @@ parse5@6.0.1:
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
+parse5@^7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32"
+ integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==
+ dependencies:
+ entities "^4.4.0"
+
parseurl@^1.3.2, parseurl@^1.3.3, parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@@ -18673,6 +19057,16 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+pathe@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.0.tgz#e2e13f6c62b31a3289af4ba19886c230f295ec03"
+ integrity sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==
+
+pathval@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d"
+ integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==
+
pause-stream@0.0.11, pause-stream@~0.0.11:
version "0.0.11"
resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
@@ -18809,7 +19203,7 @@ pinkie@^2.0.0:
resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==
-pino-abstract-transport@^1.0.0:
+pino-abstract-transport@^1.0.0, pino-abstract-transport@v1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3"
integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==
@@ -18825,6 +19219,16 @@ pino-abstract-transport@v0.5.0:
duplexify "^4.1.2"
split2 "^4.0.0"
+pino-http@8.3.3:
+ version "8.3.3"
+ resolved "https://registry.yarnpkg.com/pino-http/-/pino-http-8.3.3.tgz#2b140e734bfc6babe0df272a43bb8f36f2b525c0"
+ integrity sha512-p4umsNIXXVu95HD2C8wie/vXH7db5iGRpc+yj1/ZQ3sRtTQLXNjoS6Be5+eI+rQbqCRxen/7k/KSN+qiZubGDw==
+ dependencies:
+ get-caller-file "^2.0.5"
+ pino "^8.0.0"
+ pino-std-serializers "^6.0.0"
+ process-warning "^2.0.0"
+
pino-http@^6.5.0:
version "6.6.0"
resolved "https://registry.yarnpkg.com/pino-http/-/pino-http-6.6.0.tgz#d0a1deacada8c93327fdaa48f5bdc94bc43d3407"
@@ -18870,7 +19274,42 @@ pino-std-serializers@^5.0.0:
resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-5.6.0.tgz#31b141155d6520967c5ec72944d08fb45c490fd3"
integrity sha512-VdUXCw8gO+xhir7sFuoYSjTnzB+TMDGxhAC/ph3YS3sdHnXNdsK0wMtADNUltfeGkn2KDxEM21fnjF3RwXyC8A==
-pino@7.11.0, pino@^7.5.0:
+pino-std-serializers@^6.0.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.2.0.tgz#169048c0df3f61352fce56aeb7fb962f1b66ab43"
+ integrity sha512-IWgSzUL8X1w4BIWTwErRgtV8PyOGOOi60uqv0oKuS/fOA8Nco/OeI6lBuc4dyP8MMfdFwyHqTMcBIA7nDiqEqA==
+
+pino@8.11.0, pino@^8.0.0:
+ version "8.11.0"
+ resolved "https://registry.yarnpkg.com/pino/-/pino-8.11.0.tgz#2a91f454106b13e708a66c74ebc1c2ab7ab38498"
+ integrity sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg==
+ dependencies:
+ atomic-sleep "^1.0.0"
+ fast-redact "^3.1.1"
+ on-exit-leak-free "^2.1.0"
+ pino-abstract-transport v1.0.0
+ pino-std-serializers "^6.0.0"
+ process-warning "^2.0.0"
+ quick-format-unescaped "^4.0.3"
+ real-require "^0.2.0"
+ safe-stable-stringify "^2.3.1"
+ sonic-boom "^3.1.0"
+ thread-stream "^2.0.0"
+
+pino@^6.11.2:
+ version "6.14.0"
+ resolved "https://registry.yarnpkg.com/pino/-/pino-6.14.0.tgz#b745ea87a99a6c4c9b374e4f29ca7910d4c69f78"
+ integrity sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==
+ dependencies:
+ fast-redact "^3.0.0"
+ fast-safe-stringify "^2.0.8"
+ flatstr "^1.0.12"
+ pino-std-serializers "^3.1.0"
+ process-warning "^1.0.0"
+ quick-format-unescaped "^4.0.3"
+ sonic-boom "^1.0.2"
+
+pino@^7.5.0:
version "7.11.0"
resolved "https://registry.yarnpkg.com/pino/-/pino-7.11.0.tgz#0f0ea5c4683dc91388081d44bff10c83125066f6"
integrity sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==
@@ -18887,19 +19326,6 @@ pino@7.11.0, pino@^7.5.0:
sonic-boom "^2.2.1"
thread-stream "^0.15.1"
-pino@^6.11.2:
- version "6.14.0"
- resolved "https://registry.yarnpkg.com/pino/-/pino-6.14.0.tgz#b745ea87a99a6c4c9b374e4f29ca7910d4c69f78"
- integrity sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==
- dependencies:
- fast-redact "^3.0.0"
- fast-safe-stringify "^2.0.8"
- flatstr "^1.0.12"
- pino-std-serializers "^3.1.0"
- process-warning "^1.0.0"
- quick-format-unescaped "^4.0.3"
- sonic-boom "^1.0.2"
-
pirates@^4.0.1, pirates@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b"
@@ -18933,6 +19359,15 @@ pkg-fetch@3.4.2:
tar-fs "^2.1.1"
yargs "^16.2.0"
+pkg-types@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.0.2.tgz#c233efc5210a781e160e0cafd60c0d0510a4b12e"
+ integrity sha512-hM58GKXOcj8WTqUXnsQyJYXdeAPbythQgEF3nTcEo+nkD49chjQ9IKm/QJy9xf6JakXptz86h7ecP2024rrLaQ==
+ dependencies:
+ jsonc-parser "^3.2.0"
+ mlly "^1.1.1"
+ pathe "^1.1.0"
+
pkg@5.8.0:
version "5.8.0"
resolved "https://registry.yarnpkg.com/pkg/-/pkg-5.8.0.tgz#a77644aeff0b94a1656d7f76558837f7c754a4c0"
@@ -19264,6 +19699,15 @@ postcss@^8.2.10, postcss@^8.2.9, postcss@^8.3.11, postcss@^8.4.12, postcss@^8.4.
picocolors "^1.0.0"
source-map-js "^1.0.2"
+postcss@^8.4.21:
+ version "8.4.22"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.22.tgz#c29e6776b60ab3af602d4b513d5bd2ff9aa85dc1"
+ integrity sha512-XseknLAfRHzVWjCEtdviapiBtfLdgyzExD50Rg2ePaucEesyh8Wv4VPdW0nbyDa1ydbrAxV19jvMT4+LFmcNUA==
+ dependencies:
+ nanoid "^3.3.6"
+ picocolors "^1.0.0"
+ source-map-js "^1.0.2"
+
postgres-array@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e"
@@ -19757,6 +20201,11 @@ process-warning@^1.0.0:
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-1.0.0.tgz#980a0b25dc38cd6034181be4b7726d89066b4616"
integrity sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==
+process-warning@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.2.0.tgz#008ec76b579820a8e5c35d81960525ca64feb626"
+ integrity sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==
+
process@^0.11.10:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
@@ -19977,7 +20426,7 @@ punycode@^2.1.0:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
-punycode@^2.1.1:
+punycode@^2.1.1, punycode@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
@@ -20360,6 +20809,11 @@ real-require@^0.1.0:
resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.1.0.tgz#736ac214caa20632847b7ca8c1056a0767df9381"
integrity sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==
+real-require@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78"
+ integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
+
recast@^0.10.1:
version "0.10.43"
resolved "https://registry.yarnpkg.com/recast/-/recast-0.10.43.tgz#b95d50f6d60761a5f6252e15d80678168491ce7f"
@@ -21012,6 +21466,18 @@ rollup@^2.36.2, rollup@^2.44.0, rollup@^2.45.2, rollup@^2.79.1:
optionalDependencies:
fsevents "~2.3.2"
+rollup@^3.18.0:
+ version "3.20.5"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.20.5.tgz#efeb4495a87c73d2f6ee19e48159953d061612de"
+ integrity sha512-Mx6NE3nLPIP6a9ReV4dTPOYYmDiyarJNtSbc37Jx0jvh8SHySoFPgyZAp9aDP3LnYvaJOrz+fclcwq3oZDzlnA==
+ optionalDependencies:
+ fsevents "~2.3.2"
+
+rrweb-cssom@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1"
+ integrity sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==
+
rrweb-snapshot@^1.1.14:
version "1.1.14"
resolved "https://registry.yarnpkg.com/rrweb-snapshot/-/rrweb-snapshot-1.1.14.tgz#9d4d9be54a28a893373428ee4393ec7e5bd83fcc"
@@ -21152,6 +21618,13 @@ saxes@^5.0.1:
dependencies:
xmlchars "^2.2.0"
+saxes@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5"
+ integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==
+ dependencies:
+ xmlchars "^2.2.0"
+
schema-utils@^3.1.0, schema-utils@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281"
@@ -21169,7 +21642,7 @@ scim-patch@^0.7.0:
fast-deep-equal "3.1.3"
scim2-parse-filter "0.2.8"
-scim2-parse-filter@0.2.8:
+scim2-parse-filter@0.2.8, scim2-parse-filter@^0.2.8:
version "0.2.8"
resolved "https://registry.yarnpkg.com/scim2-parse-filter/-/scim2-parse-filter-0.2.8.tgz#12e836514b9a55ae51218dd6e7fbea91daccfa4d"
integrity sha512-1V+6FIMIiP+gDiFkC3dIw86KfoXhnQRXhfPaiQImeeFukpLtEkTtYq/Vmy1yDgHQcIHQxQQqOWyGLKX0FTvvaA==
@@ -21461,6 +21934,11 @@ side-channel@^1.0.4:
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
+siginfo@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30"
+ integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==
+
sigmund@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
@@ -21550,6 +22028,14 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
+slice-ansi@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a"
+ integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==
+ dependencies:
+ ansi-styles "^6.0.0"
+ is-fullwidth-code-point "^4.0.0"
+
smart-buffer@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
@@ -21636,7 +22122,7 @@ socket.io-adapter@~2.5.2:
dependencies:
ws "~8.11.0"
-socket.io-client@^4.5.1:
+socket.io-client@^4.5.1, socket.io-client@^4.6.1:
version "4.6.1"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.6.1.tgz#80d97d5eb0feca448a0fb6d69a7b222d3d547eab"
integrity sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==
@@ -21654,7 +22140,7 @@ socket.io-parser@~4.2.1:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
-socket.io@^4.5.1:
+socket.io@4.6.1:
version "4.6.1"
resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.6.1.tgz#62ec117e5fce0692fa50498da9347cfb52c3bc70"
integrity sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==
@@ -21707,7 +22193,7 @@ sonic-boom@^2.2.1:
dependencies:
atomic-sleep "^1.0.0"
-sonic-boom@^3.0.0:
+sonic-boom@^3.0.0, sonic-boom@^3.1.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.3.0.tgz#cffab6dafee3b2bcb88d08d589394198bee1838c"
integrity sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==
@@ -21998,6 +22484,11 @@ stack-utils@^2.0.2, stack-utils@^2.0.3:
dependencies:
escape-string-regexp "^2.0.0"
+stackback@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b"
+ integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==
+
stackframe@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310"
@@ -22040,6 +22531,11 @@ statuses@2.0.1, statuses@^2.0.0:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
+std-env@^3.3.1:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.3.2.tgz#af27343b001616015534292178327b202b9ee955"
+ integrity sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==
+
step@0.0.x:
version "0.0.6"
resolved "https://registry.yarnpkg.com/step/-/step-0.0.6.tgz#143e7849a5d7d3f4a088fe29af94915216eeede2"
@@ -22150,6 +22646,15 @@ string-width@^3.0.0, string-width@^3.1.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
+string-width@^5.0.0:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
+ integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
+ dependencies:
+ eastasianwidth "^0.2.0"
+ emoji-regex "^9.2.2"
+ strip-ansi "^7.0.1"
+
string.prototype.startswith@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/string.prototype.startswith/-/string.prototype.startswith-1.0.0.tgz#92a361fb1ac172033d53eb1db3d659b0cfab6280"
@@ -22234,6 +22739,13 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1:
dependencies:
ansi-regex "^5.0.1"
+strip-ansi@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
+ integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==
+ dependencies:
+ ansi-regex "^6.0.1"
+
strip-bom@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
@@ -22285,6 +22797,13 @@ strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
+strip-literal@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-1.0.1.tgz#0115a332710c849b4e46497891fb8d585e404bd2"
+ integrity sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==
+ dependencies:
+ acorn "^8.8.2"
+
strip-outer@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631"
@@ -22438,7 +22957,7 @@ svelte-dnd-action@^0.9.8:
resolved "https://registry.yarnpkg.com/svelte-dnd-action/-/svelte-dnd-action-0.9.22.tgz#003eee9dddb31d8c782f6832aec8b1507fff194d"
integrity sha512-lOQJsNLM1QWv5mdxIkCVtk6k4lHCtLgfE59y8rs7iOM6erchbLC9hMEFYSveZz7biJV0mpg7yDSs4bj/RT/YkA==
-svelte-flatpickr@^3.1.0, svelte-flatpickr@^3.2.3:
+svelte-flatpickr@^3.1.0, svelte-flatpickr@^3.2.3, svelte-flatpickr@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-3.3.2.tgz#f08bcde83d439cb30df6fd07b974d87371f130c1"
integrity sha512-VNJLYyLRDplI63oWX5hJylzAJc2VhTh3z9SNecfjtuPZmP6FZPpg9Fw7rXpkEV2DPovIWj2PtaVxB6Kp9r423w==
@@ -22846,6 +23365,13 @@ thread-stream@^0.15.1:
dependencies:
real-require "^0.1.0"
+thread-stream@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.3.0.tgz#4fc07fb39eff32ae7bad803cb7dd9598349fed33"
+ integrity sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==
+ dependencies:
+ real-require "^0.2.0"
+
throat@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
@@ -22925,11 +23451,26 @@ tiny-queue@^0.2.0:
resolved "https://registry.yarnpkg.com/tiny-queue/-/tiny-queue-0.2.1.tgz#25a67f2c6e253b2ca941977b5ef7442ef97a6046"
integrity sha512-EijGsv7kzd9I9g0ByCl6h42BWNGUZrlCSejfrb3AKeHC33SGbASu1VDf5O3rRiiUOhAC9CHdZxFPbZu0HmR70A==
+tinybench@^2.3.1:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.4.0.tgz#83f60d9e5545353610fe7993bd783120bc20c7a7"
+ integrity sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg==
+
tinycolor2@^1.4.1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e"
integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==
+tinypool@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.4.0.tgz#3cf3ebd066717f9f837e8d7d31af3c127fdb5446"
+ integrity sha512-2ksntHOKf893wSAH4z/+JbPpi92esw8Gn9N2deXX+B0EO92hexAVI9GIZZPx7P5aYo5KULfeOSt3kMOmSOy6uA==
+
+tinyspy@^1.0.2:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-1.1.1.tgz#0cb91d5157892af38cb2d217f5c7e8507a5bf092"
+ integrity sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g==
+
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -23093,6 +23634,13 @@ tr46@^3.0.0:
dependencies:
punycode "^2.1.1"
+tr46@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-4.1.1.tgz#281a758dcc82aeb4fe38c7dfe4d11a395aac8469"
+ integrity sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==
+ dependencies:
+ punycode "^2.3.0"
+
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
@@ -23290,7 +23838,7 @@ type-check@~0.3.2:
dependencies:
prelude-ls "~1.1.2"
-type-detect@4.0.8:
+type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5:
version "4.0.8"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
@@ -23421,6 +23969,11 @@ uc.micro@^1.0.1, uc.micro@^1.0.5:
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
+ufo@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.1.1.tgz#e70265e7152f3aba425bd013d150b2cdf4056d7c"
+ integrity sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==
+
uglify-js@^3.1.4:
version "3.16.1"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.16.1.tgz#0e7ec928b3d0b1e1d952bce634c384fd56377317"
@@ -23870,6 +24423,30 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
+vite-node@0.29.8:
+ version "0.29.8"
+ resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.29.8.tgz#6a1c9d4fb31e7b4e0f825d3a37abe3404e52bd8e"
+ integrity sha512-b6OtCXfk65L6SElVM20q5G546yu10/kNrhg08afEoWlFRJXFq9/6glsvSVY+aI6YeC1tu2TtAqI2jHEQmOmsFw==
+ dependencies:
+ cac "^6.7.14"
+ debug "^4.3.4"
+ mlly "^1.1.0"
+ pathe "^1.1.0"
+ picocolors "^1.0.0"
+ vite "^3.0.0 || ^4.0.0"
+
+"vite@^3.0.0 || ^4.0.0":
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/vite/-/vite-4.2.2.tgz#014c30e5163844f6e96d7fe18fbb702236516dc6"
+ integrity sha512-PcNtT5HeDxb3QaSqFYkEum8f5sCVe0R3WK20qxgIvNBZPXU/Obxs/+ubBMeE7nLWeCo2LDzv+8hRYSlcaSehig==
+ dependencies:
+ esbuild "^0.17.5"
+ postcss "^8.4.21"
+ resolve "^1.22.1"
+ rollup "^3.18.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
vite@^3.0.8:
version "3.2.5"
resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.5.tgz#dee5678172a8a0ab3e547ad4148c3d547f90e86a"
@@ -23882,12 +24459,50 @@ vite@^3.0.8:
optionalDependencies:
fsevents "~2.3.2"
+vitest@^0.29.2:
+ version "0.29.8"
+ resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.29.8.tgz#9c13cfa007c3511e86c26e1fe9a686bb4dbaec80"
+ integrity sha512-JIAVi2GK5cvA6awGpH0HvH/gEG9PZ0a/WoxdiV3PmqK+3CjQMf8c+J/Vhv4mdZ2nRyXFw66sAg6qz7VNkaHfDQ==
+ dependencies:
+ "@types/chai" "^4.3.4"
+ "@types/chai-subset" "^1.3.3"
+ "@types/node" "*"
+ "@vitest/expect" "0.29.8"
+ "@vitest/runner" "0.29.8"
+ "@vitest/spy" "0.29.8"
+ "@vitest/utils" "0.29.8"
+ acorn "^8.8.1"
+ acorn-walk "^8.2.0"
+ cac "^6.7.14"
+ chai "^4.3.7"
+ debug "^4.3.4"
+ local-pkg "^0.4.2"
+ pathe "^1.1.0"
+ picocolors "^1.0.0"
+ source-map "^0.6.1"
+ std-env "^3.3.1"
+ strip-literal "^1.0.0"
+ tinybench "^2.3.1"
+ tinypool "^0.4.0"
+ tinyspy "^1.0.2"
+ vite "^3.0.0 || ^4.0.0"
+ vite-node "0.29.8"
+ why-is-node-running "^2.2.2"
+
vlq@^0.2.2:
version "0.2.3"
resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
-vm2@3.9.16, vm2@^3.9.11, vm2@^3.9.15, vm2@^3.9.4:
+vm2@3.9.17:
+ version "3.9.17"
+ resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.17.tgz#251b165ff8a0e034942b5181057305e39570aeab"
+ integrity sha512-AqwtCnZ/ERcX+AVj9vUsphY56YANXxRuqMb7GsDtAr0m0PcQX3u0Aj3KWiXM0YAHy7i6JEeHrwOnwXbGYgRpAw==
+ dependencies:
+ acorn "^8.7.0"
+ acorn-walk "^8.2.0"
+
+vm2@^3.9.11, vm2@^3.9.15, vm2@^3.9.4:
version "3.9.16"
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.16.tgz#0fbc2a265f7bf8b837cea6f4a908f88a3f93b8e6"
integrity sha512-3T9LscojNTxdOyG+e8gFeyBXkMlOBYDoF6dqZbj+MPVHi9x10UfiTAJIobuchRCp3QvC+inybTbMJIUrLsig0w==
@@ -23914,6 +24529,13 @@ w3c-xmlserializer@^2.0.0:
dependencies:
xml-name-validator "^3.0.0"
+w3c-xmlserializer@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073"
+ integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==
+ dependencies:
+ xml-name-validator "^4.0.0"
+
wait-on@7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-7.0.1.tgz#5cff9f8427e94f4deacbc2762e6b0a489b19eae9"
@@ -24066,11 +24688,23 @@ whatwg-encoding@^1.0.5:
dependencies:
iconv-lite "0.4.24"
+whatwg-encoding@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53"
+ integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==
+ dependencies:
+ iconv-lite "0.6.3"
+
whatwg-mimetype@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
+whatwg-mimetype@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"
+ integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==
+
whatwg-url@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018"
@@ -24079,6 +24713,14 @@ whatwg-url@^11.0.0:
tr46 "^3.0.0"
webidl-conversions "^7.0.0"
+whatwg-url@^12.0.0, whatwg-url@^12.0.1:
+ version "12.0.1"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-12.0.1.tgz#fd7bcc71192e7c3a2a97b9a8d6b094853ed8773c"
+ integrity sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==
+ dependencies:
+ tr46 "^4.1.1"
+ webidl-conversions "^7.0.0"
+
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
@@ -24164,6 +24806,14 @@ which@^3.0.0:
dependencies:
isexe "^2.0.0"
+why-is-node-running@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.2.2.tgz#4185b2b4699117819e7154594271e7e344c9973e"
+ integrity sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==
+ dependencies:
+ siginfo "^2.0.0"
+ stackback "0.0.2"
+
wide-align@^1.1.0, wide-align@^1.1.2, wide-align@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
@@ -24363,6 +25013,11 @@ ws@^7.4.6:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
+ws@^8.13.0:
+ version "8.13.0"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
+ integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
+
ws@~8.11.0:
version "8.11.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
@@ -24393,6 +25048,11 @@ xml-name-validator@^3.0.0:
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
+xml-name-validator@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
+ integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
+
xml-parse-from-string@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28"
@@ -24656,6 +25316,11 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
+yocto-queue@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
+ integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
+
yup@0.29.2:
version "0.29.2"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.29.2.tgz#5302abd9024cca335b987793f8df868e410b7b67"
@@ -24690,8 +25355,3 @@ z-schema@^5.0.1:
validator "^13.7.0"
optionalDependencies:
commander "^10.0.0"
-
-zlib@1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/zlib/-/zlib-1.0.5.tgz#6e7c972fc371c645a6afb03ab14769def114fcc0"
- integrity sha512-40fpE2II+Cd3k8HWTWONfeKE2jL+P42iWJ1zzps5W51qcTsOUKM5Q5m2PFb0CLxlmFAaUuUdJGc3OfZy947v0w==