DOUG STEVENSON: Hey, everyone.
My name is Doug, and thanks for tuning back
into the series on Firebase security rules.
In the past episodes, we looked at how
rules match documents in Cloud Firestore
and the types of permission you can allow on those documents.
Today, I want to talk about the types of data
you can work with, as the typing system with security roles
is a bit different than other languages
you might be familiar with.
As I'm sure you've observed, the expression language
used with Firebase security rules
looks and feels like a programming language,
borrowing much from JavaScript.
And with any language, understanding its data types
is important for writing effective rules.
So let's jump right in and take a look
at the list of primitive types.
There's a bunch of things you're probably
already familiar with--
nulls, Booleans, integers, floating point numbers,
and strings.
I'm sure you already understand essentially
how all of these types work.
Integers and floats are 64-bit values.
Notice that these types are a bit
different than JavaScript types in that JavaScript
just has a number type.
It doesn't distinguish between integers and floats.
Also different than JavaScript is
the fact that these types don't automatically
convert to other types depending on how they're used.
The only exception to this is that ints will become floats
when used in floating point math expressions.
So if you try to compare a string to an integer,
it's an error at runtime, and your rule
will deny the request.
In order to convert from one type to another,
you have to use special conversion functions for that.
Here's a few examples of these functions in use.
It's pretty straightforward.
Boolean values have the usual Boolean operations
that you'd expect--
with comparison, logical AND, logical OR, and negation.
Ints and floats have the usual comparison operators.
Note that unlike JavaScript, there
is no triple equal comparison that checks
both the value and the type.
Comparing two things that are not the same type
yields false, except for ints, which will be
converted to floats as needed.
There are also a variety of standard math operations
that work with numeric types.
Note that they are named spaced under math.
Most of these make sense only for floats,
as they would have no effect on an int.
You can read more about them in the API docs
for security rules.
Strings in security rules are similar in many ways
to JavaScript strings.
Note that you use double equals operator for equality.
There is no equals method as you'd use in Java.
You use the plus operator to build strings.
And you use the square bracket operator
to index characters in strings and also extract sub-strings
from them, given start and end indexes, which
are inclusive and exclusive respectively,
as in most languages.
To get the length of the string, use the size method.
Note that this is different from JavaScript
where you'd use the length property.
To uniformly change the case of a string, use upper
and lower methods.
To see if a string matches a regular expression,
pass the regex string to the matches method.
It just returns true or false.
There is currently no way to extract grouped strings
from the match as you can in other programming languages.
However, you can call the split method
to extract a list of strings when
split on a regular expression.
Be aware that regular expressions have
special characters, such as dot and star,
and you'll need to escape them if you want to split
using those characters.
So be sure to read up on regular expressions
before using this function.
And speaking of lists, there are a couple of collection types
available in security rules.
Currently there are lists and maps.
Unlike many languages, they are not genericized,
so there is no guarantee at compile time of what
types of data might be in them.
The only thing that's certain is that maps
are indexed by strings.
Lists are ordered collections of data, like an array.
Security rules don't have a separate array type.
There is just lists.
But I'm sure you'll find the syntax feels
like a combination of the two.
You can create a new constant list using square brackets
and putting the values inside them separated by commas.
You can compare two lists with a double equals operator.
This will yield true if both lists
contain exactly the same values in the same order.
You can index into a list to get a value,
and you can extract a range of values providing a start
and end index.
You can get the count of values with the size method.
And you can join the values in the array providing
a delimiter.
Each of the values will be automatically converted
to a string for the purpose of concatenation.
Bear in mind that if a value in a list being converted
to a string doesn't have a formal string representation,
it's an error at runtime.
So be careful what values you join.
Now to check the contents of the list, you have a few tools.
And these will come up a lot in security rules.
To check if a value exists anywhere
in a list, use the in operator to get a Boolean result.
In this way, you can kind of think of a list like a set.
Other set like operations are hasAny, hasOnly, and hasAll.
These methods do exactly what they sound like.
hasAny returns true if any of the values in the past list
are present.
hasOnly returns true if the list contains only the values
from the past list.
And hasAll returns true if the list contains
all of the given values.
These operations will come in handy
when implementing user and group permissions using list fields
stored in Cloud Firestore documents.
More on that in the future.
The final thing I'll say about lists
is that they're immutable, meaning you can't
change the contents of a list.
In fact, every value in security rules is immutable.
So don't expect to find ways to change
any value once it's assigned.
Moving along to maps.
In other programming languages, a map-type object
might be called a hash or a dictionary.
With security rules, maps associate a string key
to some other value.
Cloud Firestone documents show up as maps in security rules,
so you'll probably deal with them often.
You can build a new map by using curly braces that
contain key value pairs.
This works a lot like JSON, but you probably
won't do this very often.
Like lists, you can use double equals
to compare two maps, which checks
if they both have exactly the same key value pairs.
You can get a value for a key in one of three ways.
If you know the name of the key, you
can use either dot or square bracket notation.
The square bracket method is required if the name of the key
has strange characters in it that
would cause a compile error or if it's in another variable
or returned from some expression.
Now keep in mind that trying to access
a key that doesn't exist in a map
will cause an error at runtime.
So if you're unsure if it exists and you
don't want to fail immediately, you
have to check it first or use the get method and specify
a default value to return in case it doesn't exist.
You can check to see if a key exists
in the map using the in operator,
like so, which gives you a Boolean response.
Yes, the strings for keys are always case sensitive.
You can get the size of the map, which
is the number of key value pairs, using the size method.
And you can get a list of individual keys and values
with--
no surprise-- the keys and values method.
The Firebase security rules team is also
working on a set type, which you can create from a list.
This gives you a view of a list where there
can be no duplicate values.
And you will be more easily able to see what's
different between two sets.
Sets aren't available at the time of this recording,
so be sure to look for an update and read the API documentation
where they will be described.
There's a few more special object types in security rules,
and I'll list those out real fast.
Timestamp is probably the most common,
as you'll use this to check timestamp type fields in Cloud
Firestore documents.
This is also an object used to represent the current time
of the request in request.time.
Timestamps are internally always represented in UTC.
And you can access all the components of the calendar date
and time in UTC using a variety of methods that don't really
need explanation.
Now, it turns out you can perform mathematical operations
on timestamps just like they're numbers.
This is how you would check if two timestamps are equal
or if one is later than another.
Things get more interesting when you subtract one timestamp
from another.
What you get as a result, in this case,
is a duration object.
A duration object, as you might expect,
describes a length of time.
Like timestamps, duration objects
have both seconds and nanosecond components,
which you can access with a couple of methods.
You can also work with durations mathematically
as if they're numbers.
Timestamps and durations work well together.
Here's a table that describes the results
of various mathematical operations involving both.
If you pause the video and take a moment
to examine what's going on here, you
can see that adding and subtracting durations
with timestamps is the way to verify if a timestamp is
within a valid range.
In order to effectively work with ranges like this,
you need to be able to create duration objects.
There's a function called duration.value,
which you can use for this.
You can see here that it takes two arguments-- a magnitude
and a unit measurement.
If you're familiar with how time units are expressed in Java,
this is similar.
The unit string specifies the base unit
of time, which can be one of these values here,
then combined with the magnitude gives you the final duration.
So if you want a duration of three weeks,
you'd pass 3 and a lowercase w to this function.
If you need a duration of three weeks and two days,
you can use duration math to add two duration objects together,
one for three weeks and another for two days.
Or you can specify the duration entirely
in number of days, whatever you find most convenient.
There's is another type of object called path.
And it typically represents either a path
to a document in Cloud Firestore or a path
to an object in Cloud Storage.
They look just like Unix file paths with forward slashes.
That's how you build them as well.
Note that if you need to use a variable or expression to build
the path, you can use the dollar sign with parentheses
in place of a path segment.
You can also convert a formatted string to a path
with a function called path.
You can compare paths for a quality,
just like other objects.
Many paths you'll deal with will come
from the request.path property, which
is the document or object being checked by a rule,
or from reference type fields from documents
in Cloud Firestore.
You might also want to build a path of a document
to get and examine in your rules,
and we'll see more about that in videos to come.
There is also an object called latlng,
which is short for latitude-longitude, also known
as a GPS coordinate.
You'll only encounter it if you're
using a Geopointe-type field in Cloud Firestore.
So I won't say too much about it.
But it should be clear.
That's all the types we currently have
available in security rules.
I'll say again that lists and maps
can contain values of any type.
Also, when you receive documents from Cloud Firestore,
the types of the fields might not be exactly what you expect.
So before you access any of those values,
you might want to check their types before deciding
what you want to do with them.
Here's how you check those types.
You can use the operator called is
to check the type of something.
This is similar to instance of operator
in Java, which checks types of objects at runtime.
Checking types like this helps prevent you
from generating an error at runtime
if you try to do something with the value
that it doesn't support.
Note that the number type check here works for both ints
and floats.
And speaking of errors, there's one last bit of information
that might be helpful.
As I mentioned before, an error at runtime
will normally make your rule deny
access, just as if the expression yielded
a Boolean false.
This is a defensive measure that's
intended to prevent bad or malicious data from impacting
your system.
But you should be aware that an expression that
triggers an error actually evaluates to an error
value, not a Boolean false.
That's right.
An error is a value just like any other value.
And it can participate in Boolean logic
that can convert it to a Boolean or propagate
that error further.
Here's a table of possible outcomes
when an error is logically combined with a Boolean.
Note that the first and last cases result in another error.
But if you logically and an error with false,
you get another false.
And if you logically or an error with true, you get true.
You can use this behavior to ensure that rule execution is
able to continue in the case of an error that should be ignored
instead of propagating that error all the way
to the end of the expression.
So to be perfectly accurate, if evaluating
an expression for a rule yields true,
then access will be allowed.
But if the expression yields false or error,
access is denied.
The client can't tell the difference
between false or error because it would be a possible security
hole to give away that information.
This is why it's helpful to test your rules
in the local emulator, which will give you
more information about how your rule is actually executed.
That's about all you need to know about types
in Firebase security rules.
I've skipped a few minor details here.
But as usual, you should use the documentation
for more complete information, and the links
are in the description below.
In the next episode of the series,
I'll talk about the two global objects--
request and resource.
You've already seen them a little bit so far,
but there's plenty more to know.
So be sure to subscribe and join me right here
on the Firebase channel on YouTube
to get the next video when it's ready.
And I'll see you then.
[MUSIC PLAYING]