0.0.10 • Published 2 years ago

@firebase-graphql/graphql-codegen-firestore-rules v0.0.10

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

@firebase-graphql/graphql-codegen-firestore-rules

npm downloads npm npm

Abstract

  • This package is a member of the firebase-graphql package 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 graphql

Make 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-codegen

Concept and Usage

Data Validation

this package generate validation functions for firestore.rules.

Literals

This package supports the following literals:

  • String → tha value should be a string
  • Int → tha value should be a int
  • Float → tha value should be a float
  • Boolean → tha value should be a bool
  • ID → tha value should be a string
  • Date → tha value should be a timestamp

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

FieldTyperequiredDescription
allowpublic, private, owner:white_check_mark:type of access control
operationslist of {get, list,create, update, delete}:white_check_mark:The operations to allow access to.
ownerFieldstring (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()
  )
}

Contributors

License

License: MIT

0.0.10

2 years ago

0.0.9

2 years ago

0.0.8

2 years ago

0.0.7

2 years ago

0.0.6

2 years ago

0.0.5

2 years ago

0.0.4

2 years ago

0.0.2

2 years ago

0.0.1

2 years ago