Im havving trouble building an express api in TS node.
Im new at express and im learning Node, JS and TS since 2022 so im sorry if the question is not to complex.
The thing is that im tryng to build a class Controller that handles the needed instructions for every route in my express router.
And im passing through the constructor a DAO that ive builded to access the firebase firestore database.
But when i instanciate the object and i try to run it Gives me the cannot read properties of undefined error.
Even when ive found a solution by using a clousure i want to learn how to do this using classes
Here it is the code of the DAO
import { setDoc, doc, getDocs, collection, query, where, deleteDoc } from 'firebase/firestore'
import { getStorage, ref, uploadBytes, getDownloadURL } from 'firebase/storage'
import { DataResponse, GenericItem } from '../types'
import { DAO } from '../clases/abstractClasses'
import { v4 } from 'uuid'
import db from '../config/firebase'
// import fs from 'fs/promises'
const fs = require('fs').promises
const storage = getStorage()
export class DataResponseClass implements DataResponse {
data: GenericItem[]
status: number
statusText: string
err: string
ok: boolean
constructor (data: GenericItem[], status: number, statusText: string, err: string, ok: boolean) {
this.data = data
this.status = status
this.statusText = statusText
this.err = err
this.ok = ok
}
}
export class DbManager extends DAO {
constructor (collectionRef: string) {
super(collectionRef)
}
async addItem (item: GenericItem): Promise<DataResponse> {
const id = v4()
console.log(id, typeof id)
return await setDoc(doc(db, this.collectionRef, id), { ...item, id }).then(res => {
return new DataResponseClass([{ ...item, id }], 201, 'Item added successfully', '', true)
}).catch(err => {
return new DataResponseClass([item], 400, "Couldn't add item", err.toString(), false)
})
}
async getAll (): Promise<DataResponse> {
return await getDocs(collection(db, this.collectionRef)).then(response => {
const dataArray: any = []
response.forEach(item => dataArray.push(item.data()))
return new DataResponseClass(dataArray, 200, 'Information obtained', '', true)
}).catch(err => new DataResponseClass([], 400, 'Couldnt Retrieve data', err.toString(), false))
}
async getById (passedId: string): Promise<DataResponse> {
const q = query(collection(db, this.collectionRef), where('id', '==', passedId))
return await getDocs(q)
.then(res => {
const dataArray: any[] = []
res.forEach(item => {
dataArray.push(item.data())
})
if (dataArray.length === 0) throw new Error('No data found for the id')
return new DataResponseClass(dataArray, 200, 'Information obtained', '', true)
})
.catch(err => new DataResponseClass([], 400, 'Couldnt Retrieve data', err.toString(), false))
}
async updateById (id: string, item: GenericItem): Promise<DataResponse> {
return await setDoc(doc(db, this.collectionRef, id), item)
.then(() => new DataResponseClass([{ ...item, id }], 200, 'Item succesifuly updated', '', true))
.catch(err => new DataResponseClass([], 400, 'Couldnt update item', err.toString(), false))
}
async deleteByid (id: string): Promise<DataResponse> {
return await deleteDoc(doc(db, this.collectionRef, id))
.then(() => new DataResponseClass([], 200, 'Success deleting a document', '', true))
.catch(err => new DataResponseClass([], 400, 'Couldnt Delete data', err, false))
}
async upLoadFile (file: Express.Multer.File | undefined): Promise<string> {
if (file !== undefined) {
const buffer = await fs.readFile(file.path).then()
const reference = ref(storage, `/${this.collectionRef}/${file.filename}`)
try {
await uploadBytes(reference, buffer)
return await getDownloadURL(reference)
} catch (err: any) {
console.log(err)
return 'There was an error uploading the file'
}
}
return 'No file was uploaded'
}
}
And this is the Controller class code..
import colors from 'colors'
import { Request, Response } from 'express'
import { DbManager, DataResponseClass } from '../services/firebase'
import fs from 'fs/promises'
export class Controller {
protected readonly dbManager: DbManager
constructor (collection: string) {
this.dbManager = new DbManager(collection)
}
async readData (req: Request, res: Response): Promise<void> {
const id: string = req.params.id
if (id !== undefined) {
res.send(await this.dbManager.getById(id))
} else {
res.send(await this.dbManager.getAll())
}
}
async createData (req: Request, res: Response): Promise<void> {
if (req.file !== undefined) {
const uploadedFilePath = await this.dbManager.upLoadFile(req.file)
.then((response: any) => {
console.log(`${response}/${req.file?.filename || ' '}`)
if (req.file?.path !== undefined) {
fs.unlink(req.file.path).then(() => console.log('Upload Complete')).catch(err => console.log(err))
}
return `${response}`
})
.catch((err: any) => {
console.log(err)
res.send(false)
})
const data = { ...req.body, images: uploadedFilePath }
console.log(colors.bgRed.white(data))
res.send(await this.dbManager.addItem({ ...req.body, images: uploadedFilePath }))
} else res.send(new DataResponseClass([], 400, 'Invalid Request no image uploaded', 'Invalid Request no image uploaded', false))
}
async editData (req: Request, res: Response): Promise<void> {
const { id } = req.params
if (req.file !== undefined) {
const uploadedFilePath = await this.dbManager.upLoadFile(req.file)
.then((response: any) => {
if (req.file?.path !== undefined) {
fs.unlink(req.file.path).then(() => console.log('Upload Complete')).catch(err => console.log(err))
}
return `${response}`
})
.catch((err: { toString: () => string }) => {
console.log(err)
res.send(new DataResponseClass([], 400, 'Imposible to upload the file', err.toString(), false))
})
res.send(await this.dbManager.updateById(id, { ...req.body, images: uploadedFilePath }))
} else res.send(new DataResponseClass([], 400, 'Invalid Request no image uploaded', 'Invalid Request no image uploaded', false))
}
async deleteData (req: Request, res: Response): Promise<void> {
const { id } = req.params
if (id !== undefined) {
res.send(await this.dbManager.deleteByid(id))
} else res.send(new DataResponseClass([], 400, 'Invalid Request no id', 'Invalid Request no id', false))
}
}
Thanks for your time Im tryng to learn this beautifull world that is the backend development
Ive tryed to call the constructor outside the class and pass the constant to the constructor
Ive tryed to instanciate the DAO object as a param,Ive even called the dao constructor in a global variable and defined the properti taking value from it .
But the only solution ive found for my issue es transforming the class into a clousure function and calling the constructor in the body of the closure
TypeError: Cannot read properties of undefined (reading 'dbManager')
at /run/media/adrianabadin/code/dcsbackend/src/controllers/controllerClass.ts:20:27
at Generator.next (<anonymous>)
at /run/media/adrianabadin/code/dcsbackend/src/controllers/controllerClass.ts:8:71
at new Promise (<anonymous>)
at __awaiter (/run/media/adrianabadin/code/dcsbackend/src/controllers/controllerClass.ts:4:12)
at readData (/run/media/adrianabadin/code/dcsbackend/src/controllers/controllerClass.ts:24:16)
at Layer.handle [as handle_request] (/run/media/adrianabadin/code/dcsbackend/node_modules/express/lib/router/layer.js:95:5)
at next (/run/media/adrianabadin/code/dcsbackend/node_modules/express/lib/router/route.js:144:13)
at Route.dispatch (/run/media/adrianabadin/code/dcsbackend/node_modules/express/lib/router/route.js:114:3)
at Layer.handle [as handle_request] (/run/media/adrianabadin/code/dcsbackend/node_modules/express/lib/router/layer.js:95:5)
[ERROR] 16:55:20 TypeError: Cannot read properties of undefined (reading 'dbManager')
Routes
Here I call the method readData
import { Router } from 'express'
import { Controller } from '../controllers/controllerClass'
// import { Validation } from '../services/validation'
import { upload } from '../config/multer'
const router = Router()
// const { validate } = new Validation('welcome')
const { readData, createData, editData, deleteData } = new Controller('welcome')
router.get('/', readData)
router.get('/:id', readData)
router.post('/', upload.single('images'), createData)
router.put('/:id', upload.single('images'), editData)
router.delete('/:id', deleteData)
export default router
const { readData, createData, editData, deleteData } = new Controller('welcome')
You can't destructure normally declared instance methods from classes.
I'm going to vastly simplify this to this example:
class Foo {
private data = 123
getData() { return this.data }
}
Now if you call getData like so, it works:
const foo = new Foo()
console.log(foo.getData())
// 123
But if you destructure the method:
const { getData } = new Foo()
console.log(getData())
// Cannot read properties of undefined (reading 'data')
Then it crashes.
The value of this is being lost because you don't call it with a ., which is what provides the class instance to the function.
Now let's try this one:
class Foo {
private data = 123
getData = () => { return this.data }
}
const foo = new Foo()
console.log(foo.getData())
// 123
const { getData } = new Foo()
console.log(getData())
// 123
This works because typescript compiles property assignments in classes to happen in the constructor, and because the arrow function => captures the value of this from when it was declared. So now you can break off the method and it works.
Just note that while the traditional instance method declaration is shared between all instances, this arrow function method will create a new function for every instance. This may hurt performance if you plan to create a very large number of instances. But for backend service classes like this that's probably not a concern.
So in your case just call the method on the instance:
const controller = new Controller('welcome')
router.get('/', (req, res) => controller.readData(req, res))
Or you declare your method as a arrow function.
export class Controller {
//...
readData = async (req: Request, res: Response): Promise<void> => {
const id: string = req.params.id
if (id !== undefined) {
res.send(await this.dbManager.getById(id))
} else {
res.send(await this.dbManager.getAll())
}
}
//...
}
const { readData } = new Controller()
readData() // fine now
How can I resolve this? I put req.query parameter with an array but I can't destructure or use items from my array. I getting on my next.js API backend just [object Object] or undefined. How can I select what I want?
const fetchData = async (queryKey: any, manufacturer) => {
const res = await fetch(
`http://localhost:3000/api/data/get?&manufacturer=${manufacturers}`
);
return await res.json();
};
const manufacturers = [
{ name: 'Samsung', type: 'TV' },
{ name: 'Nokia', type: 'Phone' },
];
const { data, status } = useQuery(
['somekey', manufacturer],
({ queryKey }) => fetchData(queryKey, manufacturer)
);
And here is the next API where I can't get values from the query I getting just
[Object, Object] or undefined,
this code work if just put on the front end just a simple array or string without an object in variable manufacturers.
But how i can get values from [Object, object]?
I try it
const { name, type } = req.query.manufacturer
const { name, type } = req.query.manufacturer[0]
or select just one field dont work to = const some = req.query.manufacturer[0].name
export const handler = async (
req: NextApiRequest,
res: NextApiResponse<Data>
) => {
const { name, type } = req.query.manufacturer;
};
export default handler;
Could you try const [{ name, type }] = req.query.manufacturer? Seems to be what you need
edit
Also, you may want to try instead of http://localhost:3000/api/data/get?&manufacturer=${manufacturers} => http://localhost:3000/api/data/get?&manufacturer=${encodeURIComponent(JSON.stringify(manufacturers))}
It probably will be accessible with { name, type } = req.query.manufacturer then
I am trying to set up a dynamic page in Next.js with getStaticPaths(). I have it 90% of the way there, but I can't get the getStaticProps() function to reference the URL query so that it loads the proper info.
My code is:
//Get Static Paths
export async function getStaticPaths() {
const paths = await (
await youtube.get("search?")
).data.items.map((video: any) => {
const id = video.id.videoId;
return { params: { id } };
});
return {
paths: paths.map((path: any) => path),
fallback: false,
};
}
//Get Static Props
export async function getStaticProps(context: any) {
const { query = "" } = context.params.query;
const videos = await (await youtube.get("search?")).data.items;
const video = videos.find((vid: any) => {
return vid.id.videoId === query;
});
return {
props: {
video,
},
revalidate: 10,
};
}
I am receiving the error:
Error: Error serializing `.video` returned from `getStaticProps` in "/media/videos/[id]".
Reason: `undefined` cannot be serialized as JSON. Please use `null` or omit this value.
If I replace the 'query' variable in the getStaticProps() function with the actual ID of the video I am trying to load, everything goes through fine and the page loads. But then this is not dynamic, and is not using the url query to match with the proper video like I want.
Here is the working, non-dynamic code with a manually typed in ID to force a match.
//Get Static Props
export async function getStaticProps(context: any) {
// It's important to default the slug so that it doesn't return "undefined"
const { query = "" } = context.params;
const videos = await (await youtube.get("search?")).data.items;
const video = videos.find((vid: any) => {
return vid.id.videoId === "_TACZtT1irI";
});
return {
props: {
video,
},
revalidate: 10,
};
}
How can I make sure that the 'video' variable is accessing the url query? It should be grabbing it with context.params, but that is not working, what am I doing wrong?
I figured it out. This is working.
//Get Static Props
export async function getStaticProps(context: any) {
// It's important to default the slug so that it doesn't return "undefined"
const { query = "" } = context.params;
const videos = await (await youtube.get("search?")).data.items;
const video = videos.find((vid: any) => {
return vid.id.videoId === context.params.id;
});
return {
props: {
video,
},
revalidate: 10,
};
}
Right now I have to return my data like this way, with this code:
import middy from '#middy/core';
import httpErrorHandler from '#middy/http-error-handler';
import jsonBodyParser from '#middy/http-json-body-parser';
async function sampleFunction (event) {
const result = await someCodeWith(event.body);
return result;
}
export const handler = middy(sampleFunction)
.use(jsonBodyParser())
.use(httpErrorHandler());
But I'm getting an error as a response:
{
"message": "Internal server error"
}
so I need to return the data inside this object:
return {
statusCode: 200,
body: JSON.stringify(error)
};
But I thought that by using middy I didn't need to create that object, and it would be already stringified and also with a statusCode inside. I don't want to set manually each statusCode for 200, 400, 500, etc. Is there a way to only return data with data as a javascript object?
This works with Middy 2:
const strigifyResponse = () => ({
after: async (request) => {
request.response = {
statusCode: 200,
body: JSON.stringify(request.response)
}
}
})
Of course, you need to use it the same way:
export const handler = middy(sampleFunction)
.use(strigifyResponse())
.use(jsonBodyParser())
.use(httpErrorHandler());
Middy can be used by multiple event contexts. In this case API Gateway requires the response to follow the following pattern as you described. You can add in your own middleware to transform the response before it's returned.
Something like this:
// middy v2
const strigifyResponseV2 = () => {
return {
after: (request) => {
request.response = {
statusCode: 200,
body: JSON.stringify(request.response)
}
}
}
}
// middy v1
const strigifyResponseV1 = () => {
return {
after: (handler, next) => {
handler.response = {
statusCode: 200,
body: JSON.stringify(handler.response)
}
next()
}
}
}
export const handler = middy(sampleFunction)
.use(strigifyResponseV2())
.use(jsonBodyParser())
.use(httpErrorHandler());
I am writing tests and I do not know js well, but I need to find the answer quickly. In the courses, I have not reached this point yet, or maybe I just don’t understand. Please help me figure out what I'm doing wrong.
I have two questions:
The test starts working as soon as I delete the line:
.then((response) => {
authId = response.body.result.sessions[0].authId;
});
if you do not delete this line, an error appears:
TypeError: Cannot read property 'sessions' of undefined
How to write so that the fields are optional, that is, you can pass, or you can not pass
(props: { authId: string; deviceId: string })
This is the response.body I am trying to parse:
{
"result": {
"sessions": [
{
"type": "web",
"authId": "jRXtO7oNiBR5Ldeq",
"platform": "platform",
"application": "application",
"seenAt": 1592052380
}
],
"integrations": []
},
"success": true
}
My code:
import { agent } from 'supertest';
import { config } from '../../config';
import { getSsoId } from '../../helpers/getUserFromGenesis';
describe('First', () => {
let id;
let authId;
beforeEach(async () => {
id = await getId();
});
const userAuthorizations = (fn: (response: any) => void) =>
agent(config.baseUrl)
.get(`users/${id}/authorizations?client_id=test`)
.auth(config.user, config.pass)
.expect((response) => {
fn(response);
});
const deleteUserAuthorizations = (props: { authId: string; deviceId: string }) =>
agent(config.baseUrl)
.delete(`users/authorizations`)
.auth(config.user, config.pass)
.send(props)
.expect((response) => {
expect(response.body.result.success).toEqual(true);
});
const getSession = () =>
agent(config.tokenQaUrl)
.post(`/token`)
.auth(config.user, config.pass)
.send({
clientId: 'test',
userId: id,
})
.expect((response) => {
expect(response.body.sessionId).not.toEqual('');
expect(response.body.sessionId).not.toEqual(null);
})
.then((response) => {
authId = response.body.result.sessions[0].authId;
});
it('test', async () => {
await getSession().then(() =>
userAuthorizations((response) => {
expect(response.body.result.sessions.length).toBeGreaterThan(0);
}),
);
});
});
As discussed in the comment
It seems like some time your response is getting the JSON data in the way which you are trying to access, but some time its not. Its better to add conditions before trying to access data where the JSON is not of fixed structure.
To make these property optional just add ?
it will make the properties option
props: { authId?: string; deviceId?: string }
I am a bit unclear on your question. There are some good answers up top.
In case you are trying to avoid a "property of undefined error" you can do something like this:
authId = response?.body?.result?.sessions[0]?.authId;
it prevents the error. Ideally you would type-check all of them.