Skip to content
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

Merged
merged 12 commits into from
Nov 6, 2020
Merged

Conversation

anmolarora1
Copy link
Contributor

fixes #865

Adds some basic functions to parse ROAM JSON into { contextIndexUpdates, thoughtIndexUpdates } that can be sync'd with the state

Copy link
Contributor

@raineorshine raineorshine left a 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.

declare module 'roam' {
import { GenericObject } from '../../utilTypes'

interface ROAMChild {
Copy link
Contributor

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.

@@ -0,0 +1,19 @@
declare module 'roam' {
import { GenericObject } from '../../utilTypes'
Copy link
Contributor

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.

Copy link
Contributor Author

@anmolarora1 anmolarora1 Oct 28, 2020

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks!

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Comment on lines 1 to 3
/**
* Remove root, de-indent (trim), and append newline to make tests cleaner.
*/
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

@raineorshine raineorshine Oct 31, 2020

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'))
Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/**
* Parses ROAM JSON and generates { contextIndexUpdates, thoughtIndexUpdates } that can be sync'd to state.
*/
export const importROAM = (state: State, thoughtsRanked: SimplePath, ROAM: RoamNode[]) => {
Copy link
Contributor

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.

/**
* Converts the ROAM JSON to an array of blocks.
*/
const convertROAMJSONToBlocks = (ROAM: RoamNode[]) => {
Copy link
Contributor

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.

Copy link
Contributor Author

@anmolarora1 anmolarora1 Oct 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, thanks 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

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

src/util/__tests__/importRoam.ts Outdated Show resolved Hide resolved
src/util/__tests__/importRoam.ts Outdated Show resolved Hide resolved
src/util/__tests__/importRoam.ts Outdated Show resolved Hide resolved
src/@types/ROAM/index.d.ts Outdated Show resolved Hide resolved
@raineorshine raineorshine changed the title adds basic functions to import from roam json format and respective t… Basic importRoam Oct 21, 2020
@raineorshine
Copy link
Contributor

Please name PR titles succinctly. Full descriptions go in the description field.

} = importROAM(testState, RANKED_ROOT as SimplePath, testData)

Object.keys(thoughtIndex)
// ignore root blocks of ROAM page since they won't have create/edit time
Copy link
Contributor

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.


Object.keys(thoughtIndex)
// ignore root blocks of ROAM page since they won't have create/edit time
.filter((_, index) => index !== 0 && index !== 4)
Copy link
Contributor

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.

src/util/__tests__/importRoam.ts Outdated Show resolved Hide resolved
src/util/__tests__/importRoam.ts Outdated Show resolved Hide resolved
Comment on lines +21 to +22
created: thoughtOld?.created || created,
lastUpdated
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent

Comment on lines 29 to 30
/** Insert the given value at the context. Modifies contextIndex and thoughtIndex. */
type insertThought = (value: string, context: Context, rank: number, created?:Timestamp, lastUpdated?: Timestamp) => void
Copy link
Contributor

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.

/**
* Converts the ROAM JSON to an array of blocks.
*/
const roamJsontoBlocks = (ROAM: RoamPage[]) => {
Copy link
Contributor

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.

/**
* Recursively converts the roam children to blocks.
*/
const convertRoamBlocksToBlocks = (children: RoamBlock[]): Block[] => children.map(({ string, children, 'edit-time': editTime, 'create-time': createTime }) => ({
Copy link
Contributor

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.

Comment on lines 23 to 28
return ROAM.map((item: RoamPage) => {
return {
scope: item.title,
children: convertRoamBlocksToBlocks(item.children)
}
})
Copy link
Contributor

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)
  }))

Copy link
Contributor Author

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 👍

}
]

const roamBlocks = [...testData[0].children, ...testData[1].children]
Copy link
Contributor

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()

@anmolarora1 anmolarora1 force-pushed the 865_import_from_roam branch 2 times, most recently from dd5e556 to 75882eb Compare November 2, 2020 11:17
Copy link
Contributor

@raineorshine raineorshine left a 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.

Comment on lines 237 to 240
Object.keys(contextIndex)
.forEach(contextHash => {
expect(contextIndex[contextHash].lastUpdated).toEqual('2020-11-02T01:11:58.869Z')
})
Copy link
Contributor

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.

Copy link
Contributor

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.

expect(contextIndex[contextHash].lastUpdated).toEqual('2020-11-02T01:11:58.869Z')
})

Object.keys(thoughtIndex)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

thoughtIndexUpdates: thoughtIndex,
} = importRoam(testState, RANKED_ROOT as SimplePath, testData)

Object.keys(thoughtIndex)
Copy link
Contributor

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

src/util/importROAM.ts Outdated Show resolved Hide resolved
Copy link
Contributor

@raineorshine raineorshine left a 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!

Comment on lines +3 to +19
/**
* 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'
)
Copy link
Contributor

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.

Comment on lines +81 to +99
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)
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very thorough! Thanks!

Comment on lines +180 to +186
// /**
// * 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 })
// }
Copy link
Contributor

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

@raineorshine raineorshine marked this pull request as ready for review November 6, 2020 14:53
@raineorshine raineorshine merged commit efa308b into cybersemics:dev Nov 6, 2020
anmolarora1 pushed a commit to anmolarora1/em that referenced this pull request Mar 10, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Import from Roam Research
2 participants