Merge pull request #391 from Budibase/data-model-redesign

Data model redesign
This commit is contained in:
Martin McKeaveney 2020-06-26 10:56:07 +01:00 committed by GitHub
commit 961226a63f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 119 additions and 48 deletions

View File

@ -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"
} }

View File

@ -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>

View File

@ -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

View File

@ -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 {

View File

@ -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>

View File

@ -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);
} }

View File

@ -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 {

View File

@ -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>

View File

@ -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}

View File

@ -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}

View File

@ -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

View File

@ -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],
} }
}) })

View File

@ -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,