Comparing document timestamps in Firestore rules - google-cloud-firestore

I'm running into a weird problem while writing and testing my Firestore rules. Here's what I want to achieve:
When the application starts, the user gets logged in anonymously. The
user starts a new game.
I create a 'Session' that basically consists of just a timestamp.
The player plays the game, gets a certain highscore and goes to a screen where the score can be sent to the global highscore list. When the highscore is submitted, I check if there's an existing session for this player and if the time that has passed is long enough for the highscore to be considered valid.
On the client (javascript) I use the following line to send the timestamp in my documents:
firebase.firestore.FieldValue.serverTimestamp()
This is the current ruleset. You can see that a score can only be created when the createdAt of the new higscore is later than the createdAt of the session.
service cloud.firestore {
match /databases/{database}/documents {
function isValidNewScoreEntry() {
return request.resource.data.keys().hasOnly(['createdAt', 'name', 'score']) &&
request.resource.data.createdAt is timestamp &&
request.resource.data.name is string &&
request.resource.data.score is int &&
request.resource.data.name.size() <= 20
}
match /highscores/{entry} {
allow list: if request.query.limit <= 10;
allow get: if true;
allow create: if isValidNewScoreEntry() &&
request.resource.data.createdAt > get(/databases/$(database)/documents/sessions/$(request.auth.uid)).data.createdAt;
}
function isValidNewSession() {
return request.resource.data.keys().hasOnly(['createdAt']) &&
request.resource.data.createdAt is timestamp
}
match /sessions/{entry} {
allow list: if false;
allow get: if false;
allow create: if isValidNewSession();
allow update: if isValidNewSession();
}
}
}
When I simulate/test these rules, I get an error that says that I cannot compare a 'timestamp' to a 'map'. I don't know why the 'createdAt' value is a map, but it seems like the get() method returns something different than expected.
My question is: What would be the correct way to compare the property createdAt from the newly submitted entry to the property createdAt of the existing session document, like I'm trying to do in the rules described above.
This is what a'Score' entry look like
This is what a 'Session' entry looks like
EDIT:
I've done some more digging, and found that this line works:
if request.resource.data.createdAt.toMillis() > get(/databases/$(database)/documents/sessions/$(request.auth.uid)).data.createdAt.seconds * 1000;
This makes it pretty clear that not both createdAt are the same format. The last one seems to be a basic object with the properties 'seconds' and 'nanoseconds'. I'm sure it stems from the Timestamp interface, but it gets returned as a flat object since none of the methods found here exist and give an error when trying to call them. The property 'seconds' however does exists on the second timestamp, but is not accessible on the first one.

I've found out why the timestamp is not what I expected and got cast to a 'map'.
After digging through the documentation I found that the get() method returns a resource. resource has a property data: a map. So the get() method does not return a document as I expected but a flat JSON object that gives me all properties found in de database.
https://firebase.google.com/docs/reference/rules/rules.firestore
https://firebase.google.com/docs/reference/rules/rules.firestore.Resource

Related

Can't cast database type timestamp without time zone to Instant

reservationLogs = await this.dbContext.ReservationLogs
   .Where(r => r.ProcessedAt == null)
   .Where(r => r.PropertyId == validPropertyId)
   .OrderBy(r => r.CreatedAt).ThenBy(r => r.Operation)
   .Take(200)
   .ToListAsync();
some times with the same query i get the error
' Can't cast database type timestamp without time zone to Instant'
note: CreatedAt nodaTime instane
i am trying to find the exact reason
The issue is that even though the date and time is clear, it is unclear whether or which timezone was in use. If I tell you that tomorrow at 5 P.M. I will go for a walk, then it will be unclear from your perspective what the exact time it will be, unless you know what timezone was I assuming while saying so.
You have the exact same type of confusion in your code and first, you need to install this plugin: https://www.npgsql.org/doc/types/nodatime.html
According to the docs, you need to add a dependency like this:
using Npgsql;
// Place this at the beginning of your program to use NodaTime everywhere (recommended)
NpgsqlConnection.GlobalTypeMapper.UseNodaTime();
// Or to temporarily use NodaTime on a single connection only:
conn.TypeMapper.UseNodaTime();
The docs go further in specifying how you can read and write values:
// Write NodaTime Instant to PostgreSQL "timestamp with time zone" (UTC)
using (var cmd = new NpgsqlCommand(#"INSERT INTO mytable (my_timestamptz) VALUES (#p)", conn))
{
cmd.Parameters.Add(new NpgsqlParameter("p", Instant.FromUtc(2011, 1, 1, 10, 30)));
cmd.ExecuteNonQuery();
}
// Read timestamp back from the database as an Instant
using (var cmd = new NpgsqlCommand(#"SELECT my_timestamptz FROM mytable", conn))
using (var reader = cmd.ExecuteReader())
{
reader.Read();
var instant = reader.GetFieldValue<Instant>(0);
}
Since you are not directly writing the query, but use the Entity Framework to do so you have another level of expression. But this is well-documented as well. You can safely and soundly declare types like this:
public LocalDate Date {get; set;}
Read this full article: https://www.davepaquette.com/archive/2019/03/26/using-noda-time-with-ef-core.aspx
You will need to find out exactly where the error occurs. It seems to me that the OrderBy is the culprit as well as the selection. You can change the type of the data member of your model.
You can do cast using Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime package
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
builder.UseNpgsql("connection-string",
o => o.UseNodaTime());
}
or:
builder.Services.AddDbContext<ApplicationDbContext>(
options => options.UseNpgsql(
builder.Configuration.GetConnectionString("DefaultConection"),
o => o.UseNodaTime()));
source

Create a trigger that validates the data for the Product object

I have an apex trigger (before insert/update) and a helper class to that trigger. The problem is: When creating an object record, the trigger should check if the AddedDate field is filled and if it's not - then assign it today's date and current time.
And when I create and update a Product object record, the trigger must check the length of the Description field, if the field is longer than 200 characters, I must trim it to 197 characters and add a triple to the end of the line.
What am I doing wrong and how should I proceed?
My trigger:
trigger ProductTrigger on Product__c (before insert, before update) {
if(Trigger.isUpdate && Trigger.isAfter){
ProductTriggerHelper.producthandler(Trigger.new);
}
}
Trigger helper class:
public class ProductTriggerHelper {
public static void producthandler(List<Product__c> products) {
Schema.DescribeFieldResult F = Product__c.Description__c.getDescribe();
Integer lengthOfField = F.getLength();
//List<Product__c> prList = new list<Product__c>();
for(Product__c pr: products){
pr.AddedDate__c=system.today();
if (String.isNotEmpty(pr.Description__c)) {
pr.Description__c = pr.Description__c.abbreviate(lengthOfField);
}
}
}
}
According to your requirements
When creating an object record, the trigger should check if the AddedDate field is filled and if it's not - then assign it today's date and current time.
You aren't doing that.
Change pr.AddedDate__c=system.today(); to
if (pr.AddedDate__c == null) { pr.AddedDate__c=system.today(); }
Also according to the abbreviate function documentation the parameter it takes is the max length including the 3 elipses.
So change pr.Description__c = pr.Description__c.abbreviate(lengthOfField); to
pr.Description__c = pr.Description__c.abbreviate(200);
To add to Programmatic's answer...
You defined the trigger as before insert, before update. Cool, that's perfect place for doing data validations, field prepopulation... And you'll get save to database for free!
But then this clashes with next line if(Trigger.isUpdate && Trigger.isAfter){. With this setup it'll never fire. Either remove the if completely or (if you think trigger can get more events in future) go with trigger.isBefore && (trigger.isInsert || trigger.isUpdate).
P.S. It's datetime field? So pr.AddedDate__c=system.now(); is better

Which is better for database seeding: Add or AddOrUpdate?

I don't understand why it is recommended everywhere to use AddOrUpdate in the Seed method?
We develop application for half a year already and the AddOrUpdates overwrites user changes every time we update the server. E.g. if we call in the Seed:
context.Styles.AddOrUpdate(new Style { Id = 1, Color = "red" });
And user changes the Style to "green" then on next server update we overwrite it to "red" again and we get very annoyed user.
It looks that if we change AddOrUpdate to Add we will be guaranteed from overwriting user data. If we still need some special case we can put it to separate migration. Unlike the general Configuration.Seed method particular migrations don't run twice over the same database version.
I assume that Style's primary key is Id. The overload of AddOrUpdate that you use only checks if there is a record having Id == 1. If so, it updates it. That's all.
What's going wrong here is that the primary key is a surrogate key, i.e. it's there for querying convenience, but it's got no business meaning. Usually, with migrations you want to look for the natural keys of entities though. That's how the user identifies data. S/he wants a green style, not a style identified by 1.
So I think you should use this overload of AddOrUpdate:
context.Styles.AddOrUpdate( s => s.Color,
new Style { Id = 1, Color = "red" });
Now when there is no red style anymore, a new one is inserted, overriding the Id value (assuming that it's generated by the database).
From your later comments I understand that you want to Add data when they're new, but not update them when they exist (compared by primary key). For this you could use a slightly adapted version of an AddWhenNew method I described here. For your case I would do it like so:
public T void MarkAsAddedWhenNew<T>(this DbContext context,
Expression<Func<T, object>> identifierExpression, T item)
where T : class
{
context.Set<T>().AddOrUpdate(identifierExpression, item);
if (context.Entry(item).State != System.Data.Entity.EntityState.Added)
{
var identifierFunction = identifierExpression.Compile();
item = context.Set<T>()
.Local
.Single(x => identifierFunction(item)
.Equals(identifierFunction(x)));
context.Entry(item).State = System.Data.Entity.EntityState.Unchanged;
}
return item;
}
Re-fetching the item from the local collection is a nuisance, but necessary because of a bug in AddOrUpdate(). This bug also caused the error you got when setting the state of the original entry to Unchanged: it was a different instance than the attached one.
The way Add method acts is misleading. It Inserts data into database even if there is already a row with the same PrimaryKey as we do Add. It just creates new PrimaryKey ignoring our value silently. I should have tried it before asking the question, but anyway, I think I'm not the only one who confused by this. So, in my situation Add is even worse than AddOrUpdate.
The only solution I've come to is following:
public static void AddWhenNew<T>(this DbContext ctx, T item) where T : Entity
{
var old = ctx.Set<T>().Find(item.Id);
if (old == null)
ctx.Set<T>().AddOrUpdate(item);
/* Unfortunately this approach throws exception when I try to set state to Unchanged.
Something like:"The entity already exists in the context"
ctx.Set<T>().AddOrUpdate(item);
if (ctx.Entry(item).State != System.Data.Entity.EntityState.Added)
ctx.Entry(item).State = System.Data.Entity.EntityState.Unchanged;
*/
}

Null entity field makes #PostUpdate method silently stop

I have a JPA domain entity that I'm updating from user input. Depending on lots of factors about that object, it has actions to perform when it's updated (in this case, the update is to "mark it completed").
In the database, two "post-completion action configuration" fields (note and next_workflow) are nullable, and either have a value of interest, or are NULL. There may be very many of these, but I'm starting with these two.
I wrote the following method in the model class:
#PostUpdate
public void gotUpdate() {
System.out.println("Got post update for " + this.getDescription());
if (! this.getNote().isEmpty()) {
Note n = new Note();
n.setAssetId(this.getAssetId());
n.setNotifyLessor(1);
n.setNote(this.getLessorNote() + this.getCapturedData());
n.setCreatedDate(new Date());
n.persist();
}
System.out.println("In the middle of post update for " + this.getDescription());
if (this.getNextWorkflow() != 0) {
Asset a = this.getAssetId();
a.setWorkflowId(Workflow.findWorkflow(this.getNextWorkflow()));
a.merge();
}
System.out.println("Finishing post update for " + this.getDescription());
}
For entities with NULL "note" values, the console output is:
Got post update for this item
For entities with non-NULL "note" values and with NULL "nextWorkflow" values, the console output is:
Got post update for this item
In the middle of post update for this item
No errors anywhere, no stack dump, no nothing. The method just silently quits, AND the merge I'm doing on this entity doesn't complete (the database remains unchanged).
Stepping through this in the debugger gets to that line where things are being tested, and if the value is NULL, it pops a tab saying "Source not found", which I don't really know what to make of. I think that's just the debugger saying it can't step into something, but I'm not actually asking it to...
Rookie question, it turns out.
If Object's field field is null, then object.getField().length() is a call to a null pointer.
The answer is to test the field for nullness, not some side-effect of nullness.
if (! (this.getNote() == null)) {
...
}

NullReferenceException while trying to including a one-to-many relationship item after saving parent

Framework: I'm using using MVC 3 + EntityFramework 4.1 Code-First.
Concept: One Legislation entity has many Provision entities. The idea is that the user enters a Legislation entity, that gets saved then the function that saves it passes it along to another function to see whether that Legislation has a ShortTitle. If it does, then it formats it into a properly worded string and includes it as the Legislation's first Provision, then saves the changes to db.
Issue: The problem is, I've tried coding it in different ways, I keep getting a NullReferenceException, telling me to create a new object instance with the "new" keyword, and points me to the savedLegislation.Provisions.Add(provision); line in my second function.
Here are the two functions at issue, this first one saves the Legislation proper:
public Legislation Save(NewLegislationView legislation)
{
Legislation newLegislation = new Legislation();
// Simple transfers
newLegislation.ShortTile = legislation.ShortTile;
newLegislation.LongTitle = legislation.LongTitle;
newLegislation.BillType = legislation.BillType;
newLegislation.OriginatingChamber = legislation.OriginatingChamber;
newLegislation.Preamble = legislation.Preamble;
// More complicated properties
newLegislation.Stage = 1;
this.NumberBill(newLegislation); // Provides bill number
newLegislation.Parliament = db.LegislativeSessions.First(p => p.Ending >= DateTime.Today);
newLegislation.Sponsor = db.Members.Single(m => m.Username == HttpContext.Current.User.Identity.Name);
// And save
db.Legislations.Add(newLegislation);
db.SaveChanges();
// Check for Short titles
this.IncludeShortTitle(newLegislation);
// return the saved legislation
return newLegislation;
}
And the second function which is invoked by the first one deals with checking whether ShortTitle is not empty and create a Provision that is related to that Legislation, then save changes.
public void IncludeShortTitle(Legislation legislation)
{
var savedLegislation = db.Legislations.Find(legislation.LegislationID);
if (savedLegislation.ShortTile.Any() && savedLegislation.ShortTile.ToString().Length >= 5)
{
string shortTitle = "This Act may be cited as the <i>" + savedLegislation.ShortTile.ToString() + "</i>.";
var provision = new Provision()
{
Article = Numbers.CountOrNull(savedLegislation.Provisions) + 1,
Proponent = savedLegislation.Sponsor,
Text = shortTitle
};
savedLegislation.Provisions.Add(provision);
db.SaveChanges();
}
}
I've been researching how SaveChanges() works and whether it is properly returning the updated entity, it does (since I get no issue looking it up in the second function). If it works properly, and the legislation is found and the provision is newly created in the second function, I don't see what is the "null" reference it keeps spitting out.
The null reference in this case would be savedLegislation.Provisions. The Provisions collection won't be initialized to a new List<Provision> when EF returns your Legislation instance from the db.Legislations.Find(...) method.
The first thing I'd try is something like this:
var savedLegislation = db.Legislations
.Include("Provisions")
.First(l => l.LegislationID == legislation.LegislationID);
... but I'd also consider just using the legislation instance that was passed into the method rather than fetching it from the database again.