Browse Source

chore: collection name should be pluralized by default

e22m4u 3 months ago
parent
commit
17839ef48a

File diff suppressed because it is too large
+ 824 - 18
dist/cjs/index.cjs


+ 18 - 4
src/mongodb-adapter.js

@@ -11,8 +11,10 @@ import {ServiceContainer} from '@e22m4u/js-service';
 import {transformValuesDeep} from './utils/index.js';
 import {transformValuesDeep} from './utils/index.js';
 import {stringToRegexp} from '@e22m4u/js-repository';
 import {stringToRegexp} from '@e22m4u/js-repository';
 import {selectObjectKeys} from '@e22m4u/js-repository';
 import {selectObjectKeys} from '@e22m4u/js-repository';
+import {DefinitionRegistry} from '@e22m4u/js-repository';
 import {ModelDefinitionUtils} from '@e22m4u/js-repository';
 import {ModelDefinitionUtils} from '@e22m4u/js-repository';
 import {InvalidArgumentError} from '@e22m4u/js-repository';
 import {InvalidArgumentError} from '@e22m4u/js-repository';
+import {modelNameToCollectionName} from './utils/index.js';
 import {InvalidOperatorValueError} from '@e22m4u/js-repository';
 import {InvalidOperatorValueError} from '@e22m4u/js-repository';
 
 
 /**
 /**
@@ -297,6 +299,17 @@ export class MongodbAdapter extends Adapter {
     });
     });
   }
   }
 
 
+  /**
+   * Get collection name by model name.
+   *
+   * @param {string} modelName
+   */
+  _getCollectionNameByModelName(modelName) {
+    const modelDef = this.getService(DefinitionRegistry).getModel(modelName);
+    if (modelDef.tableName != null) return modelDef.tableName;
+    return modelNameToCollectionName(modelName);
+  }
+
   /**
   /**
    * Get collection.
    * Get collection.
    *
    *
@@ -307,9 +320,10 @@ export class MongodbAdapter extends Adapter {
   _getCollection(modelName) {
   _getCollection(modelName) {
     let collection = this._collections.get(modelName);
     let collection = this._collections.get(modelName);
     if (collection) return collection;
     if (collection) return collection;
-    const tableName =
-      this.getService(ModelDefinitionUtils).getTableNameByModelName(modelName);
-    collection = this.client.db(this.settings.database).collection(tableName);
+    const collectionName = this._getCollectionNameByModelName(modelName);
+    collection = this.client
+      .db(this.settings.database)
+      .collection(collectionName);
     this._collections.set(modelName, collection);
     this._collections.set(modelName, collection);
     return collection;
     return collection;
   }
   }
@@ -929,6 +943,6 @@ export class MongodbAdapter extends Adapter {
   async count(modelName, where = undefined) {
   async count(modelName, where = undefined) {
     const query = this._buildQuery(modelName, where);
     const query = this._buildQuery(modelName, where);
     const table = this._getCollection(modelName);
     const table = this._getCollection(modelName);
-    return await table.count(query);
+    return await table.countDocuments(query);
   }
   }
 }
 }

File diff suppressed because it is too large
+ 278 - 106
src/mongodb-adapter.spec.js


+ 3 - 0
src/utils/index.js

@@ -1,4 +1,7 @@
+export * from './pluralize.js';
 export * from './is-iso-date.js';
 export * from './is-iso-date.js';
 export * from './is-object-id.js';
 export * from './is-object-id.js';
+export * from './to-camel-case.js';
 export * from './create-mongodb-url.js';
 export * from './create-mongodb-url.js';
 export * from './transform-values-deep.js';
 export * from './transform-values-deep.js';
+export * from './model-name-to-collection-name.js';

+ 26 - 0
src/utils/model-name-to-collection-name.js

@@ -0,0 +1,26 @@
+import {pluralize} from './pluralize.js';
+import {toCamelCase} from './to-camel-case.js';
+
+/**
+ * Создает имя таблицы/коллекции по названию модели.
+ *
+ * @param {string} modelName
+ * @returns {string}
+ */
+export function modelNameToCollectionName(modelName) {
+  // приведение имени класса к стандартному camelCase
+  // "UserModel" -> "userModel", "Article" -> "article"
+  const ccName = toCamelCase(modelName);
+  // удаление постфикса "Model" с конца строки
+  // "userModel" -> "user", "myModel" -> "my"
+  const woModel = ccName.replace(/Model$/i, '');
+  // если базовое имя слишком короткое (как "my" для "myModel"),
+  // то используется имя, включающее постфикс "Model"
+  if (woModel.length <= 2) {
+    // pluralize('myModel') -> "myModels"
+    return pluralize(ccName);
+  }
+  // для обычных имен обрабатывается без суффикса
+  // pluralize('user') -> "users"
+  return pluralize(woModel);
+}

+ 89 - 0
src/utils/model-name-to-collection-name.spec.js

@@ -0,0 +1,89 @@
+import {expect} from 'chai';
+import {modelNameToCollectionName} from './model-name-to-collection-name.js';
+
+describe('modelNameToCollectionName', function () {
+  it('should correctly pluralize and remove the "Model" suffix for standard names', function () {
+    expect(modelNameToCollectionName('userModel')).to.equal('users');
+    expect(modelNameToCollectionName('UserModel')).to.equal('users');
+    expect(modelNameToCollectionName('user_model')).to.equal('users');
+    expect(modelNameToCollectionName('USER_MODEL')).to.equal('users');
+    expect(modelNameToCollectionName('articleModel')).to.equal('articles');
+    expect(modelNameToCollectionName('ArticleModel')).to.equal('articles');
+    expect(modelNameToCollectionName('article_model')).to.equal('articles');
+    expect(modelNameToCollectionName('ARTICLE_MODEL')).to.equal('articles');
+  });
+
+  it('should just pluralize names that do not have the "Model" suffix', function () {
+    expect(modelNameToCollectionName('user')).to.equal('users');
+    expect(modelNameToCollectionName('User')).to.equal('users');
+    expect(modelNameToCollectionName('USER')).to.equal('users');
+    expect(modelNameToCollectionName('article')).to.equal('articles');
+    expect(modelNameToCollectionName('Article')).to.equal('articles');
+    expect(modelNameToCollectionName('ARTICLE')).to.equal('articles');
+  });
+
+  it('should correctly handle already pluralized names with the "Model" suffix', function () {
+    expect(modelNameToCollectionName('usersModel')).to.equal('users');
+    expect(modelNameToCollectionName('UsersModel')).to.equal('users');
+    expect(modelNameToCollectionName('users_model')).to.equal('users');
+    expect(modelNameToCollectionName('USERS_MODEL')).to.equal('users');
+    expect(modelNameToCollectionName('articlesModel')).to.equal('articles');
+    expect(modelNameToCollectionName('ArticlesModel')).to.equal('articles');
+    expect(modelNameToCollectionName('articles_model')).to.equal('articles');
+    expect(modelNameToCollectionName('ARTICLES_MODEL')).to.equal('articles');
+  });
+
+  it('should correctly handle already pluralized names', function () {
+    expect(modelNameToCollectionName('users')).to.equal('users');
+    expect(modelNameToCollectionName('Users')).to.equal('users');
+    expect(modelNameToCollectionName('USERS')).to.equal('users');
+    expect(modelNameToCollectionName('articles')).to.equal('articles');
+    expect(modelNameToCollectionName('Articles')).to.equal('articles');
+    expect(modelNameToCollectionName('ARTICLES')).to.equal('articles');
+  });
+
+  it('should correctly handle different pluralization rules (like y -> ies)', function () {
+    expect(modelNameToCollectionName('companyModel')).to.equal('companies');
+    expect(modelNameToCollectionName('CompanyModel')).to.equal('companies');
+    expect(modelNameToCollectionName('company_model')).to.equal('companies');
+    expect(modelNameToCollectionName('COMPANY_MODEL')).to.equal('companies');
+  });
+
+  it('should correctly handle exceptions from pluralize (like status -> statuses)', function () {
+    expect(modelNameToCollectionName('statusModel')).to.equal('statuses');
+    expect(modelNameToCollectionName('StatusModel')).to.equal('statuses');
+    expect(modelNameToCollectionName('status_model')).to.equal('statuses');
+    expect(modelNameToCollectionName('STATUS_MODEL')).to.equal('statuses');
+  });
+
+  it('should handle edge cases where removing "Model" leaves a short word', function () {
+    expect(modelNameToCollectionName('myModel')).to.equal('myModels');
+    expect(modelNameToCollectionName('MyModel')).to.equal('myModels');
+    expect(modelNameToCollectionName('my_model')).to.equal('myModels');
+    expect(modelNameToCollectionName('MY_MODEL')).to.equal('myModels');
+    expect(modelNameToCollectionName('doModel')).to.equal('doModels');
+    expect(modelNameToCollectionName('DoModel')).to.equal('doModels');
+    expect(modelNameToCollectionName('do_model')).to.equal('doModels');
+    expect(modelNameToCollectionName('DO_MODEL')).to.equal('doModels');
+  });
+
+  it('should remove the "Model" suffix case-insensitively', function () {
+    expect(modelNameToCollectionName('Usermodel')).to.equal('users');
+    expect(modelNameToCollectionName('USERMODEL')).to.equal('users');
+  });
+
+  it('should handle names that contain "Model" but not at the end', function () {
+    expect(modelNameToCollectionName('remodelAction')).to.equal(
+      'remodelActions',
+    );
+    expect(modelNameToCollectionName('RemodelAction')).to.equal(
+      'remodelActions',
+    );
+    expect(modelNameToCollectionName('remodel_action')).to.equal(
+      'remodelActions',
+    );
+    expect(modelNameToCollectionName('REMODEL_ACTION')).to.equal(
+      'remodelActions',
+    );
+  });
+});

+ 63 - 0
src/utils/pluralize.js

@@ -0,0 +1,63 @@
+/**
+ * Список регулярных выражений для проверки слов-исключений в единственном
+ * числе, которые заканчиваются на "s" и требуют особого обращения.
+ * Использование /word$/i позволяет корректно обрабатывать camelCase
+ * и snake_case строки.
+ */
+const singularExceptions = [
+  /access$/i,
+  /address$/i,
+  /alias$/i,
+  /bonus$/i,
+  /boss$/i,
+  /bus$/i,
+  /business$/i,
+  /canvas$/i,
+  /class$/i,
+  /cross$/i,
+  /dress$/i,
+  /focus$/i,
+  /gas$/i,
+  /glass$/i,
+  /kiss$/i,
+  /lens$/i,
+  /loss$/i,
+  /pass$/i,
+  /plus$/i,
+  /process$/i,
+  /status$/i,
+  /success$/i,
+  /virus$/i,
+];
+
+/**
+ * Pluralize.
+ *
+ * @param {string} input
+ * @returns {string}
+ */
+export function pluralize(input) {
+  if (!input || typeof input !== 'string') {
+    return input;
+  }
+  // если слово уже во множественном числе,
+  // то возвращается без изменений
+  if (/s$/i.test(input) && !singularExceptions.some(re => re.test(input))) {
+    return input;
+  }
+  // определение регистра для окончания по последнему символу
+  // учитывает случай, когда последним символом является цифра
+  const lastChar = input.slice(-1);
+  const isLastCharUpper =
+    lastChar === lastChar.toUpperCase() && lastChar !== lastChar.toLowerCase();
+  // если заканчивается на s, x, z, ch, sh -> добавляем "es"
+  if (/(s|x|z|ch|sh)$/i.test(input)) {
+    return input + (isLastCharUpper ? 'ES' : 'es');
+  }
+  // если заканчивается на согласную + y -> меняем "y" на "ies"
+  if (/[^aeiou]y$/i.test(input)) {
+    return input.slice(0, -1) + (isLastCharUpper ? 'IES' : 'ies');
+  }
+  // по умолчанию добавляется "s"
+  return input + (isLastCharUpper ? 'S' : 's');
+}

+ 110 - 0
src/utils/pluralize.spec.js

@@ -0,0 +1,110 @@
+import {expect} from 'chai';
+import {pluralize} from './pluralize.js';
+
+describe('pluralize function', function () {
+  describe('basic pluralization rules', function () {
+    it('should add "s" to simple nouns', function () {
+      expect(pluralize('apple')).to.equal('apples');
+      expect(pluralize('table')).to.equal('tables');
+    });
+
+    it('should add "es" to nouns ending in s, x, z, ch, sh', function () {
+      expect(pluralize('box')).to.equal('boxes');
+      expect(pluralize('watch')).to.equal('watches');
+      expect(pluralize('dish')).to.equal('dishes');
+      expect(pluralize('buzz')).to.equal('buzzes');
+    });
+
+    it('should change "y" to "ies" for nouns ending in a consonant + y', function () {
+      expect(pluralize('city')).to.equal('cities');
+      expect(pluralize('party')).to.equal('parties');
+    });
+
+    it('should just add "s" for nouns ending in a vowel + y', function () {
+      expect(pluralize('boy')).to.equal('boys');
+      expect(pluralize('key')).to.equal('keys');
+    });
+  });
+
+  describe('handling already plural nouns', function () {
+    it('should not change nouns that are already plural', function () {
+      expect(pluralize('apples')).to.equal('apples');
+      expect(pluralize('boxes')).to.equal('boxes');
+      expect(pluralize('cities')).to.equal('cities');
+    });
+  });
+
+  describe('handling singular exceptions', function () {
+    it('should correctly pluralize nouns from the exceptions list', function () {
+      expect(pluralize('bus')).to.equal('buses');
+      expect(pluralize('process')).to.equal('processes');
+      expect(pluralize('status')).to.equal('statuses');
+      expect(pluralize('business')).to.equal('businesses');
+    });
+
+    it('should not change already pluralized exceptions', function () {
+      expect(pluralize('buses')).to.equal('buses');
+      expect(pluralize('processes')).to.equal('processes');
+    });
+
+    it('should not change words that were removed from exceptions (like analysis)', function () {
+      expect(pluralize('analysis')).to.equal('analysis');
+      expect(pluralize('thesis')).to.equal('thesis');
+    });
+  });
+
+  describe('handling capital letters', function () {
+    it('should preserve case for the base word and add lowercase endings', function () {
+      expect(pluralize('Apple')).to.equal('Apples');
+      expect(pluralize('Bus')).to.equal('Buses');
+      expect(pluralize('City')).to.equal('Cities');
+    });
+
+    it('should add uppercase endings for all-caps words', function () {
+      expect(pluralize('APPLE')).to.equal('APPLES');
+      expect(pluralize('BUS')).to.equal('BUSES');
+      expect(pluralize('CITY')).to.equal('CITIES');
+    });
+  });
+
+  describe('handling multi-word strings and different casings', function () {
+    it('should pluralize the end of a camelCase string', function () {
+      expect(pluralize('userProfile')).to.equal('userProfiles');
+      expect(pluralize('accessPass')).to.equal('accessPasses');
+      expect(pluralize('dataEntry')).to.equal('dataEntries');
+    });
+
+    it('should pluralize the end of a PascalCase string', function () {
+      expect(pluralize('UserProfile')).to.equal('UserProfiles');
+      expect(pluralize('AccessPass')).to.equal('AccessPasses');
+      expect(pluralize('DataEntry')).to.equal('DataEntries');
+    });
+
+    it('should pluralize the end of a snake_case string', function () {
+      expect(pluralize('user_profile')).to.equal('user_profiles');
+      expect(pluralize('access_pass')).to.equal('access_passes');
+      expect(pluralize('data_entry')).to.equal('data_entries');
+    });
+
+    it('should pluralize the end of an UPPER_CASE string', function () {
+      expect(pluralize('USER_PROFILE')).to.equal('USER_PROFILES');
+      expect(pluralize('ACCESS_PASS')).to.equal('ACCESS_PASSES');
+      expect(pluralize('DATA_ENTRY')).to.equal('DATA_ENTRIES');
+      // проверка сохранения регистра для 'y' -> 'ies'
+      expect(pluralize('API_KEY')).to.equal('API_KEYS');
+      expect(pluralize('COMPANY_PARTY')).to.equal('COMPANY_PARTIES');
+    });
+
+    it('should not change already pluralized multi-word strings', function () {
+      expect(pluralize('userProfiles')).to.equal('userProfiles');
+      expect(pluralize('access_passes')).to.equal('access_passes');
+      expect(pluralize('DATA_ENTRIES')).to.equal('DATA_ENTRIES');
+    });
+  });
+
+  describe('edge cases and invalid input', function () {
+    it('should return the input unchanged for empty strings', function () {
+      expect(pluralize('')).to.equal('');
+    });
+  });
+});

+ 18 - 0
src/utils/to-camel-case.js

@@ -0,0 +1,18 @@
+/**
+ * To camel case.
+ *
+ * @param {string} input
+ * @returns {string}
+ */
+export function toCamelCase(input) {
+  if (!input) return '';
+  const spacedString = String(input)
+    .replace(/([-_])/g, ' ')
+    .replace(/([a-z])([A-Z])/g, '$1 $2');
+  const intermediateCased = spacedString
+    .toLowerCase()
+    .replace(/\s(.)/g, $1 => $1.toUpperCase())
+    .replace(/\s/g, '');
+  if (!intermediateCased) return '';
+  return intermediateCased.charAt(0).toLowerCase() + intermediateCased.slice(1);
+}

+ 75 - 0
src/utils/to-camel-case.spec.js

@@ -0,0 +1,75 @@
+import {expect} from 'chai';
+import {toCamelCase} from './to-camel-case.js';
+
+describe('toCamelCase function', function () {
+  describe('PascalCase inputs', function () {
+    it('should convert a simple PascalCase string to camelCase', function () {
+      expect(toCamelCase('UserModel')).to.equal('userModel');
+    });
+
+    it('should convert a single PascalCase word to lowercase', function () {
+      expect(toCamelCase('User')).to.equal('user');
+    });
+
+    it('should correctly split multiple words in PascalCase', function () {
+      expect(toCamelCase('UserLoginAttempt')).to.equal('userLoginAttempt');
+    });
+  });
+
+  describe('snake_case and kebab-case inputs', function () {
+    it('should convert a snake_case string to camelCase', function () {
+      expect(toCamelCase('user_model')).to.equal('userModel');
+    });
+
+    it('should convert a kebab-case string to camelCase', function () {
+      expect(toCamelCase('user-model-data')).to.equal('userModelData');
+    });
+
+    it('should handle leading and trailing separators', function () {
+      expect(toCamelCase('_user_model_')).to.equal('userModel');
+      expect(toCamelCase('-user-model-')).to.equal('userModel');
+    });
+  });
+
+  describe('ALL_CAPS inputs', function () {
+    it('should convert an ALL_CAPS word to lowercase', function () {
+      expect(toCamelCase('USER')).to.equal('user');
+    });
+
+    it('should convert an ALL_CAPS_SNAKE_CASE string to camelCase', function () {
+      expect(toCamelCase('USER_MODEL')).to.equal('userModel');
+    });
+
+    it('should handle ALL_CAPS words without separators', function () {
+      expect(toCamelCase('USERMODEL')).to.equal('usermodel');
+    });
+  });
+
+  describe('Mixed and complex inputs', function () {
+    it('should be idempotent (not change an already camelCased string)', function () {
+      expect(toCamelCase('userModel')).to.equal('userModel');
+    });
+
+    it('should handle mixed cases like Pascal_Snake_Case', function () {
+      expect(toCamelCase('User_Login_Attempt')).to.equal('userLoginAttempt');
+    });
+
+    it('should handle strings with numbers', function () {
+      expect(toCamelCase('Version1_2_3')).to.equal('version123');
+      expect(toCamelCase('user_model_v2')).to.equal('userModelV2');
+    });
+  });
+
+  describe('Edge cases and invalid inputs', function () {
+    it('should return an empty string for falsy or empty inputs', function () {
+      expect(toCamelCase('')).to.equal('');
+      expect(toCamelCase(null)).to.equal('');
+      expect(toCamelCase(undefined)).to.equal('');
+    });
+
+    it('should handle a string that is just a separator', function () {
+      expect(toCamelCase('_')).to.equal('');
+      expect(toCamelCase('__--__')).to.equal('');
+    });
+  });
+});

Some files were not shown because too many files changed in this diff