forked from tsai/budibase
12 changed files with 502 additions and 247 deletions
@ -1,59 +1,162 @@ |
|||||
<script> |
<script> |
||||
import cm from "./codemirror" |
import CodeMirror from "./codemirror" |
||||
import { onMount, createEventDispatcher } from "svelte" |
import { onMount, createEventDispatcher } from "svelte" |
||||
|
import { themeStore } from "builderStore" |
||||
|
|
||||
const dispatch = createEventDispatcher() |
const dispatch = createEventDispatcher() |
||||
|
|
||||
export let value |
const THEMES = { |
||||
export let mode = "sql" |
DARK: "tomorrow-night-eighties", |
||||
|
LIGHT: "default", |
||||
|
} |
||||
|
|
||||
let editor |
export let value = "" |
||||
let codemirror |
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 |
||||
if (codemirror) { |
// than making this state-driven through props, |
||||
const { left, top } = codemirror.getScrollInfo() |
// because it's difficult to update an editor |
||||
codemirror.setValue(value) |
// without resetting scroll otherwise |
||||
codemirror.scrollTo(left, top) |
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(() => { |
onMount(() => { |
||||
if (codemirror) codemirror.toTextArea() |
createEditor(mode).then(() => { |
||||
|
if (editor) editor.setValue(value || "") |
||||
|
}) |
||||
|
|
||||
codemirror = cm.fromTextArea(editor, { |
return () => { |
||||
lineNumbers: true, |
destroyed = true |
||||
mode, |
if (editor) editor.toTextArea() |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
let first = true |
||||
|
|
||||
|
async function createEditor(mode) { |
||||
|
if (destroyed || !CodeMirror) return |
||||
|
|
||||
|
if (editor) editor.toTextArea() |
||||
|
|
||||
|
const opts = { |
||||
|
lineNumbers, |
||||
lineWrapping: true, |
lineWrapping: true, |
||||
|
indentWithTabs: true, |
||||
indentUnit: 2, |
indentUnit: 2, |
||||
tabSize: 2, |
tabSize: 2, |
||||
|
value: "", |
||||
|
mode: modes[mode] || { |
||||
|
name: mode, |
||||
|
}, |
||||
|
readOnly, |
||||
autoCloseBrackets: true, |
autoCloseBrackets: true, |
||||
autoCloseTags: true, |
autoCloseTags: true, |
||||
extraKeys: { |
theme: $themeStore.darkMode ? THEMES.DARK : THEMES.LIGHT, |
||||
"Ctrl-/": "toggleComment", |
} |
||||
}, |
|
||||
}) |
if (!tab) |
||||
codemirror.on("change", instance => { |
opts.extraKeys = { |
||||
const code = instance.getValue() |
Tab: tab, |
||||
dispatch("change", code) |
"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 () => { |
first = false |
||||
if (codemirror) codemirror.toTextArea() |
} |
||||
} |
|
||||
}) |
function sleep(ms) { |
||||
|
return new Promise(fulfil => setTimeout(fulfil, ms)) |
||||
|
} |
||||
</script> |
</script> |
||||
|
|
||||
<textarea bind:value bind:this={editor} /> |
<textarea tabindex="0" bind:this={refs.editor} readonly {value} /> |
||||
|
|
||||
<style> |
<style> |
||||
textarea { |
textarea { |
||||
background: var(--background); |
visibility: hidden; |
||||
border-radius: var(--border-radius-m); |
|
||||
} |
} |
||||
|
|
||||
:global(.CodeMirror) { |
:global(.CodeMirror) { |
||||
|
height: auto !important; |
||||
border-radius: var(--border-radius-m); |
border-radius: var(--border-radius-m); |
||||
|
font-family: var(--font-sans) !important; |
||||
} |
} |
||||
</style> |
</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