How do I make a Hasura data API query to fetch rows based on the length of the their array relationship's value? - hasura

Referring to the default sample schema mentioned in https://hasura.io/hub/project/hasura/hello-world/data-apis i.e. to the following two tables:
1) author: id,name
2) article: id, title, content, rating, author_id
where article:author_id has an array relationship to author:id.
How do I make a query to select authors who have written at least one article? Basically, something like select author where len(author.articles) > 0

TL;DR:
There's no length function that you can use in the Hasura data API syntax right now. Workaround 1) filter on a property that is guaranteed to be true for every row. Like id > 0. 2) Build a view and expose APIs on your view.
Option 1:
Use an 'always true' attribute as a filter.
{
"type": "select",
"args": {
"table": "author",
"columns": [
"*"
],
"where": {
"articles": {
"id": {
"$gt": "0"
}
}
}
}
}
This reads as: select all authors where ANY article has id > 0
This works because id is an auto-incrementing int.
Option 2:
Create a view and then expose data APIs on them.
Head to the Run SQL window in the API console and run a migration:
CREATE VIEW author_article_count as (
SELECT au.*, ar.no_articles
FROM
author au,
(SELECT author_id, COUNT(*) no_articles FROM article GROUP BY author_id) ar
WHERE
au.id = ar.author_id)
Make sure you mark this as a migration (a checkbox below the RunSQL window) so that this gets added to your migrations folder.
Now add data APIs to the view, by hitting "Track table" on the API console's schema page.
Now you can make select queries using no_articles as the length attribute:
{
"type": "select",
"args": {
"table": "author_article_count",
"columns": [
"*"
],
"where": {
"no_articles": {
"$gt": "0"
}
}
}
}

Related

ConditionExpression for PutItem not evaluating to false

I am trying to guarantee uniqueness in my DynamoDB table, across the partition key and other attributes (but not the sort key). Something is wrong with my ConditionExpression, because it is evaluating to true and the same values are getting inserted, leading to data duplication.
Here is my table design:
email: partition key (String)
id: sort key (Number)
firstName (String)
lastName (String)
Note: The id (sort key) holds randomly generated unique number. I know... this looks like a bad design, but that is the use case I have to support.
Here is the NodeJS code with PutItem:
const dynamodb = new AWS.DynamoDB({apiVersion: '2012-08-10'})
const params = {
TableName: <table-name>,
Item: {
"email": { "S": "<email>" },
"id": { "N": "<someUniqueRandomNumber>" },
"firstName": { "S": "<firstName>" },
"lastName": { "S": "<lastName>" }
},
ConditionExpression: "attribute_not_exists(email) AND attribute_not_exists(firstName) AND attribute_not_exists(lastName)"
}
dynamodb.putItem(params, function(err, data) {
if (err) {
console.error("Put failed")
}
else {
console.log("Put succeeded")
}
})
The documentation https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html says the following:
attribute_not_exists (path)
True if the attribute specified by path does not exist in the item.
Example: Check whether an item has a Manufacturer attribute.
attribute_not_exists (Manufacturer)
it specifically says "item" not "items" or "any item", so I think it really means that it checks only the item being overwritten. As you have a random sort key, it will always create a new item and the condition will be always true.
Any implementation which would check against a column which is not an index and would test all the records would cause a scan of all items and that is something what would not perform very well.
Here is an interesting article which covers how to deal with unique attributes in dynamodb https://advancedweb.hu/how-to-properly-implement-unique-constraints-in-dynamodb/ - the single table design together with transactions would be a possible solution for you if you can allow the additional partition keys in your table. Any other solution may be challenging under your current schema. DynamoDB has its own way of doing things and it may be frustrating to try to push to do things which it is not designed for.

Firebase / Firestore - How to manually create indexes

I'm developing a mobile app with react-native.
I am using a third party template to get going.
This templates creates a Firestore database collection named "users".
This template also keeps giving me errors about not having an index on that "users" collection:
[FirebaseError: The query requires an index. You can create it here: https://console.firebase.google.com/v1/r/project/social-marketplace-3c8df/firestore/indexes?create_composite=CmRwcm9qZWN0cy9zb2NpYWwtbWFya2V0cGxhY2UtM2M4ZGYvZGF0YWJhc2VzLyhkZWZhdWx0KS9jb2xsZWN0aW9uR3JvdXBzL1NvY2lhbE5ldHdvcmtfUG9zdHMvaW5kZXhlcy9fEAEaDAoIYXV0aG9ySUQQARoNCgljcmVhdGVkQXQQAhoMCghfX25hbWVfXxAC]
When I copy this "create index" link from the error message and paste it into the browser, it takes me to an empty Firebase console page. No indexes being created as the result of this.
WhenI attempt to manually create such index by navigating to the "indexes" tab on the Firestore page, selecting composite index, and entering "/users" as my collection name, I'm being told that "/users" is reserved, so I am unable to create such index.
Is there a way to create such an index on my "/users" collection?
Can create a file firestore.indexes.json
Leter create your index:
{
"indexes": [
{
"collectionGroup": "collection_name",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "your_field_name",
"order": "ASCENDING"
},
{
"fieldPath": "your_field_name",
"order": "DESCENDING"
}
]
},
]
}
Remember:
CollectionGroup = collection name
queryScope = type (collection | document)
fields = fields for orderBy
singleField = only field for orderBy
As #Renaud Tarnec's comment it fixed by using the Firebase console in :
Cloud Firestore > Indexes panel > Click Add Index button.
Input "users" or value in Collection ID text-box.
In Field to index, add at least two fields - this is required
Scroll down > Query scopes > Select if Collection or Collection Group
And it will enable the Create index button.

JsonPath For multiple object

I want to know the name who purchased items "abc" and "def" from the bellow json data.
(Expected result is "tom")
Please tell me how to do using JsonPath.
[
{
"name": "tom",
"purchased": [
{
"name": "abc"
},
{
"name": "def"
}
]
},
{
"name": "bob",
"purchased": [
{
"name": "xyz"
}
]
}
]
If you are doing this in Java you could select the name of the buyer like this:
$..[?(#.purchased && #..name contains 'abc' && #..name contains 'def' )].name
In JavaScript, you can use this a query like this:
$.[?(#.purchased && #.purchased.some(e => e.name==='abc') && #.purchased.some(e => e.name==='def') )].name
Both queries use a similar approach:
We filter for a purchased-property, then use a function to search the keys in the map for certain values
Jayway's JsonPath offers a contains function to do so;
In JavaScript we leverage script evaluation to search the map/dict for our key/values
So, the .some() is not a JsonPath function but some modern JavaScript (:D).
This is kind of a workaround in both cases but it gets the job done.
You can test both online here. Select the tab JayWay and click Go for the first query, switch to tab Goessner to run the JavaScript sample (you can run the second here as well).

How to update a nested object inside an array in DynamoDB

Consider the following document item / syntax in a DynamoDB table:
{
"id": "0f00b15e-83ee-4340-99ea-6cb890830d96",
"name": "region-1",
"controllers": [
{
"id": "93014cf0-bb05-4fbb-9466-d56ff51b1d22",
"routes": [
{
"direction": "N",
"cars": 0,
"sensors": [
{
"id": "e82c45a3-d356-41e4-977e-f7ec947aad46",
"light": true,
},
{
"id": "78a6883e-1ced-4727-9c94-2154e0eb6139",
}
]
}
]
}
]
}
My goal is to update a single attribute in this JSON representation, in this case cars.
My approach
I know all the sensors IDs. So, the easiest way to reach that attribute is to find, in the array, the route which has a sensor with any of the ids. Having found that sensor, Dynamo should know which object in the routes array he has to update. However, I cannot run this code without my condition being rejected.
In this case, update attribute cars, where the route has a sensor with id e82c45a3-d356-41e4-977e-f7ec947aad46 or 78a6883e-1ced-4727-9c94-2154e0eb6139.
var params = {
TableName: table,
Key:{
"id": "0f00b15e-83ee-4340-99ea-6cb890830d96",
"name": "region-1"
},
UpdateExpression: "set controllers.intersections.routes.cars = :c",
ConditionExpression: ""controllers.intersections.routes.sensors.id = :s",
ExpressionAttributeValues:{
":c": 1,
":s": "e82c45a3-d356-41e4-977e-f7ec947aad46"
},
ReturnValues:"UPDATED_NEW"
};
docClient.update(params, ...);
How can I achieve this?
Unfortunately, you can't achieve this in DynamoDB without knowing the array index. You have very complex nested structure. The DynamoDB API doesn't have a feature to handle this scenario.
I think you need the array index for controllers, routes and sensors to get the update to work.
Your approach may work in other databases like MongoDB. However, it wouldn't work on DynamoDB. Generally, it is not recommended to have this complex structure in DynamoDB especially if your use case has update scenario.
TableName : 'tablename',
Key : { id: id},
ReturnValues : 'ALL_NEW',
UpdateExpression : 'set someitem['+`index`+'].somevalue = :reply_content',
ExpressionAttributeValues : { ':reply_content' : updateddata }
For updating nested array element need to fing out array index . Then you can update nested array element in dynamo db.

Can one use .validate directives to synchronize schema in Firebase?

In the Firebase security rules file, could one effectively use the '.validate' directives to synchronize schema, that is, make sure when one part of the Firebase is modified, another part gets updated as well? Or is this a bad/invalid idea?
Basically what I'm trying to do is create a Todo list app. I want to be able to create tasks in my todo list with specified tags. For each tag in the app, there should be a respective tag child field in the "tags" field in my Firebase backend. The tag field should contain an ordered list of references to tasks in the "tasks" field. So, if a task is created with "#foo" tag, then it's name (id) should be referenced in "tasks/foo". Also, whenever a task is removed, it's reference in each tag should be removed, and when a tag in has no children it should be removed.
To clarify here's an example of the schema:
{
"tasks": {
"-sdjfaowneono": {
"headline": "Get some milk",
"completed": false,
"tags": {
"0": "all",
"1": "shopping"
}
},
"-asdfhsadfsafwirn": {
"headline": "Pick up the kids",
"completed": false,
"tags": {
"0": "all"
}
}
},
"tags": {
"all": {
"0": "-sdjfaowneono",
"1": "-asdfhsadfsafwirn"
},
"shopping": {
"0": "-sdjfaowneono"
}
}
}
Again, if task "-sdjfaowneono" was removed, so would the "shopping" tag be removed.
In conclusion, I need to synchronize the "tags" schema with the "tasks" schema in a specific way. Should I do this synchronization in the Security Rules? Or should I do this at the app level? And if I do it at the app level, there is a risk that the app bugs out and doesn't add/remove a tag which it should, so how do I overcome that?
To use this with security rules, you will need to change one or the other to use the tag id as the key, rather than an array value, since there is no way to specify a wild card or search multiple children to see if one contains the correct value.
So if you changed your "tags" path to look like this:
"tags": {
"all": {
"-sdjfaowneono": true,
"-asdfhsadfsafwirn": true
},
"shopping": {
"-sdjfaowneono": true
}
}
Then you could validate like this:
"tasks": {
"$task": {
"tags": {
"$tag": {
".validate": "root.child('tags/'+newData.val()+'/'+$task).exists()"
}
}
}
}
This uncovers a basic design practice that I've discovered while building with Firebase: I tend to prefer keys to values for storing any sort of foreign key reference.
Also, keep in mind that this only enforces that they match up on write. It does help with deletions since .validate will only run if the value exists.
You could enforce the same behavior on deletions by appending this to the write rule:
".write": " ... && (newData.exists() || !root.child('tags/'+newData.val()+'/'+$task).exists())"

Resources