Associations not working for Sequelize - node.js

I run the below mutation first, creates the fish, I also created a river all ready. Why are rivers and fishes empty?
Graphql:
query {
fishes {
name
species
rivers {
name
}
}
river(id: 1) {
name
fishes {
name}
}
}
# mutation {
# createFish (name: "Orange-Vall Smallmouth Yellowfish", species: "Labeobarbus aeneus", riverId: 1) {
# name
# rivers {
# name
# }
# }
# }
The results are below:
{
"data": {
"fishes": [
{
"name": "Orange-Vall Smallmouth Yellowfish",
"species": "Labeobarbus aeneus",
"rivers": []
}
],
"river": {
"name": "Orange River",
"fishes": []
}
}
}
Below are the two models:
const Fish = sequelize.define('fish', schema);
Fish.associate = models => {
Fish.belongsToMany(models.River, { through: 'RiverFish' });
};
const River = sequelize.define('river', schema);
River.associate = models => {
River.belongsToMany(models.Fish, { through: 'RiverFish', as: 'Fishes' });
};
Part of the graphql query:
River: {
fishes: river => river.getFishes(), },
Fish: {
rivers: fish => fish.getRivers(), },
Sequelize logs:
Executing (default): CREATE TABLE IF NOT EXISTS `fishes` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` VARCHAR(255), `species` VARCHAR(255) DEFAULT NULL, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL);
Connection has been established successfully.
Executing (default): PRAGMA INDEX_LIST(`fishes`)
Executing (default): CREATE TABLE IF NOT EXISTS `rivers` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` VARCHAR(255), `alternative` VARCHAR(255) DEFAULT NULL, `geojson` JSON, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL);
Executing (default): PRAGMA INDEX_LIST(`rivers`)
Executing (default): CREATE TABLE IF NOT EXISTS `RiverFish` (`createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, `fishId` INTEGER NOT NULL REFERENCES `fishes` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, `riverId` INTEGER NOT NULL REFERENCES `rivers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY (`fishId`, `riverId`));
Executing (default): PRAGMA INDEX_LIST(`RiverFish`)
Executing (default): PRAGMA INDEX_INFO(`sqlite_autoindex_RiverFish_1`)
EDIT
If I change my models the results are working from the River side of things:
River.hasMany(models.Fish, { as: 'Fishes' });
Fish.belongsToMany(models.River, { through: 'RiverFish', as: 'Rivers' });
{
"data": {
"fishes": [
{
"id": "1",
"name": "Orange-Vall Smallmouth Yellowfish",
"species": "Labeobarbus aeneus",
"rivers": []
}
],
"river": {
"name": "Orange River",
"fishes": [
{
"name": "Orange-Vall Smallmouth Yellowfish"
}
]
},
"rivers": null
}
UPDATE
I believe this could be the issue. I have this in a my models directory:
import Fish from './Fish';
import River from "./River";
const models = {
Fish,
River
};
Object.keys(models).forEach(name => {
const model = models[name];
if (typeof model.associate === 'function') {
model.associate(models);
}
});
export {
Fish, River,
};

Related

Migrations: queryrunner instead sql statements using TypeORM

Can I generate a migration file with objects from my Entities? Just like:
export class CreateUsers1613877849611 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'users',
columns: [
{
name: 'id',
type: 'int',
isPrimary: true,
},
Instead:
await queryRunner.query("CREATE TABLE `users` (`id` int NOT NULL AUTO_INCREMENT, `isDeleted` tinyint NULL DEFAULT 0, `createdAt` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updatedAt` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), `name` varchar(255) NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB");

Sequelize Update with Object

I have a code using sequelize to update one record and then return this updated record:
const companyId = req.params.id
const companyNewDetail = req.body
console.log("companyNewDetail", companyNewDetail)
await Company.update(companyNewDetail, {
where: {
companyId,
},
})
const company = await Company.findOne({
where: {
companyId,
},
})
res.status(200).send({ status: 0, data: company })
It is working but I have to seperate into two query statements. Is there a way I can mix them together? I want to pass the update parameter with a object rather than destructuring the object and assign to the instance one by one.
The returning option of Model.update still only supports PostgreSQL for sequelize v6.
For example:
import { sequelize } from '../../db';
import { Model, DataTypes, Sequelize } from 'sequelize';
// const sequelize = new Sequelize('sqlite::memory:');
class Company extends Model {}
Company.init(
{
companyId: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
name: DataTypes.STRING,
},
{ sequelize, tableName: 'companies' },
);
(async function test() {
try {
await sequelize.sync({ force: true });
// seed
await Company.create({ name: 'google' });
// test
const companyId = 1;
const companyNewDetail = { name: 'reddit' };
const result = await Company.update(companyNewDetail, {
where: {
companyId,
},
returning: true,
});
console.log(result);
} catch (error) {
console.log(error);
} finally {
await sequelize.close();
}
})();
If you use postgres dialect, you will get the following result:
Executing (default): DROP TABLE IF EXISTS "companies" CASCADE;
Executing (default): DROP TABLE IF EXISTS "companies" CASCADE;
Executing (default): CREATE TABLE IF NOT EXISTS "companies" ("companyId" SERIAL , "name" VARCHAR(255), PRIMARY KEY ("companyId"));
Executing (default): SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND t.relkind = 'r' and t.relname = 'companies' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;
Executing (default): INSERT INTO "companies" ("companyId","name") VALUES (DEFAULT,$1) RETURNING *;
Executing (default): UPDATE "companies" SET "name"=$1 WHERE "companyId" = $2 RETURNING *
[
1,
[
Company {
dataValues: [Object],
_previousDataValues: [Object],
_changed: {},
_modelOptions: [Object],
_options: [Object],
isNewRecord: false
}
]
]
The first element of the result array is the number of affected rows for the updating operation. The second element is the sequelize model instances of updated rows.
For sqlite3, you will get:
Executing (default): DROP TABLE IF EXISTS `companies`;
Executing (default): DROP TABLE IF EXISTS `companies`;
Executing (default): CREATE TABLE IF NOT EXISTS `companies` (`companyId` INTEGER PRIMARY KEY AUTOINCREMENT, `name` VARCHAR(255), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL);
Executing (default): PRAGMA INDEX_LIST(`companies`)
Executing (default): INSERT INTO `companies` (`companyId`,`name`,`createdAt`,`updatedAt`) VALUES (NULL,$1,$2,$3);
Executing (default): UPDATE `companies` SET `name`=$1,`updatedAt`=$2 WHERE `companyId` = $3
[ undefined, 1 ]
So, you need to do an additional query for other databases after updating the rows in the table.

sequelize model exclude subfield within a jsonb field

I have a postgresql table structured with a jsonb field named external_data:
external_data: {a: 1, b:2, c:3}
I know that in sequelize model definitions, you can exclude a field by including in the options properties a defaultScope like this:
{defaultScope: {attributes: { exclude: ['external_data'] }}
When I do that to exclude the entire field, it works, i.e. external_data doesn't show up in returned results. However, I'd like to exclude a specific subfield. I've tried exclude: ['external_data.b'] but that doesn't seem to work.
Does sequelize allow excluding subfields within jsonb fields? And if so, what's the proper format of the exclude attribute to do it?
From the docs: https://sequelize.org/v5/manual/scopes.html
Scopes are defined in the model definition and can be finder objects, or functions returning finder objects - except for the default scope, which can only be an object
So, you can define the scope via a function that returns the finder objects so that you can implement the complex scope.
E.g.
import { sequelize } from '../../db';
import { Model, DataTypes, Op } from 'sequelize';
class TableA extends Model {}
TableA.init(
{
external_data: DataTypes.JSONB,
},
{
sequelize,
modelName: 'table_a',
defaultScope: {
attributes: { exclude: ['external_data'] },
},
scopes: {
exclude(subField) {
return {
where: {
external_data: {
[subField]: {
[Op.eq]: null,
},
},
},
};
},
},
},
);
(async function test() {
try {
// create tables
await sequelize.sync({ force: true });
// seed
await TableA.bulkCreate([
{ external_data: { a: 1, b: 2, c: 3 } },
{ external_data: { a: 10, c: 30 } },
{ external_data: { a: 100 } },
]);
// test
// test case 1: exclude subfield "b"
const result1 = await TableA.scope({ method: ['exclude', 'b'] }).findAll({ raw: true });
console.log('result1:', result1);
// test case 2: exclude subfield "c"
const result2 = await TableA.scope({ method: ['exclude', 'c'] }).findAll({ raw: true });
console.log('result2:', result2);
} catch (error) {
console.log(error);
} finally {
await sequelize.close();
}
})();
The execution results:
Executing (default): DROP TABLE IF EXISTS "table_a" CASCADE;
Executing (default): DROP TABLE IF EXISTS "table_a" CASCADE;
Executing (default): CREATE TABLE IF NOT EXISTS "table_a" ("id" SERIAL , "external_data" JSONB, PRIMARY KEY ("id"));
Executing (default): SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND t.relkind = 'r' and t.relname = 'table_a' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;
Executing (default): INSERT INTO "table_a" ("id","external_data") VALUES (DEFAULT,'{"a":1,"b":2,"c":3}'),(DEFAULT,'{"a":10,"c":30}'),(DEFAULT,'{"a":100}') RETURNING *;
Executing (default): SELECT "id", "external_data" FROM "table_a" AS "table_a" WHERE ("table_a"."external_data"#>>'{b}') IS NULL;
result1: [ { id: 2, external_data: { a: 10, c: 30 } },
{ id: 3, external_data: { a: 100 } } ]
Executing (default): SELECT "id", "external_data" FROM "table_a" AS "table_a" WHERE ("table_a"."external_data"#>>'{c}') IS NULL;
result2: [ { id: 3, external_data: { a: 100 } } ]
check the database:
node-sequelize-examples=# select * from "table_a";
id | external_data
----+--------------------------
1 | {"a": 1, "b": 2, "c": 3}
2 | {"a": 10, "c": 30}
3 | {"a": 100}
(3 rows)
Dependencies versions: "sequelize": "^5.21.3", postgres:9.6
source code: https://github.com/mrdulin/node-sequelize-examples/tree/master/src/examples/stackoverflow/61053931

Sequelize findAll() try to get unregistered field

i have problem with sequelize, i try to use findAll() to get data from my table, but there is an error which says unknown column jobs.userId in field list which i never register in my model or in my table
i have tried to use findAndCountAll() but still have same error, i also tried to remove all associatoin at get same result, the only work solution so far is put userId on exclude array
here's my controller
const jobData = await jobs.findAll({
attributes:{exclude:['updatedAt']},
include:[{
model:jobOwners,
as:'owner',
attributes:['id','profilePicture'],
include:[{
model:users,
as:'user',
attributes:['id','name'],
},{
model:districts,
as:'district',
attributes:['id','name']
}]
}]
});
here's my jobs model
'use strict';
module.exports = (sequelize, DataTypes) => {
const jobs = sequelize.define('jobs', {
jobOwnerId:{
type:DataTypes.INTEGER,
allowNull:false
},
caption: {
type:DataTypes.STRING,
allowNull:false
},
description: {
type:DataTypes.TEXT,
allowNull:false
},
districtId: {
type:DataTypes.INTEGER,
allowNull:false
},
address: {
type:DataTypes.TEXT,
allowNull:false
},
poster: {
type:DataTypes.TEXT,
allowNull:true
},
contactPerson: {
type:DataTypes.STRING,
allowNull:false
}
}, {});
jobs.associate = function(models) {
jobs.belongsTo(models.jobOwners,{as:'owner',foreignKey:'jobOwnerId'})
jobs.belongsTo(models.districts,{as:'districts', foreignKey:'districtId'})
};
return jobs;
};
here's my table
CREATE TABLE jobs (
id int(11) NOT NULL,
jobOwnerId int(11) NOT NULL,
caption varchar(255) NOT NULL,
description text,
districtId int(11) NOT NULL,
address text NOT NULL,
poster text,
contactPerson varchar(15) NOT NULL,
createdAt datetime NOT NULL,
updatedAt datetime NOT NULL
)
here's is the query result shown on console
SELECT jobs.id, jobs.jobOwnerId, jobs.caption, jobs.description, jobs.districtId, jobs.address,
jobs.poster, jobs.contactPerson, jobs.createdAt, jobs.userId, owner.id AS owner.id, owner.profilePicture AS owner.profilePicture,
owner->user.id AS owner.user.id, owner->user.name AS owner.user.name, owner->district.id
AS owner.district.id, owner->district.name AS owner.district.name FROM jobs AS jobs
LEFT OUTER JOIN jobOwners AS owner ON jobs.jobOwnerId = owner.id LEFT OUTER JOIN users AS owner->user
ON owner.userId = owner->user.id LEFT OUTER JOIN districts AS owner->district ON owner.districtId = owner->district.id;
there should be no jobs.userId
Problem solved, the problem not from jobs model or jobs controller, but it comes from users model, i create association hasMany jobs and it couses the problem..

Query a Global Secondary Index using contains in DynamoDB local

I have id as the hash key of my table and returnItemId which is the GSI. The returnItemId is a string which contains values separated by commas. Given a number for the GSI, I want to be able to query and get the correct item that contains it by using contains
var params = {
"AttributeDefinitions": [ // describbes the key schema of the table
{
"AttributeName": "id",
"AttributeType": "S"
},
{
"AttributeName": "returnItemId",
"AttributeType": "S"
}
],
// Hash for Primary Table
"KeySchema": [
{
"AttributeName": "id",
"KeyType": "HASH"
}
],
"GlobalSecondaryIndexes": [
{
"IndexName": "ReturnItemIndex",
"KeySchema": [
{
"AttributeName": "returnItemId", //must match one of attributedefinitions names
"KeyType": "HASH"
}
],
"Projection": {
"ProjectionType": "ALL"
},
"ProvisionedThroughput": {
"ReadCapacityUnits": 5,
"WriteCapacityUnits": 5
}
}
],
"ProvisionedThroughput": {
"ReadCapacityUnits": 5,
"WriteCapacityUnits": 5
},
"TableName": "my-table"
};
dynamodb.createTable(params, function(err, data) {
if (err) ppJson(err); // an error occurred
else ppJson(data); // successful response
});
Then I am going to create 2 items
var params = {
TableName: 'my-table',
Item: {
"id": "the_first_item",
"returnItemId": "123,456,789"
},
};
docClient.put(params, function(err, data) {
if (err) ppJson(err); // an error occurred
else ppJson(data); // successful response
});
And the second item
var params = {
TableName: 'my-table',
Item: {
"id": "the_second_item",
"returnItemId": "987,654,321"
},
};
docClient.put(params, function(err, data) {
if (err) ppJson(err); // an error occurred
else ppJson(data); // successful response
});
The two items look like
I am trying to run a query and get the correct item which contains 987 using the following query. Since my first item has 123,456,789 and the second item has 987,654,321 this method should return the second item.
var params = {
TableName: 'my-table',
IndexName: 'ReturnItemIndex', // optional (if querying an index)
KeyConditionExpression: 'contains(returnItemId, :return_id)',
//FilterExpression: 'contains(returnItemId, :return_id)', // a string representing a constraint on the attribute
ExpressionAttributeValues: { ':return_id': '987' },
};
docClient.query(params, function(err, data) {
if (err) ppJson(err); // an error occurred
else ppJson(data); // successful response
});
But am getting errors about using contains in keyconditionexpression. Is this method possible?
contains can only be used in filters which means:
- query or scan operations will traverse all data to apply your filters
- your cost of read operations will include all data read not just matched data
- with contains "12", you would probably match "123" and "124" too
- better than comma separated is to use StringSet or NumberSet data type
I would suggest another layout
Keyschema:
Partiton Key: id
Sort Key: returnItemId
GSI
Partition Key: returnItemId
Data:
------------------------------------
| id | returnItemId |
------------------------------------
| "the_first_item" | "123" |
| "the_first_item" | "456" |
| "the_first_item" | "789" |
| "the_second_item" | "987" |
| "the_second_item" | "654" |
| "the_second_item" | "321" |
------------------------------------
then query GSI for key condition returnItemId = 987 ( no filter expression )

Resources