RealmJS:How to write sorted fetch query where inner object is Key-Value? - react-native

Product has many details like: productName, manufactured date, manufacturer name, product type etc. But I need to support flexibility of having no defined schema for product details. To achieve this below is the Realm-JS schema.
import Realm from 'realm';
export default class ProductModel extends Realm.Object { }
ProductModel.schema = {
name: 'ProductModel',
properties: {
productAttributes: { type: 'list', objectType: 'ProductDetailsModel' },
ID: { type: 'string', optional: true },
}
};
import Realm from 'realm';
export default class ProductDetailsModel extends Realm.Object { }
ProductDetailsModel.schema = {
name: 'ProductDetailsModel',
properties: {
attributeName: { type: 'string', optional: true },
attributeValue: { type: 'string', optional: true },
}
};
This flexibility is yielding in complexity of writing sort query. I need to be able to sort based on Product Attribute values. Let us say 'Product Name' is on attribute of Product, I need to fetch Product sorted by 'Product Name' in 'A-Z' or reverse order.
var productList = realm.objects('ProductModel').sorted(<Some query string>)
Please help me to write query to fetch the ProductModel from DB based on some values(productName or manufacturer name etc..) sorted order?
P.S. Other option which I am aware is get the realm objects and sort using Array.sort.
Update 1:
When I try to do sort with below query:
let item = this.realm.objects('ProductModel').filtered("productAttributes.attributeName = " + "\"" + "serialNo" + "\"")
.sorted('productAttributes.attributeValue', false);
I am getting below error
Cannot sort on key path 'productAttributes.attributeValue': property 'ProductModel.productAttributes' is of unsupported type 'array'.

Related

How to return the entity with its relations after saving it?

I am building a storage application, with GraphQL as the backend, using Typegraphql and TypeORM.
The categories need to be added separately and then when adding a product, you choose from a dropdown one of the available categories. This in turn passes the categoryId to the product in a one-to-many/many-to-one relationship.
Here is my Category entity:
import {
Entity,
PrimaryColumn,
Column,
BaseEntity,
Generated,
OneToMany
} from 'typeorm';
import Product from './Product';
#ObjectType()
#Entity('categories')
export default class Category extends BaseEntity {
#Field()
#PrimaryColumn()
#Generated('uuid')
categoryId: string;
#Field()
#Column()
categoryName: string;
#OneToMany(() => Product, (product: Product) => product.category)
products: Product[];
}
and here is my Product entity
import {
Entity,
PrimaryColumn,
Column,
BaseEntity,
Generated,
ManyToOne,
JoinColumn
} from 'typeorm';
import Category from './Category';
#ObjectType()
#Entity('products')
export default class Product extends BaseEntity {
#Field()
#PrimaryColumn()
#Generated('uuid')
productID: string;
#Field()
#Column()
productName: string;
#Field(() => Category)
#ManyToOne(() => Category, (category: Category) => category.products, {
cascade: true,
lazy: true
})
#JoinColumn()
category: Category;
#Field()
#Column()
productQuantity: number;
#Field()
#Column({ type: 'decimal', precision: 2 })
productPrice: number;
#Field()
#Column({ type: 'decimal', precision: 2 })
productPriceRA: number;
#Field()
#Column({ type: 'decimal', precision: 2 })
productPriceKK: number;
#Field()
#Column('varchar', { length: 255 })
productSupplier: string;
#Field()
#Column('varchar', { length: 255 })
productOrderLink: string;
#Field()
#Column('longtext')
productImage: string;
}
For the save mutation, I've created an Input type as well:
export default class ProductInput implements Partial<Product> {
#Field()
productName: string;
#Field(() => String)
category: Category;
#Field()
productQuantity: number;
#Field()
productPrice: number;
#Field()
productPriceRA: number;
#Field()
productPriceKK: number;
#Field()
productSupplier: string;
#Field()
productOrderLink: string;
#Field()
productImage: string;
}
The relations work, as I am able to query the products, along with their category data with the following query:
{
getProducts {
productID
productName
category {
categoryId
categoryName
}
}
}
However, when saving a product it always returns
"message": "Cannot return null for non-nullable field Category.categoryName."
This is the Mutation's code in the Resolver:
#Mutation(() => Product, { description: 'Add new product' })
async addProduct(
#Arg('product') productInput: ProductInput
): Promise<Product | any> {
try {
const product = await Product.create(productInput).save();
console.log('product: ', product);
return product;
} catch (error) {
return error;
}
}
I've been trying different things, however nothing seems to work and I am wondering if it's even possible to directly return the entity with its relations. If it's not, the other option I can think of is to return true/false based on the result and re-query all of the data. But this seems very inefficient and I am actively trying to avoid going this route.
Any help will be much appreciated.
After some more research and I decided to go with the following approach:
try {
const { productID } = await Product.create(productInput).save();
return await Product.findOne(productID);
} catch (error) {
return error;
}
This allows me to directly return the product, based on the productID after it's saved in the database and properly returns the object with it's relationship.
GraphQL uses an notation to recognize data. You can see it as __typename object property. Of course, this must be turned on in the GraphQL server configuration. If you see it, it's already clear. You can reach the correct result without refetching the relation changes in the cached data on the client side with a trick like this.
For example, let's say we have updated the Product with category. In the data to return from the update mutation, it is sufficient to return only the id of the relation.
For this to work, category and product must be cached separately on the client beforehand.
for example:
mutation UpdateProduct($product: UpdateProductInput!) {
updateProduct(product: $product) {
id
title
category {
id
}
}
}
You can also write in writeFragment, which is a separate method, which is the most stingy, but it can make your job difficult in nested data.
export class ProductFragmentService {
constructor(private apollo: Apollo) {}
updateProduct(product: Product): void {
const client = this.apollo.client;
client.writeFragment({
id: `Product:${product.id}`,
fragment: gql`
fragment UpdateProductCategoryFragment on Product {
__typename
id
title
category {
id
}
}
`,
data: {
__typename: 'Product',
...product,
},
});
}
}
If you want all the fields belonging to category, you need to send them to resolver and return as a response from there. Otherwise, yes, it gives a warning that I could not find the name property.
The more profitable way of doing it is to send this data to the resolver with the input, as I wrote above, and return to the client as a response from the server.
If you still have to make another SQL request, it is necessary to call the same id after registration.
#Authorized()
#Mutation(() => Product, { description: 'Add new product' })
async addProduct(
#Arg('product') productInput: ProductInput
): Promise<Product> {
await this.productRepo.save(productInput);
return await this.productRepo.findOne({ where: { id: productInfo.id } });
}
that's all :)

RealmJS: Correct query to fetch object from list with AND consition

Schema of Product has many details like: productName, manufactured date, manufacturer name, product type etc. But I need to support flexibility of having no defined schema for product details. To achieve this below is the Realm-JS schema.
import Realm from 'realm';
export default class ProductModel extends Realm.Object { }
ProductModel.schema = {
name: 'ProductModel',
properties: {
productAttributes: { type: 'list', objectType: 'ProductDetailsModel' },
ID: { type: 'string', optional: true },
}
};
import Realm from 'realm';
export default class ProductDetailsModel extends Realm.Object { }
ProductDetailsModel.schema = {
name: 'ProductDetailsModel',
properties: {
attributeName: { type: 'string', optional: true },
attributeValue: { type: 'string', optional: true },
}
};
This flexibility is yielding in complexity of writing filter query. I need to be able to filter based on particular Product attribute name and Product Attribute values.
let query = "productAttributes.attributeName = " + "\"" + "Manufacturer" + "\"" + " AND productAttributes.attributeValue = [c]" + "\"" + "Pepsico" + "\"";
var productList = this.realm.objects('ProductModel').filtered(query).snapshot();
Please help me to write query to filter the ProductModel from DB matching ProductDetailsModel both attributeName and attributeValue ?
Current query does not match single ProductDetailsModel where attributeName ="Manufacturer" AND attributeValue = "Pepsico"
You reference it by the Objects local attribute, not the name of the linked object. e.g.
let attributeName = "Manufacturer"
let attributeValue = "Pepsico"
let query = `productAttributes.attributeName ==[c] '${attributeName}' AND productAttributes.attributeValue ==[c] '${attributeValue}'`

Realm react native schema versioning

I have the following 2 schemas in realm.js file
class Bill extends Realm.Object {}
Bill.schema = {
name: 'Bill',
primaryKey: 'id',
properties: {
id: { type: 'string', indexed: true },
year: 'int',
month: 'int',
description: 'string',
dateCreated: 'int',
}
};
class User extends Realm.Object {}
User.schema = {
name: 'User',
primaryKey: 'id',
properties: {
id: 'string',
name: 'string?',
email: 'string?'
}
};
const realm = new Realm({schema: [Bill, User]});
export default realm;
this works perfectly when I first release my app to AppStore or PlayStore.
I need to change both schema and release to AppStore or PlayStore again and I need to handle both new installation or update of my app where the schema change to following
class Bill extends Realm.Object {}
Bill.schema = {
name: 'Bill',
primaryKey: 'id',
properties: {
id: { type: 'string', indexed: true },
year: 'int',
month: 'int',
description: 'string',
dateCreated: 'int',
dateDeleted: 'int',
}
};
class User extends Realm.Object {}
User.schema = {
name: 'User',
primaryKey: 'id',
properties: {
id: 'string',
name: 'string?',
email: 'string?',
photoUrl: 'string?',
}
};
by adding one more field in each schema.
So how should I configure my realm schema version?
Should I configure like below:
const realm = new Realm([
{schema: Bill, schemaVersion: 1},
{schema: User, schemaVersion: 1}
]);
But this may crash for new installation.
You should set global schemaVersion for all database, not for each model. And perform migrations to the current version, like it's described in the docs:
You define a migration and the associated schema version by updating
the schemaVersion and defining an optional migration function. Your
migration function provides any logic needed to convert data models
from previous schemas to the new schema. When opening a Realm the
migration function will be applied to update the Realm to the given
schema version only if a migration is needed.
If no migration function is supplied then any new properties an
automatically added and old properties are removed from the database
when updating to the new schemaVersion. If you need to update old or
populate new properties when upgrading your version you can do this in
the migration function. For example, suppose we want to migrate the
Person model declared earlier. You can populate the name property of
the new schema using the old firstName and lastName properties:
Realm.open({
schema: [PersonSchema],
schemaVersion: 1,
migration: (oldRealm, newRealm) => {
// only apply this change if upgrading to schemaVersion 1
if (oldRealm.schemaVersion < 1) {
const oldObjects = oldRealm.objects('Person');
const newObjects = newRealm.objects('Person');
// loop through all objects and set the name property in the new schema
for (let i = 0; i < oldObjects.length; i++) {
newObjects[i].name = oldObjects[i].firstName + ' ' + oldObjects[i].lastName;
}
}
}
}).then(realm => {
const fullName = realm.objects('Person')[0].name;
});
Once the migration is successfully completed the Realm and all of its
objects can be accessed as usual by your app.

Sequelize and Graphql reverse lookup

I have two Models:
River.associate = models => {
River.hasMany(models.Fish, { as: 'Fishes' });
};
Fish.associate = models => {
Fish.belongsTo(models.River);
};
type River {
id: ID!
name: String!
alternative: String!
geojson: JSON
fishes: [Fish]
}
type Fish {
id: ID!
name: String!
}
How would I findAll Rivers given a list of Fish ID's? Im not sure how the query must look for this for graphql and sequelize?
type Query {
river(id: ID!): River
**rivers(fishIds: ARRAY): River ??????**
fishes: [Fish]
}
Query: {
rivers: (_, { fishIds }) => {
return River.findAll({
where: {
fishes: fishIds
}
});
},
}
You can specify WHERE clauses for each of the models you include. Moreover, doing so will convert the eager load to an inner join (which is what you want), unless you explicitly set the required param to false.
That means you should be able to do something like:
River.findAll({ include:
[
{
model: Fish,
where: { id: fishIds }
},
],
})

Virtual "name" field?

I need to have the name field of a model be virtual, created by concatenating two real fields together. This name is just for display only. I've tried the virtual examples in the doc, no luck. Keystone 4 beta5.
var keystone = require('keystone')
_ = require('underscore');
var Types = keystone.Field.Types;
/**
* Foo Model
* ==================
*/
var Foo = new keystone.List('Foo', {
map: {name: 'fooname'},
track: true
});
Foo.add({
bar: { type: Types.Relationship, required: true, initial: true, label: 'Barref', ref: 'Bar', many: false },
order: { type: Types.Select, required: true, initial: true, label: 'Order', options: _.range(1,100) },
price: { type: Types.Money, format: '$0,0.00', label: 'Price', required: true, initial: true },
});
Foo.schema.virtual('fooname').get(function() {
return this.bar+ ' ' + this.order;
});
Foo.defaultColumns = 'fooname, bar, order, price';
Foo.register();
When I use this model definition, I don't see the virtual name in the defaultcolumns list. I want to make a virtual name so lookups are easier when this model is used as a relationship.
You don't need a virtual to do this. Keystone allows you to track and recalculate a field every time the document is saved. You can enable those options in order to create a function which concatenates these two values for you (either synchronously or asynchronously, your choice.)
One other thing I noticed is that bar is a Relationship, which means you will need to populate that relationship prior to getting any useful information out of it. That also means your value function will have to be asynchronous, which is as simple as passing a callback function as an argument to that function. Keystone does the rest. If you don't need any information from this bar, and you only need the _id (which the model always has), you can do without the keystone.list('Bar') function that I included.
http://keystonejs.com/docs/database/#fields-watching
The map object also refers to an option on your model, so you'll need a fooname attribute on your model in any scenario, though it gets calculated dynamically.
var keystone = require('keystone'),
_ = require('underscore');
var Types = keystone.Field.Types;
/**
* Foo Model
* ==================
*/
var Foo = new keystone.List('Foo', {
map: {name: 'fooname'},
track: true
});
Foo.add({
fooname: { type: Types.Text, watch: true, value: function (cb) {
// Use this if the "bar" that this document refers to has some information that is relevant to the naming of this document.
keystone.list('Bar').model.findOne({_id: this.bar.toString()}).exec(function (err, result) {
if (!err && result) {
// Result now has all the information of the current "bar"
// If you just need the _id of the "bar", and don't need any information from it, uncomment the code underneath the closure of the "keystone.list('Bar')" function.
return cb(this.bar.name + " " + this.order);
}
});
// Use this if you don't need anything out of the "bar" that this document refers to, just its _id.
// return cb(this.bar.toString() + " " + this.order);
} },
bar: { type: Types.Relationship, required: true, initial: true, label: 'Barref', ref: 'Bar', many: false },
order: { type: Types.Select, required: true, initial: true, label: 'Order', options: _.range(1,100) },
price: { type: Types.Money, format: '$0,0.00', label: 'Price', required: true, initial: true },
});
Foo.defaultColumns = 'fooname, bar, order, price';
Foo.register();
try this:
Foo.schema.pre('save', function (next) {
this.name = this.bar+ ' '+ this.order;
next();
});
Could you provide more information? What is currently working? How should it work?
Sample Code?
EDIT:
After creating the model Foo, you can access the Mongoose schema using the attribute Foo.schema. (Keystone Concepts)
This schema provides a pre-hook for all methods, which registered hooks. (Mongoose API Schema#pre)
One of those methods is save, which can be used like this:
Foo.schema.pre('save', function(next){
console.log('pre-save');
next();
});