Skip to content

Commit

Permalink
Survey node defs fixer (#236)
Browse files Browse the repository at this point in the history
* survey node def fixer

* fix node def hierarchy

* code cleenup

* code cleanup

* code cleanup

* adjusted types

* added node defs fixer tests

* code cleanup

* code cleanup

* code cleanup

---------

Co-authored-by: Stefano Ricci <[email protected]>
  • Loading branch information
SteRiccio and SteRiccio authored Jul 15, 2024
1 parent b6a0a06 commit c03bf9c
Show file tree
Hide file tree
Showing 11 changed files with 305 additions and 35 deletions.
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export { ServiceRegistry, ServiceType } from './registry'
export { DEFAULT_SRS, DEFAULT_SRS_INDEX, SRSFactory } from './srs'
export type { SRS } from './srs'

export { SurveyDependencyType, SurveyFactory, SurveyRefDataFactory, Surveys } from './survey'
export { NodeDefsFixer, SurveyDependencyType, SurveyFactory, SurveyRefDataFactory, Surveys } from './survey'
export type {
Survey,
SurveyService,
Expand Down
33 changes: 27 additions & 6 deletions src/nodeDef/nodeDefs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LanguageCode } from '../language'
import { valuePropsCoordinate } from '../node/nodeValueProps'
import { defaultCycle } from '../survey'
import { Numbers, Objects, Strings } from '../utils'
import {
NodeDef,
Expand All @@ -13,12 +14,15 @@ import {
import { NodeDefCode } from './types/code'
import { NodeDefCoordinate } from './types/coordinate'
import { NodeDefDecimal } from './types/decimal'
import { NodeDefEntity, NodeDefEntityChildPosition, NodeDefEntityRenderType } from './types/entity'
import {
NodeDefEntity,
NodeDefEntityLayout,
NodeDefEntityLayoutChildItem,
NodeDefEntityRenderType,
} from './types/entity'
import { NodeDefTaxon } from './types/taxon'
import { NodeDefText } from './types/text'

const defaultCycle = '0'

const isRoot = (nodeDef: NodeDef<NodeDefType>): boolean => !nodeDef.parentUuid

const isAttribute = (nodeDef: NodeDef<NodeDefType>): boolean => !isEntity(nodeDef)
Expand Down Expand Up @@ -149,12 +153,22 @@ const isLayoutRenderTypeTable =
const getChildrenEntitiesInOwnPageUudis =
(cycle = defaultCycle) =>
(nodeDef: NodeDefEntity): string[] =>
getLayoutProps(cycle)(nodeDef).indexChildren
(getLayoutProps(cycle)(nodeDef) as NodeDefEntityLayout).indexChildren ?? []

const getLayoutChildren =
(cycle = defaultCycle) =>
(nodeDef: NodeDefEntity): NodeDefEntityChildPosition[] | string[] | undefined =>
getLayoutProps(cycle)(nodeDef).layoutChildren
(nodeDef: NodeDefEntity): NodeDefEntityLayoutChildItem[] =>
(getLayoutProps(cycle)(nodeDef) as NodeDefEntityLayout).layoutChildren ?? []

const getPageUuid =
(cycle = defaultCycle) =>
(nodeDef: NodeDefEntity): string | undefined =>
(getLayoutProps(cycle)(nodeDef) as NodeDefEntityLayout).pageUuid

const isDisplayInOwnPage =
(cycle = defaultCycle) =>
(nodeDef: NodeDefEntity): boolean =>
!!getPageUuid(cycle)(nodeDef)

const isHiddenInMobile =
(cycle = defaultCycle) =>
Expand Down Expand Up @@ -182,6 +196,9 @@ const getAreaBasedEstimatedOf = (nodeDef: NodeDef<any>): string | undefined =>

const getIndexInChain = (nodeDef: NodeDef<any>): number | undefined => nodeDef.propsAdvanced?.index

// Metadata
const getMetaHieararchy = (nodeDef: NodeDef<any>): string[] => nodeDef.meta?.h ?? []

export const NodeDefs = {
isEntity,
isMultiple,
Expand Down Expand Up @@ -224,6 +241,8 @@ export const NodeDefs = {
isLayoutRenderTypeTable,
getChildrenEntitiesInOwnPageUudis,
getLayoutChildren,
getPageUuid,
isDisplayInOwnPage,
isHiddenInMobile,
isIncludedInMultipleEntitySummary,
isHiddenWhenNotRelevant,
Expand All @@ -238,4 +257,6 @@ export const NodeDefs = {
// Analysis
getAreaBasedEstimatedOf,
getIndexInChain,
// Metadata
getMetaHieararchy,
}
12 changes: 7 additions & 5 deletions src/nodeDef/types/entity.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { NodeDef, NodeDefLayout, NodeDefPropsWithLayout, NodeDefType } from '../nodeDef'

export interface NodeDefEntityChildPosition {
h: number
h?: number
i: string
w: number
moved?: boolean
static?: number
w?: number
x: number
y: number
moved: boolean
static: number
}

export type NodeDefEntityLayoutChildItem = NodeDefEntityChildPosition | string

export interface NodeDefEntityProps extends NodeDefPropsWithLayout<NodeDefEntityLayout> {
enumerate?: boolean
}
Expand All @@ -22,7 +24,7 @@ export enum NodeDefEntityRenderType {
export interface NodeDefEntityLayout extends NodeDefLayout {
columnsNo?: number
indexChildren?: string[] // sorted children pages uuids
layoutChildren?: Array<NodeDefEntityChildPosition | string>
layoutChildren?: NodeDefEntityLayoutChildItem[]
pageUuid?: string
renderType: NodeDefEntityRenderType
}
Expand Down
4 changes: 3 additions & 1 deletion src/record/_records/recordGetters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { TraverseMethod } from '../../common'
import { NodeDef, NodeDefCode, NodeDefCodeProps, NodeDefProps, NodeDefType, NodeDefs } from '../../nodeDef'
import { Node, NodePointer, Nodes, NodesMap } from '../../node'
import { Record } from '../record'
import { Surveys } from '../../survey'
import { defaultCycle, Surveys } from '../../survey'
import { Arrays, Queue } from '../../utils'
import { Survey, SurveyDependencyType } from '../../survey/survey'
import { SystemError } from '../../error'
import { NodeValues } from '../../node/nodeValues'
import { RecordNodesIndexReader } from './recordNodesIndexReader'

export const getCycle = (record: Record): string => record.cycle ?? defaultCycle

export const getNodes = (record: Record): { [key: string]: Node } => record.nodes ?? {}

export const getNodesArray = (record: Record): Node[] => Object.values(getNodes(record))
Expand Down
48 changes: 29 additions & 19 deletions src/record/recordFixer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,33 +60,43 @@ const insertMissingSingleNodes = (params: {
return updateResult
}

const deleteNodesByDefUuid = (params: { record: Record; nodeDefUuid: string; sideEffect: boolean }) => {
const { record, nodeDefUuid, sideEffect } = params
const updateResult = new RecordUpdateResult({ record })

const nodesToDelete = Records.getNodesByDefUuid(nodeDefUuid)(updateResult.record)
nodesToDelete.forEach((nodeToDelete) => {
// cleanup child applicability
const parentNode = Records.getParent(nodeToDelete)(updateResult.record)
if (parentNode && !Nodes.isChildApplicable(parentNode, nodeDefUuid)) {
const parentNodeUpdated = Nodes.dissocChildApplicability(parentNode, nodeDefUuid)
const recordWithParentNodeUpdated = Records.addNode(parentNodeUpdated, { sideEffect })(updateResult.record)
updateResult.merge(new RecordUpdateResult({ record: recordWithParentNodeUpdated }))
}
})

const nodeUuidsToDelete = nodesToDelete.map((node) => node.uuid)
const nodesDeleteUpdateResult = Records.deleteNodes(nodeUuidsToDelete, { sideEffect })(updateResult.record)
updateResult.merge(nodesDeleteUpdateResult)

return updateResult
}

const fixRecord = (params: { survey: Survey; record: Record; sideEffect?: boolean }): RecordUpdateResult => {
const { survey, record, sideEffect = false } = params
const updateResult = new RecordUpdateResult({ record })
const result = new RecordUpdateResult({ record })

Records.getNodesArray(record).forEach((node) => {
const { nodeDefUuid } = node
const nodeDef = Surveys.findNodeDefByUuid({ survey, uuid: nodeDefUuid })
if (!nodeDef) {
const nodesToDelete = Records.getNodesByDefUuid(nodeDefUuid)(updateResult.record)

nodesToDelete.forEach((nodeToDelete) => {
// cleanup child applicability
const parentNode = Records.getParent(nodeToDelete)(updateResult.record)
if (parentNode && !Nodes.isChildApplicable(parentNode, nodeDefUuid)) {
const parentNodeUpdated = Nodes.dissocChildApplicability(parentNode, nodeDefUuid)
const recordWithParentNodeUpdated = Records.addNode(parentNodeUpdated, { sideEffect })(updateResult.record)
updateResult.merge(new RecordUpdateResult({ record: recordWithParentNodeUpdated }))
}
})

const nodeUuidsToDelete = nodesToDelete.map((node) => node.uuid)
const nodesDeleteUpdateResult = Records.deleteNodes(nodeUuidsToDelete, { sideEffect })(updateResult.record)
updateResult.merge(nodesDeleteUpdateResult)
const nodesDeletedUpdatedResult = deleteNodesByDefUuid({ record: result.record, nodeDefUuid, sideEffect })
result.merge(nodesDeletedUpdatedResult)
}
})
const missingNodesUpdateResult = insertMissingSingleNodes({ survey, record: updateResult.record, sideEffect })
updateResult.merge(missingNodesUpdateResult)
return updateResult
const missingNodesUpdateResult = insertMissingSingleNodes({ survey, record: result.record, sideEffect })
result.merge(missingNodesUpdateResult)
return result
}

export const RecordFixer = {
Expand Down
2 changes: 2 additions & 0 deletions src/record/records.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
getCycle,
getRoot,
getNodes,
getNodesArray,
Expand Down Expand Up @@ -33,6 +34,7 @@ import { addNode, addNodes, deleteNode, deleteNodes } from './_records/recordUpd

export const Records = {
// READ
getCycle,
getRoot,
getNodes,
getNodesArray,
Expand Down
4 changes: 3 additions & 1 deletion src/survey/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export { NodeDefsFixer } from './nodeDefsFixer'

export type { Survey, SurveyProps, SurveyCycle, SurveyDependencyGraph, SurveyDependency } from './survey'

export { SurveyDependencyType } from './survey'
export { defaultCycle, SurveyDependencyType } from './survey'

export { Surveys } from './surveys'

Expand Down
96 changes: 96 additions & 0 deletions src/survey/nodeDefsFixer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { NodeDefEntity, NodeDefs } from '../nodeDef'
import { NodeDefsFixer, Survey, Surveys } from '../survey'
import { SurveyBuilder, SurveyObjectBuilders } from '../tests/builder/surveyBuilder'
import { createTestAdminUser } from '../tests/data'
import { Objects, UUIDs } from '../utils'

const { booleanDef, entityDef, integerDef } = SurveyObjectBuilders

let survey: Survey

describe('Survey NodeDefsFixer', () => {
beforeEach(async () => {
const user = createTestAdminUser()

survey = new SurveyBuilder(
user,
entityDef(
'cluster',
integerDef('cluster_id').key(),
integerDef('cluster_attr'),
booleanDef('accessible'),
entityDef('plot', integerDef('plot_id').key(), integerDef('plot_attribute')).multiple()
)
).build()

const cycle = '0'
const rootDef = Surveys.getNodeDefRoot({ survey })
const clusterIdDef = Surveys.getNodeDefByName({ survey, name: 'cluster_id' })
const clusterAttrDef = Surveys.getNodeDefByName({ survey, name: 'cluster_attr' })
const plotDef: NodeDefEntity = Surveys.getNodeDefByName({ survey, name: 'plot' }) as NodeDefEntity
const plotPageUuid = UUIDs.v4()
Objects.setInPath({ obj: plotDef, path: ['props', 'layout', cycle, 'pageUuid'], value: plotPageUuid })

const rootLayoutChildren = [
{ i: clusterIdDef.uuid, x: 0, y: 0 },
{ i: clusterAttrDef.uuid, x: 0, y: 1 },
]
Objects.setInPath({ obj: rootDef, path: ['props', 'layout', cycle, 'layoutChildren'], value: rootLayoutChildren })
const rootLayoutIndexChildren = [plotDef.uuid]
Objects.setInPath({
obj: rootDef,
path: ['props', 'layout', cycle, 'indexChildren'],
value: rootLayoutIndexChildren,
})
})

test('Hierarchy fixed', () => {
const plotDef = Surveys.getNodeDefByName({ survey, name: 'plot' })
plotDef.meta.h.unshift('NOT_EXISTING_NODE_DEF_UUID')

const { nodeDefs, updatedNodeDefs } = NodeDefsFixer.fixNodeDefs({ nodeDefs: survey.nodeDefs!, cycles: ['0'] })

const updatedNodeDefsArray = Object.values(updatedNodeDefs)
// all node defs in "nodeDefs"
expect(Object.values(nodeDefs).length).toBe(7)
// expect 1 updated node def (plot)
expect(updatedNodeDefsArray.length).toBe(1)
expect(NodeDefs.getName(updatedNodeDefsArray[0])).toBe('plot')
// expect same node defs objects in nodeDefs and updatedNodeDefs
expect(updatedNodeDefs[plotDef.uuid]).toBe(nodeDefs[plotDef.uuid])
})

test('Layout index children fixed', () => {
const cycle = '0'
const rootDef = Surveys.getNodeDefRoot({ survey })
const layoutIndexChildren = NodeDefs.getChildrenEntitiesInOwnPageUudis(cycle)(rootDef)
layoutIndexChildren.push(UUIDs.v4()) // add a non existing uuid to indexChildren

const { nodeDefs, updatedNodeDefs } = NodeDefsFixer.fixNodeDefs({ nodeDefs: survey.nodeDefs!, cycles: [cycle] })
const updatedNodeDefsArray = Object.values(updatedNodeDefs)
expect(Object.values(nodeDefs).length).toBe(7)
expect(updatedNodeDefsArray.length).toBe(1)
const updatedNodeDef = updatedNodeDefsArray[0]
expect(NodeDefs.getName(updatedNodeDef)).toBe('cluster')

const plotDef = Surveys.getNodeDefByName({ survey, name: 'plot' })
expect(NodeDefs.getChildrenEntitiesInOwnPageUudis(cycle)(updatedNodeDef)).toEqual([plotDef.uuid])
})

test('Layout children fixed', () => {
const cycle = '0'
const rootDef = Surveys.getNodeDefRoot({ survey })
const plotDef = Surveys.getNodeDefByName({ survey, name: 'plot' })
const layoutChildren = NodeDefs.getLayoutChildren(cycle)(rootDef)
layoutChildren.push({ i: plotDef.uuid, x: 0, y: 2 }) // add plot def in layout children (it has its own page, it cannot be in layoutChildren)

const { nodeDefs, updatedNodeDefs } = NodeDefsFixer.fixNodeDefs({ nodeDefs: survey.nodeDefs!, cycles: [cycle] })
const updatedNodeDefsArray = Object.values(updatedNodeDefs)
expect(Object.values(nodeDefs).length).toBe(7)
expect(updatedNodeDefsArray.length).toBe(1)
const updatedNodeDef = updatedNodeDefsArray[0]
expect(NodeDefs.getName(updatedNodeDef)).toBe('cluster')

expect(NodeDefs.getLayoutChildren(cycle)(updatedNodeDef).length).toBe(2)
})
})
Loading

0 comments on commit c03bf9c

Please sign in to comment.