Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions packages/inflection/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# @interweb/inflection

Inflection utilities for pluralization and singularization with PostGraphile-compatible Latin suffix handling.

## Installation

```bash
npm install @interweb/inflection
```

## Usage

```typescript
import {
singularize,
pluralize,
singularizeLast,
pluralizeLast,
distinctPluralize,
lcFirst,
ucFirst,
toFieldName,
toQueryName,
} from '@interweb/inflection';

// Basic singularization/pluralization
singularize('Users'); // 'User'
pluralize('User'); // 'Users'

// Latin suffix handling (PostGraphile-compatible)
singularize('Schemata'); // 'Schema' (not 'Schematum')
singularize('Criteria'); // 'Criterion'
singularize('Media'); // 'Medium'

// Compound word handling (only transforms last word)
singularizeLast('UserProfiles'); // 'UserProfile'
pluralizeLast('UserProfile'); // 'UserProfiles'

// Case transformations
lcFirst('UserProfile'); // 'userProfile'
ucFirst('userProfile'); // 'UserProfile'

// GraphQL naming helpers
toFieldName('Users'); // 'user'
toQueryName('User'); // 'users'
```

## API

### Pluralization

- `singularize(word)` - Convert a word to singular form with Latin suffix handling
- `pluralize(word)` - Convert a word to plural form
- `singularizeLast(str)` - Singularize only the last word in a compound name
- `pluralizeLast(str)` - Pluralize only the last word in a compound name
- `distinctPluralize(str)` - Create a distinct plural form (handles cases where singular === plural)
- `distinctPluralizeLast(str)` - Distinctly pluralize only the last word

### Case Transformations

- `lcFirst(str)` - Convert first character to lowercase (PascalCase to camelCase)
- `ucFirst(str)` - Convert first character to uppercase (camelCase to PascalCase)
- `fixCapitalisedPlural(str)` - Fix capitalized S after numbers (e.g., `Table1S` -> `Table1s`)

### Naming Helpers

- `toFieldName(pluralTypeName)` - Convert plural PascalCase to singular camelCase field name
- `toQueryName(singularTypeName)` - Convert singular PascalCase to plural camelCase query name

## Latin Suffix Overrides

This library handles Latin plural suffixes differently than the standard `inflection` package to match PostGraphile's behavior:

| Plural | Singular |
|--------|----------|
| schemata | schema |
| criteria | criterion |
| phenomena | phenomenon |
| media | medium |
| memoranda | memorandum |
| strata | stratum |
| curricula | curriculum |
| data | datum |

## License

MIT
200 changes: 200 additions & 0 deletions packages/inflection/__tests__/inflection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import {
singularize,
pluralize,
singularizeLast,
pluralizeLast,
distinctPluralize,
distinctPluralizeLast,
lcFirst,
ucFirst,
camelize,
underscore,
fixCapitalisedPlural,
toFieldName,
toQueryName,
} from '../src';

describe('singularize', () => {
it('should singularize regular words', () => {
expect(singularize('Users')).toBe('User');
expect(singularize('users')).toBe('user');
expect(singularize('People')).toBe('Person');
expect(singularize('people')).toBe('person');
expect(singularize('Categories')).toBe('Category');
});

it('should handle Latin suffix overrides', () => {
expect(singularize('Schemata')).toBe('Schema');
expect(singularize('schemata')).toBe('schema');
expect(singularize('Criteria')).toBe('Criterion');
expect(singularize('criteria')).toBe('criterion');
expect(singularize('Phenomena')).toBe('Phenomenon');
expect(singularize('Media')).toBe('Medium');
expect(singularize('Memoranda')).toBe('Memorandum');
expect(singularize('Strata')).toBe('Stratum');
expect(singularize('Curricula')).toBe('Curriculum');
expect(singularize('Data')).toBe('Datum');
});

it('should handle compound words with Latin suffixes', () => {
expect(singularize('ApiSchemata')).toBe('ApiSchema');
expect(singularize('UserMedia')).toBe('UserMedium');
expect(singularize('TestCriteria')).toBe('TestCriterion');
});

it('should preserve case of suffix', () => {
expect(singularize('apiSchemata')).toBe('apiSchema');
expect(singularize('SCHEMATA')).toBe('SCHEMA');
});
});

describe('pluralize', () => {
it('should pluralize regular words', () => {
expect(pluralize('User')).toBe('Users');
expect(pluralize('user')).toBe('users');
expect(pluralize('Person')).toBe('People');
expect(pluralize('Category')).toBe('Categories');
});
});

describe('singularizeLast', () => {
it('should singularize only the last word in compound names', () => {
expect(singularizeLast('user_profiles')).toBe('user_profile');
expect(singularizeLast('UserProfiles')).toBe('UserProfile');
expect(singularizeLast('order_items')).toBe('order_item');
expect(singularizeLast('OrderItems')).toBe('OrderItem');
});

it('should handle Latin suffixes in compound names', () => {
expect(singularizeLast('api_schemata')).toBe('api_schema');
expect(singularizeLast('ApiSchemata')).toBe('ApiSchema');
});
});

describe('pluralizeLast', () => {
it('should pluralize only the last word in compound names', () => {
expect(pluralizeLast('user_profile')).toBe('user_profiles');
expect(pluralizeLast('UserProfile')).toBe('UserProfiles');
expect(pluralizeLast('order_item')).toBe('order_items');
expect(pluralizeLast('OrderItem')).toBe('OrderItems');
});
});

describe('distinctPluralize', () => {
it('should pluralize regular words', () => {
expect(distinctPluralize('user')).toBe('users');
expect(distinctPluralize('User')).toBe('Users');
});

it('should handle words where singular equals plural', () => {
expect(distinctPluralize('sheep')).toBe('sheeps');
expect(distinctPluralize('fish')).toBe('fishes');
});

it('should handle words ending in ch, s, sh, x, z', () => {
expect(distinctPluralize('bus')).toBe('buses');
expect(distinctPluralize('box')).toBe('boxes');
});
});

describe('distinctPluralizeLast', () => {
it('should distinctly pluralize only the last word', () => {
expect(distinctPluralizeLast('user_profile')).toBe('user_profiles');
expect(distinctPluralizeLast('UserProfile')).toBe('UserProfiles');
});
});

describe('lcFirst', () => {
it('should lowercase the first character', () => {
expect(lcFirst('UserProfile')).toBe('userProfile');
expect(lcFirst('User')).toBe('user');
expect(lcFirst('ABC')).toBe('aBC');
});

it('should handle already lowercase strings', () => {
expect(lcFirst('user')).toBe('user');
});
});

describe('ucFirst', () => {
it('should uppercase the first character', () => {
expect(ucFirst('userProfile')).toBe('UserProfile');
expect(ucFirst('user')).toBe('User');
expect(ucFirst('abc')).toBe('Abc');
});

it('should handle already uppercase strings', () => {
expect(ucFirst('User')).toBe('User');
});
});

describe('camelize', () => {
it('should convert snake_case to PascalCase by default', () => {
expect(camelize('user_profile')).toBe('UserProfile');
expect(camelize('order_item')).toBe('OrderItem');
expect(camelize('api_schema')).toBe('ApiSchema');
});

it('should convert snake_case to camelCase when lowFirstLetter is true', () => {
expect(camelize('user_profile', true)).toBe('userProfile');
expect(camelize('order_item', true)).toBe('orderItem');
expect(camelize('api_schema', true)).toBe('apiSchema');
});

it('should handle single words', () => {
expect(camelize('user')).toBe('User');
expect(camelize('user', true)).toBe('user');
});
});

describe('underscore', () => {
it('should convert PascalCase to snake_case', () => {
expect(underscore('UserProfile')).toBe('user_profile');
expect(underscore('OrderItem')).toBe('order_item');
expect(underscore('ApiSchema')).toBe('api_schema');
});

it('should convert camelCase to snake_case', () => {
expect(underscore('userProfile')).toBe('user_profile');
expect(underscore('orderItem')).toBe('order_item');
});

it('should handle single words', () => {
expect(underscore('User')).toBe('user');
expect(underscore('user')).toBe('user');
});
});

describe('fixCapitalisedPlural', () => {
it('should fix capitalized S after numbers', () => {
expect(fixCapitalisedPlural('Table1S')).toBe('Table1s');
expect(fixCapitalisedPlural('blahTable1S')).toBe('blahTable1s');
expect(fixCapitalisedPlural('Table1SConnection')).toBe('Table1sConnection');
});

it('should not affect normal strings', () => {
expect(fixCapitalisedPlural('Users')).toBe('Users');
expect(fixCapitalisedPlural('Table1')).toBe('Table1');
});
});

describe('toFieldName', () => {
it('should convert plural PascalCase to singular camelCase', () => {
expect(toFieldName('Users')).toBe('user');
expect(toFieldName('OrderItems')).toBe('orderItem');
expect(toFieldName('Categories')).toBe('category');
});

it('should handle Latin suffixes', () => {
expect(toFieldName('Schemata')).toBe('schema');
expect(toFieldName('ApiSchemata')).toBe('apiSchema');
});
});

describe('toQueryName', () => {
it('should convert singular PascalCase to plural camelCase', () => {
expect(toQueryName('User')).toBe('users');
expect(toQueryName('OrderItem')).toBe('orderItems');
expect(toQueryName('Category')).toBe('categories');
});
});
18 changes: 18 additions & 0 deletions packages/inflection/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
babelConfig: false,
tsconfig: 'tsconfig.json',
},
],
},
transformIgnorePatterns: [`/node_modules/*`],
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
modulePathIgnorePatterns: ['dist/*']
};
45 changes: 45 additions & 0 deletions packages/inflection/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "@interweb/inflection",
"version": "0.0.1",
"description": "Inflection utilities for pluralization and singularization with PostGraphile-compatible Latin suffix handling",
"author": "Constructive <developers@constructive.io>",
"homepage": "https://github.com/constructive-io/dev-utils",
"license": "MIT",
"main": "index.js",
"module": "esm/index.js",
"types": "index.d.ts",
"publishConfig": {
"access": "public",
"directory": "dist"
},
"scripts": {
"copy": "makage assets",
"clean": "makage clean",
"prepublishOnly": "npm run build",
"build": "makage build",
"lint": "eslint . --fix",
"test": "jest",
"test:watch": "jest --watch"
},
"repository": {
"type": "git",
"url": "https://github.com/constructive-io/dev-utils"
},
"keywords": [
"inflection",
"pluralize",
"singularize",
"postgraphile",
"graphql",
"constructive"
],
"bugs": {
"url": "https://github.com/constructive-io/dev-utils/issues"
},
"devDependencies": {
"makage": "0.1.8"
},
"dependencies": {
"inflection": "^3.0.0"
}
}
Loading