Excluding results from Objection/Knex query based on withGraphFetched results - javascript

I have two models in Objection - "brands" and "offers".
Brand:
const { Model } = require('objection')
class Brand extends Model {
static get tableName() {
return 'brands'
}
...
static get relationMappings() {
const Offer = require('./offer-model')
return {
offer: {
relation: Model.HasManyRelation,
modelClass: Offer,
join: { from: 'brands.id', to: 'offers.brand_id' }
}
}
}
}
Offer:
const { Model } = require('objection')
class Offer extends Model {
static get tableName() {
return 'offers'
}
}
A brand has many offers, but I want to get brands which have at least 1 offer using withGraphFetched, excluding brands which have no offers. Here's what I have so far:
const brandModel = this.objection.models.brand
const query = brandModel.query().withGraphFetched('offer')
query.page(page, page_size)
const offers = await query
This returns the "joined" data, but also returns brands which don't have offers. For example:
[{
id:1,
name: 'brand 1',
offers: [{offerId: 1, offerName: 'offer 1'}]
},{
id:2,
name: 'brand 2',
offers: []
}]
In the above data, I don't want the brand with ID 2 to be in the result set.
I am using Objection/Knex to paginate the results, so I can't just exclude the brands with empty object arrays after the query has been executed.
I can achieve this using raw queries, but that means I can't use the Objection dynamic attributes and a few other key parts of Objection.
Thanks!

You can just tack a whereExists onto the query; something like
const query = brandModel.query()
.withGraphFetched('offer')
.whereExists(
(qb) => qb.select('id').from('offers')
.where('offers.brand_id', knex.ref('brands.id'))
);
Even though the whereExists bit is directly Knex, the query still goes through your models so stuff you've defined there should still apply (maybe unless you're doing something very wild that directly affects the columns used inside the whereExists)

Related

Sequelize - Delete association records from parent instance

I am trying to find a way to remove rows from the DB through the parent model (menu) that has many children (foods). I only want to delete certain rows though, not all.
Menu.js
...
Menu.hasMany(models.Food, {
as: 'foods',
foreignKey: 'menuId',
sourceKey: 'id'
});
...
In my controller I have the following to try and delete certain foods off the menu.
...
const result = await menu.destroyFoods({
where: {
name: ['Pasta', 'Pizza']
}
});
...
I have also tried singular destroyFood as well. For both I am getting destoryFood/destoryFoods is not a function. Is there any easy way to do this from the instance of menu? New to sequelize, would love some help. Thanks.
Thanks
You can use menu.removeFoods() and menu.removeFood() - see Special methods/mixins added to instances: Foo.hasMany(Bar) for more information.
You will also need to use the Op.in query operator to specify multiple values for Food.name.
const { Op } = require('sequelize');
const result = await menu.removeFoods({
where: {
name: {
[Op.in]: ['Pasta', 'Pizza'],
}
}
});
This is the equivalent of calling Food.destroy() where the menuId is equal to the menu.id from the earlier result.
const results = await Food.destroy({
where: {
menuId: menu.id,
name: {
[Op.in]: ['Pasta', 'Pizza'],
},
},
});

Resolve Custom Types at the root in GraphQL

I feel like I'm missing something obvious. I have IDs stored as [String] that I want to be able to resolve to the full objects they represent.
Background
This is what I want to enable. The missing ingredient is the resolvers:
const bookstore = `
type Author {
id: ID!
books: [Book]
}
type Book {
id: ID!
title: String
}
type Query {
getAuthor(id: ID!): Author
}
`;
const my_query = `
query {
getAuthor(id: 1) {
books { /* <-- should resolve bookIds to actual books I can query */
title
}
}
}
`;
const REAL_AUTHOR_DATA = [
{
id: 1,
books: ['a', 'b'],
},
];
const REAL_BOOK_DATA = [
{
id: 'a',
title: 'First Book',
},
{
id: 'b',
title: 'Second Book',
},
];
Desired result
I want to be able to drop a [Book] in the SCHEMA anywhere a [String] exists in the DATA and have Books load themselves from those Strings. Something like this:
const resolve = {
Book: id => fetchToJson(`/some/external/api/${id}`),
};
What I've Tried
This resolver does nothing, the console.log doesn't even get called
const resolve = {
Book(...args) {
console.log(args);
}
}
HOWEVER, this does get some results...
const resolve = {
Book: {
id(id) {
console.log(id)
return id;
}
}
}
Where the console.log does emit 'a' and 'b'. But I obviously can't scale that up to X number of fields and that'd be ridiculous.
What my team currently does is tackle it from the parent:
const resolve = {
Author: {
books: ({ books }) => books.map(id => fetchBookById(id)),
}
}
This isn't ideal because maybe I have a type Publisher { books: [Book]} or a type User { favoriteBooks: [Book] } or a type Bookstore { newBooks: [Book] }. In each of these cases, the data under the hood is actually [String] and I do not want to have to repeat this code:
const resolve = {
X: {
books: ({ books }) => books.map(id => fetchBookById(id)),
}
};
The fact that defining the Book.id resolver lead to console.log actually firing is making me think this should be possible, but I'm not finding my answer anywhere online and this seems like it'd be a pretty common use case, but I'm not finding implementation details anywhere.
What I've Investigated
Schema Directives seems like overkill to get what I want, and I just want to be able to plug [Books] anywhere a [String] actually exists in the data without having to do [Books] #rest('/external/api') in every single place.
Schema Delegation. In my use case, making Books publicly queryable isn't really appropriate and just clutters my Public schema with unused Queries.
Thanks for reading this far. Hopefully there's a simple solution I'm overlooking. If not, then GQL why are you like this...
If it helps, you can think of this way: types describe the kind of data returned in the response, while fields describe the actual value of the data. With this in mind, only a field can have a resolver (i.e. a function to tell it what kind of value to resolve to). A resolver for a type doesn't make sense in GraphQL.
So, you can either:
1. Deal with the repetition. Even if you have ten different types that all have a books field that needs to be resolved the same way, it doesn't have to be a big deal. Obviously in a production app, you wouldn't be storing your data in a variable and your code would be potentially more complex. However, the common logic can easily be extracted into a function that can be reused across multiple resolvers:
const mapIdsToBooks = ({ books }) => books.map(id => fetchBookById(id))
const resolvers = {
Author: {
books: mapIdsToBooks,
},
Library: {
books: mapIdsToBooks,
}
}
2. Fetch all the data at the root level instead. Rather than writing a separate resolver for the books field, you can return the author along with their books inside the getAuthor resolver:
function resolve(root, args) {
const author = REAL_AUTHOR_DATA.find(row => row.id === args.id)
if (!author) {
return null
}
return {
...author,
books: author.books.map(id => fetchBookById(id)),
}
}
When dealing with databases, this is often the better approach anyway because it reduces the number of requests you make to the database. However, if you're wrapping an existing API (which is what it sounds like you're doing), you won't really gain anything by going this route.

Put Nested ReactJS values into GraphQL create mutation

I presently have a page with a dynamically created form. I am having trouble understanding how to manipulate the state and GraphQL query to handle nested queries.
With my present implementation it does not seem to be able to create any new entries. I want to create 1 "target" with several sub "addr" tied to it in one mutation.
This is the state definitions:
state = {
name:'',
addr:[{
mobilepkg:'',
target_url:'',
target_ip: '',
idCars:[]
}],
category:'',
date: '',
location:''
}
Handler for Graph:
handleTarget = async e => {
e.preventDefault()
const { name,
target_url,
target_ip,category,
mobilepkg,date,location } = this.state
let idCars = this.state.idCars
let adras = this.state.addr
await this.props.createTargetMutation({
variables: {
data: {
name,
addr:{
create:
[{
target_url,
target_ip,
mobilepkg,
cars: {
connect: idCars
},
}]
},
date,
location,
category
}
}
})
this.props.history.replace('/targets')
}
}
My create mutation
const CREATE_DRAFT_MUTATION = gql`
mutation CreateTargetMutation($data: TargetCreateInput!) {
createTarget(data: $data) {
id
name
addr
category
}
}
`
GraphQL datamodel
type Target {
id: ID! #unique
name: String!
addr: [Addr!]!
category: String!
date:String!
location:String!
}
type Addr {
id: ID! #unique
target_url:String!
target_ip:String!
mobilepkg:String!
cars: [Car!]!
}
How do I put my ReactJS state which has a nested array into GraphQL?
PS:I am new to GraphQL and ReactJS.
EDIT: In playground im able to create my items but its not working in my actual application.
mutation CreateTargetMutation($data: TargetCreateInput!) {
createTarget(data: $data) {
id
name
addr{
target_ip
target_url
mobilepkg
cars{
id
}
}
category
date
location
}
}
{
"data": {
"name":"testerquery",
"addr": {
"create": {
"target_ip":"123",
"target_url":"123",
"mobilepkg":"asd",
"cars":{"connect":{"id":"cjs3yd83u004a0781jffzaqqr"}}
}
},
"category":"simple",
"date":"2019-03-12",
"location":"kl"
}
}
Bro, you are one the right path. You just need to iterate your values in order to solve this problem. Once you iterate through the values you simply need to make a call to the new array which contains everything and it will work. As your values are nested you will need to add "{}" to your car variable and within that contain your connect since you wish to create new "addr" whilst connecting to existing "car".
let create = []
for(let i=0; i < this.state.addr.length; i++){
create.push({
'mobilepkg':this.state.addr[i].mobilepkg,
'target_url':this.state.addr[i].target_url,
'target_ip':this.state.addr[i].target_ip,
'cars': {
'connect': this.state.addr[i].cars}
})
}
await this.props.createTargetMutation({
variables: {
data: {
name,
addr: {
create
},
category,
date,
location
}
}
})
this.props.history.replace('/targets')
}
}
Your values should now successfully pass into GraphQL and create targets with many "addr" whilst connecting to many "car"

Normalizr - is it a way to generate IDs for non-ids entity model?

I'm using normalizr util to process API response based on non-ids model. As I know, typically normalizr works with ids model, but maybe there is a some way to generate ids "on the go"?
My API response example:
```
// input data:
const inputData = {
doctors: [
{
name: Jon,
post: chief
},
{
name: Marta,
post: nurse
},
//....
}
// expected output data:
const outputData = {
entities: {
nameCards : {
uniqueID_0: { id: uniqueID_0, name: Jon, post: uniqueID_3 },
uniqueID_1: { id: uniqueID_1, name: Marta, post: uniqueID_4 }
},
positions: {
uniqueID_3: { id: uniqueID_3, post: chief },
uniqueID_4: { id: uniqueID_4, post: nurse }
}
},
result: uniqueID_0
}
```
P.S.
I heard from someone about generating IDs "by the hood" in normalizr for such cases as my, but I did found such solution.
As mentioned in this issue:
Normalizr is never going to be able to generate unique IDs for you. We
don't do any memoization or anything internally, as that would be
unnecessary for most people.
Your working solution is okay, but will fail if you receive one of
these entities again later from another API endpoint.
My recommendation would be to find something that's constant and
unique on your entities and use that as something to generate unique
IDs from.
And then, as mentioned in the docs, you need to set idAttribute to replace 'id' with another key:
const data = { id_str: '123', url: 'https://twitter.com', user: { id_str: '456', name: 'Jimmy' } };
const user = new schema.Entity('users', {}, { idAttribute: 'id_str' });
const tweet = new schema.Entity('tweets', { user: user }, {
idAttribute: 'id_str',
// Apply everything from entityB over entityA, except for "favorites"
mergeStrategy: (entityA, entityB) => ({
...entityA,
...entityB,
favorites: entityA.favorites
}),
// Remove the URL field from the entity
processStrategy: (entity) => omit(entity, 'url')
});
const normalizedData = normalize(data, tweet);
EDIT
You can always provide unique id's using external lib or by hand:
inputData.doctors = inputData.doctors.map((doc, idx) => ({
...doc,
id: `doctor_${idx}`
}))
Have a processStrategy which is basically a function and in that function assign your id's there, ie. value.id = uuid(). Visit the link below to see an example https://github.com/paularmstrong/normalizr/issues/256

How to extend/modify the object in nodejs api?

i am using the angular-fullstack yeoman generator. created a schema for a Product, and a set of api crud operations. all works well. now in the get list operations, i don't want to receive all the fields, only a subset. like a select in sql. i would also wish to alter one value. instead of the price, i need price * 1.1 .
how to do that?
here is the code for the index method (returns list of products):
// Gets a list of Products
export function index(req, res) {
Product.findAsync()
.then(respondWithResult(res))
.catch(handleError(res));
}
function respondWithResult(res, statusCode) {
statusCode = statusCode || 200;
return function(entity) {
if (entity) {
res.status(statusCode).json(entity);
}
};
}
As stated in the documentation, .find() takes two params, query and projection.
// params
Product.findAsync(query, projection)
You can use projection to "select" a subset of fields;
// example
Product.findAsync({}, { _id: 1, name: 1, description: 1 })
// result, only the three specified field will be returned
[
{ _id: 'abc123', name: 'Some name', description: 'Some description'},
{...}
]
If you want to manipulate data I think you have to use the aggregation pipeline

Categories

Resources