How can I increment a counter variable in LoopBack 4 with a MongoDB datasource? - mongodb

I'm trying to convert my Nodejs Express app to Loopback 4 and I can't figure out how to increment a counter. In my Angular 9 app when a user clicks an icon a counter is incremented. This works perfectly in Express
In Express
const updateIconCount = async function (dataset, collection = 'icons') {
let query = { _id: new ObjectId(dataset.id), userId: dataset.userId };
return await mongoController.update(
collection,
query,
{ $inc: { counter: 1 } },
function (err, res) {
logAccess(res, 'debug', true, 'function update updateIconLink');
if (err) {
return false;
} else {
return true;
}
}
);
};
I tried to first get the value of counter and then increment but every time I save VS Code reformats the code in an an unusual way. In this snippet I commented out the line of code that causes this reformatting. I can set the counter value, e.g. 100.
In Loopback 4
#patch('/icons/count/{id}', {
responses: {
'204': {
description: 'Icons PATCH success',
},
},
})
async incrementCountById(
#param.path.string('id') id: string,
#requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Icons, {partial: true}),
},
},
})
icons: Icons,
): Promise<void> {
// let targetIcon = this.findById(id).then(icon => {return icon});
icons.counter = 100;
console.log(icons.counter);
await this.iconsRepository.updateById(id, icons);
}
How do I implement { $inc: { counter: 1 } } in Loopback 4?
Added to aid solution
My mongo.datasource.ts
import {inject, lifeCycleObserver, LifeCycleObserver} from '#loopback/core';
import {juggler} from '#loopback/repository';
const config = {
name: 'mongo',
connector: 'mongodb',
url: '',
host: '192.168.253.53',
port: 32813,
user: '',
password: '',
database: 'firstgame',
useNewUrlParser: true,
allowExtendedOperators: true,
};
// Observe application's life cycle to disconnect the datasource when
// application is stopped. This allows the application to be shut down
// gracefully. The `stop()` method is inherited from `juggler.DataSource`.
// Learn more at https://loopback.io/doc/en/lb4/Life-cycle.html
#lifeCycleObserver('datasource')
export class MongoDataSource extends juggler.DataSource
implements LifeCycleObserver {
static dataSourceName = 'mongo';
static readonly defaultConfig = config;
constructor(
#inject('datasources.config.mongo', {optional: true})
dsConfig: object = config,
) {
super(dsConfig);
}
}
Amended endpoint
#patch('/icons/count/{id}', {
responses: {
'204': {
description: 'Icons PATCH success',
},
},
})
async incrementCountById(
#param.path.string('id') id: string,
#requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Icons, {partial: true}),
},
},
})
icons: Icons,
): Promise<void> {
console.log(id);
// #ts-ignore
await this.iconsRepository.updateById(id, {$inc: {counter: 1}});//this line fails
// icons.counter = 101; //these lines will set the icon counter to 101 so I know it is connecting to the mongodb
// await this.iconsRepository.updateById(id, icons);
}

You can use the mongo update-operators.
Basically, you just have to set allowExtendedOperators=true at your MongoDB datasource definition (guide). After that, you can directly use these operators.
Usage example:
// increment icon.counter by 3
await this.iconsRepository.updateById(id, {$inc: {counter: 3}} as Partial<Counter>);
Currently, these operators are missing from the lb4 types so you must cheat typescript to accept them. It's ugly but that's the only solution I could find right now.
You can follow this issue to see what's going on with these operators.

Related

ConnectorError Prisma with MongoDB

I am developing an API with Nestjs and MongoDB. Well sometimes I have this problem. It doesn't always happen and that's what I have to solve because this error rarely happens.
This is the error:
{
"status": "error",
"message": "\nInvalid this.prisma.people.findMany() invocation in\nC:\ima\empresas\datec\api-official\src\modules\Events\services\utils.service.ts:57: 28\n\n 54 }),\n 55 \n 56 // Get attendees by id\n→ 57 this.prisma.people.findMany(\nError in batch request 3: Error occurred during query execution:\nConnectorError(ConnectorError { user_facing_error: None, kind: RawDatabaseError { code: "unknown", message: "An existing connection was forced to be terminated by the remote host. (os error 10054)" } })"
}
I really can't find much information to solve this error. I have followed the steps in the documentation on Nestjs when using prisma with nestjs. I need help to solve this error, since it is essential to find a solution as soon as possible. Thanks so much for read this question
UPDATE
The Prisma service
// Librarys
import { Injectable } from '#nestjs/common'
import { PrismaClient } from '#prisma/client'
// Interfaces
import { INestApplication, OnModuleInit, OnModuleDestroy } from '#nestjs/common'
#Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
async onModuleInit() {
await this.$connect()
}
async onModuleDestroy() {
await this.$disconnect()
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close()
})
}
}
File that starts the application
//Libraries
import { NestFactory } from '#nestjs/core'
import { ValidationPipe } from '#nestjs/common'
import { SwaggerModule, DocumentBuilder } from '#nestjs/swagger'
import { useContainer } from 'class-validator'
// Modules
import { MainModule } from './main.module'
// config
import pk from '#root/package.json'
import { corstOptions } from '#config/cors'
import { PORT, APP_NAME, PUBLIC_URL } from '#config/env'
// Exceptions
import { HttpExceptionFilter } from '#utils/HttpExceptionFilter'
import { PrismaService } from '#root/src/services/prisma'
async function bootstrap() {
// Create application
const app = await NestFactory.create(MainModule)
// Enable cors
app.enableCors(corstOptions)
// Use global 'api' prefix, all calls will come after '/api/*'
app.setGlobalPrefix('api')
// Globally define custom response
app.useGlobalFilters(new HttpExceptionFilter())
// Enable prism on custom validations
useContainer(app.select(MainModule), { fallbackOnErrors: true })
// Get service from primsa and enable 'shutdowns'
const prismaService = app.get(PrismaService)
await prismaService.enableShutdownHooks(app)
// Use 'pipe' validation to validate the 'body' structure
app.useGlobalPipes(
newValidationPipe({
whitelist: true,
transform: true,
forbidUnknownValues: true,
forbidNonWhitelisted: true,
transformOptions: { enableImplicitConversion: true }
})
)
// Create API documentation
const config = new DocumentBuilder()
.addBearerAuth()
.setTitle(`API | ${APP_NAME}`)
.setContact(pk.author.name, pk.author.url, pk.author.email)
.setDescription(pk.description)
.setVersion(pk.version)
.build()
const document = SwaggerModule.createDocument(app, config)
SwaggerModule.setup('/', app, document)
// Listen to the application on the PORT defined in the environment variables
await app.listen(PORT || 0, () => {
// eslint-disable-next-line no-console
console.log(
'\x1b[33m%s\x1b[0m',
`[INFO] The server has been started at '${PUBLIC_URL}'`
)
})
}
bootstrap() // Run application
utils.service of Event module
// Librarys
import { Prisma } from '#prisma/client'
import { Inject, Injectable } from '#nestjs/common'
// Services
import { PrismaService } from '#services/prisma'
// DTO
import { EventBodyDTO } from '#modules/Events/events.dto'
// Arguments
import { EventsArgs } from '#modules/Events/events.args'
#Injectable()
export class UtilsService {
#Inject(PrismaService)
private readonly prisma: PrismaService
private readonly selects = {
'id.title': { id: true, title: true },
'id.fullname': { id: true, fullname: true },
'id.title.attributes': { id: true, title: true, attributes: true },
'id.code.description.attributes': {
id: true,
code: true,
description: true,
attributes: true
}
}
/**
* Get reminders, types of activities, case, client and attendees of an event
* #param {EventBodyDTO} payload Event data
*/
async getEventFields(payload: EventBodyDTO) {
const [activityType, eventCase, client, assistants, reminders] =
await Promise.all([
// Get an activity type by id
this.prisma.parameters.findUnique({
select: this.selects['id.title'],
where: { id: payload.activityType }
}),
// Get a case by id
this.prisma.expedients.findUnique({
where: { id: payload.case },
select: this.selects['id.code.description.attributes']
}),
// Get a person by id
this.prisma.people.findFirst({
select: this.selects['id.fullname'],
where: { isClient: true, id: payload.client }
}),
// Get attendees by id
this.prisma.people.findMany({
select: this.selects['id.fullname'],
where: {
isEmployee: true,
id: { in: payload.assistants }
}
}),
// Get reminders by id
this.prisma.parameters.findMany({
select: this.selects['id.title.attributes'],
where: {
id: { in: payload.reminders }
}
})
])
return {
reminders: reminders,
assistants: assistants,
client: client === null ? {} : client,
case: eventCase === null ? {} : eventCase,
activityType: activityType === null ? {} : activityType
}
/**
* Create filters to filter by customer or event attendees
* #param {EventsArgs} args Arguments to filter
* #returns {Promise<Prisma.EventsFindManyArgs['where']>} Returns an object with filters
*/
async createEventFilters(
args: EventsArgs
): Promise<Prisma.EventsFindManyArgs['where']> {
const filters: Prism.EventsFindManyArgs['where'] = {
userId: args.user
}
// Filter by customer
if (typeof args.client === 'string') {
const clients = await this.prisma.people.findMany({
where: {
isClient: true,
fullname: {
mode: 'insensitive',
contains: args.client
}
},
select: {
id: true,
fullname: true
}
})
filters.OR = []
for (const client of clients) {
filters.OR.push({
client: { equals: client }
})
}
}
// Filter by attendees
if (Array.isArray(args.assistants)) {
const assistants = await this.prisma.people.findMany({
where: {
isEmployee: true,
id: {
in: args.assistants
}
},
select: {
id: true,
fullname: true
}
})
if (!Array.isArray(filters.OR)) {
filters.OR = []
}
filters.OR.push({
assistants: { hasSome: assistants }
})
}
return filters
}
}

Pulumi aws lambda unit test fails with "Could not find property info for real property on object: sdk"

I am trying to use Pulumi to create an AWS Lambda that manipulates a DynamoDB table and is triggered by an API Gateway HTTP request.
My configuration works perfectly when I run pulumi up, but when I run Vitest, my test passes but exits with non-zero and this message:
⎯⎯⎯ Unhandled Rejection ⎯⎯⎯
Error: Could not find property info for real property on object: sdk
I can see that the error comes from this code in Pulumi, but I can't figure out what causes it. Am I doing something wrong or is this a bug (in which case I can create an issue)?
Below is a summary that I think has all the relevant info, but there is a minimal repo demonstrating the problem here (GitHub actions fail with the problem I'm describing).
I have an index.ts file that creates a database, gateway, and lambda:
import * as aws from '#pulumi/aws'
import * as apigateway from '#pulumi/aws-apigateway'
import handler from './handler'
const table = new aws.dynamodb.Table('Table', {...})
const tableAccessPolicy = new aws.iam.Policy('DbAccessPolicy', {
// removed for brevity. Allows put, get, delete
})
const lambdaRole = new aws.iam.Role('lambdaRole', {...})
new aws.iam.RolePolicyAttachment('RolePolicyAttachment', {...})
const callbackFunction = new aws.lambda.CallbackFunction(
'callbackFunction',
{
role: lambdaRole,
callback: handler(table.name),
}
)
const api = new apigateway.RestAPI('api', {
routes: [{
method: 'GET',
path: '/',
eventHandler: callbackFunction,
}]
})
export const dbTable = table
export const url = api.url
The handler is imported from a separate file:
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import * as pulumi from '#pulumi/pulumi';
import * as aws from '#pulumi/aws';
export default function (tableName: pulumi.Output<string>) {
return async function handleDocument(
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> {
try {
const client = new aws.sdk.DynamoDB.DocumentClient();
await client
.put({
TableName: tableName.get(),
Item: { PK: 'hello', roomId: '12345' },
})
.promise();
const result = await client
.get({
TableName: tableName.get(),
Key: { PK: 'hello' },
})
.promise();
await client
.delete({
TableName: tableName.get(),
Key: { PK: 'hello' },
})
.promise();
return {
statusCode: 200,
body: JSON.stringify({
item: result.Item,
}),
};
} catch (err) {
return {
statusCode: 200,
body: JSON.stringify({
error: err,
}),
};
}
};
}
Finally, I have a simple test:
import * as pulumi from '#pulumi/pulumi';
import { describe, it, expect, beforeAll } from 'vitest';
pulumi.runtime.setMocks(
{
newResource: function (args: pulumi.runtime.MockResourceArgs): {
id: string;
state: Record<string, any>;
} {
return {
id: `${args.name}_id`,
state: args.inputs,
};
},
call: function (args: pulumi.runtime.MockCallArgs) {
return args.inputs;
},
},
'project',
'stack',
false
);
describe('infrastructure', () => {
let infra: typeof import('./index');
beforeAll(async function () {
// It's important to import the program _after_ the mocks are defined.
infra = await import('./index');
});
it('Creates a DynamoDB table', async () => {
const tableId = await new Promise((resolve) => {
infra?.dbTable?.id.apply((id) => resolve(id));
});
expect(tableId).toBe('Table_id');
});
});
Your function is importing the Pulumi SDK, and you're trying to set the table name as a pulumi.Output<string>
Using the Pulumi SDK inside a lambda function isn't recommended or support.
I would recommend removing the Pulumi dependency from your function
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
export default function (tableName: string) {
return async function handleDocument(
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> {
try {
const client = new aws.sdk.DynamoDB.DocumentClient();
await client
.put({
TableName: tableName.get(),
Item: { PK: 'hello', roomId: '12345' },
})
.promise();
const result = await client
.get({
TableName: tableName.get(),
Key: { PK: 'hello' },
})
.promise();
await client
.delete({
TableName: tableName.get(),
Key: { PK: 'hello' },
})
.promise();
return {
statusCode: 200,
body: JSON.stringify({
item: result.Item,
}),
};
} catch (err) {
return {
statusCode: 200,
body: JSON.stringify({
error: err,
}),
};
}
};
}
The callback function should take take non inputty types, which should then remove the need to call the Pulumi SDK during your test suite. You can see an example here:
https://github.com/pulumi/examples/blob/258d3bad0a00020704743e37911c51be63c06bb4/aws-ts-lambda-efs/index.ts#L32-L40

Why mockReturnValueOnce seems not to work?

I'm trying to use Jest along Meteor, but I'm getting the following problem while trying to implement tests using Collection.find().fetch().
I'm mocking the function find and fetch, as you can see below. However, the value return by fetch is always the first one.
Shouldn't the value be different every time, since I'm using mockReturnValueOnce?
import { describe, test } from "#jest/globals";
import { Users } from "../../../collections"; // a Mongo collection
describe("#Users test suite", () => {
const mock1 = {
userId: "111aaa",
email: "user1#test.com",
};
const mock2 = {
userId: "222bbb",
email: "user1#test.com",
};
const mock3 = {
userId: "333ccc",
email: "user1#test.com",
};
test("Fetch test", () => {
Users.find.mockImplementation(() => ({
fetch: jest
.fn()
.mockReturnValueOnce(mock1)
.mockReturnValueOnce(mock2)
.mockReturnValueOnce(mock3),
}));
// getting same result in every fetch call
console.log(Users.find().fetch()); // { userId: '111aaa', email: 'user1#test.com' }
console.log(Users.find().fetch()); // { userId: '111aaa', email: 'user1#test.com' }
console.log(Users.find().fetch()); // { userId: '111aaa', email: 'user1#test.com' }
});
});
Thanks in advance!

Mongoose getters are either not working the way I want or I'm misunderstanding what they are

I created some sample code to demonstrate my issue on a smaller scale. From my understanding, a getter function will not affect anything on my database, but when I want to make a get request to view items on my database, it will change the value to whatever is returned only when the data is displayed. However, when I make my get request to view items on my database, the item I am shown is exactly how it was saved. I'm not sure if I'm misunderstanding what a getter function is, or if my syntax is just incorrect somewhere.
Here is my main server:
const express = require('express')
const mongoose = require('mongoose')
// Linking my model
const User = require('./User')
// Initializing express
const app = express()
const PORT = 9999
app.use(express.json())
// Connecting to mongodb
const connectDB = async () => {
try {
await mongoose.connect('mongodb://localhost/testdatabase', {
useUnifiedTopology: true,
useNewUrlParser: true
})
console.log('Connected')
} catch (error) {
console.log('Failed to connect')
}
}
connectDB()
// Creates a new user
app.post('/user/create', async (req, res) => {
await User.create({
name: 'John Cena',
password: 'somepassword'
})
return res.json('User created')
})
// Allows me to view all my users
app.get('/user/view', async (req, res) => {
const findUser = await User.find()
return res.json(findUser)
})
// Running my server
app.listen(PORT, () => {
console.log(`Listening on localhost:${PORT}...`)
})
Here is my model:
const mongoose = require('mongoose')
// My setter - initialPassword is 'somepassword'
// This seems to work properly, in my database the password is changed to 'everyone has the same password here'
const autoChangePassword = (initialPassword) => {
console.log(initialPassword)
return 'everyone has the same password here'
}
// My getter - changedPassword should be 'everyone has the same password here' I think
// The console.log doesn't even run
const passwordReveal = (changedPassword) => {
console.log(changedPassword)
return 'fakehash1234'
}
// Creating my model
const UserSchema = mongoose.Schema({
name: {
type: String
},
password: {
type: String,
set: autoChangePassword,
get: passwordReveal
}
})
// Exporting my model
const model = mongoose.model('user', UserSchema)
module.exports = model
Not sure if it would help anyone since I found my answer on another StackOverflow post, but the issue was I had to set getters to true when converting back to JSON:
// Creating my model
const UserSchema = mongoose.Schema({
name: {
type: String
},
password: {
type: String,
set: autoChangePassword,
get: passwordReveal
}
}, {
toJSON: { getters: true }
})
Any similar problems can be solved by adding some combination of the following:
{
toJSON: {
getters: true,
setters: true
},
toObject: {
getters: true,
setters: true
}
}

Using the $inc function across the MongoDB documents

I am currently working on a moderation system for my Discord bot and came across an unexpected issue. I've been using the $inc function to increase the values for a single document, though I have sadly not achieved to use the $inc function across multiple different documents, meaning I would like to increase ($inc) the value of the new document according to the numbers of the previous document.
Example: Cases
Current code:
async run(client, message, args, Discord) {
const targetMention = message.mentions.users.first()
const userid = args[0]
const targetId = client.users.cache.find(user => user.id === userid)
const username = targetMention.tag
if(targetMention){
args.shift()
const userId = targetMention.id
const WarnedBy = message.author.tag
const reason = args.join(' ')
if(!reason) {
message.delete()
message.reply('You must state the reason behind the warning you are attempting to apply.').then(message => {
message.delete({ timeout: 6000})
});
return;
}
const warningApplied = new Discord.MessageEmbed()
.setColor('#ffd200')
.setDescription(`A warning has been applied to ${targetMention.tag} :shield:`)
let reply = await message.reply(warningApplied)
let replyID = reply.id
message.reply(replyID)
const warning = {
UserId: userId,
WarnedBy: WarnedBy,
Timestamp: new Date().getTime(),
Reason: reason,
}
await database().then(async database => {
try{
await warnsSchema.findOneAndUpdate({
Username: username,
MessageID: replyID
}, {
$inc: {
Case: 1
},
WarnedBy: WarnedBy,
$push: {
warning: warning
}
}, {
upsert: true
})
} finally {
database.connection.close()
}
})
}
if(targetId){
args.shift()
const userId = message.member.id
const WarnedBy = message.author.tag
const reason = args.join(' ')
if(!reason) {
message.delete()
message.reply('You must state the reason behind the warning you are attempting to apply.').then(message => {
message.delete({ timeout: 6000})
});
return;
}
const warning = {
userId: userId,
WarnedBy: WarnedBy,
timestamp: new Date().getTime(),
reason: reason
}
await database().then(async database => {
try{
await warnsSchema.findOneAndUpdate({
userId,
}, {
$inc: {
Case: 1
},
WarnedBy: WarnedBy,
$push: {
warning: warning
}
}, {
upsert: true
})
} finally {
database.connection.close()
}
const warningApplied = new Discord.MessageEmbed()
.setColor('#ffd200')
.setDescription(`A warning has been applied to ${targetId.tag} :shield:`)
message.reply(warningApplied)
message.delete();
})
}
}
Schema attached to the Code:
const warnsSchema = database.Schema({
Username: {
type: String,
required: true
},
MessageID: {
type: String,
required: true
},
Case: {
type: Number,
required: true
},
warning: {
type: [Object],
required: true
}
})
module.exports = database.model('punishments', warnsSchema)
Answer to my own question. For all of those who are attempting to do exactly the same as me, there is an easier way to get this to properly work. The $inc (increase) function will not work as the main property of a document. An easier way to implement this into your database would be by creating a .json file within your Discord bot files and adding a line such as the following:
{
"Number": 0
}
After that, you'd want to "npm i fs" in order to read directories in live time.
You can proceed to add a function to either increase or decrease the value of the "Number".
You must make sure to import the variable to your current coding document by typing:
const {Number} = require('./config.json')
config.json can be named in any way, it just serves as an example.
Now you'd be able to console.log(Number) in order to make sure the number is what you expected it to be, as well as you can now increase it by typing Number+=[amount]
Hope it was helpful.