Skip to content

Commit

Permalink
feat: sort when syncing by parent (#973)
Browse files Browse the repository at this point in the history
| 🚥 Resolves RM-8336 |
| :------------------ |

## 🧰 Changes

When syncing docs, it sorts them by their parents!

Users reported issues where if a parent document was in a directory, it
could error out because it would get synced after one of its children.

## 🧬 QA & Testing

Not sure 😬.

---------

Co-authored-by: Kanad Gupta <8854718+kanadgupta@users.noreply.github.com>
  • Loading branch information
kellyjosephprice and kanadgupta authored Feb 15, 2024
1 parent 9faab30 commit 9795840
Show file tree
Hide file tree
Showing 18 changed files with 307 additions and 13 deletions.
6 changes: 6 additions & 0 deletions __tests__/__fixtures__/docs/docs-with-parent-ids/child.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Child
parentDocSlug: parent
---

# Child Body
5 changes: 5 additions & 0 deletions __tests__/__fixtures__/docs/docs-with-parent-ids/friend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Friend
---

# Friend Body
5 changes: 5 additions & 0 deletions __tests__/__fixtures__/docs/docs-with-parent-ids/parent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Parent
---

# Parent Body
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: With Parent Doc
parentDoc: 1234
---

# With Parent Doc Body
6 changes: 6 additions & 0 deletions __tests__/__fixtures__/docs/multiple-docs-cycle/child.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Child
parentDocSlug: parent
---

# Child Body
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Grandparent
parentDocSlug: child
---

# Grandparent Body
6 changes: 6 additions & 0 deletions __tests__/__fixtures__/docs/multiple-docs-cycle/parent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Parent
parentDocSlug: grandparent
---

# Parent Body
6 changes: 6 additions & 0 deletions __tests__/__fixtures__/docs/multiple-docs-no-parents/child.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Child
parentDocSlug: parent
---

# Child Body
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Friend
---

# Friend Body
6 changes: 6 additions & 0 deletions __tests__/__fixtures__/docs/multiple-docs/child.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Child
parentDocSlug: parent
---

# Child Body
5 changes: 5 additions & 0 deletions __tests__/__fixtures__/docs/multiple-docs/friend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Friend
---

# Friend Body
5 changes: 5 additions & 0 deletions __tests__/__fixtures__/docs/multiple-docs/grandparent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Grandparent
---

# Grandparent Body
6 changes: 6 additions & 0 deletions __tests__/__fixtures__/docs/multiple-docs/parent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Parent
parentDocSlug: grandparent
---

# Parent Body
170 changes: 170 additions & 0 deletions __tests__/cmds/docs/multiple.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import fs from 'node:fs';
import path from 'node:path';

import frontMatter from 'gray-matter';
import nock from 'nock';
import { describe, beforeAll, afterAll, afterEach, it, expect, vi } from 'vitest';

import DocsCommand from '../../../src/cmds/docs/index.js';
import getAPIMock, { getAPIMockWithVersionHeader } from '../../helpers/get-api-mock.js';
import hashFileContents from '../../helpers/hash-file-contents.js';

const docs = new DocsCommand();

const fixturesBaseDir = '__fixtures__/docs';
const fullFixturesDir = `${__dirname}./../../${fixturesBaseDir}`;

const key = 'API_KEY';
const version = '1.0.0';

describe('rdme docs (multiple)', () => {
beforeAll(() => {
nock.disableNetConnect();
});

afterEach(() => {
vi.restoreAllMocks();
});

afterAll(() => nock.cleanAll());

it('should upload parent docs first', async () => {
const dir = 'multiple-docs';
const slugs = ['grandparent', 'parent', 'child', 'friend'];
let id = 1234;

const mocks = slugs.flatMap(slug => {
const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/${dir}/${slug}.md`)));
const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/${dir}/${slug}.md`)));

return [
getAPIMockWithVersionHeader(version)
.get(`/api/v1/docs/${slug}`)
.basicAuth({ user: key })
.reply(404, {
error: 'DOC_NOTFOUND',
message: `The doc with the slug '${slug}' couldn't be found`,
suggestion: '...a suggestion to resolve the issue...',
help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".',
}),
getAPIMockWithVersionHeader(version)
.post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash })
.basicAuth({ user: key })
// eslint-disable-next-line no-plusplus
.reply(201, { slug, _id: id++, body: doc.content, ...doc.data, lastUpdatedHash: hash }),
];
});

const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version });

const promise = docs.run({ filePath: `./__tests__/${fixturesBaseDir}/${dir}`, key, version });

await expect(promise).resolves.toStrictEqual(
[
`🌱 successfully created 'friend' (ID: 1237) with contents from __tests__/${fixturesBaseDir}/${dir}/friend.md`,
`🌱 successfully created 'grandparent' (ID: 1234) with contents from __tests__/${fixturesBaseDir}/${dir}/grandparent.md`,
`🌱 successfully created 'parent' (ID: 1235) with contents from __tests__/${fixturesBaseDir}/${dir}/parent.md`,
`🌱 successfully created 'child' (ID: 1236) with contents from __tests__/${fixturesBaseDir}/${dir}/child.md`,
].join('\n'),
);

mocks.forEach(mock => mock.done());
versionMock.done();
});

it('should upload docs with parent doc ids first', async () => {
const dir = 'docs-with-parent-ids';
const slugs = ['child', 'friend', 'with-parent-doc', 'parent'];
let id = 1234;

const mocks = slugs.flatMap(slug => {
const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/${dir}/${slug}.md`)));
const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/${dir}/${slug}.md`)));

return [
getAPIMockWithVersionHeader(version)
.get(`/api/v1/docs/${slug}`)
.basicAuth({ user: key })
.reply(404, {
error: 'DOC_NOTFOUND',
message: `The doc with the slug '${slug}' couldn't be found`,
suggestion: '...a suggestion to resolve the issue...',
help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".',
}),
getAPIMockWithVersionHeader(version)
.post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash })
.basicAuth({ user: key })
// eslint-disable-next-line no-plusplus
.reply(201, { slug, _id: id++, body: doc.content, ...doc.data, lastUpdatedHash: hash }),
];
});

const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version });

const promise = docs.run({ filePath: `./__tests__/${fixturesBaseDir}/${dir}`, key, version });

await expect(promise).resolves.toStrictEqual(
[
`🌱 successfully created 'with-parent-doc' (ID: 1236) with contents from __tests__/${fixturesBaseDir}/${dir}/with-parent-doc.md`,
`🌱 successfully created 'friend' (ID: 1235) with contents from __tests__/${fixturesBaseDir}/${dir}/friend.md`,
`🌱 successfully created 'parent' (ID: 1237) with contents from __tests__/${fixturesBaseDir}/${dir}/parent.md`,
`🌱 successfully created 'child' (ID: 1234) with contents from __tests__/${fixturesBaseDir}/${dir}/child.md`,
].join('\n'),
);

mocks.forEach(mock => mock.done());
versionMock.done();
});

it('should upload child docs without the parent', async () => {
const dir = 'multiple-docs-no-parents';
const slugs = ['child', 'friend'];
let id = 1234;

const mocks = slugs.flatMap(slug => {
const doc = frontMatter(fs.readFileSync(path.join(fullFixturesDir, `/${dir}/${slug}.md`)));
const hash = hashFileContents(fs.readFileSync(path.join(fullFixturesDir, `/${dir}/${slug}.md`)));

return [
getAPIMockWithVersionHeader(version)
.get(`/api/v1/docs/${slug}`)
.basicAuth({ user: key })
.reply(404, {
error: 'DOC_NOTFOUND',
message: `The doc with the slug '${slug}' couldn't be found`,
suggestion: '...a suggestion to resolve the issue...',
help: 'If you need help, email support@readme.io and mention log "fake-metrics-uuid".',
}),
getAPIMockWithVersionHeader(version)
.post('/api/v1/docs', { slug, body: doc.content, ...doc.data, lastUpdatedHash: hash })
.basicAuth({ user: key })
// eslint-disable-next-line no-plusplus
.reply(201, { slug, _id: id++, body: doc.content, ...doc.data, lastUpdatedHash: hash }),
];
});

const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version });

const promise = docs.run({ filePath: `./__tests__/${fixturesBaseDir}/${dir}`, key, version });

await expect(promise).resolves.toStrictEqual(
[
`🌱 successfully created 'child' (ID: 1234) with contents from __tests__/${fixturesBaseDir}/${dir}/child.md`,
`🌱 successfully created 'friend' (ID: 1235) with contents from __tests__/${fixturesBaseDir}/${dir}/friend.md`,
].join('\n'),
);

mocks.forEach(mock => mock.done());
versionMock.done();
});

it('should return an error message when it encounters a cycle', async () => {
const dir = 'multiple-docs-cycle';
const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version });

const promise = docs.run({ filePath: `./__tests__/${fixturesBaseDir}/${dir}`, key, version });

await expect(promise).rejects.toThrow('Cyclic dependency');
versionMock.done();
});
});
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"string-argv": "^0.3.1",
"table": "^6.8.1",
"tmp-promise": "^3.0.2",
"toposort": "^2.0.2",
"update-notifier": "^7.0.0",
"validator": "^13.7.0"
},
Expand All @@ -83,6 +84,7 @@
"@types/pluralize": "^0.0.33",
"@types/prompts": "^2.4.2",
"@types/semver": "^7.3.12",
"@types/toposort": "^2.0.7",
"@types/update-notifier": "^6.0.5",
"@types/validator": "^13.7.6",
"@vitest/coverage-v8": "^1.1.0",
Expand Down
18 changes: 10 additions & 8 deletions src/lib/readDoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import grayMatter from 'gray-matter';

import { debug } from './logger.js';

interface ReadDocMetadata {
export interface ReadDocMetadata {
/** The contents of the file below the YAML front matter */
content: string;
/** A JSON object with the YAML front matter */
data: Record<string, unknown>;
/** The original filePath */
filePath: string;
/**
* A hash of the file contents (including the front matter)
*/
Expand All @@ -22,19 +24,19 @@ interface ReadDocMetadata {
/**
* Returns the content, matter and slug of the specified Markdown or HTML file
*
* @param {String} filepath path to the HTML/Markdown file
* @param {String} filePath path to the HTML/Markdown file
* (file extension must end in `.html`, `.md`., or `.markdown`)
*/
export default function readDoc(filepath: string): ReadDocMetadata {
debug(`reading file ${filepath}`);
const rawFileContents = fs.readFileSync(filepath, 'utf8');
export default function readDoc(filePath: string): ReadDocMetadata {
debug(`reading file ${filePath}`);
const rawFileContents = fs.readFileSync(filePath, 'utf8');
const matter = grayMatter(rawFileContents);
const { content, data } = matter;
debug(`front matter for ${filepath}: ${JSON.stringify(matter)}`);
debug(`front matter for ${filePath}: ${JSON.stringify(matter)}`);

// Stripping the subdirectories and markdown extension from the filename and lowercasing to get the default slug.
const slug = matter.data.slug || path.basename(filepath).replace(path.extname(filepath), '').toLowerCase();
const slug = matter.data.slug || path.basename(filePath).replace(path.extname(filePath), '').toLowerCase();

const hash = crypto.createHash('sha1').update(rawFileContents).digest('hex');
return { content, data, hash, slug };
return { content, data, filePath, hash, slug };
}
Loading

0 comments on commit 9795840

Please sign in to comment.