How to define array of objects in Sequelize.js in NestJS for Postgres? - nestjs

How can I define an array of objects field in Sequelize.js model in NestJS for Postgres?
I need something like this
{
userId: { type: String, required: true},
products: [
{
productId: {
type: String
},
quantity: {
type: Number,
default: 1
}
}
]
}
I tried to translate the above code to NestJS using Sequelize.js, but using Postman I sent post requests and got an untyped object in the products array. And I don't understand how to solve this problem.
import {Column, DataType, Model, Table} from "sequelize-typescript";
export interface IProductItem {
productId: string
quantity: number
}
interface ICartsCreationAttrs {
userId: number
products: Array<IProductItem>
}
#Table({tableName: 'carts'})
export class CartsModel extends Model<CartsModel, ICartsCreationAttrs> {
#Column({type: DataType.INTEGER, unique: true, autoIncrement: true, primaryKey: true})
id: number
#Column({type: DataType.INTEGER, unique: true, allowNull: false})
userId: number
#Column({type: DataType.ARRAY(DataType.JSON), allowNull: false})
products: Array<IProductItem>
}

For sequelize doesn't needs a type definition in an array or JSON object. You can define only column types.
(JSONB allows only for Postgress)
products: { type: JSONB, required: true}

Related

operator does not exist: character varying = integer in sequelize node js

I am trying to connect two tables with sequelize, but the primary key and foreign key have different data types,
I am getting errors when I triggered the query. it is not possible to change the schema, it will affect the whole data.
Can you provide some possible solutions to fix this error?
The only way to join them in findAll/findOne calls is to use the on option in the include option:
const items = Items.findAll({
include: [{
model: ChildItem,
on: {
// assuming that childId is an integer and parentId is a string
childId: Sequelize.cast('parentId', 'integer')
}
}]
})
I stumbled on the same issue here is how i resolved it:
The cause of the issue is that Sequlize doing typecasting from integer to string when you create in one schema primaryKey as id and give it type of integer and in another schema you use it as secondaryKey with alias for instance userId so when you reference to connected schema from main schema you receive error operator does not exist: character varying = integer
Code examples (i am using Sequilze-typescript):
main schema where id is id
#Table({ tableName: 'users' })
export class User extends Model<User,> {
#Column({
type: DataType.INTEGER,
primaryKey: true,
unique: true,
autoIncrement: true
})
id: number
#HasMany(() => Post)
posts:Post[]
}
secondary schema that uses User id as secondaryKey
#Table({ tableName: 'posts' })
export class Task extends Model<Task, TaskCreationAttributes> {
#Column({
type: DataType.INTEGER,
primaryKey: true,
unique: true,
autoIncrement: true
})
id: number
#BelongsTo(() => User)
creator: User
#ForeignKey(() => User)
#Column({ type: DataType.INTEGER, allowNull: false })
userId: number
}
so here even-through we explicitly telling that our secondaryKey is number when we query User schema, Sequlize casts id -> userId, integer -> string
so in order to prevent the typecasting from id -> userId we can change
main schema(User) to
#Column({
type: DataType.INTEGER,
primaryKey: true,
unique: true,
autoIncrement: true
})
userId: number
and secondary(Post) schema to:
#Column({
type: DataType.INTEGER,
primaryKey: true,
unique: true,
autoIncrement: true
})
postId: number
#BelongsTo(() => User)
creator: User
#ForeignKey(() => User)
#Column({ type: DataType.INTEGER, allowNull: false })
userId: number
so no collision and typecasting will be done

'number' only refers to a type, but is being used as a value here

src/db/models/point.ts:10:11 - error TS2693: 'number' only refers to a type, but is being used as a value here.
const PointSchema: Schema = new Schema({
id: {
type: String,
required: true,
unique: true,
index: true,
},
point: {
type: number,
required: true,
},
});
export interface PointProp extends Document {
id: string;
point: number;
}
export default model<PointProp>('point', PointSchema);
You are using a TypeScript type inside an object, as value, just as what the error says. You should have given more info, but I am guessing you are working with Mongoose. In that case, number (the TypeScript type) should be Number (the object)
const PointSchema: Schema = new Schema({
id: {
type: String,
required: true,
unique: true,
index: true,
},
point: {
type: Number, // <---
required: true,
},
});
See https://mongoosejs.com/docs/guide.html#definition for more information.
It's just a typo. change number to Number
point: {
type: **Number**,
required: true,
},

is there any object to object Mapper for node.js?

I was hoping if there is any way to dynamically map my API's response to my model class in node js. for example I have a model dynamically defined using sequelize-Auto which looks like this
return sequelize.define('clients', {
id: {
type: Sequelize.STRING(255),
allowNull: false,
primaryKey: true
},
dateCreated: {
type: Sequelize.DATE,
allowNull: true
},
dateModified: {
type: Sequelize.DATE,
allowNull: true
},
createdBy: {
type: Sequelize.STRING(255),
allowNull: true
},
createdByName: {
type: Sequelize.STRING(255),
allowNull: true
},
modifiedBy: {
type: Sequelize.STRING(255),
allowNull: true
},
modifiedByName: {
type: Sequelize.STRING(255),
allowNull: true
},
c_code: {
type: Sequelize.TEXT,
allowNull: true
},
c_name: {
type: Sequelize.TEXT,
allowNull: true
},
...
...
...
}
and this is how I am mapping the response from my API but if I change the model(which happens dynamically) I have to manually update the fields in my controller as shown the code below
const client = {
id: req.body.id,
dateCreated: req.body.dateCreated,
dateModified: req.body.dateModified,
createdBy: req.body.createdBy,
createdByName: req.body.createdByName,
modifiedBy: req.body.modifiedBy,
modifiedByName: req.body.modifiedByName,
c_code: req.body.c_code,
...
...
...
};
models.clients
.upsert(client);
I was hoping if there was a way where I can extract my model object from res.body dynamically something like automapper from .net
Take a look at AutoMapper. It has first-class support for Sequelize to automatically map to your models.
Alternatively, if you don't want to define a DTO but just take the req.body and map it to your model, you could use class-transformer with the excludeExtraneousValues option to discard any extra props.
You might need to create new modal entity class where you can pass the body object and inside it's constructor use Object.assign as below code snippet.
class ClientEntity {
id;
dateCreated;
dateModified;
createdBy;
constructor(data) {
Object.assign(this, data);
}
}
//assuming below request object
let req = {
body: {
id: 1,
dateCreated: new Date(),
dateModified: new Date(),
createdBy: 'John',
}
}
let client = new ClientEntity(req.body)
console.log(client);

nestjs mongoose schema with embedded objects without embedded documents

I am creating a schema that included embedded objects (not documents). I do not need embedded documents because the embedded objects do not exist independently and I do not need associated object IDs. This is not well documented in the Nest documentation but I came up with this which is working for the most part:
const pointType = {
coordinates: { type: [Number], required: true },
type: { type: ['Point'], required: true },
}
const locationType = {
name: String,
address: addressType,
geolocation: { pointType },
}
const timedLocationType = {
timestamp: { type: Date, required: true, default: Date.now() },
geolocation: { type: pointType, required: true }, // <<---- Problem is here with the 'required' attribute
}
export class TripLocation {
name: string
geolocation: Point
address?: Address
}
export class TimedLocation {
time: Date
geolocation: Point
}
#Schema()
export class Trip {
#Prop({ type: locationType, required: true })
start: TripLocation
#Prop({ type: locationType, required: true })
end: TripLocation
#Prop({ type: [timedLocationType], default: [] }) // <<-- Problem is here
route: TimedLocation[]
}
This works except for having the timedLocation array. Mongoose does not seem to like having the route property as an array. As is, the code throws this error:
.../node_modules/mongoose/lib/schema.js:1001
throw new TypeError(`Invalid schema configuration: \`${name}\` is not ` +
^
TypeError: Invalid schema configuration: `True` is not a valid type at path `geolocation.required`. See .../mongoose-schematypes for a list of valid schema types.
at Schema.interpretAsType (.../node_modules/mongoose/lib/schema.js:1001:11)
If I remove the required attribute on the timedLocationType.geolocation, the error goes away. Alternatively, if I turn the route property from an array to a single object, and keep the required` attribute, the error also goes away:
#Prop({ type: timedLocationType })
route: TimedLocation

Sequelize create through association

I'm working on a create method for an association between two classes. The sequelize documentation indicates that this can be done in one step using includes
IntramuralAthlete.create(intramuralAthlete,{
include: [Person]
}).then((data,err)=>{
if(data)res.json(data);
else res.status(422).json(err);
}).catch(function(error) {
res.status(422).json({message: "failed to create athlete", error: error.message});
});
My model association looks like this
var Person = require('../models').person;
var IntramuralAthlete = require('../models').intramuralAthlete;
IntramuralAthlete.belongsTo(Person);
And the value of intramural athlete when I log it is
{
person:
{ firstName: 'Test',
lastName: 'User',
email: 'test#user.com'
},
grade: '12th',
organizationId: 1
}
But I get the error notNull Violation: personId cannot be null. This error makes it sound like something is wrong with the way I'm indicating to Sequelize that I'm intending to create the personId in that same call.
Is there something wrong in the way I indicate to the create statement what associated tables to create with the IntramuralAthlete?
Thanks!
EDIT:
I have also tried with the following structure with the same result
{
Person: {
firstName: 'Test',
lastName: 'User',
email: 'test#user.com'
},
grade: '12th',
organizationId: 1
}
My model is as follows:
module.exports = function(sequelize, DataTypes) {
return sequelize.define('intramuralAthlete', {
id: {
type: DataTypes.INTEGER(11),
allowNull: false,
primaryKey: true,
autoIncrement: true
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: sequelize.literal('CURRENT_TIMESTAMP')
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: sequelize.literal('CURRENT_TIMESTAMP')
},
grade: {
type: DataTypes.STRING,
allowNull: true
},
age: {
type: DataTypes.INTEGER(11),
allowNull: true
},
school: {
type: DataTypes.STRING,
allowNull: true
},
notes: {
type: DataTypes.STRING,
allowNull: true
},
guardianId: {
type: DataTypes.INTEGER(11),
allowNull: true,
references: {
model: 'contact',
key: 'id'
}
},
personId: {
type: DataTypes.INTEGER(11),
allowNull: false,
references: {
model: 'person',
key: 'id'
}
},
mobileAthleteId: {
type: DataTypes.INTEGER(11),
allowNull: true,
references: {
model: 'mobileAthlete',
key: 'id'
}
},
organizationId: {
type: DataTypes.INTEGER(11),
allowNull: false,
references: {
model: 'organization',
key: 'id'
}
}
}, {
tableName: 'intramuralAthlete'
});
};
I suppose that your models are named Person and IntramuralAthlete (first arguments of sequelize.define method). In this case, when you create an association like yours, and do not define the as attribute, your create data object should look as follows
{
Person: {
firstName: 'Test',
lastName: 'User',
email: 'test#user.com'
},
grade: '12th',
organizationId: 1
}
If you want to use person instead (just as in your code), you should define the association a little bit differently
IntramuralAthlete.belongsTo(Person, { as: 'person' });
Then, you would have to perform some changes in the create query in the include attribute of the options like this
IntramuralAthlete.create(data, {
include: [
{ model: Person, as: 'person' }
]
}).then((result) => {
// both instances should be created now
});
EDIT: Trick the save() method with empty value of personId
You can maintain the allowNull: false if you do something like that
{
person: {
// person data
},
personId: '', // whatever value - empty string, empty object etc.
grade: '12th',
organizationId: 1
}
EDIT 2: Disable validation when creating.
This case assumes that the validation is turned off. It seems like a bad idea to omit model validation, however there still maintains the database table level validation - defined in migrations, where it can still check if personId value was set
IntramuralAthlete.create(data, {
include: [
{ model: Person, as: 'person' }
],
validate: false
}).then((result) => {
// both instances should be created now
});
In this case the data object can be as in your example - without the personId attribute. We omit the model level validation which allows to pass null value, however if during the save() method it would still be null value - database level validation would throw an error.
first of all, when you associatea a model with belongsTo, sequelize will add automatically the target model primary key as a foreign key in the source model. in most of cases you don't need to define it by yourself, so in your case when you define IntramuralAthlete.belongsTo(Person) sequelize adds PersonId as a foreign key in IntramuralAthlete. your IntramuralAthlete model should looks like:
module.exports = function(sequelize, DataTypes) {
return sequelize.define('intramuralAthlete', {
grade: {
type: DataTypes.STRING,
allowNull: true
},
age: {
type: DataTypes.INTEGER(11),
allowNull: true
},
school: {
type: DataTypes.STRING,
allowNull: true
},
notes: {
type: DataTypes.STRING,
allowNull: true
}
});
};
now you can create an intramuralAthlete like your code above. for example:
let data = {
Person: {
firstName: 'Test',
lastName: 'User',
email: 'test#user.com'
},
grade: '12th',
notes: 'test notes'
}
IntramuralAthlete.create(data, {include: [Person]}).then((result) => {
// both instances should be created now
});
be carefull with the model name.
second I suppose that your IntramuralAthlete model has more than one belongsTo association. just you need to define them as the previous one association and sequelize will add their primary keys as foreign keys in the IntramuralAthlete model.
third, when you define a model, sequelize adds automatically an id datafield as a primary key and autoincrement and also adds createdAt and updatedAt datafields with a default CURRENT_TIMESTAMP value, so you don't need to define them in your model

Resources