@rafikidota/serpens
v2.3.0
Published
Sometimes, the best way to solve your own problems is to help someone else.
Maintainers
Readme
TypeORM Snake Naming Strategy
Sometimes, the best way to solve your own problems is to help someone else.
@rafikidota/serpens ships a TypeORM SnakeNamingStrategy plus three standalone string-case utilities (camelCase, snakeCase, titleCase) you can use independently of TypeORM.
Installation
npm install @rafikidota/serpens typeorm
# or
pnpm add @rafikidota/serpens typeormRequirements:
typeorm^1.1.0— peer dependency, bring your own install.- Node.js 24+ (the version used in CI, see
.nvmrc).
The package is published as ESM-first with a dual build: import resolves to dist/index.mjs, require to dist/index.cjs, each with its own type declarations.
Using SnakeNamingStrategy with TypeORM
import { DataSource, DataSourceOptions } from 'typeorm';
import { SnakeNamingStrategy } from '@rafikidota/serpens';
const config: DataSourceOptions = {
type: 'postgres',
host: 'localhost',
port: 5432,
database: 'postgres',
username: 'postgres',
password: 'postgres',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
migrations: [__dirname + '/migrations/*{.ts,.js}'],
synchronize: false,
namingStrategy: new SnakeNamingStrategy(),
};
export default new DataSource(config);With this strategy applied, an entity like:
@Entity()
class UserProfile {
@Column()
firstName: string;
}maps to table user_profile, column first_name.
The strategy overrides three members of TypeORM's DefaultNamingStrategy:
| Member | Behaviour |
| --- | --- |
| tableName | Explicit customName wins, otherwise snakeCase(className) |
| columnName | Joins embedded prefixes with the column name, then snake-cases the result |
| relationName | snakeCase(propertyName) |
Everything else falls back to DefaultNamingStrategy (index names, foreign keys, join tables, etc.).
String-case utilities
The same conversion helpers used internally by SnakeNamingStrategy are exported for standalone use — no TypeORM required.
import { camelCase, snakeCase, titleCase } from '@rafikidota/serpens';
snakeCase('firstName'); // 'first_name'
snakeCase('UserHTTPServer'); // 'user_http_server'
camelCase('first_name'); // 'firstName'
camelCase('first_name', true); // 'FirstName'
titleCase('first name'); // 'First Name'Development
This package uses pnpm (version pinned via packageManager in package.json), Vitest and tsdown.
pnpm install
pnpm typecheck # tsc --noEmit
pnpm test # run the test suite (pnpm test:watch for watch mode)
pnpm lint # lint and auto-fix
pnpm lint:check # lint without fixing (CI variant)
pnpm format # prettier --write
pnpm build # build dual ESM/CJS output to dist/A husky pre-commit hook runs lint-staged over staged .ts/.json files (prettier, then eslint).
Releasing
CI (GitHub Actions) runs typecheck, lint, test and build on pushes to main/development and on every pull request.
To cut a release:
pnpm version patch # or minor / major — updates package.json, commits, tags
git push --follow-tags # pushing the v* tag triggers the publish workflowPushing a v* tag runs three jobs:
| Job | Does |
| --- | --- |
| ci | Reruns the full CI workflow as a reusable workflow |
| guard | Fails if the tag doesn't match package.json, then resolves the npm dist-tag — prereleases go to next, stable versions to latest |
| publish | Waits for maintainer approval, then publishes |
The publish job runs in the release environment, so it pauses for manual approval with the CI and guard results already visible. It publishes with provenance via OIDC trusted publishing — no npm token secret involved — and only dist/ is included in the tarball.
Use pnpm version rather than editing package.json by hand: it keeps the tag and the version in sync, which is what guard checks. The dist-tag matters because a trusted-publishing token cannot change dist-tags after the fact, so a mistagged prerelease would stay on latest permanently.
