Merge branch 'develop' into cheeks-lab-day-portal-poc

This commit is contained in:
Andrew Kingston 2023-09-25 19:34:04 +01:00 committed by GitHub
commit 38cad518a4
45 changed files with 879 additions and 108 deletions

View File

@ -12,7 +12,8 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: passeidireto/trigger-external-workflow-action@main - uses: passeidireto/trigger-external-workflow-action@main
env: env:
BRANCH: ${{ github.head_ref }} PAYLOAD_BRANCH: ${{ github.head_ref }}
PAYLOAD_PR_NUMBER: ${{ github.ref }}
with: with:
repository: budibase/budibase-deploys repository: budibase/budibase-deploys
event: featurebranch-qa-deploy event: featurebranch-qa-deploy

View File

@ -1,5 +1,5 @@
{ {
"version": "2.10.12-alpha.7", "version": "2.10.12-alpha.11",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -5,7 +5,7 @@
"@esbuild-plugins/tsconfig-paths": "^0.1.2", "@esbuild-plugins/tsconfig-paths": "^0.1.2",
"@nx/js": "16.4.3", "@nx/js": "16.4.3",
"@rollup/plugin-json": "^4.0.2", "@rollup/plugin-json": "^4.0.2",
"@typescript-eslint/parser": "5.45.0", "@typescript-eslint/parser": "6.7.2",
"esbuild": "^0.18.17", "esbuild": "^0.18.17",
"esbuild-node-externals": "^1.8.0", "esbuild-node-externals": "^1.8.0",
"eslint": "^8.44.0", "eslint": "^8.44.0",
@ -22,7 +22,7 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup-plugin-replace": "^2.2.0", "rollup-plugin-replace": "^2.2.0",
"svelte": "^3.38.2", "svelte": "^3.38.2",
"typescript": "4.7.3", "typescript": "5.2.2",
"@babel/core": "^7.22.5", "@babel/core": "^7.22.5",
"@babel/eslint-parser": "^7.22.5", "@babel/eslint-parser": "^7.22.5",
"@babel/preset-env": "^7.22.5", "@babel/preset-env": "^7.22.5",

View File

@ -87,7 +87,7 @@
"timekeeper": "2.2.0", "timekeeper": "2.2.0",
"ts-node": "10.8.1", "ts-node": "10.8.1",
"tsconfig-paths": "4.0.0", "tsconfig-paths": "4.0.0",
"typescript": "4.7.3" "typescript": "5.2.2"
}, },
"nx": { "nx": {
"targets": { "targets": {

View File

@ -86,8 +86,8 @@ export const useAuditLogs = () => {
return useFeature(Feature.AUDIT_LOGS) return useFeature(Feature.AUDIT_LOGS)
} }
export const usePublicApiUserRoles = () => { export const useExpandedPublicApi = () => {
return useFeature(Feature.USER_ROLE_PUBLIC_API) return useFeature(Feature.EXPANDED_PUBLIC_API)
} }
export const useScimIntegration = () => { export const useScimIntegration = () => {

View File

@ -10,14 +10,13 @@ import { authDetails } from "./sso"
import { uuid } from "./common" import { uuid } from "./common"
import { generator } from "./generator" import { generator } from "./generator"
import { tenant } from "." import { tenant } from "."
import { generateGlobalUserID } from "../../../../src/docIds"
export const newEmail = () => { export const newEmail = () => {
return `${uuid()}@test.com` return `${uuid()}@test.com`
} }
export const user = (userProps?: Partial<Omit<User, "userId">>): User => { export const user = (userProps?: Partial<Omit<User, "userId">>): User => {
const userId = userProps?._id || generateGlobalUserID() const userId = userProps?._id
return { return {
_id: userId, _id: userId,
userId, userId,
@ -53,7 +52,7 @@ export const adminOnlyUser = (userProps?: any): AdminOnlyUser => {
} }
} }
export const builderUser = (userProps?: any): BuilderUser => { export const builderUser = (userProps?: Partial<User>): BuilderUser => {
return { return {
...user(userProps), ...user(userProps),
builder: { builder: {

View File

@ -108,7 +108,7 @@
"svelte-jester": "^1.3.2", "svelte-jester": "^1.3.2",
"ts-node": "10.8.1", "ts-node": "10.8.1",
"tsconfig-paths": "4.0.0", "tsconfig-paths": "4.0.0",
"typescript": "4.7.3", "typescript": "5.2.2",
"vite": "^3.0.8", "vite": "^3.0.8",
"vite-plugin-static-copy": "^0.16.0", "vite-plugin-static-copy": "^0.16.0",
"vitest": "^0.29.2" "vitest": "^0.29.2"

View File

@ -0,0 +1,69 @@
<script>
import {
ModalContent,
Toggle,
Input,
Layout,
Dropzone,
notifications,
Body,
} from "@budibase/bbui"
import { API } from "api"
import { automationStore, store } from "../../builderStore"
export let app
$: disabled = (encrypted && !password) || !file
let encrypted = false,
password
let file
async function updateApp() {
try {
let data = new FormData()
data.append("appExport", file)
if (encrypted) {
data.append("encryptionPassword", password.trim())
}
const appId = app.devId
await API.updateAppFromExport(appId, data)
const pkg = await API.fetchAppPackage(appId)
await store.actions.initialise(pkg)
await automationStore.actions.fetch()
notifications.success("App updated successfully")
} catch (err) {
notifications.error(`Failed to update app - ${err.message || err}`)
}
}
</script>
<ModalContent
title={`Update ${app.name}`}
confirmText="Update"
onConfirm={updateApp}
bind:disabled
>
<Body size="S"
>Updating an app using an app export will replace all tables, datasources,
queries, screens and automations. It is recommended to perform a backup
before running this operation.</Body
>
<Layout noPadding gap="XS">
<Dropzone
gallery={false}
label="App export"
on:change={e => {
file = e.detail?.[0]
}}
/>
<Toggle text="Encrypted" bind:value={encrypted} />
{#if encrypted}
<Input
type="password"
label="Password"
placeholder="Type here..."
bind:value={password}
/>
{/if}
</Layout>
</ModalContent>

View File

@ -32,9 +32,9 @@
active={$isActive("./embed")} active={$isActive("./embed")}
/> />
<SideNavItem <SideNavItem
text="Export" text="Export/Import"
url={$url("./export")} url={$url("./exportImport")}
active={$isActive("./export")} active={$isActive("./exportImport")}
/> />
<SideNavItem <SideNavItem
text="Name and URL" text="Name and URL"

View File

@ -11,31 +11,39 @@
import { apps } from "stores/portal" import { apps } from "stores/portal"
import { store } from "builderStore" import { store } from "builderStore"
import ExportAppModal from "components/start/ExportAppModal.svelte" import ExportAppModal from "components/start/ExportAppModal.svelte"
import ImportAppModal from "components/start/ImportAppModal.svelte"
$: filteredApps = $apps.filter(app => app.devId == $store.appId) $: filteredApps = $apps.filter(app => app.devId == $store.appId)
$: app = filteredApps.length ? filteredApps[0] : {} $: app = filteredApps.length ? filteredApps[0] : {}
$: appDeployed = app?.status === AppStatus.DEPLOYED $: appDeployed = app?.status === AppStatus.DEPLOYED
let exportModal let exportModal, importModal
let exportPublishedVersion = false let exportPublishedVersion = false
const exportApp = opts => { const exportApp = opts => {
exportPublishedVersion = !!opts?.published exportPublishedVersion = !!opts?.published
exportModal.show() exportModal.show()
} }
const importApp = () => {
importModal.show()
}
</script> </script>
<Modal bind:this={exportModal} padding={false}> <Modal bind:this={exportModal} padding={false}>
<ExportAppModal {app} published={exportPublishedVersion} /> <ExportAppModal {app} published={exportPublishedVersion} />
</Modal> </Modal>
<Modal bind:this={importModal} padding={false}>
<ImportAppModal {app} />
</Modal>
<Layout noPadding> <Layout noPadding>
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<Heading>Export your app</Heading> <Heading>Export your app</Heading>
<Body>Export your latest edited or published app</Body> <Body>Export your latest edited or published app</Body>
</Layout> </Layout>
<Divider /> <div class="body">
<div class="export-body">
<ActionButton secondary on:click={() => exportApp({ published: false })}> <ActionButton secondary on:click={() => exportApp({ published: false })}>
Export latest edited app Export latest edited app
</ActionButton> </ActionButton>
@ -47,10 +55,20 @@
Export latest published app Export latest published app
</ActionButton> </ActionButton>
</div> </div>
<Divider />
<Layout gap="XS" noPadding>
<Heading>Import your app</Heading>
<Body>Import an export to update this app</Body>
</Layout>
<div class="body">
<ActionButton secondary on:click={() => importApp()}>
Import app
</ActionButton>
</div>
</Layout> </Layout>
<style> <style>
.export-body { .body {
display: flex; display: flex;
gap: var(--spacing-l); gap: var(--spacing-l);
} }

View File

@ -62,6 +62,6 @@
"eslint": "^7.20.0", "eslint": "^7.20.0",
"renamer": "^4.0.0", "renamer": "^4.0.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "4.7.3" "typescript": "5.2.2"
} }
} }

View File

@ -49,8 +49,8 @@ async function askAboutTopLevel(name: string) {
} }
} }
async function init(opts: PluginOpts) { async function init(opts: PluginOpts | PluginType) {
const type = opts["init"] || (opts as PluginType) const type = (opts as PluginOpts).init || (opts as PluginType)
if (!type || !PLUGIN_TYPE_ARR.includes(type)) { if (!type || !PLUGIN_TYPE_ARR.includes(type)) {
console.log( console.log(
error( error(

View File

@ -1,3 +1,5 @@
import { sdk } from "@budibase/shared-core"
export const buildAppEndpoints = API => ({ export const buildAppEndpoints = API => ({
/** /**
* Fetches screen definition for an app. * Fetches screen definition for an app.
@ -81,6 +83,22 @@ export const buildAppEndpoints = API => ({
}) })
}, },
/**
* Update an application using an export - the body
* should be of type FormData, with a "file" and a "password" if encrypted.
* @param appId The ID of the app to update - this will always be
* converted to development ID.
* @param body a FormData body with a file and password.
*/
updateAppFromExport: async (appId, body) => {
const devId = sdk.applications.getDevAppID(appId)
return await API.post({
url: `/api/applications/${devId}/import`,
body,
json: false,
})
},
/** /**
* Imports an export of all apps. * Imports an export of all apps.
* @param apps the FormData containing the apps to import * @param apps the FormData containing the apps to import

@ -1 +1 @@
Subproject commit 4638ae916e55ce89166095578cbd01745d0ee9ee Subproject commit 30385682141e5ba9d98de7d71d5be1672109cd15

View File

@ -153,7 +153,6 @@
"@types/server-destroy": "1.0.1", "@types/server-destroy": "1.0.1",
"@types/supertest": "2.0.12", "@types/supertest": "2.0.12",
"@types/tar": "6.1.5", "@types/tar": "6.1.5",
"@typescript-eslint/parser": "5.45.0",
"apidoc": "0.50.4", "apidoc": "0.50.4",
"babel-jest": "29.6.2", "babel-jest": "29.6.2",
"copyfiles": "2.4.1", "copyfiles": "2.4.1",
@ -174,7 +173,7 @@
"timekeeper": "2.2.0", "timekeeper": "2.2.0",
"ts-node": "10.8.1", "ts-node": "10.8.1",
"tsconfig-paths": "4.0.0", "tsconfig-paths": "4.0.0",
"typescript": "4.7.3", "typescript": "5.2.2",
"update-dotenv": "1.1.1" "update-dotenv": "1.1.1"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@ -9,6 +9,7 @@ CREATE TABLE Persons (
Address varchar(255), Address varchar(255),
City varchar(255) DEFAULT 'Belfast', City varchar(255) DEFAULT 'Belfast',
Age INTEGER DEFAULT 20 NOT NULL, Age INTEGER DEFAULT 20 NOT NULL,
Year INTEGER,
Type person_job Type person_job
); );
CREATE TABLE Tasks ( CREATE TABLE Tasks (
@ -49,9 +50,10 @@ CREATE TABLE CompositeTable (
Name varchar(255), Name varchar(255),
PRIMARY KEY (KeyPartOne, KeyPartTwo) PRIMARY KEY (KeyPartOne, KeyPartTwo)
); );
INSERT INTO Persons (FirstName, LastName, Address, City, Type) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast', 'qa'); INSERT INTO Persons (FirstName, LastName, Address, City, Type, Year) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast', 'qa', 1999);
INSERT INTO Persons (FirstName, LastName, Address, City, Type) VALUES ('John', 'Smith', '64 Updown Road', 'Dublin', 'programmer'); INSERT INTO Persons (FirstName, LastName, Address, City, Type, Year) VALUES ('John', 'Smith', '64 Updown Road', 'Dublin', 'programmer', 1996);
INSERT INTO Persons (FirstName, LastName, Address, City, Type, Age) VALUES ('Foo', 'Bar', 'Foo Street', 'Bartown', 'support', 0); INSERT INTO Persons (FirstName, LastName, Address, City, Type, Age, Year) VALUES ('Foo', 'Bar', 'Foo Street', 'Bartown', 'support', 0, 1993);
INSERT INTO Persons (FirstName, LastName, Address, City, Type) VALUES ('Jonny', 'Muffin', 'Muffin Street', 'Cork', 'support');
INSERT INTO Tasks (ExecutorID, QaID, TaskName, Completed) VALUES (1, 2, 'assembling', TRUE); INSERT INTO Tasks (ExecutorID, QaID, TaskName, Completed) VALUES (1, 2, 'assembling', TRUE);
INSERT INTO Tasks (ExecutorID, QaID, TaskName, Completed) VALUES (2, 1, 'processing', FALSE); INSERT INTO Tasks (ExecutorID, QaID, TaskName, Completed) VALUES (2, 1, 'processing', FALSE);
INSERT INTO Products (ProductName) VALUES ('Computers'); INSERT INTO Products (ProductName) VALUES ('Computers');

View File

@ -613,6 +613,23 @@
"data" "data"
] ]
}, },
"appExport": {
"type": "object",
"properties": {
"encryptPassword": {
"description": "An optional password used to encrypt the export.",
"type": "string"
},
"excludeRows": {
"description": "Set whether the internal table rows should be excluded from the export.",
"type": "boolean"
}
},
"required": [
"encryptPassword",
"excludeRows"
]
},
"row": { "row": {
"description": "The row to be created/updated, based on the table schema.", "description": "The row to be created/updated, based on the table schema.",
"type": "object", "type": "object",
@ -2163,6 +2180,87 @@
} }
} }
}, },
"/applications/{appId}/import": {
"post": {
"operationId": "appImport",
"summary": "Import an app to an existing app 🔒",
"description": "This endpoint is only available on a business or enterprise license.",
"tags": [
"applications"
],
"parameters": [
{
"$ref": "#/components/parameters/appIdUrl"
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"encryptedPassword": {
"description": "Password for the export if it is encrypted.",
"type": "string"
},
"appExport": {
"description": "The app export to import.",
"type": "string",
"format": "binary"
}
},
"required": [
"appExport"
]
}
}
}
},
"responses": {
"204": {
"description": "Application has been updated."
}
}
}
},
"/applications/{appId}/export": {
"post": {
"operationId": "appExport",
"summary": "Export an app 🔒",
"description": "This endpoint is only available on a business or enterprise license.",
"tags": [
"applications"
],
"parameters": [
{
"$ref": "#/components/parameters/appIdUrl"
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/appExport"
}
}
}
},
"responses": {
"200": {
"description": "A gzip tarball containing the app export, encrypted if password provided.",
"content": {
"application/gzip": {
"schema": {
"type": "string",
"format": "binary",
"example": "Tarball containing database and object store contents..."
}
}
}
}
}
}
},
"/applications/search": { "/applications/search": {
"post": { "post": {
"operationId": "appSearch", "operationId": "appSearch",

View File

@ -587,6 +587,19 @@ components:
- appUrl - appUrl
required: required:
- data - data
appExport:
type: object
properties:
encryptPassword:
description: An optional password used to encrypt the export.
type: string
excludeRows:
description: Set whether the internal table rows should be excluded from the
export.
type: boolean
required:
- encryptPassword
- excludeRows
row: row:
description: The row to be created/updated, based on the table schema. description: The row to be created/updated, based on the table schema.
type: object type: object
@ -1763,6 +1776,57 @@ paths:
examples: examples:
deployment: deployment:
$ref: "#/components/examples/deploymentOutput" $ref: "#/components/examples/deploymentOutput"
"/applications/{appId}/import":
post:
operationId: appImport
summary: Import an app to an existing app 🔒
description: This endpoint is only available on a business or enterprise license.
tags:
- applications
parameters:
- $ref: "#/components/parameters/appIdUrl"
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
encryptedPassword:
description: Password for the export if it is encrypted.
type: string
appExport:
description: The app export to import.
type: string
format: binary
required:
- appExport
responses:
"204":
description: Application has been updated.
"/applications/{appId}/export":
post:
operationId: appExport
summary: Export an app 🔒
description: This endpoint is only available on a business or enterprise license.
tags:
- applications
parameters:
- $ref: "#/components/parameters/appIdUrl"
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/appExport"
responses:
"200":
description: A gzip tarball containing the app export, encrypted if password
provided.
content:
application/gzip:
schema:
type: string
format: binary
example: Tarball containing database and object store contents...
/applications/search: /applications/search:
post: post:
operationId: appSearch operationId: appSearch

View File

@ -134,4 +134,15 @@ export default new Resource()
deploymentOutput: object({ deploymentOutput: object({
data: deploymentOutputSchema, data: deploymentOutputSchema,
}), }),
appExport: object({
encryptPassword: {
description: "An optional password used to encrypt the export.",
type: "string",
},
excludeRows: {
description:
"Set whether the internal table rows should be excluded from the export.",
type: "boolean",
},
}),
}) })

View File

@ -39,9 +39,8 @@ import {
} from "../../db/defaultData/datasource_bb_default" } from "../../db/defaultData/datasource_bb_default"
import { removeAppFromUserRoles } from "../../utilities/workerRequests" import { removeAppFromUserRoles } from "../../utilities/workerRequests"
import { stringToReadStream } from "../../utilities" import { stringToReadStream } from "../../utilities"
import { doesUserHaveLock, getLocksById } from "../../utilities/redis" import { doesUserHaveLock } from "../../utilities/redis"
import { cleanupAutomations } from "../../automations/utils" import { cleanupAutomations } from "../../automations/utils"
import { checkAppMetadata } from "../../automations/logging"
import { getUniqueRows } from "../../utilities/usageQuota/rows" import { getUniqueRows } from "../../utilities/usageQuota/rows"
import { groups, licensing, quotas } from "@budibase/pro" import { groups, licensing, quotas } from "@budibase/pro"
import { import {
@ -51,7 +50,6 @@ import {
PlanType, PlanType,
Screen, Screen,
UserCtx, UserCtx,
ContextUser,
} from "@budibase/types" } from "@budibase/types"
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
import sdk from "../../sdk" import sdk from "../../sdk"
@ -575,6 +573,28 @@ export async function sync(ctx: UserCtx) {
} }
} }
export async function importToApp(ctx: UserCtx) {
const { appId } = ctx.params
const appExport = ctx.request.files?.appExport
const password = ctx.request.body.encryptionPassword as string
if (!appExport) {
ctx.throw(400, "Must supply app export to import")
}
if (Array.isArray(appExport)) {
ctx.throw(400, "Must only supply one app export")
}
const fileAttributes = { type: appExport.type!, path: appExport.path! }
try {
await sdk.applications.updateWithExport(appId, fileAttributes, password)
} catch (err: any) {
ctx.throw(
500,
`Unable to perform update, please retry - ${err?.message || err}`
)
}
ctx.body = { message: "app updated" }
}
export async function updateAppPackage(appPackage: any, appId: any) { export async function updateAppPackage(appPackage: any, appId: any) {
return context.doInAppContext(appId, async () => { return context.doInAppContext(appId, async () => {
const db = context.getAppDB() const db = context.getAppDB()

View File

@ -2,9 +2,11 @@ import { db as dbCore, context } from "@budibase/backend-core"
import { search as stringSearch, addRev } from "./utils" import { search as stringSearch, addRev } from "./utils"
import * as controller from "../application" import * as controller from "../application"
import * as deployController from "../deploy" import * as deployController from "../deploy"
import * as backupController from "../backup"
import { Application } from "../../../definitions/common" import { Application } from "../../../definitions/common"
import { UserCtx } from "@budibase/types" import { UserCtx } from "@budibase/types"
import { Next } from "koa" import { Next } from "koa"
import { sdk as proSdk } from "@budibase/pro"
function fixAppID(app: Application, params: any) { function fixAppID(app: Application, params: any) {
if (!params) { if (!params) {
@ -80,6 +82,8 @@ export async function destroy(ctx: UserCtx, next: Next) {
export async function unpublish(ctx: UserCtx, next: Next) { export async function unpublish(ctx: UserCtx, next: Next) {
await context.doInAppContext(ctx.params.appId, async () => { await context.doInAppContext(ctx.params.appId, async () => {
await controller.unpublish(ctx) await controller.unpublish(ctx)
ctx.body = undefined
ctx.status = 204
await next() await next()
}) })
} }
@ -91,12 +95,22 @@ export async function publish(ctx: UserCtx, next: Next) {
}) })
} }
// get licensed endpoints from pro
export const importToApp = proSdk.publicApi.applications.buildImportFn(
controller.importToApp
)
export const exportApp = proSdk.publicApi.applications.buildExportFn(
backupController.exportAppDump
)
export default { export default {
create, create,
update, update,
read, read,
destroy, destroy,
search, search,
publish,
unpublish, unpublish,
publish,
importToApp,
exportApp,
} }

View File

@ -4,6 +4,7 @@ import * as deploymentController from "../controllers/deploy"
import authorized from "../../middleware/authorized" import authorized from "../../middleware/authorized"
import { permissions } from "@budibase/backend-core" import { permissions } from "@budibase/backend-core"
import { applicationValidator } from "./utils/validators" import { applicationValidator } from "./utils/validators"
import { importToApp } from "../controllers/application"
const router: Router = new Router() const router: Router = new Router()
@ -58,5 +59,10 @@ router
authorized(permissions.GLOBAL_BUILDER), authorized(permissions.GLOBAL_BUILDER),
controller.destroy controller.destroy
) )
.post(
"/api/applications/:appId/import",
authorized(permissions.BUILDER),
controller.importToApp
)
export default router export default router

View File

@ -137,6 +137,70 @@ write.push(
new Endpoint("post", "/applications/:appId/publish", controller.publish) new Endpoint("post", "/applications/:appId/publish", controller.publish)
) )
/**
* @openapi
* /applications/{appId}/import:
* post:
* operationId: appImport
* summary: Import an app to an existing app 🔒
* description: This endpoint is only available on a business or enterprise license.
* tags:
* - applications
* parameters:
* - $ref: '#/components/parameters/appIdUrl'
* requestBody:
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* encryptedPassword:
* description: Password for the export if it is encrypted.
* type: string
* appExport:
* description: The app export to import.
* type: string
* format: binary
* required:
* - appExport
* responses:
* 204:
* description: Application has been updated.
*/
write.push(
new Endpoint("post", "/applications/:appId/import", controller.importToApp)
)
/**
* @openapi
* /applications/{appId}/export:
* post:
* operationId: appExport
* summary: Export an app 🔒
* description: This endpoint is only available on a business or enterprise license.
* tags:
* - applications
* parameters:
* - $ref: '#/components/parameters/appIdUrl'
* requestBody:
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/appExport'
* responses:
* 200:
* description: A gzip tarball containing the app export, encrypted if password provided.
* content:
* application/gzip:
* schema:
* type: string
* format: binary
* example: Tarball containing database and object store contents...
*/
read.push(
new Endpoint("post", "/applications/:appId/export", controller.exportApp)
)
/** /**
* @openapi * @openapi
* /applications/{appId}: * /applications/{appId}:

View File

@ -1,3 +1,4 @@
import { Ctx } from "@budibase/types"
import mapping from "../../../controllers/public/mapping" import mapping from "../../../controllers/public/mapping"
enum Resources { enum Resources {
@ -9,11 +10,19 @@ enum Resources {
SEARCH = "search", SEARCH = "search",
} }
function isArrayResponse(ctx: any) { function isAttachment(ctx: Ctx) {
return ctx.body?.path && ctx.body?.flags && ctx.body?.mode
}
function isArrayResponse(ctx: Ctx) {
return ctx.url.endsWith(Resources.SEARCH) || Array.isArray(ctx.body) return ctx.url.endsWith(Resources.SEARCH) || Array.isArray(ctx.body)
} }
function processApplications(ctx: any) { function noResponse(ctx: Ctx) {
return !Array.isArray(ctx.body) && Object.keys(ctx.body).length === 0
}
function processApplications(ctx: Ctx) {
if (isArrayResponse(ctx)) { if (isArrayResponse(ctx)) {
return mapping.mapApplications(ctx) return mapping.mapApplications(ctx)
} else { } else {
@ -21,7 +30,7 @@ function processApplications(ctx: any) {
} }
} }
function processTables(ctx: any) { function processTables(ctx: Ctx) {
if (isArrayResponse(ctx)) { if (isArrayResponse(ctx)) {
return mapping.mapTables(ctx) return mapping.mapTables(ctx)
} else { } else {
@ -29,7 +38,7 @@ function processTables(ctx: any) {
} }
} }
function processRows(ctx: any) { function processRows(ctx: Ctx) {
if (isArrayResponse(ctx)) { if (isArrayResponse(ctx)) {
return mapping.mapRowSearch(ctx) return mapping.mapRowSearch(ctx)
} else { } else {
@ -37,7 +46,7 @@ function processRows(ctx: any) {
} }
} }
function processUsers(ctx: any) { function processUsers(ctx: Ctx) {
if (isArrayResponse(ctx)) { if (isArrayResponse(ctx)) {
return mapping.mapUsers(ctx) return mapping.mapUsers(ctx)
} else { } else {
@ -45,7 +54,7 @@ function processUsers(ctx: any) {
} }
} }
function processQueries(ctx: any) { function processQueries(ctx: Ctx) {
if (isArrayResponse(ctx)) { if (isArrayResponse(ctx)) {
return mapping.mapQueries(ctx) return mapping.mapQueries(ctx)
} else { } else {
@ -53,8 +62,8 @@ function processQueries(ctx: any) {
} }
} }
export default async (ctx: any, next: any) => { export default async (ctx: Ctx, next: any) => {
if (!ctx.body) { if (!ctx.body || noResponse(ctx) || isAttachment(ctx)) {
return await next() return await next()
} }
let urlParts = ctx.url.split("/") let urlParts = ctx.url.split("/")

View File

@ -0,0 +1,91 @@
import * as setup from "../../tests/utilities"
import {
generateMakeRequest,
generateMakeRequestWithFormData,
MakeRequestResponse,
MakeRequestWithFormDataResponse,
} from "./utils"
import { User } from "@budibase/types"
import { join } from "path"
import { mocks } from "@budibase/backend-core/tests"
const PASSWORD = "testtest"
const NO_LICENSE_MSG = "Endpoint unavailable, license required."
let config = setup.getConfig()
let apiKey: string,
globalUser: User,
makeRequest: MakeRequestResponse,
makeRequestFormData: MakeRequestWithFormDataResponse
beforeAll(async () => {
await config.init()
globalUser = await config.globalUser()
apiKey = await config.generateApiKey(globalUser._id)
makeRequest = generateMakeRequest(apiKey)
makeRequestFormData = generateMakeRequestWithFormData(apiKey)
})
afterAll(setup.afterAll)
describe("check export/import", () => {
async function runExport() {
return await makeRequest("post", `/applications/${config.appId}/export`, {
encryptionPassword: PASSWORD,
excludeRows: true,
})
}
async function runImport() {
const pathToExport = join(
__dirname,
"..",
"..",
"tests",
"assets",
"export.tar.gz"
)
return await makeRequestFormData(
"post",
`/applications/${config.appId}/import`,
{
encryptionPassword: PASSWORD,
appExport: { path: pathToExport },
}
)
}
it("check licensing for export", async () => {
const res = await runExport()
expect(res.status).toBe(403)
expect(res.body.message).toBe(NO_LICENSE_MSG)
})
it("check licensing for import", async () => {
const res = await runImport()
expect(res.status).toBe(403)
expect(res.body.message).toBe(NO_LICENSE_MSG)
})
it("should be able to export app", async () => {
mocks.licenses.useExpandedPublicApi()
const res = await runExport()
expect(res.headers["content-disposition"]).toMatch(
/attachment; filename=".*-export-.*\.tar.gz"/g
)
expect(res.body instanceof Buffer).toBe(true)
expect(res.status).toBe(200)
})
it("should be able to import app", async () => {
mocks.licenses.useExpandedPublicApi()
const res = await runImport()
expect(Object.keys(res.body).length).toBe(0)
// check screens imported correctly
const screens = await config.api.screen.list()
expect(screens.length).toBe(2)
expect(screens[0].routing.route).toBe("/derp")
expect(screens[1].routing.route).toBe("/blank")
expect(res.status).toBe(204)
})
})

View File

@ -92,7 +92,7 @@ describe("no user role update in free", () => {
describe("no user role update in business", () => { describe("no user role update in business", () => {
beforeAll(() => { beforeAll(() => {
updateMock() updateMock()
mocks.licenses.usePublicApiUserRoles() mocks.licenses.useExpandedPublicApi()
}) })
it("should allow 'roles' to be updated", async () => { it("should allow 'roles' to be updated", async () => {
@ -105,7 +105,7 @@ describe("no user role update in business", () => {
}) })
it("should allow 'admin' to be updated", async () => { it("should allow 'admin' to be updated", async () => {
mocks.licenses.usePublicApiUserRoles() mocks.licenses.useExpandedPublicApi()
const res = await makeRequest("post", "/users", { const res = await makeRequest("post", "/users", {
...base(), ...base(),
admin: { global: true }, admin: { global: true },
@ -115,7 +115,7 @@ describe("no user role update in business", () => {
}) })
it("should allow 'builder' to be updated", async () => { it("should allow 'builder' to be updated", async () => {
mocks.licenses.usePublicApiUserRoles() mocks.licenses.useExpandedPublicApi()
const res = await makeRequest("post", "/users", { const res = await makeRequest("post", "/users", {
...base(), ...base(),
builder: { global: true }, builder: { global: true },

View File

@ -11,6 +11,32 @@ export type MakeRequestResponse = (
intAppId?: string intAppId?: string
) => Promise<supertest.Response> ) => Promise<supertest.Response>
export type MakeRequestWithFormDataResponse = (
method: HttpMethod,
endpoint: string,
fields: Record<string, string | { path: string }>,
intAppId?: string
) => Promise<supertest.Response>
function base(
apiKey: string,
endpoint: string,
intAppId: string | null,
isInternal: boolean
) {
const extraHeaders: any = {
"x-budibase-api-key": apiKey,
}
if (intAppId) {
extraHeaders["x-budibase-app-id"] = intAppId
}
const url = isInternal
? endpoint
: checkSlashesInUrl(`/api/public/v1/${endpoint}`)
return { headers: extraHeaders, url }
}
export function generateMakeRequest( export function generateMakeRequest(
apiKey: string, apiKey: string,
isInternal = false isInternal = false
@ -23,18 +49,8 @@ export function generateMakeRequest(
body?: any, body?: any,
intAppId: string | null = config.getAppId() intAppId: string | null = config.getAppId()
) => { ) => {
const extraHeaders: any = { const { headers, url } = base(apiKey, endpoint, intAppId, isInternal)
"x-budibase-api-key": apiKey, const req = request[method](url).set(config.defaultHeaders(headers))
}
if (intAppId) {
extraHeaders["x-budibase-app-id"] = intAppId
}
const url = isInternal
? endpoint
: checkSlashesInUrl(`/api/public/v1/${endpoint}`)
const req = request[method](url).set(config.defaultHeaders(extraHeaders))
if (body) { if (body) {
req.send(body) req.send(body)
} }
@ -43,3 +59,30 @@ export function generateMakeRequest(
return res return res
} }
} }
export function generateMakeRequestWithFormData(
apiKey: string,
isInternal = false
): MakeRequestWithFormDataResponse {
const request = setup.getRequest()!
const config = setup.getConfig()!
return async (
method: HttpMethod,
endpoint: string,
fields: Record<string, string | { path: string }>,
intAppId: string | null = config.getAppId()
) => {
const { headers, url } = base(apiKey, endpoint, intAppId, isInternal)
const req = request[method](url).set(config.defaultHeaders(headers))
for (let [field, value] of Object.entries(fields)) {
if (typeof value === "string") {
req.field(field, value)
} else {
req.attach(field, value.path)
}
}
const res = await req
expect(res.body).toBeDefined()
return res
}
}

View File

@ -0,0 +1,32 @@
import * as setup from "./utilities"
import path from "path"
jest.setTimeout(15000)
const PASSWORD = "testtest"
describe("/applications/:appId/import", () => {
let request = setup.getRequest()
let config = setup.getConfig()
afterAll(setup.afterAll)
beforeAll(async () => {
await config.init()
})
it("should be able to perform import", async () => {
const appId = config.getAppId()
const res = await request
.post(`/api/applications/${appId}/import`)
.field("encryptionPassword", PASSWORD)
.attach("appExport", path.join(__dirname, "assets", "export.tar.gz"))
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.message).toBe("app updated")
const screens = await config.api.screen.list()
expect(screens.length).toBe(2)
expect(screens[0].routing.route).toBe("/derp")
expect(screens[1].routing.route).toBe("/blank")
})
})

View File

@ -18,6 +18,14 @@ export interface paths {
"/applications/{appId}/publish": { "/applications/{appId}/publish": {
post: operations["appPublish"]; post: operations["appPublish"];
}; };
"/applications/{appId}/import": {
/** This endpoint is only available on a business or enterprise license. */
post: operations["appImport"];
};
"/applications/{appId}/export": {
/** This endpoint is only available on a business or enterprise license. */
post: operations["appExport"];
};
"/applications/search": { "/applications/search": {
/** Based on application properties (currently only name) search for applications. */ /** Based on application properties (currently only name) search for applications. */
post: operations["appSearch"]; post: operations["appSearch"];
@ -158,6 +166,12 @@ export interface components {
appUrl: string; appUrl: string;
}; };
}; };
appExport: {
/** @description An optional password used to encrypt the export. */
encryptPassword: string;
/** @description Set whether the internal table rows should be excluded from the export. */
excludeRows: boolean;
};
/** @description The row to be created/updated, based on the table schema. */ /** @description The row to be created/updated, based on the table schema. */
row: { [key: string]: unknown }; row: { [key: string]: unknown };
searchOutput: { searchOutput: {
@ -889,6 +903,54 @@ export interface operations {
}; };
}; };
}; };
/** This endpoint is only available on a business or enterprise license. */
appImport: {
parameters: {
path: {
/** The ID of the app which this request is targeting. */
appId: components["parameters"]["appIdUrl"];
};
};
responses: {
/** Application has been updated. */
204: never;
};
requestBody: {
content: {
"multipart/form-data": {
/** @description Password for the export if it is encrypted. */
encryptedPassword?: string;
/**
* Format: binary
* @description The app export to import.
*/
appExport: string;
};
};
};
};
/** This endpoint is only available on a business or enterprise license. */
appExport: {
parameters: {
path: {
/** The ID of the app which this request is targeting. */
appId: components["parameters"]["appIdUrl"];
};
};
responses: {
/** A gzip tarball containing the app export, encrypted if password provided. */
200: {
content: {
"application/gzip": string;
};
};
};
requestBody: {
content: {
"application/json": components["schemas"]["appExport"];
};
};
};
/** Based on application properties (currently only name) search for applications. */ /** Based on application properties (currently only name) search for applications. */
appSearch: { appSearch: {
responses: { responses: {

View File

@ -0,0 +1,102 @@
import { db as dbCore } from "@budibase/backend-core"
import {
DocumentTypesToImport,
Document,
Database,
RowValue,
} from "@budibase/types"
import backups from "../backups"
export type FileAttributes = {
type: string
path: string
}
function mergeUpdateAndDeleteDocuments(
updateDocs: Document[],
deleteDocs: Document[]
) {
// compress the documents to create and to delete (if same ID, then just update the rev)
const finalToDelete = []
for (let deleteDoc of deleteDocs) {
const found = updateDocs.find(doc => doc._id === deleteDoc._id)
if (found) {
found._rev = deleteDoc._rev
} else {
finalToDelete.push(deleteDoc)
}
}
return [...updateDocs, ...finalToDelete]
}
async function removeImportableDocuments(db: Database) {
// get the references to the documents, not the whole document
const docPromises = []
for (let docType of DocumentTypesToImport) {
docPromises.push(db.allDocs(dbCore.getDocParams(docType)))
}
let documentRefs: { _id: string; _rev: string }[] = []
for (let response of await Promise.all(docPromises)) {
documentRefs = documentRefs.concat(
response.rows.map(row => ({
_id: row.id,
_rev: (row.value as RowValue).rev,
}))
)
}
// add deletion key
return documentRefs.map(ref => ({ _deleted: true, ...ref }))
}
async function getImportableDocuments(db: Database) {
// get the whole document
const docPromises = []
for (let docType of DocumentTypesToImport) {
docPromises.push(
db.allDocs(dbCore.getDocParams(docType, null, { include_docs: true }))
)
}
// map the responses to the document itself
let documents: Document[] = []
for (let response of await Promise.all(docPromises)) {
documents = documents.concat(response.rows.map(row => row.doc))
}
// remove the _rev, stops it being written
documents.forEach(doc => {
delete doc._rev
})
return documents
}
export async function updateWithExport(
appId: string,
file: FileAttributes,
password?: string
) {
const devId = dbCore.getDevAppID(appId)
const tempAppName = `temp_${devId}`
const tempDb = dbCore.getDB(tempAppName)
const appDb = dbCore.getDB(devId)
try {
const template = {
file: {
type: file.type!,
path: file.path!,
password,
},
}
// get a temporary version of the import
// don't need obj store, the existing app already has everything we need
await backups.importApp(devId, tempDb, template, {
importObjStoreContents: false,
})
// get the documents to copy
const toUpdate = await getImportableDocuments(tempDb)
// clear out the old documents
const toDelete = await removeImportableDocuments(appDb)
// now bulk update documents - add new ones, delete old ones and update common ones
await appDb.bulkDocs(mergeUpdateAndDeleteDocuments(toUpdate, toDelete))
} finally {
await tempDb.destroy()
}
}

View File

@ -1,9 +1,11 @@
import * as sync from "./sync" import * as sync from "./sync"
import * as utils from "./utils" import * as utils from "./utils"
import * as applications from "./applications" import * as applications from "./applications"
import * as imports from "./import"
export default { export default {
...sync, ...sync,
...utils, ...utils,
...applications, ...applications,
...imports,
} }

View File

@ -8,11 +8,7 @@ import {
TABLE_ROW_PREFIX, TABLE_ROW_PREFIX,
USER_METDATA_PREFIX, USER_METDATA_PREFIX,
} from "../../../db/utils" } from "../../../db/utils"
import { import { DB_EXPORT_FILE, STATIC_APP_FILES } from "./constants"
DB_EXPORT_FILE,
GLOBAL_DB_EXPORT_FILE,
STATIC_APP_FILES,
} from "./constants"
import fs from "fs" import fs from "fs"
import { join } from "path" import { join } from "path"
import env from "../../../environment" import env from "../../../environment"

View File

@ -151,7 +151,8 @@ export function getListOfAppsInMulti(tmpPath: string) {
export async function importApp( export async function importApp(
appId: string, appId: string,
db: Database, db: Database,
template: TemplateType template: TemplateType,
opts: { importObjStoreContents: boolean } = { importObjStoreContents: true }
) { ) {
let prodAppId = dbCore.getProdAppID(appId) let prodAppId = dbCore.getProdAppID(appId)
let dbStream: any let dbStream: any
@ -165,7 +166,7 @@ export async function importApp(
} }
const contents = fs.readdirSync(tmpPath) const contents = fs.readdirSync(tmpPath)
// have to handle object import // have to handle object import
if (contents.length) { if (contents.length && opts.importObjStoreContents) {
let promises = [] let promises = []
let excludedFiles = [GLOBAL_DB_EXPORT_FILE, DB_EXPORT_FILE] let excludedFiles = [GLOBAL_DB_EXPORT_FILE, DB_EXPORT_FILE]
for (let filename of contents) { for (let filename of contents) {

View File

@ -53,7 +53,6 @@ import {
View, View,
FieldType, FieldType,
RelationshipType, RelationshipType,
ViewV2,
CreateViewRequest, CreateViewRequest,
} from "@budibase/types" } from "@budibase/types"

View File

@ -5,6 +5,7 @@ import { TableAPI } from "./table"
import { ViewV2API } from "./viewV2" import { ViewV2API } from "./viewV2"
import { DatasourceAPI } from "./datasource" import { DatasourceAPI } from "./datasource"
import { LegacyViewAPI } from "./legacyView" import { LegacyViewAPI } from "./legacyView"
import { ScreenAPI } from "./screen"
export default class API { export default class API {
table: TableAPI table: TableAPI
@ -13,6 +14,7 @@ export default class API {
row: RowAPI row: RowAPI
permission: PermissionAPI permission: PermissionAPI
datasource: DatasourceAPI datasource: DatasourceAPI
screen: ScreenAPI
constructor(config: TestConfiguration) { constructor(config: TestConfiguration) {
this.table = new TableAPI(config) this.table = new TableAPI(config)
@ -21,5 +23,6 @@ export default class API {
this.row = new RowAPI(config) this.row = new RowAPI(config)
this.permission = new PermissionAPI(config) this.permission = new PermissionAPI(config)
this.datasource = new DatasourceAPI(config) this.datasource = new DatasourceAPI(config)
this.screen = new ScreenAPI(config)
} }
} }

View File

@ -0,0 +1,18 @@
import TestConfiguration from "../TestConfiguration"
import { Screen } from "@budibase/types"
import { TestAPI } from "./base"
export class ScreenAPI extends TestAPI {
constructor(config: TestConfiguration) {
super(config)
}
list = async (): Promise<Screen[]> => {
const res = await this.request
.get(`/api/screens`)
.set(this.config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
return res.body as Screen[]
}
}

View File

@ -19,7 +19,7 @@
"devDependencies": { "devDependencies": {
"concurrently": "^7.6.0", "concurrently": "^7.6.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"typescript": "4.7.3" "typescript": "5.2.2"
}, },
"nx": { "nx": {
"targets": { "targets": {
@ -43,7 +43,6 @@
} }
] ]
} }
} }
} }
} }

View File

@ -45,6 +45,6 @@
"rollup-plugin-node-globals": "^1.4.0", "rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^7.0.2", "rollup-plugin-terser": "^7.0.2",
"typescript": "4.7.3" "typescript": "5.2.2"
} }
} }

View File

@ -24,7 +24,7 @@
"concurrently": "^7.6.0", "concurrently": "^7.6.0",
"koa-body": "4.2.0", "koa-body": "4.2.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"typescript": "4.7.3" "typescript": "5.2.2"
}, },
"dependencies": { "dependencies": {
"scim-patch": "^0.7.0" "scim-patch": "^0.7.0"

View File

@ -39,6 +39,25 @@ export enum DocumentType {
AUDIT_LOG = "al", AUDIT_LOG = "al",
} }
// these are the core documents that make up the data, design
// and automation sections of an app. This excludes any internal
// rows as we shouldn't import data.
export const DocumentTypesToImport: DocumentType[] = [
DocumentType.ROLE,
DocumentType.DATASOURCE,
DocumentType.DATASOURCE_PLUS,
DocumentType.TABLE,
DocumentType.AUTOMATION,
DocumentType.WEBHOOK,
DocumentType.SCREEN,
DocumentType.QUERY,
DocumentType.METADATA,
DocumentType.MEM_VIEW,
// Deprecated but still copied
DocumentType.INSTANCE,
DocumentType.LAYOUT,
]
// these documents don't really exist, they are part of other // these documents don't really exist, they are part of other
// documents or enriched into existence as part of get requests // documents or enriched into existence as part of get requests
export enum VirtualDocumentType { export enum VirtualDocumentType {

View File

@ -11,7 +11,7 @@ export enum Feature {
SYNC_AUTOMATIONS = "syncAutomations", SYNC_AUTOMATIONS = "syncAutomations",
APP_BUILDERS = "appBuilders", APP_BUILDERS = "appBuilders",
OFFLINE = "offline", OFFLINE = "offline",
USER_ROLE_PUBLIC_API = "userRolePublicApi", EXPANDED_PUBLIC_API = "expandedPublicApi",
VIEW_PERMISSIONS = "viewPermissions", VIEW_PERMISSIONS = "viewPermissions",
} }

View File

@ -88,7 +88,6 @@
"@types/server-destroy": "1.0.1", "@types/server-destroy": "1.0.1",
"@types/supertest": "2.0.12", "@types/supertest": "2.0.12",
"@types/uuid": "8.3.4", "@types/uuid": "8.3.4",
"@typescript-eslint/parser": "5.45.0",
"copyfiles": "2.4.1", "copyfiles": "2.4.1",
"eslint": "6.8.0", "eslint": "6.8.0",
"jest": "29.6.2", "jest": "29.6.2",
@ -100,7 +99,7 @@
"timekeeper": "2.2.0", "timekeeper": "2.2.0",
"ts-node": "10.8.1", "ts-node": "10.8.1",
"tsconfig-paths": "4.0.0", "tsconfig-paths": "4.0.0",
"typescript": "4.7.3", "typescript": "5.2.2",
"update-dotenv": "1.1.1" "update-dotenv": "1.1.1"
}, },
"nx": { "nx": {

View File

@ -38,7 +38,7 @@
"ts-jest": "29.1.1", "ts-jest": "29.1.1",
"ts-node": "10.8.1", "ts-node": "10.8.1",
"tsconfig-paths": "4.0.0", "tsconfig-paths": "4.0.0",
"typescript": "4.7.3" "typescript": "5.2.2"
}, },
"dependencies": { "dependencies": {
"@budibase/backend-core": "^2.3.17", "@budibase/backend-core": "^2.3.17",

View File

@ -6696,51 +6696,52 @@
dependencies: dependencies:
"@types/yargs-parser" "*" "@types/yargs-parser" "*"
"@typescript-eslint/parser@5.45.0": "@typescript-eslint/parser@6.7.2":
version "5.45.0" version "6.7.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.45.0.tgz#b18a5f6b3cf1c2b3e399e9d2df4be40d6b0ddd0e" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.7.2.tgz#e0ae93771441b9518e67d0660c79e3a105497af4"
integrity sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ== integrity sha512-KA3E4ox0ws+SPyxQf9iSI25R6b4Ne78ORhNHeVKrPQnoYsb9UhieoiRoJgrzgEeKGOXhcY1i8YtOeCHHTDa6Fw==
dependencies: dependencies:
"@typescript-eslint/scope-manager" "5.45.0" "@typescript-eslint/scope-manager" "6.7.2"
"@typescript-eslint/types" "5.45.0" "@typescript-eslint/types" "6.7.2"
"@typescript-eslint/typescript-estree" "5.45.0" "@typescript-eslint/typescript-estree" "6.7.2"
"@typescript-eslint/visitor-keys" "6.7.2"
debug "^4.3.4" debug "^4.3.4"
"@typescript-eslint/scope-manager@5.45.0": "@typescript-eslint/scope-manager@6.7.2":
version "5.45.0" version "6.7.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.45.0.tgz#7a4ac1bfa9544bff3f620ab85947945938319a96" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.7.2.tgz#cf59a2095d2f894770c94be489648ad1c78dc689"
integrity sha512-noDMjr87Arp/PuVrtvN3dXiJstQR1+XlQ4R1EvzG+NMgXi8CuMCXpb8JqNtFHKceVSQ985BZhfRdowJzbv4yKw== integrity sha512-bgi6plgyZjEqapr7u2mhxGR6E8WCzKNUFWNh6fkpVe9+yzRZeYtDTbsIBzKbcxI+r1qVWt6VIoMSNZ4r2A+6Yw==
dependencies: dependencies:
"@typescript-eslint/types" "5.45.0" "@typescript-eslint/types" "6.7.2"
"@typescript-eslint/visitor-keys" "5.45.0" "@typescript-eslint/visitor-keys" "6.7.2"
"@typescript-eslint/types@4.33.0": "@typescript-eslint/types@4.33.0":
version "4.33.0" version "4.33.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72"
integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ== integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==
"@typescript-eslint/types@5.45.0":
version "5.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.45.0.tgz#794760b9037ee4154c09549ef5a96599621109c5"
integrity sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA==
"@typescript-eslint/types@5.53.0": "@typescript-eslint/types@5.53.0":
version "5.53.0" version "5.53.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.53.0.tgz#f79eca62b97e518ee124086a21a24f3be267026f" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.53.0.tgz#f79eca62b97e518ee124086a21a24f3be267026f"
integrity sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A== integrity sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A==
"@typescript-eslint/typescript-estree@5.45.0": "@typescript-eslint/types@6.7.2":
version "5.45.0" version "6.7.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.0.tgz#f70a0d646d7f38c0dfd6936a5e171a77f1e5291d" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.7.2.tgz#75a615a6dbeca09cafd102fe7f465da1d8a3c066"
integrity sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ== integrity sha512-flJYwMYgnUNDAN9/GAI3l8+wTmvTYdv64fcH8aoJK76Y+1FCZ08RtI5zDerM/FYT5DMkAc+19E4aLmd5KqdFyg==
"@typescript-eslint/typescript-estree@6.7.2":
version "6.7.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.2.tgz#ce5883c23b581a5caf878af641e49dd0349238c7"
integrity sha512-kiJKVMLkoSciGyFU0TOY0fRxnp9qq1AzVOHNeN1+B9erKFCJ4Z8WdjAkKQPP+b1pWStGFqezMLltxO+308dJTQ==
dependencies: dependencies:
"@typescript-eslint/types" "5.45.0" "@typescript-eslint/types" "6.7.2"
"@typescript-eslint/visitor-keys" "5.45.0" "@typescript-eslint/visitor-keys" "6.7.2"
debug "^4.3.4" debug "^4.3.4"
globby "^11.1.0" globby "^11.1.0"
is-glob "^4.0.3" is-glob "^4.0.3"
semver "^7.3.7" semver "^7.5.4"
tsutils "^3.21.0" ts-api-utils "^1.0.1"
"@typescript-eslint/typescript-estree@^4.33.0": "@typescript-eslint/typescript-estree@^4.33.0":
version "4.33.0" version "4.33.0"
@ -6776,14 +6777,6 @@
"@typescript-eslint/types" "4.33.0" "@typescript-eslint/types" "4.33.0"
eslint-visitor-keys "^2.0.0" eslint-visitor-keys "^2.0.0"
"@typescript-eslint/visitor-keys@5.45.0":
version "5.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz#e0d160e9e7fdb7f8da697a5b78e7a14a22a70528"
integrity sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg==
dependencies:
"@typescript-eslint/types" "5.45.0"
eslint-visitor-keys "^3.3.0"
"@typescript-eslint/visitor-keys@5.53.0": "@typescript-eslint/visitor-keys@5.53.0":
version "5.53.0" version "5.53.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz#8a5126623937cdd909c30d8fa72f79fa56cc1a9f" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz#8a5126623937cdd909c30d8fa72f79fa56cc1a9f"
@ -6792,6 +6785,14 @@
"@typescript-eslint/types" "5.53.0" "@typescript-eslint/types" "5.53.0"
eslint-visitor-keys "^3.3.0" eslint-visitor-keys "^3.3.0"
"@typescript-eslint/visitor-keys@6.7.2":
version "6.7.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.2.tgz#4cb2bd786f1f459731b0ad1584c9f73e1c7a4d5c"
integrity sha512-uVw9VIMFBUTz8rIeaUT3fFe8xIUx8r4ywAdlQv1ifH+6acn/XF8Y6rwJ7XNmkNMDrTW+7+vxFFPIF40nJCVsMQ==
dependencies:
"@typescript-eslint/types" "6.7.2"
eslint-visitor-keys "^3.4.1"
"@vitest/expect@0.29.8": "@vitest/expect@0.29.8":
version "0.29.8" version "0.29.8"
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.29.8.tgz#6ecdd031b4ea8414717d10b65ccd800908384612" resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.29.8.tgz#6ecdd031b4ea8414717d10b65ccd800908384612"
@ -21927,6 +21928,13 @@ semver@^7.3.2, semver@^7.3.7, semver@^7.3.8:
dependencies: dependencies:
lru-cache "^6.0.0" lru-cache "^6.0.0"
semver@^7.5.4:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
dependencies:
lru-cache "^6.0.0"
semver@~2.3.1: semver@~2.3.1:
version "2.3.2" version "2.3.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-2.3.2.tgz#b9848f25d6cf36333073ec9ef8856d42f1233e52" resolved "https://registry.yarnpkg.com/semver/-/semver-2.3.2.tgz#b9848f25d6cf36333073ec9ef8856d42f1233e52"
@ -23882,6 +23890,11 @@ triple-beam@^1.3.0:
resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
ts-api-utils@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331"
integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==
ts-graphviz@^1.5.0: ts-graphviz@^1.5.0:
version "1.5.4" version "1.5.4"
resolved "https://registry.yarnpkg.com/ts-graphviz/-/ts-graphviz-1.5.4.tgz#61a3059afeac4f6d4be3c6729a4d88546ca9e095" resolved "https://registry.yarnpkg.com/ts-graphviz/-/ts-graphviz-1.5.4.tgz#61a3059afeac4f6d4be3c6729a4d88546ca9e095"
@ -24120,10 +24133,10 @@ typeof@^1.0.0:
resolved "https://registry.yarnpkg.com/typeof/-/typeof-1.0.0.tgz#9c84403f2323ae5399167275497638ea1d2f2440" resolved "https://registry.yarnpkg.com/typeof/-/typeof-1.0.0.tgz#9c84403f2323ae5399167275497638ea1d2f2440"
integrity sha512-Pze0mIxYXhaJdpw1ayMzOA7rtGr1OmsTY/Z+FWtRKIqXFz6aoDLjqdbWE/tcIBSC8nhnVXiRrEXujodR/xiFAA== integrity sha512-Pze0mIxYXhaJdpw1ayMzOA7rtGr1OmsTY/Z+FWtRKIqXFz6aoDLjqdbWE/tcIBSC8nhnVXiRrEXujodR/xiFAA==
typescript@4.7.3: typescript@5.2.2:
version "4.7.3" version "5.2.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA== integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
"typescript@>=3 < 6": "typescript@>=3 < 6":
version "5.0.4" version "5.0.4"