'use strict';
/**
* Model for short human-readable identifiers used in URLs pointing to review
* subjects ({@link Thing} objects). RethinkDB only enforces uniqueness for
* primary keys, so we keep these in a separate table.
*
* This model is not versioned.
*
* @namespace ThingSlug
*/
const thinky = require('../db');
const r = thinky.r;
const type = thinky.type;
// You can use these slugs, but they'll be automatically be qualified with a number
const reservedSlugs = ['register', 'actions', 'signin', 'login', 'teams', 'user', 'new', 'signout', 'logout', 'api', 'faq', 'static', 'terms'];
// Model for unique, human-readable identifiers ('slugs') for review subjects
// ('things')
let thingSlugSchema = {
name: type.string().max(125),
baseName: type.string().max(100),
qualifierPart: type.string().max(25),
thingID: type.string().uuid(4),
createdOn: type.date(),
createdBy: type.string().uuid(4)
};
let ThingSlug = thinky.createModel("thing_slugs", thingSlugSchema, {
pk: 'name'
});
ThingSlug.ensureIndex("createdOn");
// NOTE: INSTANCE METHODS ------------------------------------------------------
ThingSlug.define("qualifiedSave", qualifiedSave);
/**
* Save a slug update, adding a numeric qualifier if necessary because we
* already have a slug pointing to a different thing.
*
* @returns {ThingSlug}
* the slug that should be associated with the {@link Thing} object.
* @memberof ThingSlug
* @instance
*/
async function qualifiedSave() {
let slug = this;
slug.baseName = slug.name; // Store base name for later reference
if (reservedSlugs.indexOf(slug.name.toLowerCase()) !== -1)
return await _resolveConflicts(); // saves new slug if needed
try {
return await slug.save();
} catch (error) {
if (error.name == 'DuplicatePrimaryKeyError')
return await _resolveConflicts(); // saves new slug if needed
else
throw error;
}
/**
* Resolves naming conflicts by creating a new slug with a numeric qualifier
* if needed.
*
* @memberof ThingSlug
* @returns {ThingSlug}
* the best available slug to use
* @inner
* @protected
*/
async function _resolveConflicts() {
// Check first if we've used this base name before for the same target
let slugs = await ThingSlug
.filter({
baseName: slug.name,
thingID: slug.thingID
})
.orderBy(r.desc('createdOn'))
.limit(1);
if (slugs.length)
return slugs[0]; // Got a match, no need to save -- just re-use :)
// Widen search for most recent use of this base name
slugs = await ThingSlug
.filter({
baseName: slug.name
})
.orderBy(r.desc('createdOn'))
.limit(1);
let latestQualifierStr;
if (slugs.length && !isNaN(+slugs[0].qualifierPart))
latestQualifierStr = String(+slugs[0].qualifierPart + 1);
else
latestQualifierStr = '2';
slug.name = `${slug.name}-${latestQualifierStr}`;
slug.qualifierPart = latestQualifierStr;
return await slug.save();
}
}
module.exports = ThingSlug;