diff --git a/package-lock.json b/package-lock.json index d9224e6..1dd318b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "eslint": "^9.19.0", "supertest": "^7.0.0", "typescript": "^5.8.3", + "unplugin-swc": "^1.5.3", "vitest": "^3.1.3" }, "engines": { @@ -1289,6 +1290,36 @@ "node": ">=14" } }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.40.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", @@ -1649,6 +1680,245 @@ "eslint": ">=8.40.0" } }, + "node_modules/@swc/core": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.24.tgz", + "integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.21" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.11.24", + "@swc/core-darwin-x64": "1.11.24", + "@swc/core-linux-arm-gnueabihf": "1.11.24", + "@swc/core-linux-arm64-gnu": "1.11.24", + "@swc/core-linux-arm64-musl": "1.11.24", + "@swc/core-linux-x64-gnu": "1.11.24", + "@swc/core-linux-x64-musl": "1.11.24", + "@swc/core-win32-arm64-msvc": "1.11.24", + "@swc/core-win32-ia32-msvc": "1.11.24", + "@swc/core-win32-x64-msvc": "1.11.24" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz", + "integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz", + "integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz", + "integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz", + "integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz", + "integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz", + "integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz", + "integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz", + "integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz", + "integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.11.24", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz", + "integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@swc/types": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", + "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tokenizer/inflate": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", @@ -5448,6 +5718,16 @@ "node": ">=13.2.0" } }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -7582,6 +7862,36 @@ "node": ">= 0.8" } }, + "node_modules/unplugin": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.4.tgz", + "integrity": "sha512-m4PjxTurwpWfpMomp8AptjD5yj8qEZN5uQjjGM3TAs9MWWD2tXSSNNj6jGR2FoVGod4293ytyV6SwBbertfyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.1", + "picomatch": "^4.0.2", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin-swc": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.5.3.tgz", + "integrity": "sha512-lfBT7Wtauf/1y89xGt+x8+T7yB7bCMq/qXeXcOcqQddKDULGEg/4O2201Eh6eCBxbEi8J1Tmy2scG5dhiBJONg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.4", + "load-tsconfig": "^0.2.5", + "unplugin": "^2.3.4" + }, + "peerDependencies": { + "@swc/core": "^1.2.108" + } + }, "node_modules/unrs-resolver": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.7.2.tgz", @@ -7811,6 +8121,13 @@ } } }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 0d501ba..ac62afa 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "eslint": "^9.19.0", "supertest": "^7.0.0", "typescript": "^5.8.3", + "unplugin-swc": "^1.5.3", "vitest": "^3.1.3" } } diff --git a/readme.md b/readme.md index 399797b..c085d79 100644 --- a/readme.md +++ b/readme.md @@ -25,11 +25,11 @@ npm install @strv/nestjs-dataloader ## Usage -The core principle is that you work with Dataloaders by creating a Factory that creates those Dataloader instances. The Factory class is part of Nest's dependency injection which means it can use other components, like services, to deliver results. +The core principle is that you work with Dataloaders by defining a Factory class that is responsible for creating those Dataloader instances. The Factory class is part of Nest's dependency injection which means it can use other injectable providers to produce results. ### Register the module -In your app module, register the Dataloader module: +In your app, or root module, register the Dataloader module. You only need to do this once in your Nest.js application. ```ts // app.module.ts @@ -38,6 +38,7 @@ import { DataloaderModule } from '@strv/nestjs-dataloader' @Module({ imports: [ + // There is also `.forRootAsync()` to use a factory to provide options for the module. DataloaderModule.forRoot(), ], }) @@ -52,7 +53,7 @@ export { A Factory is responsible for creating new instances of Dataloader. Each factory creates only one type of Dataloader so for each relation you will need to define a Factory. You define a Factory by subclassing the provided `DataloaderFactory` and implemneting `load()` and `id()` methods on it, at minimum. -> Each Factory can be considered global in the dependency graph, you do not need to import the module that provides the Factory in order to use it elsewhere in your application. +> Each Factory can be considered global in the Nest.js dependency graph, you do not need to import the module that provides the Factory in order to use it elsewhere in your application. You do need, however, to export the Factory from the hosting Nest.js module in order to use it in a different module. ```ts // AuthorBooksLoader.factory.ts @@ -122,9 +123,9 @@ export { } ``` -### Export the factory +### Register the factory -Each Dataloader factory you create must be added to Nest.js DI container via `DataloaderModule.forFeature()`. Don't forget to also export the `DataloaderModule` to make the Dataloader factory available to other modules. +Each Dataloader factory you create must be added to your Nest.js module as a provider. Optionally, if you need to use this factory also in other modules you must export the factory from the module as well. ```ts // authors.module.ts @@ -134,20 +135,25 @@ import { BooksService } from './books.service.js' import { AuthorBooksLoaderFactory } from './AuthorBooksLoader.factory.js' @Module({ - imports:[ - DataloaderModule.forFeature([AuthorBooksLoaderFactory]), + providers: [ + BooksService, + AuthorBooksLoaderFactory + ], + // If you need to use this dataloader in a different Nest.js module, export it + exports: [ + AuthorBooksLoaderFactory, ], - providers: [BooksService], - exports: [DataloaderModule], }) class AuthorsModule {} ``` ### Inject a Dataloader -Now that we have a Dataloader factory defined and available in the DI container, it's time to put it to some use! To obtain a Dataloader instance, you can use the provided `@Loader()` param decorator in your GraphQL resolvers. +Now that we have a Dataloader factory defined and available in the DI container, it's time to put it to some use! To obtain a Dataloader instance in a resolver, you can use the provided `@Loader()` parameter decorator in your GraphQL resolvers. + +> 💡 It's possible to use the `@Loader()` parameter decorator also in REST controllers although the benefits of using Dataloaders in REST APIs are not that tangible as in GraphQL. However, if your app provides both GraphQL and REST interfaces this might be a good way to share some logic between the two. -> 💡 It's possible to use the `@Loader()` param decorator also in REST controllers although the benefits of using Dataloaders in REST APIs are not that tangible as in GraphQL. However, if your app provides both GraphQL and REST interfaces this might be a good way to share some logic between the two. +A new Dataloader instance will be produced for each incoming request and Dataloader by default caches the results it returns for the duration of that request. This is important to remember when troubleshooting data inconsistencies in responses after an update has been made to a previously-fetched record. ```ts // author.resolver.ts diff --git a/src/Dataloader.module.ts b/src/Dataloader.module.ts index a0506bb..17dc6b6 100644 --- a/src/Dataloader.module.ts +++ b/src/Dataloader.module.ts @@ -1,5 +1,5 @@ import { type DynamicModule, Module } from '@nestjs/common' -import { type DataloaderModuleOptions, type Factory, type DataloaderOptions } from './types.js' +import { type DataloaderModuleOptions, type DataloaderOptions } from './types.js' import { DataloaderCoreModule } from './DataloaderCore.module.js' @Module({}) @@ -17,14 +17,6 @@ class DataloaderModule { imports: [DataloaderCoreModule.forRootAsync(options)], } } - - static forFeature(loaders: Factory[]): DynamicModule { - return { - module: DataloaderModule, - providers: loaders, - exports: loaders, - } - } } diff --git a/test/DataloaderModule.test.ts b/test/DataloaderModule.test.ts index b5f0461..3291e32 100644 --- a/test/DataloaderModule.test.ts +++ b/test/DataloaderModule.test.ts @@ -1,7 +1,6 @@ import { describe } from 'vitest' import { Test, TestingModule } from '@nestjs/testing' -import { Injectable } from '@nestjs/common' -import { DataloaderFactory, DataloaderModule } from '@strv/nestjs-dataloader' +import { DataloaderModule } from '@strv/nestjs-dataloader' describe('DataloaderModule', it => { it('exists', t => { @@ -27,27 +26,4 @@ describe('DataloaderModule', it => { t.expect(app).toBeInstanceOf(TestingModule) }) - - it('.forFeature()', async t => { - @Injectable() - class SampleLoaderFactory extends DataloaderFactory { - load = async (keys: unknown[]) => await Promise.resolve(keys) - id = (key: unknown) => key - } - - const provider = DataloaderModule.forFeature([SampleLoaderFactory]) - const module = Test.createTestingModule({ imports: [ - DataloaderModule.forRoot(), - provider, - ] }) - const app = await module.compile() - t.onTestFinished(async () => await app.close()) - - t.expect(app).toBeInstanceOf(TestingModule) - - t.expect(provider).toBeDefined() - t.expect(provider.module).toBe(DataloaderModule) - t.expect(provider.providers).toEqual([SampleLoaderFactory]) - t.expect(provider.exports).toEqual([SampleLoaderFactory]) - }) }) diff --git a/test/Loader.test.ts b/test/Loader.test.ts index 4fef91c..24218fa 100644 --- a/test/Loader.test.ts +++ b/test/Loader.test.ts @@ -25,14 +25,62 @@ describe('@Loader()', it => { const module = await Test.createTestingModule({ imports: [ DataloaderModule.forRoot(), - DataloaderModule.forFeature([SampleLoaderFactory]), + ], + providers: [ + SampleLoaderFactory, ], controllers: [TestController], - exports: [DataloaderModule], }).compile() const app = await module.createNestApplication().init() t.onTestFinished(async () => await app.close()) await request(app.getHttpServer()).get('/') }) + + it('can inject other providers into the factory', async t => { + @Injectable() + class TestService { + test() { + return { text: 'Hello world' } + } + } + + @Injectable() + class SampleLoaderFactory extends DataloaderFactory { + readonly #service: TestService + constructor(service: TestService) { + super() + this.#service = service + } + + load = async () => await Promise.resolve([this.#service.test()]) + id = () => 0 + } + + @Controller() + class TestController { + @Get('/') + async handle(@Loader(SampleLoaderFactory) loader: LoaderFrom) { + return await loader.load(0) + } + } + + const module = await Test.createTestingModule({ + imports: [ + DataloaderModule.forRoot(), + ], + providers: [ + TestService, + SampleLoaderFactory, + ], + controllers: [TestController], + }).compile() + const app = await module.createNestApplication().init() + t.onTestFinished(async () => await app.close()) + + const response = await request(app.getHttpServer()).get('/') + + t.expect(response.status).toBe(200) + t.expect(response.body).toEqual({ text: 'Hello world' }) + }) }) diff --git a/vitest.config.mjs b/vitest.config.mjs index cbcf713..b8c572d 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -1,6 +1,8 @@ import * as vitest from 'vitest/config' +import swc from 'unplugin-swc' export default vitest.defineConfig({ + plugins: [swc.vite()], test: { env: { NODE_ENV: 'test',