Nest can't resolve dependencies: multiple database connections using mongoose package - asynchronous

Hi I am trying to set up multiple database connections using mongoose nestjs package with named connections and following along the documentation found here (https://docs.nestjs.com/techniques/mongodb#multiple-databases): but I am getting a runtime error on startup:
Error: Nest can't resolve dependencies of the RetailLocationModelModel (?). Please make sure that the argument partnersConnection at index [0] is available in the MongooseModule context.
This only happens when I use named connections. If I remove the connection name 'partners' from forFeature parameter (even though keeping it at forRootAsync), it is working fine. Probably because the model connects to the default connection and since there is only one, it automatically connects with 'partners'.
// dependencies
"#nestjs/axios": "^0.0.8",
"#nestjs/common": "^8.4.5",
"#nestjs/config": "^1.0.1",
"#nestjs/core": "^8.4.5",
"#nestjs/mongoose": "^8.0.1",
#Module({
imports: [DatabaseModule, RetailPartnersModule],
})
export class AppModule {}
#Module({
imports: [ConfigModule, MongooseModule.forRootAsync(B2CPartnersDbAsyncOptions)],
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}
export const B2CPartnersDbAsyncOptions = {
imports: [ConfigModule],
useFactory: (configService: ConfigService) => {
const user = configService.get<string>(MONGODB_USERNAME)
const password = configService.get<string>(MONGODB_PASSWORD)
const database = configService.get<string>('b2c_partners_acc')
const host = configService.get<string>(MONGODB_URL)
const uri = `mongodb+srv://${user}:${password}#${host}/${database}`
return {
uri,
useNewUrlParser: true,
useUnifiedTopology: true,
connectionName: 'partners',
}
},
inject: [ConfigService],
}
#Module({
imports: [
MongooseModule.forFeature(
[{ name: RetailLocationModel.name, schema: RetailLocationSchema }],
'partners'
),
],
controllers: [RetailPartnersController],
providers: [RetailPartnersService, RetailLocationsRepository],
})
export class RetailPartnersModule {}
export class RetailLocationsRepository {
constructor(
#InjectModel(RetailLocationModel.name) private model: Model<RetailLocationDocument>
) {}
}
Note that I cannot add 'partners' as second parameter in InjectModel, as TS complains it only expects 1 argument (even though official docs say that I can pass the connection name as extra argument. When manually updating typings to support 2 parameters, I still get the same runtime error of unresolved dependencies
Update:
When I go into the mongoose module provided by the package and log the result of the static methods forRootAsync and forFeature, forRootAsync does not provide the partnersConnection token, where forFeature is trying to inject it
// Mongoose.module.ts
class MongooseModule {
static forRootAsync(options) {
return {
module: MongooseModule_1,
imports: [mongoose_core_module_1.MongooseCoreModule.forRootAsync(options)],
};
}
}
where logging forRootAsync.imports[0].providers yields:
{
provide: 'MongooseModuleOptions',
useFactory: [Function (anonymous)],
inject: [ [class ConfigService] ]
},
{
provide: 'DatabaseConnection',
useFactory: [Function: useFactory],
inject: [ 'MongooseModuleOptions' ]
},
{ provide: 'MongooseConnectionName', useValue: 'DatabaseConnection' }
And with forFeature:
static forFeature(models = [], connectionName) {
const providers = mongoose_providers_1.createMongooseProviders(connectionName, models);
const result =
{
module: MongooseModule_1,
providers: providers,
exports: providers,
};
console.log('result forFeature1: ', result.providers)
return result;
}
logs to:
[{
provide: 'RetailLocationModelModel',
useFactory: [Function: useFactory],
inject: [ 'partnersConnection' ]
}]
So it seems that the partnersConnection token is not being set properly in the forRootAsync static function, as the connection is named to the default value of 'DatabaseConnection'
I verified this by changing the connectionName of the RetailLocationsModule to 'Database' and the runtime error is resolved.
#Module({
imports: [
MongooseModule.forFeature(
[{ name: RetailLocationModel.name, schema: RetailLocationSchema }],
'Database'
),
],
controllers: [RetailPartnersController],
providers: [RetailPartnersService, RetailLocationsRepository],
})
export class RetailPartnersModule {}
Therefore either there is a bug in forRootAsync or I am missing something.

Instead of supplying connectionName to the factory, supply it to the options object of MongooseAsyncOptions:
export function createDbConfig(
dbName: string
): (configService: ConfigService) => MongooseModuleAsyncOptions {
return (configService: ConfigService): MongooseModuleOptions => {
const logger = new Logger(createDbConfig.name)
const user = configService.get<string>(MONGODB_USERNAME, '')
const password = configService.get<string>(MONGODB_PASSWORD, '')
const database = configService.get<string>(dbName, '')
const host = configService.get<string>(MONGODB_URL, '')
const mongoProtocol =
configService.get<string>(NODE_ENV) === 'local' ? Protocols.local : Protocols.production
const uri = `${mongoProtocol}://${user}:${password}#${host}/${database}`
logger.verbose(`Connecting to the Mongo database URI: ${uri}`)
return {
uri,
useNewUrlParser: true,
useUnifiedTopology: true,
retryAttempts: 0,
// connectionName: 'partners' <= remove from here
}
}
}
export const B2CPartnersDbAsyncOptions: MongooseModuleAsyncOptions = {
imports: [ConfigModule],
useFactory: createDbConfig(DB_NAME.default),
inject: [ConfigService],
connectionName: 'partners', // <= put it here instead
}
Then in the module where you use #InjectConnection, supply the name of the connection ('partners'), as well as with every MongooseModule.forFeature, e.g.
#Injectable()
export class DatabaseService {
constructor(#InjectConnection('partners') private _connection: Connection) {}
public getStatus(): DatabaseHealthStatus {
return this._connection && this._connection.readyState === 1
? DatabaseHealthStatus.CONNECTED
: DatabaseHealthStatus.DISCONNECTED
}
public get connection(): Connection {
return this._connection
}
}
#Module({
imports: [
MongooseModule.forFeature(
[{ name: RetailLocationModel.name, schema: RetailLocationSchema }],
'partners'
),
],
controllers: [RetailPartnersController],
providers: [RetailPartnersService, RetailLocationsRepository],
})
export class RetailPartnersModule {}

Related

nextjs: TypeError: createServer is not a function

I am trying to follow this tutorial:
I am stuck at step 3, which is where the server is defined as follows:
import { createServer } from "#graphql-yoga/node";
import { join } from "path";
import { readFileSync } from "fs";
const typeDefs = readFileSync(join(process.cwd(), "schema.graphql"), {
encoding: "utf-8",
});
const resolvers = {
Query: {
cart: (_, { id }) => {
return {
id,
totalItems: 0,
};
},
},
};
const server = createServer({
cors: false,
endpoint: "/api",
logging: {
prettyLog: false,
},
schema: {
typeDefs,
resolvers,
},
});
export default server;
When I try to use that definition and start the local host, I get an error that says:
TypeError: (0 ,
graphql_yoga_node__WEBPACK_IMPORTED_MODULE_0_.createServer) is not a function at eval
Can anyone see if this tutorial is now out of date. I can see that I am using next v 13.1.1 and the tutorial uses v12. I've been having an awful time trying to find an explanation of how to use these packages, in their current formats. Is this one now out of date?
Can anyone see how to define a server for next v13?
There are some change between graph-yoga v2 and v3, you can look this tutorial to solve it.
For others that might be stuck, this might be a way to define the schema using graphql-yoga (now instead of graphql-yoga/node)
import { createSchema, createYoga } from 'graphql-yoga'
import { createServer } from 'node:http'
import { join } from "path";
import { readFileSync } from "fs";
const typeDefs = readFileSync(join(process.cwd(), "schema.graphql"), {
encoding: "utf-8",
});
const resolvers = {
Query: {
cart: (_, { id }) => {
return {
id,
totalItems: 0,
};
},
},
};
const yoga = createYoga({
cors: false,
endpoint: "/api",
logging: {
prettyLog: false,
},
schema: createSchema({
typeDefs,
resolvers,
}),
});
const server = createServer(yoga);
server.listen(3000, () => {
console.info('Server is running on http://localhost:3000/graphql')
});
export default server;

stitchSchema returns null

I have stitched two schemas together and run on localhost to query it. But the query returns null for the data in the second schema and I am not sure why.
I have the following code to stitch to remote schemas together and run a localhost graphql server to serve it. It should add the linked data from the second schema under cmsMetaData in the main Product data. But cmsMetaData is null.
import { ApolloServer } from 'apollo-server-micro';
import { ApolloServerPluginInlineTraceDisabled, ApolloServerPluginLandingPageLocalDefault } from "apollo-server-core";
import { stitchSchemas } from '#graphql-tools/stitch';
import { delegateToSchema } from '#graphql-tools/delegate';
import { RenameTypes, RenameRootFields } from '#graphql-tools/wrap';
import createRemoteSchema from '../../utils/createRemoteExecutor';
// Configuration for Next.js API Routes
export const config = {
api: {
bodyParser: false,
},
};
// Export as a Next.js API Route
export default async (req, res) => {
// Setup subschema configurations
const productsSubschema = await createRemoteSchema({
url: 'https://schema1.com/graphql/'
});
const cmsSubschema = await createRemoteSchema({
url: 'https://schema2.com/graphql/',
transforms: [
new RenameRootFields(
(operationName, fieldName, fieldConfig) => `strapi_${fieldName}`,
),
new RenameTypes((name) => `Strapi_${name}`),
],
});
// Build the combined schema and set up the extended schema and resolver
const schema = stitchSchemas({
subschemas: [productsSubschema, cmsSubschema],
typeDefs: `
extend type Product {
cmsMetaData: Strapi_Product
}
`,
resolvers: {
Product: {
cmsMetaData: {
selectionSet: `{ id }`,
resolve(product, args, context, info) {
// Get the data for the extended type from the subschema for Strapi
return delegateToSchema({
schema: cmsSubschema,
operation: 'query',
fieldName: 'strapi_product',
args: { where: { SaleorID: product.id } },
context,
info,
});
},
},
},
},
});
// Set up the GraphQL server
const apolloServer = new ApolloServer({
schema,
plugins: [
ApolloServerPluginInlineTraceDisabled(),
ApolloServerPluginLandingPageLocalDefault({ embed: true }),
],
});
await apolloServer.start();
const apolloServerHandler = apolloServer.createHandler({
path: '/api/graphql',
});
// Return the GraphQL endpoint
return apolloServerHandler(req, res);
};
utils/createRemoteExecutor.js is:
import { introspectSchema, wrapSchema } from '#graphql-tools/wrap';
import { print } from 'graphql';
// Builds a remote schema executor function,
// customize any way that you need (auth, headers, etc).
// Expects to recieve an object with "document" and "variable" params,
// and asynchronously returns a JSON response from the remote.
export default async function createRemoteSchema({ url, ...filters }) {
const executor = async ({ document, variables }) => {
const query = print(document);
const fetchResult = await fetch(url, {
method: 'POST',
headers: {
// We can also do Authentication here
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, variables }),
});
return fetchResult.json();
};
return wrapSchema({
schema: await introspectSchema(executor),
executor,
...filters,
});
}
The query is:
products(first: 100, channel: "default-channel")
{
edges
{
node
{
id
name
cmsMetaData
{
Title
SaleorID
}
}
}
}
In my api.tsx, which I generate using codegen.yaml, Product contains cmsMetaData as follows, which is of type Strapi_Product:
export type Product = Node & ObjectWithMetadata & {
__typename?: 'Product';
...
cmsMetaData?: Maybe<Array<Maybe<Strapi_Product>>>;
...
}
Strapi_Product is as follows which contains Title, SaleorID etc.:
export type Strapi_Product = {
__typename?: 'Strapi_Product';
SaleorID?: Maybe<Scalars['String']>;
Title?: Maybe<Scalars['String']>;
createdAt?: Maybe<Scalars['Strapi_DateTime']>;
publishedAt?: Maybe<Scalars['Strapi_DateTime']>;
updatedAt?: Maybe<Scalars['Strapi_DateTime']>;
};
But the date in GraphQL shows null for cmsMetaData as null:
{
"data": {
"products": {
"edges": [
{
"node": {
"id": "UHJvZHVjdDoxMjc=",
"name": "52-00 Base Plate",
"cmsMetaData": null
}
},
{
"node": {
"id": "UHJvZHVjdDoxMjg=",
"name": "52-01HD Weigh Module",
"cmsMetaData": null
}
}
]
}
}
}
Your problem seems related to this github issue. Most of your code looks totally fine, so I guess it fetches the schema correctly. The cmsMetaData field is null because it did not find anything matching objects using the selection criteria. This unwanted behavior is in the transformation and/or the resolver.
A good starting point for debugging would be to remove the RenameRootFields mutation. Furthermore, this example looks like your use case, it is an excellent step-by-step guide.
I also rebuild your example from an example I found on the internet. You most probably have a typo in one of the field names, that caused a null value for me. Make sure A equals B in the code below. I am guessing the initial fieldName is "Product", so after the transformation, this does not equal "strapi_product" and returns a null value.
const cmsSubschema = await createRemoteSchema({
url: 'https://schema2.com/graphql/',
transforms: [
new RenameRootFields(
(_, fieldName) => `strapi_${fieldName}`,), // A
new RenameTypes((name) => `Strapi_${name}`),
],
});
// Build the combined schema and set up the extended schema and resolver
const schema = stitchSchemas({
subschemas: [productsSubschema, cmsSubschema],
typeDefs: `
extend type Product {
cmsMetaData: Strapi_Product
}
`,
resolvers: {
Product: {
cmsMetaData: {
selectionSet: `{ id }`,
resolve(product, args, context, info) {
// Get the data for the extended type from the subschema for Strapi
return delegateToSchema({
schema: cmsSubschema,
operation: 'query',
fieldName: 'strapi_product', // B
args: { where: { SaleorID: product.id } },
context,
info,
});
},
},
},
},
});

Auto-import vue reactivity system in vitest for testing composables in Nuxt 3 and vitest

I am using some utils in Nuxt 3. The vue reactivity system (ref, computed, ...) is also imported directly. However, it is not the case for the tests.
Running the spec file importing a ./useBusinessValidation composable throws the error ReferenceError: ref is not defined
Source file ./useBusinessValidation:
import { MaybeRef } from "#vueuse/core"
export const useBusinessValidation = <T>(rule: (payload: T) => true | string, payload: MaybeRef<T>) => {
const validation = computed(() => rule(unref(payload)))
const isValid = computed(() => validation.value === true)
const errorMessage = computed(() => isValid.value ? undefined : validation.value as string)
return {
isValid,
errorMessage
}
}
Spec file useBusinessValidation.spec.ts:
import { useBusinessValidation } from "./useBusinessValidation"
describe('useBusinessValidation', async () => {
it('should be valid with payload respecting the rule', () => {
const rule = (x: number) => x > 0 ? true : `invalid ${x} number. Expected ${x} to be greater than 0.`
const { isValid, errorMessage } = useBusinessValidation(rule, 0)
expect(isValid.value).toBe(true)
expect(errorMessage.value).toBe(undefined)
});
})
and the vitest.config.ts
{
resolve: {
alias: {
'~': '.',
'~~': './',
'##': '.',
'##/': './',
'assets': './assets',
'public': './public',
'public/': './public/'
}
},
test: {
globals: true,
setupFiles: './test/setupUnit.ts',
environment: 'jsdom',
deps: { inline: [/#nuxt\/test-utils-edge/] },
exclude: [
'test/**/**.spec.ts',
'**/node_modules/**',
'**/dist/**',
'**/cypress/**',
'**/.{idea,git,cache,output,temp}/**'
]
}
}
I also tried with the #vitejs/plugin-vue as
plugins: [Vue()]
in the vitest config. It didn't work out.
To auto-import in vitest, install the unplugin-auto-import.
Then, in the vitest.config.ts add:
import AutoImport from 'unplugin-auto-import/vite';
export default defineConfig({
...
plugins: [
AutoImport({
imports: [
'vue',
// could add 'vue-router' or 'vitest', whatever else you need.
],
}),
]
});

NGRX Cannot access 'USER_FEATURE' before initialization

I currently have this:
user.state.ts
export interface IFeatureUserState {
checkoutRow: IEntityState<CheckoutRow>;
...
};
export const USER_FEATURE = 'users';
export const userFeatureState = createFeatureSelector<IFeatureUserState>(
USER_FEATURE
);
export const userReducers: ActionReducerMap<IFeatureUserState> = {
checkoutRow: checkoutRowReducer,
...
};
user-store.module.ts
#NgModule({
imports: [
StoreModule.forFeature(USER_FEATURE, userReducers),
EffectsModule.forFeature([
CheckoutRowEffects,
...
NgrxAutoEntityModule.forFeature();
providers: [
{ provide: CheckoutRow, useClass: EntityService },
...
]);
user-layout.module.ts <- Lazy loaded module
#NgModule({
imports: [
UserStoreModule
...
]
app.module.ts
#NgModule({
StoreModule.forRoot({}, {
metaReducers: [clearState],
runtimeChecks: {
strictStateImmutability: true,
strictActionImmutability: true,
},
}),
AuthStoreModule, // Another feature with same setup as USER_FEATURE but with authentication models.
EffectsModule.forRoot(),
NgrxAutoEntityModule.forRoot(),
...
});
However i get this error:
Anyone know what i do wrong? My goal is to use NGRX with features and separate my store into 2 modules, one for root/auth handling and one for user store functionality.
my problem solved by moving
export const USER_FEATURE = 'users';
to {nameof}actions.ts

Testing ngrx store: No provider for MockStore

I am following the Official Documentation for Testing ngrx Stores: https://ngrx.io/guide/store/testing
Even the simplest implementation of injecting a MockStore has the following Error:
NullInjectorError: R3InjectorError(CompilerModule)[MockStore -> MockStore]:
NullInjectorError: No provider for MockStore!
error properties: Object({ ngTempTokenPath: null, ngTokenPath: [ 'MockStore', 'MockStore' ] })
My code looks like this:
import { TestBed } from '#angular/core/testing';
import { provideMockStore, MockStore } from '#ngrx/store/testing';
describe('Auth Guard', () => {
// #ts-ignore
let store: MockStore;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
// any modules needed
],
providers: [
provideMockStore(),
// other providers
],
});
store = TestBed.inject(MockStore);
});
it('should create', () => {
expect(store).toBeTruthy();
});
});
I am Running #ngrx/store#8.6.0
UPDATED
based on discussion store = TestBed.inject(Store); instead of MockStore is enough for the desired behavior.
ORIGINAL
It is too early,
get it in the test:
it('should create', inject([Store], (store) => {
expect(store).toBeTruthy();
}));
not sure, but you can try to call compileComponents.
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
// any modules needed
],
providers: [
provideMockStore(),
// other providers
],
}).compileComponents();
store = TestBed.inject(MockStore);
});

Resources