Как создать уникальный слаг в ватерлинии перед обратным вызовом Validate?
Проблема: я использую Waterline как ORM с OrientDB в приложении NodeJS. OrientDB использует числовые идентификаторы, поэтому я не хочу, чтобы они появлялись в моих URL при получении сообщения. Зачем? Потому что это позволит легко запрашивать полные данные, просто увеличивая ID.
Решение: создание уникального слизняка.
Вопрос: Как этого можно достичь в Waterline с асинхронными обратными вызовами? Мне нужно что-то подобное, но я не могу найти решение. Поток, вероятно, так:
- Создать слизняк
- Проверьте, существует ли сообщение с слагом
- Если нет, продолжите проверку
- Если да, измените слизняк и начните сначала
2 ответа
Ниже приведено оптимистическое решение, которое я сейчас использую:
Мой вспомогательный класс:
// databaseExtensions.js
var _ = require('lodash');
function getUnique(property, value, separator, criteria){
separator = separator || '-';
criteria = criteria || {};
var searchObject = {};
searchObject[property] = { like: value + '%' };
_.mergeDefaults(searchObject, criteria);
return this.find(searchObject)
.then(function(models){
if(!models || models.length === 0)
return value; // value is unique
var values = _.pluck(models, property);
return getUniqueFromArray(values, value, separator);
});
}
function getUniqueFromArray(existingValues, newValue, separator){
var valuesArray = _.clone(existingValues);
var version = 2; // starting version
var currentValue = newValue;
var unique;
while(version < 10000) { //just to be safe and we don't end up in a infinite loop
unique = true;
for(var i=0; i<valuesArray.length; i++){
if(currentValue === valuesArray[i]){
unique = false;
valuesArray.splice(i, 1);
break;
}
}
if (unique) {
return currentValue;
}
currentValue = newValue + separator + version;
version++;
}
}
module.exports.getUnique = getUnique;
module.exports.getUniqueFromArray = getUniqueFromArray;
Мое определение модели:
// post.model.js
{
//..
atributes: {
//...
urlSlug : {
type : 'string',
required : true,
index : true
}
},
},
getUnique: require('path/to/databaseExtensions');.getUnique
}
В моем контроллере:
// post.controller.js
var slug = require('slug');
slug.defaults.mode ='pretty';
Post.getUnique('urlSlug', slug(post.title).toLowerCase(), '-')
.then(function(uniqueSlug) {
console.log('A new unique slug:', uniqueSlug);
// assuming inserting title 'title', the results would be
// title, title-2, title-3, etc
});
В моем случае столкновения маловероятны, поэтому я не слишком беспокоюсь о проблемах параллелизма, когда две модели появляются одновременно с одинаковым названием. Но это может быть проблемой в контексте сотен тысяч пользователей, создающих сообщения.
Дайте мне знать, если это не поможет.
Я придумал собственное решение с использованием асинхронного. В конце я решил не использовать уникальный слаг в качестве своего идентификатора, а теперь использую комбинацию случайной строки, которую я называю hash_id, и слага, который не должен быть уникальным и предназначен только для SEO. Но этот ответ содержит решение и для уникальных слизней. Так что мои URL имеют такой формат:
http://example.com/posts/23hlj2l2/i_am_a_slug or
http://example.com/posts/:hash_id/:slug
Я создал вспомогательный модуль для преобразования / создания строки. Они просто имеют дело с этим и ничего не знают об ORM или о том, является ли значение уникальным.
Модуль ModelHelpers экспортирует два метода, один для нормализации ввода (например, заголовка) для создания слага. Он принимает необязательный параметр, который представляет собой число, которое будет добавлено в конец слага.
Второй метод создает случайную буквенно-цифровую строку. Вы можете передать параметр для длины строки.
var ModelHelpers = function() {
// Init
}
ModelHelpers.prototype.createSlugString = function(input_string, added_number) {
added_number = typeof added_number !== 'undefined' ? added_number : '';
// First replace all whitespaces and '-' and make sure there are no double _
var clean_string = input_string.replace(/[\s\n\-]+/g, '_').replace(/_{2,}/g, '_');
// Replace Umlaute and make lowercase
clean_string = clean_string.toLowerCase().replace(/ä/ig, 'ae').replace(/ö/ig, 'oe').replace(/ü/ig, 'ue');
// Replace any special characters and _ at the beginning or end
clean_string = clean_string.replace(/[^\w]/g, '').replace(/^_+|_$/g, '');
// Only return the first 8 words
clean_string = clean_string.split("_").slice(0,8).join("_");
// Add number if needed
if(added_number !== '') {
clean_string = clean_string + '_' + added_number.toString();
}
return clean_string;
}
ModelHelpers.prototype.makeHashID = function(hash_length)
{
hash_length = typeof hash_length !== 'undefined' ? hash_length : 10;
var text = "";
var possible = "abcdefghijklmnopqrstuvwxyz0123456789";
for( var i=0; i < hash_length; i++ ) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
module.exports = ModelHelpers;
Следующая часть моего решения - использование обратного вызова жизненного цикла Waterlines. beforeValidate
в комбинации с async
, Таким образом, я могу установить для поля slug или id хеша уникальное значение, и оно будет создано до проверки Waterline. Async - очень мощный инструмент, и я могу только рекомендовать изучить его. Я использую метод whilst
:
пока (test, fn, callback)
Повторно вызывайте fn, пока test возвращает true. Вызывает обратный вызов при остановке или возникновении ошибки.
Я создал две версии: одну, если вам нужна случайная строка (hash_id), и одну, если вы хотите добавить число в конец вашего слага, если оно не уникально.
Для хеш-идентификаторов:
var Waterline = require('Waterline');
var orientAdapter = require('sails-orientdb');
var ModelHelpers = require('../modules/model-helpers');
var async = require('async');
var mh = new ModelHelpers();
var Post = Waterline.Collection.extend({
identity: 'post',
connection: 'myLocalOrient',
attributes: {
text: {
type: 'text',
required: true
},
slug: {
type: 'string'
},
hash_id: {
type: 'string',
unique: true
}
},
// Lifecycle Callbacks
beforeValidate: function(values, next) {
var model_self = this;
var keep_running = true;
// Create first slug
values.hash_id = mh.makeHashID();
values.slug = mh.createSlugString(values.text);
async.whilst(
function () {
// execute whilst while other post has not been retrieved or while it matches a hash_id
// in the database
return keep_running;
},
function (callback) {
// search for post with this hash_id
model_self.findOne().where({hash_id: values.hash_id}).then(function(op) {
if(op === undefined) {
// Nothing found, stop executing
keep_running = false;
} else {
// Create new hash_id
values.hash_id = mh.makeHashID();
}
callback();
});
},
function (err) {
// End the process
// next(); is the callback of Waterlines' beforeValidate
next();
}
); // End whilst
}
});
module.exports = Post;
Для уникальных слизней:
var Waterline = require('Waterline');
var orientAdapter = require('sails-orientdb');
var ModelHelpers = require('../modules/model-helpers');
var async = require('async');
var mh = new ModelHelpers();
var Post = Waterline.Collection.extend({
identity: 'post',
connection: 'myLocalOrient',
attributes: {
text: {
type: 'text',
required: true
},
slug: {
type: 'string',
unique: true
},
hash_id: {
type: 'string'
}
},
// Lifecycle Callbacks
beforeValidate: function(values, next) {
var model_self = this;
var keep_running = true;
var counter = 0; // we use this to add a number
// Create first slug
values.hash_id = mh.makeHashID();
values.slug = mh.createSlugString(values.text);
async.whilst(
function () {
// execute whilst while other post has not been retrieved or while it matches a slug
// in the database
return keep_running;
},
function (callback) {
counter++;
// search for post with this slug
model_self.findOne().where({slug: values.slug}).then(function(op) {
if(op === undefined) {
// Nothing found, stop executing
keep_running = false;
} else {
// Create new slug
values.slug = mh.createSlugString(values.text, counter);
}
callback();
});
},
function (err) {
// End the test
next();
}
); // End whilst
}
});
module.exports = Post;
Преимущество этого метода заключается в том, что он просто продолжает работать до тех пор, пока не найдет уникальный slug / hash_id, и ему не нужны пропуски между числами (если slug_2 существует, но не slug_1). Это также не заботится о типе базы данных, которую вы используете.
Это может вызвать проблемы, если случайно два процесса напишут один и тот же слаг в один и тот же момент, но это должно произойти в течение нескольких миллисекунд. И я думаю, что единственный способ предотвратить это - каким-то образом заблокировать стол - и я справлюсь с этим, если мне повезет с этой проблемой...