@adonisjs/lucid-slugify
Version:
Generate and persist unique slugs in your database via Lucid models
164 lines (158 loc) • 6.41 kB
JavaScript
var __defProp = Object.defineProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/errors.ts
var errors_exports = {};
__export(errors_exports, {
E_UNSUPPORTED_DB_DIALECT: () => E_UNSUPPORTED_DB_DIALECT
});
import { createError } from "@adonisjs/core/exceptions";
var E_UNSUPPORTED_DB_DIALECT = createError(
'%s dialect is not supported by the "dbIncrement" strategy',
"E_UNSUPPORTED_DB_DIALECT"
);
// src/strategies/short_id.ts
import { nanoid } from "nanoid";
var ShortIdStrategy = class {
#config;
maxLengthBuffer = 11;
constructor(config) {
this.#config = { separator: "-", ...config };
}
/**
* Converts an existing slug to a unique slug by appending
* nanoid to it
*/
async makeSlugUnique(_, __, slug) {
return `${slug}${this.#config.separator}${nanoid(this.maxLengthBuffer - 1)}`;
}
};
// src/debug.ts
import { debuglog } from "node:util";
var debug_default = debuglog("adonisjs:lucid-slugify");
// src/strategies/db_increment.ts
var DbIncrementStrategy = class {
#config;
maxLengthBuffer = 3;
constructor(config) {
this.#config = { separator: "-", ...config };
}
/**
* Returns the counter from the rows fetched from the database by extracting
* the counter from the slug value. We do it in JavaScript since certain
* database dialects do not support Regular expressions.
*/
#getCounter(slug, columnName, rows) {
const slugCounter = rows.reduce((result, row) => {
const tokens = row[columnName].toLowerCase().split(`${slug}${this.#config.separator}`);
if (tokens.length === 2) {
const counter = Number(tokens[1]);
if (!Number.isNaN(counter)) {
result = result.concat(counter);
}
}
return result;
}, []);
return slugCounter.length ? Math.max(...slugCounter) : 0;
}
/**
* Makes the unique slug for the SQLite database
*/
async #getSlugForSqlite(model, client, columnName, slugValue) {
const result = await client.modelQuery(model).select(columnName).whereRaw("lower(??) = ?", [columnName, slugValue]).orWhereRaw("lower(??) like ?", [columnName, `${slugValue}${this.#config.separator}%`]).pojo().exec();
debug_default('sqlite: rows match slug "%s": %O', slugValue, result);
if (!result.length) {
return slugValue;
}
const counter = this.#getCounter(slugValue, columnName, result);
return `${slugValue}${this.#config.separator}${counter + 1}`;
}
/**
* Returns a unique slug for MYSQL >= 8.0 (aka 5.7)
*/
async #getSlugForOldMysql(model, client, columnName, slugValue) {
const result = await client.modelQuery(model).select(columnName).where(columnName, slugValue).orWhereRaw(`?? REGEXP ?`, [columnName, `^${slugValue}(${this.#config.separator}[0-9]*)?$`]).pojo().exec();
debug_default('mysql 5.7: rows match slug "%s": %O', slugValue, result);
if (!result.length) {
return slugValue;
}
const counter = this.#getCounter(slugValue, columnName, result);
return `${slugValue}${this.#config.separator}${counter + 1}`;
}
/**
* Returns a unique slug for MYSQL >= 8.0
*/
async #getSlugForMysql(model, client, columnName, slugValue) {
const result = await client.modelQuery(model).select(
client.raw(
`CAST(REGEXP_SUBSTR(${columnName}, '[0-9]+$') AS UNSIGNED) as lucid_slugify_counter`
)
).whereRaw(`?? REGEXP ?`, [columnName, `^${slugValue}(${this.#config.separator}[0-9]*)?$`]).orderBy("lucid_slugify_counter", "desc").pojo().exec();
debug_default('mysql: matching slug counters "%s": %O', slugValue, result);
if (!result.length) {
return slugValue;
}
const counter = result.find((row) => row.lucid_slugify_counter)?.lucid_slugify_counter ?? 0;
return `${slugValue}${this.#config.separator}${counter + 1}`;
}
/**
* Returns a unique slug for MSSQL.
*/
async #getSlugForMssql(model, client, columnName, slugValue) {
const result = await client.modelQuery(model).select(columnName).where(columnName, slugValue).orWhere(columnName, "like", `${slugValue}${this.#config.separator}%`).pojo().exec();
debug_default('mssql 5.7: rows match slug "%s": %O', slugValue, result);
if (!result.length) {
return slugValue;
}
const counter = this.#getCounter(slugValue, columnName, result);
return `${slugValue}${this.#config.separator}${counter + 1}`;
}
/**
* Makes slug for PostgreSQL and RedShift both. Redshift is not tested and
* assumed to be compatible with PostgreSQL.
*/
async #getSlugForPg(model, client, columnName, slugValue) {
const result = await client.modelQuery(model).select(
client.raw(`SUBSTRING(${columnName} from '[0-9]+$')::INTEGER as lucid_slugify_counter`)
).whereRaw(`?? ~* ?`, [columnName, `^${slugValue}(${this.#config.separator}[0-9]*)?$`]).orderBy("lucid_slugify_counter", "desc").limit(2).pojo().exec();
debug_default('pg: matching slug counters "%s": %O', slugValue, result);
if (!result.length) {
return slugValue;
}
const counter = result.find((row) => row.lucid_slugify_counter)?.lucid_slugify_counter ?? 0;
return `${slugValue}${this.#config.separator}${counter + 1}`;
}
/**
* Converts an existing slug to a unique slug by inspecting the database
*/
async makeSlugUnique(modelInstance, field, slug) {
const model = modelInstance.constructor;
const client = model.$adapter.modelClient(modelInstance);
const columnName = model.$columnsDefinitions.get(field).columnName;
const dialect = client.dialect;
const dialectName = dialect.name;
const dialectVersion = Number(dialect.version);
switch (dialectName) {
case "postgres":
case "redshift":
return this.#getSlugForPg(model, client, columnName, slug);
case "sqlite3":
case "better-sqlite3":
return this.#getSlugForSqlite(model, client, columnName, slug);
case "mysql":
return dialectVersion < 8 ? this.#getSlugForOldMysql(model, client, columnName, slug) : this.#getSlugForMysql(model, client, columnName, slug);
case "mssql":
return this.#getSlugForMssql(model, client, columnName, slug);
default:
throw new E_UNSUPPORTED_DB_DIALECT([dialectName]);
}
}
};
export {
errors_exports,
debug_default,
ShortIdStrategy,
DbIncrementStrategy
};