firebase_rules 0.2.0 copy "firebase_rules: ^0.2.0" to clipboard
firebase_rules: ^0.2.0 copied to clipboard

A type-safe Firebase rules generator for Firestore, Storage, and Realtime Database

A type-safe Firebase rules generator for Firestore, Storage, and Realtime Database

Features #

  • Create rules for Firestore, Storage, and Realtime Database in a type-safe environment
  • Custom lint rules to catch issues before deployment
  • Mimics the Firebase rules syntax for easy migration

The benefits:

  • Type-safe access to your data model. Get errors if rules don't match the model.
  • Code completion for the rules language. No more guessing what functions are available.
  • Rules are easier to read and maintain
  • Add comments to Realtime Database rules

Limitations #

  • Realtime Database rules are not really type-safe, but you do get the benefit of having code completion

Installation #

It is recommended to create rules in a dedicated project to prevent issues

pubspec.yaml

dependencies:
  firebase_rules: latest
  # If you are using types from `cloud_firestore_platform_interface`
  firebase_rules_convert: latest

dev_dependencies:
  build_runner: latest
  firebase_rules_generator: latest

  # To use `firebase_rules_linter`
  custom_lint: latest
  firebase_rules_linter: latest

analysis_options.yaml

# To use `firebase_rules_linter`
analyzer:
  plugins:
    - custom_lint

Usage #

Annotations #

The starting point of all rules is the annotation. Firestore, Storage, and Database rules should be defined in their own files to prevent conflicts.

import 'package:firebase_rules/database.dart';
import 'package:firebase_rules/firebase.dart';

/// Create rules for Firestore
@FirebaseRules(service: Service.firestore)
final firestoreRules = [];

/// Create rules for Storage
@FirebaseRules(service: Service.storage)
final storageRules = [];

/// Create rules for Realtime Database
@DatabaseRules()
final databaseRules = [];

Matches #

Now we can start defining Match statements

import 'package:firebase_rules/firebase.dart';
import 'shared.dart';

@FirebaseRules(service: Service.firestore)
final firestoreRules = [
  /// Always start with this match. [firestoreRoot] is the root of Firestore.
  Match<FirestoreResource>(
    firestoreRoot,

    /// Match statements give access to type-safe contextual information:
    /// - The first parameter is the wildcard. Use `_` if there is no wildcard.
    /// - [request] gives access to the [Request] object
    /// - [resource] gives access to the [Resource] object
    ///
    /// The wildcard parameter must match the the path wildcard
    /// The wildcard for [firestoreRoot] is `database`
    /// The [request] and [resource] parameters must not be renamed
    matches: (database, request, resource) => [
      /// Subsequent matches should use typed [FirestoreResource] objects.
      /// This makes the [request] and [resource] parameters type-safe.
      Match<FirestoreResource<User>>(
        /// Paths are only allowed to contain one wildcard. If you need more
        /// wildcards, nest matches.
        '/users/{userId}',

        /// The [userId] parameter matches the `userId` wildcard
        rules: (userId, request, resource) => [],
      ),
      Match<FirestoreResource>(
        '/other/stuff',

        /// Since there is no wildcard in this path, use `_`
        rules: (_, request, resource) => [],
      ),
    ],
  ),
];

@FirebaseRules(service: Service.storage)
final storageRules = [
  /// Always start with this match. [storageRoot] is the root of Storage.
  Match<StorageResource>(
    storageRoot,

    /// The wildcard for [storageRoot] is `bucket`
    matches: (bucket, request, resource) => [
      /// All storage matches use [StorageResource] objects
      Match<StorageResource>(
        '/images/{imageId}',
      ),
    ],
  ),
];

Rules #

Rules are why we're here

import 'package:firebase_rules/firebase.dart';
import 'shared.dart';

@FirebaseRules(service: Service.firestore)
final firestoreRules = [
  Match<FirestoreResource>(
    firestoreRoot,
    matches: (database, request, resource) => [
      Match<FirestoreResource<User>>(
        '/users/{userId}',
        rules: (userId, request, resource) => [
          Allow([Operation.read], resource.data.userId.rules() == userId),
        ],
      ),
    ],
  ),
];

Rules Language #

This package contains a reimplementation of the Firebase rules language in Dart. These calls are translated to the correct Firebase rules syntax by the generator.

import 'package:firebase_rules/firebase.dart';

void example() {
  /// Dart objects can be converted to rules objects by calling `.rules()` on them
  ''.rules().range(0, 1);

  /// Methods called on `rules` types also take `rules` types as arguments.
  /// Calling `.rules()` on an iterable or map also allows for casting.
  [].rules<RulesString>().concat([].rules());

  /// Global rules functions are available on the `rules` object
  rules.string(true);

  /// Use the `raw` function if type-safe code is impractical.
  /// The `raw` function also allows for casting.
  rules.raw<bool>("foo.bar.baz == 'qux'");

  /// Types from `cloud_firestore_platform_interface` can also be converted
  /// with the `firebase_rules_convert` package
  /// ex: `Blob`, `GeoPoint`, `Timestamp`
}

Functions #

Top-level functions can be used as rules functions

import 'package:firebase_rules/firebase.dart';

bool isSignedIn() {
  /// Null-safety operators will be stripped by the generator
  ///
  /// There is a globally available [request] object if type-safe access to
  /// [RulesRequest.resource] is not required. Otherwise, pass a typed
  /// [RulesRequest] object to the function.
  return request.auth?.uid != null;
}

@FirebaseRules(
  service: Service.firestore,

  /// Functions must be declared at a top level
  functions: [isSignedIn],
)
final rules = [
  Match<FirestoreResource>(
    firestoreRoot,
  ),
];

Organization #

Any of the function arguments of a match statement can be split out for organization

import 'package:firebase_rules/firebase.dart';
import 'shared.dart';

/// Match parameter functions can be split out for organization. However, these
/// must be declared in the same file. Note that match functions cannot contain
/// a body.
List<Match> detached(
  RulesString database,
  RulesRequest<FirestoreResource> request,
  FirestoreResource resource,
) =>
    [
      Match<FirestoreResource<User>>(
        '/users/{userId}',
        rules: (userId, request, resource) => [
          Allow([Operation.read], resource.data.userId.rules() == userId),
        ],
      ),
    ];

@FirebaseRules(service: Service.firestore)
final firestoreRules = [
  Match<FirestoreResource>(firestoreRoot, matches: detached),
];

Enums #

Enums can be replaced with raw strings by the generator

import 'package:firebase_rules/firebase.dart';

@FirebaseRules(
  service: Service.firestore,

  /// Pass in enum conversion maps for all enums you plan to use in these rules
  enums: [Test.map],
)
final firestoreRules = [
  Match<FirestoreResource>(
    firestoreRoot,
    matches: (database, request, resource) => [
      Match<FirestoreResource<TestResource>>(
        '/test',
        rules: (_, request, resource) => [
          Allow([Operation.read], resource.data.test == Test.a),
        ],
      ),
    ],
  ),
];

enum Test {
  a,
  b,
  c;

  static const map = {
    Test.a: 'a',
    Test.b: 'b',
    Test.c: 'c',
  };
}

abstract class TestResource {
  Test get test;
}

Generation #

Finally, we can run the generator

$ dart pub run build_runner build

For every rules.dart file, this will generate a rules.rules file in the same directory. Point your Firebase config to this file to use the rules.

Realtime Database #

Database rules are similar to Firestore and Storage rules, but they have a few differences:

  • The first match must start with rules. That is the root of the database.
  • Wildcards are denoted with $
import 'package:firebase_rules/database.dart';

@DatabaseRules()
final databaseRules = [
  Match(
    /// First match must start with `rules`
    r'rules/users/$userId',

    /// The path parameter must match the wildcard name
    read: (userId) => auth != null && auth?.uid == userId,
    write: (userId) => userId == 'user1'.rules(),
    validate: (userId) => !data.exists(),
    indexOn: ['uid', 'email'],
    matches: (userId) => [
      Match(
        r'contracts/$contractId',
        read: (contractId) =>
            root
                .child('users'.rules())
                .child(userId)
                .child(contractId)

                /// The `val` type parameters will be stripped by the generator
                .val<int?>() !=
            null,
        write: (contractId) =>
            root.child('users'.rules()).child(userId).child(contractId).val() !=
            null,
      ),
    ],
  ),
];

Additional Information #

This package is still early in development. If you encounter any issues, please create an issue with a minimal reproducible sample.

14
likes
160
pub points
60%
popularity

Publisher

verified publisheriodesignteam.com

A type-safe Firebase rules generator for Firestore, Storage, and Realtime Database

Repository (GitHub)
View/report issues

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

meta

More

Packages that depend on firebase_rules