├── .npmignore ├── .travis.yml ├── LICENSE ├── package.json ├── index.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - lts/* 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Feross Aboukhadijeh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "objection-slug", 3 | "description": "Automatically generate slugs for an Objection.js model", 4 | "version": "1.1.1", 5 | "author": { 6 | "name": "Feross Aboukhadijeh", 7 | "email": "feross@feross.org", 8 | "url": "https://feross.org" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/feross/objection-slug/issues" 12 | }, 13 | "dependencies": { 14 | "mollusc": "^2.0.0", 15 | "uuid": "^8.3.1" 16 | }, 17 | "devDependencies": { 18 | "standard": "*" 19 | }, 20 | "homepage": "https://github.com/feross/objection-slug", 21 | "keywords": [ 22 | "objection", 23 | "slug", 24 | "slugify", 25 | "url", 26 | "objection.js", 27 | "model", 28 | "objection-js", 29 | "plugin", 30 | "plugins", 31 | "objection-slug", 32 | "objection slug", 33 | "objection url" 34 | ], 35 | "license": "MIT", 36 | "main": "index.js", 37 | "repository": { 38 | "type": "git", 39 | "url": "git://github.com/feross/objection-slug.git" 40 | }, 41 | "scripts": { 42 | "test": "standard" 43 | }, 44 | "funding": [ 45 | { 46 | "type": "github", 47 | "url": "https://github.com/sponsors/feross" 48 | }, 49 | { 50 | "type": "patreon", 51 | "url": "https://www.patreon.com/feross" 52 | }, 53 | { 54 | "type": "consulting", 55 | "url": "https://feross.org/support" 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! objection-slug. MIT License. Feross Aboukhadijeh */ 2 | module.exports = objectionSlug 3 | 4 | const mollusc = require('mollusc') 5 | const { v4: uuidv4 } = require('uuid') 6 | 7 | const MOLLUSC_OPTS = { 8 | limit: 15, // limit slug to this many words 9 | charmap: { '.': '-', ...mollusc.charmap } 10 | } 11 | 12 | const MAX_SLUG_LENGTH = 75 13 | 14 | function objectionSlug (opts) { 15 | opts = { 16 | sourceField: null, 17 | slugField: 'slug', 18 | ...opts 19 | } 20 | 21 | if (!opts.sourceField || !opts.slugField) { 22 | throw new Error('You must specify `sourceField` and `slugField`.') 23 | } 24 | 25 | return Model => { 26 | return class extends Model { 27 | async $beforeInsert (context) { 28 | await super.$beforeInsert(context) 29 | 30 | const source = this[opts.sourceField] 31 | if (!source) return 32 | 33 | const slug = await this.generateSlug(source) 34 | this[opts.slugField] = slug 35 | } 36 | 37 | async generateSlug (str) { 38 | let slug = mollusc(str, MOLLUSC_OPTS) 39 | 40 | // If slug has only unicode symbols without an English equivalent, 41 | // then slug will be empty, so use a UUID. 42 | if (slug.length === 0) { 43 | slug = uuidv4().replace(/-/g, '') 44 | } 45 | 46 | // Truncate excessively long slugs 47 | if (slug.length > MAX_SLUG_LENGTH) { 48 | slug = slug.slice(0, MAX_SLUG_LENGTH) 49 | } 50 | 51 | // Ensure that the slug is unique 52 | return this.generateUniqueSlug(slug) 53 | } 54 | 55 | async generateUniqueSlug (original, current = null, count = 0) { 56 | const isUnique = await this.isUnique(current || original) 57 | if (isUnique) return current || original 58 | 59 | count += 1 60 | 61 | return this.generateUniqueSlug( 62 | original, 63 | `${original}-${count}`, 64 | count 65 | ) 66 | } 67 | 68 | async isUnique (slug) { 69 | const row = await this.constructor 70 | .query() 71 | .findOne(opts.slugField, slug) 72 | 73 | return !row 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # objection-slug [![travis][travis-image]][travis-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] 2 | 3 | [travis-image]: https://img.shields.io/travis/feross/objection-slug/master.svg 4 | [travis-url]: https://travis-ci.org/feross/objection-slug 5 | [npm-image]: https://img.shields.io/npm/v/objection-slug.svg 6 | [npm-url]: https://npmjs.org/package/objection-slug 7 | [downloads-image]: https://img.shields.io/npm/dm/objection-slug.svg 8 | [downloads-url]: https://npmjs.org/package/objection-slug 9 | [standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg 10 | [standard-url]: https://standardjs.com 11 | 12 | ### Automatically generate slugs for an [Objection.js](https://vincit.github.io/objection.js/) model 13 | 14 | This plugin will automatically generate slugs for your model based on a source 15 | field and a slug field. It will ensure that the slugs are unique by checking to 16 | see if the slug already exists in the model's table. If so, it will attempt to 17 | append a number to the end of the slug. 18 | 19 | For example, if the source field is `'How to Fry an Egg'`, then the slug will be 20 | `'how-to-fry-an-egg'`. However, if that slug already exists in the model's table 21 | then the slug will be `'how-to-fry-an-egg-1'` (note that `-1` was appended). 22 | 23 | And if ***that*** slug also exists, then the slug would be 24 | `'how-to-fry-an-egg-2'` and so on... 25 | 26 | ## Install 27 | 28 | ``` 29 | npm install objection-slug 30 | ``` 31 | 32 | ## Why this package? 33 | 34 | This package was inspired by 35 | [`objection-slugify`](https://github.com/combine/objection-slugify) but it's 36 | different in the following ways: 37 | 38 | 1. Appends a number instead of a UUID. 39 | 40 | Instead of attempting to append a UUID to the end of the slug, which does not 41 | look nice, this package appends a sequential number to the end of duplicate 42 | slugs. 43 | 44 | 2. Removed unwanted features 45 | 46 | There are several options which aren't useful and were removed. For example, 47 | instead of changing the slug when the source field changes (which breaks any 48 | URLs based on the slug, which is very bad for SEO), this package never 49 | changes the slug after it is generated. 50 | 51 | 3. Handles many more unicode symbols by default, because it uses the `mollusc` 52 | library instead of `slugify`. 53 | 54 | ## Usage 55 | 56 | ```js 57 | const objectionSlug = require('objection-slug') 58 | const { Model } = require('objection') 59 | 60 | // Create the mixin 61 | const slug = objectionSlug({ 62 | sourceField: 'title', 63 | slugField: 'slug' 64 | }) 65 | 66 | // Create the Model and add the mixin 67 | class Post extends slug(Model) { 68 | // ...code 69 | } 70 | 71 | const post = await Post 72 | .query() 73 | .insert({ title: 'How to Fry an Egg' }) 74 | 75 | console.log(post.slug) 76 | // how-to-fry-an-egg 77 | ``` 78 | 79 | ## API 80 | 81 | ### `slug = objectionSlug([opts])` 82 | 83 | Create a slug mixin to be used with 84 | [Objection.js](https://vincit.github.io/objection.js/) models. See usage example 85 | above. 86 | 87 | #### `opts.sourceField` (required) 88 | 89 | The source of the slugged content. 90 | 91 | #### `opts.slugField` (defaults to `'slug'`) 92 | 93 | The field to store the slug on. 94 | 95 | ## License 96 | 97 | MIT. Copyright (c) [Feross Aboukhadijeh](https://feross.org). 98 | --------------------------------------------------------------------------------