Skip to content

Commit

Permalink
187637447 v3 DI caseByID and caseByIndex Requests (#1276)
Browse files Browse the repository at this point in the history
* Handle delete, get, and update caseByID requests.

* Handle delete, get, and update caseByIndex requests.
  • Loading branch information
tealefristoe authored May 22, 2024
1 parent e46fc37 commit ffac044
Show file tree
Hide file tree
Showing 22 changed files with 379 additions and 97 deletions.
31 changes: 30 additions & 1 deletion v3/src/data-interactive/data-interactive-type-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { kAttrIdPrefix, maybeToV2Id, toV2Id } from "../utilities/codap-utils"
import {
ICodapV2AttributeV3, ICodapV2CollectionV3, ICodapV2DataContextV3, v3TypeFromV2TypeString
} from "../v2/codap-v2-types"
import { DIAttribute, DIResources, DISingleValues } from "./data-interactive-types"
import { DIAttribute, DIGetCaseResult, DIResources, DISingleValues } from "./data-interactive-types"
import { getCaseValues } from "./data-interactive-utils"

export function convertValuesToAttributeSnapshot(_values: DISingleValues): IAttributeSnapshot | undefined {
Expand Down Expand Up @@ -73,6 +73,35 @@ export function convertCaseToV2FullCase(c: ICase, dataContext: IDataSet) {
}
}

export function getCaseRequestResultValues(c: ICase, dataContext: IDataSet): DIGetCaseResult {
const caseId = c.__id__

const id = toV2Id(caseId)

const collectionId = dataContext.pseudoCaseMap.get(caseId)?.collectionId ?? dataContext.ungrouped.id

const parent = maybeToV2Id(dataContext.getParentCase(caseId, collectionId)?.pseudoCase.__id__)

const _collection = dataContext.getCollection(collectionId)
const collection = _collection ? {
id: toV2Id(_collection.id),
name: _collection.name
} : undefined

const values = getCaseValues(caseId, collectionId, dataContext)

const pseudoCase = dataContext.pseudoCaseMap.get(caseId)
const children = pseudoCase?.childPseudoCaseIds?.map(cId => toV2Id(cId)) ??
pseudoCase?.childCaseIds?.map(cId => toV2Id(cId)) ?? []

const caseIndex = dataContext.getCasesForCollection(collectionId).findIndex(aCase => aCase.__id__ === caseId)

return {
case: { id, parent, collection, values, children },
caseIndex
}
}

export function convertAttributeToV2(attribute: IAttribute, dataContext?: IDataSet): ICodapV2AttributeV3 {
const metadata = dataContext && getSharedCaseMetadataFromDataset(dataContext)
const { name, type, title, description, editable, id, precision } = attribute
Expand Down
27 changes: 23 additions & 4 deletions v3/src/data-interactive/data-interactive-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { RequireAtLeastOne } from "type-fest"
import { IAttribute } from "../models/data/attribute"
import { ICodapV2Attribute, ICodapV2AttributeV3, ICodapV2Collection, ICodapV2DataContext } from "../v2/codap-v2-types"
import { IDataSet } from "../models/data/data-set"
import { ICase } from "../models/data/data-set-types"
import { IGlobalValue } from "../models/global/global-value"
import { ITileModel } from "../models/tiles/tile-model"
import { ICollectionPropsModel } from "../models/data/collection"
Expand Down Expand Up @@ -59,6 +60,19 @@ export interface DIGlobal {
value?: number
}
export type DIDataContext = Partial<ICodapV2DataContext>
export interface DIGetCaseResult {
case: {
id?: number
parent?: number
collection?: {
id?: number
name?: string
}
values?: DICaseValues
children?: number[]
}
caseIndex?: number
}
export interface DIInteractiveFrame {
dimensions?: {
height?: number
Expand All @@ -79,6 +93,9 @@ export interface DINewCase {
id?: number
itemID?: number
}
export interface DIUpdateCase {
values: DICaseValues
}
export interface DINotification {
request?: string
}
Expand All @@ -87,8 +104,8 @@ export interface DIResources {
attribute?: IAttribute
attributeList?: IAttribute[]
attributeLocation?: IAttribute
caseByID?: DICase
caseByIndex?: DICase
caseByID?: ICase
caseByIndex?: ICase
caseFormulaSearch?: DICase[]
caseSearch?: DICase[]
collection?: ICollectionPropsModel
Expand All @@ -107,12 +124,12 @@ export interface DIResources {

// types for values accepted as inputs by the API
export type DISingleValues = DIAttribute | DICase | DIDataContext |
DIGlobal | DIInteractiveFrame | DINewCase | DINotification | V2Component
DIGlobal | DIInteractiveFrame | DINewCase | DIUpdateCase | DINotification | V2Component
export type DIValues = DISingleValues | DISingleValues[] | number | string[]

// types returned as outputs by the API
export type DIResultAttributes = { attrs: ICodapV2AttributeV3[] }
export type DIResultSingleValues = DICase | DIComponentInfo | DIGlobal | DIInteractiveFrame
export type DIResultSingleValues = DICase | DIComponentInfo | DIGetCaseResult | DIGlobal | DIInteractiveFrame
export type DIResultValues = DIResultSingleValues | DIResultSingleValues[] |
DIAllCases | DIResultAttributes | number

Expand Down Expand Up @@ -159,6 +176,8 @@ export interface DIResourceSelector {
attributeLocation?: string
attributes?: string
case?: string
caseByID?: string
caseByIndex?: string
collection?: string
component?: string
dataContext?: string
Expand Down
6 changes: 4 additions & 2 deletions v3/src/data-interactive/handlers/all-cases-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { registerDIHandler } from "../data-interactive-handler"
import { getCaseValues } from "../data-interactive-utils"
import { DIHandler, DIResources } from "../data-interactive-types"
import { maybeToV2Id, toV2Id } from "../../utilities/codap-utils"
import { collectionNotFoundResult, dataContextNotFoundResult } from "./di-results"

export const diAllCasesHandler: DIHandler = {
delete(resources: DIResources) {
const { dataContext } = resources
if (!dataContext) return { success: false }
if (!dataContext) return dataContextNotFoundResult

dataContext.applyModelChange(() => {
dataContext.removeCases(dataContext.cases.map(c => c.__id__))
Expand All @@ -18,7 +19,8 @@ export const diAllCasesHandler: DIHandler = {
},
get(resources: DIResources) {
const { collection, dataContext } = resources
if (!collection || !dataContext) return { success: false }
if (!dataContext) return dataContextNotFoundResult
if (!collection) return collectionNotFoundResult

const cases = dataContext.getGroupsForCollection(collection.id)?.map((c, caseIndex) => {
const id = c.pseudoCase.__id__
Expand Down
63 changes: 32 additions & 31 deletions v3/src/data-interactive/handlers/attribute-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,9 @@ import { registerDIHandler } from "../data-interactive-handler"
import { convertAttributeToV2, convertAttributeToV2FromResources } from "../data-interactive-type-utils"
import { DIAttribute, DIHandler, DIResources, DIValues } from "../data-interactive-types"
import { createAttribute } from "./di-handler-utils"
import { attributeNotFoundResult, dataContextNotFoundResult } from "./di-results"

const attributeNotFoundResult = { success: false, values: { error: t("V3.DI.Error.attributeNotFound") } } as const
const dataContextNotFoundResult = { success: false, values: { error: t("V3.DI.Error.dataContextNotFound") } } as const
export const diAttributeHandler: DIHandler = {
get(resources: DIResources) {
const attribute = convertAttributeToV2FromResources(resources)
if (attribute) {
return {
success: true,
values: attribute
}
}
return attributeNotFoundResult
},
create(resources: DIResources, _values?: DIValues) {
const { dataContext } = resources
if (!dataContext) return dataContextNotFoundResult
Expand Down Expand Up @@ -56,6 +45,28 @@ export const diAttributeHandler: DIHandler = {
attrs: attributes.map(attribute => convertAttributeToV2(attribute, dataContext))
} }
},

delete(resources: DIResources) {
const { attribute, dataContext } = resources
if (!attribute) return attributeNotFoundResult
if (!dataContext) return dataContextNotFoundResult

dataContext.applyModelChange(() => {
dataContext.removeAttribute(attribute.id)
})
return { success: true }
},

get(resources: DIResources) {
const attribute = convertAttributeToV2FromResources(resources)
if (!attribute) return attributeNotFoundResult

return {
success: true,
values: attribute
}
},

update(resources: DIResources, _values?: DIValues) {
const { attribute, dataContext } = resources
if (!attribute || Array.isArray(_values)) return attributeNotFoundResult
Expand All @@ -81,28 +92,18 @@ export const diAttributeHandler: DIHandler = {
}, {
notifications: () => updateAttributesNotification([attribute], dataContext)
})

const attributeV2 = convertAttributeToV2FromResources(resources)
if (attributeV2) {
return {
success: true,
values: {
attrs: [
attributeV2
]
}
if (!attributeV2) return attributeNotFoundResult

return {
success: true,
values: {
attrs: [
attributeV2
]
}
}
return attributeNotFoundResult
},
delete(resources: DIResources) {
const { attribute, dataContext } = resources
if (!attribute) return attributeNotFoundResult
if (!dataContext) return dataContextNotFoundResult

dataContext.applyModelChange(() => {
dataContext.removeAttribute(attribute.id)
})
return { success: true }
}
}

Expand Down
4 changes: 2 additions & 2 deletions v3/src/data-interactive/handlers/attribute-list-handler.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { t } from "../../utilities/translation/translate"
import { registerDIHandler } from "../data-interactive-handler"
import { basicAttributeInfo } from "../data-interactive-type-utils"
import { DIHandler, DIResources } from "../data-interactive-types"
import { collectionNotFoundResult } from "./di-results"

export const diAttributeListHandler: DIHandler = {
get(resources: DIResources) {
const { attributeList } = resources
if (!attributeList) return { success: false, values: { error: t("V3.DI.Error.collectionNotFound") } }
if (!attributeList) return collectionNotFoundResult

return {
success: true,
Expand Down
50 changes: 50 additions & 0 deletions v3/src/data-interactive/handlers/case-by-handler-functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ICase } from "../../models/data/data-set-types"
import { t } from "../../utilities/translation/translate"
import { DIResources, DIUpdateCase, DIValues } from "../data-interactive-types"
import { getCaseRequestResultValues } from "../data-interactive-type-utils"
import { attrNamesToIds } from "../data-interactive-utils"
import { caseNotFoundResult, dataContextNotFoundResult } from "./di-results"

export function deleteCaseBy(resources: DIResources, aCase?: ICase) {
const { dataContext } = resources
if (!dataContext) return dataContextNotFoundResult
if (!aCase) return caseNotFoundResult

const pseudoCase = dataContext.pseudoCaseMap.get(aCase.__id__)
const cases = pseudoCase?.childCaseIds ?? [aCase.__id__]

dataContext.applyModelChange(() => {
dataContext.removeCases(cases)
})

return { success: true }
}

export function getCaseBy(resources: DIResources, aCase?: ICase) {
const { dataContext } = resources
if (!dataContext) return dataContextNotFoundResult
if (!aCase) return caseNotFoundResult

return { success: true, values: getCaseRequestResultValues(aCase, dataContext) } as const
}

export function updateCaseBy(resources: DIResources, values?: DIValues, aCase?: ICase) {
const { dataContext } = resources
if (!dataContext) return dataContextNotFoundResult
if (!aCase) return caseNotFoundResult

const missingFieldResult = {
success: false,
values: { error: t("V3.DI.Error.fieldRequired", { vars: ["update", "caseByID/Index", "values.values"] }) }
} as const
if (!values) return missingFieldResult
const updateCase = values as DIUpdateCase
if (!updateCase.values) return missingFieldResult

dataContext.applyModelChange(() => {
const updatedAttributes = attrNamesToIds(updateCase.values, dataContext)
dataContext.setCaseValues([{ ...updatedAttributes, __id__: aCase.__id__ }])
})

return { success: true }
}
66 changes: 66 additions & 0 deletions v3/src/data-interactive/handlers/case-by-id-handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { maybeToV2Id } from "../../utilities/codap-utils"
import { DIGetCaseResult } from "../data-interactive-types"
import { diCaseByIDHandler } from "./case-by-id-handler"
import { setupTestDataset } from "./handler-test-utils"

describe("DataInteractive CaseByIDHandler", () => {
const handler = diCaseByIDHandler
function setup() {
const { dataset, a3 } = setupTestDataset()
// eslint-disable-next-line no-unused-expressions
dataset.collectionGroups
const aCase = dataset.getCaseAtIndex(4)
const caseId = aCase!.__id__
const pseudoCase = Array.from(dataset.pseudoCaseMap.values())[1].pseudoCase
const pseudoCaseId = pseudoCase.__id__
return { dataContext: dataset, aCase, caseId, pseudoCase, pseudoCaseId, a3 }
}

it("get works as expected", () => {
const { dataContext, aCase, caseId, pseudoCase, pseudoCaseId } = setup()

expect(handler.get?.({})?.success).toBe(false)
expect(handler.get?.({ dataContext })?.success).toBe(false)
expect(handler.get?.({ caseByID: aCase })?.success).toBe(false)

const caseResult = handler.get?.({ dataContext, caseByID: aCase })?.values as DIGetCaseResult
expect(caseResult.case.id).toBe(maybeToV2Id(caseId))

const pseudoCaseResult = handler.get?.({ dataContext, caseByID: pseudoCase })?.values as DIGetCaseResult
expect(pseudoCaseResult.case.id).toBe(maybeToV2Id(pseudoCaseId))
})

it("update works as expected", () => {
const { dataContext, aCase, caseId, pseudoCase, pseudoCaseId, a3 } = setup()
const caseResources = { dataContext, caseByID: aCase }

expect(handler.update?.({}).success).toBe(false)
expect(handler.update?.({ dataContext }).success).toBe(false)
expect(handler.update?.(caseResources).success).toBe(false)
expect(handler.update?.(caseResources, {}).success).toBe(false)

expect(handler.update?.(caseResources, { values: { a3: 10 } }).success).toBe(true)
expect(a3.numValues[dataContext.caseIndexFromID(caseId)!]).toBe(10)

expect(handler.update?.({ dataContext, caseByID: pseudoCase }, { values: { a3: 100 } }).success).toBe(true)
dataContext.pseudoCaseMap.get(pseudoCaseId)?.childCaseIds.forEach(id => {
expect(a3.numValues[dataContext.caseIndexFromID(id)!]).toBe(100)
})
})

it("delete works as expected", () => {
const { dataContext, aCase, caseId, pseudoCase, pseudoCaseId } = setup()

expect(handler.delete?.({}).success).toBe(false)
expect(handler.delete?.({ dataContext }).success).toBe(false)

expect(dataContext.getCase(caseId)).toBeDefined()
expect(handler.delete?.({ dataContext, caseByID: aCase }).success).toBe(true)
expect(dataContext.getCase(caseId)).toBeUndefined()

const childCaseIds = dataContext.pseudoCaseMap.get(pseudoCaseId)!.childCaseIds
childCaseIds.forEach(id => expect(dataContext.getCase(id)).toBeDefined())
expect(handler.delete?.({ dataContext, caseByID: pseudoCase }).success).toBe(true)
childCaseIds.forEach(id => expect(dataContext.getCase(id)).toBeUndefined())
})
})
15 changes: 11 additions & 4 deletions v3/src/data-interactive/handlers/case-by-id-handler.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { registerDIHandler } from "../data-interactive-handler"
import { DIHandler, diNotImplementedYet } from "../data-interactive-types"
import { DIHandler, DIResources, DIValues } from "../data-interactive-types"
import { deleteCaseBy, getCaseBy, updateCaseBy } from "./case-by-handler-functions"

export const diCaseByIDHandler: DIHandler = {
delete: diNotImplementedYet,
get: diNotImplementedYet,
update: diNotImplementedYet
delete(resources: DIResources) {
return deleteCaseBy(resources, resources.caseByID)
},
get(resources: DIResources) {
return getCaseBy(resources, resources.caseByID)
},
update(resources: DIResources, values?: DIValues) {
return updateCaseBy(resources, values, resources.caseByID)
}
}

registerDIHandler("caseByID", diCaseByIDHandler)
Loading

0 comments on commit ffac044

Please sign in to comment.