commit
e9ecafb3e1
|
@ -47,8 +47,9 @@ context('Create a View', () => {
|
||||||
|
|
||||||
it('creates a stats calculation view based on age', () => {
|
it('creates a stats calculation view based on age', () => {
|
||||||
cy.contains("Calculate").click()
|
cy.contains("Calculate").click()
|
||||||
cy.get(".menu-container").find("select").first().select("Statistics")
|
// we may reinstate this - have commented this dropdown for now as there is only one option
|
||||||
cy.get(".menu-container").find("select").eq(1).select("age")
|
//cy.get(".menu-container").find("select").first().select("Statistics")
|
||||||
|
cy.get(".menu-container").find("select").eq(0).select("age")
|
||||||
cy.contains("Save").click()
|
cy.contains("Save").click()
|
||||||
cy.get("thead th").should(($headers) => {
|
cy.get("thead th").should(($headers) => {
|
||||||
expect($headers).to.have.length(7)
|
expect($headers).to.have.length(7)
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
import AttachmentList from "./AttachmentList.svelte"
|
import AttachmentList from "./AttachmentList.svelte"
|
||||||
import TablePagination from "./TablePagination.svelte"
|
import TablePagination from "./TablePagination.svelte"
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
|
|
||||||
import RowPopover from "./popovers/Row.svelte"
|
import RowPopover from "./popovers/Row.svelte"
|
||||||
import ColumnPopover from "./popovers/Column.svelte"
|
import ColumnPopover from "./popovers/Column.svelte"
|
||||||
import ViewPopover from "./popovers/View.svelte"
|
import ViewPopover from "./popovers/View.svelte"
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import ActionButton from "components/common/ActionButton.svelte"
|
||||||
import AttachmentList from "./AttachmentList.svelte"
|
import AttachmentList from "./AttachmentList.svelte"
|
||||||
import TablePagination from "./TablePagination.svelte"
|
import TablePagination from "./TablePagination.svelte"
|
||||||
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
|
|
||||||
import RowPopover from "./popovers/Row.svelte"
|
import RowPopover from "./popovers/Row.svelte"
|
||||||
import ColumnPopover from "./popovers/Column.svelte"
|
import ColumnPopover from "./popovers/Column.svelte"
|
||||||
import ViewPopover from "./popovers/View.svelte"
|
import ViewPopover from "./popovers/View.svelte"
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import ActionButton from "components/common/ActionButton.svelte"
|
||||||
import LinkedRecord from "./LinkedRecord.svelte"
|
import LinkedRecord from "./LinkedRecord.svelte"
|
||||||
import TablePagination from "./TablePagination.svelte"
|
import TablePagination from "./TablePagination.svelte"
|
||||||
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
|
|
||||||
import RowPopover from "./popovers/Row.svelte"
|
import RowPopover from "./popovers/Row.svelte"
|
||||||
import ColumnPopover from "./popovers/Column.svelte"
|
import ColumnPopover from "./popovers/Column.svelte"
|
||||||
import ViewPopover from "./popovers/View.svelte"
|
import ViewPopover from "./popovers/View.svelte"
|
||||||
|
|
|
@ -24,11 +24,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let originalName = field.name
|
let originalName = field.name
|
||||||
|
$: required = field && field.constraints && field.constraints.presence
|
||||||
$: required =
|
|
||||||
field.constraints &&
|
|
||||||
field.constraints.presence &&
|
|
||||||
!field.constraints.presence.allowEmpty
|
|
||||||
|
|
||||||
async function saveColumn() {
|
async function saveColumn() {
|
||||||
backendUiStore.update(state => {
|
backendUiStore.update(state => {
|
||||||
|
@ -50,6 +46,14 @@
|
||||||
field.type = type
|
field.type = type
|
||||||
field.constraints = constraints
|
field.constraints = constraints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPresence = required => (required ? { allowEmpty: false } : false)
|
||||||
|
|
||||||
|
const requiredChanged = ev => {
|
||||||
|
const req = ev.target.checked
|
||||||
|
field.constraints.presence = req ? { allowEmpty: false } : false
|
||||||
|
required = req
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
@ -68,10 +72,7 @@
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Required</label>
|
<label>Required</label>
|
||||||
<input
|
<input type="checkbox" checked={required} on:change={requiredChanged} />
|
||||||
type="checkbox"
|
|
||||||
bind:checked={required}
|
|
||||||
on:change={() => (field.constraints.presence.allowEmpty = required)} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if field.type === 'string' && field.constraints}
|
{#if field.type === 'string' && field.constraints}
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
|
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
|
||||||
const CALCULATIONS = [
|
const CALCULATIONS = [
|
||||||
|
@ -34,6 +33,7 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
function saveView() {
|
function saveView() {
|
||||||
|
if (!view.calculation) view.calculation = "stats"
|
||||||
backendUiStore.actions.views.save(view)
|
backendUiStore.actions.views.save(view)
|
||||||
notifier.success(`View ${view.name} saved.`)
|
notifier.success(`View ${view.name} saved.`)
|
||||||
analytics.captureEvent("Added View Calculate", { field: view.field })
|
analytics.captureEvent("Added View Calculate", { field: view.field })
|
||||||
|
@ -50,14 +50,15 @@
|
||||||
<Popover bind:this={dropdown} {anchor} align="left">
|
<Popover bind:this={dropdown} {anchor} align="left">
|
||||||
<h5>Calculate</h5>
|
<h5>Calculate</h5>
|
||||||
<div class="input-group-row">
|
<div class="input-group-row">
|
||||||
<p>The</p>
|
<!-- <p>The</p>
|
||||||
<Select secondary thin bind:value={view.calculation}>
|
<Select secondary thin bind:value={view.calculation}>
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each CALCULATIONS as calculation}
|
{#each CALCULATIONS as calculation}
|
||||||
<option value={calculation.key}>{calculation.name}</option>
|
<option value={calculation.key}>{calculation.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
<p>of</p>
|
<p>of</p> -->
|
||||||
|
<p>The statistics of</p>
|
||||||
<Select secondary thin bind:value={view.field}>
|
<Select secondary thin bind:value={view.field}>
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
|
@ -86,7 +87,7 @@
|
||||||
|
|
||||||
.input-group-row {
|
.input-group-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 50px 1fr 20px 1fr;
|
grid-template-columns: auto 1fr 20px 1fr;
|
||||||
gap: var(--spacing-s);
|
gap: var(--spacing-s);
|
||||||
margin-bottom: var(--spacing-l);
|
margin-bottom: var(--spacing-l);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -6,10 +6,10 @@
|
||||||
Icon,
|
Icon,
|
||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
|
DatePicker,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
|
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
|
||||||
const CONDITIONS = [
|
const CONDITIONS = [
|
||||||
|
@ -81,11 +81,38 @@
|
||||||
|
|
||||||
function isMultipleChoice(field) {
|
function isMultipleChoice(field) {
|
||||||
return (
|
return (
|
||||||
viewModel.schema[field].constraints &&
|
(viewModel.schema[field].constraints &&
|
||||||
viewModel.schema[field].constraints.inclusion &&
|
viewModel.schema[field].constraints.inclusion &&
|
||||||
viewModel.schema[field].constraints.inclusion.length
|
viewModel.schema[field].constraints.inclusion.length) ||
|
||||||
|
viewModel.schema[field].type === "boolean"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fieldOptions(field) {
|
||||||
|
return viewModel.schema[field].type === "string"
|
||||||
|
? viewModel.schema[field].constraints.inclusion
|
||||||
|
: [true, false]
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDate(field) {
|
||||||
|
return viewModel.schema[field].type === "datetime"
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNumber(field) {
|
||||||
|
return viewModel.schema[field].type === "number"
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldChanged = filter => ev => {
|
||||||
|
// reset if type changed
|
||||||
|
if (
|
||||||
|
filter.key &&
|
||||||
|
ev.target.value &&
|
||||||
|
viewModel.schema[filter.key].type !==
|
||||||
|
viewModel.schema[ev.target.value].type
|
||||||
|
) {
|
||||||
|
filter.value = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
<div bind:this={anchor}>
|
||||||
|
@ -112,7 +139,11 @@
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
{/if}
|
{/if}
|
||||||
<Select secondary thin bind:value={filter.key}>
|
<Select
|
||||||
|
secondary
|
||||||
|
thin
|
||||||
|
bind:value={filter.key}
|
||||||
|
on:change={fieldChanged(filter)}>
|
||||||
<option value="">Choose an option</option>
|
<option value="">Choose an option</option>
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
<option value={field}>{field}</option>
|
<option value={field}>{field}</option>
|
||||||
|
@ -126,10 +157,21 @@
|
||||||
</Select>
|
</Select>
|
||||||
{#if filter.key && isMultipleChoice(filter.key)}
|
{#if filter.key && isMultipleChoice(filter.key)}
|
||||||
<Select secondary thin bind:value={filter.value}>
|
<Select secondary thin bind:value={filter.value}>
|
||||||
{#each viewModel.schema[filter.key].constraints.inclusion as option}
|
<option value="">Choose an option</option>
|
||||||
<option value={option}>{option}</option>
|
{#each fieldOptions(filter.key) as option}
|
||||||
|
<option value={option}>{option.toString()}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
|
{:else if filter.key && isDate(filter.key)}
|
||||||
|
<DatePicker
|
||||||
|
bind:value={filter.value}
|
||||||
|
placeholder={filter.key || fields[0]} />
|
||||||
|
{:else if filter.key && isNumber(filter.key)}
|
||||||
|
<Input
|
||||||
|
thin
|
||||||
|
bind:value={filter.value}
|
||||||
|
placeholder={filter.key || fields[0]}
|
||||||
|
type="number" />
|
||||||
{:else}
|
{:else}
|
||||||
<Input
|
<Input
|
||||||
thin
|
thin
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
|
|
||||||
|
|
||||||
const CALCULATIONS = [
|
const CALCULATIONS = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
import { goto } from "@sveltech/routify"
|
import { goto } from "@sveltech/routify"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
|
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
|
||||||
let anchor
|
let anchor
|
||||||
|
|
|
@ -33,9 +33,9 @@
|
||||||
})
|
})
|
||||||
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 })
|
||||||
name = ""
|
name = ""
|
||||||
dropdown.hide()
|
dropdown.hide()
|
||||||
analytics.captureEvent("Table Created", { name })
|
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -147,7 +147,7 @@
|
||||||
})
|
})
|
||||||
const appJson = await appResp.json()
|
const appJson = await appResp.json()
|
||||||
analytics.captureEvent("App Created", {
|
analytics.captureEvent("App Created", {
|
||||||
name,
|
name: $createAppStore.values.applicationName,
|
||||||
appId: appJson._id,
|
appId: appJson._id,
|
||||||
template,
|
template,
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
on:input={() => (blurred.api = true)}
|
on:input={() => (blurred.api = true)}
|
||||||
label="API Key"
|
label="API Key"
|
||||||
name="apiKey"
|
name="apiKey"
|
||||||
placeholder="Enter your API Key"
|
placeholder="Use command-V to paste your API Key"
|
||||||
type="password"
|
type="password"
|
||||||
error={blurred.api && validationErrors.apiKey} />
|
error={blurred.api && validationErrors.apiKey} />
|
||||||
<a target="_blank" href="https://portal.budi.live/">Get API Key</a>
|
<a target="_blank" href="https://portal.budi.live/">Get API Key</a>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
label="Password"
|
label="Password"
|
||||||
name="password"
|
name="password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
type="pasword"
|
type="password"
|
||||||
error={blurred.password && validationErrors.password} />
|
error={blurred.password && validationErrors.password} />
|
||||||
<Select secondary name="accessLevelId">
|
<Select secondary name="accessLevelId">
|
||||||
<option value="ADMIN">Admin</option>
|
<option value="ADMIN">Admin</option>
|
||||||
|
|
|
@ -6,7 +6,7 @@ export const FIELDS = {
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
length: {},
|
length: {},
|
||||||
presence: { allowEmpty: true },
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
NUMBER: {
|
NUMBER: {
|
||||||
|
@ -15,7 +15,7 @@ export const FIELDS = {
|
||||||
type: "number",
|
type: "number",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "number",
|
type: "number",
|
||||||
presence: { allowEmpty: true },
|
presence: false,
|
||||||
numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" },
|
numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -25,7 +25,7 @@ export const FIELDS = {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
presence: { allowEmpty: true },
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// OPTIONS: {
|
// OPTIONS: {
|
||||||
|
@ -44,7 +44,7 @@ export const FIELDS = {
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
length: {},
|
length: {},
|
||||||
presence: { allowEmpty: true },
|
presence: false,
|
||||||
datetime: {
|
datetime: {
|
||||||
latest: "",
|
latest: "",
|
||||||
earliest: "",
|
earliest: "",
|
||||||
|
@ -57,7 +57,7 @@ export const FIELDS = {
|
||||||
type: "attachment",
|
type: "attachment",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "array",
|
type: "array",
|
||||||
presence: { allowEmpty: true },
|
presence: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// LINKED_FIELDS: {
|
// LINKED_FIELDS: {
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import ActionButton from "components/common/ActionButton.svelte"
|
||||||
import * as api from "components/database/DataTable/api"
|
import * as api from "components/database/DataTable/api"
|
||||||
import { CreateEditRecordModal } from "components/database/DataTable/modals"
|
|
||||||
|
|
||||||
const { open, close } = getContext("simple-modal")
|
const { open, close } = getContext("simple-modal")
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import ActionButton from "components/common/ActionButton.svelte"
|
||||||
import * as api from "components/database/DataTable/api"
|
import * as api from "components/database/DataTable/api"
|
||||||
import { CreateEditRecordModal } from "components/database/DataTable/modals"
|
|
||||||
|
|
||||||
const { open, close } = getContext("simple-modal")
|
const { open, close } = getContext("simple-modal")
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const validateJs = require("validate.js")
|
const validateJs = require("validate.js")
|
||||||
const { getRecordParams, generateRecordID } = require("../../db/utils")
|
const { getRecordParams, generateRecordID } = require("../../db/utils")
|
||||||
|
const { cloneDeep } = require("lodash")
|
||||||
|
|
||||||
const MODEL_VIEW_BEGINS_WITH = "all_model:"
|
const MODEL_VIEW_BEGINS_WITH = "all_model:"
|
||||||
|
|
||||||
|
@ -31,10 +32,12 @@ validateJs.extend(validateJs.validators.datetime, {
|
||||||
|
|
||||||
exports.patch = async function(ctx) {
|
exports.patch = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const record = await db.get(ctx.params.id)
|
let record = await db.get(ctx.params.id)
|
||||||
const model = await db.get(record.modelId)
|
const model = await db.get(record.modelId)
|
||||||
const patchfields = ctx.request.body
|
const patchfields = ctx.request.body
|
||||||
|
|
||||||
|
record = coerceRecordValues(record, model)
|
||||||
|
|
||||||
for (let key in patchfields) {
|
for (let key in patchfields) {
|
||||||
if (!model.schema[key]) continue
|
if (!model.schema[key]) continue
|
||||||
record[key] = patchfields[key]
|
record[key] = patchfields[key]
|
||||||
|
@ -64,7 +67,7 @@ exports.patch = async function(ctx) {
|
||||||
|
|
||||||
exports.save = async function(ctx) {
|
exports.save = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const record = ctx.request.body
|
let record = ctx.request.body
|
||||||
record.modelId = ctx.params.modelId
|
record.modelId = ctx.params.modelId
|
||||||
|
|
||||||
if (!record._rev && !record._id) {
|
if (!record._rev && !record._id) {
|
||||||
|
@ -73,6 +76,8 @@ exports.save = async function(ctx) {
|
||||||
|
|
||||||
const model = await db.get(record.modelId)
|
const model = await db.get(record.modelId)
|
||||||
|
|
||||||
|
record = coerceRecordValues(record, model)
|
||||||
|
|
||||||
const validateResult = await validate({
|
const validateResult = await validate({
|
||||||
record,
|
record,
|
||||||
model,
|
model,
|
||||||
|
@ -231,3 +236,50 @@ async function validate({ instanceId, modelId, record, model }) {
|
||||||
}
|
}
|
||||||
return { valid: Object.keys(errors).length === 0, errors }
|
return { valid: Object.keys(errors).length === 0, errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function coerceRecordValues(rec, model) {
|
||||||
|
const record = cloneDeep(rec)
|
||||||
|
for (let [key, value] of Object.entries(record)) {
|
||||||
|
const field = model.schema[key]
|
||||||
|
if (!field) continue
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) {
|
||||||
|
record[key] = TYPE_TRANSFORM_MAP[field.type][value]
|
||||||
|
} else if (TYPE_TRANSFORM_MAP[field.type].parse) {
|
||||||
|
record[key] = TYPE_TRANSFORM_MAP[field.type].parse(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
const TYPE_TRANSFORM_MAP = {
|
||||||
|
string: {
|
||||||
|
"": "",
|
||||||
|
[null]: "",
|
||||||
|
[undefined]: undefined,
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
"": null,
|
||||||
|
[null]: null,
|
||||||
|
[undefined]: undefined,
|
||||||
|
parse: n => parseFloat(n),
|
||||||
|
},
|
||||||
|
datetime: {
|
||||||
|
"": null,
|
||||||
|
[undefined]: undefined,
|
||||||
|
[null]: null,
|
||||||
|
},
|
||||||
|
attachment: {
|
||||||
|
"": [],
|
||||||
|
[null]: [],
|
||||||
|
[undefined]: undefined,
|
||||||
|
},
|
||||||
|
boolean: {
|
||||||
|
"": null,
|
||||||
|
[null]: null,
|
||||||
|
[undefined]: undefined,
|
||||||
|
true: true,
|
||||||
|
false: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -61,8 +61,11 @@ function parseFilterExpression(filters) {
|
||||||
`doc["${filter.key}"].${TOKEN_MAP[filter.condition]}("${filter.value}")`
|
`doc["${filter.key}"].${TOKEN_MAP[filter.condition]}("${filter.value}")`
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
const value =
|
||||||
|
typeof filter.value == "string" ? `"${filter.value}"` : filter.value
|
||||||
|
|
||||||
expression.push(
|
expression.push(
|
||||||
`doc["${filter.key}"] ${TOKEN_MAP[filter.condition]} "${filter.value}"`
|
`doc["${filter.key}"] ${TOKEN_MAP[filter.condition]} ${value}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,13 +46,13 @@ exports.createModel = async (request, appId, instanceId, model) => {
|
||||||
key: "name",
|
key: "name",
|
||||||
schema: {
|
schema: {
|
||||||
name: {
|
name: {
|
||||||
type: "text",
|
type: "string",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: "text",
|
type: "string",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
|
|
|
@ -180,7 +180,7 @@ describe("/models", () => {
|
||||||
key: "name",
|
key: "name",
|
||||||
schema: {
|
schema: {
|
||||||
name: {
|
name: {
|
||||||
type: "text",
|
type: "string",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
|
|
|
@ -38,7 +38,7 @@ describe("/records", () => {
|
||||||
|
|
||||||
const createRecord = async r =>
|
const createRecord = async r =>
|
||||||
await request
|
await request
|
||||||
.post(`/api/${model._id}/records`)
|
.post(`/api/${r ? r.modelId : record.modelId}/records`)
|
||||||
.send(r || record)
|
.send(r || record)
|
||||||
.set(defaultHeaders(app._id, instance._id))
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
|
@ -152,6 +152,95 @@ describe("/records", () => {
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(404)
|
.expect(404)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("record values are coerced", async () => {
|
||||||
|
const str = {type:"string", constraints: { type: "string", presence: false }}
|
||||||
|
const attachment = {type:"attachment", constraints: { type: "array", presence: false }}
|
||||||
|
const bool = {type:"boolean", constraints: { type: "boolean", presence: false }}
|
||||||
|
const number = {type:"number", constraints: { type: "number", presence: false }}
|
||||||
|
const datetime = {type:"datetime", constraints: { type: "string", presence: false, datetime: {earliest:"", latest: ""} }}
|
||||||
|
|
||||||
|
model = await createModel(request, app._id, instance._id, {
|
||||||
|
name: "TestModel2",
|
||||||
|
type: "model",
|
||||||
|
key: "name",
|
||||||
|
schema: {
|
||||||
|
name: str,
|
||||||
|
stringUndefined: str,
|
||||||
|
stringNull: str,
|
||||||
|
stringString: str,
|
||||||
|
numberEmptyString: number,
|
||||||
|
numberNull: number,
|
||||||
|
numberUndefined: number,
|
||||||
|
numberString: number,
|
||||||
|
datetimeEmptyString: datetime,
|
||||||
|
datetimeNull: datetime,
|
||||||
|
datetimeUndefined: datetime,
|
||||||
|
datetimeString: datetime,
|
||||||
|
datetimeDate: datetime,
|
||||||
|
boolNull: bool,
|
||||||
|
boolEmpty: bool,
|
||||||
|
boolUndefined: bool,
|
||||||
|
boolString: bool,
|
||||||
|
boolBool: bool,
|
||||||
|
attachmentNull : attachment,
|
||||||
|
attachmentUndefined : attachment,
|
||||||
|
attachmentEmpty : attachment,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
record = {
|
||||||
|
name: "Test Record",
|
||||||
|
stringUndefined: undefined,
|
||||||
|
stringNull: null,
|
||||||
|
stringString: "i am a string",
|
||||||
|
numberEmptyString: "",
|
||||||
|
numberNull: null,
|
||||||
|
numberUndefined: undefined,
|
||||||
|
numberString: "123",
|
||||||
|
numberNumber: 123,
|
||||||
|
datetimeEmptyString: "",
|
||||||
|
datetimeNull: null,
|
||||||
|
datetimeUndefined: undefined,
|
||||||
|
datetimeString: "1984-04-20T00:00:00.000Z",
|
||||||
|
datetimeDate: new Date("1984-04-20"),
|
||||||
|
boolNull: null,
|
||||||
|
boolEmpty: "",
|
||||||
|
boolUndefined: undefined,
|
||||||
|
boolString: "true",
|
||||||
|
boolBool: true,
|
||||||
|
modelId: model._id,
|
||||||
|
attachmentNull : null,
|
||||||
|
attachmentUndefined : undefined,
|
||||||
|
attachmentEmpty : "",
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = (await createRecord(record)).body._id
|
||||||
|
|
||||||
|
const saved = (await loadRecord(id)).body
|
||||||
|
|
||||||
|
expect(saved.stringUndefined).toBe(undefined)
|
||||||
|
expect(saved.stringNull).toBe("")
|
||||||
|
expect(saved.stringString).toBe("i am a string")
|
||||||
|
expect(saved.numberEmptyString).toBe(null)
|
||||||
|
expect(saved.numberNull).toBe(null)
|
||||||
|
expect(saved.numberUndefined).toBe(undefined)
|
||||||
|
expect(saved.numberString).toBe(123)
|
||||||
|
expect(saved.numberNumber).toBe(123)
|
||||||
|
expect(saved.datetimeEmptyString).toBe(null)
|
||||||
|
expect(saved.datetimeNull).toBe(null)
|
||||||
|
expect(saved.datetimeUndefined).toBe(undefined)
|
||||||
|
expect(saved.datetimeString).toBe(new Date(record.datetimeString).toISOString())
|
||||||
|
expect(saved.datetimeDate).toBe(record.datetimeDate.toISOString())
|
||||||
|
expect(saved.boolNull).toBe(null)
|
||||||
|
expect(saved.boolEmpty).toBe(null)
|
||||||
|
expect(saved.boolUndefined).toBe(undefined)
|
||||||
|
expect(saved.boolString).toBe(true)
|
||||||
|
expect(saved.boolBool).toBe(true)
|
||||||
|
expect(saved.attachmentNull).toEqual([])
|
||||||
|
expect(saved.attachmentUndefined).toBe(undefined)
|
||||||
|
expect(saved.attachmentEmpty).toEqual([])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("patch", () => {
|
describe("patch", () => {
|
||||||
|
|
|
@ -69,13 +69,13 @@ describe("/views", () => {
|
||||||
filters: [],
|
filters: [],
|
||||||
schema: {
|
schema: {
|
||||||
name: {
|
name: {
|
||||||
type: "text",
|
type: "string",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string"
|
type: "string"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
type: "text",
|
type: "string",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string"
|
type: "string"
|
||||||
},
|
},
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue