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

feat: sort when syncing by parent #973

Merged
merged 6 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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