Merge pull request #15236 from Budibase/typing/stores-grid-datasources

Typing grid datasources stores
This commit is contained in:
Adria Navarro 2024-12-24 16:23:18 +01:00 committed by GitHub
commit 0d64ce42f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 318 additions and 179 deletions

View File

@ -1,10 +1,54 @@
import { derived, get } from "svelte/store"
import { derived, get, Readable, Writable } from "svelte/store"
import { getDatasourceDefinition, getDatasourceSchema } from "../../../fetch"
import { enrichSchemaWithRelColumns, memo } from "../../../utils"
import { cloneDeep } from "lodash"
import { ViewV2Type } from "@budibase/types"
import {
Row,
SaveRowRequest,
SaveTableRequest,
UIDatasource,
UIFieldMutation,
UIFieldSchema,
UpdateViewRequest,
ViewV2Type,
} from "@budibase/types"
import { Store as StoreContext } from "."
import { DatasourceActions } from "./datasources"
export const createStores = () => {
interface DatasourceStore {
definition: Writable<UIDatasource>
schemaMutations: Writable<Record<string, UIFieldMutation>>
subSchemaMutations: Writable<Record<string, Record<string, UIFieldMutation>>>
}
interface DerivedDatasourceStore {
schema: Readable<Record<string, UIFieldSchema> | null>
enrichedSchema: Readable<Record<string, UIFieldSchema> | null>
hasBudibaseIdentifiers: Readable<boolean>
}
interface ActionDatasourceStore {
datasource: DatasourceStore["definition"] & {
actions: DatasourceActions & {
refreshDefinition: () => Promise<void>
changePrimaryDisplay: (column: string) => Promise<void>
addSchemaMutation: (field: string, mutation: UIFieldMutation) => void
addSubSchemaMutation: (
field: string,
fromField: string,
mutation: UIFieldMutation
) => void
saveSchemaMutations: () => Promise<void>
resetSchemaMutations: () => void
}
}
}
export type Store = DatasourceStore &
DerivedDatasourceStore &
ActionDatasourceStore
export const createStores = (): DatasourceStore => {
const definition = memo(null)
const schemaMutations = memo({})
const subSchemaMutations = memo({})
@ -16,7 +60,7 @@ export const createStores = () => {
}
}
export const deriveStores = context => {
export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
const {
API,
definition,
@ -27,7 +71,7 @@ export const deriveStores = context => {
} = context
const schema = derived(definition, $definition => {
let schema = getDatasourceSchema({
let schema: Record<string, UIFieldSchema> = getDatasourceSchema({
API,
datasource: get(datasource),
definition: $definition,
@ -40,7 +84,7 @@ export const deriveStores = context => {
// Certain datasources like queries use primitives.
Object.keys(schema || {}).forEach(key => {
if (typeof schema[key] !== "object") {
schema[key] = { type: schema[key] }
schema[key] = { name: key, type: schema[key] }
}
})
@ -58,19 +102,18 @@ export const deriveStores = context => {
const schemaWithRelatedColumns = enrichSchemaWithRelColumns($schema)
const enrichedSchema = {}
Object.keys(schemaWithRelatedColumns).forEach(field => {
const enrichedSchema: Record<string, UIFieldSchema> = {}
Object.keys(schemaWithRelatedColumns || {}).forEach(field => {
enrichedSchema[field] = {
...schemaWithRelatedColumns[field],
...schemaWithRelatedColumns?.[field],
...$schemaOverrides?.[field],
...$schemaMutations[field],
}
if ($subSchemaMutations[field]) {
enrichedSchema[field].columns ??= {}
for (const [fieldName, mutation] of Object.entries(
$subSchemaMutations[field]
)) {
for (const fieldName of Object.keys($subSchemaMutations[field])) {
const mutation = $subSchemaMutations[field][fieldName]
enrichedSchema[field].columns[fieldName] = {
...enrichedSchema[field].columns[fieldName],
...mutation,
@ -87,10 +130,14 @@ export const deriveStores = context => {
([$datasource, $definition]) => {
let type = $datasource?.type
if (type === "provider") {
type = $datasource.value?.datasource?.type
type = ($datasource as any).value?.datasource?.type
}
// Handle calculation views
if (type === "viewV2" && $definition?.type === ViewV2Type.CALCULATION) {
if (
type === "viewV2" &&
"type" in $definition &&
$definition?.type === ViewV2Type.CALCULATION
) {
return false
}
return ["table", "viewV2", "link"].includes(type)
@ -104,7 +151,7 @@ export const deriveStores = context => {
}
}
export const createActions = context => {
export const createActions = (context: StoreContext): ActionDatasourceStore => {
const {
API,
datasource,
@ -147,21 +194,23 @@ export const createActions = context => {
}
// Saves the datasource definition
const saveDefinition = async newDefinition => {
const saveDefinition = async (
newDefinition: SaveTableRequest | UpdateViewRequest
) => {
// Update local state
const originalDefinition = get(definition)
definition.set(newDefinition)
definition.set(newDefinition as UIDatasource)
// Update server
if (get(config).canSaveSchema) {
try {
await getAPI()?.actions.saveDefinition(newDefinition)
await getAPI()?.actions.saveDefinition(newDefinition as never)
// Broadcast change so external state can be updated, as this change
// will not be received by the builder websocket because we caused it
// ourselves
dispatch("updatedatasource", newDefinition)
} catch (error) {
} catch (error: any) {
const msg = error?.message || error || "Unknown error"
get(notifications).error(`Error saving schema: ${msg}`)
@ -172,7 +221,7 @@ export const createActions = context => {
}
// Updates the datasources primary display column
const changePrimaryDisplay = async column => {
const changePrimaryDisplay = async (column: string) => {
let newDefinition = cloneDeep(get(definition))
// Update primary display
@ -183,12 +232,14 @@ export const createActions = context => {
newDefinition.schema[column].constraints = {}
}
newDefinition.schema[column].constraints.presence = { allowEmpty: false }
delete newDefinition.schema[column].default
return await saveDefinition(newDefinition)
if ("default" in newDefinition.schema[column]) {
delete newDefinition.schema[column].default
}
return await saveDefinition(newDefinition as any)
}
// Adds a schema mutation for a single field
const addSchemaMutation = (field, mutation) => {
const addSchemaMutation = (field: string, mutation: UIFieldMutation) => {
if (!field || !mutation) {
return
}
@ -204,7 +255,11 @@ export const createActions = context => {
}
// Adds a nested schema mutation for a single field
const addSubSchemaMutation = (field, fromField, mutation) => {
const addSubSchemaMutation = (
field: string,
fromField: string,
mutation: UIFieldMutation
) => {
if (!field || !fromField || !mutation) {
return
}
@ -231,8 +286,8 @@ export const createActions = context => {
const $definition = get(definition)
const $schemaMutations = get(schemaMutations)
const $subSchemaMutations = get(subSchemaMutations)
const $schema = get(schema)
let newSchema = {}
const $schema = get(schema) || {}
let newSchema: Record<string, UIFieldSchema> = {}
// Build new updated datasource schema
Object.keys($schema).forEach(column => {
@ -242,9 +297,8 @@ export const createActions = context => {
}
if ($subSchemaMutations[column]) {
newSchema[column].columns ??= {}
for (const [fieldName, mutation] of Object.entries(
$subSchemaMutations[column]
)) {
for (const fieldName of Object.keys($subSchemaMutations[column])) {
const mutation = $subSchemaMutations[column][fieldName]
newSchema[column].columns[fieldName] = {
...newSchema[column].columns[fieldName],
...mutation,
@ -257,7 +311,7 @@ export const createActions = context => {
await saveDefinition({
...$definition,
schema: newSchema,
})
} as any)
resetSchemaMutations()
}
@ -267,32 +321,32 @@ export const createActions = context => {
}
// Adds a row to the datasource
const addRow = async row => {
const addRow = async (row: SaveRowRequest) => {
return await getAPI()?.actions.addRow(row)
}
// Updates an existing row in the datasource
const updateRow = async row => {
const updateRow = async (row: SaveRowRequest) => {
return await getAPI()?.actions.updateRow(row)
}
// Deletes rows from the datasource
const deleteRows = async rows => {
const deleteRows = async (rows: Row[]) => {
return await getAPI()?.actions.deleteRows(rows)
}
// Gets a single row from a datasource
const getRow = async id => {
const getRow = async (id: string) => {
return await getAPI()?.actions.getRow(id)
}
// Checks if a certain datasource config is valid
const isDatasourceValid = datasource => {
const isDatasourceValid = (datasource: UIDatasource) => {
return getAPI()?.actions.isDatasourceValid(datasource)
}
// Checks if this datasource can use a specific column by name
const canUseColumn = name => {
const canUseColumn = (name: string) => {
return getAPI()?.actions.canUseColumn(name)
}

View File

@ -0,0 +1,31 @@
import {
Row,
SaveRowRequest,
SaveTableRequest,
UIDatasource,
UpdateViewRequest,
} from "@budibase/types"
interface DatasourceBaseActions<
TSaveDefinitionRequest = UpdateViewRequest | SaveTableRequest
> {
saveDefinition: (newDefinition: TSaveDefinitionRequest) => Promise<void>
addRow: (row: SaveRowRequest) => Promise<Row | void>
updateRow: (row: SaveRowRequest) => Promise<Row | void>
deleteRows: (rows: Row[]) => Promise<void>
getRow: (id: string) => Promise<Row | void>
isDatasourceValid: (datasource: UIDatasource) => boolean | void
canUseColumn: (name: string) => boolean | void
}
export interface DatasourceTableActions
extends DatasourceBaseActions<SaveTableRequest> {}
export interface DatasourceViewActions
extends DatasourceBaseActions<UpdateViewRequest> {}
export interface DatasourceNonPlusActions
extends DatasourceBaseActions<never> {}
export type DatasourceActions =
| DatasourceTableActions & DatasourceViewActions & DatasourceNonPlusActions

View File

@ -1,18 +1,11 @@
import { SortOrder, UIDatasource } from "@budibase/types"
import { get } from "svelte/store"
import { Store as StoreContext } from ".."
import { DatasourceNonPlusActions } from "."
interface NonPlusActions {
nonPlus: {
actions: {
saveDefinition: () => Promise<void>
addRow: () => Promise<void>
updateRow: () => Promise<void>
deleteRows: () => Promise<void>
getRow: () => Promise<void>
isDatasourceValid: (datasource: UIDatasource) => boolean
canUseColumn: (name: string) => boolean
}
actions: DatasourceNonPlusActions
}
}

View File

@ -1,27 +1,19 @@
import {
Row,
SaveRowRequest,
SaveRowResponse,
SaveTableRequest,
SortOrder,
UIDatasource,
} from "@budibase/types"
import { get } from "svelte/store"
import { Store as StoreContext } from ".."
import { DatasourceTableActions } from "."
const SuppressErrors = true
interface TableActions {
table: {
actions: {
saveDefinition: (newDefinition: SaveTableRequest) => Promise<void>
addRow: (row: SaveRowRequest) => Promise<SaveRowResponse>
updateRow: (row: SaveRowRequest) => Promise<SaveRowResponse>
deleteRows: (rows: (string | Row)[]) => Promise<void>
getRow: (id: string) => Promise<Row>
isDatasourceValid: (datasource: UIDatasource) => boolean
canUseColumn: (name: string) => boolean
}
actions: DatasourceTableActions
}
}
@ -42,7 +34,7 @@ export const createActions = (context: StoreContext): TableActions => {
return await API.saveRow(row, SuppressErrors)
}
const deleteRows = async (rows: (string | Row)[]) => {
const deleteRows = async (rows: Row[]) => {
await API.deleteRows(get(datasource).tableId, rows)
}

View File

@ -4,23 +4,17 @@ import {
SaveRowRequest,
SortOrder,
UIDatasource,
UIView,
UpdateViewRequest,
} from "@budibase/types"
import { Store as StoreContext } from ".."
import { DatasourceViewActions } from "."
const SuppressErrors = true
interface ViewActions {
viewV2: {
actions: {
saveDefinition: (newDefinition: UpdateViewRequest) => Promise<void>
addRow: (row: SaveRowRequest) => Promise<Row>
updateRow: (row: SaveRowRequest) => Promise<Row>
deleteRows: (rows: (string | Row)[]) => Promise<void>
getRow: (id: string) => Promise<Row>
isDatasourceValid: (datasource: UIDatasource) => boolean
canUseColumn: (name: string) => boolean
}
actions: DatasourceViewActions
}
}
@ -46,7 +40,7 @@ export const createActions = (context: StoreContext): ViewActions => {
}
}
const deleteRows = async (rows: (string | Row)[]) => {
const deleteRows = async (rows: Row[]) => {
await API.deleteRows(get(datasource).id, rows)
}
@ -154,7 +148,7 @@ export const initialise = (context: StoreContext) => {
unsubscribers.push(
sort.subscribe(async $sort => {
// Ensure we're updating the correct view
const $view = get(definition)
const $view = get(definition) as UIView
if ($view?.id !== $datasource.id) {
return
}
@ -205,7 +199,7 @@ export const initialise = (context: StoreContext) => {
await datasource.actions.saveDefinition({
...$view,
queryUI: $filter,
})
} as never as UpdateViewRequest)
// Refresh data since view definition changed
await rows.actions.refreshData()

View File

@ -59,11 +59,9 @@ export type Store = BaseStore &
Columns.Store &
Table.Store &
ViewV2.Store &
NonPlus.Store & {
NonPlus.Store &
Datasource.Store & {
// TODO while typing the rest of stores
datasource: Writable<any> & { actions: any }
definition: Writable<any>
enrichedSchema: any
fetch: Writable<any>
filter: Writable<any>
inlineFilters: Writable<any>
@ -75,6 +73,9 @@ export type Store = BaseStore &
rows: Writable<any> & { actions: any }
subscribe: any
config: Writable<any>
dispatch: (event: string, data: any) => any
notifications: Writable<any>
schemaOverrides: Writable<any>
}
export const attachStores = (context: Store): Store => {
@ -106,5 +107,5 @@ export const attachStores = (context: Store): Store => {
}
}
return context
return context as Store
}

View File

@ -1,103 +0,0 @@
import { FieldType, RelationshipType } from "@budibase/types"
import { Helpers } from "@budibase/bbui"
const columnTypeManyTypeOverrides = {
[FieldType.DATETIME]: FieldType.STRING,
[FieldType.BOOLEAN]: FieldType.STRING,
[FieldType.SIGNATURE_SINGLE]: FieldType.ATTACHMENTS,
}
const columnTypeManyParser = {
[FieldType.DATETIME]: (value, field) => {
function parseDate(value) {
const { timeOnly, dateOnly, ignoreTimezones } = field || {}
const enableTime = !dateOnly
const parsedValue = Helpers.parseDate(value, {
timeOnly,
enableTime,
ignoreTimezones,
})
const parsed = Helpers.getDateDisplayValue(parsedValue, {
enableTime,
timeOnly,
})
return parsed
}
return value.map(v => parseDate(v))
},
[FieldType.BOOLEAN]: value => value.map(v => !!v),
[FieldType.BB_REFERENCE_SINGLE]: value => [
...new Map(value.map(i => [i._id, i])).values(),
],
[FieldType.BB_REFERENCE]: value => [
...new Map(value.map(i => [i._id, i])).values(),
],
[FieldType.ARRAY]: value => Array.from(new Set(value)),
}
export function enrichSchemaWithRelColumns(schema) {
if (!schema) {
return
}
const result = Object.keys(schema).reduce((result, fieldName) => {
const field = schema[fieldName]
result[fieldName] = field
if (field.visible !== false && field.columns) {
const fromSingle =
field?.relationshipType === RelationshipType.ONE_TO_MANY
for (const relColumn of Object.keys(field.columns)) {
const relField = field.columns[relColumn]
if (!relField.visible) {
continue
}
const name = `${field.name}.${relColumn}`
result[name] = {
...relField,
name,
related: { field: fieldName, subField: relColumn },
cellRenderType:
(!fromSingle && columnTypeManyTypeOverrides[relField.type]) ||
relField.type,
}
}
}
return result
}, {})
return result
}
export function getRelatedTableValues(row, field, fromField) {
const fromSingle =
fromField?.relationshipType === RelationshipType.ONE_TO_MANY
let result = ""
if (fromSingle) {
result = row[field.related.field]?.[0]?.[field.related.subField]
} else {
const parser = columnTypeManyParser[field.type] || (value => value)
const value = row[field.related.field]
?.flatMap(r => r[field.related.subField])
?.filter(i => i !== undefined && i !== null)
result = parser(value || [], field)
if (
[
FieldType.STRING,
FieldType.NUMBER,
FieldType.BIGINT,
FieldType.BOOLEAN,
FieldType.DATETIME,
FieldType.LONGFORM,
FieldType.BARCODEQR,
].includes(field.type)
) {
result = result?.join(", ")
}
}
return result
}

View File

@ -0,0 +1,129 @@
import { Helpers } from "@budibase/bbui"
import {
FieldType,
isRelationshipField,
RelationshipType,
Row,
UIFieldSchema,
} from "@budibase/types"
const columnTypeManyTypeOverrides: Partial<Record<FieldType, FieldType>> = {
[FieldType.DATETIME]: FieldType.STRING,
[FieldType.BOOLEAN]: FieldType.STRING,
[FieldType.SIGNATURE_SINGLE]: FieldType.ATTACHMENTS,
}
const columnTypeManyParser = {
[FieldType.DATETIME]: (
value: any[],
field: {
timeOnly?: boolean
dateOnly?: boolean
}
) => {
function parseDate(value: any) {
const { timeOnly, dateOnly } = field || {}
const enableTime = !dateOnly
const parsedValue = Helpers.parseDate(value, { enableTime })
const parsed = Helpers.getDateDisplayValue(parsedValue, {
enableTime,
timeOnly,
})
return parsed
}
return value.map(v => parseDate(v))
},
[FieldType.BOOLEAN]: (value: any[]) => value.map(v => !!v),
[FieldType.BB_REFERENCE_SINGLE]: (value: any[]) => [
...new Map(value.map(i => [i._id, i])).values(),
],
[FieldType.BB_REFERENCE]: (value: any[]) => [
...new Map(value.map(i => [i._id, i])).values(),
],
[FieldType.ARRAY]: (value: any[]) => Array.from(new Set(value)),
}
export function enrichSchemaWithRelColumns(
schema: Record<string, UIFieldSchema>
): Record<string, UIFieldSchema> | undefined {
if (!schema) {
return
}
const result = Object.keys(schema).reduce<Record<string, UIFieldSchema>>(
(result, fieldName) => {
const field = schema[fieldName]
result[fieldName] = field
if (
field.visible !== false &&
isRelationshipField(field) &&
field.columns
) {
const fromSingle =
field?.relationshipType === RelationshipType.ONE_TO_MANY
for (const relColumn of Object.keys(field.columns)) {
const relField = field.columns[relColumn]
if (!relField.visible) {
continue
}
const name = `${field.name}.${relColumn}`
result[name] = {
...relField,
type: relField.type as any, // TODO
name,
related: { field: fieldName, subField: relColumn },
cellRenderType:
(!fromSingle && columnTypeManyTypeOverrides[relField.type]) ||
relField.type,
}
}
}
return result
},
{}
)
return result
}
export function getRelatedTableValues(
row: Row,
field: UIFieldSchema & { related: { field: string; subField: string } },
fromField: UIFieldSchema
) {
const fromSingle =
isRelationshipField(fromField) &&
fromField?.relationshipType === RelationshipType.ONE_TO_MANY
let result = ""
if (fromSingle) {
result = row[field.related.field]?.[0]?.[field.related.subField]
} else {
const parser =
columnTypeManyParser[field.type as keyof typeof columnTypeManyParser] ||
((value: any) => value)
const value = row[field.related.field]
?.flatMap((r: Row) => r[field.related.subField])
?.filter((i: any) => i !== undefined && i !== null)
const parsed = parser(value || [], field as any)
result = parsed as any
if (
[
FieldType.STRING,
FieldType.NUMBER,
FieldType.BIGINT,
FieldType.BOOLEAN,
FieldType.DATETIME,
FieldType.LONGFORM,
FieldType.BARCODEQR,
].includes(field.type)
) {
result = parsed?.join(", ")
}
}
return result
}

View File

@ -1,5 +1,11 @@
export interface UIDatasource {
import { UITable, UIView } from "@budibase/types"
export type UIDatasource = (UITable | UIView) & {
type: string
id: string
tableId: string
}
export interface UIFieldMutation {
visible?: boolean
readonly?: boolean
width?: number
}

View File

@ -1,2 +1,4 @@
export * from "./columns"
export * from "./datasource"
export * from "./table"
export * from "./view"

View File

@ -0,0 +1,34 @@
import {
BasicViewFieldMetadata,
FieldSchema,
FieldType,
RelationSchemaField,
SortOrder,
Table,
UISearchFilter,
} from "@budibase/types"
export interface UITable extends Omit<Table, "type"> {
name: string
id: string
type: string
tableId: string
primaryDisplay?: string
sort?: {
field: string
order: SortOrder
}
queryUI: UISearchFilter
schema: Record<string, UIFieldSchema>
}
export type UIFieldSchema = FieldSchema &
BasicViewFieldMetadata & {
related?: { field: string; subField: string }
columns?: Record<string, UIRelationSchemaField>
cellRenderType?: string
}
interface UIRelationSchemaField extends RelationSchemaField {
type: FieldType
}

View File

@ -0,0 +1,6 @@
import { ViewV2 } from "@budibase/types"
import { UIFieldSchema } from "./table"
export interface UIView extends ViewV2 {
schema: Record<string, UIFieldSchema>
}