Как создать уникальный слаг в ватерлинии перед обратным вызовом 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). Это также не заботится о типе базы данных, которую вы используете.

Это может вызвать проблемы, если случайно два процесса напишут один и тот же слаг в один и тот же момент, но это должно произойти в течение нескольких миллисекунд. И я думаю, что единственный способ предотвратить это - каким-то образом заблокировать стол - и я справлюсь с этим, если мне повезет с этой проблемой...

Другие вопросы по тегам