diff --git a/lib/index.coffee b/lib/index.coffee index c4a713f..df440eb 100644 --- a/lib/index.coffee +++ b/lib/index.coffee @@ -6,6 +6,9 @@ contentful = require 'contentful' pluralize = require 'pluralize' RootsUtil = require 'roots-util' querystring = require 'querystring' +nodefn = require 'when/node' +fs = require 'fs' +mkdirp = require 'mkdirp' errors = no_token: 'Missing required options for roots-contentful. Please ensure @@ -20,6 +23,10 @@ hosts = production: 'cdn.contentful.com' module.exports = (opts) -> + # default namespace + opts.namespace ?= 'contentful' + opts.cache ?= false + # throw error if missing required config if not (opts.access_token && opts.space_id) throw new Error errors.no_token @@ -37,16 +44,15 @@ module.exports = (opts) -> constructor: (@roots) -> @util = new RootsUtil(@roots) @roots.config.locals ?= {} - @roots.config.locals.contentful ?= {} @roots.config.locals.asset = asset_view_helper setup: -> configure_content(opts.content_types).with(@) .then(get_all_content) - .tap(set_urls) .then(transform_entries) .then(sort_entries) .tap(set_locals) + .tap(set_urls) .tap(compile_entries) .tap(write_entries) @@ -84,6 +90,7 @@ module.exports = (opts) -> res , [] + ###* * Fetches data from Contentful for content types, and formats the raw data * @param {Array} types - configured content_type objects @@ -91,11 +98,48 @@ module.exports = (opts) -> ### get_all_content = (types) -> - W.map types, (t) -> - fetch_content(t) - .then(format_content) - .then((c) -> t.content = c) - .yield(t) + W.map types, (t) => + + cache = t.cache ? opts.cache + + if typeof cache == 'string' + t.write_raw ?= cache + t.read_raw ?= cache + else if typeof cache == 'function' + cacheFilePaths = cache(t) + t.write_raw ?= cacheFilePaths.write + t.read_raw ?= cacheFilePaths.read + else if typeof cache == 'boolean' and cache == true + t.write_raw ?= path.join(@roots.config.output_path(), "#{t.name}.json") + t.read_raw ?= path.join(@roots.config.output_path(), "#{t.name}.json") + + + read_file_exists = W(false) + if t.read_raw + readFrom = if typeof t.read_raw == 'function' then t.read_raw(t) else t.read_raw + read_file_exists = nodefn.call(fs.stat, readFrom) + + read_file_exists.then (exists) -> + + if not exists + return throw new Error('Cache file does not exists. Fetching from External.') + + #no need to write cache as its the same data.. + delete t.write_raw + + nodefn.call(fs.readFile, readFrom) + .then (data) -> + JSON.parse(data) + + #fetch data from external if cache file not found + .catch -> + fetch_content(t) + + .then(format_content) + .then((c) -> t.content = c) + .yield(t) + .tap(write_raw) + ###* * Fetch entries for a single content type object @@ -137,9 +181,9 @@ module.exports = (opts) -> ### set_urls = (types) -> - W.map types, (t) -> - if t.template then W.map t.content, (entry) -> - paths = t.path(entry) + W.map types, (t) => + if t.template then W.map t.content, (entry) => + paths = t.path(entry, _.cloneDeep(@roots.config.locals)) paths = [paths] if _.isString(paths) entry._urls = ("/#{p}.html" for p in paths) entry._url = if entry._urls.length is 1 then entry._urls[0] else null @@ -152,7 +196,14 @@ module.exports = (opts) -> set_locals = (types) -> W.map types, (t) => - @roots.config.locals.contentful[t.name] = t.content + + namespace = if t.namespace then t.namespace else opts.namespace + @roots.config.locals[namespace] ?= {} + + locals_data = t.content + if t.set_locals and typeof t.set_locals == 'function' + locals_data = t.set_locals(t) + @roots.config.locals[namespace][t.name] = locals_data ###* * Transforms every type with content with the user provided callback @@ -213,6 +264,21 @@ module.exports = (opts) -> if not t.write then return W.resolve() @util.write(t.write, JSON.stringify(t.content)) + ###* + * Writes all data returned from contentful as json + * @param {Array} types - Populated content type objects + * @return {Promise} - promise for when compilation is finished + ### + + write_raw = (t) -> + if not t.write_raw then return W.resolve() + + writeTo = if typeof t.write_raw == 'function' then t.write_raw(t) else t.write_raw + + nodefn.call(mkdirp, path.dirname(writeTo)) + .then(-> nodefn.call(fs.writeFile, writeTo , JSON.stringify(t))) + + ###* * View helper for accessing the actual url from a Contentful asset * and appends any query string params diff --git a/package.json b/package.json index 07ae4e5..bdc7606 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "dependencies": { "contentful": "^1.1.5", "lodash": "^3.10.1", + "mkdirp": "^0.5.1", "pluralize": "^1.2.1", "roots-util": "0.1.x", "string": "^3.1.3", diff --git a/raw_posts.json b/raw_posts.json new file mode 100644 index 0000000..1cb17d5 --- /dev/null +++ b/raw_posts.json @@ -0,0 +1 @@ +{"id":"6BYT1gNiIEyIw8Og8aQAO6","write_raw":"raw_posts.json","filters":{"content_type":"6BYT1gNiIEyIw8Og8aQAO6","include":10},"name":"blog_posts","content":[{"title":"Throw Some Ds","body":"Rich Boy selling crick"}]} \ No newline at end of file diff --git a/test/fixtures/cache/app.coffee b/test/fixtures/cache/app.coffee new file mode 100644 index 0000000..6b62613 --- /dev/null +++ b/test/fixtures/cache/app.coffee @@ -0,0 +1,25 @@ +contentful = require '../../..' + +module.exports = + ignores: ["**/_*", "**/.DS_Store"] + extensions: [ + contentful( + access_token: 'YOUR_ACCESS_TOKEN' + space_id: 'aqzq2qya2jm4' + cache: true + content_types: + index: + id: '6BYT1gNiIEyIw8Og8aQAO6', + read: + id: '6BYT1gNiIEyIw8Og8aQAO2', + read_raw: 'test/fixtures/cache/index.json' + write_raw: 'test/fixtures/cache/public/raw_posts.json' + cache: + id: '6BYT1gNiIEyIw8Og8aQAO2', + cache: (type) -> + return { + read: 'test/fixtures/cache/index.json' + write: 'test/fixtures/cache/public/cache_posts.json' + } + ) + ] diff --git a/test/fixtures/cache/index.jade b/test/fixtures/cache/index.jade new file mode 100644 index 0000000..e9fb8b1 --- /dev/null +++ b/test/fixtures/cache/index.jade @@ -0,0 +1,10 @@ +ul + - for p in contentful.index + li + h1= p.title + p= p.body + + - for p in contentful.read + li + h1= p.title + p= p.body diff --git a/test/fixtures/cache/package.json b/test/fixtures/cache/package.json new file mode 100644 index 0000000..2d0ae2c --- /dev/null +++ b/test/fixtures/cache/package.json @@ -0,0 +1,6 @@ +{ + "name": "test", + "dependencies": { + "jade": "*" + } +} diff --git a/test/fixtures/namespace/app.coffee b/test/fixtures/namespace/app.coffee new file mode 100644 index 0000000..10efba6 --- /dev/null +++ b/test/fixtures/namespace/app.coffee @@ -0,0 +1,20 @@ +contentful = require '../../..' + +module.exports = + ignores: ["**/_*", "**/.DS_Store"] + extensions: [ + contentful( + namespace: 'custom' + access_token: 'YOUR_ACCESS_TOKEN' + space_id: 'aqzq2qya2jm4' + content_types: [ + { + id: '6BYT1gNiIEyIw8Og8aQAO6', + }, + { + namespace: 'custom2' + id: '6BYT1gNiIEyIw8Og8aQAO6', + } + ] + ) + ] diff --git a/test/fixtures/namespace/index.jade b/test/fixtures/namespace/index.jade new file mode 100644 index 0000000..a978c20 --- /dev/null +++ b/test/fixtures/namespace/index.jade @@ -0,0 +1,10 @@ +ul + - for p in custom.blog_posts + li + h1= p.title + p= p.body + + - for p in custom2.blog_posts + li + h1= p.title_2 + p= p.body_2 diff --git a/test/fixtures/namespace/package.json b/test/fixtures/namespace/package.json new file mode 100644 index 0000000..2d0ae2c --- /dev/null +++ b/test/fixtures/namespace/package.json @@ -0,0 +1,6 @@ +{ + "name": "test", + "dependencies": { + "jade": "*" + } +} diff --git a/test/fixtures/set_locals/app.coffee b/test/fixtures/set_locals/app.coffee new file mode 100644 index 0000000..64da0e2 --- /dev/null +++ b/test/fixtures/set_locals/app.coffee @@ -0,0 +1,19 @@ +contentful = require '../../..' + +module.exports = + ignores: ["**/_*", "**/.DS_Store"] + extensions: [ + contentful( + access_token: 'YOUR_ACCESS_TOKEN' + space_id: 'aqzq2qya2jm4' + content_types: [ + { + name: 'local_test' + id: '6BYT1gNiIEyIw8Og8aQAO6', + write: 'posts.json', + set_locals: (content_type) -> + return {specific_local: content_type.content[0].body} + }, + ] + ) + ] diff --git a/test/fixtures/set_locals/index.jade b/test/fixtures/set_locals/index.jade new file mode 100644 index 0000000..95783bd --- /dev/null +++ b/test/fixtures/set_locals/index.jade @@ -0,0 +1 @@ +h1= contentful.local_test.specific_local diff --git a/test/fixtures/set_locals/package.json b/test/fixtures/set_locals/package.json new file mode 100644 index 0000000..2d0ae2c --- /dev/null +++ b/test/fixtures/set_locals/package.json @@ -0,0 +1,6 @@ +{ + "name": "test", + "dependencies": { + "jade": "*" + } +} diff --git a/test/fixtures/single_entry_custom/app.coffee b/test/fixtures/single_entry_custom/app.coffee index c5b8d3d..649c205 100644 --- a/test/fixtures/single_entry_custom/app.coffee +++ b/test/fixtures/single_entry_custom/app.coffee @@ -12,7 +12,9 @@ module.exports = id: '6BYT1gNiIEyIw8Og8aQAO6' name: 'blog_posts' template: 'views/_blog_post.jade' - path: (e) -> "blogging/#{e.category}/#{S(e.title).slugify().s}" + path: (e, locals) -> + category = locals.contentful['blog_posts'][0].category + return "blogging/#{category}/#{S(e.title).slugify().s}" } ] ) diff --git a/test/test.coffee b/test/test.coffee index cc46f31..0516afb 100644 --- a/test/test.coffee +++ b/test/test.coffee @@ -111,6 +111,34 @@ describe 'write as json', -> after -> unmock_contentful() +describe 'data caching', -> + before (done) -> + @title = 'Throw Some Ds' + @body = 'Rich Boy selling crick' + mock_contentful(entries: [{fields: {title: @title, body: @body}}]) + compile_fixture.call(@, 'cache').then(-> done()).catch(done) + + it 'compiles project', -> + p = path.join(@public, 'index.html') + h.file.exists(p).should.be.ok + + it 'writes and reads cache files correctly', -> + p_index = path.join(@public, 'index.json') + p_posts = path.join(@public, 'raw_posts.json') + h.file.exists(p_index).should.be.ok + h.file.exists(p_posts).should.be.ok + h.file.contains(p_index, @title).should.be.true + h.file.contains(p_index, @body).should.be.true + + it 'cache function should read exists and write new cache file', -> + p_cache_posts = path.join(@public, 'cache_posts.json') + h.file.exists(p_cache_posts).should.be.ok + h.file.contains(p_cache_posts, @title).should.be.true + h.file.contains(p_cache_posts, @body).should.be.true + + + after -> unmock_contentful() + describe 'data manipulation', -> describe 'sort', -> before (done) -> @@ -186,6 +214,46 @@ describe 'data manipulation', -> after -> unmock_contentful() + describe 'custom namespace', -> + before (done) -> + @title = 'Throw Some Ds' + @body = 'Rich Boy selling crack' + @title_2 = 'Yes sir' + @body_2 = 'No mum' + + mock_contentful + entries: [ + {fields: {title: @title, body: @body}} + {fields: {title: @title_2, body: @body_2}} + ] + compile_fixture.call(@, 'namespace').then(-> done()).catch(done) + + it 'has contentful data available in views under a custom global namespace', -> + p = path.join(@public, 'index.html') + h.file.contains(p, @title).should.be.true + h.file.contains(p, @body).should.be.true + + it 'has contentful data available in views under a custom local(type) namespace', -> + p = path.join(@public, 'index.html') + h.file.contains(p, @title_2).should.be.true + h.file.contains(p, @body_2).should.be.true + + after -> unmock_contentful() + + describe 'set_locals', -> + before (done) -> + @title = 'Throw Some Ds' + @body = 'Rich Boy selling crack' + + mock_contentful entries: [{fields: {title: @title, body: @body}}] + compile_fixture.call(@, 'set_locals').then(-> done()).catch(done) + + it 'can set custom content_type locals', -> + p = path.join(@public, 'index.html') + h.file.contains(p, @body).should.be.true + + after -> unmock_contentful() + describe 'custom name for view helper local', -> before (done) -> @title = 'Throw Some Ds'