From 1ae405b095e96f40e0a714cac997af887439abd7 Mon Sep 17 00:00:00 2001 From: Nikaple Date: Wed, 21 Jul 2021 14:25:14 +0800 Subject: [PATCH] feat: support directory loader --- README.md | 77 +++++++++++++++++++++++++++++++++- lib/loader/directory-loader.ts | 49 ++++++++++++++++++++++ lib/loader/index.ts | 1 + package.json | 2 + tests/e2e/directory.spec.ts | 29 +++++++++++++ tests/src/app.module.ts | 17 +++++++- tests/src/config.model.ts | 12 ++++++ tests/src/dir/database.toml | 5 +++ tests/src/dir/table.toml | 1 + 9 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 lib/loader/directory-loader.ts create mode 100644 tests/e2e/directory.spec.ts create mode 100644 tests/src/dir/database.toml create mode 100644 tests/src/dir/table.toml diff --git a/README.md b/README.md index b753a68c..87e64bbf 100644 --- a/README.md +++ b/README.md @@ -252,9 +252,9 @@ TypedConfigModule.forRoot({ The `fileLoader` function optionally expects a `FileLoaderOptions` object as a first parameter: ```ts -import { Options } from 'cosmiconfig'; +import { OptionsSync } from 'cosmiconfig'; -export interface FileLoaderOptions extends Partial { +export interface FileLoaderOptions extends Partial { /** * basename of config file, defaults to `.env`. * @@ -287,6 +287,79 @@ TypedConfigModule.forRoot({ }) ``` +### Using directory loader + +The `directoryLoader` function allows you to load configuration within a given directory. + +The basename of files will be interpreted as config namespace, for example: + +``` +. +└─config + ├── app.toml + └── db.toml + +// app.toml +foo = 1 + +// db.toml +bar = 1 +``` + +The folder above will generate configuration as follows: + +```json +{ + "app": { + "foo": 1 + }, + "db": { + "bar": 1 + } +} +``` + +#### Example + +```ts +TypedConfigModule.forRoot({ + schema: RootConfig, + load: directoryLoader({ + directory: '/absolute/path/to/config/directory', + /* other cosmiconfig options */ + }), +}) +``` + +#### Passing options + +The `directoryLoader` function optionally expects a `DirectoryLoaderOptions` object as a first parameter: + +```ts +import { OptionsSync } from 'cosmiconfig'; + +export interface DirectoryLoaderOptions extends OptionsSync { + /** + * The directory containing all configuration files. + */ + directory: string; +} +``` + +If you want to add support for other extensions, you can use [`loaders`](https://github.com/davidtheclark/cosmiconfig#loaders) property provided by `cosmiconfig`: + +```ts +TypedConfigModule.forRoot({ + schema: RootConfig, + load: directoryLoader({ + directory: '/path/to/configuration', + // .env.ini has the highest priority now + loaders: { + '.ini': iniLoader + } + }), +}) +``` ### Using remote loader The `remoteLoader` function allows you to load configuration from a remote endpoint, such as configuration center. Internally [axios](https://github.com/axios/axios) is used to perform http requests. diff --git a/lib/loader/directory-loader.ts b/lib/loader/directory-loader.ts new file mode 100644 index 00000000..c9f4b2c1 --- /dev/null +++ b/lib/loader/directory-loader.ts @@ -0,0 +1,49 @@ +import { OptionsSync } from 'cosmiconfig'; +import { readdirSync } from 'fs'; +import { fileLoader } from './file-loader'; +import fromPairs from 'lodash.frompairs'; + +export interface DirectoryLoaderOptions extends OptionsSync { + /** + * The directory containing all configuration files. + */ + directory: string; +} + +/** + * Directory loader loads configuration in a specific folder. + * The basename of file will be used as configuration key, for the directory below: + * + * ``` + * . + * └─config + * ├── app.toml + * └── db.toml + * ``` + * + * The parsed config will be `{ app: "config in app.toml", db: "config in db.toml" }` + * @param options directory loader options. + */ +export const directoryLoader = ({ + directory, + ...options +}: DirectoryLoaderOptions) => { + return (): Record => { + const files = readdirSync(directory); + const fileNames = [ + ...new Set(files.map(file => file.replace(/\.\w+$/, ''))), + ]; + const configs = fromPairs( + fileNames.map(name => [ + name, + fileLoader({ + basename: name, + searchFrom: directory, + ...options, + })(), + ]), + ); + + return configs; + }; +}; diff --git a/lib/loader/index.ts b/lib/loader/index.ts index 3fdd3c77..56df12ef 100644 --- a/lib/loader/index.ts +++ b/lib/loader/index.ts @@ -1,3 +1,4 @@ export * from './file-loader'; export * from './remote-loader'; export * from './dotenv-loader'; +export * from './directory-loader'; diff --git a/package.json b/package.json index 05be869e..ed1f6a77 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "debug": "^4.3.1", "dotenv": "^9.0.0", "dotenv-expand": "^5.1.0", + "lodash.frompairs": "^4.0.1", "lodash.merge": "^4.6.2", "lodash.set": "^4.3.2", "parse-json": "^5.2.0", @@ -53,6 +54,7 @@ "@types/debug": "^4.1.5", "@types/express": "^4.17.11", "@types/jest": "26.0.23", + "@types/lodash.frompairs": "^4.0.6", "@types/lodash.merge": "^4.6.6", "@types/lodash.set": "^4.3.6", "@types/node": "^7.10.14", diff --git a/tests/e2e/directory.spec.ts b/tests/e2e/directory.spec.ts new file mode 100644 index 00000000..e70137c4 --- /dev/null +++ b/tests/e2e/directory.spec.ts @@ -0,0 +1,29 @@ +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { AppModule } from '../src/app.module'; +import { DirectoryConfig, DatabaseConfig } from '../src/config.model'; + +describe('Directory loader', () => { + let app: INestApplication; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule.withDirectory()], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + }); + + it(`should be able to config from specific folder`, () => { + const config = app.get(DirectoryConfig); + expect(config.table.name).toEqual('table2'); + + const databaseConfig = app.get(DatabaseConfig); + expect(databaseConfig.port).toBe(3000); + }); + + afterEach(async () => { + await app?.close(); + }); +}); diff --git a/tests/src/app.module.ts b/tests/src/app.module.ts index 586f0eb8..4de77caf 100644 --- a/tests/src/app.module.ts +++ b/tests/src/app.module.ts @@ -2,6 +2,7 @@ import { DynamicModule, Module } from '@nestjs/common'; import { join } from 'path'; import { parse as parseYaml } from 'yaml'; import { + directoryLoader, remoteLoader, RemoteLoaderOptions, TypedConfigModule, @@ -11,7 +12,7 @@ import { dotenvLoader, DotenvLoaderOptions, } from '../../lib/loader/dotenv-loader'; -import { Config, TableConfig } from './config.model'; +import { Config, DirectoryConfig, TableConfig } from './config.model'; const loadYaml = function loadYaml(filepath: string, content: string) { try { @@ -74,6 +75,20 @@ export class AppModule { }; } + static withDirectory(): DynamicModule { + return { + module: AppModule, + imports: [ + TypedConfigModule.forRoot({ + schema: DirectoryConfig, + load: directoryLoader({ + directory: join(__dirname, 'dir'), + }), + }), + ], + }; + } + static withRawModule(): DynamicModule { return TypedConfigModule.forRoot({ schema: Config, diff --git a/tests/src/config.model.ts b/tests/src/config.model.ts index eb760c79..1c7fcc3f 100644 --- a/tests/src/config.model.ts +++ b/tests/src/config.model.ts @@ -34,3 +34,15 @@ export class Config { @IsBoolean() public readonly isAuthEnabled!: boolean; } + +export class DirectoryConfig { + @Type(() => DatabaseConfig) + @ValidateNested() + @IsDefined() + public readonly database!: DatabaseConfig; + + @Type(() => TableConfig) + @ValidateNested() + @IsDefined() + public readonly table!: TableConfig; +} diff --git a/tests/src/dir/database.toml b/tests/src/dir/database.toml new file mode 100644 index 00000000..2b3dc320 --- /dev/null +++ b/tests/src/dir/database.toml @@ -0,0 +1,5 @@ +host = '127.0.0.1' +port = 3000 + +[table] +name = 'test' \ No newline at end of file diff --git a/tests/src/dir/table.toml b/tests/src/dir/table.toml new file mode 100644 index 00000000..2a58f015 --- /dev/null +++ b/tests/src/dir/table.toml @@ -0,0 +1 @@ +name = 'table2' \ No newline at end of file