WIP, made to work with object corresponding to db in frontend too

Seems to work pretty good, but not fully tested, & only locality stuff tested, only prints does not write to db
This commit is contained in:
arttu
2024-07-03 12:05:00 +03:00
committed by ShootingStar91
parent 04442972f6
commit 7aee6160aa
7 changed files with 242 additions and 62 deletions
+1 -2
View File
@@ -102,8 +102,7 @@ export const processLocalityForEdit = async (editedLocality: EditDataType<Locali
const oldLocality = await getLocalityDetails(fixedEditedLocality.lid!)
const validationErrors = validateEntireLocality(editedLocality)
if (validationErrors.length > 0) return { validationErrors }
// const testLoc = await getLocalityDetails(fixedEditedLocality.lid!)
const result = write(editedLocality, 'now_loc', oldLocality!)
const result = await write(editedLocality, 'now_loc', oldLocality!)
return { result }
}
+111 -46
View File
@@ -13,50 +13,115 @@ import {
TimeBoundDetailsType,
TimeUnitDetailsType,
} from '../../../../frontend/src/backendTypes'
import { revertFieldNames } from './writeUtils'
import { nowDb } from '../../utils/db'
import { logger } from '../../utils/logger'
import { isEmptyValue } from './writeUtils'
const DEBUG_MODE = true
const debugLog = (msg: string) => {
if (DEBUG_MODE) logger.info(msg)
}
type WriteFunction = (
data: EditDataType<LocalityDetailsType | SpeciesDetailsType | TimeUnitDetailsType | TimeBoundDetailsType>,
tableName: string,
oldObject: EditDataType<LocalityDetailsType | SpeciesDetailsType | TimeUnitDetailsType | TimeBoundDetailsType>
) => string
) => Promise<string>
const ids = { now_loc: 'lid', now_ls: 'species_id' }
const ids = {
now_loc: ['lid'],
now_ls: ['lid', 'species_id'],
now_proj: ['pid'],
now_plr: ['lid', 'pid'],
now_syn_loc: ['syn_id'],
now_mus: ['lid', 'museum'],
now_coll_meth: ['lid', 'coll_meth'],
com_mlist: ['museum'],
com_species: ['species_id'],
now_ss: ['lid', 'sed_struct'],
}
type Item = { column: string; value: any }
type WriteItem = { table: string; type: 'add' | 'update' | 'delete'; items: Item[] }
const getFindFunction = (tableName: string, newItem: any) => (item: any) => {
const matchFields = (fields: string[]) => {
return !fields.find(f => newItem[f] !== item[f])
}
const matches = { now_ls: ['species_id', 'lid'], now_proj: ['pid'], now_plr: ['lid', 'pid'], now_syn_loc: ['syn_id'] }
if (!matches[tableName]) console.log('Alert matcher missing for ', tableName)
return matchFields(matches[tableName])
if (!ids[tableName]) throw new Error(`Matcher missing for ${tableName}`)
return !ids[tableName].find(f => newItem[f] !== item[f])
}
// TODO change ignored fields to allowed fields. form those by taking prisma.model.fields, and adding the accepted relation fields.
const ignoredFields = ['now_lau']
export const write: WriteFunction = (data, tableName, oldObject) => {
console.log('Write')
const allowedFields = {}
const addFieldsToAllowed = fields =>
fields.forEach(f => {
allowedFields[f] = true
})
const supportedTables = [
'now_loc',
'now_ls',
'now_proj',
'now_plr',
'now_syn_loc',
'now_mus',
'now_coll_meth',
'com_mlist',
'com_species',
'now_ss',
]
// Adding tables and fields to allowed field values
addFieldsToAllowed(supportedTables)
supportedTables.forEach(table => addFieldsToAllowed(Object.keys(nowDb[table].fields)))
// Remove some fields that should be ignored
allowedFields['now_lau'] = false
allowedFields['now_sau'] = false
allowedFields['now_tau'] = false
allowedFields['now_bau'] = false
export const write: WriteFunction = async (data, tableName, oldObject) => {
debugLog('Write')
const writeList: WriteItem[] = []
const writeTable = (obj: object, tableName: string, oldObj: object | null) => {
const allFields = Object.keys(obj).filter(f => !ignoredFields.includes(f))
const fields = allFields.filter(f => typeof obj[f as keyof object] !== 'object' || obj[f as keyof object] === null)
const relationFields = allFields.filter(
f => typeof obj[f as keyof object] === 'object' && obj[f as keyof object] !== null
const writeTable = async (obj: object, tableName: string) => {
debugLog(`writeTable ${tableName} ${Object.keys(obj)}`)
const where = ids[tableName].reduce((acc, cur) => {
if (obj[cur] !== undefined) return { ...acc, [cur]: obj[cur] }
return acc
}, {})
let whereObject = { [ids[tableName].join('_')]: where }
if (ids[tableName].length === 1) whereObject = { [ids[tableName][0]]: obj[ids[tableName][0]] }
debugLog(ids[tableName], JSON.stringify(where))
const oldObj =
ids[tableName] && Object.keys(where).length === ids[tableName].length
? await nowDb[tableName].findUnique({
where: whereObject,
})
: null
debugLog(`old obj: ${!!oldObj} ${Object.keys(whereObject).length} ${ids[tableName].length}`)
const allFields = Object.keys(obj).filter(f => allowedFields[f])
const basicFields = allFields.filter(
f => typeof obj[f as keyof object] !== 'object' || obj[f as keyof object] === null
)
// console.log('fields: ', JSON.stringify(fields))
// console.log('relations: ', JSON.stringify(relationFields))
const arrayFields = allFields.filter(f => Array.isArray(obj[f as keyof object]))
const objectFields = allFields.filter(f => typeof obj[f] === 'object' && obj[f] !== null && !Array.isArray(obj[f]))
const fieldsToWrite: Item[] = []
const rowsToDelete: Item[] = []
for (const field of fields) {
const newValue = obj[field as keyof object] as any
const relationIds = {}
for (const objectField of objectFields) {
const newId = await writeTable(obj[objectField], objectField, oldObj?.[objectField])
relationIds[ids[objectField][0]] = newId
debugLog(`Processed objectField ${objectField} and assigned id ${newId} to ${ids[objectField][0]}`)
}
basicFields.push(Object.keys(relationIds))
for (const field of basicFields) {
const newValue = relationIds[field] ?? (obj[field as keyof object] as any)
const oldValue = oldObj?.[field as keyof object]
if (newValue === oldValue) continue
console.log(
debugLog(
`Field: ${field} Old value: ${oldValue} - ${typeof oldValue} New value: ${newValue} - ${typeof newValue}`
)
if (newValue === oldValue) continue
if (isEmptyValue(newValue) && isEmptyValue(oldValue)) continue
if (typeof oldValue === 'bigint' && typeof newValue === 'number' && BigInt(newValue) === oldValue) continue
fieldsToWrite.push({ column: field, value: newValue })
}
@@ -65,7 +130,7 @@ export const write: WriteFunction = (data, tableName, oldObject) => {
const idFieldName = ids[tableName as keyof object] as string
let id = obj[idFieldName as keyof object] as string
if (values.length > 0) {
if (id) {
if (id && !!oldObj) {
query(`UPDATE ${tableName} SET ${columns.map(c => `${c} = ?`).join(', ')} RETURNING ${idFieldName}`, [
...values,
])
@@ -73,36 +138,36 @@ export const write: WriteFunction = (data, tableName, oldObject) => {
id = query(`INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (?) RETURNING ${idFieldName}`, values)
}
}
for (const relationField of relationFields) {
const value = obj[relationField]
if (Array.isArray(value)) {
const toBeDeleted: Item[] = value
.filter(({ rowState }) => rowState === 'removed')
.map(item => ({ column: relationField, value: item[idFieldName] }))
rowsToDelete.push(...toBeDeleted)
for (const item of value.filter(({ rowState }) => rowState !== 'removed')) {
const itemWithId = { ...item, [ids[tableName]]: id }
// console.log('item', item, JSON.stringify(item))
writeTable(itemWithId, relationField, oldObj[relationField].find(getFindFunction(relationField, itemWithId)))
}
} else {
// writing a relation object
const itemWithId = { ...value, [idFieldName]: id }
writeTable(itemWithId, relationField, oldObj?.[relationField as keyof object] as any)
for (const arrayField of arrayFields) {
const items = obj[arrayField]
// The ones with rowState 'removed' will be deleted
const toBeDeleted: Item[] = items
.filter(({ rowState }) => rowState === 'removed')
.map(item => ({ column: arrayField, value: item[idFieldName] }))
rowsToDelete.push(...toBeDeleted)
// Ones without that rowstate will be recursed into
for (const item of items.filter(({ rowState }) => rowState !== 'removed')) {
const itemWithId = { ...item, [ids[tableName]]: id }
await writeTable(itemWithId, arrayField, oldObj?.[arrayField]?.find(getFindFunction(arrayField, itemWithId)))
debugLog(`Processed array item ${JSON.stringify(item)}`)
}
}
rowsToDelete.map(item => query(`DELETE FROM ? WHERE ? = ?`, [item.column, ids[item.column], item.value])) // NOTE column here is actually table name
//console.log(JSON.stringify(fieldsToWrite, null, 2))
rowsToDelete.map(item => query(`DELETE FROM ${tableName} WHERE ? = ?`, [ids[item.column], item.value])) // NOTE column here is actually table name
//debugLog(JSON.stringify(fieldsToWrite, null, 2))
debugLog(`Returning id: ${id}`)
return id
}
// console.log('Old object', JSON.stringify(oldObject, null, 2))
// console.log('New object', JSON.stringify(data, null, 2))
writeTable(revertFieldNames(data), tableName, revertFieldNames(oldObject))
// console.log(JSON.stringify(writeList, null, 2))
// debugLog('Old object', JSON.stringify(oldObject, null, 2))
// debugLog('New object', JSON.stringify(data, null, 2))
await writeTable(data, tableName)
// debugLog(JSON.stringify(writeList, null, 2))
return 'ok'
}
const query = (queryString: string, values: any[]) => {
console.log(`${queryString} \t| values: ${values.join(', ')}`)
logger.info(`${queryString} \t| values: ${values.join(', ')}`)
return 'some_id'
}
+6
View File
@@ -48,3 +48,9 @@ export const printJSON = (obj: object) =>
},
2
)
export const isEmptyValue = (value: string | number | null | undefined) => {
if (value === null || value === undefined) return true
if (typeof value === 'string' && value === '') return true
return false
}
+5 -4
View File
@@ -25,7 +25,7 @@ export type Editable<T> = T & { rowState?: RowState }
export type SedimentaryStructureValues = Prisma.now_ss_values
export type CollectingMethod = Prisma.now_coll_meth
export type LocalityProject = Prisma.now_plr & { now_proj: Prisma.now_proj }
export type LocalitySpecies = Omit<Prisma.now_ls, 'com_species'> & { com_species: SpeciesType }
export type LocalitySpecies = Prisma.now_ls & { com_species: SpeciesType }
export type SpeciesLocality = Prisma.now_ls & { now_loc: Prisma.now_loc }
export type LocalityUpdate = Prisma.now_lau & { now_lr: LocalityReference[] } & { updates: UpdateLog[] }
export type SpeciesUpdate = Prisma.now_sau & { now_sr: Prisma.now_sr }
@@ -40,12 +40,13 @@ export type RegionCountry = Prisma.now_reg_coord_country
export type SedimentaryStructure = Prisma.now_ss
export type LocalitySynonym = Prisma.now_syn_loc
export type SpeciesSynonym = Prisma.com_taxa_synonym
export type LocalityDetailsType = Omit<Prisma.now_loc, 'now_ls' | 'now_mus' | 'now_proj'> & {
museums: Array<Museum>
export type LocalityMuseum = Prisma.now_mus & { com_mlist: Museum }
export type LocalityDetailsType = Prisma.now_loc & {
now_mus: Array<LocalityMuseum>
} & {
projects: Array<Project>
} & {
species: Array<LocalitySpecies>
now_ls: Array<LocalitySpecies>
} & { now_plr: Array<LocalityProject> } & { now_syn_loc: Array<LocalitySynonym> } & {
now_ss: SedimentaryStructure[]
} & {
+32
View File
@@ -44,3 +44,35 @@ export const referenceTableColumns: MRT_ColumnDef<Reference>[] = [
maxSize: 60,
},
]
export const emptyLocalitySpeciesFields = {
nis: null,
pct: null,
quad: null,
mni: null,
qua: null,
id_status: null,
orig_entry: null,
source_name: null,
body_mass: null,
mesowear: null,
mw_or_high: null,
mw_or_low: null,
mw_cs_sharp: null,
mw_cs_round: null,
mw_cs_blunt: null,
mw_scale_min: null,
mw_scale_max: null,
mw_value: null,
microwear: null,
dc13_mean: null,
dc13_n: null,
dc13_max: null,
dc13_min: null,
dc13_stdev: null,
do18_mean: null,
do18_n: null,
do18_max: null,
do18_min: null,
do18_stdev: null,
}
@@ -9,7 +9,7 @@ import { Box, CircularProgress } from '@mui/material'
import { MRT_ColumnDef } from 'material-react-table'
export const MuseumTab = () => {
const { mode } = useDetailContext<LocalityDetailsType>()
const { mode, editData, setEditData } = useDetailContext<LocalityDetailsType>()
const { data: museumData, isError } = useGetAllMuseumsQuery()
if (isError) return 'Error loading museums.'
@@ -51,18 +51,38 @@ export const MuseumTab = () => {
<EditingForm<Museum, LocalityDetailsType>
buttonText="Add new museum"
formFields={formFields}
arrayFieldName="museums"
editAction={(newMuseum: Museum) => {
setEditData({
...editData,
now_mus: [
...editData.now_mus,
{ lid: editData.lid, museum: newMuseum.museum, com_mlist: newMuseum, rowState: 'new' },
],
})
}}
/>
<SelectingTable<Museum, LocalityDetailsType>
buttonText="Select Museum"
data={museumData}
columns={columns}
fieldName="museums"
fieldName="now_mus"
idFieldName="museum"
editingAction={(newMuseum: Museum) => {
setEditData({
...editData,
now_mus: [
...editData.now_mus,
{ lid: editData.lid, museum: newMuseum.museum, com_mlist: newMuseum, rowState: 'new' },
],
})
}}
/>
</Box>
)}
<EditableTable<Editable<Museum>, LocalityDetailsType> columns={columns} field="museums" />
<EditableTable<Editable<Museum>, LocalityDetailsType>
columns={columns.map(col => ({ ...col, accessorKey: `com_mlist.${col.accessorKey}` }))}
field="now_mus"
/>
</Grouped>
)
}
@@ -1,4 +1,4 @@
import { LocalityDetailsType, Species } from '@/backendTypes'
import { LocalityDetailsType, LocalitySpecies, Species } from '@/backendTypes'
import { EditableTable } from '@/components/DetailView/common/EditableTable'
import { EditingForm } from '@/components/DetailView/common/EditingForm'
import { SelectingTable } from '@/components/DetailView/common/SelectingTable'
@@ -49,7 +49,40 @@ export const SpeciesTab = () => {
header: 'Taxon status',
},
]
const columns2: MRT_ColumnDef<LocalitySpecies>[] = [
{
accessorKey: 'com_species.order_name',
header: 'Order',
},
{
accessorKey: 'com_species.family_name',
header: 'Family',
},
{
accessorKey: 'com_species.genus_name',
header: 'Genus',
},
{
accessorKey: 'com_species.species_name',
header: 'Species',
},
{
accessorKey: 'com_species.subclass_or_superorder_name',
header: 'Subclass or Superorder',
},
{
accessorKey: 'com_species.suborder_or_superfamily_name',
header: 'Suborder or Superfamily',
},
{
accessorKey: 'com_species.unique_identifier',
header: 'Unique Identifier',
},
{
accessorKey: 'com_species.taxonomic_status',
header: 'Taxon status',
},
]
const formFields: { name: string; label: string; required?: boolean }[] = [
{ name: 'order_name', label: 'Order', required: true },
{ name: 'family_name', label: 'Family', required: true },
@@ -68,21 +101,45 @@ export const SpeciesTab = () => {
<EditingForm<Species, LocalityDetailsType>
buttonText="Add new Species"
formFields={formFields}
arrayFieldName="species"
editAction={(newSpecies: Species) => {
setEditData({
...editData,
now_ls: [
...editData.now_ls,
{
lid: editData.lid,
species_id: newSpecies.species_id,
com_species: { ...newSpecies },
rowState: 'new',
},
],
})
}}
/>
<SelectingTable<Species, LocalityDetailsType>
buttonText="Select Species"
data={speciesData}
columns={columns}
fieldName="species"
fieldName="now_ls"
idFieldName="species_id"
editingAction={(newSpecies: Species) => {
setEditData({ ...editData, species: [...editData.species, { ...newSpecies, rowState: 'new' }] })
setEditData({
...editData,
now_ls: [
...editData.now_ls,
{
lid: editData.lid,
species_id: newSpecies.species_id,
com_species: { ...newSpecies },
rowState: 'new',
},
],
})
}}
/>
</Box>
)}
<EditableTable<Species, LocalityDetailsType> columns={columns} field="species" />
<EditableTable<LocalitySpecies, LocalityDetailsType> columns={columns2} field="now_ls" />
</Grouped>
)
}