merge conflicts

This commit is contained in:
Michael Shanks 2020-03-24 10:56:48 +00:00
parent 60013d3d3b
commit 6ab3003106
54 changed files with 1319 additions and 362 deletions

View File

@ -1,7 +1,8 @@
/* Budibase Component Styles */ /* Budibase Component Styles */
.header { .header {
font-size: 0.75rem; font-size: 0.75rem;
color: #999; color: #000333;
opacity: 0.4;
text-transform: uppercase; text-transform: uppercase;
margin-top: 1rem; margin-top: 1rem;
font-weight: 500; font-weight: 500;
@ -58,7 +59,7 @@
cursor: pointer; cursor: pointer;
padding: 0 7px 0 3px; padding: 0 7px 0 3px;
height: 35px; height: 35px;
margin: 5px 0; margin: 5px 20px 5px 0px;
border-radius: 0 5px 5px 0; border-radius: 0 5px 5px 0;
display: flex; display: flex;
align-items: center; align-items: center;
@ -68,7 +69,7 @@
.budibase__nav-item.selected { .budibase__nav-item.selected {
color: var(--button-text); color: var(--button-text);
background: var(--background-button) !important; background: #fafafa !important;
} }
.budibase__nav-item:hover { .budibase__nav-item:hover {
@ -82,7 +83,7 @@
border: 1px solid #DBDBDB; border: 1px solid #DBDBDB;
text-align: left; text-align: left;
letter-spacing: 0.7px; letter-spacing: 0.7px;
color: #163057; color: #000333;
font-size: 16px; font-size: 16px;
padding-left: 5px; padding-left: 5px;
} }

View File

@ -2,6 +2,7 @@
export let disabled = false export let disabled = false
export let hidden = false export let hidden = false
export let primary = true export let primary = true
export let cancel = false
export let alert = false export let alert = false
export let warning = false export let warning = false
</script> </script>
@ -12,6 +13,7 @@
class:hidden class:hidden
class:primary class:primary
class:alert class:alert
class:cancel
class:warning class:warning
{disabled}> {disabled}>
<slot /> <slot />
@ -19,8 +21,8 @@
<style> <style>
.primary { .primary {
color: #0055ff; color: #ffffff;
background: rgb(54, 133, 249, 0.1); background: #0055ff;
} }
.alert { .alert {
@ -28,17 +30,23 @@
background: rgba(255, 0, 31, 0.1); background: rgba(255, 0, 31, 0.1);
} }
.cancel {
color: var(--secondary40);
background: none;
}
.button { .button {
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: 600;
border-radius: 5px; border-radius: 5px;
border: none; border: none;
min-width: 120px; padding: 10px 20px;
height: 45px; height: 45px;
} }
.button:hover { .button:hover {
cursor: pointer; cursor: pointer;
font-weight: 700;
} }
.button:disabled { .button:disabled {

View File

@ -8,9 +8,8 @@
<style> <style>
.root { .root {
display: grid; display: flex;
grid-auto-flow: column; flex-direction: row;
grid-gap: 5px; justify-content: space-between;
width: 50%;
} }
</style> </style>

View File

@ -41,13 +41,26 @@
<h4 class="budibase__title--4">{title}</h4> <h4 class="budibase__title--4">{title}</h4>
</div> </div>
<div class="uk-modal-body"> <div class="uk-modal-body">
<slot>{body}</slot> <slot class="rows">{body}</slot>
</div> </div>
<div class="uk-modal-footer"> <div class="uk-modal-footer">
<ButtonGroup> <ButtonGroup>
<ActionButton cancel on:click={cancel}>{cancelText}</ActionButton>
<ActionButton primary on:click={ok}>{okText}</ActionButton> <ActionButton primary on:click={ok}>{okText}</ActionButton>
<ActionButton alert on:click={cancel}>{cancelText}</ActionButton>
</ButtonGroup> </ButtonGroup>
</div> </div>
</div> </div>
</div> </div>
<style>
.uk-modal-footer {
background: var(--lightslate);
}
.uk-modal-dialog {
width: 400px;
border-radius: 5px;
}
</style>

View File

@ -8,8 +8,6 @@
outline: none; outline: none;
border: none; border: none;
border-radius: 5px; border-radius: 5px;
background: rgba(249, 249, 249, 1);
min-width: 1.8rem; min-width: 1.8rem;
min-height: 1.8rem; min-height: 1.8rem;
padding-bottom: 10px; padding-bottom: 10px;
@ -20,6 +18,6 @@
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 700; font-weight: 700;
color: rgba(22, 48, 87, 1); color: var(--secondary100);
} }
</style> </style>

View File

@ -41,11 +41,11 @@
} }
select { select {
height: 35px; height: 40px;
display: block; display: block;
font-family: sans-serif; font-family: sans-serif;
font-weight: 500; font-weight: 400;
color: #163057; color: #000333;
padding: 0 2.6em 0em 1.4em; padding: 0 2.6em 0em 1.4em;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
@ -54,8 +54,7 @@
-moz-appearance: none; -moz-appearance: none;
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
background: #fff; background: var(--lightslate);
border: 1px solid #ccc;
} }
.arrow { .arrow {

View File

@ -41,6 +41,7 @@
display: flex; display: flex;
width: 100%; width: 100%;
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
box-sizing: border-box;
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;

View File

@ -1,21 +1,23 @@
@import "./budibase.css"; @import "./budibase.css";
:root { :root {
--primary100: #173157FF; --primary100: #0055ff;
--primary75: #454CA0BF; --primary80: rgba(0, 85, 255, 0.8);
--primary50: #454CA080; --primary60: #rgba(0, 85, 255, 0.6);
--primary25: #454CA040; --primary40: #rgba(0, 85, 255, 0.4);
--primary10: #454CA01A; --primary20: #rgba(0, 85, 255, 0.2);
--primary5: #454ca00c; --primary10: #rgba(0, 85, 255, 0.1);
--primarydark: #3F448A; --primary5: #rgba(0, 85, 255, 0.05);
--primarydark: #0044cc;
--secondary100:#828fa5; --secondary100:#000333;
--secondary75: #162B4DBF; --secondary80: rgba(0, 3, 51, 0.8);
--secondary50: #162B4D80; --secondary60: rgba(0, 3, 51, 0.6);
--secondary25: #162B4D40; --secondary40: rgba(0, 3, 51, 0.4);
--secondary10: #162B4D1A; --secondary20: rgba(0, 3, 51, 0.2);
--secondary5:#fff; --secondary10: rgba(0, 3, 51, 0.1);
--secondarydark: #3F448A; --secondary5: rgba(0, 3, 51, 0.05);
--secondarydark: #00021a;
--tertiary: #F2F5F7; --tertiary: #F2F5F7;
@ -35,8 +37,8 @@
--white: #FFFFFF; --white: #FFFFFF;
--darkslate: #1a202c; --darkslate: #1a202c;
--slate: #a0aec0; --slate: #d8d8d8;
--lightslate: #f7fafc; --lightslate: #f9f9f9;
--borderradius: 2px; --borderradius: 2px;
--borderradiusall: 2px 2px 2px 2px; --borderradiusall: 2px 2px 2px 2px;
@ -55,13 +57,13 @@
--quotation: var(--fontnormal) "italics" var(--darkslate) 16pt; --quotation: var(--fontnormal) "italics" var(--darkslate) 16pt;
--smallheavybodytext: var(--fontbold) "regular" var(--secondary100) 14pt; --smallheavybodytext: var(--fontbold) "regular" var(--secondary100) 14pt;
--background-button: #e6eeff; --background-button: #f9f9f9;
--button-text: #0055ff; --button-text: #0055ff;
} }
html, body { html, body {
font-family: var(--fontnormal); font-family: var(--fontnormal);
color: var(--secondary100); color: var(--secondary80);
padding: 0; padding: 0;
margin: 0; margin: 0;
height:100%; height:100%;
@ -83,7 +85,7 @@ h2 {
h3 { h3 {
font-family: var(--fontbold); font-family: var(--fontbold);
font-size: 24pt; font-size: 24pt;
color: var(--darkslate); color: var(--secondary60);
} }
h4 { h4 {

View File

@ -49,7 +49,7 @@
flex-direction: column; flex-direction: column;
max-height: 100%; max-height: 100%;
height: 100%; height: 100%;
background-color: var(--secondary5); background-color: var(--white);
} }
.nav-group-header { .nav-group-header {

View File

@ -162,38 +162,49 @@
.component-props-container { .component-props-container {
margin-top: 10px; margin-top: 10px;
flex: 1 1 auto; flex: 1 1 auto;
overflow-y: auto;
} }
ul { ul {
list-style: none; list-style: none;
display: flex; display: flex;
justify-content: space-between;
padding: 0; padding: 0;
} }
li { li {
margin-right: 20px;
background: none; background: none;
border-radius: 5px; border-radius: 3px;
width: 48px; width: 48px;
height: 48px; height: 48px;
} }
li button { li button {
width: 100%; width: 48px;
height: 100%; height: 48px;
background: none; background: none;
border: none; border: none;
border-radius: 5px; border-radius: 3px;
padding: 12px; padding: 7px;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
} }
li:nth-last-child(1) {
margin-right: 0px;
background: none;
border-radius: 3px;
width: 48px;
height: 48px;
}
.selected { .selected {
color: var(--button-text); color: var(--button-text);
background: var(--background-button) !important; background: #f9f9f9 !important;
width: 48px;
height: 48px;
} }
.button-indicator { .button-indicator {

View File

@ -239,12 +239,12 @@
position: relative; position: relative;
padding: 0 15px; padding: 0 15px;
cursor: pointer; cursor: pointer;
border: 1px solid #ebebeb; border: 1px solid #d8d8d8;
border-radius: 2px; border-radius: 2px;
margin: 5px 0; margin: 5px 0;
height: 40px; height: 40px;
box-sizing: border-box; box-sizing: border-box;
color: #163057; color: #000333;
display: flex; display: flex;
align-items: center; align-items: center;
flex: 1; flex: 1;
@ -256,11 +256,10 @@
} }
.component > .name { .component > .name {
color: #163057; color: #000333;
display: inline-block; display: inline-block;
font-size: 12px; font-size: 12px;
font-weight: bold; opacity: 0.8;
opacity: 0.6;
} }
ul { ul {
@ -279,12 +278,11 @@
background: #fafafa; background: #fafafa;
padding: 10px; padding: 10px;
border-radius: 2px; border-radius: 2px;
color: rgba(22, 48, 87, 0.6); color:var(--secondary80);
} }
.preset-menu > span { .preset-menu > span {
font-size: 12px; font-size: 12px;
font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
margin-top: 5px; margin-top: 5px;
} }

View File

@ -94,7 +94,7 @@
border-radius: 3px; border-radius: 3px;
height: 35px; height: 35px;
align-items: center; align-items: center;
font-weight: normal; font-weight: 400;
} }
.item button { .item button {

View File

@ -54,14 +54,15 @@
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 2rem 0; padding: 20px 0;
} }
.switcher { .switcher {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 25px; margin-bottom: 20px;
padding: 0 1.5rem; padding: 0 20px 20px;
border-bottom: 1px solid #d8d8d8;
} }
.switcher > button { .switcher > button {
@ -70,21 +71,21 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
cursor: pointer; cursor: pointer;
font-weight: 600; font-size: 14px;
font-size: 0.85rem; font-weight: 400;
text-transform: uppercase; text-transform: uppercase;
color: #999; color: var(--secondary60);
background-color: rgba(0, 0, 0, 0);
} }
.switcher > .selected { .switcher > .selected {
color: #333; color: var(--secondary100);
font-weight: 500;
} }
.panel { .panel {
flex: 1 1 auto; flex: 1 1 auto;
height: 0px; height: 0px;
overflow-y: auto; overflow-y: auto;
padding: 0 1.5rem 1.5rem 1.5rem; padding: 0 20px 40px 20px;
} }
</style> </style>

View File

@ -136,7 +136,8 @@
text-transform: uppercase; text-transform: uppercase;
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
color: #8997ab; color: #000333;
opacity: 0.6;
margin-bottom: 10px; margin-bottom: 10px;
} }
@ -144,16 +145,16 @@
text-transform: uppercase; text-transform: uppercase;
font-size: 10px; font-size: 10px;
font-weight: 700; font-weight: 700;
color: #163057; color: #000333;
opacity: 0.3; opacity: 0.4;
margin-bottom: 15px; margin-bottom: 15px;
} }
h5 { h5 {
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 400;
color: #163057; color: #000333;
opacity: 0.6; opacity: 0.8;
padding-top: 12px; padding-top: 12px;
margin-bottom: 0; margin-bottom: 0;
} }

View File

@ -51,26 +51,26 @@
<style> <style>
.selected { .selected {
color: var(--button-text); color: var(--button-text);
background: var(--background-button); background: #f9f9f9;
opacity: 1;
} }
button { button {
cursor: pointer; cursor: pointer;
outline: none; outline: none;
border: none; border: none;
border-radius: 5px; border-radius: 3px;
background: rgba(249, 249, 249, 1);
min-width: 1.6rem; min-width: 1.6rem;
min-height: 1.6rem; min-height: 1.6rem;
display: flex; display: flex;
justify-content: center; justify-content: space-between;
align-items: center; align-items: center;
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 500; font-weight: 500;
color: rgba(22, 48, 87, 1); color: #000333;
} }
.inputs { .inputs {

View File

@ -126,3 +126,36 @@
</ConfirmDialog> </ConfirmDialog>
<style>
.uk-margin {
display: flex;
flex-direction: column;
}
.uk-form-controls {
margin-left: 0 !important;
}
.uk-form-label {
padding-bottom: 10px;
font-weight: 500;
font-size: 16px;
color: var(--secondary80);
}
.uk-input {
height: 40px !important;
border-radius: 3px;
}
.uk-select {
height: 40px !important;
font-weight: 500px;
color: var(--secondary60);
border: 1px solid var(--slate);
border-radius: 3px;
}
</style>

View File

@ -46,7 +46,7 @@
.root { .root {
padding-bottom: 10px; padding-bottom: 10px;
font-size: 0.9rem; font-size: 0.9rem;
color: var(--secondary50); color: #000333;
font-weight: bold; font-weight: bold;
position: relative; position: relative;
padding-left: 1.8rem; padding-left: 1.8rem;
@ -79,6 +79,6 @@
.icon { .icon {
display: inline-block; display: inline-block;
width: 14px; width: 14px;
color: #333; color: #000333;
} }
</style> </style>

View File

@ -34,14 +34,15 @@
grid-template-rows: 1fr; grid-template-rows: 1fr;
grid-template-columns: 70px 1fr; grid-template-columns: 70px 1fr;
grid-gap: 10px; grid-gap: 10px;
align-items: baseline;
} }
h5 { h5 {
word-wrap: break-word; word-wrap: break-word;
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 400;
color: #163057; color: #000333;
opacity: 0.6; opacity: 0.8;
padding-top: 12px; padding-top: 12px;
margin-bottom: 0; margin-bottom: 0;
} }

View File

@ -91,9 +91,7 @@
cursor: pointer; cursor: pointer;
outline: none; outline: none;
border: none; border: none;
border-radius: 5px; border-radius: 3px;
background: rgba(249, 249, 249, 1);
font-size: 1.6rem; font-size: 1.6rem;
font-weight: 700; font-weight: 700;
color: rgba(22, 48, 87, 1); color: rgba(22, 48, 87, 1);

View File

@ -43,7 +43,9 @@
<div class="pages-list-container"> <div class="pages-list-container">
<div class="nav-header"> <div class="nav-header">
<span class="navigator-title">Navigator</span> <span class="navigator-title">Navigator</span>
<span class="components-nav-header">Pages</span> <div class="border-line" />
<span class="components-nav-page">Pages</span>
</div> </div>
<div class="nav-items-container"> <div class="nav-items-container">
@ -108,17 +110,28 @@
padding: 0; padding: 0;
} }
.root {
display: grid;
grid-template-columns: 275px 1fr 275px;
height: 100%;
width: 100%;
background: #fafafa;
}
@media only screen and (min-width: 1800px) {
.root { .root {
display: grid; display: grid;
grid-template-columns: 290px 1fr 350px; grid-template-columns: 300px 1fr 300px;
height: 100%; height: 100%;
width: 100%; width: 100%;
background: #fafafa; background: #fafafa;
} }
}
.ui-nav { .ui-nav {
grid-column: 1; grid-column: 1;
background-color: var(--secondary5); background-color: var(--white);
height: calc(100vh - 49px); height: calc(100vh - 49px);
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
@ -136,29 +149,34 @@
.components-pane { .components-pane {
grid-column: 3; grid-column: 3;
background-color: var(--secondary5); background-color: var(--white);
min-height: 0px; min-height: 0px;
overflow-y: hidden; overflow-y: hidden;
} }
.components-nav-header { .components-nav-page {
font-size: 0.75rem; font-size: 12px;
color: #999; color: #000333;
text-transform: uppercase; text-transform: uppercase;
margin-top: 1rem; padding-left: 20px;
font-weight: 500; margin-top: 20px;
font-weight: 700;
opacity: 0.6;
} }
.nav-group-header { .components-nav-header {
font-size: 0.9rem; font-size: 12px;
padding-left: 1rem; color: #000333;
text-transform: uppercase;
margin-top: 20px;
font-weight: 700;
opacity: 0.6;
} }
.nav-header { .nav-header {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-top: 1.5rem; margin-top: 20px;
padding: 0 1.8rem;
} }
.nav-items-container { .nav-items-container {
@ -167,7 +185,7 @@
.nav-group-header { .nav-group-header {
display: flex; display: flex;
padding: 1.5rem 0 0 1.8rem; padding: 0px 20px 0px 20px;
font-size: 0.9rem; font-size: 0.9rem;
font-weight: bold; font-weight: bold;
justify-content: space-between; justify-content: space-between;
@ -200,20 +218,20 @@
} }
.navigator-title { .navigator-title {
font-size: 14px;
color: var(--secondary100);
font-weight: 500;
text-transform: uppercase; text-transform: uppercase;
font-weight: 400; padding: 0 20px 20px 20px;
color: #999; line-height: 1rem !important;
font-size: 0.9rem;
} }
.border-line { .border-line {
border-bottom: 1px solid #ddd; border-bottom: 1px solid #d8d8d8;
margin-top: 1.5rem;
width: calc(100% + 1.5rem);
} }
.components-list-container { .components-list-container {
overflow: auto; overflow: auto;
padding: 0 30px 0 0; padding: 20px 0px 0 0;
} }
</style> </style>

View File

@ -0,0 +1,8 @@
import { setCleanupFunc } from "../transactions/setCleanupFunc"
export const cloneApp = (app, mergeWith) => {
const newApp = { ...app }
Object.assign(newApp, mergeWith)
setCleanupFunc(newApp)
return newApp
}

View File

@ -21,18 +21,25 @@ export const initialiseData = async (
applicationDefinition, applicationDefinition,
accessLevels accessLevels
) => { ) => {
if (!await datastore.exists(configFolder))
await datastore.createFolder(configFolder) await datastore.createFolder(configFolder)
if (!await datastore.exists(appDefinitionFile))
await datastore.createJson(appDefinitionFile, applicationDefinition) await datastore.createJson(appDefinitionFile, applicationDefinition)
await initialiseRootCollections(datastore, applicationDefinition.hierarchy) await initialiseRootCollections(datastore, applicationDefinition.hierarchy)
await initialiseRootIndexes(datastore, applicationDefinition.hierarchy) await initialiseRootIndexes(datastore, applicationDefinition.hierarchy)
if (!await datastore.exists(TRANSACTIONS_FOLDER))
await datastore.createFolder(TRANSACTIONS_FOLDER) await datastore.createFolder(TRANSACTIONS_FOLDER)
if (!await datastore.exists(AUTH_FOLDER))
await datastore.createFolder(AUTH_FOLDER) await datastore.createFolder(AUTH_FOLDER)
if (!await datastore.exists(USERS_LIST_FILE))
await datastore.createJson(USERS_LIST_FILE, []) await datastore.createJson(USERS_LIST_FILE, [])
if (!await datastore.exists(ACCESS_LEVELS_FILE))
await datastore.createJson( await datastore.createJson(
ACCESS_LEVELS_FILE, ACCESS_LEVELS_FILE,
accessLevels ? accessLevels : { version: 0, levels: [] } accessLevels ? accessLevels : { version: 0, levels: [] }
@ -64,6 +71,7 @@ const initialiseRootSingleRecords = async (datastore, hierarchy) => {
const singleRecords = $(flathierarchy, [filter(isSingleRecord)]) const singleRecords = $(flathierarchy, [filter(isSingleRecord)])
for (let record of singleRecords) { for (let record of singleRecords) {
if (await datastore.exists(record.nodeKey())) continue
await datastore.createFolder(record.nodeKey()) await datastore.createFolder(record.nodeKey())
const result = _getNew(record, "") const result = _getNew(record, "")
await _save(app, result) await _save(app, result)

View File

@ -7,7 +7,7 @@ import getActionsApi from "./actionsApi"
import { setupDatastore, createEventAggregator } from "./appInitialise" import { setupDatastore, createEventAggregator } from "./appInitialise"
import { initialiseActions } from "./actionsApi/initialise" import { initialiseActions } from "./actionsApi/initialise"
import { isSomething, crypto } from "./common" import { isSomething, crypto } from "./common"
import { cleanup } from "./transactions/cleanup" import { setCleanupFunc } from "./transactions/setCleanupFunc"
import { generateFullPermissions } from "./authApi/generateFullPermissions" import { generateFullPermissions } from "./authApi/generateFullPermissions"
import { getApplicationDefinition } from "./templateApi/getApplicationDefinition" import { getApplicationDefinition } from "./templateApi/getApplicationDefinition"
import common from "./common" import common from "./common"
@ -40,9 +40,7 @@ export const getAppApis = async (
const templateApi = getTemplateApi(app) const templateApi = getTemplateApi(app)
app.cleanupTransactions = isSomething(cleanupTransactions) setCleanupFunc(app, cleanupTransactions)
? cleanupTransactions
: async () => await cleanup(app)
app.getEpochTime = isSomething(getEpochTime) app.getEpochTime = isSomething(getEpochTime)
? getEpochTime ? getEpochTime

View File

@ -6,8 +6,10 @@ import {
getNode, getNode,
isIndex, isIndex,
isRecord, isRecord,
getActualKeyOfParent,
getAllowedRecordNodesForIndex, getAllowedRecordNodesForIndex,
fieldReversesReferenceToIndex, fieldReversesReferenceToIndex,
isTopLevelIndex,
} from "../templateApi/hierarchy" } from "../templateApi/hierarchy"
import { joinKey, apiWrapper, events, $ } from "../common" import { joinKey, apiWrapper, events, $ } from "../common"
import { import {
@ -16,6 +18,8 @@ import {
} from "../transactions/create" } from "../transactions/create"
import { permission } from "../authApi/permissions" import { permission } from "../authApi/permissions"
import { BadRequestError } from "../common/errors" import { BadRequestError } from "../common/errors"
import { initialiseIndex } from "../indexing/initialiseIndex"
import { getRecordInfo } from "../recordApi/recordInfo"
/** rebuilds an index /** rebuilds an index
* @param {object} app - the application container * @param {object} app - the application container
@ -32,7 +36,7 @@ export const buildIndex = app => async indexNodeKey =>
indexNodeKey indexNodeKey
) )
const _buildIndex = async (app, indexNodeKey) => { export const _buildIndex = async (app, indexNodeKey) => {
const indexNode = getNode(app.hierarchy, indexNodeKey) const indexNode = getNode(app.hierarchy, indexNodeKey)
await createBuildIndexFolder(app.datastore, indexNodeKey) await createBuildIndexFolder(app.datastore, indexNodeKey)
@ -89,12 +93,6 @@ const buildReverseReferenceIndex = async (app, indexNode) => {
} }
} }
/*
const getAllowedParentCollectionNodes = (hierarchy, indexNode) => $(getAllowedRecordNodesForIndex(hierarchy, indexNode), [
map(n => n.parent()),
]);
*/
const buildHeirarchalIndex = async (app, indexNode) => { const buildHeirarchalIndex = async (app, indexNode) => {
let recordCount = 0 let recordCount = 0
@ -139,77 +137,8 @@ const buildHeirarchalIndex = async (app, indexNode) => {
return recordCount return recordCount
} }
// const chooseChildRecordNodeByKey = (collectionNode, recordId) => find(c => recordId.startsWith(c.nodeId))(collectionNode.children);
const recordNodeApplies = indexNode => recordNode => const recordNodeApplies = indexNode => recordNode =>
includes(recordNode.nodeId)(indexNode.allowedRecordNodeIds) includes(recordNode.nodeId)(indexNode.allowedRecordNodeIds)
/*
const hasApplicableDecendant = (hierarchy, ancestorNode, indexNode) => $(hierarchy, [
getFlattenedHierarchy,
filter(
allTrue(
isRecord,
isDecendant(ancestorNode),
recordNodeApplies(indexNode),
),
),
]);
*/
/*
const applyAllDecendantRecords = async (app, collection_Key_or_NodeKey,
indexNode, indexKey, currentIndexedData,
currentIndexedDataKey, recordCount = 0) => {
const collectionNode = getCollectionNodeByKeyOrNodeKey(
app.hierarchy,
collection_Key_or_NodeKey,
);
const allIdsIterator = await getAllIdsIterator(app)(collection_Key_or_NodeKey);
const createTransactionsForIds = async (collectionKey, allIds) => {
for (const recordId of allIds) {
const recordKey = joinKey(collectionKey, recordId);
const recordNode = chooseChildRecordNodeByKey(
collectionNode,
recordId,
);
if (recordNodeApplies(indexNode)(recordNode)) {
await transactionForBuildIndex(
app, indexNode.nodeKey(),
recordKey, recordCount,
);
recordCount++;
}
if (hasApplicableDecendant(app.hierarchy, recordNode, indexNode)) {
for (const childCollectionNode of recordNode.children) {
recordCount = await applyAllDecendantRecords(
app,
joinKey(recordKey, childCollectionNode.collectionName),
indexNode, indexKey, currentIndexedData,
currentIndexedDataKey, recordCount,
);
}
}
}
};
let allIds = await allIdsIterator();
while (allIds.done === false) {
await createTransactionsForIds(
allIds.result.collectionKey,
allIds.result.ids,
);
allIds = await allIdsIterator();
}
return recordCount;
};
*/
export default buildIndex export default buildIndex

View File

@ -33,7 +33,7 @@ const defaultOptions = {
searchPhrase: null, searchPhrase: null,
} }
const _listItems = async (app, indexKey, options = defaultOptions) => { export const _listItems = async (app, indexKey, options = defaultOptions) => {
const { searchPhrase, rangeStartParams, rangeEndParams } = $({}, [ const { searchPhrase, rangeStartParams, rangeEndParams } = $({}, [
merge(options), merge(options),
merge(defaultOptions), merge(defaultOptions),

View File

@ -2,6 +2,7 @@ import { flatten, orderBy, filter, isUndefined } from "lodash/fp"
import { import {
getFlattenedHierarchy, getFlattenedHierarchy,
getCollectionNodeByKeyOrNodeKey, getCollectionNodeByKeyOrNodeKey,
getNodeByKeyOrNodeKey,
isCollectionRecord, isCollectionRecord,
isAncestor, isAncestor,
} from "../templateApi/hierarchy" } from "../templateApi/hierarchy"
@ -60,7 +61,7 @@ export const getAllIdsIterator = app => async collection_Key_or_NodeKey => {
const recordNode = getCollectionNodeByKeyOrNodeKey( const recordNode = getCollectionNodeByKeyOrNodeKey(
app.hierarchy, app.hierarchy,
collection_Key_or_NodeKey collection_Key_or_NodeKey
) ) || getNodeByKeyOrNodeKey(app.hierarchy, collection_Key_or_NodeKey)
const getAllIdsIteratorForCollectionKey = async ( const getAllIdsIteratorForCollectionKey = async (
recordNode, recordNode,

View File

@ -9,11 +9,19 @@ import {
export const initialiseIndex = async (datastore, dir, index) => { export const initialiseIndex = async (datastore, dir, index) => {
const indexDir = joinKey(dir, index.name) const indexDir = joinKey(dir, index.name)
let newDir = false
if (!await datastore.exists(indexDir)) {
await datastore.createFolder(indexDir) await datastore.createFolder(indexDir)
newDir = true
}
if (isShardedIndex(index)) { if (isShardedIndex(index)) {
await datastore.createFile(getShardMapKey(indexDir), "[]") const shardFile = getShardMapKey(indexDir)
if (newDir || !await datastore.exists(shardFile))
await datastore.createFile(shardFile, "[]")
} else { } else {
await createIndexFile(datastore, getUnshardedIndexDataKey(indexDir), index) const indexFile = getUnshardedIndexDataKey(indexDir)
if (newDir || !await datastore.exists(indexFile))
await createIndexFile(datastore, indexFile, index)
} }
} }

View File

@ -3,6 +3,9 @@ import { promiseReadableStream } from "./promiseReadableStream"
import { createIndexFile } from "./sharding" import { createIndexFile } from "./sharding"
import { generateSchema } from "./indexSchemaCreator" import { generateSchema } from "./indexSchemaCreator"
import { getIndexReader, CONTINUE_READING_RECORDS } from "./serializer" import { getIndexReader, CONTINUE_READING_RECORDS } from "./serializer"
import { getAllowedRecordNodesForIndex, getRecordNodeId } from "../templateApi/hierarchy"
import { $ } from "../common"
import { filter, includes, find } from "lodash/fp"
export const readIndex = async ( export const readIndex = async (
hierarchy, hierarchy,
@ -11,8 +14,10 @@ export const readIndex = async (
indexedDataKey indexedDataKey
) => { ) => {
const records = [] const records = []
const getType = typeLoader(index, hierarchy)
const doRead = iterateIndex( const doRead = iterateIndex(
async item => { async item => {
item.type = getType(item.key)
records.push(item) records.push(item)
return CONTINUE_READING_RECORDS return CONTINUE_READING_RECORDS
}, },
@ -31,8 +36,10 @@ export const searchIndex = async (
) => { ) => {
const records = [] const records = []
const schema = generateSchema(hierarchy, index) const schema = generateSchema(hierarchy, index)
const getType = typeLoader(index, hierarchy)
const doRead = iterateIndex( const doRead = iterateIndex(
async item => { async item => {
item.type = getType(item.key)
const idx = lunr(function() { const idx = lunr(function() {
this.ref("key") this.ref("key")
for (const field of schema) { for (const field of schema) {
@ -76,3 +83,8 @@ export const iterateIndex = (onGetItem, getFinalResult) => async (
return [] return []
} }
} }
const typeLoader = (index, hierarchy) => {
const allowedNodes = getAllowedRecordNodesForIndex(hierarchy, index)
return key => find(n => getRecordNodeId(key) === n.nodeId)(allowedNodes).name
}

View File

@ -0,0 +1,81 @@
import { isString, flatten, map, filter } from "lodash/fp"
import { initialiseChildCollections } from "../collectionApi/initialise"
import { _loadFromInfo } from "./load"
import { $ } from "../common"
import {
getFlattenedHierarchy,
isRecord,
getNode,
isTopLevelRecord,
fieldReversesReferenceToNode,
} from "../templateApi/hierarchy"
import { initialiseIndex } from "../indexing/initialiseIndex"
import { getRecordInfo } from "./recordInfo"
export const initialiseChildren = async (app, recordInfoOrKey) => {
const recordInfo = isString(recordInfoOrKey)
? getRecordInfo(app.hierarchy, recordInfoOrKey)
: recordInfoOrKey
await initialiseReverseReferenceIndexes(app, recordInfo)
await initialiseAncestorIndexes(app, recordInfo)
await initialiseChildCollections(app, recordInfo)
}
export const initialiseChildrenForNode = async (app, recordNode) => {
if (isTopLevelRecord(recordNode)) {
await initialiseChildren(
app, recordNode.nodeKey())
return
}
const iterate = await getAllIdsIterator(app)(recordNode.parent().nodeKey())
let iterateResult = await iterate()
while (!iterateResult.done) {
const { result } = iterateResult
for (const id of result.ids) {
const initialisingRecordKey = joinKey(
result.collectionKey, id)
await initialiseChildren(app, initialisingRecordKey)
}
iterateResult = await iterate()
}
}
const initialiseAncestorIndexes = async (app, recordInfo) => {
for (const index of recordInfo.recordNode.indexes) {
const indexKey = recordInfo.child(index.name)
if (!(await app.datastore.exists(indexKey))) {
await initialiseIndex(app.datastore, recordInfo.dir, index)
}
}
}
const initialiseReverseReferenceIndexes = async (app, recordInfo) => {
const indexNodes = $(
fieldsThatReferenceThisRecord(app, recordInfo.recordNode),
[
map(f =>
$(f.typeOptions.reverseIndexNodeKeys, [
map(n => getNode(app.hierarchy, n)),
])
),
flatten,
]
)
for (const indexNode of indexNodes) {
await initialiseIndex(app.datastore, recordInfo.dir, indexNode)
}
}
const fieldsThatReferenceThisRecord = (app, recordNode) =>
$(app.hierarchy, [
getFlattenedHierarchy,
filter(isRecord),
map(n => n.fields),
flatten,
filter(fieldReversesReferenceToNode(recordNode)),
])

View File

@ -1,5 +1,4 @@
import { cloneDeep, take, takeRight, flatten, map, filter } from "lodash/fp" import { cloneDeep, take, takeRight, flatten, map, filter } from "lodash/fp"
import { initialiseChildCollections } from "../collectionApi/initialise"
import { validate } from "./validate" import { validate } from "./validate"
import { _loadFromInfo } from "./load" import { _loadFromInfo } from "./load"
import { apiWrapper, events, $, joinKey } from "../common" import { apiWrapper, events, $, joinKey } from "../common"
@ -17,6 +16,7 @@ import { permission } from "../authApi/permissions"
import { initialiseIndex } from "../indexing/initialiseIndex" import { initialiseIndex } from "../indexing/initialiseIndex"
import { BadRequestError } from "../common/errors" import { BadRequestError } from "../common/errors"
import { getRecordInfo } from "./recordInfo" import { getRecordInfo } from "./recordInfo"
import { initialiseChildren } from "./initialiseChildren"
export const save = app => async (record, context) => export const save = app => async (record, context) =>
apiWrapper( apiWrapper(
@ -59,9 +59,7 @@ export const _save = async (app, record, context, skipValidation = false) => {
await createRecordFolderPath(app.datastore, pathInfo) await createRecordFolderPath(app.datastore, pathInfo)
await app.datastore.createFolder(files) await app.datastore.createFolder(files)
await app.datastore.createJson(recordJson, recordClone) await app.datastore.createJson(recordJson, recordClone)
await initialiseReverseReferenceIndexes(app, recordInfo) await initialiseChildren(app, recordInfo)
await initialiseAncestorIndexes(app, recordInfo)
await initialiseChildCollections(app, recordInfo)
await app.publish(events.recordApi.save.onRecordCreated, { await app.publish(events.recordApi.save.onRecordCreated, {
record: recordClone, record: recordClone,
}) })
@ -87,42 +85,6 @@ export const _save = async (app, record, context, skipValidation = false) => {
return returnedClone return returnedClone
} }
const initialiseAncestorIndexes = async (app, recordInfo) => {
for (const index of recordInfo.recordNode.indexes) {
const indexKey = recordInfo.child(index.name)
if (!(await app.datastore.exists(indexKey))) {
await initialiseIndex(app.datastore, recordInfo.dir, index)
}
}
}
const initialiseReverseReferenceIndexes = async (app, recordInfo) => {
const indexNodes = $(
fieldsThatReferenceThisRecord(app, recordInfo.recordNode),
[
map(f =>
$(f.typeOptions.reverseIndexNodeKeys, [
map(n => getNode(app.hierarchy, n)),
])
),
flatten,
]
)
for (const indexNode of indexNodes) {
await initialiseIndex(app.datastore, recordInfo.dir, indexNode)
}
}
const fieldsThatReferenceThisRecord = (app, recordNode) =>
$(app.hierarchy, [
getFlattenedHierarchy,
filter(isRecord),
map(n => n.fields),
flatten,
filter(fieldReversesReferenceToNode(recordNode)),
])
const createRecordFolderPath = async (datastore, pathInfo) => { const createRecordFolderPath = async (datastore, pathInfo) => {
const recursiveCreateFolder = async ( const recursiveCreateFolder = async (
subdirs, subdirs,

View File

@ -0,0 +1,52 @@
import {
findRoot,
getFlattenedHierarchy,
fieldReversesReferenceToIndex,
isRecord
} from "./hierarchy"
import { $ } from "../common"
import { map, filter, reduce } from "lodash/fp"
export const canDeleteIndex = indexNode => {
const flatHierarchy = $(indexNode, [
findRoot,
getFlattenedHierarchy
])
const reverseIndexes = $(flatHierarchy,[
filter(isRecord),
reduce((obj, r) => {
for (let field of r.fields) {
if (fieldReversesReferenceToIndex(indexNode)(field)) {
obj.push({ ...field, record:r })
}
}
return obj
},[]),
map(f => `field ${f.name} on record ${f.record.name} uses this index as a reference`)
])
const lookupIndexes = $(flatHierarchy,[
filter(isRecord),
reduce((obj, r) => {
for (let field of r.fields) {
if (field.type === "reference"
&& field.typeOptions.indexNodeKey === indexNode.nodeKey()) {
obj.push({ ...field, record:r })
}
}
return obj
},[]),
map(f => `field ${f.name} on record ${f.record.name} uses this index as a lookup`)
])
const errors = [
...reverseIndexes,
...lookupIndexes
]
return {
canDelete: errors.length === 0,
errors
}
}

View File

@ -0,0 +1,44 @@
import {
findRoot,
getFlattenedHierarchy,
fieldReversesReferenceToIndex,
isRecord,
isAncestorIndex,
isAncestor
} from "./hierarchy"
import { $ } from "../common"
import { map, filter, includes } from "lodash/fp"
export const canDeleteRecord = recordNode => {
const flatHierarchy = $(recordNode, [
findRoot,
getFlattenedHierarchy
])
const ancestors = $(flatHierarchy, [
filter(isAncestor(recordNode))
])
const belongsToAncestor = i =>
ancestors.includes(i.parent())
const errorsForNode = node => {
const errorsThisNode = $(flatHierarchy, [
filter(i => isAncestorIndex(i)
&& belongsToAncestor(i)
&& includes(node.nodeId)(i.allowedRecordNodeIds)),
map(i => `index ${i.name} indexes this record. Please remove the record from allowedRecordIds, or delete the index`)
])
for (let child of node.children) {
for (let err of errorsForNode(child)) {
errorsThisNode.push(err)
}
}
return errorsThisNode
}
return errorsForNode(recordNode)
}

View File

@ -0,0 +1,34 @@
import { getAllIdsIterator } from "../indexing/allIds"
import { getRecordInfo } from "../recordApi/recordInfo"
import { isTopLevelIndex, getParentKey, getLastPartInKey } from "./hierarchy"
import { safeKey, joinKey } from "../common"
export const deleteAllIndexFilesForNode = async (app, indexNode) => {
if (isTopLevelIndex(indexNode)) {
await app.datastore.deleteFolder(indexNode.nodeKey())
return
}
const iterate = await getAllIdsIterator(app)(indexNode.parent().nodeKey())
let iterateResult = await iterate()
while (!iterateResult.done) {
const { result } = iterateResult
for (const id of result.ids) {
const deletingIndexKey = joinKey(
result.collectionKey, id, indexNode.name)
await deleteIndexFolder(app, deletingIndexKey)
}
iterateResult = await iterate()
}
}
const deleteIndexFolder = async (app, indexKey) => {
indexKey = safeKey(indexKey)
const indexName = getLastPartInKey(indexKey)
const parentRecordKey = getParentKey(indexKey)
const recordInfo = getRecordInfo(app.hierarchy, parentRecordKey)
await app.datastore.deleteFolder(
joinKey(recordInfo.dir, indexName))
}

View File

@ -0,0 +1,32 @@
import { getAllIdsIterator } from "../indexing/allIds"
import { getCollectionDir } from "../recordApi/recordInfo"
import { isTopLevelRecord, getCollectionKey } from "./hierarchy"
import { safeKey, joinKey } from "../common"
export const deleteAllRecordsForNode = async (app, recordNode) => {
if (isTopLevelRecord(recordNode)) {
await deleteRecordCollection(
app, recordNode.collectionName)
return
}
const iterate = await getAllIdsIterator(app)(recordNode.parent().nodeKey())
let iterateResult = await iterate()
while (!iterateResult.done) {
const { result } = iterateResult
for (const id of result.ids) {
const deletingCollectionKey = joinKey(
result.collectionKey, id, recordNode.collectionName)
await deleteRecordCollection(app, deletingCollectionKey)
}
iterateResult = await iterate()
}
}
const deleteRecordCollection = async (app, collectionKey) => {
collectionKey = safeKey(collectionKey)
await app.datastore.deleteFolder(
getCollectionDir(app.hierarchy, collectionKey))
}

View File

@ -1,6 +1,6 @@
import { getFlattenedHierarchy, isRecord, isIndex, isAncestor } from "./hierarchy" import { getFlattenedHierarchy, isRecord, isIndex, isAncestor } from "./hierarchy"
import { $, none } from "../common" import { $, none } from "../common"
import { map, filter, some, find } from "lodash/fp" import { map, filter, some, find, difference } from "lodash/fp"
export const HierarchyChangeTypes = { export const HierarchyChangeTypes = {
recordCreated: "Record Created", recordCreated: "Record Created",
@ -10,7 +10,7 @@ export const HierarchyChangeTypes = {
recordEstimatedRecordTypeChanged: "Record's Estimated Record Count Changed", recordEstimatedRecordTypeChanged: "Record's Estimated Record Count Changed",
indexCreated: "Index Created", indexCreated: "Index Created",
indexDeleted: "Index Deleted", indexDeleted: "Index Deleted",
indexChanged: "index Changed", indexChanged: "Index Changed",
} }
export const diffHierarchy = (oldHierarchy, newHierarchy) => { export const diffHierarchy = (oldHierarchy, newHierarchy) => {
@ -123,7 +123,7 @@ const findDeletedIndexes = (oldHierarchyFlat, newHierarchyFlat, deletedRecords)
const findUpdatedIndexes = (oldHierarchyFlat, newHierarchyFlat) => const findUpdatedIndexes = (oldHierarchyFlat, newHierarchyFlat) =>
$(oldHierarchyFlat, [ $(oldHierarchyFlat, [
filter(isRecord), filter(isIndex),
filter(nodeExistsIn(newHierarchyFlat)), filter(nodeExistsIn(newHierarchyFlat)),
filter(nodeChanged(newHierarchyFlat, indexHasChanged)), filter(nodeChanged(newHierarchyFlat, indexHasChanged)),
map(n => changeItem( map(n => changeItem(
@ -150,6 +150,7 @@ const indexHasChanged = (_new, old) =>
_new.map !== old.map _new.map !== old.map
|| _new.filter !== old.filter || _new.filter !== old.filter
|| _new.getShardName !== old.getShardName || _new.getShardName !== old.getShardName
|| difference(_new.allowedRecordNodeIds)(old.allowedRecordNodeIds).length > 0
const isFieldSame = f1 => f2 => const isFieldSame = f1 => f2 =>
f1.name === f2.name && f1.type === f2.type f1.name === f2.name && f1.type === f2.type

View File

@ -191,6 +191,22 @@ export const getAllowedRecordNodesForIndex = (appHierarchy, indexNode) => {
} }
} }
export const getDependantIndexes = (hierarchy, recordNode) => {
const allIndexes = $(hierarchy, [ getFlattenedHierarchy, filter(isIndex)])
const allowedAncestors = $(allIndexes, [
filter(isAncestorIndex),
filter(i => recordNodeIsAllowed(i)(recordNode)),
])
const allowedReference = $(allIndexes, [
filter(isReferenceIndex),
filter(i => some(fieldReversesReferenceToIndex(i))(recordNode.fields))
])
return [...allowedAncestors, ...allowedReference]
}
export const getNodeFromNodeKeyHash = hierarchy => hash => export const getNodeFromNodeKeyHash = hierarchy => hash =>
$(hierarchy, [ $(hierarchy, [
getFlattenedHierarchy, getFlattenedHierarchy,
@ -206,13 +222,19 @@ export const isaggregateGroup = node =>
export const isShardedIndex = node => export const isShardedIndex = node =>
isIndex(node) && isNonEmptyString(node.getShardName) isIndex(node) && isNonEmptyString(node.getShardName)
export const isRoot = node => isSomething(node) && node.isRoot() export const isRoot = node => isSomething(node) && node.isRoot()
export const findRoot = node => isRoot(node) ? node : findRoot(node.parent())
export const isDecendantOfARecord = hasMatchingAncestor(isRecord) export const isDecendantOfARecord = hasMatchingAncestor(isRecord)
export const isGlobalIndex = node => isIndex(node) && isRoot(node.parent()) export const isGlobalIndex = node => isIndex(node) && isRoot(node.parent())
export const isReferenceIndex = node => export const isReferenceIndex = node =>
isIndex(node) && node.indexType === indexTypes.reference isIndex(node) && node.indexType === indexTypes.reference
export const isAncestorIndex = node => export const isAncestorIndex = node =>
isIndex(node) && node.indexType === indexTypes.ancestor isIndex(node) && node.indexType === indexTypes.ancestor
export const isTopLevelRecord = node => isRoot(node.parent()) && isRecord(node)
export const isTopLevelIndex = node => isRoot(node.parent()) && isIndex(node)
export const getCollectionKey = recordKey => $(recordKey, [
splitKey,
parts => joinKey(parts.slice(0, parts.length - 1))
])
export const fieldReversesReferenceToNode = node => field => export const fieldReversesReferenceToNode = node => field =>
field.type === "reference" && field.type === "reference" &&
intersection(field.typeOptions.reverseIndexNodeKeys)( intersection(field.typeOptions.reverseIndexNodeKeys)(

View File

@ -28,6 +28,7 @@ import { saveApplicationHierarchy } from "./saveApplicationHierarchy"
import { saveActionsAndTriggers } from "./saveActionsAndTriggers" import { saveActionsAndTriggers } from "./saveActionsAndTriggers"
import { all } from "../types" import { all } from "../types"
import { getBehaviourSources } from "./getBehaviourSources" import { getBehaviourSources } from "./getBehaviourSources"
import { upgradeData } from "./upgradeData"
const api = app => ({ const api = app => ({
getApplicationDefinition: getApplicationDefinition(app.datastore), getApplicationDefinition: getApplicationDefinition(app.datastore),
@ -57,6 +58,7 @@ const api = app => ({
validateNode, validateNode,
validateAll, validateAll,
validateTriggers, validateTriggers,
upgradeData: upgradeData(app)
}) })
export const getTemplateApi = app => api(app) export const getTemplateApi = app => api(app)

View File

@ -0,0 +1,27 @@
import { getAllIdsIterator } from "../indexing/allIds"
import { getRecordInfo } from "../recordApi/recordInfo"
import { isTopLevelIndex } from "./hierarchy"
import { joinKey } from "../common"
import { initialiseIndex } from "../indexing/initialiseIndex"
export const initialiseNewIndex = async (app, indexNode) => {
if (isTopLevelIndex(indexNode)) {
await initialiseIndex(app.datastore, "/", indexNode)
return
}
const iterate = await getAllIdsIterator(app)(indexNode.parent().nodeKey())
let iterateResult = await iterate()
while (!iterateResult.done) {
const { result } = iterateResult
for (const id of result.ids) {
const recordKey = joinKey(result.collectionKey, id)
await initialiseIndex(
app.datastore,
getRecordInfo(app.hierarchy, recordKey).dir,
indexNode)
}
iterateResult = await iterate()
}
}

View File

@ -1,17 +1,194 @@
/* import { diffHierarchy, HierarchyChangeTypes } from "./diffHierarchy"
const changeActions = { import { $, switchCase } from "../common"
rebuildIndex: indexNodeKey => ({ import {
type: "rebuildIndex", differenceBy,
indexNodeKey, isEqual,
}), some,
reshardRecords: recordNodeKey => ({ map,
type: "reshardRecords", filter,
recordNodeKey, uniqBy,
}), flatten
deleteRecords: recordNodeKey => ({ } from "lodash/fp"
type: "reshardRecords", import {
recordNodeKey, findRoot,
}), getDependantIndexes,
renameRecord isTopLevelRecord,
isAncestorIndex
} from "./hierarchy"
import { generateSchema } from "../indexing/indexSchemaCreator"
import { _buildIndex } from "../indexApi/buildIndex"
import { constructHierarchy } from "./createNodes"
import { deleteAllRecordsForNode } from "./deleteAllRecordsForNode"
import { deleteAllIndexFilesForNode } from "./deleteAllIndexFilesForNode"
import { cloneApp } from "../appInitialise/cloneApp"
import { initialiseData } from "../appInitialise/initialiseData"
import { initialiseChildrenForNode } from "../recordApi/initialiseChildren"
import { initialiseNewIndex } from "./initialiseNewIndex"
import { saveApplicationHierarchy } from "../templateApi/saveApplicationHierarchy"
export const upgradeData = app => async newHierarchy => {
const diff = diffHierarchy(app.hierarchy, newHierarchy)
const changeActions = gatherChangeActions(diff)
if (changeActions.length === 0) return
newHierarchy = constructHierarchy(newHierarchy)
const newApp = newHierarchy && cloneApp(app, {
hierarchy: newHierarchy
})
await doUpgrade(app, newApp, changeActions)
await saveApplicationHierarchy(newApp)(newHierarchy)
}
const gatherChangeActions = (diff) =>
$(diff, [
map(actionForChange),
flatten,
uniqBy(a => a.compareKey)
])
const doUpgrade = async (oldApp, newApp, changeActions) => {
for(let action of changeActions) {
await action.run(oldApp, newApp, action.diff)
}
}
const actionForChange = diff =>
switchCase(
[isChangeType(HierarchyChangeTypes.recordCreated), recordCreatedAction],
[isChangeType(HierarchyChangeTypes.recordDeleted), deleteRecordsAction],
[
isChangeType(HierarchyChangeTypes.recordFieldsChanged),
rebuildAffectedIndexesAction
],
[isChangeType(HierarchyChangeTypes.recordRenamed), renameRecordAction],
[
isChangeType(HierarchyChangeTypes.recordEstimatedRecordTypeChanged),
reshardRecordsAction
],
[isChangeType(HierarchyChangeTypes.indexCreated), newIndexAction],
[isChangeType(HierarchyChangeTypes.indexDeleted), deleteIndexAction],
[isChangeType(HierarchyChangeTypes.indexChanged), rebuildIndexAction],
)(diff)
const isChangeType = changeType => change =>
change.type === changeType
const action = (diff, compareKey, run) => ({
diff,
compareKey,
run,
})
const reshardRecordsAction = diff =>
[action(diff, `reshardRecords-${diff.oldNode.nodeKey()}`, runReshardRecords)]
const rebuildIndexAction = diff =>
[action(diff, `rebuildIndex-${diff.newNode.nodeKey()}`, runRebuildIndex)]
const newIndexAction = diff => {
if (isAncestorIndex(diff.newNode)) {
return [action(diff, `rebuildIndex-${diff.newNode.nodeKey()}`, runRebuildIndex)]
} else {
return [action(diff, `newIndex-${diff.newNode.nodeKey()}`, runNewIndex)]
}
}
const deleteIndexAction = diff =>
[action(diff, `deleteIndex-${diff.oldNode.nodeKey()}`, runDeleteIndex)]
const deleteRecordsAction = diff =>
[action(diff, `deleteRecords-${diff.oldNode.nodeKey()}`, runDeleteRecords)]
const renameRecordAction = diff =>
[action(diff, `renameRecords-${diff.oldNode.nodeKey()}`, runRenameRecord)]
const recordCreatedAction = diff => {
if (isTopLevelRecord(diff.newNode)) {
return [action(diff, `initialiseRoot`, runInitialiseRoot)]
}
return [action(diff, `initialiseChildRecord-${diff.newNode.nodeKey()}`, runInitialiseChildRecord)]
}
const rebuildAffectedIndexesAction = diff =>{
const newHierarchy = findRoot(diff.newNode)
const oldHierarchy = findRoot(diff.oldNode)
const indexes = getDependantIndexes(newHierarchy, diff.newNode)
const changedFields = (() => {
const addedFields = differenceBy(f => f.name)
(diff.oldNode.fields)
(diff.newNode.fields)
const removedFields = differenceBy(f => f.name)
(diff.newNode.fields)
(diff.oldNode.fields)
return map(f => f.name)([...addedFields, ...removedFields])
})()
const isIndexAffected = i => {
if (!isEqual(
generateSchema(oldHierarchy, i),
generateSchema(newHierarchy, i))) return true
if (some(f => indexes.filter.indexOf(`record.${f}`) > -1)(changedFields))
return true
if (some(f => indexes.getShardName.indexOf(`record.${f}`) > -1)(changedFields))
return true
return false
}
return $(indexes, [
filter(isIndexAffected),
map(i => action({ newNode:i }, `rebuildIndex-${i.nodeKey()}`, runRebuildIndex))
])
}
const runReshardRecords = async change => {
throw new Error("Resharding of records is not supported yet")
}
const runRebuildIndex = async (_, newApp, diff) => {
await _buildIndex(newApp, diff.newNode.nodeKey())
}
const runDeleteIndex = async (oldApp, _, diff) => {
await deleteAllIndexFilesForNode(oldApp, diff.oldNode)
}
const runDeleteRecords = async (oldApp, _, diff) => {
await deleteAllRecordsForNode(oldApp, diff.oldNode)
}
const runNewIndex = async (_, newApp, diff) => {
await initialiseNewIndex(newApp, diff.newNode)
}
const runRenameRecord = change => {
/*
Going to disllow this in the builder. once a collection key is set... its done
*/
}
const runInitialiseRoot = async (_, newApp) => {
await initialiseData(newApp.datastore, newApp)
}
const runInitialiseChildRecord = async (_, newApp, diff) => {
await initialiseChildrenForNode(newApp.datastore, diff.newNode)
} }
*/

View File

@ -14,8 +14,10 @@ export const cleanup = async app => {
const lock = await getTransactionLock(app) const lock = await getTransactionLock(app)
if (isNolock(lock)) return if (isNolock(lock)) return
try { const _cleanupBatch = async () => {
let processed = 0
const transactions = await retrieve(app) const transactions = await retrieve(app)
let i = 1
if (transactions.length > 0) { if (transactions.length > 0) {
await executeTransactions(app)(transactions) await executeTransactions(app)(transactions)
@ -34,10 +36,21 @@ export const cleanup = async app => {
]) ])
await Promise.all(deleteFiles) await Promise.all(deleteFiles)
processed = transactions.length
}
return processed
}
try {
let count = -1
while (count !== 0) {
count = await _cleanupBatch()
} }
} finally { } finally {
await releaseLock(app, lock) await releaseLock(app, lock)
} }
} }
const getTransactionLock = async app => const getTransactionLock = async app =>

View File

@ -1,6 +1,7 @@
import { import {
filter, filter,
map, map,
reduce,
isUndefined, isUndefined,
includes, includes,
flatten, flatten,
@ -10,6 +11,7 @@ import {
keys, keys,
differenceBy, differenceBy,
difference, difference,
some,
} from "lodash/fp" } from "lodash/fp"
import { union } from "lodash" import { union } from "lodash"
import { import {
@ -38,14 +40,22 @@ import {
fieldReversesReferenceToIndex, fieldReversesReferenceToIndex,
isReferenceIndex, isReferenceIndex,
getExactNodeForKey, getExactNodeForKey,
getParentKey
} from "../templateApi/hierarchy" } from "../templateApi/hierarchy"
import { getRecordInfo } from "../recordApi/recordInfo" import { getRecordInfo } from "../recordApi/recordInfo"
import { getIndexDir } from "../indexApi/getIndexDir" import { getIndexDir } from "../indexApi/getIndexDir"
import { initialiseIndex } from "../indexing/initialiseIndex"
export const executeTransactions = app => async transactions => { export const executeTransactions = app => async transactions => {
const recordsByShard = mappedRecordsByIndexShard(app.hierarchy, transactions) const recordsByShard = mappedRecordsByIndexShard(app.hierarchy, transactions)
for (const shard of keys(recordsByShard)) { for (const shard of keys(recordsByShard)) {
if (recordsByShard[shard].isRebuild)
await initialiseIndex(
app.datastore,
getParentKey(recordsByShard[shard].indexDir),
recordsByShard[shard].indexNode
)
await applyToShard( await applyToShard(
app.hierarchy, app.hierarchy,
app.datastore, app.datastore,
@ -66,9 +76,9 @@ const mappedRecordsByIndexShard = (hierarchy, transactions) => {
const indexBuild = getBuildIndexTransactionsByShard(hierarchy, transactions) const indexBuild = getBuildIndexTransactionsByShard(hierarchy, transactions)
const toRemove = [...deletes, ...updates.toRemove] const toRemove = [...deletes, ...updates.toRemove, ...indexBuild.toRemove]
const toWrite = [...created, ...updates.toWrite, ...indexBuild] const toWrite = [...created, ...updates.toWrite, ...indexBuild.toWrite]
const transByShard = {} const transByShard = {}
@ -77,6 +87,8 @@ const mappedRecordsByIndexShard = (hierarchy, transactions) => {
transByShard[t.indexShardKey] = { transByShard[t.indexShardKey] = {
writes: [], writes: [],
removes: [], removes: [],
isRebuild: some(i => i.indexShardKey === t.indexShardKey)(indexBuild.toWrite)
|| some(i => i.indexShardKey === t.indexShardKey)(indexBuild.toRemove),
indexDir: t.indexDir, indexDir: t.indexDir,
indexNodeKey: t.indexNode.nodeKey(), indexNodeKey: t.indexNode.nodeKey(),
indexNode: t.indexNode, indexNode: t.indexNode,
@ -207,7 +219,7 @@ const getUpdateTransactionsByShard = (hierarchy, transactions) => {
const getBuildIndexTransactionsByShard = (hierarchy, transactions) => { const getBuildIndexTransactionsByShard = (hierarchy, transactions) => {
const buildTransactions = $(transactions, [filter(isBuildIndex)]) const buildTransactions = $(transactions, [filter(isBuildIndex)])
if (!isNonEmptyArray(buildTransactions)) return [] if (!isNonEmptyArray(buildTransactions)) return { toWrite:[], toRemove:[] }
const indexNode = transactions.indexNode const indexNode = transactions.indexNode
const getIndexDirs = t => { const getIndexDirs = t => {
@ -248,7 +260,7 @@ const getBuildIndexTransactionsByShard = (hierarchy, transactions) => {
return $(buildTransactions, [ return $(buildTransactions, [
map(t => { map(t => {
const mappedRecord = evaluate(t.record)(indexNode) const mappedRecord = evaluate(t.record)(indexNode)
if (!mappedRecord.passedFilter) return null mappedRecord.result = mappedRecord.result || t.record
const indexDirs = getIndexDirs(t) const indexDirs = getIndexDirs(t)
return $(indexDirs, [ return $(indexDirs, [
map(indexDir => ({ map(indexDir => ({
@ -262,9 +274,16 @@ const getBuildIndexTransactionsByShard = (hierarchy, transactions) => {
), ),
})), })),
]) ])
}), }),
flatten, flatten,
filter(isSomething), reduce((obj, res) => {
if (res.mappedRecord.passedFilter)
obj.toWrite.push(res)
else
obj.toRemove.push(res)
return obj
}, { toWrite: [], toRemove: [] })
]) ])
} }

View File

@ -22,32 +22,41 @@ export const retrieve = async app => {
TRANSACTIONS_FOLDER TRANSACTIONS_FOLDER
) )
let transactions = []
if (some(isBuildIndexFolder)(transactionFiles)) { if (some(isBuildIndexFolder)(transactionFiles)) {
const buildIndexFolder = find(isBuildIndexFolder)(transactionFiles) const buildIndexFolders = filter(isBuildIndexFolder)(transactionFiles)
let currentFolderIndex = 0
transactions = await retrieveBuildIndexTransactions( while (currentFolderIndex < buildIndexFolders.length) {
const buildIndexFolder = buildIndexFolders[currentFolderIndex]
const transactions = await retrieveBuildIndexTransactions(
app, app,
joinKey(TRANSACTIONS_FOLDER, buildIndexFolder) joinKey(TRANSACTIONS_FOLDER, buildIndexFolder)
) )
if(transactions.length === 0) {
await app.datastore.deleteFolder(
joinKey(TRANSACTIONS_FOLDER, buildIndexFolder))
} else {
return transactions
}
currentFolderIndex += 1
} }
if (transactions.length > 0) return transactions return []
}
return await retrieveStandardTransactions(app, transactionFiles) return await retrieveStandardTransactions(app, transactionFiles)
} }
const retrieveBuildIndexTransactions = async (app, buildIndexFolder) => { const retrieveBuildIndexTransactions = async (app, buildIndexFolder) => {
const childFolders = await app.datastore.getFolderContents(buildIndexFolder) const childFolders = await app.datastore.getFolderContents(buildIndexFolder)
if (childFolders.length === 0) { const childFolderCount = childFolders.length
// cleanup if (childFolderCount === 0) {
await app.datastore.deleteFolder(buildIndexFolder)
return [] return []
} }
const getTransactionFiles = async (childFolderIndex = 0) => { const getTransactionFiles = async (childFolderIndex = 0) => {
if (childFolderIndex >= childFolders.length) return [] if (childFolderIndex >= childFolders.length) {
return { childFolderKey: "", files: [] }
}
const childFolderKey = joinKey( const childFolderKey = joinKey(
buildIndexFolder, buildIndexFolder,
@ -55,17 +64,19 @@ const retrieveBuildIndexTransactions = async (app, buildIndexFolder) => {
) )
const files = await app.datastore.getFolderContents(childFolderKey) const files = await app.datastore.getFolderContents(childFolderKey)
if (files.length === 0) { if (files.length > 0) {
return { childFolderKey, files }
}
await app.datastore.deleteFolder(childFolderKey) await app.datastore.deleteFolder(childFolderKey)
return await getTransactionFiles(childFolderIndex + 1) return await getTransactionFiles(childFolderIndex + 1)
} }
return { childFolderKey, files }
}
const transactionFiles = await getTransactionFiles() const transactionFiles = await getTransactionFiles()
if (transactionFiles.files.length === 0) return [] if (transactionFiles.files.length === 0) {
return []
}
const transactions = $(transactionFiles.files, [map(parseTransactionId)]) const transactions = $(transactionFiles.files, [map(parseTransactionId)])

View File

@ -0,0 +1,14 @@
import { cleanup } from "./cleanup"
export const setCleanupFunc = (app, cleanupTransactions) => {
if (cleanupTransactions) {
app.cleanupTransactions = cleanupTransactions
return
}
if (!app.cleanupTransactions || app.cleanupTransactions.isDefault) {
const newCleanup = async () => cleanup(app)
newCleanup.isDefault = true
app.cleanupTransactions = newCleanup
}
}

View File

@ -1,18 +1,20 @@
import { joinKey, keySep, getHashCode } from "../common" import { joinKey, keySep, getHashCode } from "../common"
import { getLastPartInKey } from "../templateApi/hierarchy" import { getLastPartInKey } from "../templateApi/hierarchy"
import { includes } from "lodash/fp"
export const TRANSACTIONS_FOLDER = `${keySep}.transactions` export const TRANSACTIONS_FOLDER = `${keySep}.transactions`
export const LOCK_FILENAME = "lock" export const LOCK_FILENAME = "lock"
export const LOCK_FILE_KEY = joinKey(TRANSACTIONS_FOLDER, LOCK_FILENAME) export const LOCK_FILE_KEY = joinKey(TRANSACTIONS_FOLDER, LOCK_FILENAME)
export const idSep = "$" export const idSep = "$"
const isOfType = typ => trans => trans.transactionType === typ const isOfType = (...typ) => trans => includes(trans.transactionType)(typ)
export const CREATE_RECORD_TRANSACTION = "create" export const CREATE_RECORD_TRANSACTION = "create"
export const UPDATE_RECORD_TRANSACTION = "update" export const UPDATE_RECORD_TRANSACTION = "update"
export const DELETE_RECORD_TRANSACTION = "delete" export const DELETE_RECORD_TRANSACTION = "delete"
export const BUILD_INDEX_TRANSACTION = "build" export const BUILD_INDEX_TRANSACTION = "build"
export const isUpdate_Or_Rebuild = isOfType(UPDATE_RECORD_TRANSACTION, BUILD_INDEX_TRANSACTION)
export const isUpdate = isOfType(UPDATE_RECORD_TRANSACTION) export const isUpdate = isOfType(UPDATE_RECORD_TRANSACTION)
export const isDelete = isOfType(DELETE_RECORD_TRANSACTION) export const isDelete = isOfType(DELETE_RECORD_TRANSACTION)
export const isCreate = isOfType(CREATE_RECORD_TRANSACTION) export const isCreate = isOfType(CREATE_RECORD_TRANSACTION)

View File

@ -24,7 +24,7 @@ import { filter, find } from "lodash/fp"
import { createBehaviourSources } from "../src/actionsApi/buildBehaviourSource" import { createBehaviourSources } from "../src/actionsApi/buildBehaviourSource"
import { createAction, createTrigger } from "../src/templateApi/createActions" import { createAction, createTrigger } from "../src/templateApi/createActions"
import { initialiseActions } from "../src/actionsApi/initialise" import { initialiseActions } from "../src/actionsApi/initialise"
import { cleanup } from "../src/transactions/cleanup" import { setCleanupFunc } from "../src/transactions/setCleanupFunc"
import { permission } from "../src/authApi/permissions" import { permission } from "../src/authApi/permissions"
import { generateFullPermissions } from "../src/authApi/generateFullPermissions" import { generateFullPermissions } from "../src/authApi/generateFullPermissions"
import { initialiseData } from "../src/appInitialise/initialiseData" import { initialiseData } from "../src/appInitialise/initialiseData"
@ -39,9 +39,9 @@ export const testTemplatesPath = testAreaName =>
path.join(testFileArea(testAreaName), templateDefinitions) path.join(testFileArea(testAreaName), templateDefinitions)
export const getMemoryStore = () => setupDatastore(memory({})) export const getMemoryStore = () => setupDatastore(memory({}))
export const getMemoryTemplateApi = () => { export const getMemoryTemplateApi = (store) => {
const app = { const app = {
datastore: getMemoryStore(), datastore: store || getMemoryStore(),
publish: () => {}, publish: () => {},
getEpochTime: async () => new Date().getTime(), getEpochTime: async () => new Date().getTime(),
user: { name: "", permissions: [permission.writeTemplates.get()] }, user: { name: "", permissions: [permission.writeTemplates.get()] },
@ -78,8 +78,9 @@ export const appFromTempalteApi = async (
const fullPermissions = generateFullPermissions(app) const fullPermissions = generateFullPermissions(app)
app.user.permissions = fullPermissions app.user.permissions = fullPermissions
if (disableCleanupTransactions) app.cleanupTransactions = async () => {} if (disableCleanupTransactions) setCleanupFunc(app, async () => {})
else app.cleanupTransactions = async () => await cleanup(app) else setCleanupFunc(app)
return app return app
} }
@ -100,8 +101,9 @@ export const getRecordApiFromTemplateApi = async (
disableCleanupTransactions = false disableCleanupTransactions = false
) => { ) => {
const app = await appFromTempalteApi(templateApi, disableCleanupTransactions) const app = await appFromTempalteApi(templateApi, disableCleanupTransactions)
const recordapi = getRecordApi() const recordapi = getRecordApi(app)
recordapi._storeHandle = app.datastore recordapi._storeHandle = app.datastore
return recordapi
} }
export const getCollectionApiFromTemplateApi = async ( export const getCollectionApiFromTemplateApi = async (

View File

@ -0,0 +1,63 @@
import {
setupApphierarchy,
basicAppHierarchyCreator_WithFields,
stubEventHandler,
} from "./specHelpers"
import { canDeleteIndex } from "../src/templateApi/canDeleteIndex"
import { canDeleteRecord } from "../src/templateApi/canDeleteRecord"
describe("canDeleteIndex", () => {
it("should return no errors if deltion is valid", async () => {
const { appHierarchy } = await setupApphierarchy(
basicAppHierarchyCreator_WithFields
)
const partnerIndex = appHierarchy.root.indexes.find(i => i.name === "partner_index")
const result = canDeleteIndex(partnerIndex)
expect(result.canDelete).toBe(true)
expect(result.errors).toEqual([])
})
it("should return errors if index is a lookup for a reference field", async () => {
const { appHierarchy } = await setupApphierarchy(
basicAppHierarchyCreator_WithFields
)
const customerIndex = appHierarchy.root.indexes.find(i => i.name === "customer_index")
const result = canDeleteIndex(customerIndex)
expect(result.canDelete).toBe(false)
expect(result.errors.length).toBe(1)
})
it("should return errors if index is a manyToOne index for a reference field", async () => {
const { appHierarchy } = await setupApphierarchy(
basicAppHierarchyCreator_WithFields
)
const referredToCustomersIndex = appHierarchy.customerRecord.indexes.find(i => i.name === "referredToCustomers")
const result = canDeleteIndex(referredToCustomersIndex)
expect(result.canDelete).toBe(false)
expect(result.errors.length).toBe(1)
})
})
describe("canDeleteRecord", () => {
it("should return no errors when deletion is valid", () => {
const { appHierarchy } = await setupApphierarchy(
basicAppHierarchyCreator_WithFields
)
appHierarchy.root.
const result = canDeleteIndex(appHierarchy.customerRecord)
expect(result.canDelete).toBe(true)
expect(result.errors).toEqual([])
})
})

View File

@ -1,6 +1,5 @@
import { getMemoryTemplateApi } from "./specHelpers" import { setup } from "./upgradeDataSetup"
import { diffHierarchy, HierarchyChangeTypes } from "../src/templateApi/diffHierarchy" import { diffHierarchy, HierarchyChangeTypes } from "../src/templateApi/diffHierarchy"
import { getFlattenedHierarchy } from "../src/templateApi/hierarchy"
describe("diffHierarchy", () => { describe("diffHierarchy", () => {
@ -13,7 +12,7 @@ describe("diffHierarchy", () => {
it("should detect root record created", async () => { it("should detect root record created", async () => {
const oldHierarchy = (await setup()).root; const oldHierarchy = (await setup()).root;
const newSetup = (await setup()); const newSetup = await setup()
const opportunity = newSetup.templateApi.getNewRecordTemplate(newSetup.root, "opportunity", false) const opportunity = newSetup.templateApi.getNewRecordTemplate(newSetup.root, "opportunity", false)
const diff = diffHierarchy(oldHierarchy, newSetup.root) const diff = diffHierarchy(oldHierarchy, newSetup.root)
expect(diff).toEqual([{ expect(diff).toEqual([{
@ -25,7 +24,7 @@ describe("diffHierarchy", () => {
it("should only detect root record, when newly created root record has children ", async () => { it("should only detect root record, when newly created root record has children ", async () => {
const oldHierarchy = (await setup()).root; const oldHierarchy = (await setup()).root;
const newSetup = (await setup()); const newSetup = await setup()
const opportunity = newSetup.templateApi.getNewRecordTemplate(newSetup.root, "opportunity", false) const opportunity = newSetup.templateApi.getNewRecordTemplate(newSetup.root, "opportunity", false)
newSetup.templateApi.getNewRecordTemplate(opportunity, "invoice", true) newSetup.templateApi.getNewRecordTemplate(opportunity, "invoice", true)
const diff = diffHierarchy(oldHierarchy, newSetup.root) const diff = diffHierarchy(oldHierarchy, newSetup.root)
@ -38,7 +37,7 @@ describe("diffHierarchy", () => {
it("should detect child record created", async () => { it("should detect child record created", async () => {
const oldHierarchy = (await setup()).root; const oldHierarchy = (await setup()).root;
const newSetup = (await setup()); const newSetup = await setup()
const opportunity = newSetup.templateApi.getNewRecordTemplate(newSetup.contact, "opportunity", false) const opportunity = newSetup.templateApi.getNewRecordTemplate(newSetup.contact, "opportunity", false)
const diff = diffHierarchy(oldHierarchy, newSetup.root) const diff = diffHierarchy(oldHierarchy, newSetup.root)
expect(diff).toEqual([{ expect(diff).toEqual([{
@ -49,8 +48,8 @@ describe("diffHierarchy", () => {
}) })
it("should detect root record deleted", async () => { it("should detect root record deleted", async () => {
const oldSetup = (await setup()); const oldSetup = await setup()
const newSetup = (await setup()); const newSetup = await setup()
newSetup.root.children = newSetup.root.children.filter(n => n.name !== "contact") newSetup.root.children = newSetup.root.children.filter(n => n.name !== "contact")
const diff = diffHierarchy(oldSetup.root, newSetup.root) const diff = diffHierarchy(oldSetup.root, newSetup.root)
expect(diff).toEqual([{ expect(diff).toEqual([{
@ -61,8 +60,8 @@ describe("diffHierarchy", () => {
}) })
it("should detect child record deleted", async () => { it("should detect child record deleted", async () => {
const oldSetup = (await setup()); const oldSetup = await setup()
const newSetup = (await setup()); const newSetup = await setup()
newSetup.contact.children = newSetup.contact.children.filter(n => n.name !== "deal") newSetup.contact.children = newSetup.contact.children.filter(n => n.name !== "deal")
const diff = diffHierarchy(oldSetup.root, newSetup.root) const diff = diffHierarchy(oldSetup.root, newSetup.root)
expect(diff).toEqual([{ expect(diff).toEqual([{
@ -73,8 +72,8 @@ describe("diffHierarchy", () => {
}) })
it("should detect root record renamed", async () => { it("should detect root record renamed", async () => {
const oldSetup = (await setup()); const oldSetup = await setup()
const newSetup = (await setup()); const newSetup = await setup()
newSetup.contact.collectionKey = "CONTACTS" newSetup.contact.collectionKey = "CONTACTS"
const diff = diffHierarchy(oldSetup.root, newSetup.root) const diff = diffHierarchy(oldSetup.root, newSetup.root)
expect(diff).toEqual([{ expect(diff).toEqual([{
@ -85,8 +84,8 @@ describe("diffHierarchy", () => {
}) })
it("should detect child record renamed", async () => { it("should detect child record renamed", async () => {
const oldSetup = (await setup()); const oldSetup = await setup()
const newSetup = (await setup()); const newSetup = await setup()
newSetup.deal.collectionKey = "CONTACTS" newSetup.deal.collectionKey = "CONTACTS"
const diff = diffHierarchy(oldSetup.root, newSetup.root) const diff = diffHierarchy(oldSetup.root, newSetup.root)
expect(diff).toEqual([{ expect(diff).toEqual([{
@ -97,8 +96,8 @@ describe("diffHierarchy", () => {
}) })
it("should detect root record field removed", async () => { it("should detect root record field removed", async () => {
const oldSetup = (await setup()); const oldSetup = await setup()
const newSetup = (await setup()); const newSetup = await setup()
newSetup.contact.fields = newSetup.contact.fields.filter(f => f.name !== "name") newSetup.contact.fields = newSetup.contact.fields.filter(f => f.name !== "name")
const diff = diffHierarchy(oldSetup.root, newSetup.root) const diff = diffHierarchy(oldSetup.root, newSetup.root)
expect(diff).toEqual([{ expect(diff).toEqual([{
@ -109,8 +108,8 @@ describe("diffHierarchy", () => {
}) })
it("should detect child record field removed", async () => { it("should detect child record field removed", async () => {
const oldSetup = (await setup()); const oldSetup = await setup()
const newSetup = (await setup()); const newSetup = await setup()
newSetup.deal.fields = newSetup.deal.fields.filter(f => f.name !== "name") newSetup.deal.fields = newSetup.deal.fields.filter(f => f.name !== "name")
const diff = diffHierarchy(oldSetup.root, newSetup.root) const diff = diffHierarchy(oldSetup.root, newSetup.root)
expect(diff).toEqual([{ expect(diff).toEqual([{
@ -121,8 +120,8 @@ describe("diffHierarchy", () => {
}) })
it("should detect record field added", async () => { it("should detect record field added", async () => {
const oldSetup = (await setup()); const oldSetup = await setup()
const newSetup = (await setup()); const newSetup = await setup()
const notesField = newSetup.templateApi.getNewField("string") const notesField = newSetup.templateApi.getNewField("string")
notesField.name = "notes" notesField.name = "notes"
newSetup.templateApi.addField(newSetup.contact, notesField) newSetup.templateApi.addField(newSetup.contact, notesField)
@ -136,8 +135,8 @@ describe("diffHierarchy", () => {
}) })
it("should detect 1 record field added and 1 removed (total no. fields unchanged)", async () => { it("should detect 1 record field added and 1 removed (total no. fields unchanged)", async () => {
const oldSetup = (await setup()); const oldSetup = await setup()
const newSetup = (await setup()); const newSetup = await setup()
const notesField = newSetup.templateApi.getNewField("string") const notesField = newSetup.templateApi.getNewField("string")
notesField.name = "notes" notesField.name = "notes"
newSetup.templateApi.addField(newSetup.contact, notesField) newSetup.templateApi.addField(newSetup.contact, notesField)
@ -151,8 +150,8 @@ describe("diffHierarchy", () => {
}) })
it("should detect root record estimated record count changed", async () => { it("should detect root record estimated record count changed", async () => {
const oldSetup = (await setup()); const oldSetup = await setup()
const newSetup = (await setup()); const newSetup = await setup()
newSetup.contact.estimatedRecordCount = 987 newSetup.contact.estimatedRecordCount = 987
const diff = diffHierarchy(oldSetup.root, newSetup.root) const diff = diffHierarchy(oldSetup.root, newSetup.root)
expect(diff).toEqual([{ expect(diff).toEqual([{
@ -163,8 +162,8 @@ describe("diffHierarchy", () => {
}) })
it("should detect root record estimated record count changed", async () => { it("should detect root record estimated record count changed", async () => {
const oldSetup = (await setup()); const oldSetup = await setup()
const newSetup = (await setup()); const newSetup = await setup()
newSetup.deal.estimatedRecordCount = 987 newSetup.deal.estimatedRecordCount = 987
const diff = diffHierarchy(oldSetup.root, newSetup.root) const diff = diffHierarchy(oldSetup.root, newSetup.root)
expect(diff).toEqual([{ expect(diff).toEqual([{
@ -174,44 +173,97 @@ describe("diffHierarchy", () => {
}]) }])
}) })
it("should detect root record created", async () => { it("should detect root index created", async () => {
const oldHierarchy = (await setup()).root; const oldHierarchy = (await setup()).root
const newSetup = (await setup()); const newSetup = await setup()
const opportunity = newSetup.templateApi.getNewRecordTemplate(newSetup.root, "opportunity", false) const all_deals = newSetup.templateApi.getNewIndexTemplate(newSetup.root)
const diff = diffHierarchy(oldHierarchy, newSetup.root) const diff = diffHierarchy(oldHierarchy, newSetup.root)
expect(diff).toEqual([{ expect(diff).toEqual([{
newNode: opportunity, newNode: all_deals,
oldNode: null, oldNode: null,
type: HierarchyChangeTypes.recordCreated type: HierarchyChangeTypes.indexCreated
}]) }])
}) })
it("should detect child index created", async () => {
const oldHierarchy = (await setup()).root
const newSetup = await setup()
const all_deals = newSetup.templateApi.getNewIndexTemplate(newSetup.contact)
const diff = diffHierarchy(oldHierarchy, newSetup.root)
expect(diff).toEqual([{
newNode: all_deals,
oldNode: null,
type: HierarchyChangeTypes.indexCreated
}])
})
it("should detect root index deleted", async () => {
const oldSetup = await setup()
const newSetup = await setup()
newSetup.root.indexes = newSetup.root.indexes.filter(i => i.name !== "contact_index")
const diff = diffHierarchy(oldSetup.root, newSetup.root)
expect(diff).toEqual([{
newNode: null,
oldNode: oldSetup.root.indexes.find(i => i.name === "contact_index"),
type: HierarchyChangeTypes.indexDeleted
}])
})
it("should detect child index deleted", async () => {
const oldSetup = await setup()
const newSetup = await setup()
newSetup.contact.indexes = newSetup.contact.indexes.filter(i => i.name !== "deal_index")
const diff = diffHierarchy(oldSetup.root, newSetup.root)
expect(diff).toEqual([{
newNode: null,
oldNode: oldSetup.contact.indexes.find(i => i.name === "deal_index"),
type: HierarchyChangeTypes.indexDeleted
}])
})
const testIndexChanged = (parent, makechange) => async () => {
const oldSetup = await setup()
const newSetup = await setup()
makechange(newSetup)
const diff = diffHierarchy(oldSetup.root, newSetup.root)
expect(diff).toEqual([{
newNode: newSetup[parent].indexes[0],
oldNode: oldSetup[parent].indexes[0],
type: HierarchyChangeTypes.indexChanged
}])
}
it("should detect root index map changed", testIndexChanged("root", newSetup => {
newSetup.root.indexes[0].map = "new"
}))
it("should detect root index filter changed", testIndexChanged("root", newSetup => {
newSetup.root.indexes[0].filter = "new"
}))
it("should detect root index shardName changed", testIndexChanged("root", newSetup => {
newSetup.root.indexes[0].getShardName = "new"
}))
it("should detect root index allowedRecordIds changed", testIndexChanged("root", newSetup => {
newSetup.root.indexes[0].allowedRecordNodeIds.push(3)
}))
it("should detect child index allowedRecordIds changed", testIndexChanged("contact", newSetup => {
newSetup.contact.indexes[0].allowedRecordNodeIds.push(3)
}))
it("should detect child index map changed", testIndexChanged("contact", newSetup => {
newSetup.contact.indexes[0].map = "new"
}))
it("should detect child index filter changed", testIndexChanged("contact", newSetup => {
newSetup.contact.indexes[0].filter = "new"
}))
it("should detect child index shardName changed", testIndexChanged("contact", newSetup => {
newSetup.contact.indexes[0].getShardName = "new"
}))
}) })
const setup = async () => {
const { templateApi } = await getMemoryTemplateApi()
const root = templateApi.getNewRootLevel()
const contact = templateApi.getNewRecordTemplate(root, "contact", true)
const nameField = templateApi.getNewField("string")
nameField.name = "name"
const statusField = templateApi.getNewField("string")
statusField.name = "status"
templateApi.addField(contact, nameField)
templateApi.addField(contact, statusField)
const lead = templateApi.getNewRecordTemplate(root, "lead", true)
const deal = templateApi.getNewRecordTemplate(contact, "deal", true)
templateApi.addField(deal, {...nameField})
templateApi.addField(deal, {...statusField})
getFlattenedHierarchy(root)
return {
root, contact, lead, deal, templateApi,
all_contacts: root.indexes[0],
all_leads: root.indexes[1],
deals_for_contacts: contact.indexes[0]
}
}

View File

@ -0,0 +1,244 @@
import {
getRecordApiFromTemplateApi,
getIndexApiFromTemplateApi,
} from "./specHelpers"
import { upgradeData } from "../src/templateApi/upgradeData"
import { setup } from "./upgradeDataSetup"
import { $, splitKey } from "../src/common"
import { keys, filter } from "lodash/fp"
import { _listItems } from "../src/indexApi/listItems"
import { _save } from "../src/recordApi/save"
describe("upgradeData", () => {
it("should delete all records and child records, when root record node deleted", async () => {
const { oldSetup, newSetup, recordApi } = await configure()
newSetup.root.children = newSetup.root.children.filter(n => n.name !== "contact")
await upgradeData(oldSetup.app)(newSetup.root)
const remainingKeys = $(recordApi._storeHandle.data, [
keys,
filter(k => splitKey(k)[0] === "contacts"),
])
expect(remainingKeys.length).toBe(0)
})
it("should not delete other root record types, when root record node deleted", async () => {
const { oldSetup, newSetup, recordApi } = await configure()
newSetup.root.children = newSetup.root.children.filter(n => n.name !== "contact")
await upgradeData(oldSetup.app)(newSetup.root)
const remainingKeys = $(recordApi._storeHandle.data, [
keys,
filter(k => splitKey(k)[0] === "leads"),
])
expect(remainingKeys.length > 0).toBe(true)
})
it("should delete all child records, when child record node deleted", async () => {
const { oldSetup, newSetup, recordApi } = await configure()
newSetup.contact.children = newSetup.contact.children.filter(n => n.name !== "deal")
const startingKeys = $(recordApi._storeHandle.data, [
keys,
filter(k => k.includes("/deals/")),
])
expect(startingKeys.length > 0).toBe(true)
await upgradeData(oldSetup.app)(newSetup.root)
const remainingKeys = $(recordApi._storeHandle.data, [
keys,
filter(k => k.includes("/deals/")),
])
expect(remainingKeys.length).toBe(0)
})
it("should build a new root index", async () => {
const { oldSetup, newSetup } = await configure()
const newIndex = newSetup.templateApi.getNewIndexTemplate(newSetup.root)
newIndex.name = "more_contacts"
newIndex.allowedRecordNodeIds = [newSetup.contact.nodeId]
await upgradeData(oldSetup.app)(newSetup.root)
const itemsInNewIndex = await _listItems(newSetup.app, "/more_contacts")
expect(itemsInNewIndex.length).toBe(2)
})
it("should update a root index", async () => {
const { oldSetup, newSetup } = await configure()
const contact_index = indexByName(newSetup.root, "contact_index")
contact_index.filter = "record.name === 'bobby'"
await upgradeData(oldSetup.app)(newSetup.root)
const itemsInNewIndex = await _listItems(newSetup.app, "/contact_index")
expect(itemsInNewIndex.length).toBe(1)
})
it("should delete a root index", async () => {
const { oldSetup, newSetup } = await configure()
// no exception
await _listItems(newSetup.app, "/contact_index")
newSetup.root.indexes = newSetup.root.indexes.filter(i => i.name !== "contact_index")
await upgradeData(oldSetup.app)(newSetup.root)
let er
try {
await _listItems(newSetup.app, "/contact_index")
} catch (e) {
er = e
}
expect(er).toBeDefined()
})
it("should build a new child index", async () => {
const { oldSetup, newSetup, records } = await configure()
const newIndex = newSetup.templateApi.getNewIndexTemplate(newSetup.contact)
newIndex.name = "more_deals"
newIndex.allowedRecordNodeIds = [newSetup.deal.nodeId]
await upgradeData(oldSetup.app)(newSetup.root)
const itemsInNewIndex = await _listItems(newSetup.app, `${records.contact1.key}/more_deals`)
expect(itemsInNewIndex.length).toBe(2)
})
it("should update a child index", async () => {
const { oldSetup, newSetup, records } = await configure()
const deal_index = indexByName(newSetup.contact, "deal_index")
deal_index.filter = "record.status === 'new'"
let itemsInIndex = await _listItems(newSetup.app, `${records.contact1.key}/deal_index`)
expect(itemsInIndex.length).toBe(2)
await upgradeData(oldSetup.app)(newSetup.root)
itemsInIndex = await _listItems(newSetup.app, `${records.contact1.key}/deal_index`)
expect(itemsInIndex.length).toBe(1)
})
it("should delete a child index", async () => {
const { oldSetup, newSetup, records } = await configure()
// no exception
await _listItems(newSetup.app, `${records.contact1.key}/deal_index`)
newSetup.contact.indexes = newSetup.contact.indexes.filter(i => i.name !== "deal_index")
await upgradeData(oldSetup.app)(newSetup.root)
let er
try {
await _listItems(newSetup.app, `${records.contact1.key}/deal_index`)
} catch (e) {
er = e
}
expect(er).toBeDefined()
})
it("should build a new reference index", async () => {
const { oldSetup, newSetup, records, recordApi } = await configure()
const newIndex = newSetup.templateApi.getNewIndexTemplate(newSetup.lead)
newIndex.name = "contact_leads"
newIndex.allowedRecordNodeIds = [newSetup.lead.nodeId]
newIndex.indexType = "reference"
const leadField = newSetup.templateApi.getNewField("string")
leadField.name = "lead"
leadField.type = "reference"
leadField.typeOptions = {
reverseIndexNodeKeys: [ newIndex.nodeKey() ],
indexNodeKey: "/lead_index",
displayValue: "name"
}
newSetup.templateApi.addField(newSetup.contact, leadField)
await upgradeData(oldSetup.app)(newSetup.root)
const indexKey = `${records.lead1.key}/contact_leads`
let itemsInNewIndex = await _listItems(newSetup.app, indexKey)
expect(itemsInNewIndex.length).toBe(0)
records.contact1.lead = records.lead1
records.contact1.isNew = false
await _save(newSetup.app, records.contact1)
itemsInNewIndex = await _listItems(newSetup.app, indexKey)
expect(itemsInNewIndex.length).toBe(1)
})
})
const configure = async () => {
const oldSetup = await setup()
const recordApi = await getRecordApiFromTemplateApi(oldSetup.templateApi)
const indexApi = await getIndexApiFromTemplateApi(oldSetup.templateApi)
const newSetup = await setup(oldSetup.store)
const records = await createSomeRecords(recordApi)
return { oldSetup, newSetup, recordApi, records, indexApi }
}
const createSomeRecords = async recordApi => {
const contact1 = recordApi.getNew("/contacts", "contact")
contact1.name = "bobby"
const contact2 = recordApi.getNew("/contacts", "contact")
contact2.name = "poppy"
await recordApi.save(contact1)
await recordApi.save(contact2)
const deal1 = recordApi.getNew(`${contact1.key}/deals`, "deal")
deal1.name = "big mad deal"
deal1.status = "new"
const deal2 = recordApi.getNew(`${contact1.key}/deals`, "deal")
deal2.name = "smaller deal"
deal2.status = "old"
const deal3 = recordApi.getNew(`${contact2.key}/deals`, "deal")
deal3.name = "ok deal"
deal3.status = "new"
await recordApi.save(deal1)
await recordApi.save(deal2)
await recordApi.save(deal3)
const lead1 = recordApi.getNew("/leads", "lead")
lead1.name = "big new lead"
await recordApi.save(lead1)
return {
contact1, contact2, deal1, deal2, deal3, lead1,
}
}
const indexByName = (parent, name) => parent.indexes.find(i => i.name === name)

View File

@ -0,0 +1,47 @@
import { getMemoryTemplateApi, appFromTempalteApi } from "./specHelpers"
import { getFlattenedHierarchy } from "../src/templateApi/hierarchy"
import { initialiseData } from "../src/appInitialise/initialiseData"
export const setup = async (store) => {
const { templateApi } = await getMemoryTemplateApi(store)
const root = templateApi.getNewRootLevel()
const contact = templateApi.getNewRecordTemplate(root, "contact", true)
contact.collectionName = "contacts"
const nameField = templateApi.getNewField("string")
nameField.name = "name"
const statusField = templateApi.getNewField("string")
statusField.name = "status"
templateApi.addField(contact, nameField)
templateApi.addField(contact, statusField)
const lead = templateApi.getNewRecordTemplate(root, "lead", true)
lead.collectionName = "leads"
const deal = templateApi.getNewRecordTemplate(contact, "deal", true)
deal.collectionName = "deals"
templateApi.addField(deal, {...nameField})
templateApi.addField(deal, {...statusField})
templateApi.addField(lead, {...nameField})
getFlattenedHierarchy(root)
if (!store)
await initialiseData(templateApi._storeHandle, {
hierarchy: root,
actions: [],
triggers: [],
})
const app = await appFromTempalteApi(templateApi)
app.hierarchy = root
return {
root, contact, lead, app,
deal, templateApi, store: templateApi._storeHandle,
all_contacts: root.indexes[0],
all_leads: root.indexes[1],
deals_for_contacts: contact.indexes[0],
}
}

View File

@ -18,6 +18,7 @@ const lookupField = require("./lookupField")
const getRecord = require("./getRecord") const getRecord = require("./getRecord")
const deleteRecord = require("./deleteRecord") const deleteRecord = require("./deleteRecord")
const saveAppHierarchy = require("./saveAppHierarchy") const saveAppHierarchy = require("./saveAppHierarchy")
const upgradeData = require("./saveAppHierarchy")
module.exports = { module.exports = {
authenticate, authenticate,
@ -40,4 +41,5 @@ module.exports = {
getRecord, getRecord,
deleteRecord, deleteRecord,
saveAppHierarchy, saveAppHierarchy,
upgradeData,
} }

View File

@ -0,0 +1,6 @@
const StatusCodes = require("../../utilities/statusCodes")
module.exports = async ctx => {
await ctx.instance.templateApi.upgradeData(ctx.request.body.newHierarchy)
ctx.response.status = StatusCodes.OK
}

View File

@ -238,6 +238,10 @@ module.exports = (config, app) => {
ctx.response.status = StatusCodes.UNAUTHORIZED ctx.response.status = StatusCodes.UNAUTHORIZED
} }
}) })
.post(
"/_builder/instance/:appname/:instanceid/api/upgradeData",
routeHandlers.upgradeData
)
.post("/:appname/api/changeMyPassword", routeHandlers.changeMyPassword) .post("/:appname/api/changeMyPassword", routeHandlers.changeMyPassword)
.post( .post(
"/_builder/instance/:appname/:instanceid/api/changeMyPassword", "/_builder/instance/:appname/:instanceid/api/changeMyPassword",

View File

@ -78,13 +78,12 @@
box-sizing: border-box; box-sizing: border-box;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 2px; border-radius: 2px;
color: #333; color: #000333;
background-color: #f4f4f4;
outline: none; outline: none;
} }
.default:active { .default:active {
background-color: #ddd; background-color: #f9f9f9;
} }
.default:focus { .default:focus {

View File

@ -155,16 +155,15 @@
box-sizing: border-box; box-sizing: border-box;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 2px; border-radius: 2px;
color: #333; color: #000333;
background-color: #f4f4f4;
outline: none; outline: none;
} }
.default-button:active { .default-button:active {
background-color: #ddd; background-color: #f9f9f9;
} }
.default-button:focus { .default-button:focus {
border-color: #666; border-color: #f9f9f9;
} }
</style> </style>