mirror of https://github.com/Budibase/budibase.git
12 changed files with 502 additions and 247 deletions
@ -1,59 +1,162 @@ |
|||
<script> |
|||
import cm from "./codemirror" |
|||
import CodeMirror from "./codemirror" |
|||
import { onMount, createEventDispatcher } from "svelte" |
|||
import { themeStore } from "builderStore" |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
export let value |
|||
export let mode = "sql" |
|||
const THEMES = { |
|||
DARK: "tomorrow-night-eighties", |
|||
LIGHT: "default", |
|||
} |
|||
|
|||
let editor |
|||
let codemirror |
|||
export let value = "" |
|||
export let readOnly = false |
|||
export let lineNumbers = true |
|||
export let tab = true |
|||
export let mode |
|||
|
|||
let width |
|||
let height |
|||
|
|||
$: { |
|||
if (codemirror) { |
|||
const { left, top } = codemirror.getScrollInfo() |
|||
codemirror.setValue(value) |
|||
codemirror.scrollTo(left, top) |
|||
// We have to expose set and update methods, rather |
|||
// than making this state-driven through props, |
|||
// because it's difficult to update an editor |
|||
// without resetting scroll otherwise |
|||
export async function set(new_value, new_mode) { |
|||
if (new_mode !== mode) { |
|||
await createEditor((mode = new_mode)) |
|||
} |
|||
|
|||
value = new_value |
|||
updating_externally = true |
|||
if (editor) editor.setValue(value) |
|||
updating_externally = false |
|||
} |
|||
|
|||
export function update(new_value) { |
|||
value = new_value |
|||
|
|||
if (editor) { |
|||
const { left, top } = editor.getScrollInfo() |
|||
editor.setValue((value = new_value)) |
|||
editor.scrollTo(left, top) |
|||
} |
|||
} |
|||
|
|||
export function resize() { |
|||
editor.refresh() |
|||
} |
|||
|
|||
export function focus() { |
|||
editor.focus() |
|||
} |
|||
|
|||
const modes = { |
|||
js: { |
|||
name: "javascript", |
|||
json: false, |
|||
}, |
|||
json: { |
|||
name: "javascript", |
|||
json: true, |
|||
}, |
|||
sql: { |
|||
name: "sql", |
|||
}, |
|||
svelte: { |
|||
name: "handlebars", |
|||
base: "text/html", |
|||
}, |
|||
} |
|||
|
|||
const refs = {} |
|||
let editor |
|||
let updating_externally = false |
|||
let marker |
|||
let error_line |
|||
let destroyed = false |
|||
|
|||
$: if (editor && width && height) { |
|||
editor.refresh() |
|||
} |
|||
|
|||
onMount(() => { |
|||
if (codemirror) codemirror.toTextArea() |
|||
createEditor(mode).then(() => { |
|||
if (editor) editor.setValue(value || "") |
|||
}) |
|||
|
|||
codemirror = cm.fromTextArea(editor, { |
|||
lineNumbers: true, |
|||
mode, |
|||
return () => { |
|||
destroyed = true |
|||
if (editor) editor.toTextArea() |
|||
} |
|||
}) |
|||
|
|||
let first = true |
|||
|
|||
async function createEditor(mode) { |
|||
if (destroyed || !CodeMirror) return |
|||
|
|||
if (editor) editor.toTextArea() |
|||
|
|||
const opts = { |
|||
lineNumbers, |
|||
lineWrapping: true, |
|||
indentWithTabs: true, |
|||
indentUnit: 2, |
|||
tabSize: 2, |
|||
value: "", |
|||
mode: modes[mode] || { |
|||
name: mode, |
|||
}, |
|||
readOnly, |
|||
autoCloseBrackets: true, |
|||
autoCloseTags: true, |
|||
extraKeys: { |
|||
"Ctrl-/": "toggleComment", |
|||
}, |
|||
}) |
|||
codemirror.on("change", instance => { |
|||
const code = instance.getValue() |
|||
dispatch("change", code) |
|||
theme: $themeStore.darkMode ? THEMES.DARK : THEMES.LIGHT, |
|||
} |
|||
|
|||
if (!tab) |
|||
opts.extraKeys = { |
|||
Tab: tab, |
|||
"Shift-Tab": tab, |
|||
} |
|||
|
|||
// Creating a text editor is a lot of work, so we yield |
|||
// the main thread for a moment. This helps reduce jank |
|||
if (first) await sleep(50) |
|||
|
|||
if (destroyed) return |
|||
|
|||
editor = CodeMirror.fromTextArea(refs.editor, opts) |
|||
|
|||
editor.on("change", instance => { |
|||
if (!updating_externally) { |
|||
const value = instance.getValue() |
|||
dispatch("change", { value }) |
|||
} |
|||
}) |
|||
|
|||
codemirror.setValue(value || "") |
|||
if (first) await sleep(50) |
|||
editor.refresh() |
|||
|
|||
return () => { |
|||
if (codemirror) codemirror.toTextArea() |
|||
} |
|||
}) |
|||
first = false |
|||
} |
|||
|
|||
function sleep(ms) { |
|||
return new Promise(fulfil => setTimeout(fulfil, ms)) |
|||
} |
|||
</script> |
|||
|
|||
<textarea bind:value bind:this={editor} /> |
|||
<textarea tabindex="0" bind:this={refs.editor} readonly {value} /> |
|||
|
|||
<style> |
|||
textarea { |
|||
background: var(--background); |
|||
border-radius: var(--border-radius-m); |
|||
visibility: hidden; |
|||
} |
|||
|
|||
:global(.CodeMirror) { |
|||
height: auto !important; |
|||
border-radius: var(--border-radius-m); |
|||
font-family: var(--font-sans) !important; |
|||
} |
|||
</style> |
|||
|
|||
@ -1,162 +0,0 @@ |
|||
<script> |
|||
import CodeMirror from "./codemirror" |
|||
import { onMount, createEventDispatcher } from "svelte" |
|||
import { themeStore } from "builderStore" |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
const THEMES = { |
|||
DARK: "tomorrow-night-eighties", |
|||
LIGHT: "default", |
|||
} |
|||
|
|||
export let value = "" |
|||
export let readOnly = false |
|||
export let lineNumbers = true |
|||
export let tab = true |
|||
export let mode |
|||
|
|||
let width |
|||
let height |
|||
|
|||
// We have to expose set and update methods, rather |
|||
// than making this state-driven through props, |
|||
// because it's difficult to update an editor |
|||
// without resetting scroll otherwise |
|||
export async function set(new_value, new_mode) { |
|||
if (new_mode !== mode) { |
|||
await createEditor((mode = new_mode)) |
|||
} |
|||
|
|||
value = new_value |
|||
updating_externally = true |
|||
if (editor) editor.setValue(value) |
|||
updating_externally = false |
|||
} |
|||
|
|||
export function update(new_value) { |
|||
value = new_value |
|||
|
|||
if (editor) { |
|||
const { left, top } = editor.getScrollInfo() |
|||
editor.setValue((value = new_value)) |
|||
editor.scrollTo(left, top) |
|||
} |
|||
} |
|||
|
|||
export function resize() { |
|||
editor.refresh() |
|||
} |
|||
|
|||
export function focus() { |
|||
editor.focus() |
|||
} |
|||
|
|||
const modes = { |
|||
js: { |
|||
name: "javascript", |
|||
json: false, |
|||
}, |
|||
json: { |
|||
name: "javascript", |
|||
json: true, |
|||
}, |
|||
sql: { |
|||
name: "sql", |
|||
}, |
|||
svelte: { |
|||
name: "handlebars", |
|||
base: "text/html", |
|||
}, |
|||
} |
|||
|
|||
const refs = {} |
|||
let editor |
|||
let updating_externally = false |
|||
let marker |
|||
let error_line |
|||
let destroyed = false |
|||
|
|||
$: if (editor && width && height) { |
|||
editor.refresh() |
|||
} |
|||
|
|||
onMount(() => { |
|||
createEditor(mode).then(() => { |
|||
if (editor) editor.setValue(value || "") |
|||
}) |
|||
|
|||
return () => { |
|||
destroyed = true |
|||
if (editor) editor.toTextArea() |
|||
} |
|||
}) |
|||
|
|||
let first = true |
|||
|
|||
async function createEditor(mode) { |
|||
if (destroyed || !CodeMirror) return |
|||
|
|||
if (editor) editor.toTextArea() |
|||
|
|||
const opts = { |
|||
lineNumbers, |
|||
lineWrapping: true, |
|||
indentWithTabs: true, |
|||
indentUnit: 2, |
|||
tabSize: 2, |
|||
value: "", |
|||
mode: modes[mode] || { |
|||
name: mode, |
|||
}, |
|||
readOnly, |
|||
autoCloseBrackets: true, |
|||
autoCloseTags: true, |
|||
theme: $themeStore.darkMode ? THEMES.DARK : THEMES.LIGHT, |
|||
} |
|||
|
|||
if (!tab) |
|||
opts.extraKeys = { |
|||
Tab: tab, |
|||
"Shift-Tab": tab, |
|||
} |
|||
|
|||
// Creating a text editor is a lot of work, so we yield |
|||
// the main thread for a moment. This helps reduce jank |
|||
if (first) await sleep(50) |
|||
|
|||
if (destroyed) return |
|||
|
|||
editor = CodeMirror.fromTextArea(refs.editor, opts) |
|||
|
|||
editor.on("change", instance => { |
|||
if (!updating_externally) { |
|||
const value = instance.getValue() |
|||
dispatch("change", { value }) |
|||
} |
|||
}) |
|||
|
|||
if (first) await sleep(50) |
|||
editor.refresh() |
|||
|
|||
first = false |
|||
} |
|||
|
|||
function sleep(ms) { |
|||
return new Promise(fulfil => setTimeout(fulfil, ms)) |
|||
} |
|||
</script> |
|||
|
|||
<textarea tabindex="0" bind:this={refs.editor} readonly {value} /> |
|||
|
|||
<style> |
|||
textarea { |
|||
visibility: hidden; |
|||
} |
|||
|
|||
:global(.CodeMirror) { |
|||
height: auto !important; |
|||
border-radius: var(--border-radius-m); |
|||
font-family: var(--font-sans) !important; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,149 @@ |
|||
const { |
|||
supertest, |
|||
createApplication, |
|||
defaultHeaders, |
|||
builderEndpointShouldBlockNormalUsers, |
|||
getDocument, |
|||
insertDocument |
|||
} = require("./couchTestUtils") |
|||
let { generateDatasourceID, generateQueryID } = require("../../../db/utils") |
|||
|
|||
const DATASOURCE_ID = generateDatasourceID() |
|||
const TEST_DATASOURCE = { |
|||
_id: DATASOURCE_ID, |
|||
type: "datasource", |
|||
name: "Test", |
|||
source: "POSTGRES", |
|||
config: {}, |
|||
type: "datasource", |
|||
} |
|||
|
|||
const TEST_QUERY = { |
|||
_id: generateQueryID(DATASOURCE_ID), |
|||
datasourceId: DATASOURCE_ID, |
|||
name:"New Query", |
|||
parameters:[], |
|||
fields:{}, |
|||
schema:{}, |
|||
queryVerb:"read", |
|||
queryType:"Table", |
|||
} |
|||
|
|||
describe("/datasources", () => { |
|||
let request |
|||
let server |
|||
let app |
|||
let appId |
|||
let datasource |
|||
|
|||
beforeAll(async () => { |
|||
({ request, server } = await supertest()) |
|||
}); |
|||
|
|||
afterAll(() => { |
|||
server.close() |
|||
}) |
|||
|
|||
beforeEach(async () => { |
|||
app = await createApplication(request) |
|||
appId = app.instance._id |
|||
}); |
|||
|
|||
async function createDatasource() { |
|||
return await insertDocument(appId, TEST_DATASOURCE) |
|||
} |
|||
|
|||
async function createQuery() { |
|||
return await insertDocument(appId, TEST_QUERY) |
|||
} |
|||
|
|||
describe("create", () => { |
|||
it("should create a new datasource", async () => { |
|||
const res = await request |
|||
.post(`/api/datasources`) |
|||
.send(TEST_DATASOURCE) |
|||
.set(defaultHeaders(appId)) |
|||
.expect('Content-Type', /json/) |
|||
.expect(200) |
|||
|
|||
expect(res.res.statusMessage).toEqual("Datasource saved successfully."); |
|||
expect(res.body.name).toEqual("Test"); |
|||
}) |
|||
}); |
|||
|
|||
describe("fetch", () => { |
|||
let datasource |
|||
|
|||
beforeEach(async () => { |
|||
datasource = await createDatasource() |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
delete datasource._rev |
|||
}); |
|||
|
|||
it("returns all the datasources from the server", async () => { |
|||
const res = await request |
|||
.get(`/api/datasources`) |
|||
.set(defaultHeaders(appId)) |
|||
.expect('Content-Type', /json/) |
|||
.expect(200) |
|||
|
|||
const datasources = res.body; |
|||
expect(datasources).toEqual([ |
|||
{ |
|||
"_rev": datasources[0]._rev, |
|||
...TEST_DATASOURCE |
|||
} |
|||
]); |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await builderEndpointShouldBlockNormalUsers({ |
|||
request, |
|||
method: "GET", |
|||
url: `/api/datasources`, |
|||
appId: appId, |
|||
}) |
|||
}) |
|||
}); |
|||
|
|||
describe("destroy", () => { |
|||
let datasource; |
|||
|
|||
beforeEach(async () => { |
|||
datasource = await createDatasource() |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
delete datasource._rev |
|||
}); |
|||
|
|||
it("deletes queries for the datasource after deletion and returns a success message", async () => { |
|||
await createQuery(datasource.id) |
|||
|
|||
await request |
|||
.delete(`/api/datasources/${datasource.id}/${datasource.rev}`) |
|||
.set(defaultHeaders(appId)) |
|||
.expect(200) |
|||
|
|||
const res = await request |
|||
.get(`/api/datasources`) |
|||
.set(defaultHeaders(appId)) |
|||
.expect('Content-Type', /json/) |
|||
.expect(200) |
|||
|
|||
expect(res.body).toEqual([]) |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await builderEndpointShouldBlockNormalUsers({ |
|||
request, |
|||
method: "DELETE", |
|||
url: `/api/datasources/${datasource._id}/${datasource._rev}`, |
|||
appId: appId, |
|||
}) |
|||
}) |
|||
|
|||
}); |
|||
}); |
|||
@ -0,0 +1,153 @@ |
|||
const { |
|||
supertest, |
|||
createApplication, |
|||
defaultHeaders, |
|||
builderEndpointShouldBlockNormalUsers, |
|||
getDocument, |
|||
insertDocument |
|||
} = require("./couchTestUtils") |
|||
let { generateDatasourceID, generateQueryID } = require("../../../db/utils") |
|||
|
|||
const DATASOURCE_ID = generateDatasourceID() |
|||
const TEST_DATASOURCE = { |
|||
_id: DATASOURCE_ID, |
|||
type: "datasource", |
|||
name: "Test", |
|||
source: "POSTGRES", |
|||
config: {}, |
|||
type: "datasource", |
|||
} |
|||
|
|||
const TEST_QUERY = { |
|||
_id: generateQueryID(DATASOURCE_ID), |
|||
datasourceId: DATASOURCE_ID, |
|||
name:"New Query", |
|||
parameters:[], |
|||
fields:{}, |
|||
schema:{}, |
|||
queryVerb:"read", |
|||
queryType:"Table", |
|||
} |
|||
|
|||
describe("/queries", () => { |
|||
let request |
|||
let server |
|||
let app |
|||
let appId |
|||
let datasource |
|||
let query |
|||
|
|||
beforeAll(async () => { |
|||
({ request, server } = await supertest()) |
|||
}); |
|||
|
|||
afterAll(() => { |
|||
server.close() |
|||
}) |
|||
|
|||
beforeEach(async () => { |
|||
app = await createApplication(request) |
|||
appId = app.instance._id |
|||
}); |
|||
|
|||
async function createDatasource() { |
|||
return await insertDocument(appId, TEST_DATASOURCE) |
|||
} |
|||
|
|||
async function createQuery() { |
|||
return await insertDocument(appId, TEST_QUERY) |
|||
} |
|||
|
|||
describe("create", () => { |
|||
it("should create a new query", async () => { |
|||
const res = await request |
|||
.post(`/api/queries`) |
|||
.send(TEST_QUERY) |
|||
.set(defaultHeaders(appId)) |
|||
.expect('Content-Type', /json/) |
|||
.expect(200) |
|||
|
|||
expect(res.res.statusMessage).toEqual(`Query ${TEST_QUERY.name} saved successfully.`); |
|||
expect(res.body).toEqual({ |
|||
_rev: res.body._rev, |
|||
...TEST_QUERY, |
|||
}); |
|||
}) |
|||
}); |
|||
|
|||
describe("fetch", () => { |
|||
let datasource |
|||
|
|||
beforeEach(async () => { |
|||
datasource = await createDatasource() |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
delete datasource._rev |
|||
}); |
|||
|
|||
it("returns all the queries from the server", async () => { |
|||
const query = await createQuery() |
|||
const res = await request |
|||
.get(`/api/queries`) |
|||
.set(defaultHeaders(appId)) |
|||
.expect('Content-Type', /json/) |
|||
.expect(200) |
|||
|
|||
const queries = res.body; |
|||
expect(queries).toEqual([ |
|||
{ |
|||
"_rev": query.rev, |
|||
...TEST_QUERY |
|||
} |
|||
]); |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await builderEndpointShouldBlockNormalUsers({ |
|||
request, |
|||
method: "GET", |
|||
url: `/api/datasources`, |
|||
appId: appId, |
|||
}) |
|||
}) |
|||
}); |
|||
|
|||
describe("destroy", () => { |
|||
let datasource; |
|||
|
|||
beforeEach(async () => { |
|||
datasource = await createDatasource() |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
delete datasource._rev |
|||
}); |
|||
|
|||
it("deletes a query and returns a success message", async () => { |
|||
const query = await createQuery() |
|||
|
|||
await request |
|||
.delete(`/api/queries/${query.id}/${query.rev}`) |
|||
.set(defaultHeaders(appId)) |
|||
.expect(200) |
|||
|
|||
const res = await request |
|||
.get(`/api/queries`) |
|||
.set(defaultHeaders(appId)) |
|||
.expect('Content-Type', /json/) |
|||
.expect(200) |
|||
|
|||
expect(res.body).toEqual([]) |
|||
}) |
|||
|
|||
it("should apply authorization to endpoint", async () => { |
|||
await builderEndpointShouldBlockNormalUsers({ |
|||
request, |
|||
method: "DELETE", |
|||
url: `/api/datasources/${datasource._id}/${datasource._rev}`, |
|||
appId: appId, |
|||
}) |
|||
}) |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue