mongoose-slug-updater
Sophisticated slugifier plugin for Mongoose.
Features:
Installation
The best way to install it is using npm
npm install mongoose-slug-updater --save
Loading
var slug = require('mongoose-slug-updater');
Initialization
var mongoose = require('mongoose');
mongoose.plugin(slug);
Usage
This plugin is based on the idea of using the mongoose schema as the way to check the use of slug fields.
The plugin checks and updates automatically the slug field with the correct slug.
Basic Usage
If you only want to create the slug based on a simple field.
var mongoose = require('mongoose'),
slug = require('mongoose-slug-updater'),
mongoose.plugin(slug),
Schema = mongoose.Schema,
schema = new Schema({
title: String,
slug: { type: String, slug: "title" }
});
Multiple slug fields
You can add as many slug fields as you wish
var mongoose = require('mongoose'),
slug = require('mongoose-slug-updater'),
mongoose.plugin(slug),
Schema = mongoose.Schema,
schema = new Schema({
title: String,
subtitle: String,
slug: { type: String, slug: "title" },
slug2: { type: String, slug: "title" },
slug3: { type: String, slug: "subtitle" }
});
Multiple fields to create the slug
If you want, you can use more than one field in order to create a new slug field.
var mongoose = require('mongoose'),
slug = require('mongoose-slug-updater'),
mongoose.plugin(slug),
Schema = mongoose.Schema,
schema = new Schema({
title: String,
subtitle: String,
slug: { type: String, slug: ["title", "subtitle"] }
});
Unique slug field
To create a unique slug field, you must only add add the unique: true parameter in the path (also, this way the default mongo unique index gets created)
var mongoose = require('mongoose'),
slug = require('mongoose-slug-updater'),
mongoose.plugin(slug),
Schema = mongoose.Schema,
schema = new Schema({
title: String,
subtitle: String,
slug: { type: String, slug: ["title", "subtitle"], unique: true }
});
If unique
or unique_slug
is set, the plugin searches in the mongo database, and if the slug already exists in the collection, it appends to the slug a separator (default: "-") and a random string (generated with the shortid module).
MongoDB supports unique index for nested arrays elements, but he check for duplication conflict only on per-document basis, so inside document duplicate nested array's elements are still allowed.
Same logic implemented for unique slugs of nested arrays too.
example random
mongoose.model('Resource').create({
title: "Am I wrong, fallin' in love with you!",
subtitle: "tell me am I wrong, well, fallin' in love with you",
});
mongoose.model('Resource').create({
title: "Am I wrong, fallin' in love with you!",
subtitle: "tell me am I wrong, well, fallin' in love with you",
});
mongoose.model('Resource').create({
title: "Am I wrong, fallin' in love with you!",
subtitle: "tell me am I wrong, well, fallin' in love with you",
});
Alternatively you can modify this behaviour and instead of appending a random string, an incremental counter will be used. For that to happen, you must use the parameter slug_padding_size
specifying the total length of the counter:
example counter
var mongoose = require('mongoose'),
slug = require('mongoose-slug-updater'),
mongoose.plugin(slug),
Schema = mongoose.Schema,
schema = new Schema({
title: String,
subtitle: String,
slug: { type: String, slug: ["title", "subtitle"], slug_padding_size: 4, unique: true }
});
mongoose.model('Resource').create({
title: 'Am I wrong, fallin\' in love with you!',
subtitle: "tell me am I wrong, well, fallin' in love with you"
})
mongoose.model('Resource').create({
title: 'Am I wrong, fallin\' in love with you!',
subtitle: "tell me am I wrong, well, fallin' in love with you"
})
mongoose.model('Resource').create({
title: 'Am I wrong, fallin\' in love with you!',
subtitle: "tell me am I wrong, well, fallin' in love with you"
})
If you don't want to define your field as unique for some reasons, but still need slug to be unique,
you can use unique_slug:true
option instead of unique
.
This option will not cause index creation, but still will be considered by the plugin.
force_id
option will append shortId even if no duplicates were found.
This is useful for applications with high chance of concurrent modification of unique fields.
Check for conflict made by plugin is not atomic with subsequent insert/update operation,
so there is a possibility of external change of data in the moment between check and write.
If this happened, mongo will throw unique index violation error.
Chances of such case higher for counter unique mode, but with shortId this is possible too.
You can just retry operation, so plugin will check collection again and regenerate correct unique slug.
Or you can set force_id
option - this will solve the problem completely, but you will pay for this by less readabilty of your slugs, because they will always be appended with random string.
In most cases write operations not so frequent to care about possible conflicts.
note: force_id
option will also overwite unique
to the true
, and slug_padding_size
option will be ignored.
Unique slug within a group
Sometimes you only want slugs to be unique within a specific group.
This is done with the uniqueGroup
property which is an array of fields to group by:
example unique per group (using the field named 'group')
ResourceGroupedUnique = new mongoose.Schema({
title: { type: String },
subtitle: { type: String },
group: { type: String },
uniqueSlug: {
type: String,
uniqueGroup: ['group'],
slug_padding_size: 4,
slug: 'title',
index: true,
},
});
mongoose.model('ResourceGroupedUnique').create({
title: "Am I wrong, fallin' in love with you!",
subtitle: "tell me am I wrong, well, fallin' in love with you",
group: 'group 1',
});
mongoose.model('ResourceGroupedUnique').create({
title: "Am I wrong, fallin' in love with you!",
subtitle: "tell me am I wrong, well, fallin' in love with you",
group: 'group 2',
});
mongoose.model('ResourceGroupedUnique').create({
title: "Am I wrong, fallin' in love with you!",
subtitle: "tell me am I wrong, well, fallin' in love with you",
group: 'group 1',
});
mongoose.model('ResourceGroupedUnique').create({
title: "Am I wrong, fallin' in love with you!",
subtitle: "tell me am I wrong, well, fallin' in love with you",
group: 'group 2',
});
Important: you must not have a unique: true
option, but it's a good idea to have an index: true
option.
Updating slug or keeping it permanent
By default slugs will be created/updated for any related fields changed by any of create
(it's actually a save
too), save
, update
, updateOne
, updateMany
and findOneAndUpdate
operations.
You can specify which of supported methods should be watched:
const HooksSchema = new mongoose.Schema({
title: { type: String },
slug: {
type: String,
slug: 'title',
},
slugNoSave: { type: String, slug: 'title', on: { save: false } },
slugNoUpdate: { type: String, slug: 'title', on: { update: false } },
slugNoUpdateOne: { type: String, slug: 'title', on: { updateOne: false } },
slugNoUpdateMany: {
type: String,
slug: 'title',
on: { updateMany: false },
},
slugNoFindOneAndUpdate: {
type: String,
slug: 'title',
on: { findOneAndUpdate: false },
},
});
Note, that flags will affect both creation and updating of documents.
If you disabled save
and still want slug to be generated initially, create
method will not work,
becacuse mongoose emits save
event both for save
and create
methods.
Use upsert
option of update***
methods instead.
For update
and updateMany
methods multiply affected records also handled, but be careful with performance,
because one-by-one iteration over affected documents may happen in case of unique slugs.
In this case _id
field is required.
For update*
family of operations additional queries may be performed, to retrieve data missing in the query (fields not listed in the query but needed for compound or grouped unique slugs).
permanent
option
If you want to generate slug initially, but keep it unchanged during further modifications of related fields, use permanent
flag like this:
ResourcePermanent = new mongoose.Schema({
title: { type: String },
subtitle: { type: String },
otherField: { type: String },
slug: { type: String, slug: ['title', 'subtitle'] },
titleSlug: { type: String, slug: 'title', permanent: true },
subtitleSlug: {
type: String,
slug: 'subtitle',
permanent: true,
slug_padding_size: 4,
},
});
Nested docs. Relative and absolute paths.
Nested docs and arrays declared inline right in the scheme or as a nested schemas declared separately are also supported.
Non-unique nested slugs are currently implemented. Unique nested slugs are coming soon.
Slug fields can be declared as relative or absolute(starting with slash) path to any point of current document.
Since MongoDB uses dot path notation, colon :
symbol used for relative paths as a reference to the parent, same as double dot ..
for file system paths.
Example of scheme with inline nested docs:
const InlineSchema = new mongoose.Schema({
title: { type: String },
slug: { type: String, slug: 'title' },
absoluteSlug: { type: String, slug: '/title' },
childSlug: { type: String, slug: 'child.title' },
absoluteChildSlug: { type: String, slug: '/child.title' },
subChildSlug: { type: String, slug: 'child.subChild.title' },
childrenSlug0: { type: String, slug: 'children.0.title' },
childrenSlug4: { type: String, slug: 'children.4.title' },
subChildrenSlug3: { type: String, slug: 'children.0.subChildren.3.title' },
subChildrenSlug7: { type: String, slug: 'children.0.subChildren.7.title' },
subChildrenSlug5SubChild: {
type: String,
slug: 'children.0.subChildren.5.subChild.title',
},
subChildrenSlug2SubChild: {
type: String,
slug: 'children.0.subChildren.2.subChild.title',
},
child: {
title: { type: String },
slug: { type: String, slug: 'title' },
absoluteSlug: { type: String, slug: '/child.title' },
absoluteParentSlug: { type: String, slug: '/title' },
relativeParentSlug: { type: String, slug: ':title' },
subChild: {
title: { type: String },
slug: { type: String, slug: 'title' },
absoluteParentSlug: { type: String, slug: '/title' },
relativeParentSlug: { type: String, slug: ':title' },
relativeGrandParentSlug: { type: String, slug: '::title' },
},
},
children: [
{
title: { type: String },
slug: { type: String, slug: 'title' },
absoluteRootSlug: { type: String, slug: '/title' },
absoluteChildSlug: { type: String, slug: '/child.title' },
relativeRootSlug: { type: String, slug: ':title' },
absoluteSiblingSlug: { type: String, slug: '/children.3.title' },
relativeSiblingSlug: { type: String, slug: ':children.4.title' },
subChild: {
title: { type: String },
slug: { type: String, slug: 'title' },
absoluteParentSlug: { type: String, slug: '/title' },
absoluteChildSlug: { type: String, slug: '/child.title' },
relativeParentSlug: { type: String, slug: ':title' },
relativeGrandParentSlug: { type: String, slug: '::title' },
},
subChildren: [
{
title: { type: String },
slug: { type: String, slug: 'title' },
absoluteRootSlug: { type: String, slug: '/title' },
absoluteChildSlug: { type: String, slug: '/child.title' },
relativeRootSlug: { type: String, slug: '::title' },
absoluteSiblingSlug: {
type: String,
slug: '/children.0.subChildren.5.title',
},
relativeSiblingSlug: { type: String, slug: ':subChildren.6.title' },
subChild: {
title: { type: String },
slug: { type: String, slug: 'title' },
absoluteParentSlug: { type: String, slug: '/title' },
absoluteChildSlug: { type: String, slug: '/child.title' },
relativeParentSlug: { type: String, slug: ':title' },
relativeGrandParentSlug: { type: String, slug: '::title' },
},
},
],
},
],
});
Example of nested schemas declared separately:
const SubChildSchema = new mongoose.Schema({
title: { type: String },
slug: { type: String, slug: 'title' },
absoluteRootSlug: { type: String, slug: '/title' },
absoluteChildSlug: { type: String, slug: '/child.title' },
relativeParentSlug: { type: String, slug: ':title' },
relativeGrandParentSlug: { type: String, slug: '::title' },
});
const ChildSchema = new mongoose.Schema({
title: { type: String },
subChild: SubChildSchema,
subChildren: [SubChildSchema],
slug: { type: String, slug: 'title' },
subChildSlug: { type: String, slug: 'subChild.title' },
absoluteSlug: { type: String, slug: '/child.title' },
absoluteRootSlug: { type: String, slug: '/title' },
relativeParentSlug: { type: String, slug: ':title' },
subChildrenSlug2: { type: String, slug: 'subChildren.2.title' },
subChildrenSlug3: { type: String, slug: 'subChildren.3.title' },
});
const ParentSchema = new mongoose.Schema({
title: { type: String },
child: ChildSchema,
children: [ChildSchema],
slug: { type: String, slug: 'title' },
absoluteSlug: { type: String, slug: '/title' },
childSlug: { type: String, slug: 'child.title' },
absoluteChildSlug: { type: String, slug: '/child.title' },
subChildSlug: { type: String, slug: 'child.subChild.title' },
childrenSlug0: { type: String, slug: 'children.0.title' },
childrenSlug4: { type: String, slug: 'children.4.title' },
subChildrenSlug3: { type: String, slug: 'children.7.subChildren.3.title' },
subChildrenSlug7: { type: String, slug: 'children.3.subChildren.7.title' },
});
Updating by deep path via $set operator
This will work too:
await SimpleInline.findOneAndUpdate(
{},
{
$set: {
title: 'New root title',
'child.title': 'New nested title',
'children.2.title': 'New title for the 3d item of nested array',
},
}
);
All the slugs which depend on modified titles will be found and regenerated.
This is recommended way to do partial modifications.
When you perform updates by object value instead of path:value list,
unobvious data loss may happen for nested docs or arrays, if they contain slugs affected by your modification.
Plugin always checks will current update operation be made with $set operator or not, and adds extra slug fields to the query as an object fields or $set paths accordingly.
So if you do have whole document you want to change - better use save
,
but if you dont have it, but you need to update some particular fields - it's more safe to use $set and paths:values.
Choose your own options
You can change any options adding to the plugin
var mongoose = require('mongoose'),
slug = require('mongoose-slug-updater'),
options = {
separator: "-",
lang: "en",
truncate: 120
},
mongoose.plugin(slug, options),
Schema = mongoose.Schema,
schema = new Schema({
title: String,
subtitle: String,
slug: { type: String, slug: ["title", "subtitle"], unique: true }
});
You can find more options in the speakingURL's npm page
Support
This plugin is supported by Yuri Gor
About
This plugin was initially forked from mongoose-slug-generator, which is not maintained currently.
Merged and fixed uniqueGroup
feature by rickogden.
update
, updateOne
, updateMany
and findOneAndUpdate
operations support implemented.
Nested docs and arrays support implemented.
Absolute and relative paths added.
Updating with $set operator and deep paths now works too.
All the update operators will be implemented soon.
Plugin rewritten with modern js and a lot of tests were added.