mirror of https://github.com/Budibase/budibase.git
93 changed files with 3691 additions and 1850 deletions
@ -1,26 +0,0 @@ |
|||
// Array.flat needs polyfilled in < Node 11
|
|||
if (!Array.prototype.flat) { |
|||
Object.defineProperty(Array.prototype, "flat", { |
|||
configurable: true, |
|||
value: function flat() { |
|||
var depth = isNaN(arguments[0]) ? 1 : Number(arguments[0]) |
|||
|
|||
return depth |
|||
? Array.prototype.reduce.call( |
|||
this, |
|||
function(acc, cur) { |
|||
if (Array.isArray(cur)) { |
|||
acc.push.apply(acc, flat.call(cur, depth - 1)) |
|||
} else { |
|||
acc.push(cur) |
|||
} |
|||
|
|||
return acc |
|||
}, |
|||
[] |
|||
) |
|||
: Array.prototype.slice.call(this) |
|||
}, |
|||
writable: true, |
|||
}) |
|||
} |
|||
@ -1,13 +1,13 @@ |
|||
<script> |
|||
import { Select } from "@budibase/bbui" |
|||
import { backendUiStore } from "builderStore" |
|||
import { roles } from 'stores/backend/' |
|||
|
|||
export let value |
|||
</script> |
|||
|
|||
<Select bind:value extraThin secondary on:change> |
|||
<option value="">Choose an option</option> |
|||
{#each $backendUiStore.roles as role} |
|||
{#each $roles as role} |
|||
<option value={role._id}>{role.name}</option> |
|||
{/each} |
|||
</Select> |
|||
|
|||
@ -1,23 +1,22 @@ |
|||
<script> |
|||
import { backendUiStore } from "builderStore" |
|||
import { goto } from "@sveltech/routify" |
|||
import { onMount } from "svelte" |
|||
import { datasources, tables } from 'stores/backend/' |
|||
import { goto } from "@sveltech/routify" |
|||
import { onMount } from "svelte" |
|||
|
|||
onMount(async () => { |
|||
// navigate to first table in list, if not already selected |
|||
$backendUiStore.datasources.length > 0 && |
|||
$goto(`../${$backendUiStore.datasources[0]._id}`) |
|||
}) |
|||
</script> |
|||
|
|||
{#if $backendUiStore.tables.length === 0} |
|||
<i>Connect your first datasource to start building.</i> |
|||
{:else}<i>Select a datasource to edit</i>{/if} |
|||
|
|||
<style> |
|||
i { |
|||
font-size: var(--font-size-m); |
|||
color: var(--grey-5); |
|||
margin-top: 2px; |
|||
} |
|||
</style> |
|||
onMount(async () => { |
|||
// navigate to first table in list, if not already selected |
|||
$datasources.list.length > 0 && $goto(`../${$datasources.list[0]._id}`) |
|||
}) |
|||
</script> |
|||
|
|||
{#if $tables.list.length === 0} |
|||
<i>Connect your first datasource to start building.</i> |
|||
{:else}<i>Select a datasource to edit</i>{/if} |
|||
|
|||
<style> |
|||
i { |
|||
font-size: var(--font-size-m); |
|||
color: var(--grey-5); |
|||
margin-top: 2px; |
|||
} |
|||
</style> |
|||
|
|||
@ -0,0 +1,3 @@ |
|||
import { writable } from "svelte/store" |
|||
|
|||
export const database = writable({}) |
|||
@ -0,0 +1,66 @@ |
|||
import { writable } from "svelte/store" |
|||
import { queries } from "./" |
|||
import api from "../../builderStore/api" |
|||
|
|||
export const INITIAL_DATASOURCE_VALUES = { |
|||
list: [], |
|||
selected: null, |
|||
} |
|||
|
|||
export function createDatasourcesStore() { |
|||
const { subscribe, update, set } = writable(INITIAL_DATASOURCE_VALUES) |
|||
|
|||
return { |
|||
subscribe, |
|||
update, |
|||
init: async () => { |
|||
const response = await api.get(`/api/datasources`) |
|||
const json = await response.json() |
|||
set({ list: json, selected: null }) |
|||
}, |
|||
fetch: async () => { |
|||
const response = await api.get(`/api/datasources`) |
|||
const json = await response.json() |
|||
update(state => ({ ...state, list: json })) |
|||
return json |
|||
}, |
|||
select: async datasourceId => { |
|||
update(state => ({ ...state, selected: datasourceId })) |
|||
queries.update(state => ({ ...state, selected: null })) |
|||
}, |
|||
save: async datasource => { |
|||
const response = await api.post("/api/datasources", datasource) |
|||
const json = await response.json() |
|||
|
|||
update(state => { |
|||
const currentIdx = state.list.findIndex(ds => ds._id === json._id) |
|||
|
|||
const sources = state.list |
|||
|
|||
if (currentIdx >= 0) { |
|||
sources.splice(currentIdx, 1, json) |
|||
} else { |
|||
sources.push(json) |
|||
} |
|||
|
|||
return { list: sources, selected: json._id } |
|||
}) |
|||
return json |
|||
}, |
|||
delete: async datasource => { |
|||
const response = await api.delete( |
|||
`/api/datasources/${datasource._id}/${datasource._rev}` |
|||
) |
|||
update(state => { |
|||
const sources = state.list.filter( |
|||
existing => existing._id !== datasource._id |
|||
) |
|||
return { list: sources, selected: null } |
|||
}) |
|||
|
|||
return response |
|||
}, |
|||
} |
|||
} |
|||
|
|||
export const datasources = createDatasourcesStore() |
|||
@ -0,0 +1,9 @@ |
|||
export { database } from "./database" |
|||
export { tables } from "./tables" |
|||
export { views } from "./views" |
|||
export { rows } from "./rows" |
|||
export { permissions } from "./permissions" |
|||
export { roles } from "./roles" |
|||
export { datasources } from "./datasources" |
|||
export { integrations } from "./integrations" |
|||
export { queries } from "./queries" |
|||
@ -0,0 +1,3 @@ |
|||
import { writable } from "svelte/store" |
|||
|
|||
export const integrations = writable({}) |
|||
@ -0,0 +1,17 @@ |
|||
import { writable } from "svelte/store" |
|||
import api from "builderStore/api" |
|||
|
|||
export function createPermissionStore() { |
|||
const { subscribe } = writable([]) |
|||
|
|||
return { |
|||
subscribe, |
|||
forResource: async resourceId => { |
|||
const response = await api.get(`/api/permission/${resourceId}`) |
|||
const json = await response.json() |
|||
return json |
|||
}, |
|||
} |
|||
} |
|||
|
|||
export const permissions = createPermissionStore() |
|||
@ -0,0 +1,77 @@ |
|||
import { writable, get } from "svelte/store" |
|||
import { datasources, integrations } from "./" |
|||
import api from "builderStore/api" |
|||
|
|||
export function createQueriesStore() { |
|||
const { subscribe, set, update } = writable({ list: [], selected: null }) |
|||
|
|||
return { |
|||
subscribe, |
|||
set, |
|||
update, |
|||
init: async () => { |
|||
const response = await api.get(`/api/queries`) |
|||
const json = await response.json() |
|||
set({ list: json, selected: null }) |
|||
}, |
|||
fetch: async () => { |
|||
const response = await api.get(`/api/queries`) |
|||
const json = await response.json() |
|||
update(state => ({ ...state, list: json })) |
|||
return json |
|||
}, |
|||
save: async (datasourceId, query) => { |
|||
const _integrations = get(integrations) |
|||
const dataSource = get(datasources).list.filter( |
|||
ds => ds._id === datasourceId |
|||
) |
|||
// check if readable attribute is found
|
|||
if (dataSource.length !== 0) { |
|||
const integration = _integrations[dataSource[0].source] |
|||
const readable = integration.query[query.queryVerb].readable |
|||
if (readable) { |
|||
query.readable = readable |
|||
} |
|||
} |
|||
query.datasourceId = datasourceId |
|||
const response = await api.post(`/api/queries`, query) |
|||
if (response.status !== 200) { |
|||
throw new Error("Failed saving query.") |
|||
} |
|||
const json = await response.json() |
|||
update(state => { |
|||
const currentIdx = state.list.findIndex(query => query._id === json._id) |
|||
|
|||
const queries = state.list |
|||
|
|||
if (currentIdx >= 0) { |
|||
queries.splice(currentIdx, 1, json) |
|||
} else { |
|||
queries.push(json) |
|||
} |
|||
return { list: queries, selected: json._id } |
|||
}) |
|||
return json |
|||
}, |
|||
select: query => { |
|||
update(state => ({ ...state, selected: query._id })) |
|||
datasources.update(state => ({ ...state, selected: query.datasourceId })) |
|||
}, |
|||
delete: async query => { |
|||
const response = await api.delete( |
|||
`/api/queries/${query._id}/${query._rev}` |
|||
) |
|||
update(state => { |
|||
state.list = state.list.filter(existing => existing._id !== query._id) |
|||
if (state.selected === query._id) { |
|||
state.selected = null |
|||
} |
|||
|
|||
return state |
|||
}) |
|||
return response |
|||
}, |
|||
} |
|||
} |
|||
|
|||
export const queries = createQueriesStore() |
|||
@ -0,0 +1,30 @@ |
|||
import { writable } from "svelte/store" |
|||
import api from "builderStore/api" |
|||
|
|||
export function createRolesStore() { |
|||
const { subscribe, update, set } = writable([]) |
|||
|
|||
return { |
|||
subscribe, |
|||
fetch: async () => { |
|||
set(await getRoles()) |
|||
}, |
|||
delete: async role => { |
|||
const response = await api.delete(`/api/roles/${role._id}/${role._rev}`) |
|||
update(state => state.filter(existing => existing._id !== role._id)) |
|||
return response |
|||
}, |
|||
save: async role => { |
|||
const response = await api.post("/api/roles", role) |
|||
set(await getRoles()) |
|||
return response |
|||
}, |
|||
} |
|||
} |
|||
|
|||
async function getRoles() { |
|||
const response = await api.get("/api/roles") |
|||
return await response.json() |
|||
} |
|||
|
|||
export const roles = createRolesStore() |
|||
@ -0,0 +1,14 @@ |
|||
import { writable, get } from "svelte/store" |
|||
import { views } from "./" |
|||
|
|||
export function createRowsStore() { |
|||
const { subscribe } = writable([]) |
|||
|
|||
return { |
|||
subscribe, |
|||
save: () => views.select(get(views).selected), |
|||
delete: () => views.select(get(views).selected), |
|||
} |
|||
} |
|||
|
|||
export const rows = createRowsStore() |
|||
@ -0,0 +1,128 @@ |
|||
import { writable, get } from "svelte/store" |
|||
import { views } from "./" |
|||
import { cloneDeep } from "lodash/fp" |
|||
import api from "builderStore/api" |
|||
|
|||
export function createTablesStore() { |
|||
const store = writable({}) |
|||
const { subscribe, update, set } = store |
|||
|
|||
async function fetch() { |
|||
const tablesResponse = await api.get(`/api/tables`) |
|||
const tables = await tablesResponse.json() |
|||
update(state => ({ ...state, list: tables })) |
|||
} |
|||
|
|||
async function select(table) { |
|||
if (!table) { |
|||
update(state => ({ |
|||
...state, |
|||
selected: {}, |
|||
})) |
|||
} else { |
|||
update(state => ({ |
|||
...state, |
|||
selected: table, |
|||
draft: cloneDeep(table), |
|||
})) |
|||
views.select({ name: `all_${table._id}` }) |
|||
} |
|||
} |
|||
|
|||
async function save(table) { |
|||
const updatedTable = cloneDeep(table) |
|||
const oldTable = get(store).list.filter(t => t._id === table._id)[0] |
|||
|
|||
const fieldNames = [] |
|||
// update any renamed schema keys to reflect their names
|
|||
for (let key of Object.keys(updatedTable.schema)) { |
|||
// if field name has been seen before remove it
|
|||
if (fieldNames.indexOf(key.toLowerCase()) !== -1) { |
|||
delete updatedTable.schema[key] |
|||
continue |
|||
} |
|||
const field = updatedTable.schema[key] |
|||
const oldField = oldTable?.schema[key] |
|||
// if the type has changed then revert back to the old field
|
|||
if (oldField != null && oldField?.type !== field.type) { |
|||
updatedTable.schema[key] = oldField |
|||
} |
|||
// field has been renamed
|
|||
if (field.name && field.name !== key) { |
|||
updatedTable.schema[field.name] = field |
|||
updatedTable._rename = { old: key, updated: field.name } |
|||
delete updatedTable.schema[key] |
|||
} |
|||
// finally record this field has been used
|
|||
fieldNames.push(key.toLowerCase()) |
|||
} |
|||
|
|||
const response = await api.post(`/api/tables`, updatedTable) |
|||
const savedTable = await response.json() |
|||
await fetch() |
|||
await select(savedTable) |
|||
return savedTable |
|||
} |
|||
|
|||
return { |
|||
subscribe, |
|||
fetch, |
|||
select, |
|||
save, |
|||
init: async () => { |
|||
const response = await api.get("/api/tables") |
|||
const json = await response.json() |
|||
set({ |
|||
list: json, |
|||
selected: {}, |
|||
draft: {}, |
|||
}) |
|||
}, |
|||
delete: async table => { |
|||
await api.delete(`/api/tables/${table._id}/${table._rev}`) |
|||
update(state => ({ |
|||
...state, |
|||
list: state.list.filter(existing => existing._id !== table._id), |
|||
selected: {}, |
|||
})) |
|||
}, |
|||
saveField: ({ originalName, field, primaryDisplay = false, indexes }) => { |
|||
update(state => { |
|||
// delete the original if renaming
|
|||
// need to handle if the column had no name, empty string
|
|||
if (originalName || originalName === "") { |
|||
delete state.draft.schema[originalName] |
|||
state.draft._rename = { |
|||
old: originalName, |
|||
updated: field.name, |
|||
} |
|||
} |
|||
|
|||
// Optionally set display column
|
|||
if (primaryDisplay) { |
|||
state.draft.primaryDisplay = field.name |
|||
} |
|||
|
|||
if (indexes) { |
|||
state.draft.indexes = indexes |
|||
} |
|||
|
|||
state.draft.schema = { |
|||
...state.draft.schema, |
|||
[field.name]: cloneDeep(field), |
|||
} |
|||
save(state.draft) |
|||
return state |
|||
}) |
|||
}, |
|||
deleteField: field => { |
|||
update(state => { |
|||
delete state.draft.schema[field.name] |
|||
save(state.draft) |
|||
return state |
|||
}) |
|||
}, |
|||
} |
|||
} |
|||
|
|||
export const tables = createTablesStore() |
|||
@ -0,0 +1,68 @@ |
|||
import { get } from 'svelte/store' |
|||
import api from 'builderStore/api' |
|||
|
|||
jest.mock('builderStore/api'); |
|||
|
|||
import { SOME_DATASOURCE, SAVE_DATASOURCE} from './fixtures/datasources' |
|||
|
|||
import { createDatasourcesStore } from "../datasources" |
|||
import { queries } from '../queries' |
|||
|
|||
describe("Datasources Store", () => { |
|||
let store = createDatasourcesStore() |
|||
|
|||
beforeEach(async () => { |
|||
api.get.mockReturnValue({ json: () => [SOME_DATASOURCE]}) |
|||
await store.init() |
|||
}) |
|||
|
|||
it("Initialises correctly", async () => { |
|||
api.get.mockReturnValue({ json: () => [SOME_DATASOURCE]}) |
|||
|
|||
await store.init() |
|||
expect(get(store)).toEqual({ list: [SOME_DATASOURCE], selected: null}) |
|||
}) |
|||
|
|||
it("fetches all the datasources and updates the store", async () => { |
|||
api.get.mockReturnValue({ json: () => [SOME_DATASOURCE]}) |
|||
|
|||
await store.fetch() |
|||
expect(get(store)).toEqual({ list: [SOME_DATASOURCE], selected: null}) |
|||
}) |
|||
|
|||
it("selects a datasource", async () => { |
|||
store.select(SOME_DATASOURCE._id) |
|||
|
|||
expect(get(store).select).toEqual(SOME_DATASOURCE._id) |
|||
}) |
|||
|
|||
it("resets the queries store when new datasource is selected", async () => { |
|||
|
|||
await store.select(SOME_DATASOURCE._id) |
|||
const queriesValue = get(queries) |
|||
expect(queriesValue.selected).toEqual(null) |
|||
}) |
|||
|
|||
it("saves the datasource, updates the store and returns status message", async () => { |
|||
api.post.mockReturnValue({ json: () => SAVE_DATASOURCE}) |
|||
|
|||
await store.save({ |
|||
name: 'CoolDB', |
|||
source: 'REST', |
|||
config: SOME_DATASOURCE[0].config |
|||
|
|||
}) |
|||
|
|||
expect(get(store).list).toEqual(expect.arrayContaining([SAVE_DATASOURCE])) |
|||
}) |
|||
it("deletes a datasource, updates the store and returns status message", async () => { |
|||
api.get.mockReturnValue({ json: () => SOME_DATASOURCE}) |
|||
|
|||
await store.fetch() |
|||
|
|||
api.delete.mockReturnValue({status: 200, message: 'Datasource deleted.'}) |
|||
|
|||
await store.delete(SOME_DATASOURCE[0]) |
|||
expect(get(store)).toEqual({ list: [], selected: null}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,25 @@ |
|||
export const SOME_DATASOURCE = [ |
|||
{ |
|||
type: "datasource", |
|||
name: "erterter", |
|||
source: "REST", |
|||
config: { |
|||
url: "localhost", |
|||
defaultHeaders: {}, |
|||
}, |
|||
_id: "datasource_04b003a7b4a8428eadd3bb2f7eae0255", |
|||
_rev: "1-4e72002f1011e9392e655948469b7908", |
|||
}, |
|||
] |
|||
|
|||
export const SAVE_DATASOURCE = { |
|||
type: "datasource", |
|||
name: "CoolDB", |
|||
source: "REST", |
|||
config: { |
|||
url: "localhost", |
|||
defaultHeaders: {}, |
|||
}, |
|||
_id: "datasource_04b003a7b4a8428eadd3bb2f7eae0255", |
|||
_rev: "1-4e72002f1011e9392e655948469b7908", |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
export const SOME_QUERY = { |
|||
datasourceId: "datasource_04b003a7b4a8428eadd3bb2f7eae0255", |
|||
parameters: [], |
|||
fields: { |
|||
headers: {}, |
|||
queryString: "", |
|||
path: "Speakers", |
|||
}, |
|||
queryVerb: "read", |
|||
schema: {}, |
|||
name: "Speakers", |
|||
_id: |
|||
"query_datasource_04b003a7b4a8428eadd3bb2f7eae0255_bcb8ffc6fcbc484e8d63121fc0bf986f", |
|||
_rev: "2-941f8699eb0adf995f8bd59c99203b26", |
|||
readable: true, |
|||
} |
|||
|
|||
export const SAVE_QUERY_RESPONSE = { |
|||
datasourceId: "datasource_04b003a7b4a8428eadd3bb2f7eae0255", |
|||
parameters: [], |
|||
fields: { |
|||
headers: {}, |
|||
queryString: "", |
|||
path: "Speakers", |
|||
}, |
|||
queryVerb: "read", |
|||
schema: { |
|||
id: { |
|||
name: "id", |
|||
type: "string", |
|||
}, |
|||
firstName: { |
|||
name: "firstName", |
|||
type: "string", |
|||
}, |
|||
lastName: { |
|||
name: "lastName", |
|||
type: "string", |
|||
}, |
|||
fullName: { |
|||
name: "fullName", |
|||
type: "string", |
|||
}, |
|||
bio: { |
|||
name: "bio", |
|||
type: "string", |
|||
}, |
|||
tagLine: { |
|||
name: "tagLine", |
|||
type: "string", |
|||
}, |
|||
profilePicture: { |
|||
name: "profilePicture", |
|||
type: "string", |
|||
}, |
|||
sessions: { |
|||
name: "sessions", |
|||
type: "string", |
|||
}, |
|||
isTopSpeaker: { |
|||
name: "isTopSpeaker", |
|||
type: "string", |
|||
}, |
|||
links: { |
|||
name: "links", |
|||
type: "string", |
|||
}, |
|||
questionAnswers: { |
|||
name: "questionAnswers", |
|||
type: "string", |
|||
}, |
|||
categories: { |
|||
name: "categories", |
|||
type: "string", |
|||
}, |
|||
}, |
|||
name: "Speakers", |
|||
_id: |
|||
"query_datasource_04b003a7b4a8428eadd3bb2f7eae0255_bcb8ffc6fcbc484e8d63121fc0bf986f", |
|||
_rev: "3-5a64adef494b1e9c793dc91b51ce73c6", |
|||
readable: true, |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
export const ROLES = [ |
|||
{ |
|||
name: "Test", |
|||
permissionId: "admin", |
|||
inherits: "ADMIN", |
|||
_id: "role_04681b7e71914a0aa53e09a5bea3584f", |
|||
_rev: "1-179c71ea61d7fd987306b84b6d64b00e", |
|||
}, |
|||
{ |
|||
_id: "ADMIN", |
|||
name: "Admin", |
|||
permissionId: "admin", |
|||
inherits: "POWER", |
|||
}, |
|||
{ |
|||
_id: "POWER", |
|||
name: "Power", |
|||
permissionId: "power", |
|||
inherits: "BASIC", |
|||
}, |
|||
{ |
|||
_id: "BASIC", |
|||
name: "Basic", |
|||
permissionId: "write", |
|||
inherits: "PUBLIC", |
|||
}, |
|||
{ |
|||
_id: "PUBLIC", |
|||
name: "Public", |
|||
permissionId: "public", |
|||
}, |
|||
] |
|||
@ -0,0 +1,717 @@ |
|||
export const SOME_TABLES = [ |
|||
{ |
|||
type: "table", |
|||
views: {}, |
|||
name: "Guest", |
|||
schema: { |
|||
"Auto ID": { |
|||
name: "Auto ID", |
|||
type: "number", |
|||
subtype: "autoID", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "number", |
|||
presence: false, |
|||
numericality: { |
|||
greaterThanOrEqualTo: "", |
|||
lessThanOrEqualTo: "", |
|||
}, |
|||
}, |
|||
lastID: 1, |
|||
}, |
|||
"Created By": { |
|||
name: "Created By", |
|||
type: "link", |
|||
subtype: "createdBy", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "array", |
|||
presence: false, |
|||
}, |
|||
tableId: "ta_users", |
|||
fieldName: "Guest-Created By", |
|||
relationshipType: "many-to-many", |
|||
}, |
|||
"Created At": { |
|||
name: "Created At", |
|||
type: "datetime", |
|||
subtype: "createdAt", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "string", |
|||
length: {}, |
|||
presence: false, |
|||
datetime: { |
|||
latest: "", |
|||
earliest: "", |
|||
}, |
|||
}, |
|||
}, |
|||
"Updated By": { |
|||
name: "Updated By", |
|||
type: "link", |
|||
subtype: "updatedBy", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "array", |
|||
presence: false, |
|||
}, |
|||
tableId: "ta_users", |
|||
fieldName: "Guest-Updated By", |
|||
relationshipType: "many-to-many", |
|||
}, |
|||
"Updated At": { |
|||
name: "Updated At", |
|||
type: "datetime", |
|||
subtype: "updatedAt", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "string", |
|||
length: {}, |
|||
presence: false, |
|||
datetime: { |
|||
latest: "", |
|||
earliest: "", |
|||
}, |
|||
}, |
|||
}, |
|||
Name: { |
|||
type: "string", |
|||
constraints: { |
|||
type: "string", |
|||
length: { |
|||
maximum: "", |
|||
}, |
|||
presence: false, |
|||
}, |
|||
fieldName: "Guest", |
|||
name: "Name", |
|||
}, |
|||
Episode: { |
|||
name: "Episode", |
|||
type: "link", |
|||
tableId: "ta_d4bf541ce0d84b16a1a8e0a060e5f7f7", |
|||
fieldName: "Guest", |
|||
relationshipType: "one-to-many", |
|||
}, |
|||
}, |
|||
primaryDisplay: "Name", |
|||
indexes: [], |
|||
_id: "ta_3c78cffe33664ca9bfb6b2b6cb3ee55a", |
|||
_rev: "10-27f034bf50ec3e2f180d8f96db1f0f31", |
|||
}, |
|||
{ |
|||
type: "table", |
|||
views: {}, |
|||
name: "Sponsors", |
|||
schema: { |
|||
"Auto ID": { |
|||
name: "Auto ID", |
|||
type: "number", |
|||
subtype: "autoID", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "number", |
|||
presence: false, |
|||
numericality: { |
|||
greaterThanOrEqualTo: "", |
|||
lessThanOrEqualTo: "", |
|||
}, |
|||
}, |
|||
lastID: 1, |
|||
}, |
|||
"Created By": { |
|||
name: "Created By", |
|||
type: "link", |
|||
subtype: "createdBy", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "array", |
|||
presence: false, |
|||
}, |
|||
tableId: "ta_users", |
|||
fieldName: "Sponsors-Created By", |
|||
relationshipType: "many-to-many", |
|||
}, |
|||
"Created At": { |
|||
name: "Created At", |
|||
type: "datetime", |
|||
subtype: "createdAt", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "string", |
|||
length: {}, |
|||
presence: false, |
|||
datetime: { |
|||
latest: "", |
|||
earliest: "", |
|||
}, |
|||
}, |
|||
}, |
|||
"Updated By": { |
|||
name: "Updated By", |
|||
type: "link", |
|||
subtype: "updatedBy", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "array", |
|||
presence: false, |
|||
}, |
|||
tableId: "ta_users", |
|||
fieldName: "Sponsors-Updated By", |
|||
relationshipType: "many-to-many", |
|||
}, |
|||
"Updated At": { |
|||
name: "Updated At", |
|||
type: "datetime", |
|||
subtype: "updatedAt", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "string", |
|||
length: {}, |
|||
presence: false, |
|||
datetime: { |
|||
latest: "", |
|||
earliest: "", |
|||
}, |
|||
}, |
|||
}, |
|||
Name: { |
|||
type: "string", |
|||
constraints: { |
|||
type: "string", |
|||
length: { |
|||
maximum: "", |
|||
}, |
|||
presence: false, |
|||
}, |
|||
fieldName: "Sponsors", |
|||
name: "Name", |
|||
}, |
|||
Spot: { |
|||
type: "longform", |
|||
constraints: { |
|||
type: "string", |
|||
length: {}, |
|||
presence: false, |
|||
}, |
|||
fieldName: "Sponsors", |
|||
name: "Spot", |
|||
}, |
|||
Episode: { |
|||
name: "Episode", |
|||
type: "link", |
|||
tableId: "ta_d4bf541ce0d84b16a1a8e0a060e5f7f7", |
|||
fieldName: "Sponsors", |
|||
relationshipType: "many-to-many", |
|||
}, |
|||
}, |
|||
primaryDisplay: "Name", |
|||
indexes: [], |
|||
_id: "ta_7fd0fa15edd54e0f91a47f50b7577281", |
|||
_rev: "7-de89b81e21ae4b3f65a6b655144fe097", |
|||
}, |
|||
{ |
|||
type: "table", |
|||
views: {}, |
|||
name: "Episode", |
|||
schema: { |
|||
"Auto ID": { |
|||
name: "Auto ID", |
|||
type: "number", |
|||
subtype: "autoID", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "number", |
|||
presence: false, |
|||
numericality: { |
|||
greaterThanOrEqualTo: "", |
|||
lessThanOrEqualTo: "", |
|||
}, |
|||
}, |
|||
lastID: 1, |
|||
}, |
|||
"Created By": { |
|||
name: "Created By", |
|||
type: "link", |
|||
subtype: "createdBy", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "array", |
|||
presence: false, |
|||
}, |
|||
tableId: "ta_users", |
|||
fieldName: "Episode-Created By", |
|||
relationshipType: "many-to-many", |
|||
}, |
|||
"Created At": { |
|||
name: "Created At", |
|||
type: "datetime", |
|||
subtype: "createdAt", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "string", |
|||
length: {}, |
|||
presence: false, |
|||
datetime: { |
|||
latest: "", |
|||
earliest: "", |
|||
}, |
|||
}, |
|||
}, |
|||
"Updated By": { |
|||
name: "Updated By", |
|||
type: "link", |
|||
subtype: "updatedBy", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "array", |
|||
presence: false, |
|||
}, |
|||
tableId: "ta_users", |
|||
fieldName: "Episode-Updated By", |
|||
relationshipType: "many-to-many", |
|||
}, |
|||
"Updated At": { |
|||
name: "Updated At", |
|||
type: "datetime", |
|||
subtype: "updatedAt", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "string", |
|||
length: {}, |
|||
presence: false, |
|||
datetime: { |
|||
latest: "", |
|||
earliest: "", |
|||
}, |
|||
}, |
|||
}, |
|||
Summary: { |
|||
type: "longform", |
|||
constraints: { |
|||
type: "string", |
|||
length: {}, |
|||
presence: false, |
|||
}, |
|||
fieldName: "Episode", |
|||
name: "Summary", |
|||
}, |
|||
Author: { |
|||
type: "string", |
|||
constraints: { |
|||
type: "string", |
|||
length: { |
|||
maximum: "", |
|||
}, |
|||
presence: false, |
|||
}, |
|||
fieldName: "Episode", |
|||
name: "Author", |
|||
}, |
|||
Published: { |
|||
type: "boolean", |
|||
constraints: { |
|||
type: "boolean", |
|||
presence: false, |
|||
}, |
|||
fieldName: "Episode", |
|||
name: "Published", |
|||
}, |
|||
Guest: { |
|||
type: "link", |
|||
constraints: { |
|||
type: "array", |
|||
presence: false, |
|||
}, |
|||
fieldName: "Episode", |
|||
name: "Guest", |
|||
relationshipType: "many-to-one", |
|||
tableId: "ta_3c78cffe33664ca9bfb6b2b6cb3ee55a", |
|||
}, |
|||
Title: { |
|||
type: "string", |
|||
constraints: { |
|||
type: "string", |
|||
length: { |
|||
maximum: "", |
|||
}, |
|||
presence: false, |
|||
}, |
|||
fieldName: "Episode", |
|||
name: "Title", |
|||
}, |
|||
"Show Notes": { |
|||
type: "longform", |
|||
constraints: { |
|||
type: "string", |
|||
length: {}, |
|||
presence: false, |
|||
}, |
|||
fieldName: "Episode", |
|||
name: "Show Notes", |
|||
}, |
|||
Sponsors: { |
|||
type: "link", |
|||
constraints: { |
|||
type: "array", |
|||
presence: false, |
|||
}, |
|||
fieldName: "Episode", |
|||
name: "Sponsors", |
|||
relationshipType: "many-to-many", |
|||
tableId: "ta_7fd0fa15edd54e0f91a47f50b7577281", |
|||
}, |
|||
Number: { |
|||
type: "number", |
|||
constraints: { |
|||
type: "number", |
|||
presence: false, |
|||
numericality: { |
|||
greaterThanOrEqualTo: "", |
|||
lessThanOrEqualTo: "", |
|||
}, |
|||
}, |
|||
fieldName: "Episode", |
|||
name: "Number", |
|||
}, |
|||
Audio: { |
|||
type: "attachment", |
|||
constraints: { |
|||
type: "array", |
|||
presence: false, |
|||
}, |
|||
fieldName: "Episode", |
|||
name: "Audio", |
|||
}, |
|||
}, |
|||
indexes: [], |
|||
primaryDisplay: "Author", |
|||
_id: "ta_d4bf541ce0d84b16a1a8e0a060e5f7f7", |
|||
_rev: "13-9d70dee825154a9df5c22e1d39bf269c", |
|||
}, |
|||
{ |
|||
type: "table", |
|||
views: {}, |
|||
name: "Users", |
|||
schema: { |
|||
email: { |
|||
type: "string", |
|||
constraints: { |
|||
type: "string", |
|||
email: true, |
|||
length: { |
|||
maximum: "", |
|||
}, |
|||
presence: true, |
|||
}, |
|||
fieldName: "email", |
|||
name: "email", |
|||
}, |
|||
roleId: { |
|||
fieldName: "roleId", |
|||
name: "roleId", |
|||
type: "options", |
|||
constraints: { |
|||
type: "string", |
|||
presence: false, |
|||
inclusion: ["ADMIN", "POWER", "BASIC", "PUBLIC", "BUILDER"], |
|||
}, |
|||
}, |
|||
status: { |
|||
fieldName: "status", |
|||
name: "status", |
|||
type: "options", |
|||
constraints: { |
|||
type: "string", |
|||
presence: false, |
|||
inclusion: ["active", "inactive"], |
|||
}, |
|||
}, |
|||
"Episode-Created By": { |
|||
name: "Episode-Created By", |
|||
type: "link", |
|||
tableId: "ta_d4bf541ce0d84b16a1a8e0a060e5f7f7", |
|||
fieldName: "Created By", |
|||
relationshipType: "many-to-many", |
|||
autocolumn: true, |
|||
}, |
|||
"Episode-Updated By": { |
|||
name: "Episode-Updated By", |
|||
type: "link", |
|||
tableId: "ta_d4bf541ce0d84b16a1a8e0a060e5f7f7", |
|||
fieldName: "Updated By", |
|||
relationshipType: "many-to-many", |
|||
autocolumn: true, |
|||
}, |
|||
"Guest-Created By": { |
|||
name: "Guest-Created By", |
|||
type: "link", |
|||
tableId: "ta_3c78cffe33664ca9bfb6b2b6cb3ee55a", |
|||
fieldName: "Created By", |
|||
relationshipType: "many-to-many", |
|||
autocolumn: true, |
|||
}, |
|||
"Guest-Updated By": { |
|||
name: "Guest-Updated By", |
|||
type: "link", |
|||
tableId: "ta_3c78cffe33664ca9bfb6b2b6cb3ee55a", |
|||
fieldName: "Updated By", |
|||
relationshipType: "many-to-many", |
|||
autocolumn: true, |
|||
}, |
|||
"Sponsors-Created By": { |
|||
name: "Sponsors-Created By", |
|||
type: "link", |
|||
tableId: "ta_7fd0fa15edd54e0f91a47f50b7577281", |
|||
fieldName: "Created By", |
|||
relationshipType: "many-to-many", |
|||
autocolumn: true, |
|||
}, |
|||
"Sponsors-Updated By": { |
|||
name: "Sponsors-Updated By", |
|||
type: "link", |
|||
tableId: "ta_7fd0fa15edd54e0f91a47f50b7577281", |
|||
fieldName: "Updated By", |
|||
relationshipType: "many-to-many", |
|||
autocolumn: true, |
|||
}, |
|||
}, |
|||
primaryDisplay: "email", |
|||
_id: "ta_users", |
|||
_rev: "67-1833e6a0028c100633e31788fe958a62", |
|||
}, |
|||
] |
|||
|
|||
export const SAVE_TABLES_RESPONSE = { |
|||
type: "table", |
|||
_id: "ta_3c78cffe33664ca9bfb6b2b6cb3ee55a", |
|||
views: {}, |
|||
name: "Guest", |
|||
schema: { |
|||
"Auto ID": { |
|||
name: "Auto ID", |
|||
type: "number", |
|||
subtype: "autoID", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "number", |
|||
presence: false, |
|||
numericality: { |
|||
greaterThanOrEqualTo: "", |
|||
lessThanOrEqualTo: "", |
|||
}, |
|||
}, |
|||
lastID: 1, |
|||
}, |
|||
"Created By": { |
|||
name: "Created By", |
|||
type: "link", |
|||
subtype: "createdBy", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "array", |
|||
presence: false, |
|||
}, |
|||
tableId: "ta_users", |
|||
fieldName: "Guest-Created By", |
|||
relationshipType: "many-to-many", |
|||
}, |
|||
"Created At": { |
|||
name: "Created At", |
|||
type: "datetime", |
|||
subtype: "createdAt", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "string", |
|||
length: {}, |
|||
presence: false, |
|||
datetime: { |
|||
latest: "", |
|||
earliest: "", |
|||
}, |
|||
}, |
|||
}, |
|||
"Updated By": { |
|||
name: "Updated By", |
|||
type: "link", |
|||
subtype: "updatedBy", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "array", |
|||
presence: false, |
|||
}, |
|||
tableId: "ta_users", |
|||
fieldName: "Guest-Updated By", |
|||
relationshipType: "many-to-many", |
|||
}, |
|||
"Updated At": { |
|||
name: "Updated At", |
|||
type: "datetime", |
|||
subtype: "updatedAt", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "string", |
|||
length: {}, |
|||
presence: false, |
|||
datetime: { |
|||
latest: "", |
|||
earliest: "", |
|||
}, |
|||
}, |
|||
}, |
|||
Episode: { |
|||
name: "Episode", |
|||
type: "link", |
|||
tableId: "ta_d4bf541ce0d84b16a1a8e0a060e5f7f7", |
|||
fieldName: "Guest", |
|||
relationshipType: "one-to-many", |
|||
}, |
|||
Names: { |
|||
type: "string", |
|||
constraints: { |
|||
type: "string", |
|||
length: { |
|||
maximum: "", |
|||
}, |
|||
presence: false, |
|||
}, |
|||
fieldName: "Guest", |
|||
name: "Names", |
|||
}, |
|||
}, |
|||
primaryDisplay: "Names", |
|||
indexes: [], |
|||
_rev: "11-7c153edbc6d7c43821cfd5ed526266cf", |
|||
} |
|||
|
|||
export const A_TABLE = { |
|||
type: "table", |
|||
views: {}, |
|||
name: "Guest", |
|||
schema: { |
|||
"Auto ID": { |
|||
name: "Auto ID", |
|||
type: "number", |
|||
subtype: "autoID", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "number", |
|||
presence: false, |
|||
numericality: { |
|||
greaterThanOrEqualTo: "", |
|||
lessThanOrEqualTo: "", |
|||
}, |
|||
}, |
|||
lastID: 1, |
|||
}, |
|||
"Created By": { |
|||
name: "Created By", |
|||
type: "link", |
|||
subtype: "createdBy", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "array", |
|||
presence: false, |
|||
}, |
|||
tableId: "ta_users", |
|||
fieldName: "Guest-Created By", |
|||
relationshipType: "many-to-many", |
|||
}, |
|||
"Created At": { |
|||
name: "Created At", |
|||
type: "datetime", |
|||
subtype: "createdAt", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "string", |
|||
length: {}, |
|||
presence: false, |
|||
datetime: { |
|||
latest: "", |
|||
earliest: "", |
|||
}, |
|||
}, |
|||
}, |
|||
"Updated By": { |
|||
name: "Updated By", |
|||
type: "link", |
|||
subtype: "updatedBy", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "array", |
|||
presence: false, |
|||
}, |
|||
tableId: "ta_users", |
|||
fieldName: "Guest-Updated By", |
|||
relationshipType: "many-to-many", |
|||
}, |
|||
"Updated At": { |
|||
name: "Updated At", |
|||
type: "datetime", |
|||
subtype: "updatedAt", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "string", |
|||
length: {}, |
|||
presence: false, |
|||
datetime: { |
|||
latest: "", |
|||
earliest: "", |
|||
}, |
|||
}, |
|||
}, |
|||
Episode: { |
|||
name: "Episode", |
|||
type: "link", |
|||
tableId: "ta_d4bf541ce0d84b16a1a8e0a060e5f7f7", |
|||
fieldName: "Guest", |
|||
relationshipType: "one-to-many", |
|||
}, |
|||
Names: { |
|||
type: "string", |
|||
constraints: { |
|||
type: "string", |
|||
length: { |
|||
maximum: "", |
|||
}, |
|||
presence: false, |
|||
}, |
|||
fieldName: "Guest", |
|||
name: "Names", |
|||
}, |
|||
}, |
|||
primaryDisplay: "Names", |
|||
indexes: [], |
|||
_id: "ta_3c78cffe33664ca9bfb6b2b6cb3ee55a", |
|||
_rev: "10-27f034bf50ec3e2f180d8f96db1f0f31", |
|||
_rename: { |
|||
old: "Name", |
|||
updated: "Names", |
|||
}, |
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
export const A_VIEW = { |
|||
name: "Published", |
|||
tableId: "ta_3c78cffe33664ca9bfb6b2b6cb3ee55a", |
|||
filters: [], |
|||
schema: { |
|||
"Auto ID": { |
|||
name: "Auto ID", |
|||
type: "number", |
|||
subtype: "autoID", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "number", |
|||
presence: false, |
|||
numericality: { |
|||
greaterThanOrEqualTo: "", |
|||
lessThanOrEqualTo: "", |
|||
}, |
|||
}, |
|||
lastID: 2, |
|||
}, |
|||
"Created By": { |
|||
name: "Created By", |
|||
type: "link", |
|||
subtype: "createdBy", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "array", |
|||
presence: false, |
|||
}, |
|||
tableId: "ta_users", |
|||
fieldName: "Guest-Created By", |
|||
relationshipType: "many-to-many", |
|||
}, |
|||
"Created At": { |
|||
name: "Created At", |
|||
type: "datetime", |
|||
subtype: "createdAt", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "string", |
|||
length: {}, |
|||
presence: false, |
|||
datetime: { |
|||
latest: "", |
|||
earliest: "", |
|||
}, |
|||
}, |
|||
}, |
|||
"Updated By": { |
|||
name: "Updated By", |
|||
type: "link", |
|||
subtype: "updatedBy", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "array", |
|||
presence: false, |
|||
}, |
|||
tableId: "ta_users", |
|||
fieldName: "Guest-Updated By", |
|||
relationshipType: "many-to-many", |
|||
}, |
|||
"Updated At": { |
|||
name: "Updated At", |
|||
type: "datetime", |
|||
subtype: "updatedAt", |
|||
icon: "ri-magic-line", |
|||
autocolumn: true, |
|||
constraints: { |
|||
type: "string", |
|||
length: {}, |
|||
presence: false, |
|||
datetime: { |
|||
latest: "", |
|||
earliest: "", |
|||
}, |
|||
}, |
|||
}, |
|||
Episode: { |
|||
name: "Episode", |
|||
type: "link", |
|||
tableId: "ta_d4bf541ce0d84b16a1a8e0a060e5f7f7", |
|||
fieldName: "Guest", |
|||
relationshipType: "one-to-many", |
|||
}, |
|||
Names: { |
|||
type: "string", |
|||
constraints: { |
|||
type: "string", |
|||
length: { |
|||
maximum: "", |
|||
}, |
|||
presence: false, |
|||
}, |
|||
fieldName: "Guest", |
|||
name: "Names", |
|||
}, |
|||
}, |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
import api from 'builderStore/api' |
|||
|
|||
jest.mock('builderStore/api'); |
|||
|
|||
const PERMISSIONS_FOR_RESOURCE = { |
|||
"write": "BASIC", |
|||
"read": "BASIC" |
|||
} |
|||
|
|||
import { createPermissionStore } from "../permissions" |
|||
|
|||
describe("Permissions Store", () => { |
|||
const store = createPermissionStore() |
|||
|
|||
it("fetches permissions for specific resource", async () => { |
|||
api.get.mockReturnValueOnce({ json: () => PERMISSIONS_FOR_RESOURCE}) |
|||
|
|||
const resourceId = "ta_013657543b4043b89dbb17e9d3a4723a" |
|||
|
|||
const permissions = await store.forResource(resourceId) |
|||
|
|||
expect(api.get).toBeCalledWith(`/api/permission/${resourceId}`) |
|||
expect(permissions).toEqual(PERMISSIONS_FOR_RESOURCE) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,54 @@ |
|||
import { get } from 'svelte/store' |
|||
import api from 'builderStore/api' |
|||
|
|||
jest.mock('builderStore/api'); |
|||
|
|||
import { SOME_QUERY, SAVE_QUERY_RESPONSE } from './fixtures/queries' |
|||
|
|||
import { createQueriesStore } from "../queries" |
|||
import { datasources } from '../datasources' |
|||
|
|||
describe("Queries Store", () => { |
|||
let store = createQueriesStore() |
|||
|
|||
beforeEach(async () => { |
|||
api.get.mockReturnValue({ json: () => [SOME_QUERY]}) |
|||
await store.init() |
|||
}) |
|||
|
|||
it("Initialises correctly", async () => { |
|||
api.get.mockReturnValue({ json: () => [SOME_QUERY]}) |
|||
|
|||
await store.init() |
|||
expect(get(store)).toEqual({ list: [SOME_QUERY], selected: null}) |
|||
}) |
|||
|
|||
it("fetches all the queries", async () => { |
|||
api.get.mockReturnValue({ json: () => [SOME_QUERY]}) |
|||
|
|||
await store.fetch() |
|||
expect(get(store)).toEqual({ list: [SOME_QUERY], selected: null}) |
|||
}) |
|||
|
|||
it("selects a query and updates selected datasource", async () => { |
|||
await store.select(SOME_QUERY) |
|||
|
|||
expect(get(store).selected).toEqual(SOME_QUERY._id) |
|||
expect(get(datasources).selected).toEqual(SOME_QUERY.datasourceId) |
|||
}) |
|||
|
|||
it("saves the query, updates the store and returns status message", async () => { |
|||
api.post.mockReturnValue({ json: () => SAVE_QUERY_RESPONSE}) |
|||
|
|||
await store.select(SOME_QUERY.datasourceId, SOME_QUERY) |
|||
|
|||
expect(get(store).list).toEqual(expect.arrayContaining([SOME_QUERY])) |
|||
}) |
|||
it("deletes a query, updates the store and returns status message", async () => { |
|||
|
|||
api.delete.mockReturnValue({status: 200, message: `Query deleted.`}) |
|||
|
|||
await store.delete(SOME_QUERY) |
|||
expect(get(store)).toEqual({ list: [], selected: null}) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,35 @@ |
|||
import { get } from 'svelte/store' |
|||
import api from 'builderStore/api' |
|||
|
|||
jest.mock('builderStore/api'); |
|||
|
|||
import { createRolesStore } from "../roles" |
|||
import { ROLES } from './fixtures/roles' |
|||
|
|||
describe("Roles Store", () => { |
|||
let store = createRolesStore() |
|||
|
|||
beforeEach( async() => { |
|||
store = createRolesStore() |
|||
}) |
|||
|
|||
it("fetches roles from backend", async () => { |
|||
api.get.mockReturnValue({ json: () => ROLES}) |
|||
await store.fetch() |
|||
|
|||
expect(api.get).toBeCalledWith("/api/roles") |
|||
expect(get(store)).toEqual(ROLES) |
|||
}) |
|||
|
|||
it("deletes a role", async () => { |
|||
api.get.mockReturnValueOnce({ json: () => ROLES}) |
|||
await store.fetch() |
|||
|
|||
api.delete.mockReturnValue({status: 200, message: `Role deleted.`}) |
|||
|
|||
const updatedRoles = [...ROLES.slice(1)] |
|||
await store.delete(ROLES[0]) |
|||
|
|||
expect(get(store)).toEqual(updatedRoles) |
|||
}) |
|||
}) |
|||
@ -0,0 +1,75 @@ |
|||
import { get } from 'svelte/store' |
|||
import api from 'builderStore/api' |
|||
|
|||
jest.mock('builderStore/api'); |
|||
|
|||
import { SOME_TABLES, SAVE_TABLES_RESPONSE, A_TABLE } from './fixtures/tables' |
|||
|
|||
import { createTablesStore } from "../tables" |
|||
import { views } from '../views' |
|||
|
|||
describe("Tables Store", () => { |
|||
let store = createTablesStore() |
|||
|
|||
beforeEach(async () => { |
|||
api.get.mockReturnValue({ json: () => SOME_TABLES}) |
|||
await store.init() |
|||
}) |
|||
|
|||
it("Initialises correctly", async () => { |
|||
expect(get(store)).toEqual({ list: SOME_TABLES, selected: {}, draft: {}}) |
|||
}) |
|||
|
|||
it("fetches all the tables", async () => { |
|||
api.get.mockReturnValue({ json: () => SOME_TABLES}) |
|||
|
|||
await store.fetch() |
|||
expect(get(store)).toEqual({ list: SOME_TABLES, selected: {}, draft: {}}) |
|||
}) |
|||
|
|||
it("selects a table", async () => { |
|||
const tableToSelect = SOME_TABLES[0] |
|||
await store.select(tableToSelect) |
|||
|
|||
expect(get(store).selected).toEqual(tableToSelect) |
|||
expect(get(store).draft).toEqual(tableToSelect) |
|||
}) |
|||
|
|||
it("selecting without a param resets the selected property", async () => { |
|||
await store.select() |
|||
|
|||
expect(get(store).draft).toEqual({}) |
|||
}) |
|||
|
|||
it("selecting a table updates the view store", async () => { |
|||
const tableToSelect = SOME_TABLES[0] |
|||
await store.select(tableToSelect) |
|||
|
|||
expect(get(store).selected).toEqual(tableToSelect) |
|||
expect(get(views).selected).toEqual({ name: `all_${tableToSelect._id}` }) |
|||
}) |
|||
|
|||
it("saving a table also selects it", async () => { |
|||
api.post.mockReturnValue({ json: () => SAVE_TABLES_RESPONSE}) |
|||
|
|||
await store.save(A_TABLE) |
|||
|
|||
expect(get(store).selected).toEqual(SAVE_TABLES_RESPONSE) |
|||
}) |
|||
|
|||
it("saving the table returns a response", async () => { |
|||
api.post.mockReturnValue({ json: () => SAVE_TABLES_RESPONSE}) |
|||
|
|||
const response = await store.save(A_TABLE) |
|||
|
|||
expect(response).toEqual(SAVE_TABLES_RESPONSE) |
|||
}) |
|||
it("deleting a table removes it from the store", async () => { |
|||
api.delete.mockReturnValue({status: 200, message: `Table deleted.`}) |
|||
|
|||
await store.delete(A_TABLE) |
|||
expect(get(store).list).toEqual(expect.not.arrayContaining([A_TABLE])) |
|||
}) |
|||
|
|||
// TODO: Write tests for saving and deleting fields
|
|||
}) |
|||
@ -0,0 +1,45 @@ |
|||
import { writable, get } from "svelte/store" |
|||
import { tables } from "./" |
|||
import api from "builderStore/api" |
|||
|
|||
export function createViewsStore() { |
|||
const { subscribe, update } = writable({ |
|||
list: [], |
|||
selected: null, |
|||
}) |
|||
|
|||
return { |
|||
subscribe, |
|||
select: async view => { |
|||
update(state => ({ |
|||
...state, |
|||
selected: view, |
|||
})) |
|||
}, |
|||
delete: async view => { |
|||
await api.delete(`/api/views/${view}`) |
|||
await tables.fetch() |
|||
}, |
|||
save: async view => { |
|||
const response = await api.post(`/api/views`, view) |
|||
const json = await response.json() |
|||
|
|||
const viewMeta = { |
|||
name: view.name, |
|||
...json, |
|||
} |
|||
|
|||
const viewTable = get(tables).list.find( |
|||
table => table._id === view.tableId |
|||
) |
|||
|
|||
if (view.originalName) delete viewTable.views[view.originalName] |
|||
viewTable.views[view.name] = viewMeta |
|||
await tables.save(viewTable) |
|||
|
|||
update(state => ({ ...state, selected: viewMeta })) |
|||
}, |
|||
} |
|||
} |
|||
|
|||
export const views = createViewsStore() |
|||
@ -1,111 +0,0 @@ |
|||
export const componentsAndScreens = () => ({ |
|||
components: [ |
|||
{ |
|||
_instanceName: "TextBox", |
|||
tags: ["Text", "input"], |
|||
children: false, |
|||
props: { |
|||
size: { type: "options", options: ["small", "medium", "large"] }, |
|||
isPassword: "bool", |
|||
placeholder: "string", |
|||
label: "string", |
|||
}, |
|||
}, |
|||
{ |
|||
_instanceName: "Button", |
|||
tags: ["input"], |
|||
children: true, |
|||
props: { |
|||
size: { type: "options", options: ["small", "medium", "large"] }, |
|||
css: "string", |
|||
contentText: "string", |
|||
}, |
|||
}, |
|||
{ |
|||
_instanceName: "div", |
|||
tags: ["input"], |
|||
props: { |
|||
width: "number", |
|||
}, |
|||
}, |
|||
{ |
|||
_instanceName: "Row View", |
|||
tags: ["row"], |
|||
props: { |
|||
data: "state", |
|||
}, |
|||
}, |
|||
], |
|||
screens: [ |
|||
{ |
|||
props: { |
|||
_component: "budibase-components/TextBox", |
|||
_instanceName: "SmallTextbox", |
|||
size: "small", |
|||
}, |
|||
}, |
|||
|
|||
{ |
|||
name: "common/PasswordBox", |
|||
tags: ["mask"], |
|||
props: { |
|||
_component: "budibase-components/TextBox", |
|||
_instanceName: "PasswordBox", |
|||
isPassword: true, |
|||
size: "small", |
|||
}, |
|||
}, |
|||
|
|||
{ |
|||
props: { |
|||
_component: "budibase-components/Button", |
|||
_instanceName: "PrimaryButton", |
|||
css: "btn-primary", |
|||
}, |
|||
}, |
|||
|
|||
{ |
|||
route: "", |
|||
props: { |
|||
_component: "budibase-components/div", |
|||
width: 100, |
|||
_instanceName: "Screen 1", |
|||
_children: [ |
|||
{ |
|||
_component: "budibase-components/Button", |
|||
contentText: "Button 1", |
|||
_instanceName: "Button 1", |
|||
}, |
|||
{ |
|||
_component: "budibase-components/Button", |
|||
contentText: "Button 2", |
|||
_instanceName: "Button 2", |
|||
}, |
|||
{ |
|||
_component: "budibase-components/TextBox", |
|||
_instanceName: "TextBox", |
|||
isPassword: true, |
|||
size: "small", |
|||
}, |
|||
], |
|||
}, |
|||
}, |
|||
|
|||
{ |
|||
props: { |
|||
_component: "budibase-components/div", |
|||
_instanceName: "Field", |
|||
_children: [ |
|||
{ |
|||
_component: "common/SmallTextbox", |
|||
}, |
|||
], |
|||
}, |
|||
}, |
|||
], |
|||
}) |
|||
|
|||
export const stripStandardProps = props => { |
|||
delete props._id |
|||
delete props._styles |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,38 @@ |
|||
const PostHog = require("posthog-node") |
|||
const { |
|||
BUDIBASE_POSTHOG_URL, |
|||
BUDIBASE_POSTHOG_TOKEN, |
|||
AnalyticsEvents, |
|||
} = require("../constants") |
|||
const ConfigManager = require("../structures/ConfigManager") |
|||
|
|||
class AnalyticsClient { |
|||
constructor() { |
|||
this.client = new PostHog(BUDIBASE_POSTHOG_TOKEN, { |
|||
host: BUDIBASE_POSTHOG_URL, |
|||
}) |
|||
this.configManager = new ConfigManager() |
|||
} |
|||
|
|||
capture(event) { |
|||
if (this.manager.config.analyticsDisabled) return |
|||
|
|||
this.client.capture(event) |
|||
} |
|||
|
|||
enable() { |
|||
this.configManager.removeKey("analyticsDisabled") |
|||
this.client.capture({ event: AnalyticsEvents.OptIn, distinctId: "cli" }) |
|||
} |
|||
|
|||
disable() { |
|||
this.client.capture({ event: AnalyticsEvents.OptOut, distinctId: "cli" }) |
|||
this.configManager.setValue("analyticsDisabled", true) |
|||
} |
|||
|
|||
status() { |
|||
return this.configManager.config.analyticsDisabled ? "disabled" : "enabled" |
|||
} |
|||
} |
|||
|
|||
module.exports = AnalyticsClient |
|||
@ -0,0 +1,63 @@ |
|||
const Command = require("../structures/Command") |
|||
const { CommandWords } = require("../constants") |
|||
const { success, error } = require("../utils") |
|||
const AnalyticsClient = require("./Client") |
|||
|
|||
const client = new AnalyticsClient() |
|||
|
|||
async function optOut() { |
|||
try { |
|||
// opt them out
|
|||
client.disable() |
|||
console.log( |
|||
success( |
|||
"Successfully opted out of budibase analytics. You can opt in at any time by running 'budi analytics opt-in'" |
|||
) |
|||
) |
|||
} catch (err) { |
|||
console.log( |
|||
error( |
|||
"Error opting out of budibase analytics. Please try again later.", |
|||
err |
|||
) |
|||
) |
|||
} |
|||
} |
|||
|
|||
async function optIn() { |
|||
try { |
|||
// opt them in
|
|||
client.enable() |
|||
console.log( |
|||
success( |
|||
"Successfully opted in to budibase analytics. Thank you for helping us make budibase better!" |
|||
) |
|||
) |
|||
} catch (err) { |
|||
console.log( |
|||
error("Error opting in to budibase analytics. Please try again later.") |
|||
) |
|||
} |
|||
} |
|||
|
|||
async function status() { |
|||
try { |
|||
console.log(success(`Budibase analytics ${client.status()}`)) |
|||
} catch (err) { |
|||
console.log( |
|||
error("Error fetching analytics status. Please try again later.") |
|||
) |
|||
} |
|||
} |
|||
|
|||
const command = new Command(`${CommandWords.ANALYTICS}`) |
|||
.addHelp("Control the analytics you send to budibase.") |
|||
.addSubOption("--optin", "Opt in to sending analytics to budibase", optIn) |
|||
.addSubOption("--optout", "Opt out of sending analytics to budibase.", optOut) |
|||
.addSubOption( |
|||
"--status", |
|||
"Check whether you are currently opted in to budibase analytics.", |
|||
status |
|||
) |
|||
|
|||
exports.command = command |
|||
@ -1,5 +1,6 @@ |
|||
const analytics = require("./analytics") |
|||
const hosting = require("./hosting") |
|||
|
|||
exports.getCommands = () => { |
|||
return [hosting.command] |
|||
return [hosting.command, analytics.command] |
|||
} |
|||
|
|||
@ -0,0 +1,50 @@ |
|||
const fs = require("fs") |
|||
const path = require("path") |
|||
const os = require("os") |
|||
const { error } = require("../utils") |
|||
|
|||
class ConfigManager { |
|||
constructor() { |
|||
this.path = path.join(os.homedir(), ".budibase.json") |
|||
if (!fs.existsSync(this.path)) { |
|||
fs.writeFileSync(this.path, "{}") |
|||
} |
|||
} |
|||
|
|||
get config() { |
|||
try { |
|||
return JSON.parse(fs.readFileSync(this.path, "utf8")) |
|||
} catch (err) { |
|||
console.log( |
|||
error( |
|||
"Error parsing configuration file. Please check your .budibase.json is valid." |
|||
) |
|||
) |
|||
return {} |
|||
} |
|||
} |
|||
|
|||
set config(json) { |
|||
fs.writeFileSync(this.path, JSON.stringify(json)) |
|||
} |
|||
|
|||
getValue(key) { |
|||
return this.config[key] |
|||
} |
|||
|
|||
setValue(key, value) { |
|||
const updated = { |
|||
...this.config, |
|||
[key]: value, |
|||
} |
|||
this.config = updated |
|||
} |
|||
|
|||
removeKey(key) { |
|||
const updated = { ...this.config } |
|||
delete updated[key] |
|||
this.config = updated |
|||
} |
|||
} |
|||
|
|||
module.exports = ConfigManager |
|||
Loading…
Reference in new issue