Creating Knex Migration Referencing the Same Table Twice - postgresql

Trying to create a table Transaction in a postgresql database that references Buyers and Sellers s.t. both are objects from the Users table.
I think I have the migration working to look something like the following:
exports.up = function(knex, Promise) {
return knex.schema.createTable('likes', t => {
t.increments('id').primary()
t.integer('buyers_id').references('users.id').onDelete('CASCADE')
t.integer('sellers_id').references('users.id').onDelete('CASCADE')
...
t.datetime("created_at");
t.datetime("updated_at");
})
};
Next, I need to manage the association in the model, s.t the Transaction belongs to a Buyer and a Seller, which are both members of the User class.
To clarify the question, I am able to create the model with these attributes, but the association does not seem to be working.
here is my Transaction model:
const BaseModel = require("./BaseModel");
// const Password = require('objection-password')();
class Transaction extends BaseModel {
static get tableName() {
return "transactions";
}
static get relationMappings () {
const User = require('./User');
const Item = require('./Item')
return {
buyer: {
relation: BaseModel.BelongsToOneRelation,
modelClass: User,
join: {
from: 'transactions.buyers_id',
to: 'users.id'
}
},
seller: {
relation: BaseModel.BelongsToOneRelation,
modelClass: User,
join: {
from: 'transactions.sellers_id',
to: 'users.id'
}
},
books: {
relation: BaseModel.BelongsToOneRelation,
modelClass: Item,
join: {
from: 'transactions.items_id',
to: 'items.id'
}
}
}
}
}
module.exports = Transaction;
Here is the relevant route where I try to eager load the buyer:
let router = express.Router();
router.get('/', async (req, res) => {
const transactions = await Transaction
.query()
.eager(['buyer', 'items')
res.json(transactions);
});
I have figured this out. The above code works, using the aliases buyers and sellers and associating those two types of Users with Transactions.

For anyone who is interested... Consolidating the working solution above using Node/ExpressJS for the server, Postgresql for the db, KnexJS/ObjectiveJS to manage Models and queries.
Here is the migration that sets up columns for buyers and sellers both of which reference the same Users table:
exports.up = function(knex, Promise) {
return knex.schema.createTable('likes', t => {
t.increments('id').primary()
t.integer('buyers_id').references('users.id').onDelete('CASCADE')
t.integer('sellers_id').references('users.id').onDelete('CASCADE')
...
t.datetime("created_at");
t.datetime("updated_at");
})
};
HEre is the Transactions Model including associations s.t. a Transaction belongs to Item Buyer(user) and Seller(user) and Item:
const BaseModel = require("./BaseModel");
// const Password = require('objection-password')();
class Transaction extends BaseModel {
static get tableName() {
return "transactions";
}
static get relationMappings () {
const User = require('./User');
const Item = require('./Item')
return {
buyer: {
relation: BaseModel.BelongsToOneRelation,
modelClass: User,
join: {
from: 'transactions.buyers_id',
to: 'users.id'
}
},
seller: {
relation: BaseModel.BelongsToOneRelation,
modelClass: User,
join: {
from: 'transactions.sellers_id',
to: 'users.id'
}
},
books: {
relation: BaseModel.BelongsToOneRelation,
modelClass: Item,
join: {
from: 'transactions.items_id',
to: 'items.id'
}
}
}
}
}
module.exports = Transaction;
Lastly, here is the express route which returns all transactions including eager loading the associated models:
let router = express.Router();
router.get('/', async (req, res) => {
const transactions = await Transaction
.query()
.eager(['buyer', 'items')
res.json(transactions);
});

Related

How do I add a subdocument's data to a parent document (using mongoose)?

I am creating a MERN app and have a series of mongoose schema that are connected.
The hierarchy goes: Program -> Workout -> Exercise -> Set
Here is the model code for each Schema:
Program Schema
const programSchema = mongoose.Schema({
program_name:{
type: String,
},
workouts:[{
type: mongoose.Types.ObjectId,
ref: 'Workout'
}]
Workout Schema
const workoutSchema = mongoose.Schema({
workout_name:{
type:String
},
exercises: [{
type: mongoose.Types.ObjectId,
ref: 'Exercise'
}]
Exercise Schema
const exerciseSchema = mongoose.Schema({
exercise_name:{
type:String
},
notes:{
type:String
},
sets:[{
type: mongoose.Types.ObjectId,
ref: 'Set'
}]
Set Schema
const setSchema = mongoose.Schema({
weight:{
type: String
},
repetitions:{
type: String
},
rpe:{
type: String
}
My question is, now that they are all separate. How do I link a specific Set to a Exercise? or a specific Exercise to a Workout? etc. How do I reference them to each other so that I can create a whole program with various workouts, and each workout having various exercises, etc.
I would appreciate any wisdom. Thank you
For more info, here are the controllers.
Program Controller (CREATE NEW PROGRAM)
const createProgram = async (req, res) => {
//const {program_name, workouts} = req.body
try {
const program = new Program(req.body) // create a new program with the information requested
await program.save() // save it to database
res.status(201).send(program) // send it back to user
} catch (e) {
res.status(500).send(e)
}
WORKOUT CONTROLLER (CREATE NEW WORKOUT)
const createWorkout = async (req, res) => {
const {workout_name} = req.body
try {
const workout = await new Workout({
workout_name
})
await workout.save()
res.status(201).send(workout)
} catch(e) {
}
EXERCISE CONTROLLER (CREATE NEW EXERCISE)
const createExercise = async (req, res) => {
const { exercise_name='', notes='', sets } = req.body
try {
const exercise = await new Exercise({
exercise_name,
notes,
sets
})
await exercise.save()
res.status(201).send(exercise)
} catch (e) {
console.log(e)
}
SET CONTROLLER (CREATE NEW SET)
const createSet = async (req, res) => {
const {repetitions='', weight='', rpe=''} = req.body
try {
const set = await new Set({
weight,
repetitions,
rpe
})
await set.save()
res.status(201).send(set)
} catch (e) {
res.status(500).send(e)
}
The way I do it is on save I add the id to the attributed array. So i'll give you an example for one of your Routers then hopefully you can understand enough to do the rest.
For workouts you want to add it to a program when it's created. so when you create it, just add the id to the program you want to add it to.
Like so:
const {workout_name} = req.body
try {
const newWorkout = await Workout.create({
workout_name
})
Program.updateOne(
{ _id: req.params.ProgramId },
{ $addToSet: { workouts: newWorkout._id }},
)
res.status(201).send(workout)
} catch(e) {
}
So basically after creating your workout, you add that workout ID to the workouts array of the parent object. You would do the same for the rest of your Routers.

How to query nested data in mongoose model

I am attempting to build a Vue.js app with a MEVN stack backend and Vuex. I am configuring my Vuex action handler with a GET request that prompts a corresponding Express GET route to query data nested in Mongoose.
A username is passed into the handler as an argument and appended to the GET request URL as a parameter:
actions: {
loadPosts: async (context, username) => {
console.log(username)
let uri = `http://localhost:4000/posts/currentuser?username=${username}`;
const response = await axios.get(uri)
context.commit('setPosts', response.data)
}
}
The corresponding Express route queries activeUser.name, which represents the nested data in the Mongoose Model:
postRoutes.route('/currentuser').get(function (req, res) {
let params = {},
username = req.query.activeUser.name
if (username) {
params.username = username
}
Post.find(params, function(err, posts){
if(err){
res.json(err);
}
else {
res.json(posts);
}
});
});
Below is my Mongoose model, with activeUser.name representing the nested data queried by the Express route:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
let Post = new Schema({
title: {
type: String
},
body: {
type: String,
},
activeUser: {
name: {
type: String
}
}
},{
collection: 'posts'
});
module.exports = mongoose.model('Post', Post);
Even with this setup, the GET route does not appear to send a response back to the action handler. I thought adding username = req.query.activeUser.name in the express route would be the right method for querying the nested data in Mongoose, but apparently not. Any recommendations on how to configure the above Express route in order to query the nested data in the Mongoose model? Thanks!
name is inside activeuser so you need to construct params object variable like this:
postRoutes.route("/currentuser").get(function(req, res) {
let params = {
activeUser: {}
};
let username = req.query.activeUserName;
if (username) {
params.activeUser.name = username;
}
Post.find(params, function(err, posts) {
if (err) {
res.json(err);
} else {
res.json(posts);
}
});
});
Note that I also used activeUserName as query param like this: /currentuser?activeUserName=JS_is_awesome18

Objection.js eager many-to-many relationship problems

Whenever trying to use eager loading to get a User's groups in Objection.js, I get the following error:
Error
TypeError: Cannot read property 'size' of null
at findRelationPropsToSelect (/vagrant/node_modules/objection/lib/queryBuilder/operations/eager/WhereInEagerOperation.js:184:36)
at WhereInEagerOperation.onBuild (/vagrant/node_modules/objection/lib/queryBuilder/operations/eager/WhereInEagerOperation.js:35:48)
at QueryBuilder.callOperationMethod (/vagrant/node_modules/objection/lib/queryBuilder/QueryBuilderOperationSupport.js:353:33)
at forEachOperation.op (/vagrant/node_modules/objection/lib/queryBuilder/QueryBuilderOperationSupport.js:449:14)
at QueryBuilder.forEachOperation (/vagrant/node_modules/objection/lib/queryBuilder/QueryBuilderOperationSupport.js:287:13)
at QueryBuilder.executeOnBuild (/vagrant/node_modules/objection/lib/queryBuilder/QueryBuilderOperationSupport.js:447:10)
at QueryBuilder.buildInto (/vagrant/node_modules/objection/lib/queryBuilder/QueryBuilderOperationSupport.js:440:10)
at buildInto (/vagrant/node_modules/objection/lib/queryBuilder/QueryBuilder.js:1361:25)
at doExecute (/vagrant/node_modules/objection/lib/queryBuilder/QueryBuilder.js:1265:23)
at Bluebird.try.then (/vagrant/node_modules/objection/lib/queryBuilder/QueryBuilder.js:583:19)
at tryCatcher (/vagrant/node_modules/bluebird/js/release/util.js:16:23)
at Promise._settlePromiseFromHandler (/vagrant/node_modules/bluebird/js/release/promise.js:512:31)
at Promise._settlePromise (/vagrant/node_modules/bluebird/js/release/promise.js:569:18)
at Promise._settlePromise0 (/vagrant/node_modules/bluebird/js/release/promise.js:614:10)
at Promise._settlePromises (/vagrant/node_modules/bluebird/js/release/promise.js:694:18)
at _drainQueueStep (/vagrant/node_modules/bluebird/js/release/async.js:138:12)
My migrations and seeds are all running correctly and I can see them when I connect via PSequel. I must be doing something stupid but as anewbie to Objection.js I cannot see it.
Models
My Models are as follows:
// models/base.model.js
import { Model } from 'objection';
export class BaseModel extends Model {
static modelPaths = [__dirname];
}
// models/user.model.js
import { Model } from 'objection';
import { BaseModel } from './base.model'
export class User extends BaseModel {
static tableName = 'users';
static relationMappings = {
groups: {
relation: Model.ManyToManyRelation,
join: {
from: 'users.id',
through: {
from: 'users_groups.user_id',
to: 'users_groups.group_id'
},
to: 'groups.id'
}
}
}
}
// models/group.model.js
import { Model } from 'objection';
import { BaseModel } from './base.model';
export class Group extends BaseModel {
static tableName = 'groups';
static relationMappings = {
users: {
relation: Model.ManyToManyRelation,
join: {
from: 'groups.id',
through: {
from: 'users_groups.user_id',
to: 'users_groups.group_id'
},
to: 'users.id',
}
}
}
}
Migrations
And my migrations are as follows:
// migrations/20190307214041-create-users-table.js
exports.up = function(knex) {
return knex.schema
.createTable('users', function(table) {
table.increments('id').primary();
table.string('firstName').notNullable();
table.string('lastName').notNullable();
table.string('avatar').nullable();
table.string('email').notNullable();
table.string('password').notNullable();
table.integer('active').notNullable().defaultTo(0);
table.timestamps();
table.unique('email');
});
};
exports.down = function(knex) {
return knex.schema
.dropTable('users');
};
// migrations/20190307214814-create-groups-table.js
exports.up = function(knex) {
return knex.schema
.createTable('groups', function(table) {
table.increments('id').primary();
table.string('name');
table.integer('active');
table.timestamps();
});
};
exports.down = function(knex) {
return knex.schema
.dropTable('groups');
};
// migrations/20190307220010-create-groups_users-table.js
exports.up = function(knex) {
return knex.schema
.createTable('users_groups', function(table) {
// table.increments('id').primary();
table.integer('user_id');
table.integer('group_id');
// table.integer('users_id').references('users.id');
// table.integer('groups_id').references('groups.id');
});
};
exports.down = function(knex) {
return knex.schema
.dropTable('users_groups');
};
Query examples
Working query
This query works correctly and I get data back.
try {
const users = await User.query().orderBy('id');
res.status(200).json(users);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'There was an error' });
}
Failing query
This query fails and I get the error shown at the top of this question back.
try {
const user = await User.query()
.where('id', Number(req.params.id))
.eager('groups');
res.status(200).json(user);
} catch (err) {
console.error(err);
res.status(500).json({ message: 'There was an error' });
}
It turned out to be a babel config issue.
When I changed the code to look like the following it worked:
import path from 'path';
import { Model } from 'objection';
import { BaseModel } from './base.model';
export class Group extends BaseModel {
static get tableName() {
return 'groups';
}
static get relationMappings() {
return {
users: {
modelClass: path.join(__dirname, 'user.model'),
relation: Model.ManyToManyRelation,
join: {
from: 'groups.id',
through: {
from: 'users_groups.group_id',
to: 'users_groups.user_id'
},
to: 'users.id',
}
}
};
}
}
import path from 'path';
import { Model } from 'objection';
import { BaseModel } from './base.model'
export class User extends BaseModel {
static get tableName() {
return 'users';
}
static get relationMappings() {
return {
groups: {
modelClass: path.join(__dirname, 'group.model'),
relation: Model.ManyToManyRelation,
join: {
from: 'users.id',
through: {
from: 'users_groups.user_id',
to: 'users_groups.group_id'
},
to: 'groups.id'
}
}
};
}
}

Relationships GraphQL

The second week I try to link two collections in the apollo-server-express / MongoDB / Mongoose / GraphQL stack, but I do not understand how. I found a similar lesson with the REST API, what I need is called Relationships. I need this, but in GraphQL
watch video
How to add cars to the User?
I collected the test server, the code is here: https://github.com/gHashTag/test-graphql-server
Help
I have cloned your project and implemented some code and here what I changed to make relationship works. Note, I just did a basic code without validation or advance dataloader just to make sure non-complexity. Hope it can help.
src/graphql/resolvers/car-resolvers.js
import Car from '../../models/Car'
import User from '../../models/User'
export default {
getCar: (_, { _id }) => Car.findById(_id),
getCars: () => Car.find({}),
getCarsByUser: (user, {}) => Car.find({seller: user._id }), // for relationship
createCar: async (_, args) => {
// Create new car
return await Car.create(args)
}
}
src/graphql/resolvers/user-resolvers.js
import User from '../../models/User'
export default {
getUser: (_, { _id }) => User.findById(_id),
getUsers: () => User.find({}),
getUserByCar: (car, args) => User.findById(car.seller), // for relationship
createUser: (_, args) => {
return User.create(args)
}
}
src/graphql/resolvers/index.js
import UserResolvers from './user-resolvers'
import CarResolvers from './car-resolvers'
export default {
User:{
cars: CarResolvers.getCarsByUser // tricky part to link query relation ship between User and Car
},
Car:{
seller: UserResolvers.getUserByCar // tricky part to link query relation ship between User and Car
},
Query: {
getUser: UserResolvers.getUser,
getUsers: UserResolvers.getUsers,
getCar: CarResolvers.getCar,
getCars: CarResolvers.getCars
},
Mutation: {
createUser: UserResolvers.createUser,
createCar: CarResolvers.createCar,
}
}
src/graphql/schema.js
export default`
type Status {
message: String!
}
type User {
_id: ID!
firstName: String
lastName: String
email: String
cars: [Car]
}
type Car {
_id: ID
make: String
model: String
year: String
seller: User
}
type Query {
getUser(_id: ID!): User
getUsers: [User]
getCar(_id: ID!): Car
getCars: [Car]
}
type Mutation {
createUser(firstName: String, lastName: String, email: String): User
// change from _id to seller, due to base on logic _id conflict with CarId
createCar(seller: ID!, make: String, model: String, year: String): Car
}
schema {
query: Query
mutation: Mutation
}
`
src/middlewares.js
import bodyParser from 'body-parser'
import { graphqlExpress, graphiqlExpress } from 'apollo-server-express'
import { makeExecutableSchema } from 'graphql-tools'
import typeDefs from '../graphql/schema'
import resolvers from '../graphql/resolvers'
import constants from './constants'
export const schema = makeExecutableSchema({
typeDefs,
resolvers
})
export default app => {
app.use('/graphiql', graphiqlExpress({
endpointURL: constants.GRAPHQL_PATH
}))
app.use(
constants.GRAPHQL_PATH,
bodyParser.json(),
graphqlExpress(req => ({
schema,
context: {
event: req.event
}
}))
)
}
try to make something like this in your car resolver
export default {
getCar: ({ _id: ownId }, { _id }) =>
Car.findById(ownId || _id);
// here is the rest of your code
You need to add a resolver for the cars field on the User type.
const resolvers = {
Query: {
getUsers: ...
getCars: ...
...
},
Mutation: {
...
},
User: {
cars: ...
}
}

apollostack/graphql-server - how to get the fields requested in a query from resolver

I am trying to figure out a clean way to work with queries and mongdb projections so I don't have to retrieve excessive information from the database.
So assuming I have:
// the query
type Query {
getUserByEmail(email: String!): User
}
And I have a User with an email and a username, to keep things simple. If I send a query and I only want to retrieve the email, I can do the following:
query { getUserByEmail(email: "test#test.com") { email } }
But in the resolver, my DB query still retrieves both username and email, but only one of those is passed back by apollo server as the query result.
I only want the DB to retrieve what the query asks for:
// the resolver
getUserByEmail(root, args, context, info) {
// check what fields the query requested
// create a projection to only request those fields
return db.collection('users').findOne({ email: args.email }, { /* projection */ });
}
Of course the problem is, getting information on what the client is requesting isn't so straightforward.
Assuming I pass in request as context - I considered using context.payload (hapi.js), which has the query string, and searching it through various .split()s, but that feels kind of dirty. As far as I can tell, info.fieldASTs[0].selectionSet.selections has the list of fields, and I could check for it's existence in there. I'm not sure how reliable this is. Especially when I start using more complex queries.
Is there a simpler way?
In case you don't use mongDB, a projection is an additional argument you pass in telling it explicitly what to retrieve:
// telling mongoDB to not retrieve _id
db.collection('users').findOne({ email: 'test#test.com' }, { _id: 0 })
As always, thanks to the amazing community.
2020-Jan answer
The current answer to getting the fields requested in a GraphQL query, is to use the graphql-parse-resolve-info library for parsing the info parameter.
The library is "a pretty complete solution and is actually used under the hood by postgraphile", and is recommended going forward by the author of the other top library for parsing the info field, graphql-fields.
Use graphql-fields
Apollo server example
const rootSchema = [`
type Person {
id: String!
name: String!
email: String!
picture: String!
type: Int!
status: Int!
createdAt: Float
updatedAt: Float
}
schema {
query: Query
mutation: Mutation
}
`];
const rootResolvers = {
Query: {
users(root, args, context, info) {
const topLevelFields = Object.keys(graphqlFields(info));
return fetch(`/api/user?fields=${topLevelFields.join(',')}`);
}
}
};
const schema = [...rootSchema];
const resolvers = Object.assign({}, rootResolvers);
// Create schema
const executableSchema = makeExecutableSchema({
typeDefs: schema,
resolvers,
});
Sure you can. This is actually the same functionality that is implemented on join-monster package for SQL based db's. There's a talk by their creator: https://www.youtube.com/watch?v=Y7AdMIuXOgs
Take a look on their info analysing code to get you started - https://github.com/stems/join-monster/blob/master/src/queryASTToSqlAST.js#L6-L30
Would love to see a projection-monster package for us mongo users :)
UPDATE:
There is a package that creates a projection object from info on npm: https://www.npmjs.com/package/graphql-mongodb-projection
You can generate MongoDB projection from info argument. Here is the sample code that you can follow
/**
* #description - Gets MongoDB projection from graphql query
*
* #return { object }
* #param { object } info
* #param { model } model - MongoDB model for referencing
*/
function getDBProjection(info, model) {
const {
schema: { obj }
} = model;
const keys = Object.keys(obj);
const projection = {};
const { selections } = info.fieldNodes[0].selectionSet;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const isSelected = selections.some(
selection => selection.name.value === key
);
projection[key] = isSelected;
}
console.log(projection);
}
module.exports = getDBProjection;
With a few helper functions you can use it like this (typescript version):
import { parceGqlInfo, query } from "#backend";
import { GraphQLResolveInfo } from "graphql";
export const user = async (parent: unknown, args: unknown, ctx: unknown, info: GraphQLResolveInfo): Promise<User | null> => {
const { dbQueryStr } = parceGqlInfo(info, userFields, "id");
const [user] = await query(`SELECT ${dbQueryStr} FROM users WHERE id=$1;`, [1]);
return user;
};
Helper functions.
Few points:
gql_uid used as ID! string type from primary key to not change db types
required option is used for dataloaders (if field was not requested by user)
allowedFields used to filter additional fields from info like '__typename'
queryPrefix is used if you need to prefix selected fields like select u.id from users u
const userFields = [
"gql_uid",
"id",
"email"
]
// merge arrays and delete duplicates
export const mergeDedupe = <T>(arr: any[][]): T => {
// #ts-ignore
return ([...new Set([].concat(...arr))] as unknown) as T;
};
import { parse, simplify, ResolveTree } from "graphql-parse-resolve-info";
import { GraphQLResolveInfo } from "graphql";
export const getQueryFieldsFromInfo = <Required = string>(info: GraphQLResolveInfo, options: { required?: Required[] } = {}): string[] => {
const { fields } = simplify(parse(info) as ResolveTree, info.returnType) as { fields: { [key: string]: { name: string } } };
let astFields = Object.entries(fields).map(([, v]) => v.name);
if (options.required) {
astFields = mergeDedupe([astFields, options.required]);
}
return astFields;
};
export const onlyAllowedFields = <T extends string | number>(raw: T[] | readonly T[], allowed: T[] | readonly T[]): T[] => {
return allowed.filter((f) => raw.includes(f));
};
export const parceGqlInfo = (
info: GraphQLResolveInfo,
allowedFields: string[] | readonly string[],
gqlUidDbAlliasField: string,
options: { required?: string[]; queryPrefix?: string } = {}
): { pureDbFields: string[]; gqlUidRequested: boolean; dbQueryStr: string } => {
const fieldsWithGqlUid = onlyAllowedFields(getQueryFieldsFromInfo(info, options), allowedFields);
return {
pureDbFields: fieldsWithGqlUid.filter((i) => i !== "gql_uid"),
gqlUidRequested: fieldsWithGqlUid.includes("gql_uid"),
dbQueryStr: fieldsWithGqlUid
.map((f) => {
const dbQueryStrField = f === "gql_uid" ? `${gqlUidDbAlliasField}::Text AS gql_uid` : f;
return options.queryPrefix ? `${options.queryPrefix}.${dbQueryStrField}` : dbQueryStrField;
})
.join(),
};
};