Merge pull request #391 from Budibase/data-model-redesign
Data model redesign
This commit is contained in:
commit
961226a63f
|
@ -29,19 +29,24 @@
|
||||||
"builderStore(.*)$": "<rootDir>/src/builderStore$1"
|
"builderStore(.*)$": "<rootDir>/src/builderStore$1"
|
||||||
},
|
},
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
"js"
|
"js",
|
||||||
|
"svelte"
|
||||||
],
|
],
|
||||||
"moduleDirectories": [
|
"moduleDirectories": [
|
||||||
"node_modules"
|
"node_modules"
|
||||||
],
|
],
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+js$": "babel-jest"
|
"^.+js$": "babel-jest",
|
||||||
|
"^.+.svelte$": "svelte-jester"
|
||||||
},
|
},
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"/node_modules/(?!svelte).+\\.js$"
|
"/node_modules/(?!svelte).+\\.js$"
|
||||||
],
|
],
|
||||||
"modulePathIgnorePatterns": [
|
"modulePathIgnorePatterns": [
|
||||||
"<rootDir>/cypress/"
|
"<rootDir>/cypress/"
|
||||||
|
],
|
||||||
|
"setupFilesAfterEnv": [
|
||||||
|
"@testing-library/jest-dom/extend-expect"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
@ -76,6 +81,8 @@
|
||||||
"@rollup/plugin-alias": "^3.0.1",
|
"@rollup/plugin-alias": "^3.0.1",
|
||||||
"@rollup/plugin-json": "^4.0.3",
|
"@rollup/plugin-json": "^4.0.3",
|
||||||
"@sveltech/routify": "1.7.11",
|
"@sveltech/routify": "1.7.11",
|
||||||
|
"@testing-library/jest-dom": "^5.11.0",
|
||||||
|
"@testing-library/svelte": "^3.0.0",
|
||||||
"babel-jest": "^24.8.0",
|
"babel-jest": "^24.8.0",
|
||||||
"browser-sync": "^2.26.7",
|
"browser-sync": "^2.26.7",
|
||||||
"cypress": "^4.8.0",
|
"cypress": "^4.8.0",
|
||||||
|
@ -97,7 +104,8 @@
|
||||||
"rollup-plugin-terser": "^4.0.4",
|
"rollup-plugin-terser": "^4.0.4",
|
||||||
"rollup-plugin-url": "^2.2.2",
|
"rollup-plugin-url": "^2.2.2",
|
||||||
"start-server-and-test": "^1.11.0",
|
"start-server-and-test": "^1.11.0",
|
||||||
"svelte": "3.23.x"
|
"svelte": "3.23.x",
|
||||||
|
"svelte-jester": "^1.0.6"
|
||||||
},
|
},
|
||||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,18 @@
|
||||||
import { routes } from "../routify/routes"
|
import { routes } from "../routify/routes"
|
||||||
import { store, initialise } from "builderStore"
|
import { store, initialise } from "builderStore"
|
||||||
import NotificationDisplay from "components/common/Notification/NotificationDisplay.svelte"
|
import NotificationDisplay from "components/common/Notification/NotificationDisplay.svelte"
|
||||||
|
import { notifier } from "builderStore/store/notifications"
|
||||||
|
|
||||||
|
function showErrorBanner() {
|
||||||
|
notifier.danger(
|
||||||
|
"Whoops! Looks like we're having trouble. Please refresh the page."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
window.addEventListener("error", showErrorBanner)
|
||||||
|
window.addEventListener("unhandledrejection", showErrorBanner)
|
||||||
|
})
|
||||||
|
|
||||||
$basepath = "/_builder"
|
$basepath = "/_builder"
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { uuid } from "builderStore/uuid"
|
|
||||||
import api from "../api"
|
import api from "../api"
|
||||||
|
|
||||||
export const getBackendUiStore = () => {
|
export const getBackendUiStore = () => {
|
||||||
|
@ -70,9 +69,23 @@ export const getBackendUiStore = () => {
|
||||||
save: async ({ model }) => {
|
save: async ({ model }) => {
|
||||||
const updatedModel = cloneDeep(model)
|
const updatedModel = cloneDeep(model)
|
||||||
|
|
||||||
|
// update any renamed schema keys to reflect their names
|
||||||
|
for (let key in updatedModel.schema) {
|
||||||
|
const field = updatedModel.schema[key]
|
||||||
|
// field has been renamed
|
||||||
|
if (field.name && field.name !== key) {
|
||||||
|
updatedModel.schema[field.name] = field
|
||||||
|
updatedModel._rename = { old: key, updated: field.name }
|
||||||
|
delete updatedModel.schema[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const SAVE_MODEL_URL = `/api/models`
|
const SAVE_MODEL_URL = `/api/models`
|
||||||
await api.post(SAVE_MODEL_URL, updatedModel)
|
console.log(updatedModel)
|
||||||
|
const response = await api.post(SAVE_MODEL_URL, updatedModel)
|
||||||
|
const savedModel = await response.json()
|
||||||
await store.actions.models.fetch()
|
await store.actions.models.fetch()
|
||||||
|
store.actions.models.select(savedModel)
|
||||||
},
|
},
|
||||||
addField: field => {
|
addField: field => {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
|
@ -80,13 +93,11 @@ export const getBackendUiStore = () => {
|
||||||
state.draftModel.schema = {}
|
state.draftModel.schema = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = uuid()
|
|
||||||
|
|
||||||
state.draftModel.schema = {
|
state.draftModel.schema = {
|
||||||
...state.draftModel.schema,
|
...state.draftModel.schema,
|
||||||
[id]: field,
|
[field.name]: cloneDeep(field),
|
||||||
}
|
}
|
||||||
state.selectedField = id
|
state.selectedField = field.name
|
||||||
state.tabs.NAVIGATION_PANEL = "NAVIGATE"
|
state.tabs.NAVIGATION_PANEL = "NAVIGATE"
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
let linkedRecords = new Set(linked)
|
let linkedRecords = new Set(linked)
|
||||||
|
|
||||||
$: linked = [...linkedRecords]
|
$: linked = [...linkedRecords]
|
||||||
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel._id]
|
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel.name]
|
||||||
$: schema = $backendUiStore.selectedModel.schema
|
$: schema = $backendUiStore.selectedModel.schema
|
||||||
|
|
||||||
async function fetchRecords() {
|
async function fetchRecords() {
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.fields.selected {
|
.fields.selected {
|
||||||
background: var(--light-grey);
|
background: var(--grey-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
|
|
|
@ -44,5 +44,6 @@
|
||||||
-moz-box-sizing: border-box;
|
-moz-box-sizing: border-box;
|
||||||
-webkit-box-sizing: border-box;
|
-webkit-box-sizing: border-box;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
font-family: Inter;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
let open = false
|
let open = false
|
||||||
let model
|
let model
|
||||||
|
|
||||||
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel._id, field.modelId]
|
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel.name]
|
||||||
|
|
||||||
async function fetchRecords() {
|
async function fetchRecords() {
|
||||||
const response = await api.post("/api/records/search", {
|
const response = await api.post("/api/records/search", {
|
||||||
|
@ -88,7 +88,7 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 15%;
|
right: 15%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: var(--light-grey);
|
background: var(--grey-1);
|
||||||
border: 1px solid var(--grey);
|
border: 1px solid var(--grey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,7 @@
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
color: var(--ink-lighter);
|
color: var(--ink-lighter);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
background: var(--light-grey);
|
background: var(--grey-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
span:hover {
|
span:hover {
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { uuid } from "builderStore/uuid"
|
import { uuid } from "builderStore/uuid"
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import { FIELDS, BLOCKS, MODELS } from "constants/backend"
|
import { FIELDS, BLOCKS, MODELS } from "constants/backend"
|
||||||
import Block from "components/common/Block.svelte"
|
import Block from "components/common/Block.svelte"
|
||||||
|
|
||||||
|
@ -12,19 +13,13 @@
|
||||||
function createModel(model) {
|
function createModel(model) {
|
||||||
const { schema, ...rest } = $backendUiStore.selectedModel
|
const { schema, ...rest } = $backendUiStore.selectedModel
|
||||||
|
|
||||||
const newModel = { ...model, schema: {} }
|
|
||||||
|
|
||||||
// TODO: could be better
|
|
||||||
for (let key in model.schema) {
|
|
||||||
newModel.schema[uuid()] = model.schema[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
backendUiStore.actions.models.save({
|
backendUiStore.actions.models.save({
|
||||||
model: {
|
model: {
|
||||||
...newModel,
|
...model,
|
||||||
...rest,
|
...rest,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
notifier.success(`${model.name} model created.`)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -64,13 +64,13 @@
|
||||||
on:click={() => selectModel(model)} />
|
on:click={() => selectModel(model)} />
|
||||||
{#if model._id === $backendUiStore.selectedModel._id}
|
{#if model._id === $backendUiStore.selectedModel._id}
|
||||||
<div in:slide>
|
<div in:slide>
|
||||||
{#each Object.keys(model.schema) as fieldId}
|
{#each Object.keys(model.schema) as fieldName}
|
||||||
<ListItem
|
<ListItem
|
||||||
selected={model._id === $backendUiStore.selectedModel._id && fieldId === $backendUiStore.selectedField}
|
selected={model._id === $backendUiStore.selectedModel._id && fieldName === $backendUiStore.selectedField}
|
||||||
indented
|
indented
|
||||||
icon="ri-layout-column-fill"
|
icon="ri-layout-column-fill"
|
||||||
title={model.schema[fieldId].name}
|
title={model.schema[fieldName].name}
|
||||||
on:click={() => selectModel(model, fieldId)} />
|
on:click={() => selectModel(model, fieldName)} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -22,18 +22,6 @@
|
||||||
field.constraints &&
|
field.constraints &&
|
||||||
field.constraints.presence &&
|
field.constraints.presence &&
|
||||||
!constraints.presence.allowEmpty
|
!constraints.presence.allowEmpty
|
||||||
|
|
||||||
function attachModelIdToSchema(evt) {
|
|
||||||
const { draftModel } = $backendUiStore
|
|
||||||
if ($backendUiStore.selectedField !== evt.target.value) {
|
|
||||||
delete draftModel.schema[$backendUiStore.selectedField]
|
|
||||||
draftModel.schema[evt.target.value] = field
|
|
||||||
backendUiStore.update(state => {
|
|
||||||
state.selectedField = evt.target.value
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="info">
|
<div class="info">
|
||||||
|
@ -78,10 +66,7 @@
|
||||||
{:else if field.type === 'link'}
|
{:else if field.type === 'link'}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Link</label>
|
<label>Link</label>
|
||||||
<select
|
<select class="budibase__input" bind:value={field.modelId}>
|
||||||
class="budibase__input"
|
|
||||||
bind:value={field.modelId}
|
|
||||||
on:change={attachModelIdToSchema}>
|
|
||||||
<option value={''} />
|
<option value={''} />
|
||||||
{#each $backendUiStore.models as model}
|
{#each $backendUiStore.models as model}
|
||||||
{#if model._id !== $backendUiStore.draftModel._id}
|
{#if model._id !== $backendUiStore.draftModel._id}
|
||||||
|
|
|
@ -24,6 +24,22 @@ exports.save = async function(ctx) {
|
||||||
...ctx.request.body,
|
...ctx.request.body,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update renamed record fields when model is updated
|
||||||
|
const { _rename } = modelToSave
|
||||||
|
if (_rename) {
|
||||||
|
const records = await db.query(`database/all_${modelToSave._id}`, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
const docs = records.rows.map(({ doc }) => {
|
||||||
|
doc[_rename.updated] = doc[_rename.old]
|
||||||
|
delete doc[_rename.old]
|
||||||
|
return doc
|
||||||
|
})
|
||||||
|
|
||||||
|
await db.bulkDocs(docs)
|
||||||
|
delete modelToSave._rename
|
||||||
|
}
|
||||||
|
|
||||||
const result = await db.post(modelToSave)
|
const result = await db.post(modelToSave)
|
||||||
modelToSave._rev = result.rev
|
modelToSave._rev = result.rev
|
||||||
|
|
||||||
|
@ -33,7 +49,7 @@ exports.save = async function(ctx) {
|
||||||
if (schema[key].type === "link") {
|
if (schema[key].type === "link") {
|
||||||
// create the link field in the other model
|
// create the link field in the other model
|
||||||
const linkedModel = await db.get(schema[key].modelId)
|
const linkedModel = await db.get(schema[key].modelId)
|
||||||
linkedModel.schema[modelToSave._id] = {
|
linkedModel.schema[modelToSave.name] = {
|
||||||
name: modelToSave.name,
|
name: modelToSave.name,
|
||||||
type: "link",
|
type: "link",
|
||||||
modelId: modelToSave._id,
|
modelId: modelToSave._id,
|
||||||
|
@ -51,7 +67,7 @@ exports.save = async function(ctx) {
|
||||||
[`all_${modelToSave._id}`]: {
|
[`all_${modelToSave._id}`]: {
|
||||||
map: `function(doc) {
|
map: `function(doc) {
|
||||||
if (doc.modelId === "${modelToSave._id}") {
|
if (doc.modelId === "${modelToSave._id}") {
|
||||||
emit(doc[doc.key], doc._id);
|
emit(doc._id);
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
|
@ -75,7 +91,7 @@ exports.destroy = async function(ctx) {
|
||||||
// Delete all records for that model
|
// Delete all records for that model
|
||||||
const records = await db.query(`database/${modelViewId}`)
|
const records = await db.query(`database/${modelViewId}`)
|
||||||
await db.bulkDocs(
|
await db.bulkDocs(
|
||||||
records.rows.map(record => ({ id: record.id, _deleted: true }))
|
records.rows.map(record => ({ _id: record.id, _deleted: true }))
|
||||||
)
|
)
|
||||||
|
|
||||||
// Delete linked record fields in dependent models
|
// Delete linked record fields in dependent models
|
||||||
|
|
|
@ -56,8 +56,8 @@ exports.save = async function(ctx) {
|
||||||
const doc = row.doc
|
const doc = row.doc
|
||||||
return {
|
return {
|
||||||
...doc,
|
...doc,
|
||||||
[model._id]: doc[model._id]
|
[model.name]: doc[model.name]
|
||||||
? [...doc[model._id], record._id]
|
? [...doc[model.name], record._id]
|
||||||
: [record._id],
|
: [record._id],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -26,7 +26,6 @@ describe("/models", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
instance = await createInstance(request, app._id);
|
instance = await createInstance(request, app._id);
|
||||||
});
|
});
|
||||||
|
@ -51,6 +50,50 @@ describe("/models", () => {
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("renames all the record fields for a model when a schema key is renamed", async () => {
|
||||||
|
const testModel = await createModel(request, app._id, instance._id);
|
||||||
|
|
||||||
|
const testRecord = await request
|
||||||
|
.post(`/api/${testModel._id}/records`)
|
||||||
|
.send({
|
||||||
|
name: "test"
|
||||||
|
})
|
||||||
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
const updatedModel = await request
|
||||||
|
.post(`/api/models`)
|
||||||
|
.send({
|
||||||
|
_id: testModel._id,
|
||||||
|
_rev: testModel._rev,
|
||||||
|
name: "TestModel",
|
||||||
|
key: "name",
|
||||||
|
_rename: {
|
||||||
|
old: "name",
|
||||||
|
updated: "updatedName"
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
updatedName: { type: "string" }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(updatedModel.res.statusMessage).toEqual("Model TestModel saved successfully.");
|
||||||
|
expect(updatedModel.body.name).toEqual("TestModel");
|
||||||
|
|
||||||
|
const res = await request
|
||||||
|
.get(`/api/${testModel._id}/records/${testRecord.body._id}`)
|
||||||
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(res.body.updatedName).toEqual("test");
|
||||||
|
expect(res.body.name).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("should apply authorization to endpoint", async () => {
|
it("should apply authorization to endpoint", async () => {
|
||||||
await builderEndpointShouldBlockNormalUsers({
|
await builderEndpointShouldBlockNormalUsers({
|
||||||
request,
|
request,
|
||||||
|
|
Loading…
Reference in New Issue