@firebase-graphql/graphql-codegen-firestore-rules v0.0.10
@firebase-graphql/graphql-codegen-firestore-rules
Abstract
- This package is a member of the
firebase-graphqlpackage family. - This package generates a Firestore Rules file from a GraphQL schema which defined firestore structure.
- You can use this package without GraphQL
Example
First, you need to define the structure of the firestore according to the GraphQL format.
# firestore.graphql
type User
@firestore(document: "/users/{id}")
@auth(
rules: [
{ allow: owner, ownerField: "id", operations: [create, update] }
{ allow: public, operations: [get, list] }
]
) {
id: ID!
name: String!
age: Int
}
type Post
@firestore(document: "/users/{userId}/posts/{id}")
@auth(
rules: [
{
allow: owner
ownerField: "userId"
operations: [create, update, delete]
}
{ allow: public, operations: [get, list] }
]
) {
id: ID!
userId: ID!
title: String!
content: String!
}Then, you can generate the Firestore Rules file. Like this:
rules_version = "2"
service cloud.firestore {
match /databases/{database}/documents {
function isString(value) {
return value is string
}
function isInt(value) {
return value is int
}
function isBoolean(value) {
return value is bool
}
function isFloat(value) {
return value is float
}
function isID(value) {
return value is string
}
function isDate(value) {
return value is timestamp
}
function isMap(value) {
return value is map
}
function isRequired(source, field) {
return field in source && source[field] != null
}
function isNullable(source, field) {
return !(field in source) || source[field] == null
}
function isLoggedIn() {
return request.auth != null
}
function isAuthUserId(userId) {
return isLoggedIn() && request.auth.uid == userId
}
function isPost(value) {
return (
isMap(value) && value.keys().hasOnly(["__typename", "content", "title"])
&& isRequired(value, "__typename") && isString(value.__typename) && value.__typename == "Post"
&& isRequired(value, "content") && isString(value.content)
&& isRequired(value, "title") && isString(value.title)
)
}
function isUser(value) {
return (
isMap(value) && value.keys().hasOnly(["__typename", "age", "name"])
&& isRequired(value, "__typename") && isString(value.__typename) && value.__typename == "User"
&& (isNullable(value, "age") || isInt(value.age))
&& isRequired(value, "name") && isString(value.name)
)
}
match /users/{userId}/posts/{id} {
allow get: if (
true
)
allow list: if (
true
)
allow create: if (
isPost(request.resource.data)
&& isAuthUserId(userId)
)
allow update: if (
isPost(request.resource.data)
&& isAuthUserId(userId)
)
allow delete: if (
isAuthUserId(userId)
)
}
match /users/{id} {
allow get: if (
true
)
allow list: if (
true
)
allow create: if (
isUser(request.resource.data)
&& isAuthUserId(id)
)
allow update: if (
isUser(request.resource.data)
&& isAuthUserId(id)
)
}
}
}How to use
Install
yarn add -D @firebase-graphql/graphql-codegen-firestore-rules @graphql-codegen/cli graphqlMake schema file
# firestore.graphql
type User
@firestore(document: "/users/{id}")
@auth(
rules: [
{ allow: owner, ownerField: "id", operations: [create, update] }
{ allow: public, operations: [get, list] }
]
) {
id: ID!
name: String!
age: Int
}Configure graphql-codegen file
# codegen.yml
generates:
firestore.rules:
schema: firestore.graphql
plugins:
- '@firebase-graphql/graphql-codegen-firestore-rules'Generate Firestore Rules file
yarn graphql-codegenConcept and Usage
Data Validation
this package generate validation functions for firestore.rules.
Literals
This package supports the following literals:
String→ tha value should be astringInt→ tha value should be aintFloat→ tha value should be afloatBoolean→ tha value should be aboolID→ tha value should be astringDate→ tha value should be atimestamp
Enums
You can define enums in the schema.
# firestore.graphql
enum Category {
IT
MARKETING
EDUCATION
}Then, this package generates validation functions for enums. like this:
# firestore.rules
function isCategory(value) {
return value is string && value in ["IT", "MARKETING", "EDUCATION"]
}Other Types
# firestore.graphql
type Image {
url: String!
width: Int!
height: Int!
}
type User {
id: ID!
name: String!
age: Int
profileImage: Image
}
type Post {
id: ID!
title: String!
content: String!
header: Image!
author: User!
}Then,
# firestore.rules
function isImage(value) {
return (
isMap(value) && value.keys().hasOnly(["url", "width", "height"])
&& isRequired(value, "url") && isString(value.url)
&& isRequired(value, "width") && isInt(value.width)
&& isRequired(value, "height") && isInt(value.height)
)
}
functions isUser(value) {
return (
isMap(value) && value.keys().hasOnly(["id", "name", "age", "profileImage"])
&& isRequired(value, "id") && isID(value.id)
&& isRequired(value, "name") && isString(value.name)
&& (isNullable(value, "age") || isInt(value.age))
&& isRequired(value, "profileImage") && isImage(value.profileImage)
)
}
function isPost(value) {
return (
isMap(value) && value.keys().hasOnly(["id", "title", "content", "header", "author"])
&& isRequired(value, "id") && isID(value.id)
&& isRequired(value, "title") && isString(value.title)
&& isRequired(value, "content") && isString(value.content)
&& isRequired(value, "header") && isImage(value.header)
&& isRequired(value, "author") && isUser(value.author)
)
}Firestore Document Access
You can define Type with @firestore directive.
# firestore.graphql
type User @firestore(document: "/users/{id}") {
id: ID!
name: String!
age: Int
}By doing this, you can generate a match expression for firestore.rules
# firestore.rules
match /users/{userId} {
}This way the id field of the User will be treated as a special field and automatically understood to use the value from the path /users/{id}.
So, the validation for User will be as follows.
# firestore.rules
# Id is not included in the validation, since it is a special field obtained from the path.
function isUser(value) {
return (
isMap(value) && value.keys().hasOnly(["__typename", "age", "name"])
&& isRequired(value, "__typename") && isString(value.__typename) && value.__typename == "User"
&& (isNullable(value, "age") || isInt(value.age))
&& isRequired(value, "name") && isString(value.name)
)
}It is recommended that the @firestore directive be used in conjunction with the @auth directive, described next.
Access Control
The @firestore can be followed by an @auth directive to define access controls for that data.
Like this,
# firestore.graphql
type User
@firestore(document: "/users/{id}")
@auth(rules: [{ allow: public, operations: [get, list] }]) {
id: ID!
name: String!
age: Int
}The @auth directive needs to be specified with a rules argument. These rules are calculated as a disjunction.
The format of the rule is as follows
| Field | Type | required | Description |
|---|---|---|---|
| allow | public, private, owner | :white_check_mark: | type of access control |
| operations | list of {get, list,create, update, delete} | :white_check_mark: | The operations to allow access to. |
| ownerField | string (but should be field name) | :negative_squared_cross_mark: (if allow: owner then :white_check_mark:) | Compare that field with the ID of the user accessing it. |
Examples
Anyone can read and write.
# firestore.graphql
type User
@firestore(document: "/users/{id}")
@auth(
rules: [{ allow: public, operations: [get, list, create, update, delete] }]
) {
id: ID!
name: String!
age: Int
}# firestore.rules
match /users/{id} {
allow get: if (
true
)
allow list: if (
true
)
allow create: if (
isUser(request.resource.data)
&& true
)
allow update: if (
isUser(request.resource.data)
&& true
)
allow delete: if (
true
)
}Anyone who is logged in can read and write.
# firestore.graphql
type User
@firestore(document: "/users/{id}")
@auth(
rules: [{ allow: private, operations: [get, list, create, update, delete] }]
) {
id: ID!
name: String!
age: Int
}# firestore.rules
match /users/{id} {
allow get: if (
isLoggedIn()
)
allow list: if (
isLoggedIn()
)
allow create: if (
isUser(request.resource.data)
&& isLoggedIn()
)
allow update: if (
isUser(request.resource.data)
&& isLoggedIn()
)
allow delete: if (
isLoggedIn()
)
}Only the owner can read and write.
# firestore.graphql
type User
@firestore(document: "/users/{id}")
@auth(
rules: [
{
allow: owner
operations: [get, list, create, update, delete]
ownerField: "id"
}
]
) {
id: ID!
name: String!
age: Int
}# firestore.rules
match /users/{id} {
allow get: if (
isAuthUserId(id)
)
allow list: if (
isAuthUserId(id)
)
allow create: if (
isUser(request.resource.data)
&& isAuthUserId(id)
)
allow update: if (
isUser(request.resource.data)
&& isAuthUserId(id)
)
allow delete: if (
isAuthUserId(id)
)
}If the ownerField is not path-based
# firestore.graphql
type User
@firestore(document: "/users/{id}")
@auth(
rules: [
{
allow: owner
operations: [get, list, create, update, delete]
ownerField: "userId"
}
]
) {
id: ID!
userId: ID!
name: String!
age: Int
}# firestore.rules
match /users/{id} {
allow get: if (
isAuthUserId(resource.data.userId)
)
allow list: if (
isAuthUserId(resource.data.userId)
)
allow create: if (
isUser(request.resource.data)
&& isAuthUserId(request.resource.data.userId)
)
allow update: if (
isUser(request.resource.data)
&& (isAuthUserId(request.resource.data.userId) && isAuthUserId(resource.data.userId))
)
allow delete: if (
isAuthUserId(resource.data.userId)
)
}ServerTimestamp
You may want to match the value of a field with its creation or update time, such as createdAt or updatedAt
In such cases, the @createdAt and @updatedAt directives can be used.
# firestore.graphql
type User
@firestore(document: "/users/{id}")
@auth(
rules: [{ allow: private, operations: [get, list, create, update, delete] }]
) {
id: ID!
name: String!
age: Int
# The field name can be anything other than "createdAt" or "updatedAt".
createdAt: Date! @createdAt
updatedAt: Date! @updatedAt
}And this will generate the following rules
# firestore.rules
match /users/{id} {
allow get: if (
isLoggedIn()
)
allow list: if (
isLoggedIn()
)
allow create: if (
isUser(request.resource.data)
&& request.resource.data.createdAt == request.time
&& request.resource.data.updatedAt == request.time
&& isLoggedIn()
)
allow update: if (
isUser(request.resource.data)
&& !("createdAt" in request.resource.data)
&& request.resource.data.updatedAt == request.time
&& isLoggedIn()
)
allow delete: if (
isLoggedIn()
)
}