mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
164 changed files with 3443 additions and 1057 deletions
@ -0,0 +1,61 @@ |
|||||
|
const { DocumentTypes } = require("../db/constants") |
||||
|
const { getGlobalDB } = require("../tenancy") |
||||
|
|
||||
|
exports.MIGRATION_DBS = { |
||||
|
GLOBAL_DB: "GLOBAL_DB", |
||||
|
} |
||||
|
|
||||
|
exports.MIGRATIONS = { |
||||
|
USER_EMAIL_VIEW_CASING: "user_email_view_casing", |
||||
|
} |
||||
|
|
||||
|
const DB_LOOKUP = { |
||||
|
[exports.MIGRATION_DBS.GLOBAL_DB]: [ |
||||
|
exports.MIGRATIONS.USER_EMAIL_VIEW_CASING, |
||||
|
], |
||||
|
} |
||||
|
|
||||
|
exports.getMigrationsDoc = async db => { |
||||
|
// get the migrations doc
|
||||
|
try { |
||||
|
return await db.get(DocumentTypes.MIGRATIONS) |
||||
|
} catch (err) { |
||||
|
if (err.status && err.status === 404) { |
||||
|
return { _id: DocumentTypes.MIGRATIONS } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
exports.migrateIfRequired = async (migrationDb, migrationName, migrateFn) => { |
||||
|
try { |
||||
|
let db |
||||
|
if (migrationDb === exports.MIGRATION_DBS.GLOBAL_DB) { |
||||
|
db = getGlobalDB() |
||||
|
} else { |
||||
|
throw new Error(`Unrecognised migration db [${migrationDb}]`) |
||||
|
} |
||||
|
|
||||
|
if (!DB_LOOKUP[migrationDb].includes(migrationName)) { |
||||
|
throw new Error( |
||||
|
`Unrecognised migration name [${migrationName}] for db [${migrationDb}]` |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
const doc = await exports.getMigrationsDoc(db) |
||||
|
// exit if the migration has been performed
|
||||
|
if (doc[migrationName]) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
console.log(`Performing migration: ${migrationName}`) |
||||
|
await migrateFn() |
||||
|
console.log(`Migration complete: ${migrationName}`) |
||||
|
|
||||
|
// mark as complete
|
||||
|
doc[migrationName] = Date.now() |
||||
|
await db.put(doc) |
||||
|
} catch (err) { |
||||
|
console.error(`Error performing migration: ${migrationName}: `, err) |
||||
|
throw err |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
|
||||
|
exports[`migrations should match snapshot 1`] = ` |
||||
|
Object { |
||||
|
"_id": "migrations", |
||||
|
"_rev": "1-af6c272fe081efafecd2ea49a8fcbb40", |
||||
|
"user_email_view_casing": 1487076708000, |
||||
|
} |
||||
|
`; |
||||
@ -0,0 +1,60 @@ |
|||||
|
require("../../tests/utilities/dbConfig") |
||||
|
|
||||
|
const { migrateIfRequired, MIGRATION_DBS, MIGRATIONS, getMigrationsDoc } = require("../index") |
||||
|
const database = require("../../db") |
||||
|
const { |
||||
|
StaticDatabases, |
||||
|
} = require("../../db/utils") |
||||
|
|
||||
|
Date.now = jest.fn(() => 1487076708000) |
||||
|
let db |
||||
|
|
||||
|
describe("migrations", () => { |
||||
|
|
||||
|
const migrationFunction = jest.fn() |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
db = database.getDB(StaticDatabases.GLOBAL.name) |
||||
|
}) |
||||
|
|
||||
|
afterEach(async () => { |
||||
|
jest.clearAllMocks() |
||||
|
await db.destroy() |
||||
|
}) |
||||
|
|
||||
|
const validMigration = () => { |
||||
|
return migrateIfRequired(MIGRATION_DBS.GLOBAL_DB, MIGRATIONS.USER_EMAIL_VIEW_CASING, migrationFunction) |
||||
|
} |
||||
|
|
||||
|
it("should run a new migration", async () => { |
||||
|
await validMigration() |
||||
|
expect(migrationFunction).toHaveBeenCalled() |
||||
|
}) |
||||
|
|
||||
|
it("should match snapshot", async () => { |
||||
|
await validMigration() |
||||
|
const doc = await getMigrationsDoc(db) |
||||
|
expect(doc).toMatchSnapshot() |
||||
|
}) |
||||
|
|
||||
|
it("should skip a previously run migration", async () => { |
||||
|
await validMigration() |
||||
|
await validMigration() |
||||
|
expect(migrationFunction).toHaveBeenCalledTimes(1) |
||||
|
}) |
||||
|
|
||||
|
it("should reject an unknown migration name", async () => { |
||||
|
expect(async () => { |
||||
|
await migrateIfRequired(MIGRATION_DBS.GLOBAL_DB, "bogus_name", migrationFunction) |
||||
|
}).rejects.toThrow() |
||||
|
expect(migrationFunction).not.toHaveBeenCalled() |
||||
|
}) |
||||
|
|
||||
|
it("should reject an unknown database name", async () => { |
||||
|
expect(async () => { |
||||
|
await migrateIfRequired("bogus_db", MIGRATIONS.USER_EMAIL_VIEW_CASING, migrationFunction) |
||||
|
}).rejects.toThrow() |
||||
|
expect(migrationFunction).not.toHaveBeenCalled() |
||||
|
}) |
||||
|
|
||||
|
}) |
||||
@ -1,5 +1,5 @@ |
|||||
const PouchDB = require("pouchdb") |
const PouchDB = require("pouchdb") |
||||
const env = require("../../../../environment") |
const env = require("../../environment") |
||||
|
|
||||
let POUCH_DB_DEFAULTS |
let POUCH_DB_DEFAULTS |
||||
|
|
||||
@ -1,3 +1,3 @@ |
|||||
const packageConfiguration = require("../../../../index") |
const packageConfiguration = require("../../index") |
||||
const CouchDB = require("./db") |
const CouchDB = require("./db") |
||||
packageConfiguration.init(CouchDB) |
packageConfiguration.init(CouchDB) |
||||
@ -0,0 +1,102 @@ |
|||||
|
context("Rename an App", () => { |
||||
|
beforeEach(() => { |
||||
|
cy.login() |
||||
|
cy.createTestApp() |
||||
|
}) |
||||
|
|
||||
|
it("should rename an unpublished application", () => { |
||||
|
const appRename = "Cypress Renamed" |
||||
|
// Rename app, Search for app, Confirm name was changed
|
||||
|
cy.get(".home-logo").click() |
||||
|
renameApp(appRename) |
||||
|
cy.searchForApplication(appRename) |
||||
|
cy.get(".appGrid").find(".wrapper").should("have.length", 1) |
||||
|
}) |
||||
|
|
||||
|
xit("Should rename a published application", () => { |
||||
|
// It is not possible to rename a published application
|
||||
|
const appRename = "Cypress Renamed" |
||||
|
// Publish the app
|
||||
|
cy.get(".toprightnav") |
||||
|
cy.get(".spectrum-Button").contains("Publish").click({force: true}) |
||||
|
cy.get(".spectrum-Dialog-grid") |
||||
|
.within(() => { |
||||
|
// Click publish again within the modal
|
||||
|
cy.get(".spectrum-Button").contains("Publish").click({force: true}) |
||||
|
}) |
||||
|
// Rename app, Search for app, Confirm name was changed
|
||||
|
cy.get(".home-logo").click() |
||||
|
renameApp(appRename, true) |
||||
|
cy.searchForApplication(appRename) |
||||
|
cy.get(".appGrid").find(".wrapper").should("have.length", 1) |
||||
|
}) |
||||
|
|
||||
|
it("Should try to rename an application to have no name", () => { |
||||
|
cy.get(".home-logo").click() |
||||
|
renameApp(" ", false, true) |
||||
|
// Close modal and confirm name has not been changed
|
||||
|
cy.get(".spectrum-Dialog-grid").contains("Cancel").click() |
||||
|
cy.searchForApplication("Cypress Tests") |
||||
|
cy.get(".appGrid").find(".wrapper").should("have.length", 1) |
||||
|
}) |
||||
|
|
||||
|
xit("Should create two applications with the same name", () => { |
||||
|
// It is not possible to have applications with the same name
|
||||
|
const appName = "Cypress Tests" |
||||
|
cy.visit(`localhost:${Cypress.env("PORT")}/builder`) |
||||
|
cy.wait(500) |
||||
|
cy.get(".spectrum-Button").contains("Create app").click({force: true}) |
||||
|
cy.contains(/Start from scratch/).click() |
||||
|
cy.get(".spectrum-Modal") |
||||
|
.within(() => { |
||||
|
cy.get("input").eq(0).type(appName) |
||||
|
cy.get(".spectrum-ButtonGroup").contains("Create app").click({force: true}) |
||||
|
cy.get(".error").should("have.text", "Another app with the same name already exists") |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
it("should validate application names", () => { |
||||
|
// App name must be letters, numbers and spaces only
|
||||
|
// This test checks numbers and special characters specifically
|
||||
|
const numberName = 12345 |
||||
|
const specialCharName = "£$%^" |
||||
|
cy.get(".home-logo").click() |
||||
|
renameApp(numberName) |
||||
|
cy.searchForApplication(numberName) |
||||
|
cy.get(".appGrid").find(".wrapper").should("have.length", 1) |
||||
|
renameApp(specialCharName) |
||||
|
cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only") |
||||
|
}) |
||||
|
|
||||
|
const renameApp = (appName, published, noName) => { |
||||
|
cy.request(`localhost:${Cypress.env("PORT")}/api/applications?status=all`) |
||||
|
.its("body") |
||||
|
.then(val => { |
||||
|
if (val.length > 0) { |
||||
|
cy.get(".title > :nth-child(3) > .spectrum-Icon").click() |
||||
|
// Check for when an app is published
|
||||
|
if (published == true){ |
||||
|
// Should not have Edit as option, will unpublish app
|
||||
|
cy.should("not.have.value", "Edit") |
||||
|
cy.get(".spectrum-Menu").contains("Unpublish").click() |
||||
|
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click() |
||||
|
cy.get(".title > :nth-child(3) > .spectrum-Icon").click() |
||||
|
} |
||||
|
cy.contains("Edit").click() |
||||
|
cy.get(".spectrum-Modal") |
||||
|
.within(() => { |
||||
|
if (noName == true){ |
||||
|
cy.get("input").clear() |
||||
|
cy.get(".spectrum-Dialog-grid").click() |
||||
|
.contains("App name must be letters, numbers and spaces only") |
||||
|
return cy |
||||
|
} |
||||
|
cy.get("input").clear() |
||||
|
cy.get("input").eq(0).type(appName).should("have.value", appName).blur() |
||||
|
cy.get(".spectrum-ButtonGroup").contains("Save").click({force: true}) |
||||
|
cy.wait(500) |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
@ -0,0 +1,159 @@ |
|||||
|
<script context="module"> |
||||
|
import { Label } from "@budibase/bbui" |
||||
|
|
||||
|
export const EditorModes = { |
||||
|
JS: { |
||||
|
name: "javascript", |
||||
|
json: false, |
||||
|
}, |
||||
|
JSON: { |
||||
|
name: "javascript", |
||||
|
json: true, |
||||
|
}, |
||||
|
SQL: { |
||||
|
name: "sql", |
||||
|
}, |
||||
|
Handlebars: { |
||||
|
name: "handlebars", |
||||
|
base: "text/html", |
||||
|
}, |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<script> |
||||
|
import CodeMirror from "components/integration/codemirror" |
||||
|
import { themeStore } from "builderStore" |
||||
|
import { createEventDispatcher, onMount } from "svelte" |
||||
|
|
||||
|
export let mode = EditorModes.JS |
||||
|
export let value = "" |
||||
|
export let height = 300 |
||||
|
export let resize = "none" |
||||
|
export let readonly = false |
||||
|
export let hints = [] |
||||
|
export let label |
||||
|
|
||||
|
const dispatch = createEventDispatcher() |
||||
|
let textarea |
||||
|
let editor |
||||
|
|
||||
|
// Keep editor up to date with value |
||||
|
$: editor?.setValue(value || "") |
||||
|
|
||||
|
// Creates an instance of a code mirror editor |
||||
|
async function createEditor(mode, value) { |
||||
|
if (!CodeMirror || !textarea || editor) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Configure CM options |
||||
|
const lightTheme = $themeStore.theme.includes("light") |
||||
|
const options = { |
||||
|
mode, |
||||
|
value: value || "", |
||||
|
readOnly: readonly, |
||||
|
theme: lightTheme ? "default" : "tomorrow-night-eighties", |
||||
|
|
||||
|
// Style |
||||
|
lineNumbers: true, |
||||
|
lineWrapping: true, |
||||
|
indentWithTabs: true, |
||||
|
indentUnit: 2, |
||||
|
tabSize: 2, |
||||
|
|
||||
|
// QOL addons |
||||
|
extraKeys: { "Ctrl-Space": "autocomplete" }, |
||||
|
styleActiveLine: { nonEmpty: true }, |
||||
|
autoCloseBrackets: true, |
||||
|
matchBrackets: true, |
||||
|
} |
||||
|
|
||||
|
// Register hints plugin if desired |
||||
|
if (hints?.length) { |
||||
|
CodeMirror.registerHelper("hint", "dictionaryHint", function (editor) { |
||||
|
const cursor = editor.getCursor() |
||||
|
return { |
||||
|
list: hints, |
||||
|
from: CodeMirror.Pos(cursor.line, cursor.ch), |
||||
|
to: CodeMirror.Pos(cursor.line, cursor.ch), |
||||
|
} |
||||
|
}) |
||||
|
CodeMirror.commands.autocomplete = function (cm) { |
||||
|
CodeMirror.showHint(cm, CodeMirror.hint.dictionaryHint) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Construct CM instance |
||||
|
editor = CodeMirror.fromTextArea(textarea, options) |
||||
|
|
||||
|
// Use a blur handler to update the value |
||||
|
editor.on("blur", instance => { |
||||
|
dispatch("change", instance.getValue()) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// Export a function to expose caret position |
||||
|
export const getCaretPosition = () => { |
||||
|
const cursor = editor.getCursor() |
||||
|
return { |
||||
|
start: cursor.ch, |
||||
|
end: cursor.ch, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onMount(() => { |
||||
|
// Create the editor with initial value |
||||
|
createEditor(mode, value) |
||||
|
|
||||
|
// Clean up editor on unmount |
||||
|
return () => { |
||||
|
if (editor) { |
||||
|
editor.toTextArea() |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
{#if label} |
||||
|
<div style="margin-bottom: var(--spacing-s)"> |
||||
|
<Label small>{label}</Label> |
||||
|
</div> |
||||
|
{/if} |
||||
|
<div |
||||
|
style={`--code-mirror-height: ${height}px; --code-mirror-resize: ${resize}`} |
||||
|
> |
||||
|
<textarea tabindex="0" bind:this={textarea} readonly {value} /> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
div :global(.CodeMirror) { |
||||
|
height: var(--code-mirror-height); |
||||
|
min-height: var(--code-mirror-height); |
||||
|
font-family: monospace; |
||||
|
line-height: 1.3; |
||||
|
border: var(--spectrum-alias-border-size-thin) solid; |
||||
|
border-color: var(--spectrum-alias-border-color); |
||||
|
border-radius: var(--border-radius-s); |
||||
|
resize: var(--code-mirror-resize); |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
/* Override default active line highlight colour in dark theme */ |
||||
|
div |
||||
|
:global(.CodeMirror-focused.cm-s-tomorrow-night-eighties |
||||
|
.CodeMirror-activeline-background) { |
||||
|
background: rgba(255, 255, 255, 0.075); |
||||
|
} |
||||
|
|
||||
|
/* Remove active line styling when not focused */ |
||||
|
div |
||||
|
:global(.CodeMirror:not(.CodeMirror-focused) |
||||
|
.CodeMirror-activeline-background) { |
||||
|
background: unset; |
||||
|
} |
||||
|
|
||||
|
/* Add a spectrum themed border when focused */ |
||||
|
div :global(.CodeMirror-focused) { |
||||
|
border-color: var(--spectrum-alias-border-color-mouse-focus); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,15 @@ |
|||||
|
<script> |
||||
|
import { Input } from "@budibase/bbui" |
||||
|
import { isJSBinding } from "@budibase/string-templates" |
||||
|
|
||||
|
export let value |
||||
|
|
||||
|
$: isJS = isJSBinding(value) |
||||
|
</script> |
||||
|
|
||||
|
<Input |
||||
|
{...$$props} |
||||
|
value={isJS ? "(JavaScript function)" : value} |
||||
|
readonly={isJS} |
||||
|
on:change |
||||
|
/> |
||||
@ -1,12 +1,22 @@ |
|||||
import CodeMirror from "codemirror" |
import CodeMirror from "codemirror" |
||||
import "codemirror/lib/codemirror.css" |
import "codemirror/lib/codemirror.css" |
||||
import "codemirror/theme/tomorrow-night-eighties.css" |
|
||||
import "codemirror/addon/hint/show-hint.css" |
// Modes
|
||||
import "codemirror/theme/neo.css" |
import "codemirror/mode/javascript/javascript" |
||||
import "codemirror/mode/sql/sql" |
import "codemirror/mode/sql/sql" |
||||
import "codemirror/mode/css/css" |
import "codemirror/mode/css/css" |
||||
import "codemirror/mode/handlebars/handlebars" |
import "codemirror/mode/handlebars/handlebars" |
||||
import "codemirror/mode/javascript/javascript" |
|
||||
|
// Hints
|
||||
import "codemirror/addon/hint/show-hint" |
import "codemirror/addon/hint/show-hint" |
||||
|
import "codemirror/addon/hint/show-hint.css" |
||||
|
|
||||
|
// Theming
|
||||
|
import "codemirror/theme/tomorrow-night-eighties.css" |
||||
|
|
||||
|
// Functional addons
|
||||
|
import "codemirror/addon/selection/active-line" |
||||
|
import "codemirror/addon/edit/closebrackets" |
||||
|
import "codemirror/addon/edit/matchbrackets" |
||||
|
|
||||
export default CodeMirror |
export default CodeMirror |
||||
|
|||||
File diff suppressed because it is too large
@ -1,10 +1,9 @@ |
|||||
const { performBackup } = require("../../utilities/fileSystem") |
const { streamBackup } = require("../../utilities/fileSystem") |
||||
|
|
||||
exports.exportAppDump = async function (ctx) { |
exports.exportAppDump = async function (ctx) { |
||||
const { appId } = ctx.query |
const { appId } = ctx.query |
||||
const appname = decodeURI(ctx.query.appname) |
const appName = decodeURI(ctx.query.appname) |
||||
const backupIdentifier = `${appname}Backup${new Date().getTime()}.txt` |
const backupIdentifier = `${appName}-export-${new Date().getTime()}.txt` |
||||
|
|
||||
ctx.attachment(backupIdentifier) |
ctx.attachment(backupIdentifier) |
||||
ctx.body = await performBackup(appId, backupIdentifier) |
ctx.body = await streamBackup(appId) |
||||
} |
} |
||||
|
|||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue