UNPKG

11.4 kBJavaScriptView Raw
1'use strict';
2const h = require('highland'),
3 _ = require('lodash'),
4 b64 = require('base-64'),
5 utils = require('clayutils'),
6 composer = require('./composer'),
7 deepReduce = require('./deep-reduce'),
8 types = require('./types');
9
10/**
11 * get uri from dispatch
12 * @param {object} dispatch
13 * @return {string}
14 */
15function getDispatchURI(dispatch) {
16 return Object.keys(dispatch)[0];
17}
18
19/* convert dispatches to bootstraps, and vice versa
20 * dispatch looks like: {"/_components/article/instances/foo":{"title":"My Article","content": [{"_ref":"/_components/paragraph/instances/bar","text":"lorem ipsum"}]}}
21 * bootstrap looks like:
22 * _components:
23 * article:
24 * instances:
25 * foo:
26 * title: My Article
27 * content:
28 * - _ref: /_components/paragraph/instances/bar
29 * paragraph:
30 * instances:
31 * bar:
32 * text: lorem ipsum
33 */
34
35/**
36 * create dispatches from component defaults and instances,
37 * then deduplicate if any child components have dispatches of their own already
38 * @param {array} dispatches e.g. [{ unprefixed ref: composed data }]
39 * @param {object} components
40 * @param {object} bootstrap obj to refer to
41 * @param {object} added obj to check if components have been added already
42 * @return {array} of dispatches
43 */
44function parseComponentBootstrap(dispatches, components, { bootstrap, added }) {
45 return _.reduce(components, (dispatches, data, name) => {
46 const defaultData = _.omit(data, 'instances'),
47 defaultURI = `/_components/${name}`;
48
49 // first, compose and add the default data if it hasn't already been added
50 if (_.size(defaultData) && !added[defaultURI]) {
51 dispatches.push({ [defaultURI]: composer.denormalize(defaultData, bootstrap, added) });
52 added[defaultURI] = true;
53 }
54
55 // second, compose and add instances if they haven't already been added
56 if (data.instances && _.size(data.instances)) {
57 _.forOwn(data.instances, (instanceData, instance) => {
58 const instanceURI = `/_components/${name}/instances/${instance}`;
59
60 if (!added[instanceURI]) {
61 dispatches.push({ [instanceURI]: composer.denormalize(instanceData, bootstrap, added) });
62 added[instanceURI] = true;
63 }
64 });
65 }
66
67 // then remove any dispatches that are actually children of other components
68 return _.filter(dispatches, (dispatch) => !_.get(added, `asChild[${getDispatchURI(dispatch)}]`));
69 }, dispatches);
70}
71
72/**
73 * create dispatches from layout defaults and instances
74 * @param {array} dispatches e.g. [{ unprefixed ref: composed data }]
75 * @param {object} layouts
76 * @param {object} bootstrap obj to refer to
77 * @param {object} added
78 * @return {array} of dispatches
79 */
80function parseLayoutBootstrap(dispatches, layouts, { bootstrap, added }) {
81 return _.reduce(layouts, (dispatches, data, name) => {
82 const defaultData = _.omit(data, 'instances'),
83 defaultURI = `/_layouts/${name}`;
84
85 // first, compose and add the default data if it hasn't already been added
86 if (_.size(defaultData)) {
87 dispatches.push({ [defaultURI]: composer.denormalize(defaultData, bootstrap, added) });
88 }
89
90 // second, compose and add instances if they haven't already been added
91 if (data.instances && _.size(data.instances)) {
92 _.forOwn(data.instances, (instanceData, instance) => {
93 const instanceURI = `/_layouts/${name}/instances/${instance}`;
94
95 let meta;
96
97 // parse out metadata
98 if (instanceData.meta) {
99 meta = instanceData.meta;
100 delete instanceData.meta;
101 }
102
103 dispatches.push({ [instanceURI]: composer.denormalize(instanceData, bootstrap, added) });
104 if (meta) {
105 dispatches.push({ [`${instanceURI}/meta`]: meta });
106 }
107 });
108 }
109
110 return dispatches;
111 }, dispatches);
112}
113
114/**
115 * create dispatches from page data
116 * note: these pages are not composed
117 * @param {array} dispatches
118 * @param {object} pages
119 * @return {array}
120 */
121function parsePageBootstrap(dispatches, pages) {
122 return _.reduce(pages, (dispatches, page, id) => {
123 let meta;
124
125 if (id[0] === '/') {
126 // if a page starts with a slash, remove it so we can generate the uri
127 id = id.slice(1);
128 }
129
130 // unpublished pages should not have 'url', but rather 'customUrl'
131 if (page.url && !page.customUrl) {
132 page.customUrl = page.url;
133 delete page.url; // don't pass this through
134 }
135
136 // parse out metadata
137 if (page.meta) {
138 meta = page.meta;
139 delete page.meta;
140 }
141
142 dispatches.push({ [`/_pages/${id}`]: page });
143 // add meta dispatch _after_ page data
144 if (meta) {
145 dispatches.push({ [`/_pages/${id}/meta`]: meta });
146 }
147 return dispatches;
148 }, dispatches);
149}
150
151/**
152 * create dispatches from users
153 * @param {array} dispatches
154 * @param {array} users
155 * @return {array}
156 */
157function parseUsersBootstrap(dispatches, users) {
158 // note: dispatches match 1:1 with users
159 return _.reduce(users, (dispatches, user) => {
160 if (!user.username || !user.provider || !user.auth) {
161 throw new Error('Cannot bootstrap users without username, provider, and auth level');
162 } else {
163 dispatches.push({ [`/_users/${b64.encode(user.username.toLowerCase() + '@' + user.provider)}`]: user });
164 }
165 return dispatches;
166 }, dispatches);
167}
168
169/**
170 * parse uris, lists, etc arbitrary data in bootstraps
171 * @param {array} dispatches
172 * @param {object|array} items
173 * @param {string} type
174 * @return {array}
175 */
176function parseArbitraryBootstrapData(dispatches, items, type) {
177 return _.reduce(items, (dispatches, item, key) => {
178 if (key[0] === '/') {
179 // fix for uris, which sometimes start with /
180 key = key.slice(1);
181 }
182 dispatches.push({ [`/${type}/${key}`]: item });
183 return dispatches;
184 }, dispatches);
185}
186
187/**
188 * compose bootstrap data
189 * @param {object} bootstrap
190 * @return {Stream} of dispatches
191 */
192function parseBootstrap(bootstrap) {
193 let added = { asChild: {} },
194 dispatches = _.reduce(bootstrap, (dispatches, items, type) => {
195 switch (type) {
196 case '_components': return parseComponentBootstrap(dispatches, items, { bootstrap, added });
197 case '_layouts': return parseLayoutBootstrap(dispatches, items, { bootstrap, added });
198 case '_pages': return parsePageBootstrap(dispatches, items);
199 case '_users': return parseUsersBootstrap(dispatches, items);
200 default: return parseArbitraryBootstrapData(dispatches, items, type); // uris, lists
201 }
202 }, []);
203
204 return h(dispatches);
205}
206
207/**
208 * convert stream of bootstrap objects to dispatches
209 * @param {Stream} stream
210 * @return {Stream}
211 */
212function toDispatch(stream) {
213 return stream.flatMap(parseBootstrap);
214}
215
216/**
217 * add deep component data to a bootstrap
218 * @param {string} uri
219 * @param {object} dispatch
220 * @param {object} bootstrap
221 * @return {object}
222 */
223function parseComponentDispatch(uri, dispatch, bootstrap) {
224 const deepData = dispatch[uri],
225 name = utils.getComponentName(uri),
226 instance = utils.getComponentInstance(uri),
227 path = instance ? `_components['${name}'].instances['${instance}']` : `_components['${name}']`;
228
229 _.set(bootstrap, path, composer.normalize(deepData));
230
231 return deepReduce(bootstrap, deepData, (ref, val) => {
232 const deepName = utils.getComponentName(ref),
233 deepInstance = utils.getComponentInstance(ref),
234 deepPath = deepInstance ? `_components['${deepName}'].instances['${deepInstance}']` : `_components['${deepName}']`;
235
236 _.set(bootstrap, deepPath, composer.normalize(val));
237 });
238}
239
240/**
241 * add deep layout data to a bootstrap
242 * @param {string} uri
243 * @param {object} dispatch
244 * @param {object} bootstrap
245 * @return {object}
246 */
247function parseLayoutDispatch(uri, dispatch, bootstrap) {
248 const deepData = dispatch[uri],
249 name = utils.getLayoutName(uri),
250 instance = utils.getLayoutInstance(uri),
251 path = instance ? `_layouts['${name}'].instances['${instance}']` : `_layouts['${name}']`;
252
253 if (utils.isLayoutMeta(uri)) {
254 // if we're just setting metadata, return early
255 // note: only instances can have metadata
256 _.set(bootstrap, `_layouts['${name}'].instances['${instance}'].meta`, deepData);
257 return bootstrap;
258 }
259
260 _.set(bootstrap, path, _.assign({}, _.get(bootstrap, path, {}), composer.normalize(deepData)));
261
262 return deepReduce(bootstrap, deepData, (ref, val) => {
263 // reduce on the child components and their instances
264 const deepName = utils.getComponentName(ref),
265 deepInstance = utils.getComponentInstance(ref),
266 deepPath = deepInstance ? `_components['${deepName}'].instances['${deepInstance}']` : `_components['${deepName}']`;
267
268 _.set(bootstrap, deepPath, _.assign({}, _.get(bootstrap, deepPath, {}), composer.normalize(val)));
269 });
270}
271
272/**
273 * add page data to a bootstrap
274 * @param {string} uri
275 * @param {object} dispatch
276 * @param {object} bootstrap
277 * @return {object}
278 */
279function parsePageDispatch(uri, dispatch, bootstrap) {
280 let id = utils.getPageInstance(uri),
281 page = dispatch[uri];
282
283 if (utils.isPageMeta(uri)) {
284 // if we're just setting metadata, return early
285 _.set(bootstrap, `_pages['${id}'].meta`, page);
286 return bootstrap;
287 }
288
289 // unpublished pages should not have 'url', but rather 'customUrl'
290 if (page.url && !page.customUrl) {
291 page.customUrl = page.url;
292 delete page.url; // don't pass this through
293 }
294
295 _.set(bootstrap, `_pages['${id}']`, _.assign({}, _.get(bootstrap, `_pages['${id}']`, {}), page));
296 return bootstrap;
297}
298
299/**
300 * add user data to a bootstrap
301 * @param {string} uri
302 * @param {object} dispatch
303 * @param {object} bootstrap
304 * @return {object}
305 */
306function parseUsersDispatch(uri, dispatch, bootstrap) {
307 if (!bootstrap._users) {
308 bootstrap._users = [];
309 }
310
311 bootstrap._users.push(dispatch[uri]);
312 return bootstrap;
313}
314
315/**
316 * add uris, lists, etc arbitrary data to a bootstrap
317 * @param {string} uri
318 * @param {object} dispatch
319 * @param {object} bootstrap
320 * @return {object}
321 */
322function parseArbitraryDispatchData(uri, dispatch, bootstrap) {
323 let type = _.find(types, (t) => _.includes(uri, t)),
324 name = uri.split(`${type}/`)[1];
325
326 type = type.slice(1); // remove beginning slash
327 if (type === '_uris' && name[0] !== '/') {
328 // fix for uris, which sometimes start with /
329 name = `/${name}`;
330 }
331
332 _.set(bootstrap, `${type}['${name}']`, dispatch[uri]);
333 return bootstrap;
334}
335
336/**
337 * generate a bootstrap by reducing through a stream of dispatches
338 * @param {object} bootstrap
339 * @param {object} dispatch
340 * @return {object}
341 */
342function generateBootstrap(bootstrap, dispatch) {
343 const uri = getDispatchURI(dispatch),
344 type = _.find(types, (t) => _.includes(uri, t));
345
346 switch (type) {
347 case '/_components': return parseComponentDispatch(uri, dispatch, bootstrap);
348 case '/_layouts': return parseLayoutDispatch(uri, dispatch, bootstrap);
349 case '/_pages': return parsePageDispatch(uri, dispatch, bootstrap);
350 case '/_users': return parseUsersDispatch(uri, dispatch, bootstrap);
351 default: return parseArbitraryDispatchData(uri, dispatch, bootstrap); // uris, lists
352 }
353}
354
355/**
356 * convert stream of dispatches to a bootstrap
357 * @param {Stream} stream
358 * @return {Stream}
359 */
360function toBootstrap(stream) {
361 return stream.reduce({}, generateBootstrap);
362}
363
364module.exports.toDispatch = toDispatch;
365module.exports.toBootstrap = toBootstrap;