-
Notifications
You must be signed in to change notification settings - Fork 121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Basic importRoam #888
Basic importRoam #888
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
utilTypes
was removed and GenericObject
was renamed to Index
. Not sure how this is compiling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds good. Please replace the duplicate code in other files with a reference to exportWithoutRoot
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removeRoot
would be a better name for this function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, thanks 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's generalize this to work for any amount of testData:
const roamBlocks = testData.map(roamBlock => roamBlock.children).flat()
dd5e556
to
75882eb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Just a couple small points.
src/util/__tests__/importRoam.ts
Outdated
Object.keys(contextIndex) | ||
.forEach(contextHash => { | ||
expect(contextIndex[contextHash].lastUpdated).toEqual('2020-11-02T01:11:58.869Z') | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also assert number of keys here
…erties(current timestamp)
75882eb
to
0679694
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like how this finished up. Thanks for the thorough testing!
/** | ||
* 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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