Merge branch 'binding-ts-improvements' of github.com:Budibase/budibase into binding-ts-improvements

This commit is contained in:
mike12345567 2025-01-15 15:34:32 +00:00
commit ff78610fe4
11 changed files with 338 additions and 114 deletions

View File

@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "3.2.42",
"version": "3.2.44",
"npmClient": "yarn",
"concurrency": 20,
"command": {

View File

@ -1,27 +0,0 @@
import { writable } from "svelte/store"
import { API } from "@/api"
export function createPermissionStore() {
const { subscribe } = writable([])
return {
subscribe,
save: async ({ level, role, resource }) => {
return await API.updatePermissionForResource(resource, role, level)
},
remove: async ({ level, role, resource }) => {
return await API.removePermissionFromResource(resource, role, level)
},
forResource: async resourceId => {
return (await API.getPermissionForResource(resourceId)).permissions
},
forResourceDetailed: async resourceId => {
return await API.getPermissionForResource(resourceId)
},
getDependantsInfo: async resourceId => {
return await API.getDependants(resourceId)
},
}
}
export const permissions = createPermissionStore()

View File

@ -0,0 +1,50 @@
import { BudiStore } from "../BudiStore"
import { API } from "@/api"
import {
PermissionLevel,
GetResourcePermsResponse,
GetDependantResourcesResponse,
ResourcePermissionInfo,
} from "@budibase/types"
interface Permission {
level: PermissionLevel
role: string
resource: string
}
export class PermissionStore extends BudiStore<Permission[]> {
constructor() {
super([])
}
save = async (permission: Permission) => {
const { level, role, resource } = permission
return await API.updatePermissionForResource(resource, role, level)
}
remove = async (permission: Permission) => {
const { level, role, resource } = permission
return await API.removePermissionFromResource(resource, role, level)
}
forResource = async (
resourceId: string
): Promise<Record<string, ResourcePermissionInfo>> => {
return (await API.getPermissionForResource(resourceId)).permissions
}
forResourceDetailed = async (
resourceId: string
): Promise<GetResourcePermsResponse> => {
return await API.getPermissionForResource(resourceId)
}
getDependantsInfo = async (
resourceId: string
): Promise<GetDependantResourcesResponse> => {
return await API.getDependants(resourceId)
}
}
export const permissions = new PermissionStore()

View File

@ -1,67 +0,0 @@
import { writable, derived } from "svelte/store"
import { tables } from "./tables"
import { API } from "@/api"
export function createViewsStore() {
const store = writable({
selectedViewName: null,
})
const derivedStore = derived([store, tables], ([$store, $tables]) => {
let list = []
$tables.list?.forEach(table => {
const views = Object.values(table?.views || {}).filter(view => {
return view.version !== 2
})
list = list.concat(views)
})
return {
...$store,
list,
selected: list.find(view => view.name === $store.selectedViewName),
}
})
const select = name => {
store.update(state => ({
...state,
selectedViewName: name,
}))
}
const deleteView = async view => {
await API.deleteView(view.name)
// Update tables
tables.update(state => {
const table = state.list.find(table => table._id === view.tableId)
delete table.views[view.name]
return { ...state }
})
}
const save = async view => {
const savedView = await API.saveView(view)
select(view.name)
// Update tables
tables.update(state => {
const table = state.list.find(table => table._id === view.tableId)
if (table) {
if (view.originalName) {
delete table.views[view.originalName]
}
table.views[view.name] = savedView
}
return { ...state }
})
}
return {
subscribe: derivedStore.subscribe,
select,
delete: deleteView,
save,
}
}
export const views = createViewsStore()

View File

@ -0,0 +1,94 @@
import { DerivedBudiStore } from "../BudiStore"
import { tables } from "./tables"
import { API } from "@/api"
import { View } from "@budibase/types"
import { helpers } from "@budibase/shared-core"
import { derived, Writable } from "svelte/store"
interface BuilderViewStore {
selectedViewName: string | null
}
interface DerivedViewStore extends BuilderViewStore {
list: View[]
selected?: View
}
export class ViewsStore extends DerivedBudiStore<
BuilderViewStore,
DerivedViewStore
> {
constructor() {
const makeDerivedStore = (store: Writable<BuilderViewStore>) => {
return derived([store, tables], ([$store, $tables]): DerivedViewStore => {
let list: View[] = []
$tables.list?.forEach(table => {
const views = Object.values(table?.views || {}).filter(
(view): view is View => !helpers.views.isV2(view)
)
list = list.concat(views)
})
return {
selectedViewName: $store.selectedViewName,
list,
selected: list.find(view => view.name === $store.selectedViewName),
}
})
}
super(
{
selectedViewName: null,
},
makeDerivedStore
)
this.select = this.select.bind(this)
}
select = (name: string) => {
this.store.update(state => ({
...state,
selectedViewName: name,
}))
}
delete = async (view: View) => {
if (!view.name) {
throw new Error("View name is required")
}
await API.deleteView(view.name)
// Update tables
tables.update(state => {
const table = state.list.find(table => table._id === view.tableId)
if (table?.views && view.name) {
delete table.views[view.name]
}
return { ...state }
})
}
save = async (view: View & { originalName?: string }) => {
if (!view.name) {
throw new Error("View name is required")
}
const savedView = await API.saveView(view)
this.select(view.name)
// Update tables
tables.update(state => {
const table = state.list.find(table => table._id === view.tableId)
if (table?.views && view.name) {
if (view.originalName) {
delete table.views[view.originalName]
}
table.views[view.name] = savedView
}
return { ...state }
})
}
}
export const views = new ViewsStore()

@ -1 +1 @@
Subproject commit 193476cdfade6d3c613e6972f16ee0c527e01ff6
Subproject commit 43a5785ccb4f83ce929b29f05ea0a62199fcdf23

View File

@ -2043,6 +2043,101 @@ if (descriptions.length) {
expect(rows[0].name).toEqual("Clare updated")
expect(rows[1].name).toEqual("Jeff updated")
})
it("should reject bulkImport date only fields with wrong format", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
date: {
type: FieldType.DATETIME,
dateOnly: true,
name: "date",
},
},
})
)
await config.api.row.bulkImport(
table._id!,
{
rows: [
{
date: "01.02.2024",
},
],
},
{
status: 400,
body: {
message:
'Invalid format for field "date": "01.02.2024". Date-only fields must be in the format "YYYY-MM-DD".',
},
}
)
})
it("should reject bulkImport date time fields with wrong format", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
date: {
type: FieldType.DATETIME,
name: "date",
},
},
})
)
await config.api.row.bulkImport(
table._id!,
{
rows: [
{
date: "01.02.2024",
},
],
},
{
status: 400,
body: {
message:
'Invalid format for field "date": "01.02.2024". Datetime fields must be in ISO format, e.g. "YYYY-MM-DDTHH:MM:SSZ".',
},
}
)
})
it("should reject bulkImport time fields with wrong format", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
time: {
type: FieldType.DATETIME,
timeOnly: true,
name: "time",
},
},
})
)
await config.api.row.bulkImport(
table._id!,
{
rows: [
{
time: "3pm",
},
],
},
{
status: 400,
body: {
message:
'Invalid format for field "time": "3pm". Time-only fields must be in the format "HH:MM:SS".',
},
}
)
})
})
describe("enrich", () => {

View File

@ -1705,7 +1705,10 @@ if (descriptions.length) {
beforeAll(async () => {
tableOrViewId = await createTableOrView({
dateid: { name: "dateid", type: FieldType.STRING },
dateid: {
name: "dateid",
type: FieldType.STRING,
},
date: {
name: "date",
type: FieldType.DATETIME,
@ -1751,7 +1754,9 @@ if (descriptions.length) {
describe("notEqual", () => {
it("successfully finds a row", async () => {
await expectQuery({
notEqual: { date: `${JAN_1ST}${SEARCH_SUFFIX}` },
notEqual: {
date: `${JAN_1ST}${SEARCH_SUFFIX}`,
},
}).toContainExactly([
{ date: JAN_10TH },
{ dateid: NULL_DATE__ID },
@ -1760,7 +1765,9 @@ if (descriptions.length) {
it("fails to find nonexistent row", async () => {
await expectQuery({
notEqual: { date: `${JAN_30TH}${SEARCH_SUFFIX}` },
notEqual: {
date: `${JAN_30TH}${SEARCH_SUFFIX}`,
},
}).toContainExactly([
{ date: JAN_1ST },
{ date: JAN_10TH },
@ -1822,6 +1829,60 @@ if (descriptions.length) {
}).toFindNothing()
})
})
describe("sort", () => {
it("sorts ascending", async () => {
await expectSearch({
query: {},
sort: "date",
sortOrder: SortOrder.ASCENDING,
}).toMatchExactly([
{ dateid: NULL_DATE__ID },
{ date: JAN_1ST },
{ date: JAN_10TH },
])
})
it("sorts descending", async () => {
await expectSearch({
query: {},
sort: "date",
sortOrder: SortOrder.DESCENDING,
}).toMatchExactly([
{ date: JAN_10TH },
{ date: JAN_1ST },
{ dateid: NULL_DATE__ID },
])
})
describe("sortType STRING", () => {
it("sorts ascending", async () => {
await expectSearch({
query: {},
sort: "date",
sortType: SortType.STRING,
sortOrder: SortOrder.ASCENDING,
}).toMatchExactly([
{ dateid: NULL_DATE__ID },
{ date: JAN_1ST },
{ date: JAN_10TH },
])
})
it("sorts descending", async () => {
await expectSearch({
query: {},
sort: "date",
sortType: SortType.STRING,
sortOrder: SortOrder.DESCENDING,
}).toMatchExactly([
{ date: JAN_10TH },
{ date: JAN_1ST },
{ dateid: NULL_DATE__ID },
])
})
})
})
}
)
}

View File

@ -7,7 +7,7 @@ import {
Table,
} from "@budibase/types"
import { ValidColumnNameRegex, helpers, utils } from "@budibase/shared-core"
import { db } from "@budibase/backend-core"
import { db, HTTPError, sql } from "@budibase/backend-core"
type Rows = Array<Row>
@ -175,15 +175,27 @@ export function parse(rows: Rows, table: Table): Rows {
if ([FieldType.NUMBER].includes(columnType)) {
// If provided must be a valid number
parsedRow[columnName] = columnData ? Number(columnData) : columnData
} else if (
columnType === FieldType.DATETIME &&
!columnSchema.timeOnly &&
!columnSchema.dateOnly
) {
// If provided must be a valid date
} else if (columnType === FieldType.DATETIME) {
if (columnData && !columnSchema.timeOnly) {
if (!sql.utils.isValidISODateString(columnData)) {
let message = `Invalid format for field "${columnName}": "${columnData}".`
if (columnSchema.dateOnly) {
message += ` Date-only fields must be in the format "YYYY-MM-DD".`
} else {
message += ` Datetime fields must be in ISO format, e.g. "YYYY-MM-DDTHH:MM:SSZ".`
}
throw new HTTPError(message, 400)
}
}
if (columnData && columnSchema.timeOnly) {
if (!sql.utils.isValidTime(columnData)) {
throw new HTTPError(
`Invalid format for field "${columnName}": "${columnData}". Time-only fields must be in the format "HH:MM:SS".`,
400
)
}
}
parsedRow[columnName] = columnData
? new Date(columnData).toISOString()
: columnData
} else if (
columnType === FieldType.JSON &&
typeof columnData === "string"

View File

@ -46,7 +46,10 @@ const svelteCompilePlugin = {
let { argv } = require("yargs")
async function runBuild(entry, outfile) {
const isDev = process.env.NODE_ENV !== "production"
const isDev = !process.env.CI
console.log(`Building in mode dev mode: ${isDev}`)
const tsconfig = argv["p"] || `tsconfig.build.json`
const { data: tsconfigPathPluginContent } = loadTsConfig(
@ -58,7 +61,7 @@ async function runBuild(entry, outfile) {
entryPoints: [entry],
bundle: true,
minify: !isDev,
sourcemap: isDev,
sourcemap: tsconfigPathPluginContent.compilerOptions.sourceMap,
tsconfig,
plugins: [
svelteCompilePlugin,
@ -125,10 +128,12 @@ async function runBuild(entry, outfile) {
await Promise.all([hbsFiles, mainBuild, oldClientVersions])
fs.writeFileSync(
`dist/${path.basename(outfile)}.meta.json`,
JSON.stringify((await mainBuild).metafile)
)
if (isDev) {
fs.writeFileSync(
`dist/${path.basename(outfile)}.meta.json`,
JSON.stringify((await mainBuild).metafile)
)
}
console.log(
"\x1b[32m%s\x1b[0m",

View File

@ -11,6 +11,7 @@
"declaration": true,
"isolatedModules": true,
"baseUrl": ".",
"sourceMap": true,
"paths": {
"@budibase/types": ["./packages/types/src"],
"@budibase/backend-core": ["./packages/backend-core/src"],