'use strict';
var Requests = require('./requests'),
Cookies = require('./cookies'),
documents = require('./documents'),
ApiCache = require('./cache'),
Predicates = require('./predicates'),
experiments = require('./experiments');
var Experiments = experiments.Experiments,
Document = documents.Document;
var experimentCookie = "io.prismic.experiment",
previewCookie = "io.prismic.preview";
/**
* Initialisation of the API object.
* This is for internal use, from outside this kit, you should call Prismic.Api()
* @private
*/
function Api(url, options) {
var opts = options || {};
this.accessToken = opts.accessToken;
this.url = url + (this.accessToken ? (url.indexOf('?') > -1 ? '&' : '?') + 'access_token=' + this.accessToken : '');
this.req = opts.req;
this.apiCache = opts.apiCache || globalCache();
this.requestHandler = opts.requestHandler || Requests.request;
this.apiCacheKey = this.url + (this.accessToken ? ('#' + this.accessToken) : '');
this.apiDataTTL = opts.apiDataTTL || 5;
return this;
}
Api.prototype = {
// Predicates
AT: "at",
ANY: "any",
SIMILAR: "similar",
FULLTEXT: "fulltext",
NUMBER: {
GT: "number.gt",
LT: "number.lt"
},
DATE: {
// Other date operators are available: see the documentation.
AFTER: "date.after",
BEFORE: "date.before",
BETWEEN: "date.between"
},
// Fragment: usable as the second element of a query array on most predicates (except SIMILAR).
// You can also use "my.*" for your custom fields.
DOCUMENT: {
ID: "document.id",
TYPE: "document.type",
TAGS: "document.tags"
},
data: null,
/**
* Fetches data used to construct the api client, from cache if it's
* present, otherwise from calling the prismic api endpoint (which is
* then cached).
*
* @param {function} callback - Callback to receive the data. Optional, you can use the promise result.
* @returns {Promise} Promise holding the data or error
*/
get: function(callback) {
var self = this;
var cacheKey = this.apiCacheKey;
return new Promise(function (resolve, reject) {
var cb = function(err, value, xhr, ttl) {
if (callback) callback(err, value, xhr, ttl);
if (value) resolve(value);
if (err) reject(err);
};
self.apiCache.get(cacheKey, function (err, value) {
if (err || value) {
cb(err, value);
return;
}
self.requestHandler(self.url, function(err, data, xhr, ttl) {
if (err) {
cb(err, null, xhr, ttl);
return;
}
var parsed = self.parse(data);
ttl = ttl || self.apiDataTTL;
self.apiCache.set(cacheKey, parsed, ttl, function (err) {
cb(err, parsed, xhr, ttl);
});
});
});
});
},
/**
* Cleans api data from the cache and fetches an up to date copy.
*
* @param {function} callback - Optional callback function that is called after the data has been refreshed
* @returns {Promise}
*/
refresh: function (callback) {
var self = this;
var cacheKey = this.apiCacheKey;
return new Promise(function(resolve, reject) {
var cb = function(err, value, xhr) {
if (callback) callback(err, value, xhr);
if (value) resolve(value);
if (err) reject(err);
};
self.apiCache.remove(cacheKey, function (err) {
if (err) { cb(err); return; }
self.get(function (err, data) {
if (err) { cb(err); return; }
self.data = data;
self.bookmarks = data.bookmarks;
self.experiments = new Experiments(data.experiments);
cb();
});
});
});
},
/**
* Parses and returns the /api document.
* This is for internal use, from outside this kit, you should call Prismic.Api()
*
* @param {string} data - The JSON document responded on the API's endpoint
* @returns {Api} - The Api object that can be manipulated
* @private
*/
parse: function(data) {
var refs,
master,
forms = {},
form,
types,
tags,
f,
i;
// Parse the forms
for (i in data.forms) {
if (data.forms.hasOwnProperty(i)) {
f = data.forms[i];
if(this.accessToken) {
f.fields['access_token'] = {};
f.fields['access_token']['type'] = 'string';
f.fields['access_token']['default'] = this.accessToken;
}
form = new Form(
f.name,
f.fields,
f.form_method,
f.rel,
f.enctype,
f.action
);
forms[i] = form;
}
}
refs = data.refs.map(function (r) {
return new Ref(
r.ref,
r.label,
r.isMasterRef,
r.scheduledAt,
r.id
);
}) || [];
master = refs.filter(function (r) {
return r.isMaster === true;
});
types = data.types;
tags = data.tags;
if (master.length === 0) {
throw ("No master ref.");
}
return {
bookmarks: data.bookmarks || {},
refs: refs,
forms: forms,
master: master[0],
types: types,
tags: tags,
experiments: data.experiments,
oauthInitiate: data['oauth_initiate'],
oauthToken: data['oauth_token'],
quickRoutes: data.quickRoutes
};
},
/**
* @deprecated use form() now
* @param {string} formId - The id of a form, like "everything", or "products"
* @returns {SearchForm} - the SearchForm that can be used.
*/
forms: function(formId) {
return this.form(formId);
},
/**
* Returns a useable form from its id, as described in the RESTful description of the API.
* For instance: api.form("everything") works on every repository (as "everything" exists by default)
* You can then chain the calls: api.form("everything").query('[[:d = at(document.id, "UkL0gMuvzYUANCpf")]]').ref(ref).submit()
*
* @param {string} formId - The id of a form, like "everything", or "products"
* @returns {SearchForm} - the SearchForm that can be used.
*/
form: function(formId) {
var form = this.data.forms[formId];
if(form) {
return new SearchForm(this, form, {});
}
return null;
},
/**
* The ID of the master ref on this prismic.io API.
* Do not use like this: searchForm.ref(api.master()).
* Instead, set your ref once in a variable, and call it when you need it; this will allow to change the ref you're viewing easily for your entire page.
*
* @returns {string}
*/
master: function() {
return this.data.master.ref;
},
/**
* Returns the ref ID for a given ref's label.
* Do not use like this: searchForm.ref(api.ref("Future release label")).
* Instead, set your ref once in a variable, and call it when you need it; this will allow to change the ref you're viewing easily for your entire page.
*
* @param {string} label - the ref's label
* @returns {string}
*/
ref: function(label) {
for(var i=0; i<this.data.refs.length; i++) {
if(this.data.refs[i].label == label) {
return this.data.refs[i].ref;
}
}
return null;
},
/**
* The current experiment, or null
* @returns {Experiment}
*/
currentExperiment: function() {
return this.experiments.current();
},
quickRoutesEnabled: function() {
return this.data.quickRoutes.enabled;
},
/**
* Retrieve quick routes definitions
*/
quickRoutes: function(callback) {
var self = this;
return new Promise(function(resolve, reject) {
self.requestHandler(self.data.quickRoutes.url, function(err, data, xhr) {
if (callback) callback(err, data, xhr);
if (err) reject(err);
if (data) resolve(data);
});
});
},
/**
* Query the repository
* @param {string|array|Predicate} the query itself
* @param {object} additional parameters. In NodeJS, pass the request as 'req'.
* @param {function} callback(err, response)
*/
query: function(q, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
var opts = options || {};
var form = this.form('everything');
for (var key in opts) {
form = form.set(key, options[key]);
}
// Don't override the ref if the caller specified one in the options
if (!opts['ref']) {
// Look in cookies if we have a ref (preview or experiment)
var cookieString = '';
if (this.req) { // NodeJS
cookieString = this.req.headers["cookie"] || '';
} else if (typeof window !== 'undefined') { // Browser
cookieString = window.document.cookie || '';
}
var cookies = Cookies.parse(cookieString);
var previewRef = cookies[previewCookie];
var experimentRef = this.experiments.refFromCookie(cookies[experimentCookie]);
form = form.ref(previewRef || experimentRef || this.master());
}
if (q) {
form.query(q);
}
return form.submit(callback);
},
/**
* Retrieve the document returned by the given query
* @param {string|array|Predicate} the query
* @param {object} additional parameters. In NodeJS, pass the request as 'req'.
* @param {function} callback(err, doc)
*/
queryFirst: function(q, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
var opts = {};
for (var key in (options || {})) {
opts[key] = options[key];
}
opts.page = 1;
opts.pageSize = 1;
return this.query(q, opts, function(err, response) {
if (callback) {
var result = response && response.results && response.results[0];
callback(err, result);
}
}).then(function(response){
return response && response.results && response.results[0];
});
},
/**
* Retrieve the document with the given id
* @param {string} id
* @param {object} additional parameters
* @param {function} callback(err, doc)
*/
getByID: function(id, options, callback) {
options = options || {};
if(!options.lang) options.lang = '*';
return this.queryFirst(Predicates.at('document.id', id), options, callback);
},
/**
* Retrieve multiple documents from an array of id
* @param {array} ids
* @param {object} additional parameters
* @param {function} callback(err, response)
*/
getByIDs: function(ids, options, callback) {
options = options || {};
if(!options.lang) options.lang = '*';
return this.query(['in', 'document.id', ids], options, callback);
},
/**
* Retrieve the document with the given uid
* @param {string} type the custom type of the document
* @param {string} uid
* @param {object} additional parameters
* @param {function} callback(err, response)
*/
getByUID: function(type, uid, options, callback) {
options = options || {};
if(!options.lang) options.lang = '*';
return this.queryFirst(Predicates.at('my.'+type+'.uid', uid), options, callback);
},
/**
* Retrieve the singleton document with the given type
* @param {string} type the custom type of the document
* @param {object} additional parameters
* @param {function} callback(err, response)
*/
getSingle: function(type, options, callback) {
return this.queryFirst(Predicates.at('document.type', type), options, callback);
},
/**
* Retrieve the document with the given bookmark
* @param {string} bookmark name
* @param {object} additional parameters
* @param {function} callback(err, response)
* @returns {Promise}
*/
getBookmark: function(bookmark, options, callback) {
return new Promise(function(resolve, reject) {
var id = this.bookmarks[bookmark];
if (id) {
resolve(id);
} else {
var err = new Error("Error retrieving bookmarked id");
if (callback) callback(err);
reject(err);
}
}).then(function(id) {
return this.getByID(id, options, callback);
});
},
/**
* Return the URL to display a given preview
* @param {string} token as received from Prismic server to identify the content to preview
* @param {function} linkResolver the link resolver to build URL for your site
* @param {string} defaultUrl the URL to default to return if the preview doesn't correspond to a document
* (usually the home page of your site)
* @param {function} callback to get the resulting URL (optional, you can get it from the Promise result)
* @returns {Promise}
*/
previewSession: function(token, linkResolver, defaultUrl, callback) {
var api = this;
return new Promise(function(resolve, reject) {
var cb = function(err, value, xhr) {
if (callback) callback(err, value, xhr);
if (err) {
reject(err);
} else {
resolve(value);
}
};
api.requestHandler(token, function (err, result, xhr) {
if (err) {
cb(err, defaultUrl, xhr);
return;
}
try {
var mainDocumentId = result.mainDocument;
if (!mainDocumentId) {
cb(null, defaultUrl, xhr);
} else {
api.form("everything").query(Predicates.at("document.id", mainDocumentId)).ref(token).lang('*').submit(function(err, response) {
if (err) {
cb(err);
}
try {
if (response.results.length === 0) {
cb(null, defaultUrl, xhr);
} else {
cb(null, linkResolver(response.results[0]), xhr);
}
} catch (e) {
cb(e);
}
});
}
} catch (e) {
cb(e, defaultUrl, xhr);
}
});
});
},
/**
* Fetch a URL corresponding to a query, and parse the response as a Response object
*/
request: function(url, callback) {
var api = this;
var cacheKey = url + (this.accessToken ? ('#' + this.accessToken) : '');
var cache = this.apiCache;
function run(cb) {
cache.get(cacheKey, function (err, value) {
if (err || value) {
cb(err, api.response(value));
return;
}
api.requestHandler(url, function (err, documents, xhr, ttl) {
if (err) {
cb(err, null, xhr);
return;
}
if (ttl) {
cache.set(cacheKey, documents, ttl, function (err) {
cb(err, api.response(documents));
});
} else {
cb(null, api.response(documents));
}
});
});
}
return new Promise(function(resolve, reject) {
run(function(err, value, xhr) {
if (callback) callback(err, value, xhr);
if (err) reject(err);
if (value) resolve(value);
});
});
},
getNextPage: function(nextPage, callback) {
return this.request(nextPage + (this.accessToken ? '&access_token=' + this.accessToken : ''), callback);
},
/**
* JSON documents to Response object
*/
response: function(documents){
var results = documents.results.map(parseDoc);
return new Response(
documents.page,
documents.results_per_page,
documents.results_size,
documents.total_results_size,
documents.total_pages,
documents.next_page,
documents.prev_page,
results || []);
}
};
/**
* Embodies a submittable RESTful form as described on the API endpoint (as per RESTful standards)
* @constructor
* @private
*/
function Form(name, fields, form_method, rel, enctype, action) {
this.name = name;
this.fields = fields;
this.form_method = form_method;
this.rel = rel;
this.enctype = enctype;
this.action = action;
}
Form.prototype = {};
/**
* Parse json as a document
*
* @returns {Document}
*/
var parseDoc = function(json) {
var fragments = {};
for(var field in json.data[json.type]) {
fragments[json.type + '.' + field] = json.data[json.type][field];
}
var slugs = [];
if (json.slugs !== undefined) {
for (var i = 0; i < json.slugs.length; i++) {
slugs.push(decodeURIComponent(json.slugs[i]));
}
}
return new Document(
json.id,
json.uid || null,
json.type,
json.href,
json.tags,
slugs,
json.first_publication_date,
json.last_publication_date,
json.lang,
json.alternate_languages,
fragments,
json.data
);
};
/**
* Embodies a SearchForm object. To create SearchForm objects that are allowed in the API, please use the API.form() method.
* @constructor
* @global
* @alias SearchForm
*/
function SearchForm(api, form, data) {
this.api = api;
this.form = form;
this.data = data || {};
for(var field in form.fields) {
if(form.fields[field]['default']) {
this.data[field] = [form.fields[field]['default']];
}
}
}
SearchForm.prototype = {
/**
* Set an API call parameter. This will only work if field is a valid field of the
* RESTful form in the first place (as described in the /api document); otherwise,
* an "Unknown field" error is thrown.
* Please prefer using dedicated methods like query(), orderings(), ...
*
* @param {string} field - The name of the field to set
* @param {string} value - The value that gets assigned
* @returns {SearchForm} - The SearchForm itself
*/
set: function(field, value) {
var fieldDesc = this.form.fields[field];
if(!fieldDesc) throw new Error("Unknown field " + field);
var values= this.data[field] || [];
if(value === '' || value === undefined) {
// we must compare value to null because we want to allow 0
value = null;
}
if(fieldDesc.multiple) {
if (value) values.push(value);
} else {
values = value && [value];
}
this.data[field] = values;
return this;
},
/**
* Sets a ref to query on for this SearchForm. This is a mandatory
* method to call before calling submit(), and api.form('everything').submit()
* will not work.
*
* @param {Ref} ref - The Ref object defining the ref to query
* @returns {SearchForm} - The SearchForm itself
*/
ref: function(ref) {
return this.set("ref", ref);
},
/**
* Sets a predicate-based query for this SearchForm. This is where you
* paste what you compose in your prismic.io API browser.
*
* @example form.query(Prismic.Predicates.at("document.id", "foobar"))
* @param {string|...array} query - Either a query as a string, or as many predicates as you want. See Prismic.Predicates.
* @returns {SearchForm} - The SearchForm itself
*/
query: function(query) {
if (typeof query === 'string') {
return this.set("q", query);
} else {
var predicates;
if (query.constructor === Array && query.length > 0 && query[0].constructor === Array) {
predicates = query;
} else {
predicates = [].slice.apply(arguments); // Convert to a real JS array
}
var stringQueries = [];
predicates.forEach(function (predicate) {
stringQueries.push(Predicates.toQuery(predicate));
});
return this.query("[" + stringQueries.join("") + "]");
}
},
/**
* Sets a page size to query for this SearchForm. This is an optional method.
*
* @param {number} size - The page size
* @returns {SearchForm} - The SearchForm itself
*/
pageSize: function(size) {
return this.set("pageSize", size);
},
/**
* Restrict the results document to the specified fields
*
* @param {string|array} fields - The list of fields, array or comma separated string
* @returns {SearchForm} - The SearchForm itself
*/
fetch: function(fields) {
if (fields instanceof Array) {
fields = fields.join(",");
}
return this.set("fetch", fields);
},
/**
* Include the requested fields in the DocumentLink instances in the result
*
* @param {string|array} fields - The list of fields, array or comma separated string
* @returns {SearchForm} - The SearchForm itself
*/
fetchLinks: function(fields) {
if (fields instanceof Array) {
fields = fields.join(",");
}
return this.set("fetchLinks", fields);
},
/**
* Sets the language to query for this SearchForm. This is an optional method.
*
* @param {string} fields - The language code
* @returns {SearchForm} - The SearchForm itself
*/
lang: function(fields) {
return this.set("lang", fields);
},
/**
* Sets the page number to query for this SearchForm. This is an optional method.
*
* @param {number} p - The page number
* @returns {SearchForm} - The SearchForm itself
*/
page: function(p) {
return this.set("page", p);
},
/**
* Sets the orderings to query for this SearchForm. This is an optional method.
*
* @param {array} orderings - Array of string: list of fields, optionally followed by space and desc. Example: ['my.product.price desc', 'my.product.date']
* @returns {SearchForm} - The SearchForm itself
*/
orderings: function(orderings) {
if (typeof orderings === 'string') {
// Backward compatibility
return this.set("orderings", orderings);
} else if (!orderings) {
// Noop
return this;
} else {
// Normal usage
return this.set("orderings", "[" + orderings.join(",") + "]");
}
},
/**
* Submits the query, and calls the callback function.
*
* @param {function} callback - Optional callback function that is called after the query was made,
* to which you may pass three parameters: a potential error (null if no problem),
* a Response object (containing all the pagination specifics + the array of Docs),
* and the XMLHttpRequest
*/
submit: function(callback) {
var self = this;
var url = this.form.action;
if (this.data) {
var sep = (url.indexOf('?') > -1 ? '&' : '?');
for(var key in this.data) {
if (this.data.hasOwnProperty(key)) {
var values = this.data[key];
if (values) {
for (var i = 0; i < values.length; i++) {
url += sep + key + '=' + encodeURIComponent(values[i]);
sep = '&';
}
}
}
}
}
return self.api.request(url, callback);
}
};
/**
* Embodies the response of a SearchForm query as returned by the API.
* It includes all the fields that are useful for pagination (page, total_pages, total_results_size, ...),
* as well as the field "results", which is an array of {@link Document} objects, the documents themselves.
*
* @constructor
* @global
*/
function Response(page, results_per_page, results_size, total_results_size, total_pages, next_page, prev_page, results) {
/**
* The current page
* @type {number}
*/
this.page = page;
/**
* The number of results per page
* @type {number}
*/
this.results_per_page = results_per_page;
/**
* The size of the current page
* @type {number}
*/
this.results_size = results_size;
/**
* The total size of results across all pages
* @type {number}
*/
this.total_results_size = total_results_size;
/**
* The total number of pages
* @type {number}
*/
this.total_pages = total_pages;
/**
* The URL of the next page in the API
* @type {string}
*/
this.next_page = next_page;
/**
* The URL of the previous page in the API
* @type {string}
*/
this.prev_page = prev_page;
/**
* Array of {@link Document} for the current page
* @type {Array}
*/
this.results = results;
}
/**
* Embodies a prismic.io ref (a past or future point in time you can query)
* @constructor
* @global
*/
function Ref(ref, label, isMaster, scheduledAt, id) {
/**
* @field
* @description the ID of the ref
*/
this.ref = ref;
/**
* @field
* @description the label of the ref
*/
this.label = label;
/**
* @field
* @description is true if the ref is the master ref
*/
this.isMaster = isMaster;
/**
* @field
* @description the scheduled date of the ref
*/
this.scheduledAt = scheduledAt;
/**
* @field
* @description the name of the ref
*/
this.id = id;
}
Ref.prototype = {};
function globalCache() {
var g;
if (typeof global == 'object') {
g = global; // NodeJS
} else {
g = window; // browser
}
if (!g.prismicCache) {
g.prismicCache = new ApiCache();
}
return g.prismicCache;
}
module.exports = {
experimentCookie: experimentCookie,
previewCookie: previewCookie,
Api: Api,
Form: Form,
SearchForm: SearchForm,
Ref: Ref,
parseDoc: parseDoc
};