feat: Create library
This commit is contained in:
commit
4fe23e688f
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
|
@ -0,0 +1,91 @@
|
||||||
|
# Directus To Markdown
|
||||||
|
|
||||||
|
This library export [Directus](https://directus.io) items collections to markdown files with assets.
|
||||||
|
|
||||||
|
I used it to export article from Directus to Hugo website.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Directus
|
||||||
|
|
||||||
|
This library export data from an Directus so you should specify an url and token.
|
||||||
|
|
||||||
|
With environment variables:
|
||||||
|
|
||||||
|
```
|
||||||
|
export DIRECTUS_URL=https://your.directus.url
|
||||||
|
export DIRECTUS_TOKEN=your-token
|
||||||
|
```
|
||||||
|
|
||||||
|
or on configuration parameters :
|
||||||
|
|
||||||
|
```js
|
||||||
|
const config = {
|
||||||
|
url: 'https://your.directus.url',
|
||||||
|
token: 'your-token',
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Collection Name
|
||||||
|
|
||||||
|
The key of collections object should be the name of Directus Collection :
|
||||||
|
|
||||||
|
```js
|
||||||
|
const config = {
|
||||||
|
collections: {
|
||||||
|
news: { ... },
|
||||||
|
pages: { ... }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Content key
|
||||||
|
|
||||||
|
_default: content_
|
||||||
|
|
||||||
|
You can modify the field of the content :
|
||||||
|
|
||||||
|
```js
|
||||||
|
const config = {
|
||||||
|
contentKey: 'body',
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### readManyOption
|
||||||
|
|
||||||
|
`readManyOption` match https://docs.directus.io/reference/sdk/#read-multiple-items
|
||||||
|
|
||||||
|
### Export to specific path
|
||||||
|
|
||||||
|
For each collection you should an `pathBuilder`.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
import DirectusToMarkdown from '@resilien/directus-to-markdown'
|
||||||
|
import urlslug from 'url-slug'
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
url: 'https://your.directus.url',
|
||||||
|
token: 'your-token',
|
||||||
|
contentKey: 'body',
|
||||||
|
collections: {
|
||||||
|
news: {
|
||||||
|
readManyOption: {
|
||||||
|
fields: ['title', 'slug', 'date', 'image', 'image_credit', 'draft', 'body'],
|
||||||
|
filter: { draft: { _eq: 'false' } }
|
||||||
|
},
|
||||||
|
pathBuilder: (article) => {
|
||||||
|
if (article.slug) {
|
||||||
|
return `./content/news/${article.slug}`
|
||||||
|
}
|
||||||
|
return `./content/news/${article.date}-${urlslug(article.title, { remove: /\./g })}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new DirectusToMarkdown(config).export();
|
||||||
|
```
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { Directus } from '@directus/sdk'
|
||||||
|
import yaml from 'js-yaml'
|
||||||
|
import fs from 'fs'
|
||||||
|
import Axios from 'axios'
|
||||||
|
|
||||||
|
export default class DirectusToMarkdown {
|
||||||
|
constructor(config) {
|
||||||
|
this.url = config.url || process.env.DIRECTUS_URL
|
||||||
|
this.token = config.token || process.env.DIRECTUS_TOKEN
|
||||||
|
this.contentKey = config.contentKey || 'content'
|
||||||
|
this.collections = config.collections
|
||||||
|
this.directus = new Directus(this.url, { auth: { staticToken: this.token }});
|
||||||
|
}
|
||||||
|
|
||||||
|
_formatFrontMatter(item) {
|
||||||
|
const front = { ...item } // copie item
|
||||||
|
delete front[this.contentKey]
|
||||||
|
return `---\r\n${yaml.dump(front).trim()}\r\n---\r\n\r\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
async _writeIndex(item, itemPath) {
|
||||||
|
const frontMatter = this._formatFrontMatter(item)
|
||||||
|
const content = item[this.contentKey] ? item[this.contentKey].toString() : ''
|
||||||
|
const itemContent = `${frontMatter}${content}`
|
||||||
|
const indexName = 'index' // TODO: index or _index ?
|
||||||
|
fs.writeFileSync(`${itemPath}/${indexName}.md`, itemContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
async _writeFile(response, path) {
|
||||||
|
const writer = fs.createWriteStream(path)
|
||||||
|
|
||||||
|
response.data.pipe(writer)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
writer.on('finish', resolve)
|
||||||
|
writer.on('error', reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async _downloadAssets(item, itemPath) {
|
||||||
|
const uuidregex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
||||||
|
for (const [key, uuid] of Object.entries(item)){
|
||||||
|
// TODO: instead of trying to pull asset and hope for the best,
|
||||||
|
// crosscheck things with the /fields and play by the rules
|
||||||
|
if (typeof uuid === 'string' && uuid.match(uuidregex)) {
|
||||||
|
const filename = await this._downloadAsset(uuid, itemPath)
|
||||||
|
item[key] = filename // Update field with filename instead of uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _downloadAsset(uuid, itemPath) {
|
||||||
|
const response = await Axios({url: `${this.url}/assets/${uuid}?download&access_token=${this.token}`, method: 'GET', responseType: 'stream'})
|
||||||
|
const disposition = response.headers['content-disposition'].match(/filename="(.*)"/);
|
||||||
|
const filename = disposition ? disposition[1] : `${value}.${mime.extension(response.headers['content-type'])}`;
|
||||||
|
const savePath = `${itemPath}/${filename}`;
|
||||||
|
await this._writeFile(response, savePath)
|
||||||
|
return filename
|
||||||
|
}
|
||||||
|
|
||||||
|
async _downloadAssetsFromContent(item, itemPath) {
|
||||||
|
const uuidregex = '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'
|
||||||
|
const regEx = new RegExp(`!\\[.*\\]\\(${this.url}/assets/(${uuidregex})\\)`, "ig")
|
||||||
|
const uuids = []
|
||||||
|
let asset
|
||||||
|
do {
|
||||||
|
asset = regEx.exec(item[this.contentKey])
|
||||||
|
if (asset) uuids.push(asset[1])
|
||||||
|
} while (asset)
|
||||||
|
|
||||||
|
for (const uuid of uuids) {
|
||||||
|
const filename = await this._downloadAsset(uuid, itemPath)
|
||||||
|
const url = `${this.url}/assets/${uuid}`
|
||||||
|
item[this.contentKey] = item[this.contentKey].replace(url, filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async export() {
|
||||||
|
for (const collectionName in this.collections) {
|
||||||
|
const collection = this.collections[collectionName]
|
||||||
|
const readManyOption = collection.readManyOption
|
||||||
|
const items = (await this.directus.items(collectionName).readMany(readManyOption)).data
|
||||||
|
for (const item of items) {
|
||||||
|
const itemPath = collection.pathBuilder(item)
|
||||||
|
if (!fs.existsSync(itemPath)) {
|
||||||
|
fs.mkdirSync(itemPath, { recursive: true });
|
||||||
|
}
|
||||||
|
await this._downloadAssets(item, itemPath)
|
||||||
|
await this._downloadAssetsFromContent(item, itemPath)
|
||||||
|
await this._writeIndex(item, itemPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
{
|
||||||
|
"name": "@resilien/directus-to-markdown",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@resilien/directus-to-markdown",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@directus/sdk": "^9.5.0",
|
||||||
|
"js-yaml": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@directus/sdk": {
|
||||||
|
"version": "9.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@directus/sdk/-/sdk-9.5.0.tgz",
|
||||||
|
"integrity": "sha512-zrQmE8Wde5ITKhTpeYgJgb+QhFc8ySq/mFPfLq1+vO/xzvx5mg7v1OER1tMs7Byq3X/78euZCN4rxrvVRWQGNA==",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.24.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/argparse": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.14.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.14.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
|
||||||
|
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/js-yaml": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"js-yaml": "bin/js-yaml.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@directus/sdk": {
|
||||||
|
"version": "9.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@directus/sdk/-/sdk-9.5.0.tgz",
|
||||||
|
"integrity": "sha512-zrQmE8Wde5ITKhTpeYgJgb+QhFc8ySq/mFPfLq1+vO/xzvx5mg7v1OER1tMs7Byq3X/78euZCN4rxrvVRWQGNA==",
|
||||||
|
"requires": {
|
||||||
|
"axios": "^0.24.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"argparse": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||||
|
},
|
||||||
|
"axios": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
|
||||||
|
"requires": {
|
||||||
|
"follow-redirects": "^1.14.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"follow-redirects": {
|
||||||
|
"version": "1.14.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
|
||||||
|
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ=="
|
||||||
|
},
|
||||||
|
"js-yaml": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||||
|
"requires": {
|
||||||
|
"argparse": "^2.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "@resilien/directus-to-markdown",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Export Directus items to markdown files with assets",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.weko.io/resilien/directus-to-markdown.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"directus",
|
||||||
|
"export",
|
||||||
|
"markdown",
|
||||||
|
"assets",
|
||||||
|
"hugo"
|
||||||
|
],
|
||||||
|
"author": "RésiLien",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@directus/sdk": "^9.5.0",
|
||||||
|
"js-yaml": "^4.1.0"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue