diff --git a/app.json b/app.json index b19407a49..0a9675c8a 100644 --- a/app.json +++ b/app.json @@ -26,8 +26,7 @@ "required": true }, "MONGODB_URL": { - "description": "URL of a MongoDB host", - "required": true + "description": "URL of a MongoDB host (optional)" }, "NPM_CONFIG_PRODUCTION": { "descrition": "Skip pruning devDependencies while packages are not public", diff --git a/indiekit.config.js b/indiekit.config.js index 7defc6a31..fd41c2570 100644 --- a/indiekit.config.js +++ b/indiekit.config.js @@ -22,6 +22,7 @@ indiekit.set('application.locale', process.env.LOCALE); // Publication settings indiekit.set('publication.me', process.env.PUBLICATION_URL); indiekit.set('publication.storeId', 'github'); +indiekit.set('publication.config.categories.url', 'http://paulrobertlloyd.com/categories/index.json'); // Server const server = indiekit.server(); diff --git a/packages/endpoint-media/index.js b/packages/endpoint-media/index.js index 3fb84df01..b51a65619 100644 --- a/packages/endpoint-media/index.js +++ b/packages/endpoint-media/index.js @@ -27,10 +27,12 @@ export const MediaEndpoint = class { init(indiekitConfig) { const {application, publication} = indiekitConfig; - indiekitConfig.addNavigation({ - href: `${this.mountpath}/files`, - text: 'Files' - }); + if (application.hasDatabase) { + indiekitConfig.addNavigation({ + href: `${this.mountpath}/files`, + text: 'Files' + }); + } indiekitConfig.addRoute({ mountpath: this.mountpath, @@ -51,8 +53,11 @@ export const MediaEndpoint = class { this._router.get('/', queryController(publication)); this._router.post('/', indieauth(publication), multipartParser.single('file'), uploadController(publication)); - this._router.get('/files', authenticate, filesController(publication).list); - this._router.get('/files/:id', authenticate, filesController(publication).view); + + if (application.hasDatabase) { + this._router.get('/files', authenticate, filesController(publication).list); + this._router.get('/files/:id', authenticate, filesController(publication).view); + } return router; } diff --git a/packages/endpoint-media/lib/media.js b/packages/endpoint-media/lib/media.js index b1236be9d..bed245050 100644 --- a/packages/endpoint-media/lib/media.js +++ b/packages/endpoint-media/lib/media.js @@ -21,7 +21,11 @@ export const media = { if (uploaded) { mediaData.date = new Date(); mediaData.lastAction = 'upload'; - await media.insertOne(mediaData); + + if (media) { + await media.insertOne(mediaData); + } + return { location: mediaData.properties.url, status: 201, diff --git a/packages/endpoint-micropub/index.js b/packages/endpoint-micropub/index.js index 92d1ad3a0..25671e6ac 100644 --- a/packages/endpoint-micropub/index.js +++ b/packages/endpoint-micropub/index.js @@ -27,10 +27,12 @@ export const MicropubEndpoint = class { init(indiekitConfig) { const {application, publication} = indiekitConfig; - indiekitConfig.addNavigation({ - href: `${this.mountpath}/posts`, - text: 'Posts' - }); + if (application.hasDatabase) { + indiekitConfig.addNavigation({ + href: `${this.mountpath}/posts`, + text: 'Posts' + }); + } indiekitConfig.addRoute({ mountpath: this.mountpath, @@ -51,8 +53,11 @@ export const MicropubEndpoint = class { this._router.get('/', queryController(publication)); this._router.post('/', indieauth(publication), multipartParser.any(), actionController(publication)); - this._router.get('/posts', authenticate, postsController(publication).list); - this._router.get('/posts/:id', authenticate, postsController(publication).view); + + if (application.hasDatabase) { + this._router.get('/posts', authenticate, postsController(publication).list); + this._router.get('/posts/:id', authenticate, postsController(publication).view); + } return router; } diff --git a/packages/endpoint-micropub/lib/post.js b/packages/endpoint-micropub/lib/post.js index a100fc328..33d29d789 100644 --- a/packages/endpoint-micropub/lib/post.js +++ b/packages/endpoint-micropub/lib/post.js @@ -21,7 +21,10 @@ export const post = { if (published) { postData.date = new Date(); postData.lastAction = 'create'; - await posts.insertOne(postData); + + if (posts) { + await posts.insertOne(postData); + } return { location: postData.properties.url, @@ -55,9 +58,12 @@ export const post = { if (published) { postData.date = new Date(); postData.lastAction = 'update'; - await posts.replaceOne({ - url: postData.properties.url - }, postData); + + if (posts) { + await posts.replaceOne({ + url: postData.properties.url + }, postData); + } const hasUpdatedUrl = (url !== postData.properties.url); return { @@ -92,9 +98,12 @@ export const post = { if (published) { postData.date = new Date(); postData.lastAction = 'delete'; - await posts.replaceOne({ - url: postData.properties.url - }, postData); + + if (posts) { + await posts.replaceOne({ + url: postData.properties.url + }, postData); + } return { status: 200, @@ -131,9 +140,12 @@ export const post = { if (published) { postData.date = new Date(); postData.lastAction = 'undelete'; - await posts.replaceOne({ - url: postData.properties.url - }, postData); + + if (posts) { + await posts.replaceOne({ + url: postData.properties.url + }, postData); + } return { location: postData.properties.url, diff --git a/packages/indiekit/config/defaults.js b/packages/indiekit/config/defaults.js index f5afda8c8..429ae41db 100644 --- a/packages/indiekit/config/defaults.js +++ b/packages/indiekit/config/defaults.js @@ -29,6 +29,7 @@ export const defaultConfig = { themeColor: '#0000ee', repository: package_.repository, version: package_.version, + hasDatabase: typeof process.env.MONGODB_URL !== 'undefined', endpoints: [ mediaEndpoint, micropubEndpoint, diff --git a/packages/indiekit/config/mongodb.js b/packages/indiekit/config/mongodb.js index 1e4d1d56b..72c18b658 100644 --- a/packages/indiekit/config/mongodb.js +++ b/packages/indiekit/config/mongodb.js @@ -1,11 +1,14 @@ import mongodb from 'mongodb'; export const mongodbConfig = (async () => { - const {MongoClient} = mongodb; - const client = new MongoClient(process.env.MONGODB_URL, { - useUnifiedTopology: true - }); - await client.connect(); - const database = client.db('indiekit'); - return database; + const mongodbUrl = process.env.MONGODB_URL; + if (mongodbUrl) { + const {MongoClient} = mongodb; + const client = new MongoClient(mongodbUrl, { + useUnifiedTopology: true + }); + await client.connect(); + const database = client.db('indiekit'); + return database; + } })(); diff --git a/packages/indiekit/index.js b/packages/indiekit/index.js index 78b3feeed..1b8d6de35 100644 --- a/packages/indiekit/index.js +++ b/packages/indiekit/index.js @@ -64,16 +64,19 @@ export const Indiekit = class { } async init() { - const {locale, presets, stores} = this.application; + const {hasDatabase, locale, presets, stores} = this.application; const {config, presetId, storeId} = this.publication; + const database = await mongodbConfig; + + // Application cache collection + this.application.cache = hasDatabase ? await database.collection('cache') : false; // Publication data collections - const database = await mongodbConfig; - this.publication.posts = await database.collection('posts'); - this.publication.media = await database.collection('media'); + this.publication.posts = hasDatabase ? await database.collection('posts') : false; + this.publication.media = hasDatabase ? await database.collection('media') : false; // Publication configuration - const cache = new Cache(mongodbConfig); + const cache = new Cache(this.application.cache); const preset = getPreset(presets, presetId); const categories = await getCategories(cache, config.categories); this.publication.preset = preset; diff --git a/packages/indiekit/lib/cache.js b/packages/indiekit/lib/cache.js index ef367b0cf..87378f5fb 100644 --- a/packages/indiekit/lib/cache.js +++ b/packages/indiekit/lib/cache.js @@ -3,12 +3,10 @@ import got from 'got'; export const Cache = class { /** Fetch data from cache or remote file * - * @param {object} database Database - * @param {string} expires Timeout on key + * @param {object} collection Collection */ - constructor(database, expires) { - this.database = database; - this.expires = expires || 3600; + constructor(collection) { + this.collection = collection; } /** @@ -20,9 +18,7 @@ export const Cache = class { */ async json(key, url) { try { - const database = await this.database; - const collection = await database.collection('cache'); - const cachedData = await collection.findOne({key, url}); + const cachedData = this.collection ? await this.collection.findOne({key, url}) : false; if (cachedData) { const {data} = cachedData; return { @@ -34,9 +30,13 @@ export const Cache = class { const fetchedData = await got(url, {responseType: 'json'}); if (fetchedData) { const data = fetchedData.body; - await collection.replaceOne({}, {key, url, data}, { - upsert: true - }); + + if (this.collection) { + await this.collection.replaceOne({}, {key, url, data}, { + upsert: true + }); + } + return { source: url, data diff --git a/packages/indiekit/tests/lib/cache.js b/packages/indiekit/tests/lib/cache.js index e644c0156..883119ff5 100644 --- a/packages/indiekit/tests/lib/cache.js +++ b/packages/indiekit/tests/lib/cache.js @@ -3,10 +3,12 @@ import nock from 'nock'; import {mongodbConfig} from '../../config/mongodb.js'; import {Cache} from '../../lib/cache.js'; -const cache = new Cache(mongodbConfig); +test.beforeEach(async t => { + const database = await mongodbConfig; + const collection = await database.collection('cache'); -test.beforeEach(t => { t.context = { + cache: new Cache(collection), nock: nock('https://website.example').get('/categories.json'), url: 'https://website.example/categories.json' }; @@ -19,25 +21,23 @@ test.afterEach.always(async () => { test.serial('Returns data from remote file and saves to cache', async t => { const scope = t.context.nock.reply(200, ['Foo', 'Bar']); - const result = await cache.json('category', t.context.url); + const result = await t.context.cache.json('category', t.context.url); t.is(result.source, t.context.url); scope.done(); }); test.serial('Throws error if remote file not found', async t => { const scope = t.context.nock.replyWithError('Not found'); - const error = await t.throwsAsync(cache.json('file', t.context.url)); + const error = await t.throwsAsync(t.context.cache.json('file', t.context.url)); t.is(error.message, `Unable to fetch ${t.context.url}: Not found`); scope.done(); }); test.serial('Gets data from cache', async t => { const cache = new Cache({ - collection: () => ({ - findOne: async () => ({ - souce: 'cache', - date: {} - }) + findOne: async () => ({ + souce: 'cache', + date: {} }) }); diff --git a/packages/indiekit/tests/lib/publication.js b/packages/indiekit/tests/lib/publication.js index b2abc11cf..22b583dcf 100644 --- a/packages/indiekit/tests/lib/publication.js +++ b/packages/indiekit/tests/lib/publication.js @@ -13,8 +13,11 @@ import { } from '../../lib/publication.js'; test.beforeEach(async t => { + const database = await mongodbConfig; + const collection = await database.collection('cache'); + t.context = await { - cache: new Cache(mongodbConfig), + cache: new Cache(collection), categories: { nock: nock('https://website.example').get('/categories.json'), url: 'https://website.example/categories.json'