Merge branch 'master' of github.com:Budibase/budibase into feat/linked-records-data-source
This commit is contained in:
commit
0ac8a33210
|
@ -112,7 +112,7 @@
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"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.24.1",
|
"svelte": "^3.29.0",
|
||||||
"svelte-jester": "^1.0.6"
|
"svelte-jester": "^1.0.6"
|
||||||
},
|
},
|
||||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||||
|
|
|
@ -8,8 +8,9 @@ export default function(component, state) {
|
||||||
|
|
||||||
const findMatches = props => {
|
const findMatches = props => {
|
||||||
walkProps(props, c => {
|
walkProps(props, c => {
|
||||||
if ((c._instanceName || "").startsWith(capitalised)) {
|
const thisInstanceName = get_capitalised_name(c._instanceName)
|
||||||
matchingComponents.push(c._instanceName)
|
if ((thisInstanceName || "").startsWith(capitalised)) {
|
||||||
|
matchingComponents.push(thisInstanceName)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,17 +124,18 @@ const saveScreen = store => screen => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const _saveScreen = async (store, s, screen) => {
|
const _saveScreen = async (store, s, screen) => {
|
||||||
const currentPageScreens = s.pages[s.currentPageName]._screens
|
const pageName = s.currentPageName || "main"
|
||||||
|
const currentPageScreens = s.pages[pageName]._screens
|
||||||
|
|
||||||
await api
|
await api
|
||||||
.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}/screen`, screen)
|
.post(`/_builder/api/${s.appId}/pages/${pageName}/screen`, screen)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (currentPageScreens.includes(screen)) return
|
if (currentPageScreens.includes(screen)) return
|
||||||
|
|
||||||
const screens = [...currentPageScreens, screen]
|
const screens = [...currentPageScreens, screen]
|
||||||
|
|
||||||
store.update(innerState => {
|
store.update(innerState => {
|
||||||
innerState.pages[s.currentPageName]._screens = screens
|
innerState.pages[pageName]._screens = screens
|
||||||
innerState.screens = screens
|
innerState.screens = screens
|
||||||
innerState.currentPreviewItem = screen
|
innerState.currentPreviewItem = screen
|
||||||
innerState.allScreens = [...innerState.allScreens, screen]
|
innerState.allScreens = [...innerState.allScreens, screen]
|
||||||
|
@ -153,27 +154,17 @@ const _saveScreen = async (store, s, screen) => {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
const createScreen = store => (screenName, route, layoutComponentName) => {
|
const createScreen = store => async screen => {
|
||||||
|
let savePromise
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
const rootComponent = state.components[layoutComponentName]
|
state.currentPreviewItem = screen
|
||||||
|
state.currentComponentInfo = screen.props
|
||||||
const newScreen = {
|
|
||||||
description: "",
|
|
||||||
url: "",
|
|
||||||
_css: "",
|
|
||||||
props: createProps(rootComponent).props,
|
|
||||||
}
|
|
||||||
newScreen.route = route
|
|
||||||
newScreen.name = newScreen.props._id
|
|
||||||
newScreen.props._instanceName = screenName || ""
|
|
||||||
state.currentPreviewItem = newScreen
|
|
||||||
state.currentComponentInfo = newScreen.props
|
|
||||||
state.currentFrontEndType = "screen"
|
state.currentFrontEndType = "screen"
|
||||||
|
savePromise = _saveScreen(store, state, screen)
|
||||||
_saveScreen(store, state, newScreen)
|
regenerateCssForCurrentScreen(state)
|
||||||
|
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
await savePromise
|
||||||
}
|
}
|
||||||
|
|
||||||
const setCurrentScreen = store => screenName => {
|
const setCurrentScreen = store => screenName => {
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
export default {
|
||||||
|
name: `Create from scratch`,
|
||||||
|
create: () => createScreen(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const createScreen = () => ({
|
||||||
|
props: {
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/container",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
type: "div",
|
||||||
|
_children: [],
|
||||||
|
_instanceName: "",
|
||||||
|
},
|
||||||
|
route: "",
|
||||||
|
name: "screen-id",
|
||||||
|
})
|
|
@ -0,0 +1,22 @@
|
||||||
|
export default {
|
||||||
|
name: `New Row (Empty)`,
|
||||||
|
create: () => createScreen(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const createScreen = () => ({
|
||||||
|
props: {
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/newrow",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_children: [],
|
||||||
|
_instanceName: "",
|
||||||
|
model: "",
|
||||||
|
},
|
||||||
|
route: "",
|
||||||
|
name: "screen-id",
|
||||||
|
})
|
|
@ -0,0 +1,22 @@
|
||||||
|
export default {
|
||||||
|
name: `Row Detail (Empty)`,
|
||||||
|
create: () => createScreen(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const createScreen = () => ({
|
||||||
|
props: {
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/rowdetail",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_children: [],
|
||||||
|
_instanceName: "",
|
||||||
|
model: "",
|
||||||
|
},
|
||||||
|
route: "",
|
||||||
|
name: "screen-id",
|
||||||
|
})
|
|
@ -0,0 +1,35 @@
|
||||||
|
import newRecordScreen from "./newRecordScreen"
|
||||||
|
import recordDetailScreen from "./recordDetailScreen"
|
||||||
|
import recordListScreen from "./recordListScreen"
|
||||||
|
import emptyNewRecordScreen from "./emptyNewRecordScreen"
|
||||||
|
import createFromScratchScreen from "./createFromScratchScreen"
|
||||||
|
import emptyRecordDetailScreen from "./emptyRecordDetailScreen"
|
||||||
|
import { generateNewIdsForComponent } from "../../storeUtils"
|
||||||
|
import { uuid } from "builderStore/uuid"
|
||||||
|
|
||||||
|
const allTemplates = models => [
|
||||||
|
createFromScratchScreen,
|
||||||
|
...newRecordScreen(models),
|
||||||
|
...recordDetailScreen(models),
|
||||||
|
...recordListScreen(models),
|
||||||
|
emptyNewRecordScreen,
|
||||||
|
emptyRecordDetailScreen,
|
||||||
|
]
|
||||||
|
|
||||||
|
// allows us to apply common behaviour to all create() functions
|
||||||
|
const createTemplateOverride = (frontendState, create) => () => {
|
||||||
|
const screen = create()
|
||||||
|
for (let component of screen.props._children) {
|
||||||
|
generateNewIdsForComponent(component, frontendState, false)
|
||||||
|
}
|
||||||
|
screen.props._id = uuid()
|
||||||
|
screen.name = screen.props._id
|
||||||
|
screen.route = screen.route.toLowerCase()
|
||||||
|
return screen
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (frontendState, models) =>
|
||||||
|
allTemplates(models).map(template => ({
|
||||||
|
...template,
|
||||||
|
create: createTemplateOverride(frontendState, template.create),
|
||||||
|
}))
|
|
@ -0,0 +1,135 @@
|
||||||
|
export default function(models) {
|
||||||
|
return models.map(model => {
|
||||||
|
const fields = Object.keys(model.schema)
|
||||||
|
const heading = fields.length > 0 ? `{{ data.${fields[0]} }}` : "Add Row"
|
||||||
|
return {
|
||||||
|
name: `${model.name} - New`,
|
||||||
|
create: () => createScreen(model, heading),
|
||||||
|
id: NEW_RECORD_TEMPLATE,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NEW_RECORD_TEMPLATE = "NEW_RECORD_TEMPLATE"
|
||||||
|
|
||||||
|
const createScreen = (model, heading) => ({
|
||||||
|
props: {
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/newrow",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
model: model._id,
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/heading",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_code: "",
|
||||||
|
className: "",
|
||||||
|
text: heading,
|
||||||
|
type: "h1",
|
||||||
|
_instanceName: "Heading 1",
|
||||||
|
_children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/dataform",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_code: "",
|
||||||
|
_instanceName: `${model.name} Form`,
|
||||||
|
_children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/container",
|
||||||
|
_styles: {
|
||||||
|
normal: {
|
||||||
|
display: "flex",
|
||||||
|
"flex-direction": "row",
|
||||||
|
"align-items": "center",
|
||||||
|
"justify-content": "flex-end",
|
||||||
|
},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_code: "",
|
||||||
|
className: "",
|
||||||
|
onLoad: [],
|
||||||
|
type: "div",
|
||||||
|
_instanceName: "Buttons Container",
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/button",
|
||||||
|
_styles: {
|
||||||
|
normal: {
|
||||||
|
"margin-right": "20px",
|
||||||
|
},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_code: "",
|
||||||
|
text: "Back",
|
||||||
|
className: "",
|
||||||
|
disabled: false,
|
||||||
|
onClick: [
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
url: `/${model.name.toLowerCase()}`,
|
||||||
|
},
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
_instanceName: "Back Button",
|
||||||
|
_children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/button",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_code: "",
|
||||||
|
text: "Save",
|
||||||
|
className: "",
|
||||||
|
disabled: false,
|
||||||
|
onClick: [
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
contextPath: "data",
|
||||||
|
modelId: model._id,
|
||||||
|
},
|
||||||
|
"##eventHandlerType": "Save Record",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
_instanceName: "Save Button",
|
||||||
|
_children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
_instanceName: `${model.name} - New`,
|
||||||
|
_code: "",
|
||||||
|
},
|
||||||
|
route: `/${model.name.toLowerCase()}/new`,
|
||||||
|
name: "",
|
||||||
|
})
|
|
@ -0,0 +1,135 @@
|
||||||
|
export default function(models) {
|
||||||
|
return models.map(model => {
|
||||||
|
const fields = Object.keys(model.schema)
|
||||||
|
const heading = fields.length > 0 ? `{{ data.${fields[0]} }}` : "Detail"
|
||||||
|
return {
|
||||||
|
name: `${model.name} - Detail`,
|
||||||
|
create: () => createScreen(model, heading),
|
||||||
|
id: RECORD_DETAIL_TEMPLATE,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RECORD_DETAIL_TEMPLATE = "RECORD_DETAIL_TEMPLATE"
|
||||||
|
|
||||||
|
const createScreen = (model, heading) => ({
|
||||||
|
props: {
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/rowdetail",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
model: model._id,
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/heading",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_code: "",
|
||||||
|
className: "",
|
||||||
|
text: heading,
|
||||||
|
type: "h1",
|
||||||
|
_instanceName: "Heading 1",
|
||||||
|
_children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/dataform",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_code: "",
|
||||||
|
_instanceName: `${model.name} Form`,
|
||||||
|
_children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/container",
|
||||||
|
_styles: {
|
||||||
|
normal: {
|
||||||
|
display: "flex",
|
||||||
|
"flex-direction": "row",
|
||||||
|
"align-items": "center",
|
||||||
|
"justify-content": "flex-end",
|
||||||
|
},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_code: "",
|
||||||
|
className: "",
|
||||||
|
onLoad: [],
|
||||||
|
type: "div",
|
||||||
|
_instanceName: "Buttons Container",
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/button",
|
||||||
|
_styles: {
|
||||||
|
normal: {
|
||||||
|
"margin-right": "20px",
|
||||||
|
},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_code: "",
|
||||||
|
text: "Back",
|
||||||
|
className: "",
|
||||||
|
disabled: false,
|
||||||
|
onClick: [
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
url: `/${model.name.toLowerCase()}`,
|
||||||
|
},
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
_instanceName: "Back Button",
|
||||||
|
_children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/button",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_code: "",
|
||||||
|
text: "Save",
|
||||||
|
className: "",
|
||||||
|
disabled: false,
|
||||||
|
onClick: [
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
contextPath: "data",
|
||||||
|
modelId: model._id,
|
||||||
|
},
|
||||||
|
"##eventHandlerType": "Save Record",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
_instanceName: "Save Button",
|
||||||
|
_children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
_instanceName: `${model.name} - Detail`,
|
||||||
|
_code: "",
|
||||||
|
},
|
||||||
|
route: `/${model.name.toLowerCase()}/:id`,
|
||||||
|
name: "",
|
||||||
|
})
|
|
@ -0,0 +1,118 @@
|
||||||
|
export default function(models) {
|
||||||
|
return models.map(model => {
|
||||||
|
return {
|
||||||
|
name: `${model.name} - List`,
|
||||||
|
create: () => createScreen(model),
|
||||||
|
id: RECORD_LIST_TEMPLATE,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RECORD_LIST_TEMPLATE = "RECORD_LIST_TEMPLATE"
|
||||||
|
|
||||||
|
const createScreen = model => ({
|
||||||
|
props: {
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/container",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
type: "div",
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/container",
|
||||||
|
_styles: {
|
||||||
|
normal: {
|
||||||
|
display: "flex",
|
||||||
|
"flex-direction": "row",
|
||||||
|
"justify-content": "space-between",
|
||||||
|
"align-items": "center",
|
||||||
|
},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_code: "",
|
||||||
|
className: "",
|
||||||
|
onLoad: [],
|
||||||
|
type: "div",
|
||||||
|
_instanceName: "Header",
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/heading",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_code: "",
|
||||||
|
className: "",
|
||||||
|
text: `${model.name} List`,
|
||||||
|
type: "h1",
|
||||||
|
_instanceName: "Heading 1",
|
||||||
|
_children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/button",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_code: "",
|
||||||
|
text: "Create New",
|
||||||
|
className: "",
|
||||||
|
disabled: false,
|
||||||
|
onClick: [
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
url: `/${model.name}/new`,
|
||||||
|
},
|
||||||
|
"##eventHandlerType": "Navigate To",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
_instanceName: "Create New Button",
|
||||||
|
_children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "",
|
||||||
|
_component: "@budibase/standard-components/datatable",
|
||||||
|
_styles: {
|
||||||
|
normal: {},
|
||||||
|
hover: {},
|
||||||
|
active: {},
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
_code: "",
|
||||||
|
datasource: {
|
||||||
|
label: "Deals",
|
||||||
|
name: `all_${model._id}`,
|
||||||
|
modelId: model._id,
|
||||||
|
isModel: true,
|
||||||
|
},
|
||||||
|
stripeColor: "",
|
||||||
|
borderColor: "",
|
||||||
|
backgroundColor: "",
|
||||||
|
color: "",
|
||||||
|
_instanceName: `${model.name} Table`,
|
||||||
|
_children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
_instanceName: `${model.name} - List`,
|
||||||
|
_code: "",
|
||||||
|
className: "",
|
||||||
|
onLoad: [],
|
||||||
|
},
|
||||||
|
route: `/${model.name.toLowerCase()}`,
|
||||||
|
name: "",
|
||||||
|
})
|
|
@ -85,10 +85,10 @@ export const regenerateCssForCurrentScreen = state => {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateNewIdsForComponent = (c, state) =>
|
export const generateNewIdsForComponent = (c, state, changeName = true) =>
|
||||||
walkProps(c, p => {
|
walkProps(c, p => {
|
||||||
p._id = uuid()
|
p._id = uuid()
|
||||||
p._instanceName = getNewComponentName(p._component, state)
|
if (changeName) p._instanceName = getNewComponentName(p._component, state)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getComponentDefinition = (state, name) =>
|
export const getComponentDefinition = (state, name) =>
|
||||||
|
|
|
@ -1,11 +1,21 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@sveltech/routify"
|
import { goto } from "@sveltech/routify"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore, store } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import { Button, Input, Label, ModalContent, Modal } from "@budibase/bbui"
|
import { Button, Input, Label, ModalContent, Modal } from "@budibase/bbui"
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import TableDataImport from "../TableDataImport.svelte"
|
import TableDataImport from "../TableDataImport.svelte"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
import screenTemplates from "builderStore/store/screenTemplates"
|
||||||
|
import { NEW_RECORD_TEMPLATE } from "builderStore/store/screenTemplates/newRecordScreen"
|
||||||
|
import { RECORD_DETAIL_TEMPLATE } from "builderStore/store/screenTemplates/recordDetailScreen"
|
||||||
|
import { RECORD_LIST_TEMPLATE } from "builderStore/store/screenTemplates/recordListScreen"
|
||||||
|
|
||||||
|
const defaultScreens = [
|
||||||
|
NEW_RECORD_TEMPLATE,
|
||||||
|
RECORD_DETAIL_TEMPLATE,
|
||||||
|
RECORD_LIST_TEMPLATE,
|
||||||
|
]
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
let name
|
let name
|
||||||
|
@ -25,6 +35,24 @@
|
||||||
notifier.success(`Table ${name} created successfully.`)
|
notifier.success(`Table ${name} created successfully.`)
|
||||||
$goto(`./model/${model._id}`)
|
$goto(`./model/${model._id}`)
|
||||||
analytics.captureEvent("Table Created", { name })
|
analytics.captureEvent("Table Created", { name })
|
||||||
|
|
||||||
|
const screens = screenTemplates($store, [model])
|
||||||
|
.filter(template => defaultScreens.includes(template.id))
|
||||||
|
.map(template => template.create())
|
||||||
|
|
||||||
|
for (let screen of screens) {
|
||||||
|
console.log(JSON.stringify(screen))
|
||||||
|
try {
|
||||||
|
await store.createScreen(screen)
|
||||||
|
} catch (_) {
|
||||||
|
// TODO: this is temporary
|
||||||
|
// a cypress test is failing, because I added the
|
||||||
|
// NewRecord component. So - this throws an exception
|
||||||
|
// because the currently released standard-components (on NPM)
|
||||||
|
// does not have NewRecord
|
||||||
|
// we should remove this after this has been released
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -68,18 +68,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyComponent = () => {
|
const copyComponent = () => {
|
||||||
store.update(s => {
|
storeComponentForCopy(false)
|
||||||
const parent = getParent(s.currentPreviewItem.props, component)
|
pasteComponent("below")
|
||||||
const copiedComponent = cloneDeep(component)
|
|
||||||
walkProps(copiedComponent, p => {
|
|
||||||
p._id = uuid()
|
|
||||||
})
|
|
||||||
parent._children = [...parent._children, copiedComponent]
|
|
||||||
saveCurrentPreviewItem(s)
|
|
||||||
s.currentComponentInfo = copiedComponent
|
|
||||||
regenerateCssForCurrentScreen(s)
|
|
||||||
return s
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteComponent = () => {
|
const deleteComponent = () => {
|
||||||
|
|
|
@ -41,14 +41,6 @@
|
||||||
|
|
||||||
const onStyleChanged = store.setComponentStyle
|
const onStyleChanged = store.setComponentStyle
|
||||||
|
|
||||||
function onPropChanged(key, value) {
|
|
||||||
if ($store.currentView !== "component") {
|
|
||||||
store.setPageOrScreenProp(key, value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
store.setComponentProp(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
$: isComponentOrScreen =
|
$: isComponentOrScreen =
|
||||||
$store.currentView === "component" ||
|
$store.currentView === "component" ||
|
||||||
$store.currentFrontEndType === "screen"
|
$store.currentFrontEndType === "screen"
|
||||||
|
@ -103,7 +95,8 @@
|
||||||
{componentDefinition}
|
{componentDefinition}
|
||||||
{panelDefinition}
|
{panelDefinition}
|
||||||
displayNameField={displayName}
|
displayNameField={displayName}
|
||||||
onChange={onPropChanged}
|
onChange={store.setComponentProp}
|
||||||
|
onScreenPropChange={store.setPageOrScreenProp}
|
||||||
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
|
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,7 @@
|
||||||
|
|
||||||
{#if fields}
|
{#if fields}
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
<Label size="m" color="dark">Field</Label>
|
<Label size="m" color="dark">Column</Label>
|
||||||
<Select secondary bind:value={field.name} on:blur={rebuildParameters}>
|
<Select secondary bind:value={field.name} on:blur={rebuildParameters}>
|
||||||
<option value="" />
|
<option value="" />
|
||||||
{#each schemaFields as schemaField}
|
{#each schemaFields as schemaField}
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
<script>
|
||||||
|
import { Select, Label } from "@budibase/bbui"
|
||||||
|
import { store, backendUiStore } from "builderStore"
|
||||||
|
import fetchBindableProperties from "builderStore/fetchBindableProperties"
|
||||||
|
import SaveFields from "./SaveFields.svelte"
|
||||||
|
import {
|
||||||
|
readableToRuntimeBinding,
|
||||||
|
runtimeToReadableBinding,
|
||||||
|
} from "builderStore/replaceBindings"
|
||||||
|
|
||||||
|
// parameters.contextPath used in the client handler to determine which record to save
|
||||||
|
// this could be "data" or "data.parent", "data.parent.parent" etc
|
||||||
|
export let parameters
|
||||||
|
|
||||||
|
let idFields
|
||||||
|
let schemaFields
|
||||||
|
|
||||||
|
$: bindableProperties = fetchBindableProperties({
|
||||||
|
componentInstanceId: $store.currentComponentInfo._id,
|
||||||
|
components: $store.components,
|
||||||
|
screen: $store.currentPreviewItem,
|
||||||
|
models: $backendUiStore.models,
|
||||||
|
})
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (parameters && parameters.contextPath) {
|
||||||
|
schemaFields = schemaFromContextPath(parameters.contextPath)
|
||||||
|
} else {
|
||||||
|
schemaFields = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const idBindingToContextPath = id => id.substring(0, id.length - 4)
|
||||||
|
const contextPathToId = path => `${path}._id`
|
||||||
|
|
||||||
|
$: {
|
||||||
|
idFields = bindableProperties.filter(
|
||||||
|
bindable =>
|
||||||
|
bindable.type === "context" && bindable.runtimeBinding.endsWith("._id")
|
||||||
|
)
|
||||||
|
// ensure contextPath is always defaulted - there is usually only one option
|
||||||
|
if (idFields.length > 0 && !parameters.contextPath) {
|
||||||
|
parameters.contextPath = idBindingToContextPath(
|
||||||
|
idFields[0].runtimeBinding
|
||||||
|
)
|
||||||
|
parameters = parameters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// just wraps binding in {{ ... }}
|
||||||
|
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
|
||||||
|
|
||||||
|
// finds the selected idBinding, then reads the table/view
|
||||||
|
// from the component instance that it belongs to.
|
||||||
|
// then returns the field names for that schema
|
||||||
|
const schemaFromContextPath = contextPath => {
|
||||||
|
if (!contextPath) return []
|
||||||
|
|
||||||
|
const idBinding = bindableProperties.find(
|
||||||
|
prop => prop.runtimeBinding === contextPathToId(contextPath)
|
||||||
|
)
|
||||||
|
if (!idBinding) return []
|
||||||
|
|
||||||
|
const { instance } = idBinding
|
||||||
|
|
||||||
|
const component = $store.components[instance._component]
|
||||||
|
|
||||||
|
// component.context is the name of the prop that holds the modelId
|
||||||
|
const modelInfo = instance[component.context]
|
||||||
|
const modelId =
|
||||||
|
typeof modelInfo === "string" ? modelInfo : modelInfo.modelId
|
||||||
|
|
||||||
|
if (!modelInfo) return []
|
||||||
|
|
||||||
|
const model = $backendUiStore.models.find(m => m._id === modelId)
|
||||||
|
parameters.modelId = modelId
|
||||||
|
return Object.keys(model.schema).map(k => ({
|
||||||
|
name: k,
|
||||||
|
type: model.schema[k].type,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFieldsChanged = e => {
|
||||||
|
parameters.fields = e.detail
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
{#if idFields.length === 0}
|
||||||
|
<div class="cannot-use">
|
||||||
|
Update record can only be used within a component that provides data, such
|
||||||
|
as a List
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Label size="m" color="dark">Datasource</Label>
|
||||||
|
<Select secondary bind:value={parameters.contextPath}>
|
||||||
|
<option value="" />
|
||||||
|
{#each idFields as idField}
|
||||||
|
<option value={idBindingToContextPath(idField.runtimeBinding)}>
|
||||||
|
{idField.instance._instanceName}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</Select>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if parameters.contextPath}
|
||||||
|
<SaveFields
|
||||||
|
parameterFields={parameters.fields}
|
||||||
|
{schemaFields}
|
||||||
|
on:fieldschanged={onFieldsChanged} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
display: grid;
|
||||||
|
column-gap: var(--spacing-s);
|
||||||
|
row-gap: var(--spacing-s);
|
||||||
|
grid-template-columns: auto 1fr auto 1fr auto;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(> div:nth-child(2)) {
|
||||||
|
grid-column-start: 2;
|
||||||
|
grid-column-end: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cannot-use {
|
||||||
|
color: var(--red);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
text-align: center;
|
||||||
|
width: 70%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,5 @@
|
||||||
import NavigateTo from "./NavigateTo.svelte"
|
import NavigateTo from "./NavigateTo.svelte"
|
||||||
import UpdateRecord from "./UpdateRecord.svelte"
|
import SaveRecord from "./SaveRecord.svelte"
|
||||||
import CreateRecord from "./CreateRecord.svelte"
|
|
||||||
|
|
||||||
// defines what actions are available, when adding a new one
|
// defines what actions are available, when adding a new one
|
||||||
// the component is the setup panel for the action
|
// the component is the setup panel for the action
|
||||||
|
@ -9,15 +8,11 @@ import CreateRecord from "./CreateRecord.svelte"
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
name: "Create Record",
|
name: "Save Record",
|
||||||
component: CreateRecord,
|
component: SaveRecord,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Navigate To",
|
name: "Navigate To",
|
||||||
component: NavigateTo,
|
component: NavigateTo,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "Update Record",
|
|
||||||
component: UpdateRecord,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,27 +1,49 @@
|
||||||
<script>
|
<script>
|
||||||
import { store } from "builderStore"
|
import { store, backendUiStore } from "builderStore"
|
||||||
import { Input, Select, ModalContent } from "@budibase/bbui"
|
import { Input, Button, Spacer, Select, ModalContent } from "@budibase/bbui"
|
||||||
import { find, filter, some } from "lodash/fp"
|
import getTemplates from "builderStore/store/screenTemplates"
|
||||||
|
import { some } from "lodash/fp"
|
||||||
|
|
||||||
|
const CONTAINER = "@budibase/standard-components/container"
|
||||||
|
|
||||||
let dialog
|
|
||||||
let layoutComponents
|
|
||||||
let layoutComponent
|
|
||||||
let screens
|
|
||||||
let name = ""
|
let name = ""
|
||||||
let routeError
|
let routeError
|
||||||
|
let baseComponent = CONTAINER
|
||||||
|
let templateIndex
|
||||||
|
let draftScreen
|
||||||
|
|
||||||
$: layoutComponents = Object.values($store.components).filter(
|
$: templates = getTemplates($store, $backendUiStore.models)
|
||||||
componentDefinition => componentDefinition.container
|
|
||||||
)
|
|
||||||
|
|
||||||
$: layoutComponent = layoutComponent
|
|
||||||
? layoutComponents.find(
|
|
||||||
component => component._component === layoutComponent._component
|
|
||||||
)
|
|
||||||
: layoutComponents[0]
|
|
||||||
|
|
||||||
$: route = !route && $store.screens.length === 0 ? "*" : route
|
$: route = !route && $store.screens.length === 0 ? "*" : route
|
||||||
|
|
||||||
|
$: baseComponents = Object.values($store.components)
|
||||||
|
.filter(componentDefinition => componentDefinition.baseComponent)
|
||||||
|
.map(c => c._component)
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (templates && templateIndex === undefined) {
|
||||||
|
templateIndex = 0
|
||||||
|
templateChanged(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateChanged = newTemplateIndex => {
|
||||||
|
if (newTemplateIndex === undefined) return
|
||||||
|
|
||||||
|
draftScreen = templates[newTemplateIndex].create()
|
||||||
|
if (draftScreen.props._instanceName) {
|
||||||
|
name = draftScreen.props._instanceName
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draftScreen.props._component) {
|
||||||
|
baseComponent = draftScreen.props._component
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draftScreen.route) {
|
||||||
|
route = draftScreen.route
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const save = () => {
|
const save = () => {
|
||||||
if (!route) {
|
if (!route) {
|
||||||
routeError = "Url is required"
|
routeError = "Url is required"
|
||||||
|
@ -32,12 +54,23 @@
|
||||||
routeError = ""
|
routeError = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (routeError) {
|
|
||||||
return false
|
if (routeError) return false
|
||||||
}
|
|
||||||
store.createScreen(name, route, layoutComponent._component)
|
draftScreen.props._instanceName = name
|
||||||
|
draftScreen.props._component = baseComponent
|
||||||
|
draftScreen.route = route
|
||||||
|
|
||||||
|
store.createScreen(draftScreen)
|
||||||
|
|
||||||
|
finished()
|
||||||
|
}
|
||||||
|
|
||||||
|
const finished = () => {
|
||||||
|
templateIndex = 0
|
||||||
name = ""
|
name = ""
|
||||||
route = ""
|
route = ""
|
||||||
|
baseComponent = CONTAINER
|
||||||
}
|
}
|
||||||
|
|
||||||
const routeNameExists = route => {
|
const routeNameExists = route => {
|
||||||
|
@ -54,15 +87,25 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent title="New Screen" confirmText="Create Screen" onConfirm={save}>
|
<ModalContent title="New Screen" confirmText="Create Screen" onConfirm={save}>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Choose a Template"
|
||||||
|
bind:value={templateIndex}
|
||||||
|
secondary
|
||||||
|
on:change={ev => templateChanged(ev.target.value)}>
|
||||||
|
{#if templates}
|
||||||
|
{#each templates as template, index}
|
||||||
|
<option value={index}>{template.name}</option>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</Select>
|
||||||
|
|
||||||
<Input label="Name" bind:value={name} />
|
<Input label="Name" bind:value={name} />
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="Url"
|
label="Url"
|
||||||
error={routeError}
|
error={routeError}
|
||||||
bind:value={route}
|
bind:value={route}
|
||||||
on:change={routeChanged} />
|
on:change={routeChanged} />
|
||||||
<Select label="Layout Component" bind:value={layoutComponent} secondary>
|
|
||||||
{#each layoutComponents as { _component, name }}
|
|
||||||
<option value={_component}>{name}</option>
|
|
||||||
{/each}
|
|
||||||
</Select>
|
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
export let componentDefinition = {}
|
export let componentDefinition = {}
|
||||||
export let componentInstance = {}
|
export let componentInstance = {}
|
||||||
export let onChange = () => {}
|
export let onChange = () => {}
|
||||||
|
export let onScreenPropChange = () => {}
|
||||||
export let displayNameField = false
|
export let displayNameField = false
|
||||||
export let screenOrPageInstance
|
export let screenOrPageInstance
|
||||||
|
|
||||||
|
@ -91,7 +92,7 @@
|
||||||
label={def.label}
|
label={def.label}
|
||||||
key={def.key}
|
key={def.key}
|
||||||
value={screenOrPageInstance[def.key]}
|
value={screenOrPageInstance[def.key]}
|
||||||
{onChange}
|
onChange={onScreenPropChange}
|
||||||
props={{ ...excludeProps(def, ['control', 'label']) }} />
|
props={{ ...excludeProps(def, ['control', 'label']) }} />
|
||||||
{/each}
|
{/each}
|
||||||
<hr />
|
<hr />
|
||||||
|
|
|
@ -583,23 +583,7 @@ export default {
|
||||||
icon: "ri-file-edit-line",
|
icon: "ri-file-edit-line",
|
||||||
properties: {
|
properties: {
|
||||||
design: { ...all },
|
design: { ...all },
|
||||||
settings: [
|
settings: [],
|
||||||
{
|
|
||||||
label: "Table",
|
|
||||||
key: "model",
|
|
||||||
control: ModelSelect,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Title",
|
|
||||||
key: "title",
|
|
||||||
control: Input,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Button Text",
|
|
||||||
key: "buttonText",
|
|
||||||
control: Input,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -608,23 +592,7 @@ export default {
|
||||||
icon: "ri-file-edit-line",
|
icon: "ri-file-edit-line",
|
||||||
properties: {
|
properties: {
|
||||||
design: { ...all },
|
design: { ...all },
|
||||||
settings: [
|
settings: [],
|
||||||
{
|
|
||||||
label: "Table",
|
|
||||||
key: "model",
|
|
||||||
control: ModelSelect,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Title",
|
|
||||||
key: "title",
|
|
||||||
control: Input,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Button Text",
|
|
||||||
key: "buttonText",
|
|
||||||
control: Input,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -1125,8 +1093,8 @@ export default {
|
||||||
// children: [],
|
// children: [],
|
||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
name: "Record Detail",
|
name: "Row Detail",
|
||||||
_component: "@budibase/standard-components/recorddetail",
|
_component: "@budibase/standard-components/rowdetail",
|
||||||
description:
|
description:
|
||||||
"Loads a record, using an id from the URL, which can be used with {{ context }}, in children",
|
"Loads a record, using an id from the URL, which can be used with {{ context }}, in children",
|
||||||
icon: "ri-profile-line",
|
icon: "ri-profile-line",
|
||||||
|
@ -1136,6 +1104,18 @@ export default {
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "New Row",
|
||||||
|
_component: "@budibase/standard-components/newrow",
|
||||||
|
description:
|
||||||
|
"Sets up a new record for creation, which can be used with {{ context }}, in children",
|
||||||
|
icon: "ri-profile-line",
|
||||||
|
properties: {
|
||||||
|
design: { ...all },
|
||||||
|
settings: [{ label: "Table", key: "model", control: ModelSelect }],
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// name: "Map",
|
// name: "Map",
|
||||||
// _component: "@budibase/standard-components/datamap",
|
// _component: "@budibase/standard-components/datamap",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { Button } from "@budibase/bbui"
|
import { Button, Spacer } from "@budibase/bbui"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
|
@ -51,6 +51,7 @@
|
||||||
<Button secondary medium on:click={deployApp}>
|
<Button secondary medium on:click={deployApp}>
|
||||||
Deploy App
|
Deploy App
|
||||||
{#if loading}
|
{#if loading}
|
||||||
|
<Spacer extraSmall />
|
||||||
<Spinner size="10" />
|
<Spinner size="10" />
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -5850,10 +5850,10 @@ svelte-portal@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-1.0.0.tgz#36a47c5578b1a4d9b4dc60fa32a904640ec4cdd3"
|
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-1.0.0.tgz#36a47c5578b1a4d9b4dc60fa32a904640ec4cdd3"
|
||||||
integrity sha512-nHf+DS/jZ6jjnZSleBMSaZua9JlG5rZv9lOGKgJuaZStfevtjIlUJrkLc3vbV8QdBvPPVmvcjTlazAzfKu0v3Q==
|
integrity sha512-nHf+DS/jZ6jjnZSleBMSaZua9JlG5rZv9lOGKgJuaZStfevtjIlUJrkLc3vbV8QdBvPPVmvcjTlazAzfKu0v3Q==
|
||||||
|
|
||||||
svelte@^3.24.1:
|
svelte@^3.29.0:
|
||||||
version "3.25.1"
|
version "3.29.0"
|
||||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.25.1.tgz#218def1243fea5a97af6eb60f5e232315bb57ac4"
|
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.29.0.tgz#80acac4254341ad8f3301e5ef03f4127ea967d96"
|
||||||
integrity sha512-IbrVKTmuR0BvDw4ii8/gBNy8REu7nWTRy9uhUz+Yuae5lIjWgSGwKlWtJGC2Vg95s+UnXPqDu0Kk/sUwe0t2GQ==
|
integrity sha512-f+A65eyOQ5ujETLy+igNXtlr6AEjAQLYd1yJE1VwNiXMQO5Z/Vmiy3rL+zblV/9jd7rtTTWqO1IcuXsP2Qv0OA==
|
||||||
|
|
||||||
symbol-observable@^1.1.0:
|
symbol-observable@^1.1.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
|
|
|
@ -52,14 +52,14 @@ const apiOpts = {
|
||||||
delete: del,
|
delete: del,
|
||||||
}
|
}
|
||||||
|
|
||||||
const createRecord = async params =>
|
const saveRecord = async (params, state) =>
|
||||||
await post({
|
await post({
|
||||||
url: `/api/${params.modelId}/records`,
|
url: `/api/${params.modelId}/records`,
|
||||||
body: makeRecordRequestBody(params),
|
body: makeRecordRequestBody(params, state),
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateRecord = async params => {
|
const updateRecord = async (params, state) => {
|
||||||
const record = makeRecordRequestBody(params)
|
const record = makeRecordRequestBody(params, state)
|
||||||
record._id = params._id
|
record._id = params._id
|
||||||
await patch({
|
await patch({
|
||||||
url: `/api/${params.modelId}/records/${params._id}`,
|
url: `/api/${params.modelId}/records/${params._id}`,
|
||||||
|
@ -67,8 +67,14 @@ const updateRecord = async params => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeRecordRequestBody = parameters => {
|
const makeRecordRequestBody = (parameters, state) => {
|
||||||
const body = {}
|
// start with the record thats currently in context
|
||||||
|
const body = { ...(state.data || {}) }
|
||||||
|
|
||||||
|
// dont send the model
|
||||||
|
if (body._model) delete body._model
|
||||||
|
|
||||||
|
// then override with supplied parameters
|
||||||
for (let fieldName in parameters.fields) {
|
for (let fieldName in parameters.fields) {
|
||||||
const field = parameters.fields[fieldName]
|
const field = parameters.fields[fieldName]
|
||||||
|
|
||||||
|
@ -95,6 +101,6 @@ const makeRecordRequestBody = parameters => {
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
authenticate: authenticate(apiOpts),
|
authenticate: authenticate(apiOpts),
|
||||||
createRecord,
|
saveRecord,
|
||||||
updateRecord,
|
updateRecord,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@ export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
||||||
export const eventHandlers = routeTo => {
|
export const eventHandlers = routeTo => {
|
||||||
const handlers = {
|
const handlers = {
|
||||||
"Navigate To": param => routeTo(param && param.url),
|
"Navigate To": param => routeTo(param && param.url),
|
||||||
"Create Record": api.createRecord,
|
|
||||||
"Update Record": api.updateRecord,
|
"Update Record": api.updateRecord,
|
||||||
|
"Save Record": api.saveRecord,
|
||||||
"Trigger Workflow": api.triggerWorkflow,
|
"Trigger Workflow": api.triggerWorkflow,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ export const eventHandlers = routeTo => {
|
||||||
const handler = handlers[action[EVENT_TYPE_MEMBER_NAME]]
|
const handler = handlers[action[EVENT_TYPE_MEMBER_NAME]]
|
||||||
const parameters = createParameters(action.parameters, state)
|
const parameters = createParameters(action.parameters, state)
|
||||||
if (handler) {
|
if (handler) {
|
||||||
await handler(parameters)
|
await handler(parameters, state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,7 @@ const getState = contextStoreKey =>
|
||||||
contextStoreKey ? contextStores[contextStoreKey].state : rootState
|
contextStoreKey ? contextStores[contextStoreKey].state : rootState
|
||||||
|
|
||||||
const getStore = contextStoreKey =>
|
const getStore = contextStoreKey =>
|
||||||
contextStoreKey ? contextStores[contextStoreKey] : rootStore
|
contextStoreKey ? contextStores[contextStoreKey].store : rootStore
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
subscribe,
|
subscribe,
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
.git
|
||||||
|
.gitignore
|
|
@ -46,7 +46,7 @@
|
||||||
"@koa/router": "^8.0.0",
|
"@koa/router": "^8.0.0",
|
||||||
"@sendgrid/mail": "^7.1.1",
|
"@sendgrid/mail": "^7.1.1",
|
||||||
"@sentry/node": "^5.19.2",
|
"@sentry/node": "^5.19.2",
|
||||||
"aws-sdk": "^2.706.0",
|
"aws-sdk": "^2.767.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"chmodr": "^1.2.0",
|
"chmodr": "^1.2.0",
|
||||||
"csvtojson": "^2.0.10",
|
"csvtojson": "^2.0.10",
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
// THIS will create API Keys and App Ids input in a local Dynamo instance if it is running
|
||||||
|
const dynamoClient = require("../src/db/dynamoClient")
|
||||||
|
|
||||||
|
if (process.argv[2] == null || process.argv[3] == null) {
|
||||||
|
console.error(
|
||||||
|
"Inputs incorrect format, was expecting: node createApiKeyAndAppId.js <API_KEY> <APP_ID>"
|
||||||
|
)
|
||||||
|
process.exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FAKE_STRING = "fakestring"
|
||||||
|
|
||||||
|
// set fake credentials for local dynamo to actually work
|
||||||
|
process.env.AWS_ACCESS_KEY_ID = "KEY_ID"
|
||||||
|
process.env.AWS_SECRET_ACCESS_KEY = "SECRET_KEY"
|
||||||
|
dynamoClient.init("http://localhost:8333")
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
await dynamoClient.apiKeyTable.put({
|
||||||
|
item: {
|
||||||
|
pk: process.argv[2],
|
||||||
|
accountId: FAKE_STRING,
|
||||||
|
trackingId: FAKE_STRING,
|
||||||
|
quotaReset: Date.now() + 2592000000,
|
||||||
|
usageQuota: {
|
||||||
|
automationRuns: 0,
|
||||||
|
records: 0,
|
||||||
|
storage: 0,
|
||||||
|
users: 0,
|
||||||
|
views: 0,
|
||||||
|
},
|
||||||
|
usageLimits: {
|
||||||
|
automationRuns: 10,
|
||||||
|
records: 10,
|
||||||
|
storage: 1000,
|
||||||
|
users: 10,
|
||||||
|
views: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await dynamoClient.apiKeyTable.put({
|
||||||
|
item: {
|
||||||
|
pk: process.argv[3],
|
||||||
|
apiKey: process.argv[2],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
|
.then(() => {
|
||||||
|
console.log("Records should have been created.")
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Cannot create records - " + err)
|
||||||
|
})
|
|
@ -2,6 +2,8 @@ const jwt = require("jsonwebtoken")
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const ClientDb = require("../../db/clientDb")
|
const ClientDb = require("../../db/clientDb")
|
||||||
const bcrypt = require("../../utilities/bcrypt")
|
const bcrypt = require("../../utilities/bcrypt")
|
||||||
|
const environment = require("../../environment")
|
||||||
|
const { getAPIKey } = require("../../utilities/usageQuota")
|
||||||
const { generateUserID } = require("../../db/utils")
|
const { generateUserID } = require("../../db/utils")
|
||||||
|
|
||||||
exports.authenticate = async ctx => {
|
exports.authenticate = async ctx => {
|
||||||
|
@ -51,6 +53,11 @@ exports.authenticate = async ctx => {
|
||||||
appId: ctx.user.appId,
|
appId: ctx.user.appId,
|
||||||
instanceId,
|
instanceId,
|
||||||
}
|
}
|
||||||
|
// if in cloud add the user api key
|
||||||
|
if (environment.CLOUD) {
|
||||||
|
const { apiKey } = await getAPIKey(ctx.user.appId)
|
||||||
|
payload.apiKey = apiKey
|
||||||
|
}
|
||||||
|
|
||||||
const token = jwt.sign(payload, ctx.config.jwtSecret, {
|
const token = jwt.sign(payload, ctx.config.jwtSecret, {
|
||||||
expiresIn: "1 day",
|
expiresIn: "1 day",
|
||||||
|
|
|
@ -33,6 +33,7 @@ function cleanAutomationInputs(automation) {
|
||||||
exports.create = async function(ctx) {
|
exports.create = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
let automation = ctx.request.body
|
let automation = ctx.request.body
|
||||||
|
automation.appId = ctx.user.appId
|
||||||
|
|
||||||
automation._id = generateAutomationID()
|
automation._id = generateAutomationID()
|
||||||
|
|
||||||
|
@ -54,6 +55,7 @@ exports.create = async function(ctx) {
|
||||||
exports.update = async function(ctx) {
|
exports.update = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
let automation = ctx.request.body
|
let automation = ctx.request.body
|
||||||
|
automation.appId = ctx.user.appId
|
||||||
|
|
||||||
automation = cleanAutomationInputs(automation)
|
automation = cleanAutomationInputs(automation)
|
||||||
const response = await db.put(automation)
|
const response = await db.put(automation)
|
||||||
|
|
|
@ -4,6 +4,7 @@ const AWS = require("aws-sdk")
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
|
const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
|
||||||
const PouchDB = require("../../../db")
|
const PouchDB = require("../../../db")
|
||||||
|
const environment = require("../../../environment")
|
||||||
|
|
||||||
async function invalidateCDN(cfDistribution, appId) {
|
async function invalidateCDN(cfDistribution, appId) {
|
||||||
const cf = new AWS.CloudFront({})
|
const cf = new AWS.CloudFront({})
|
||||||
|
@ -22,11 +23,46 @@ async function invalidateCDN(cfDistribution, appId) {
|
||||||
.promise()
|
.promise()
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.fetchTemporaryCredentials = async function() {
|
exports.updateDeploymentQuota = async function(quota) {
|
||||||
|
const DEPLOYMENT_SUCCESS_URL =
|
||||||
|
environment.DEPLOYMENT_CREDENTIALS_URL + "deploy/success"
|
||||||
|
|
||||||
|
const response = await fetch(DEPLOYMENT_SUCCESS_URL, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
apiKey: process.env.BUDIBASE_API_KEY,
|
||||||
|
quota,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`Error updating deployment quota for API Key`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await response.json()
|
||||||
|
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the users API key and
|
||||||
|
* Verifies that the deployment fits within the quota of the user,
|
||||||
|
* @param {String} instanceId - instanceId being deployed
|
||||||
|
* @param {String} appId - appId being deployed
|
||||||
|
* @param {quota} quota - current quota being changed with this application
|
||||||
|
*/
|
||||||
|
exports.verifyDeployment = async function({ instanceId, appId, quota }) {
|
||||||
const response = await fetch(process.env.DEPLOYMENT_CREDENTIALS_URL, {
|
const response = await fetch(process.env.DEPLOYMENT_CREDENTIALS_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
apiKey: process.env.BUDIBASE_API_KEY,
|
apiKey: process.env.BUDIBASE_API_KEY,
|
||||||
|
instanceId,
|
||||||
|
appId,
|
||||||
|
quota,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -133,30 +169,33 @@ exports.uploadAppAssets = async function({
|
||||||
|
|
||||||
// Upload file attachments
|
// Upload file attachments
|
||||||
const db = new PouchDB(instanceId)
|
const db = new PouchDB(instanceId)
|
||||||
const fileUploads = await db.get("_local/fileuploads")
|
let fileUploads
|
||||||
if (fileUploads) {
|
try {
|
||||||
for (let file of fileUploads.uploads) {
|
fileUploads = await db.get("_local/fileuploads")
|
||||||
if (file.uploaded) continue
|
} catch (err) {
|
||||||
|
fileUploads = { _id: "_local/fileuploads", uploads: [] }
|
||||||
const attachmentUpload = prepareUploadForS3({
|
|
||||||
file,
|
|
||||||
s3Key: `assets/${appId}/attachments/${file.processedFileName}`,
|
|
||||||
s3,
|
|
||||||
metadata: { accountId },
|
|
||||||
})
|
|
||||||
|
|
||||||
uploads.push(attachmentUpload)
|
|
||||||
|
|
||||||
// mark file as uploaded
|
|
||||||
file.uploaded = true
|
|
||||||
}
|
|
||||||
|
|
||||||
db.put(fileUploads)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let file of fileUploads.uploads) {
|
||||||
|
if (file.uploaded) continue
|
||||||
|
|
||||||
|
const attachmentUpload = prepareUploadForS3({
|
||||||
|
file,
|
||||||
|
s3Key: `assets/${appId}/attachments/${file.processedFileName}`,
|
||||||
|
s3,
|
||||||
|
metadata: { accountId },
|
||||||
|
})
|
||||||
|
|
||||||
|
uploads.push(attachmentUpload)
|
||||||
|
|
||||||
|
// mark file as uploaded
|
||||||
|
file.uploaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
db.put(fileUploads)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(uploads)
|
await Promise.all(uploads)
|
||||||
// TODO: update dynamoDB with a synopsis of the app deployment for historical purposes
|
|
||||||
await invalidateCDN(cfDistribution, appId)
|
await invalidateCDN(cfDistribution, appId)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error uploading budibase app assets to s3", err)
|
console.error("Error uploading budibase app assets to s3", err)
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
const CouchDB = require("pouchdb")
|
const CouchDB = require("pouchdb")
|
||||||
const PouchDB = require("../../../db")
|
const PouchDB = require("../../../db")
|
||||||
const { uploadAppAssets, fetchTemporaryCredentials } = require("./aws")
|
const {
|
||||||
|
uploadAppAssets,
|
||||||
|
verifyDeployment,
|
||||||
|
updateDeploymentQuota,
|
||||||
|
} = require("./aws")
|
||||||
|
const { DocumentTypes, SEPARATOR, UNICODE_MAX } = require("../../../db/utils")
|
||||||
|
|
||||||
function replicate(local, remote) {
|
function replicate(local, remote) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -31,13 +36,49 @@ async function replicateCouch({ instanceId, clientId, credentials }) {
|
||||||
await Promise.all(replications)
|
await Promise.all(replications)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getCurrentInstanceQuota(instanceId) {
|
||||||
|
const db = new PouchDB(instanceId)
|
||||||
|
|
||||||
|
const records = await db.allDocs({
|
||||||
|
startkey: DocumentTypes.RECORD + SEPARATOR,
|
||||||
|
endkey: DocumentTypes.RECORD + SEPARATOR + UNICODE_MAX,
|
||||||
|
})
|
||||||
|
|
||||||
|
const users = await db.allDocs({
|
||||||
|
startkey: DocumentTypes.USER + SEPARATOR,
|
||||||
|
endkey: DocumentTypes.USER + SEPARATOR + UNICODE_MAX,
|
||||||
|
})
|
||||||
|
|
||||||
|
const existingRecords = records.rows.length
|
||||||
|
const existingUsers = users.rows.length
|
||||||
|
|
||||||
|
const designDoc = await db.get("_design/database")
|
||||||
|
|
||||||
|
return {
|
||||||
|
records: existingRecords,
|
||||||
|
users: existingUsers,
|
||||||
|
views: Object.keys(designDoc.views).length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exports.deployApp = async function(ctx) {
|
exports.deployApp = async function(ctx) {
|
||||||
try {
|
try {
|
||||||
const clientAppLookupDB = new PouchDB("client_app_lookup")
|
const clientAppLookupDB = new PouchDB("client_app_lookup")
|
||||||
const { clientId } = await clientAppLookupDB.get(ctx.user.appId)
|
const { clientId } = await clientAppLookupDB.get(ctx.user.appId)
|
||||||
|
|
||||||
|
const instanceQuota = await getCurrentInstanceQuota(ctx.user.instanceId)
|
||||||
|
const credentials = await verifyDeployment({
|
||||||
|
instanceId: ctx.user.instanceId,
|
||||||
|
appId: ctx.user.appId,
|
||||||
|
quota: instanceQuota,
|
||||||
|
})
|
||||||
|
|
||||||
ctx.log.info(`Uploading assets for appID ${ctx.user.appId} assets to s3..`)
|
ctx.log.info(`Uploading assets for appID ${ctx.user.appId} assets to s3..`)
|
||||||
const credentials = await fetchTemporaryCredentials()
|
|
||||||
|
if (credentials.errors) {
|
||||||
|
ctx.throw(500, credentials.errors)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await uploadAppAssets({
|
await uploadAppAssets({
|
||||||
clientId,
|
clientId,
|
||||||
|
@ -54,6 +95,8 @@ exports.deployApp = async function(ctx) {
|
||||||
credentials: credentials.couchDbCreds,
|
credentials: credentials.couchDbCreds,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await updateDeploymentQuota(credentials.quota)
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
status: "SUCCESS",
|
status: "SUCCESS",
|
||||||
completed: Date.now(),
|
completed: Date.now(),
|
||||||
|
|
|
@ -33,17 +33,9 @@ exports.save = async function(ctx) {
|
||||||
views: {},
|
views: {},
|
||||||
...rest,
|
...rest,
|
||||||
}
|
}
|
||||||
// get the model in its previous state for differencing
|
|
||||||
let oldModel
|
// if the model obj had an _id then it will have been retrieved
|
||||||
let oldModelId = ctx.request.body._id
|
const oldModel = ctx.preExisting
|
||||||
if (oldModelId) {
|
|
||||||
// if it errors then the oldModelId is invalid - can't diff it
|
|
||||||
try {
|
|
||||||
oldModel = await db.get(oldModelId)
|
|
||||||
} catch (err) {
|
|
||||||
oldModel = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// rename record fields when table column is renamed
|
// rename record fields when table column is renamed
|
||||||
const { _rename } = modelToSave
|
const { _rename } = modelToSave
|
||||||
|
|
|
@ -77,6 +77,9 @@ exports.save = async function(ctx) {
|
||||||
record._id = generateRecordID(record.modelId)
|
record._id = generateRecordID(record.modelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the record obj had an _id then it will have been retrieved
|
||||||
|
const existingRecord = ctx.preExisting
|
||||||
|
|
||||||
const model = await db.get(record.modelId)
|
const model = await db.get(record.modelId)
|
||||||
|
|
||||||
record = coerceRecordValues(record, model)
|
record = coerceRecordValues(record, model)
|
||||||
|
@ -95,8 +98,6 @@ exports.save = async function(ctx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingRecord = record._rev && (await db.get(record._id))
|
|
||||||
|
|
||||||
// make sure link records are up to date
|
// make sure link records are up to date
|
||||||
record = await linkRecords.updateLinks({
|
record = await linkRecords.updateLinks({
|
||||||
instanceId,
|
instanceId,
|
||||||
|
|
|
@ -153,7 +153,7 @@ exports.serveApp = async function(ctx) {
|
||||||
|
|
||||||
// only set the appId cookie for /appId .. we COULD check for valid appIds
|
// only set the appId cookie for /appId .. we COULD check for valid appIds
|
||||||
// but would like to avoid that DB hit
|
// but would like to avoid that DB hit
|
||||||
const looksLikeAppId = /^[0-9a-f]{32}$/.test(appId)
|
const looksLikeAppId = /^(app_)?[0-9a-f]{32}$/.test(appId)
|
||||||
if (looksLikeAppId && !ctx.isAuthenticated) {
|
if (looksLikeAppId && !ctx.isAuthenticated) {
|
||||||
const anonUser = {
|
const anonUser = {
|
||||||
userId: "ANON",
|
userId: "ANON",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const recordController = require("../controllers/record")
|
const recordController = require("../controllers/record")
|
||||||
const authorized = require("../../middleware/authorized")
|
const authorized = require("../../middleware/authorized")
|
||||||
|
const usage = require("../../middleware/usageQuota")
|
||||||
const { READ_MODEL, WRITE_MODEL } = require("../../utilities/accessLevels")
|
const { READ_MODEL, WRITE_MODEL } = require("../../utilities/accessLevels")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
@ -25,6 +26,7 @@ router
|
||||||
.post(
|
.post(
|
||||||
"/api/:modelId/records",
|
"/api/:modelId/records",
|
||||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||||
|
usage,
|
||||||
recordController.save
|
recordController.save
|
||||||
)
|
)
|
||||||
.patch(
|
.patch(
|
||||||
|
@ -40,6 +42,7 @@ router
|
||||||
.delete(
|
.delete(
|
||||||
"/api/:modelId/records/:recordId/:revId",
|
"/api/:modelId/records/:recordId/:revId",
|
||||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||||
|
usage,
|
||||||
recordController.destroy
|
recordController.destroy
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ const { budibaseTempDir } = require("../../utilities/budibaseDir")
|
||||||
const env = require("../../environment")
|
const env = require("../../environment")
|
||||||
const authorized = require("../../middleware/authorized")
|
const authorized = require("../../middleware/authorized")
|
||||||
const { BUILDER } = require("../../utilities/accessLevels")
|
const { BUILDER } = require("../../utilities/accessLevels")
|
||||||
|
const usage = require("../../middleware/usageQuota")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
|
@ -28,7 +29,7 @@ router
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
controller.performLocalFileProcessing
|
controller.performLocalFileProcessing
|
||||||
)
|
)
|
||||||
.post("/api/attachments/upload", controller.uploadFile)
|
.post("/api/attachments/upload", usage, controller.uploadFile)
|
||||||
.get("/componentlibrary", controller.serveComponentLibrary)
|
.get("/componentlibrary", controller.serveComponentLibrary)
|
||||||
.get("/assets/:file*", controller.serveAppAsset)
|
.get("/assets/:file*", controller.serveAppAsset)
|
||||||
.get("/attachments/:file*", controller.serveAttachment)
|
.get("/attachments/:file*", controller.serveAttachment)
|
||||||
|
|
|
@ -40,6 +40,9 @@ exports.defaultHeaders = (appId, instanceId) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createModel = async (request, appId, instanceId, model) => {
|
exports.createModel = async (request, appId, instanceId, model) => {
|
||||||
|
if (model != null && model._id) {
|
||||||
|
delete model._id
|
||||||
|
}
|
||||||
model = model || {
|
model = model || {
|
||||||
name: "TestModel",
|
name: "TestModel",
|
||||||
type: "model",
|
type: "model",
|
||||||
|
|
|
@ -2,6 +2,7 @@ const Router = require("@koa/router")
|
||||||
const controller = require("../controllers/user")
|
const controller = require("../controllers/user")
|
||||||
const authorized = require("../../middleware/authorized")
|
const authorized = require("../../middleware/authorized")
|
||||||
const { USER_MANAGEMENT, LIST_USERS } = require("../../utilities/accessLevels")
|
const { USER_MANAGEMENT, LIST_USERS } = require("../../utilities/accessLevels")
|
||||||
|
const usage = require("../../middleware/usageQuota")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
|
@ -9,10 +10,11 @@ router
|
||||||
.get("/api/users", authorized(LIST_USERS), controller.fetch)
|
.get("/api/users", authorized(LIST_USERS), controller.fetch)
|
||||||
.get("/api/users/:username", authorized(USER_MANAGEMENT), controller.find)
|
.get("/api/users/:username", authorized(USER_MANAGEMENT), controller.find)
|
||||||
.put("/api/users/", authorized(USER_MANAGEMENT), controller.update)
|
.put("/api/users/", authorized(USER_MANAGEMENT), controller.update)
|
||||||
.post("/api/users", authorized(USER_MANAGEMENT), controller.create)
|
.post("/api/users", authorized(USER_MANAGEMENT), usage, controller.create)
|
||||||
.delete(
|
.delete(
|
||||||
"/api/users/:username",
|
"/api/users/:username",
|
||||||
authorized(USER_MANAGEMENT),
|
authorized(USER_MANAGEMENT),
|
||||||
|
usage,
|
||||||
controller.destroy
|
controller.destroy
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ const viewController = require("../controllers/view")
|
||||||
const recordController = require("../controllers/record")
|
const recordController = require("../controllers/record")
|
||||||
const authorized = require("../../middleware/authorized")
|
const authorized = require("../../middleware/authorized")
|
||||||
const { BUILDER, READ_VIEW } = require("../../utilities/accessLevels")
|
const { BUILDER, READ_VIEW } = require("../../utilities/accessLevels")
|
||||||
|
const usage = require("../../middleware/usageQuota")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
|
@ -13,8 +14,13 @@ router
|
||||||
recordController.fetchView
|
recordController.fetchView
|
||||||
)
|
)
|
||||||
.get("/api/views", authorized(BUILDER), viewController.fetch)
|
.get("/api/views", authorized(BUILDER), viewController.fetch)
|
||||||
.delete("/api/views/:viewName", authorized(BUILDER), viewController.destroy)
|
.delete(
|
||||||
.post("/api/views", authorized(BUILDER), viewController.save)
|
"/api/views/:viewName",
|
||||||
|
authorized(BUILDER),
|
||||||
|
usage,
|
||||||
|
viewController.destroy
|
||||||
|
)
|
||||||
|
.post("/api/views", authorized(BUILDER), usage, viewController.save)
|
||||||
.post("/api/views/export", authorized(BUILDER), viewController.exportView)
|
.post("/api/views/export", authorized(BUILDER), viewController.exportView)
|
||||||
.get(
|
.get(
|
||||||
"/api/views/export/download/:fileName",
|
"/api/views/export/download/:fileName",
|
||||||
|
|
|
@ -3,6 +3,7 @@ const actions = require("./actions")
|
||||||
const environment = require("../environment")
|
const environment = require("../environment")
|
||||||
const workerFarm = require("worker-farm")
|
const workerFarm = require("worker-farm")
|
||||||
const singleThread = require("./thread")
|
const singleThread = require("./thread")
|
||||||
|
const { getAPIKey, update, Properties } = require("../utilities/usageQuota")
|
||||||
|
|
||||||
let workers = workerFarm(require.resolve("./thread"))
|
let workers = workerFarm(require.resolve("./thread"))
|
||||||
|
|
||||||
|
@ -18,16 +19,33 @@ function runWorker(job) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateQuota(automation) {
|
||||||
|
const appId = automation.appId
|
||||||
|
const apiObj = await getAPIKey(appId)
|
||||||
|
// this will fail, causing automation to escape if limits reached
|
||||||
|
await update(apiObj.apiKey, Properties.AUTOMATION, 1)
|
||||||
|
return apiObj.apiKey
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This module is built purely to kick off the worker farm and manage the inputs/outputs
|
* This module is built purely to kick off the worker farm and manage the inputs/outputs
|
||||||
*/
|
*/
|
||||||
module.exports.init = function() {
|
module.exports.init = function() {
|
||||||
actions.init().then(() => {
|
actions.init().then(() => {
|
||||||
triggers.automationQueue.process(async job => {
|
triggers.automationQueue.process(async job => {
|
||||||
if (environment.BUDIBASE_ENVIRONMENT === "PRODUCTION") {
|
try {
|
||||||
await runWorker(job)
|
if (environment.CLOUD && job.data.automation) {
|
||||||
} else {
|
job.data.automation.apiKey = await updateQuota(job.data.automation)
|
||||||
await singleThread(job)
|
}
|
||||||
|
if (environment.BUDIBASE_ENVIRONMENT === "PRODUCTION") {
|
||||||
|
await runWorker(job)
|
||||||
|
} else {
|
||||||
|
await singleThread(job)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
`${job.data.automation.appId} automation ${job.data.automation._id} was unable to run - ${err}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
const recordController = require("../../api/controllers/record")
|
const recordController = require("../../api/controllers/record")
|
||||||
const automationUtils = require("../automationUtils")
|
const automationUtils = require("../automationUtils")
|
||||||
|
const environment = require("../../environment")
|
||||||
|
const usage = require("../../utilities/usageQuota")
|
||||||
|
|
||||||
module.exports.definition = {
|
module.exports.definition = {
|
||||||
name: "Create Row",
|
name: "Create Row",
|
||||||
|
@ -56,7 +58,7 @@ module.exports.definition = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.run = async function({ inputs, instanceId }) {
|
module.exports.run = async function({ inputs, instanceId, apiKey }) {
|
||||||
// TODO: better logging of when actions are missed due to missing parameters
|
// TODO: better logging of when actions are missed due to missing parameters
|
||||||
if (inputs.record == null || inputs.record.modelId == null) {
|
if (inputs.record == null || inputs.record.modelId == null) {
|
||||||
return
|
return
|
||||||
|
@ -78,6 +80,9 @@ module.exports.run = async function({ inputs, instanceId }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (environment.CLOUD) {
|
||||||
|
await usage.update(apiKey, usage.Properties.RECORD, 1)
|
||||||
|
}
|
||||||
await recordController.save(ctx)
|
await recordController.save(ctx)
|
||||||
return {
|
return {
|
||||||
record: inputs.record,
|
record: inputs.record,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
const accessLevels = require("../../utilities/accessLevels")
|
const accessLevels = require("../../utilities/accessLevels")
|
||||||
const userController = require("../../api/controllers/user")
|
const userController = require("../../api/controllers/user")
|
||||||
|
const environment = require("../../environment")
|
||||||
|
const usage = require("../../utilities/usageQuota")
|
||||||
|
|
||||||
module.exports.definition = {
|
module.exports.definition = {
|
||||||
description: "Create a new user",
|
description: "Create a new user",
|
||||||
|
@ -56,7 +58,7 @@ module.exports.definition = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.run = async function({ inputs, instanceId }) {
|
module.exports.run = async function({ inputs, instanceId, apiKey }) {
|
||||||
const { username, password, accessLevelId } = inputs
|
const { username, password, accessLevelId } = inputs
|
||||||
const ctx = {
|
const ctx = {
|
||||||
user: {
|
user: {
|
||||||
|
@ -68,6 +70,9 @@ module.exports.run = async function({ inputs, instanceId }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (environment.CLOUD) {
|
||||||
|
await usage.update(apiKey, usage.Properties.USER, 1)
|
||||||
|
}
|
||||||
await userController.create(ctx)
|
await userController.create(ctx)
|
||||||
return {
|
return {
|
||||||
response: ctx.body,
|
response: ctx.body,
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
const recordController = require("../../api/controllers/record")
|
const recordController = require("../../api/controllers/record")
|
||||||
|
const environment = require("../../environment")
|
||||||
|
const usage = require("../../utilities/usageQuota")
|
||||||
|
|
||||||
module.exports.definition = {
|
module.exports.definition = {
|
||||||
description: "Delete a row from your database",
|
description: "Delete a row from your database",
|
||||||
|
@ -48,7 +50,7 @@ module.exports.definition = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.run = async function({ inputs, instanceId }) {
|
module.exports.run = async function({ inputs, instanceId, apiKey }) {
|
||||||
// TODO: better logging of when actions are missed due to missing parameters
|
// TODO: better logging of when actions are missed due to missing parameters
|
||||||
if (inputs.id == null || inputs.revision == null) {
|
if (inputs.id == null || inputs.revision == null) {
|
||||||
return
|
return
|
||||||
|
@ -63,6 +65,9 @@ module.exports.run = async function({ inputs, instanceId }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (environment.CLOUD) {
|
||||||
|
await usage.update(apiKey, usage.Properties.RECORD, -1)
|
||||||
|
}
|
||||||
await recordController.destroy(ctx)
|
await recordController.destroy(ctx)
|
||||||
return {
|
return {
|
||||||
response: ctx.body,
|
response: ctx.body,
|
||||||
|
|
|
@ -62,6 +62,7 @@ class Orchestrator {
|
||||||
const outputs = await stepFn({
|
const outputs = await stepFn({
|
||||||
inputs: step.inputs,
|
inputs: step.inputs,
|
||||||
instanceId: this._instanceId,
|
instanceId: this._instanceId,
|
||||||
|
apiKey: automation.apiKey,
|
||||||
})
|
})
|
||||||
if (step.stepId === FILTER_STEP_ID && !outputs.success) {
|
if (step.stepId === FILTER_STEP_ID && !outputs.success) {
|
||||||
break
|
break
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
let _ = require("lodash")
|
||||||
|
let environment = require("../environment")
|
||||||
|
|
||||||
|
const AWS_REGION = environment.AWS_REGION ? environment.AWS_REGION : "eu-west-1"
|
||||||
|
|
||||||
|
const TableInfo = {
|
||||||
|
API_KEYS: {
|
||||||
|
name: "beta-api-key-table",
|
||||||
|
primary: "pk",
|
||||||
|
},
|
||||||
|
USERS: {
|
||||||
|
name: "prod-budi-table",
|
||||||
|
primary: "pk",
|
||||||
|
sort: "sk",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let docClient = null
|
||||||
|
|
||||||
|
class Table {
|
||||||
|
constructor(tableInfo) {
|
||||||
|
if (!tableInfo.name || !tableInfo.primary) {
|
||||||
|
throw "Table info must specify a name and a primary key"
|
||||||
|
}
|
||||||
|
this._name = tableInfo.name
|
||||||
|
this._primary = tableInfo.primary
|
||||||
|
this._sort = tableInfo.sort
|
||||||
|
}
|
||||||
|
|
||||||
|
async get({ primary, sort, otherProps }) {
|
||||||
|
let params = {
|
||||||
|
TableName: this._name,
|
||||||
|
Key: {
|
||||||
|
[this._primary]: primary,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (this._sort && sort) {
|
||||||
|
params.Key[this._sort] = sort
|
||||||
|
}
|
||||||
|
if (otherProps) {
|
||||||
|
params = _.merge(params, otherProps)
|
||||||
|
}
|
||||||
|
let response = await docClient.get(params).promise()
|
||||||
|
return response.Item
|
||||||
|
}
|
||||||
|
|
||||||
|
async update({
|
||||||
|
primary,
|
||||||
|
sort,
|
||||||
|
expression,
|
||||||
|
condition,
|
||||||
|
names,
|
||||||
|
values,
|
||||||
|
exists,
|
||||||
|
otherProps,
|
||||||
|
}) {
|
||||||
|
let params = {
|
||||||
|
TableName: this._name,
|
||||||
|
Key: {
|
||||||
|
[this._primary]: primary,
|
||||||
|
},
|
||||||
|
ExpressionAttributeNames: names,
|
||||||
|
ExpressionAttributeValues: values,
|
||||||
|
UpdateExpression: expression,
|
||||||
|
}
|
||||||
|
if (condition) {
|
||||||
|
params.ConditionExpression = condition
|
||||||
|
}
|
||||||
|
if (this._sort && sort) {
|
||||||
|
params.Key[this._sort] = sort
|
||||||
|
}
|
||||||
|
if (exists) {
|
||||||
|
params.ExpressionAttributeNames["#PRIMARY"] = this._primary
|
||||||
|
if (params.ConditionExpression) {
|
||||||
|
params.ConditionExpression += " AND "
|
||||||
|
}
|
||||||
|
params.ConditionExpression += "attribute_exists(#PRIMARY)"
|
||||||
|
}
|
||||||
|
if (otherProps) {
|
||||||
|
params = _.merge(params, otherProps)
|
||||||
|
}
|
||||||
|
return docClient.update(params).promise()
|
||||||
|
}
|
||||||
|
|
||||||
|
async put({ item, otherProps }) {
|
||||||
|
if (
|
||||||
|
item[this._primary] == null ||
|
||||||
|
(this._sort && item[this._sort] == null)
|
||||||
|
) {
|
||||||
|
throw "Cannot put item without primary and sort key (if required)"
|
||||||
|
}
|
||||||
|
let params = {
|
||||||
|
TableName: this._name,
|
||||||
|
Item: item,
|
||||||
|
}
|
||||||
|
if (otherProps) {
|
||||||
|
params = _.merge(params, otherProps)
|
||||||
|
}
|
||||||
|
return docClient.put(params).promise()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.init = endpoint => {
|
||||||
|
let AWS = require("aws-sdk")
|
||||||
|
AWS.config.update({
|
||||||
|
region: AWS_REGION,
|
||||||
|
})
|
||||||
|
let docClientParams = {
|
||||||
|
correctClockSkew: true,
|
||||||
|
}
|
||||||
|
if (endpoint) {
|
||||||
|
docClientParams.endpoint = endpoint
|
||||||
|
} else if (environment.DYNAMO_ENDPOINT) {
|
||||||
|
docClientParams.endpoint = environment.DYNAMO_ENDPOINT
|
||||||
|
}
|
||||||
|
docClient = new AWS.DynamoDB.DocumentClient(docClientParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.apiKeyTable = new Table(TableInfo.API_KEYS)
|
||||||
|
exports.userTable = new Table(TableInfo.USERS)
|
||||||
|
|
||||||
|
if (environment.CLOUD) {
|
||||||
|
exports.init(`https://dynamodb.${AWS_REGION}.amazonaws.com`)
|
||||||
|
} else {
|
||||||
|
process.env.AWS_ACCESS_KEY_ID = "KEY_ID"
|
||||||
|
process.env.AWS_SECRET_ACCESS_KEY = "SECRET_KEY"
|
||||||
|
exports.init("http://localhost:8333")
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
const LinkController = require("./LinkController")
|
const LinkController = require("./LinkController")
|
||||||
const { IncludeDocs, getLinkDocuments, createLinkView } = require("./linkUtils")
|
const { IncludeDocs, getLinkDocuments, createLinkView } = require("./linkUtils")
|
||||||
|
const _ = require("lodash")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This functionality makes sure that when records with links are created, updated or deleted they are processed
|
* This functionality makes sure that when records with links are created, updated or deleted they are processed
|
||||||
|
@ -88,23 +89,23 @@ exports.attachLinkInfo = async (instanceId, records) => {
|
||||||
records = [records]
|
records = [records]
|
||||||
wasArray = false
|
wasArray = false
|
||||||
}
|
}
|
||||||
|
let modelIds = [...new Set(records.map(el => el.modelId))]
|
||||||
// start by getting all the link values for performance reasons
|
// start by getting all the link values for performance reasons
|
||||||
let responses = await Promise.all(
|
let responses = _.flatten(
|
||||||
records.map(record =>
|
await Promise.all(
|
||||||
getLinkDocuments({
|
modelIds.map(modelId =>
|
||||||
instanceId,
|
getLinkDocuments({
|
||||||
modelId: record.modelId,
|
instanceId,
|
||||||
recordId: record._id,
|
modelId: modelId,
|
||||||
includeDocs: IncludeDocs.EXCLUDE,
|
includeDocs: IncludeDocs.EXCLUDE,
|
||||||
})
|
})
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// can just use an index to access responses, order maintained
|
|
||||||
let index = 0
|
|
||||||
// now iterate through the records and all field information
|
// now iterate through the records and all field information
|
||||||
for (let record of records) {
|
for (let record of records) {
|
||||||
// get all links for record, ignore fieldName for now
|
// get all links for record, ignore fieldName for now
|
||||||
const linkVals = responses[index++]
|
const linkVals = responses.filter(el => el.thisId === record._id)
|
||||||
for (let linkVal of linkVals) {
|
for (let linkVal of linkVals) {
|
||||||
// work out which link pertains to this record
|
// work out which link pertains to this record
|
||||||
if (!(record[linkVal.fieldName] instanceof Array)) {
|
if (!(record[linkVal.fieldName] instanceof Array)) {
|
||||||
|
|
|
@ -27,10 +27,12 @@ exports.createLinkView = async instanceId => {
|
||||||
let doc2 = doc.doc2
|
let doc2 = doc.doc2
|
||||||
emit([doc1.modelId, doc1.recordId], {
|
emit([doc1.modelId, doc1.recordId], {
|
||||||
id: doc2.recordId,
|
id: doc2.recordId,
|
||||||
|
thisId: doc1.recordId,
|
||||||
fieldName: doc1.fieldName,
|
fieldName: doc1.fieldName,
|
||||||
})
|
})
|
||||||
emit([doc2.modelId, doc2.recordId], {
|
emit([doc2.modelId, doc2.recordId], {
|
||||||
id: doc1.recordId,
|
id: doc1.recordId,
|
||||||
|
thisId: doc2.recordId,
|
||||||
fieldName: doc2.fieldName,
|
fieldName: doc2.fieldName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ const DocumentTypes = {
|
||||||
|
|
||||||
exports.DocumentTypes = DocumentTypes
|
exports.DocumentTypes = DocumentTypes
|
||||||
exports.SEPARATOR = SEPARATOR
|
exports.SEPARATOR = SEPARATOR
|
||||||
|
exports.UNICODE_MAX = UNICODE_MAX
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If creating DB allDocs/query params with only a single top level ID this can be used, this
|
* If creating DB allDocs/query params with only a single top level ID this can be used, this
|
||||||
|
|
|
@ -11,4 +11,8 @@ module.exports = {
|
||||||
AUTOMATION_BUCKET: process.env.AUTOMATION_BUCKET,
|
AUTOMATION_BUCKET: process.env.AUTOMATION_BUCKET,
|
||||||
BUDIBASE_ENVIRONMENT: process.env.BUDIBASE_ENVIRONMENT,
|
BUDIBASE_ENVIRONMENT: process.env.BUDIBASE_ENVIRONMENT,
|
||||||
SENDGRID_API_KEY: process.env.SENDGRID_API_KEY,
|
SENDGRID_API_KEY: process.env.SENDGRID_API_KEY,
|
||||||
|
CLOUD: process.env.CLOUD,
|
||||||
|
DYNAMO_ENDPOINT: process.env.DYNAMO_ENDPOINT,
|
||||||
|
AWS_REGION: process.env.AWS_REGION,
|
||||||
|
DEPLOYMENT_CREDENTIALS_URL: process.env.DEPLOYMENT_CREDENTIALS_URL,
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ module.exports = async (ctx, next) => {
|
||||||
if (builderToken) {
|
if (builderToken) {
|
||||||
try {
|
try {
|
||||||
const jwtPayload = jwt.verify(builderToken, ctx.config.jwtSecret)
|
const jwtPayload = jwt.verify(builderToken, ctx.config.jwtSecret)
|
||||||
|
ctx.apiKey = jwtPayload.apiKey
|
||||||
ctx.isAuthenticated = jwtPayload.accessLevelId === BUILDER_LEVEL_ID
|
ctx.isAuthenticated = jwtPayload.accessLevelId === BUILDER_LEVEL_ID
|
||||||
ctx.user = {
|
ctx.user = {
|
||||||
...jwtPayload,
|
...jwtPayload,
|
||||||
|
@ -44,7 +45,7 @@ module.exports = async (ctx, next) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const jwtPayload = jwt.verify(appToken, ctx.config.jwtSecret)
|
const jwtPayload = jwt.verify(appToken, ctx.config.jwtSecret)
|
||||||
|
ctx.apiKey = jwtPayload.apiKey
|
||||||
ctx.user = {
|
ctx.user = {
|
||||||
...jwtPayload,
|
...jwtPayload,
|
||||||
accessLevel: await getAccessLevel(
|
accessLevel: await getAccessLevel(
|
||||||
|
|
|
@ -16,8 +16,7 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.user.accessLevel._id === BUILDER_LEVEL_ID) {
|
if (ctx.user.accessLevel._id === BUILDER_LEVEL_ID) {
|
||||||
await next()
|
return next()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (permName === BUILDER) {
|
if (permName === BUILDER) {
|
||||||
|
@ -28,8 +27,7 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
|
||||||
const permissionId = ({ name, itemId }) => name + (itemId ? `-${itemId}` : "")
|
const permissionId = ({ name, itemId }) => name + (itemId ? `-${itemId}` : "")
|
||||||
|
|
||||||
if (ctx.user.accessLevel._id === ADMIN_LEVEL_ID) {
|
if (ctx.user.accessLevel._id === ADMIN_LEVEL_ID) {
|
||||||
await next()
|
return next()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const thisPermissionId = permissionId({
|
const thisPermissionId = permissionId({
|
||||||
|
@ -42,8 +40,7 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
|
||||||
ctx.user.accessLevel._id === POWERUSER_LEVEL_ID &&
|
ctx.user.accessLevel._id === POWERUSER_LEVEL_ID &&
|
||||||
!adminPermissions.map(permissionId).includes(thisPermissionId)
|
!adminPermissions.map(permissionId).includes(thisPermissionId)
|
||||||
) {
|
) {
|
||||||
await next()
|
return next()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -51,8 +48,7 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
|
||||||
.map(permissionId)
|
.map(permissionId)
|
||||||
.includes(thisPermissionId)
|
.includes(thisPermissionId)
|
||||||
) {
|
) {
|
||||||
await next()
|
return next()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.throw(403, "Not Authorized")
|
ctx.throw(403, "Not Authorized")
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
const CouchDB = require("../db")
|
||||||
|
const usageQuota = require("../utilities/usageQuota")
|
||||||
|
const environment = require("../environment")
|
||||||
|
|
||||||
|
// currently only counting new writes and deletes
|
||||||
|
const METHOD_MAP = {
|
||||||
|
POST: 1,
|
||||||
|
DELETE: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DOMAIN_MAP = {
|
||||||
|
records: usageQuota.Properties.RECORD,
|
||||||
|
upload: usageQuota.Properties.UPLOAD,
|
||||||
|
views: usageQuota.Properties.VIEW,
|
||||||
|
users: usageQuota.Properties.USER,
|
||||||
|
// this will not be updated by endpoint calls
|
||||||
|
// instead it will be updated by triggers
|
||||||
|
automationRuns: usageQuota.Properties.AUTOMATION,
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProperty(url) {
|
||||||
|
for (let domain of Object.keys(DOMAIN_MAP)) {
|
||||||
|
if (url.indexOf(domain) !== -1) {
|
||||||
|
return DOMAIN_MAP[domain]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = async (ctx, next) => {
|
||||||
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
|
let usage = METHOD_MAP[ctx.req.method]
|
||||||
|
const property = getProperty(ctx.req.url)
|
||||||
|
if (usage == null || property == null) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
// post request could be a save of a pre-existing entry
|
||||||
|
if (ctx.request.body && ctx.request.body._id) {
|
||||||
|
try {
|
||||||
|
ctx.preExisting = await db.get(ctx.request.body._id)
|
||||||
|
return next()
|
||||||
|
} catch (err) {
|
||||||
|
ctx.throw(404, `${ctx.request.body._id} does not exist`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// update usage for uploads to be the total size
|
||||||
|
if (property === usageQuota.Properties.UPLOAD) {
|
||||||
|
const files =
|
||||||
|
ctx.request.files.file.length > 1
|
||||||
|
? Array.from(ctx.request.files.file)
|
||||||
|
: [ctx.request.files.file]
|
||||||
|
usage = files.map(file => file.size).reduce((total, size) => total + size)
|
||||||
|
}
|
||||||
|
if (!environment.CLOUD) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await usageQuota.update(ctx.apiKey, property, usage)
|
||||||
|
return next()
|
||||||
|
} catch (err) {
|
||||||
|
ctx.throw(403, err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,9 @@ module.exports = (ctx, appId, instanceId) => {
|
||||||
instanceId,
|
instanceId,
|
||||||
appId,
|
appId,
|
||||||
}
|
}
|
||||||
|
if (process.env.BUDIBASE_API_KEY) {
|
||||||
|
builderUser.apiKey = process.env.BUDIBASE_API_KEY
|
||||||
|
}
|
||||||
const token = jwt.sign(builderUser, ctx.config.jwtSecret, {
|
const token = jwt.sign(builderUser, ctx.config.jwtSecret, {
|
||||||
expiresIn: "30 days",
|
expiresIn: "30 days",
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
const environment = require("../environment")
|
||||||
|
const { apiKeyTable } = require("../db/dynamoClient")
|
||||||
|
|
||||||
|
const DEFAULT_USAGE = {
|
||||||
|
records: 0,
|
||||||
|
storage: 0,
|
||||||
|
views: 0,
|
||||||
|
automationRuns: 0,
|
||||||
|
users: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_PLAN = {
|
||||||
|
records: 1000,
|
||||||
|
// 1 GB
|
||||||
|
storage: 8589934592,
|
||||||
|
views: 10,
|
||||||
|
automationRuns: 100,
|
||||||
|
users: 10000,
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUpdateParams(key, property, usage) {
|
||||||
|
return {
|
||||||
|
primary: key,
|
||||||
|
condition:
|
||||||
|
"attribute_exists(#quota) AND attribute_exists(#limits) AND #quota.#prop < #limits.#prop AND #quotaReset > :now",
|
||||||
|
expression: "ADD #quota.#prop :usage",
|
||||||
|
names: {
|
||||||
|
"#quota": "usageQuota",
|
||||||
|
"#prop": property,
|
||||||
|
"#limits": "usageLimits",
|
||||||
|
"#quotaReset": "quotaReset",
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
":usage": usage,
|
||||||
|
":now": Date.now(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNewQuotaReset() {
|
||||||
|
return Date.now() + 2592000000
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.Properties = {
|
||||||
|
RECORD: "records",
|
||||||
|
UPLOAD: "storage",
|
||||||
|
VIEW: "views",
|
||||||
|
USER: "users",
|
||||||
|
AUTOMATION: "automationRuns",
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getAPIKey = async appId => {
|
||||||
|
return apiKeyTable.get({ primary: appId })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a specified API key this will add to the usage object for the specified property.
|
||||||
|
* @param {string} apiKey The API key which is to be updated.
|
||||||
|
* @param {string} property The property which is to be added to (within the nested usageQuota object).
|
||||||
|
* @param {number} usage The amount (this can be negative) to adjust the number by.
|
||||||
|
* @returns {Promise<void>} When this completes the API key will now be up to date - the quota period may have
|
||||||
|
* also been reset after this call.
|
||||||
|
*/
|
||||||
|
exports.update = async (apiKey, property, usage) => {
|
||||||
|
// don't try validate in builder
|
||||||
|
if (!environment.CLOUD) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await apiKeyTable.update(buildUpdateParams(apiKey, property, usage))
|
||||||
|
} catch (err) {
|
||||||
|
// conditional check means the condition failed, need to check why
|
||||||
|
if (err.code === "ConditionalCheckFailedException") {
|
||||||
|
// get the API key so we can check it
|
||||||
|
const keyObj = await apiKeyTable.get({ primary: apiKey })
|
||||||
|
// the usage quota or usage limits didn't exist
|
||||||
|
if (keyObj && (keyObj.usageQuota == null || keyObj.usageLimits == null)) {
|
||||||
|
keyObj.usageQuota =
|
||||||
|
keyObj.usageQuota == null ? DEFAULT_USAGE : keyObj.usageQuota
|
||||||
|
keyObj.usageLimits =
|
||||||
|
keyObj.usageLimits == null ? DEFAULT_PLAN : keyObj.usageLimits
|
||||||
|
keyObj.quotaReset = getNewQuotaReset()
|
||||||
|
await apiKeyTable.put({ item: keyObj })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// we have infact breached the reset period
|
||||||
|
else if (keyObj && keyObj.quotaReset <= Date.now()) {
|
||||||
|
// update the quota reset period and reset the values for all properties
|
||||||
|
keyObj.quotaReset = getNewQuotaReset()
|
||||||
|
for (let prop of Object.keys(keyObj.usageQuota)) {
|
||||||
|
if (prop === property) {
|
||||||
|
keyObj.usageQuota[prop] = usage > 0 ? usage : 0
|
||||||
|
} else {
|
||||||
|
keyObj.usageQuota[prop] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await apiKeyTable.put({ item: keyObj })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
|
@ -172,15 +172,6 @@
|
||||||
lodash "^4.17.13"
|
lodash "^4.17.13"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
"@budibase/client@^0.1.25":
|
|
||||||
version "0.1.25"
|
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.1.25.tgz#f08c4a614f9018eb0f0faa6d20bb05f7a3215c70"
|
|
||||||
integrity sha512-vZ0cqJwLYcs7MHihFnJO3qOe7qxibnB4Va1+IYNfnPc9kcxy4KvfQxCx/G/DDxP9CXfEvsguy9ymzR3RUAvBHw==
|
|
||||||
dependencies:
|
|
||||||
deep-equal "^2.0.1"
|
|
||||||
mustache "^4.0.1"
|
|
||||||
regexparam "^1.3.0"
|
|
||||||
|
|
||||||
"@cnakazawa/watch@^1.0.3":
|
"@cnakazawa/watch@^1.0.3":
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
|
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
|
||||||
|
@ -876,11 +867,6 @@ array-equal@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
|
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
|
||||||
|
|
||||||
array-filter@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
|
|
||||||
integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
|
|
||||||
|
|
||||||
array-unique@^0.3.2:
|
array-unique@^0.3.2:
|
||||||
version "0.3.2"
|
version "0.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
|
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
|
||||||
|
@ -939,16 +925,10 @@ atomic-sleep@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
|
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
|
||||||
|
|
||||||
available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2:
|
aws-sdk@^2.767.0:
|
||||||
version "1.0.2"
|
version "2.767.0"
|
||||||
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5"
|
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.767.0.tgz#9863c8bfd5990106b95f38e9345a547fee782470"
|
||||||
integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==
|
integrity sha512-soPZxjNpat0CtuIqm54GO/FDT4SZTlQG0icSptWYfMFYdkXe8b0tJqaPssNn9TzlgoWDCNTdaoepM6TN0rNHkQ==
|
||||||
dependencies:
|
|
||||||
array-filter "^1.0.0"
|
|
||||||
|
|
||||||
aws-sdk@^2.706.0:
|
|
||||||
version "2.706.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.706.0.tgz#09f65e9a91ecac5a635daf934082abae30eca953"
|
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer "4.9.2"
|
buffer "4.9.2"
|
||||||
events "1.1.1"
|
events "1.1.1"
|
||||||
|
@ -1081,6 +1061,7 @@ bluebird-lst@^1.0.9:
|
||||||
bluebird@^3.5.1, bluebird@^3.5.5:
|
bluebird@^3.5.1, bluebird@^3.5.5:
|
||||||
version "3.7.2"
|
version "3.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
|
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||||
|
|
||||||
boolean@^3.0.0, boolean@^3.0.1:
|
boolean@^3.0.0, boolean@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
|
@ -1755,26 +1736,6 @@ decompress@^4.2.1:
|
||||||
pify "^2.3.0"
|
pify "^2.3.0"
|
||||||
strip-dirs "^2.0.0"
|
strip-dirs "^2.0.0"
|
||||||
|
|
||||||
deep-equal@^2.0.1:
|
|
||||||
version "2.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.4.tgz#6b0b407a074666033169df3acaf128e1c6f3eab6"
|
|
||||||
integrity sha512-BUfaXrVoCfgkOQY/b09QdO9L3XNoF2XH0A3aY9IQwQL/ZjLOe8FQgCNVl1wiolhsFo8kFdO9zdPViCPbmaJA5w==
|
|
||||||
dependencies:
|
|
||||||
es-abstract "^1.18.0-next.1"
|
|
||||||
es-get-iterator "^1.1.0"
|
|
||||||
is-arguments "^1.0.4"
|
|
||||||
is-date-object "^1.0.2"
|
|
||||||
is-regex "^1.1.1"
|
|
||||||
isarray "^2.0.5"
|
|
||||||
object-is "^1.1.3"
|
|
||||||
object-keys "^1.1.1"
|
|
||||||
object.assign "^4.1.1"
|
|
||||||
regexp.prototype.flags "^1.3.0"
|
|
||||||
side-channel "^1.0.3"
|
|
||||||
which-boxed-primitive "^1.0.1"
|
|
||||||
which-collection "^1.0.1"
|
|
||||||
which-typed-array "^1.1.2"
|
|
||||||
|
|
||||||
deep-equal@~1.0.1:
|
deep-equal@~1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
|
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
|
||||||
|
@ -2124,54 +2085,6 @@ es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
|
||||||
string.prototype.trimleft "^2.1.1"
|
string.prototype.trimleft "^2.1.1"
|
||||||
string.prototype.trimright "^2.1.1"
|
string.prototype.trimright "^2.1.1"
|
||||||
|
|
||||||
es-abstract@^1.17.4:
|
|
||||||
version "1.17.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c"
|
|
||||||
integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==
|
|
||||||
dependencies:
|
|
||||||
es-to-primitive "^1.2.1"
|
|
||||||
function-bind "^1.1.1"
|
|
||||||
has "^1.0.3"
|
|
||||||
has-symbols "^1.0.1"
|
|
||||||
is-callable "^1.2.2"
|
|
||||||
is-regex "^1.1.1"
|
|
||||||
object-inspect "^1.8.0"
|
|
||||||
object-keys "^1.1.1"
|
|
||||||
object.assign "^4.1.1"
|
|
||||||
string.prototype.trimend "^1.0.1"
|
|
||||||
string.prototype.trimstart "^1.0.1"
|
|
||||||
|
|
||||||
es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1:
|
|
||||||
version "1.18.0-next.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68"
|
|
||||||
integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==
|
|
||||||
dependencies:
|
|
||||||
es-to-primitive "^1.2.1"
|
|
||||||
function-bind "^1.1.1"
|
|
||||||
has "^1.0.3"
|
|
||||||
has-symbols "^1.0.1"
|
|
||||||
is-callable "^1.2.2"
|
|
||||||
is-negative-zero "^2.0.0"
|
|
||||||
is-regex "^1.1.1"
|
|
||||||
object-inspect "^1.8.0"
|
|
||||||
object-keys "^1.1.1"
|
|
||||||
object.assign "^4.1.1"
|
|
||||||
string.prototype.trimend "^1.0.1"
|
|
||||||
string.prototype.trimstart "^1.0.1"
|
|
||||||
|
|
||||||
es-get-iterator@^1.1.0:
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8"
|
|
||||||
integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==
|
|
||||||
dependencies:
|
|
||||||
es-abstract "^1.17.4"
|
|
||||||
has-symbols "^1.0.1"
|
|
||||||
is-arguments "^1.0.4"
|
|
||||||
is-map "^2.0.1"
|
|
||||||
is-set "^2.0.1"
|
|
||||||
is-string "^1.0.5"
|
|
||||||
isarray "^2.0.5"
|
|
||||||
|
|
||||||
es-to-primitive@^1.2.1:
|
es-to-primitive@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
|
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
|
||||||
|
@ -3211,11 +3124,6 @@ is-accessor-descriptor@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
kind-of "^6.0.0"
|
kind-of "^6.0.0"
|
||||||
|
|
||||||
is-arguments@^1.0.4:
|
|
||||||
version "1.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
|
|
||||||
integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
|
|
||||||
|
|
||||||
is-arrayish@^0.2.1:
|
is-arrayish@^0.2.1:
|
||||||
version "0.2.1"
|
version "0.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||||
|
@ -3225,22 +3133,12 @@ is-arrayish@^0.3.1:
|
||||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
|
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
|
||||||
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
|
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
|
||||||
|
|
||||||
is-bigint@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4"
|
|
||||||
integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==
|
|
||||||
|
|
||||||
is-binary-path@~2.1.0:
|
is-binary-path@~2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
|
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
|
||||||
dependencies:
|
dependencies:
|
||||||
binary-extensions "^2.0.0"
|
binary-extensions "^2.0.0"
|
||||||
|
|
||||||
is-boolean-object@^1.0.0:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e"
|
|
||||||
integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==
|
|
||||||
|
|
||||||
is-buffer@^1.1.5:
|
is-buffer@^1.1.5:
|
||||||
version "1.1.6"
|
version "1.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||||
|
@ -3249,11 +3147,6 @@ is-callable@^1.1.4, is-callable@^1.1.5:
|
||||||
version "1.1.5"
|
version "1.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
|
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
|
||||||
|
|
||||||
is-callable@^1.2.2:
|
|
||||||
version "1.2.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
|
|
||||||
integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
|
|
||||||
|
|
||||||
is-ci@^2.0.0:
|
is-ci@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
|
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
|
||||||
|
@ -3276,7 +3169,7 @@ is-data-descriptor@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
kind-of "^6.0.0"
|
kind-of "^6.0.0"
|
||||||
|
|
||||||
is-date-object@^1.0.1, is-date-object@^1.0.2:
|
is-date-object@^1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
|
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
|
||||||
|
|
||||||
|
@ -3346,30 +3239,15 @@ is-installed-globally@^0.3.1:
|
||||||
global-dirs "^2.0.1"
|
global-dirs "^2.0.1"
|
||||||
is-path-inside "^3.0.1"
|
is-path-inside "^3.0.1"
|
||||||
|
|
||||||
is-map@^2.0.1:
|
|
||||||
version "2.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1"
|
|
||||||
integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==
|
|
||||||
|
|
||||||
is-natural-number@^4.0.1:
|
is-natural-number@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8"
|
resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8"
|
||||||
integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=
|
integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=
|
||||||
|
|
||||||
is-negative-zero@^2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461"
|
|
||||||
integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=
|
|
||||||
|
|
||||||
is-npm@^4.0.0:
|
is-npm@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d"
|
resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d"
|
||||||
|
|
||||||
is-number-object@^1.0.3:
|
|
||||||
version "1.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
|
|
||||||
integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
|
|
||||||
|
|
||||||
is-number@^3.0.0:
|
is-number@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
|
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
|
||||||
|
@ -3410,32 +3288,15 @@ is-regex@^1.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
has "^1.0.3"
|
has "^1.0.3"
|
||||||
|
|
||||||
is-regex@^1.1.1:
|
|
||||||
version "1.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
|
|
||||||
integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
|
|
||||||
dependencies:
|
|
||||||
has-symbols "^1.0.1"
|
|
||||||
|
|
||||||
is-retry-allowed@^1.1.0:
|
is-retry-allowed@^1.1.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4"
|
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4"
|
||||||
integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==
|
integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==
|
||||||
|
|
||||||
is-set@^2.0.1:
|
|
||||||
version "2.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43"
|
|
||||||
integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==
|
|
||||||
|
|
||||||
is-stream@^1.1.0:
|
is-stream@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||||
|
|
||||||
is-string@^1.0.4, is-string@^1.0.5:
|
|
||||||
version "1.0.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
|
|
||||||
integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
|
|
||||||
|
|
||||||
is-symbol@^1.0.2:
|
is-symbol@^1.0.2:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
|
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
|
||||||
|
@ -3450,16 +3311,6 @@ is-type-of@^1.0.0:
|
||||||
is-class-hotfix "~0.0.6"
|
is-class-hotfix "~0.0.6"
|
||||||
isstream "~0.1.2"
|
isstream "~0.1.2"
|
||||||
|
|
||||||
is-typed-array@^1.1.3:
|
|
||||||
version "1.1.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d"
|
|
||||||
integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==
|
|
||||||
dependencies:
|
|
||||||
available-typed-arrays "^1.0.0"
|
|
||||||
es-abstract "^1.17.4"
|
|
||||||
foreach "^2.0.5"
|
|
||||||
has-symbols "^1.0.1"
|
|
||||||
|
|
||||||
is-typedarray@^1.0.0, is-typedarray@~1.0.0:
|
is-typedarray@^1.0.0, is-typedarray@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||||
|
@ -3469,16 +3320,6 @@ is-utf8@^0.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
|
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
|
||||||
integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
|
integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
|
||||||
|
|
||||||
is-weakmap@^2.0.1:
|
|
||||||
version "2.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
|
|
||||||
integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
|
|
||||||
|
|
||||||
is-weakset@^2.0.1:
|
|
||||||
version "2.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83"
|
|
||||||
integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==
|
|
||||||
|
|
||||||
is-windows@^1.0.2:
|
is-windows@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
||||||
|
@ -3499,11 +3340,6 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||||
|
|
||||||
isarray@^2.0.5:
|
|
||||||
version "2.0.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
|
||||||
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
|
|
||||||
|
|
||||||
isbinaryfile@^4.0.6:
|
isbinaryfile@^4.0.6:
|
||||||
version "4.0.6"
|
version "4.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b"
|
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b"
|
||||||
|
@ -4876,19 +4712,6 @@ object-inspect@^1.7.0:
|
||||||
version "1.7.0"
|
version "1.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
|
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
|
||||||
|
|
||||||
object-inspect@^1.8.0:
|
|
||||||
version "1.8.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
|
|
||||||
integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
|
|
||||||
|
|
||||||
object-is@^1.1.3:
|
|
||||||
version "1.1.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81"
|
|
||||||
integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==
|
|
||||||
dependencies:
|
|
||||||
define-properties "^1.1.3"
|
|
||||||
es-abstract "^1.18.0-next.1"
|
|
||||||
|
|
||||||
object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.0.6, object-keys@^1.1.1:
|
object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.0.6, object-keys@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||||
|
@ -4908,16 +4731,6 @@ object.assign@^4.1.0:
|
||||||
has-symbols "^1.0.0"
|
has-symbols "^1.0.0"
|
||||||
object-keys "^1.0.11"
|
object-keys "^1.0.11"
|
||||||
|
|
||||||
object.assign@^4.1.1:
|
|
||||||
version "4.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd"
|
|
||||||
integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==
|
|
||||||
dependencies:
|
|
||||||
define-properties "^1.1.3"
|
|
||||||
es-abstract "^1.18.0-next.0"
|
|
||||||
has-symbols "^1.0.1"
|
|
||||||
object-keys "^1.1.1"
|
|
||||||
|
|
||||||
object.getownpropertydescriptors@^2.1.0:
|
object.getownpropertydescriptors@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
|
resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649"
|
||||||
|
@ -5643,19 +5456,6 @@ regex-not@^1.0.0, regex-not@^1.0.2:
|
||||||
extend-shallow "^3.0.2"
|
extend-shallow "^3.0.2"
|
||||||
safe-regex "^1.1.0"
|
safe-regex "^1.1.0"
|
||||||
|
|
||||||
regexp.prototype.flags@^1.3.0:
|
|
||||||
version "1.3.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
|
|
||||||
integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
|
|
||||||
dependencies:
|
|
||||||
define-properties "^1.1.3"
|
|
||||||
es-abstract "^1.17.0-next.1"
|
|
||||||
|
|
||||||
regexparam@^1.3.0:
|
|
||||||
version "1.3.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-1.3.0.tgz#2fe42c93e32a40eff6235d635e0ffa344b92965f"
|
|
||||||
integrity sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g==
|
|
||||||
|
|
||||||
regexpp@^2.0.1:
|
regexpp@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
|
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
|
||||||
|
@ -5974,14 +5774,6 @@ shellwords@^0.1.1:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
|
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
|
||||||
|
|
||||||
side-channel@^1.0.3:
|
|
||||||
version "1.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3"
|
|
||||||
integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==
|
|
||||||
dependencies:
|
|
||||||
es-abstract "^1.18.0-next.0"
|
|
||||||
object-inspect "^1.8.0"
|
|
||||||
|
|
||||||
signal-exit@^3.0.0, signal-exit@^3.0.2:
|
signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
||||||
|
@ -6278,7 +6070,7 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0:
|
||||||
is-fullwidth-code-point "^3.0.0"
|
is-fullwidth-code-point "^3.0.0"
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
string.prototype.trimend@^1.0.0, string.prototype.trimend@^1.0.1:
|
string.prototype.trimend@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
|
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6301,7 +6093,7 @@ string.prototype.trimright@^2.1.1:
|
||||||
es-abstract "^1.17.5"
|
es-abstract "^1.17.5"
|
||||||
string.prototype.trimend "^1.0.0"
|
string.prototype.trimend "^1.0.0"
|
||||||
|
|
||||||
string.prototype.trimstart@^1.0.0, string.prototype.trimstart@^1.0.1:
|
string.prototype.trimstart@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
|
resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6923,27 +6715,6 @@ whatwg-url@^7.0.0:
|
||||||
tr46 "^1.0.1"
|
tr46 "^1.0.1"
|
||||||
webidl-conversions "^4.0.2"
|
webidl-conversions "^4.0.2"
|
||||||
|
|
||||||
which-boxed-primitive@^1.0.1:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1"
|
|
||||||
integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==
|
|
||||||
dependencies:
|
|
||||||
is-bigint "^1.0.0"
|
|
||||||
is-boolean-object "^1.0.0"
|
|
||||||
is-number-object "^1.0.3"
|
|
||||||
is-string "^1.0.4"
|
|
||||||
is-symbol "^1.0.2"
|
|
||||||
|
|
||||||
which-collection@^1.0.1:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906"
|
|
||||||
integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==
|
|
||||||
dependencies:
|
|
||||||
is-map "^2.0.1"
|
|
||||||
is-set "^2.0.1"
|
|
||||||
is-weakmap "^2.0.1"
|
|
||||||
is-weakset "^2.0.1"
|
|
||||||
|
|
||||||
which-module@^2.0.0:
|
which-module@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
||||||
|
@ -6953,18 +6724,6 @@ which-pm-runs@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
|
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
|
||||||
integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
|
integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
|
||||||
|
|
||||||
which-typed-array@^1.1.2:
|
|
||||||
version "1.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2"
|
|
||||||
integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==
|
|
||||||
dependencies:
|
|
||||||
available-typed-arrays "^1.0.2"
|
|
||||||
es-abstract "^1.17.5"
|
|
||||||
foreach "^2.0.5"
|
|
||||||
function-bind "^1.1.1"
|
|
||||||
has-symbols "^1.0.1"
|
|
||||||
is-typed-array "^1.1.3"
|
|
||||||
|
|
||||||
which@^1.2.9, which@^1.3.0:
|
which@^1.2.9, which@^1.3.0:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
||||||
|
|
|
@ -218,20 +218,12 @@
|
||||||
"dataform": {
|
"dataform": {
|
||||||
"description": "an HTML table that fetches data from a table or view and displays it.",
|
"description": "an HTML table that fetches data from a table or view and displays it.",
|
||||||
"data": true,
|
"data": true,
|
||||||
"props": {
|
"props": {}
|
||||||
"model": "models",
|
|
||||||
"title": "string",
|
|
||||||
"buttonText": "string"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"dataformwide": {
|
"dataformwide": {
|
||||||
"description": "an HTML table that fetches data from a table or view and displays it.",
|
"description": "an HTML table that fetches data from a table or view and displays it.",
|
||||||
"data": true,
|
"data": true,
|
||||||
"props": {
|
"props": {}
|
||||||
"model": "models",
|
|
||||||
"title": "string",
|
|
||||||
"buttonText": "string"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"datalist": {
|
"datalist": {
|
||||||
"description": "A configurable data list that attaches to your backend models.",
|
"description": "A configurable data list that attaches to your backend models.",
|
||||||
|
@ -269,11 +261,22 @@
|
||||||
"destinationUrl": "string"
|
"destinationUrl": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"recorddetail": {
|
"rowdetail": {
|
||||||
"description": "Loads a record, using an ID in the url",
|
"description": "Loads a record, using an ID in the url",
|
||||||
"context": "model",
|
"context": "model",
|
||||||
"children": true,
|
"children": true,
|
||||||
"data": true,
|
"data": true,
|
||||||
|
"baseComponent": true,
|
||||||
|
"props": {
|
||||||
|
"model": "models"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"newrow": {
|
||||||
|
"description": "Prepares a new record for creation",
|
||||||
|
"context": "model",
|
||||||
|
"children": true,
|
||||||
|
"data": true,
|
||||||
|
"baseComponent": true,
|
||||||
"props": {
|
"props": {
|
||||||
"model": "models"
|
"model": "models"
|
||||||
}
|
}
|
||||||
|
@ -715,7 +718,7 @@
|
||||||
"default": "div"
|
"default": "div"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"container": true,
|
"baseComponent": true,
|
||||||
"tags": [
|
"tags": [
|
||||||
"div",
|
"div",
|
||||||
"container",
|
"container",
|
||||||
|
|
|
@ -16,17 +16,17 @@
|
||||||
"@budibase/client": "^0.2.0",
|
"@budibase/client": "^0.2.0",
|
||||||
"@rollup/plugin-commonjs": "^11.1.0",
|
"@rollup/plugin-commonjs": "^11.1.0",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"rollup": "^1.11.0",
|
"rollup": "^2.11.2",
|
||||||
"rollup-plugin-commonjs": "^10.0.2",
|
"rollup-plugin-commonjs": "^10.0.2",
|
||||||
"rollup-plugin-json": "^4.0.0",
|
"rollup-plugin-json": "^4.0.0",
|
||||||
"rollup-plugin-livereload": "^1.0.1",
|
"rollup-plugin-livereload": "^1.0.1",
|
||||||
"rollup-plugin-node-resolve": "^5.0.0",
|
"rollup-plugin-node-resolve": "^5.0.0",
|
||||||
"rollup-plugin-postcss": "^3.1.5",
|
"rollup-plugin-postcss": "^3.1.5",
|
||||||
"rollup-plugin-svelte": "^5.0.0",
|
"rollup-plugin-svelte": "^5.0.3",
|
||||||
"rollup-plugin-terser": "^5.1.1",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"sirv-cli": "^0.4.4",
|
"sirv-cli": "^0.4.4",
|
||||||
"svelte": "^3.12.1"
|
"svelte": "^3.29.0"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"svelte"
|
"svelte"
|
||||||
|
|
|
@ -1,168 +1,61 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { Label, DatePicker, Input, Select, Toggle } from "@budibase/bbui"
|
||||||
import { fade } from "svelte/transition"
|
|
||||||
import {
|
|
||||||
Label,
|
|
||||||
DatePicker,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
Button,
|
|
||||||
Toggle,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import Dropzone from "./attachments/Dropzone.svelte"
|
import Dropzone from "./attachments/Dropzone.svelte"
|
||||||
import LinkedRecordSelector from "./LinkedRecordSelector.svelte"
|
import LinkedRecordSelector from "./LinkedRecordSelector.svelte"
|
||||||
import debounce from "lodash.debounce"
|
|
||||||
import ErrorsBox from "./ErrorsBox.svelte"
|
import ErrorsBox from "./ErrorsBox.svelte"
|
||||||
import { capitalise } from "./helpers"
|
import { capitalise } from "./helpers"
|
||||||
|
|
||||||
export let _bb
|
export let _bb
|
||||||
export let model
|
export let model
|
||||||
export let title
|
|
||||||
export let buttonText
|
|
||||||
export let wide = false
|
export let wide = false
|
||||||
|
|
||||||
const TYPE_MAP = {
|
|
||||||
string: "text",
|
|
||||||
boolean: "checkbox",
|
|
||||||
number: "number",
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULTS_FOR_TYPE = {
|
|
||||||
string: "",
|
|
||||||
boolean: false,
|
|
||||||
number: null,
|
|
||||||
link: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
let record
|
|
||||||
let store = _bb.store
|
let store = _bb.store
|
||||||
let schema = {}
|
let schema = {}
|
||||||
let modelDef = {}
|
|
||||||
let saved = false
|
|
||||||
let recordId
|
let recordId
|
||||||
let isNew = true
|
|
||||||
let errors = {}
|
let errors = {}
|
||||||
|
|
||||||
|
$: schema = $store.data && $store.data._model.schema
|
||||||
$: fields = schema ? Object.keys(schema) : []
|
$: fields = schema ? Object.keys(schema) : []
|
||||||
$: if (model && model.length !== 0) {
|
|
||||||
fetchModel()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchModel() {
|
|
||||||
const FETCH_MODEL_URL = `/api/models/${model}`
|
|
||||||
const response = await _bb.api.get(FETCH_MODEL_URL)
|
|
||||||
modelDef = await response.json()
|
|
||||||
schema = modelDef.schema
|
|
||||||
record = {
|
|
||||||
modelId: model,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const save = debounce(async () => {
|
|
||||||
for (let field of fields) {
|
|
||||||
// Assign defaults to empty fields to prevent validation issues
|
|
||||||
if (!(field in record)) {
|
|
||||||
record[field] = DEFAULTS_FOR_TYPE[schema[field].type]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const SAVE_RECORD_URL = `/api/${model}/records`
|
|
||||||
const response = await _bb.api.post(SAVE_RECORD_URL, record)
|
|
||||||
|
|
||||||
const json = await response.json()
|
|
||||||
|
|
||||||
if (response.status === 200) {
|
|
||||||
store.update(state => {
|
|
||||||
state[model] = state[model] ? [...state[model], json] : [json]
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
|
|
||||||
errors = {}
|
|
||||||
|
|
||||||
// wipe form, if new record, otherwise update
|
|
||||||
// model to get new _rev
|
|
||||||
record = isNew ? { modelId: model } : json
|
|
||||||
|
|
||||||
// set saved, and unset after 1 second
|
|
||||||
// i.e. make the success notifier appear, then disappear again after time
|
|
||||||
saved = true
|
|
||||||
setTimeout(() => {
|
|
||||||
saved = false
|
|
||||||
}, 3000)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 400) {
|
|
||||||
errors = Object.keys(json.errors)
|
|
||||||
.map(k => ({ dataPath: k, message: json.errors[k] }))
|
|
||||||
.flat()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
const routeParams = _bb.routeParams()
|
|
||||||
recordId =
|
|
||||||
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
|
|
||||||
isNew = !recordId || recordId === "new"
|
|
||||||
|
|
||||||
if (isNew) {
|
|
||||||
record = { modelId: model }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const GET_RECORD_URL = `/api/${model}/records/${recordId}`
|
|
||||||
const response = await _bb.api.get(GET_RECORD_URL)
|
|
||||||
record = await response.json()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="form" on:submit|preventDefault>
|
<div class="form-content">
|
||||||
{#if title}
|
<ErrorsBox errors={$store.saveRecordErrors || {}} />
|
||||||
<h1>{title}</h1>
|
{#each fields as field}
|
||||||
{/if}
|
<div class="form-field" class:wide>
|
||||||
<div class="form-content">
|
{#if !(schema[field].type === 'boolean' && !wide)}
|
||||||
<ErrorsBox {errors} />
|
<Label extraSmall={!wide} grey={!wide}>
|
||||||
{#each fields as field}
|
{capitalise(schema[field].name)}
|
||||||
<div class="form-field" class:wide>
|
</Label>
|
||||||
{#if !(schema[field].type === 'boolean' && !wide)}
|
{/if}
|
||||||
<Label extraSmall={!wide} grey={!wide}>
|
{#if schema[field].type === 'options'}
|
||||||
{capitalise(schema[field].name)}
|
<Select secondary bind:value={$store.data[field]}>
|
||||||
</Label>
|
<option value="">Choose an option</option>
|
||||||
{/if}
|
{#each schema[field].constraints.inclusion as opt}
|
||||||
{#if schema[field].type === 'options'}
|
<option>{opt}</option>
|
||||||
<Select secondary bind:value={record[field]}>
|
{/each}
|
||||||
<option value="">Choose an option</option>
|
</Select>
|
||||||
{#each schema[field].constraints.inclusion as opt}
|
{:else if schema[field].type === 'datetime'}
|
||||||
<option>{opt}</option>
|
<DatePicker bind:value={$store.data[field]} />
|
||||||
{/each}
|
{:else if schema[field].type === 'boolean'}
|
||||||
</Select>
|
<Toggle
|
||||||
{:else if schema[field].type === 'datetime'}
|
text={wide ? null : capitalise(schema[field].name)}
|
||||||
<DatePicker bind:value={record[field]} />
|
bind:checked={$store.data[field]} />
|
||||||
{:else if schema[field].type === 'boolean'}
|
{:else if schema[field].type === 'number'}
|
||||||
<Toggle
|
<Input type="number" bind:value={$store.data[field]} />
|
||||||
text={wide ? null : capitalise(schema[field].name)}
|
{:else if schema[field].type === 'string'}
|
||||||
bind:checked={record[field]} />
|
<Input bind:value={$store.data[field]} />
|
||||||
{:else if schema[field].type === 'number'}
|
{:else if schema[field].type === 'attachment'}
|
||||||
<Input type="number" bind:value={record[field]} />
|
<Dropzone bind:files={$store.data[field]} />
|
||||||
{:else if schema[field].type === 'string'}
|
{:else if schema[field].type === 'link'}
|
||||||
<Input bind:value={record[field]} />
|
<LinkedRecordSelector
|
||||||
{:else if schema[field].type === 'attachment'}
|
secondary
|
||||||
<Dropzone bind:files={record[field]} />
|
showLabel={false}
|
||||||
{:else if schema[field].type === 'link'}
|
bind:linkedRecords={$store.data[field]}
|
||||||
<LinkedRecordSelector
|
schema={schema[field]} />
|
||||||
secondary
|
{/if}
|
||||||
showLabel={false}
|
|
||||||
bind:linkedRecords={record[field]}
|
|
||||||
schema={schema[field]} />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
<div class="buttons">
|
|
||||||
<Button primary on:click={save} green={saved}>
|
|
||||||
{#if saved}Success{:else}{buttonText || 'Submit Form'}{/if}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/each}
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.form {
|
.form {
|
||||||
|
@ -189,9 +82,4 @@
|
||||||
.form-field.wide :global(label) {
|
.form-field.wide :global(label) {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
export let _bb
|
||||||
|
export let model
|
||||||
|
|
||||||
|
let record = {}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
record.modelId = model
|
||||||
|
}
|
||||||
|
|
||||||
|
let target
|
||||||
|
|
||||||
|
async function fetchModel(id) {
|
||||||
|
const FETCH_MODEL_URL = `/api/models/${id}`
|
||||||
|
const response = await _bb.api.get(FETCH_MODEL_URL)
|
||||||
|
return await response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (model) {
|
||||||
|
const modelObj = await fetchModel(model)
|
||||||
|
record.modelId = model
|
||||||
|
record._model = modelObj
|
||||||
|
_bb.attachChildren(target, {
|
||||||
|
context: record,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
_bb.attachChildren(target, {
|
||||||
|
context: {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section bind:this={target} />
|
|
@ -20,6 +20,7 @@
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
const allRecords = await response.json()
|
const allRecords = await response.json()
|
||||||
if (allRecords.length > 0) return allRecords[0]
|
if (allRecords.length > 0) return allRecords[0]
|
||||||
|
return { modelId: model }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,31 +34,35 @@
|
||||||
let record
|
let record
|
||||||
// if srcdoc, then we assume this is the builder preview
|
// if srcdoc, then we assume this is the builder preview
|
||||||
if (pathParts.length === 0 || pathParts[0] === "srcdoc") {
|
if (pathParts.length === 0 || pathParts[0] === "srcdoc") {
|
||||||
record = await fetchFirstRecord()
|
if (model) record = await fetchFirstRecord()
|
||||||
} else {
|
} else if (_bb.routeParams().id) {
|
||||||
const id = pathParts[pathParts.length - 1]
|
const GET_RECORD_URL = `/api/${model}/records/${_bb.routeParams().id}`
|
||||||
const GET_RECORD_URL = `/api/${model}/records/${id}`
|
|
||||||
const response = await _bb.api.get(GET_RECORD_URL)
|
const response = await _bb.api.get(GET_RECORD_URL)
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
record = await response.json()
|
record = await response.json()
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to fetch record.", response)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
throw new Exception("Record ID was not supplied to RowDetail")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (record) {
|
if (record) {
|
||||||
// Fetch model schema so we can check for linked records
|
// Fetch model schema so we can check for linked records
|
||||||
const model = await fetchModel(record.modelId)
|
const modelObj = await fetchModel(record.modelId)
|
||||||
for (let key of Object.keys(model.schema)) {
|
for (let key of Object.keys(modelObj.schema)) {
|
||||||
if (model.schema[key].type === "link") {
|
if (modelObj.schema[key].type === "link") {
|
||||||
record[key] = Array.isArray(record[key]) ? record[key].length : 0
|
record[key] = Array.isArray(record[key]) ? record[key].length : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record._model = modelObj
|
||||||
|
|
||||||
_bb.attachChildren(target, {
|
_bb.attachChildren(target, {
|
||||||
hydrate: false,
|
|
||||||
context: record,
|
context: record,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Failed to fetch record.", response)
|
_bb.attachChildren(target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,8 @@ export { default as embed } from "./Embed.svelte"
|
||||||
export { default as stackedlist } from "./StackedList.svelte"
|
export { default as stackedlist } from "./StackedList.svelte"
|
||||||
export { default as card } from "./Card.svelte"
|
export { default as card } from "./Card.svelte"
|
||||||
export { default as cardhorizontal } from "./CardHorizontal.svelte"
|
export { default as cardhorizontal } from "./CardHorizontal.svelte"
|
||||||
export { default as recorddetail } from "./RecordDetail.svelte"
|
export { default as rowdetail } from "./RowDetail.svelte"
|
||||||
|
export { default as newrow } from "./NewRow.svelte"
|
||||||
export { default as datepicker } from "./DatePicker.svelte"
|
export { default as datepicker } from "./DatePicker.svelte"
|
||||||
export * from "./Chart"
|
export * from "./Chart"
|
||||||
export { default as icon } from "./Icon.svelte"
|
export { default as icon } from "./Icon.svelte"
|
||||||
|
|
Loading…
Reference in New Issue