@aginix/adonis-object-id
v0.1.1
Published
Stable string identifiers for AdonisJS Lucid models — reference any record by a namespaced key instead of its auto-increment id
Maintainers
Readme
@aginix/adonis-object-id
Stable string identifiers for AdonisJS Lucid models.
Master-data rows usually have auto-incremented IDs, which makes them hard to
reference from code, seeders, or cross-environment imports — the ID changes
depending on insert order. Adding a code / slug / reference column to
every table that needs one is repetitive and rigid. This package solves the
problem with a single polymorphic object_ids table that maps a string
reference ("namespace.name") to a record in any model.
Features
- One table, every model. No per-table boilerplate.
- Mixin that adds object_id utilities to any Lucid model.
- Static service for cross-cutting access (seeders, scripts).
- Cascade delete of references when a row is removed (opt-out).
- Ace command to scaffold the table migration.
Install
npm install @aginix/adonis-object-id
node ace configure @aginix/adonis-object-id
node ace migration:runThe configure hook registers the package's provider and drops a
timestamped migration into database/migrations that creates the
object_ids table. Re-running configure is safe — it detects an
existing *_create_object_ids_table.ts file and skips regeneration.
Reference format
References are strings of the form namespace.name (e.g. app.admin_user).
The namespace is required for clarity but defaults to app when omitted, so
"admin_user" is normalized to "app.admin_user". Only the first . is
treated as a separator — web.editor.iframe parses as namespace=web,
name=editor.iframe.
Usage — HasObjectId mixin
Compose the mixin alongside BaseModel:
import { compose } from '@adonisjs/core/helpers'
import { BaseModel, column } from '@adonisjs/lucid/orm'
import { HasObjectId } from '@aginix/adonis-object-id'
export default class Faculty extends compose(BaseModel, HasObjectId) {
@column({ isPrimary: true })
declare id: number
@column()
declare name: string
// Optional. Defaults to the Lucid table name ("faculties" here).
// static objectIdModelKey = 'school.faculty'
// Optional. Default true — wipes orphan references on delete.
// static cascadeObjectIdsOnDelete = false
}Static methods
// Create a row AND bind a reference to it atomically. Equivalent to
// `Faculty.create({...})` + `faculty.assignObjectId('...')`, but
// wrapped in a transaction so a duplicate-reference error rolls the
// new row back instead of leaving it orphaned. Pass `{ client: trx }`
// to piggyback on an existing transaction.
const faculty = await Faculty.createWithObjectId(
{ name: 'Engineering' },
'app.engineering_faculty'
)
// Resolve a reference to a row of this model (null if missing /
// pointing at a different model / row deleted)
const found = await Faculty.findByObjectId('app.engineering_faculty')
// Same but throws ObjectIdNotFoundError on miss
const required = await Faculty.findByObjectIdOrFail('app.engineering_faculty')
// Just the primary-key value — useful for FK wiring in seeders
const id = await Faculty.refObjectId('app.engineering_faculty')
const idStrict = await Faculty.refObjectIdOrFail('app.engineering_faculty')Instance methods
const faculty = await Faculty.create({ name: 'Engineering' })
// Bind a reference (upsert; idempotent if it already points here)
await faculty.assignObjectId('app.engineering_faculty')
// Read back
await faculty.getObjectId() // "app.engineering_faculty"
await faculty.getObjectIds() // ["app.engineering_faculty"]
// Add another reference (e.g. legacy alias)
await faculty.assignObjectId('legacy.fac_eng')
// Rename a reference that points at this row
await faculty.renameObjectId('legacy.fac_eng', 'archive.fac_eng')
// Remove a specific reference (only if it points here)
await faculty.removeObjectId('archive.fac_eng')
// Remove every reference for this row
await faculty.removeAllObjectIds()Usage — ObjectIds service
Use the static service when you don't want to mix in the model (e.g. in
seeders, scripts, or when the model has no HasObjectId):
import { ObjectIds } from '@aginix/adonis-object-id'
import Faculty from '#models/faculty'
// Resolve a reference to its (model, recordId) target
const target = await ObjectIds.ref('app.engineering_faculty')
// => { model: 'faculties', recordId: '42' } | null
// Throws on miss
const target2 = await ObjectIds.refOrFail('app.engineering_faculty')
// Resolve directly to a row of a given model
const faculty = await ObjectIds.find(Faculty, 'app.engineering_faculty')
const facultyStrict = await ObjectIds.findOrFail(Faculty, 'app.engineering_faculty')
// Assign a reference. Accepts either a Lucid row or an explicit target.
await ObjectIds.assign(faculty, 'app.engineering_faculty')
await ObjectIds.assign({ model: 'faculties', recordId: 42 }, 'app.engineering_faculty')
// Remove a reference
await ObjectIds.remove('app.engineering_faculty')
// Remove every reference for a target (called automatically by the
// mixin's beforeDelete hook)
await ObjectIds.removeAllFor(faculty)
// List every reference pointing at a target
await ObjectIds.listFor(faculty) // => ["app.engineering_faculty", ...]All service methods accept an optional { connection?, client? } argument
to override the database connection or piggyback on an open transaction.
Seeders
Object_ids shine in seeders — bind every master record to a stable reference once, then look up by reference everywhere else:
// database/seeders/01_faculties.ts
import { BaseSeeder } from '@adonisjs/lucid/seeders'
import Faculty from '#models/faculty'
export default class extends BaseSeeder {
async run() {
const engineering = await Faculty.updateOrCreate(
{ name: 'Engineering' },
{ name: 'Engineering' }
)
await engineering.assignObjectId('seed.faculty_engineering')
const science = await Faculty.updateOrCreate({ name: 'Science' }, { name: 'Science' })
await science.assignObjectId('seed.faculty_science')
}
}// database/seeders/02_courses.ts
import { BaseSeeder } from '@adonisjs/lucid/seeders'
import Course from '#models/course'
import Faculty from '#models/faculty'
export default class extends BaseSeeder {
async run() {
const facultyId = await Faculty.refObjectIdOrFail('seed.faculty_engineering')
await Course.create({ name: 'CS 101', facultyId: Number(facultyId) })
}
}The second seeder no longer cares about insertion order or auto-IDs.
Backfilling existing data
Adding object_ids to a record that already exists is a one-liner — they're stored in a separate table, so no schema change is needed:
const admin = await User.findByOrFail('email', '[email protected]')
await admin.assignObjectId('app.admin_user')Model key
The model column in object_ids defaults to the consumer model's Lucid
table name. This is stable across class renames but changes if you
rename the table — override it with static objectIdModelKey if you want
a portable key:
class Faculty extends compose(BaseModel, HasObjectId) {
static objectIdModelKey = 'school.faculty'
}Errors
InvalidObjectIdError(status 400) — reference fails parsing.ObjectIdNotFoundError(status 404) —*OrFailmethod couldn't resolve.
Both extend Error and expose a status field, so AdonisJS's default
exception handler turns uncaught instances into appropriate HTTP responses.
License
MIT — see LICENSE.md.
