NestboltNestbolt

@nestbolt/sluggable

Mixin

Add getSlug(), findBySlug(), and regenerateSlug() methods directly on your entity instances.

SluggableMixin adds convenience methods to your entity class so you can work with slugs directly on entity instances, without injecting SluggableService.

Basic Usage

Extend your entity from SluggableMixin():

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
import { Sluggable, SluggableMixin } from "@nestbolt/sluggable";

@Sluggable({ from: "title", slugField: "permalink" })
@Entity("articles")
export class Article extends SluggableMixin(class {
  id!: string;
  title!: string;
  permalink!: string;
}) {
  @PrimaryGeneratedColumn("uuid")
  declare id: string;

  @Column()
  declare title: string;

  @Column({ default: "" })
  declare permalink: string;
}

The base class passed to SluggableMixin() declares the shape of your entity fields. Use declare in the extending class to redeclare the TypeORM-decorated columns.

Methods

getSlug()

Returns the current slug value from the entity:

const article = await repo.findOneBy({ id: "..." });
console.log(article.getSlug()); // "my-article-title"

Uses the slugField from the @Sluggable() metadata. Defaults to "slug" if no metadata is found.

getSlugField()

Returns the name of the slug column:

const article = new Article();
console.log(article.getSlugField()); // "permalink"

findBySlug(slug)

Finds an entity of the same type by slug:

const article = new Article();
const found = await article.findBySlug("my-article-title");
// Article | null

This method requires SluggableModule to be initialized. If called before module initialization, it throws SluggableNotInitializedException.

regenerateSlug()

Regenerates the slug from the current source field values and updates the entity:

const article = await repo.findOneBy({ id: "..." });
article.title = "Updated Title";
const newSlug = await article.regenerateSlug();
console.log(newSlug); // "updated-title"
console.log(article.permalink); // "updated-title"

The new slug is both returned and set on the entity. Uniqueness is enforced if unique: true in the decorator.

Composing with Other Mixins

SluggableMixin composes with other mixins like HasMediaMixin from @nestbolt/medialibrary:

import { HasMedia, HasMediaMixin } from "@nestbolt/medialibrary";
import { Sluggable, SluggableMixin } from "@nestbolt/sluggable";

@Sluggable({ from: "name" })
@HasMedia({ modelType: "Product" })
@Entity("products")
export class Product extends SluggableMixin(
  HasMediaMixin(class {
    id!: string;
  }),
) {
  @PrimaryGeneratedColumn("uuid")
  declare id: string;

  @Column()
  name!: string;

  @Column({ default: "" })
  slug!: string;
}

// Product now has both media methods and slug methods:
// product.getSlug(), product.findBySlug(), product.regenerateSlug()
// product.addMedia(), product.getMedia(), product.getFirstMediaUrl()

Without the Decorator

If you use SluggableMixin without the @Sluggable() decorator, the methods still work with defaults:

  • getSlug() reads from the "slug" field
  • getSlugField() returns "slug"
  • regenerateSlug() returns "" (no source fields to generate from)

Error Handling

If SluggableModule has not been initialized when you call findBySlug() or regenerateSlug(), a SluggableNotInitializedException is thrown:

import { SluggableNotInitializedException } from "@nestbolt/sluggable";

try {
  await article.findBySlug("test");
} catch (error) {
  if (error instanceof SluggableNotInitializedException) {
    console.error("SluggableModule has not been initialized");
  }
}