Conversation
raineorshine
left a comment
There was a problem hiding this comment.
Functionality looks great! I made some recommendations regarding naming and structure.
In addition, please rename all instances of ROAM to Roam (or roam, as the case may be). Do the same thing for JSON and Json.
src/@types/ROAM/index.d.ts
Outdated
| declare module 'roam' { | ||
| import { GenericObject } from '../../utilTypes' | ||
|
|
||
| interface ROAMChild { |
There was a problem hiding this comment.
Let's call this RoamBlock since that's what they are called within Roam.
src/@types/ROAM/index.d.ts
Outdated
| @@ -0,0 +1,19 @@ | |||
| declare module 'roam' { | |||
| import { GenericObject } from '../../utilTypes' | |||
There was a problem hiding this comment.
utilTypes was removed and GenericObject was renamed to Index. Not sure how this is compiling.
There was a problem hiding this comment.
You're right. I found out that it's being ignored by eslint based on its current configuration. Do you think we should update that?
There was a problem hiding this comment.
@raineorshine Do you think we can do this in a different PR or maybe towards the end of this PR because there are other .d.ts files as well that might need to be handled?
There was a problem hiding this comment.
Yes, let's do a separate PR. I'd like to know what your overall plan is before starting on it.
| /** | ||
| * Remove root, de-indent (trim), and append newline to make tests cleaner. | ||
| */ |
There was a problem hiding this comment.
Unless you are using this function across multiple files, it should be defined in the module it is being used. There's no point adding to the surface area of the internal API if it is only used in one module.
There was a problem hiding this comment.
Yes, this is being used by some tests. Moreover, we can use it in other tests where we're using exportContext, to get rid of the ROOT_TOKEN from our tests' assertions, which is quite redundant.
There was a problem hiding this comment.
That sounds good. Please replace the duplicate code in other files with a reference to exportWithoutRoot.
There was a problem hiding this comment.
Sure. Just to be clear, are you referring to this file, or all the possible use-cases of this function that are currently using exportContext?
There was a problem hiding this comment.
Just duplicate functions that can be replaced with an import, like in importHtml, that look like this:
const exportedWithoutRoot = exported.slice(exported.indexOf('\n'))
.split('\n')
.map(line => line.slice(2))
.join('\n')
+ '\n'| /** | ||
| * Remove root, de-indent (trim), and append newline to make tests cleaner. | ||
| */ | ||
| export const exportedWithoutRoot = exported => exported.slice(exported.indexOf('\n')) |
There was a problem hiding this comment.
removeRoot would be a better name for this function.
src/util/importROAM.ts
Outdated
| /** | ||
| * Parses ROAM JSON and generates { contextIndexUpdates, thoughtIndexUpdates } that can be sync'd to state. | ||
| */ | ||
| export const importROAM = (state: State, thoughtsRanked: SimplePath, ROAM: RoamNode[]) => { |
There was a problem hiding this comment.
We're calling thoughtsRanked by the name simplePath now for clarity.
src/util/importROAM.ts
Outdated
| /** | ||
| * Converts the ROAM JSON to an array of blocks. | ||
| */ | ||
| const convertROAMJSONToBlocks = (ROAM: RoamNode[]) => { |
There was a problem hiding this comment.
Let's call this roamJsonToBlocks. The word convert is extraneous. All pure functions "convert` one thing to another.
There was a problem hiding this comment.
Good point, thanks 👍
There was a problem hiding this comment.
Sorry about this miss, @raineorshine . Would refactor it
|
Please name PR titles succinctly. Full descriptions go in the description field. |
9963e3b to
df061ef
Compare
src/util/__tests__/importRoam.ts
Outdated
| } = importROAM(testState, RANKED_ROOT as SimplePath, testData) | ||
|
|
||
| Object.keys(thoughtIndex) | ||
| // ignore root blocks of ROAM page since they won't have create/edit time |
There was a problem hiding this comment.
Let's test that the root blocks have created and lastUpdated set to a new Timestamp.
src/util/__tests__/importRoam.ts
Outdated
|
|
||
| Object.keys(thoughtIndex) | ||
| // ignore root blocks of ROAM page since they won't have create/edit time | ||
| .filter((_, index) => index !== 0 && index !== 4) |
There was a problem hiding this comment.
Matching by index here is quite fragile. If the test changes at all it will break, or worse create a false positive. In general we want to avoid creating dependencies between things that are not semantically related, or only incidentally related.
Instead, I would suggest filtering out based on the key.
| created: thoughtOld?.created || created, | ||
| lastUpdated |
src/util/importJSON.ts
Outdated
| /** Insert the given value at the context. Modifies contextIndex and thoughtIndex. */ | ||
| type insertThought = (value: string, context: Context, rank: number, created?:Timestamp, lastUpdated?: Timestamp) => void |
There was a problem hiding this comment.
Thanks for eliminating the duplicate code!
Another way to do this is to type insertThought inline and then use typeof insertThought to type the saveThoughts parameter.
src/util/importROAM.ts
Outdated
| /** | ||
| * Converts the ROAM JSON to an array of blocks. | ||
| */ | ||
| const roamJsontoBlocks = (ROAM: RoamPage[]) => { |
There was a problem hiding this comment.
Please change all instances of ROAM to roam as appropriate. This was requested in my previous review. You should also rename ROAM to Roam in the comments.
src/util/importROAM.ts
Outdated
| /** | ||
| * Recursively converts the roam children to blocks. | ||
| */ | ||
| const convertRoamBlocksToBlocks = (children: RoamBlock[]): Block[] => children.map(({ string, children, 'edit-time': editTime, 'create-time': createTime }) => ({ |
There was a problem hiding this comment.
Please rename as requested in previous review.
src/util/importROAM.ts
Outdated
| return ROAM.map((item: RoamPage) => { | ||
| return { | ||
| scope: item.title, | ||
| children: convertRoamBlocksToBlocks(item.children) | ||
| } | ||
| }) |
There was a problem hiding this comment.
I recommend the more succinct inline functions when the function body consists only of a return statement:
return ROAM.map((item: RoamPage) => ({
scope: item.title,
children: convertRoamBlocksToBlocks(item.children)
}))There was a problem hiding this comment.
Yes, I observed this as well. Updated now 👍
src/util/__tests__/importRoam.ts
Outdated
| } | ||
| ] | ||
|
|
||
| const roamBlocks = [...testData[0].children, ...testData[1].children] |
There was a problem hiding this comment.
Let's generalize this to work for any amount of testData:
const roamBlocks = testData.map(roamBlock => roamBlock.children).flat()dd5e556 to
75882eb
Compare
raineorshine
left a comment
There was a problem hiding this comment.
Looks good! Just a couple small points.
| Object.keys(contextIndex) | ||
| .forEach(contextHash => { | ||
| expect(contextIndex[contextHash].lastUpdated).toEqual('2020-11-02T01:11:58.869Z') | ||
| }) |
There was a problem hiding this comment.
If you make assertions in a loop, you also need to assert the expected length of the array. Otherwise if no results came back the test would incorrectly pass.
There was a problem hiding this comment.
@anmolarora1 I found an even better way to do this. Using toMatchObject we can assert arbitrary keys in a single assertion. This obviates the need for asserting the number of keys and has the advantage of displaying all values that fail instead of just the first one that fails in the forEach loop. See: https://github.com/cybersemics/em/blob/staged-loading/src/util/__tests__/roamJsonToBlocks.ts#L226-L237
The object can be built up abstractly (i.e. reduce), but here I prefer the hard-coded values as they are easier to read and make it easier to assert exceptions, such as Fruits and Veggies using the edit time of their child.
src/util/__tests__/importRoam.ts
Outdated
| expect(contextIndex[contextHash].lastUpdated).toEqual('2020-11-02T01:11:58.869Z') | ||
| }) | ||
|
|
||
| Object.keys(thoughtIndex) |
src/util/__tests__/importRoam.ts
Outdated
| thoughtIndexUpdates: thoughtIndex, | ||
| } = importRoam(testState, RANKED_ROOT as SimplePath, testData) | ||
|
|
||
| Object.keys(thoughtIndex) |
There was a problem hiding this comment.
Also assert number of keys here
…erties(current timestamp)
75882eb to
0679694
Compare
| /** | ||
| * Validates the strucutre of RoamBlocks. | ||
| */ | ||
| const isRoamBlock = (children: RoamBlock[] = []): boolean => children.every(({ | ||
| uid, | ||
| string, | ||
| children = [], | ||
| 'create-time': createTime, | ||
| 'create-email': createEmail | ||
| }: RoamBlock) => | ||
| Array.isArray(children) | ||
| && isRoamBlock(children) | ||
| && typeof uid === 'string' | ||
| && typeof string === 'string' | ||
| && typeof createTime === 'number' | ||
| && typeof createEmail === 'string' | ||
| ) |
There was a problem hiding this comment.
I think it's generally cleaner to extract the mapping logic outside of functions. So isRoamBlock would just take a single RoamBlock. The aggregate can be calculated with children.every(isRoamBlock) in validateRoam. This decouples the validation from the aggregate structure.
| test('it returns false for an invalid create-time value', () => { | ||
| const invalidRoamString = JSON.stringify([{ ...testData[0], children: testData[0].children.map(child => ({ ...child, 'create-time': '2020-11-06T08:52:50.742Z' })) }]) | ||
| expect(validateRoam(invalidRoamString)).toBe(false) | ||
| }) | ||
|
|
||
| test('it returns false for an invalid uid value', () => { | ||
| const invalidRoamString = JSON.stringify([{ ...testData[0], children: testData[0].children.map(child => ({ ...child, uid: 1011 })) }]) | ||
| expect(validateRoam(invalidRoamString)).toBe(false) | ||
| }) | ||
|
|
||
| test('it returns false for an invalid string value', () => { | ||
| const invalidRoamString = JSON.stringify([{ ...testData[0], children: testData[0].children.map(child => ({ ...child, string: 1011 })) }]) | ||
| expect(validateRoam(invalidRoamString)).toBe(false) | ||
| }) | ||
|
|
||
| test('it returns false for an invalid children value', () => { | ||
| const invalidRoamString = JSON.stringify([{ ...testData[0], children: 1234 }]) | ||
| expect(validateRoam(invalidRoamString)).toBe(false) | ||
| }) |
There was a problem hiding this comment.
Very thorough! Thanks!
| // /** | ||
| // * Parses Roam and generates { contextIndexUpdates, thoughtIndexUpdates } that can be sync'd to state. | ||
| // */ | ||
| // export const importRoam = (state: State, simplePath: SimplePath, roam: RoamPage[]) => { | ||
| // const thoughtsJSON = roamJsonToBlocks(roam) | ||
| // return importJSON(state, simplePath, thoughtsJSON, { skipRoot: false }) | ||
| // } |
There was a problem hiding this comment.
Old code should be removed
fixes #865
Adds some basic functions to parse ROAM JSON into { contextIndexUpdates, thoughtIndexUpdates } that can be sync'd with the state