npm package: @firestore-rules/cli
Description: > TODO: description
Author: YutaUra
Latest version: 0.1.16

# `@firestore-rules/cli`

CLI library for automatic generation of Firestore rules.

## Install

```shell
yarn add -D @firestore-rules/cli
# or npm add @firestore-rules/cli --save-dev
```

## Usage

### Create Firestore Rules Model

create `./src/firestore-rules.ts`

```ts
// ./src/firestore-rules.ts

import { FirestoreRulesModel, type, op } from '@firestore-rules/cli'

export default class extends FirestoreRulesModel {
  get path() {
    return '/databases/{database}/documents'
  }
  get isAuthenticated() {
    return this.defineFunc(op.ne(this.ctx.request.auth, new type.Null()))
  }
  get isMe() {
    const uid = this.args('uid')
    return this.defineFunc(
      op.and(
        this.isAuthenticated.call([]),
        op.eq(uid, this.ctx.request.auth.uid)
      )
    )
  }

  get userCollection() {
    const self = this
    return this.defineModel(
      class extends FirestoreRulesModel {
        get path() {
          return '/users/{uid}'
        }
        get() {
          return self.isMe.call([this.variables.uid])
        }
        create() {
          return self.isAuthenticated.call([])
        }
        update() {
          return self.isMe.call([this.variables.uid])
        }
      }
    )
  }
}
```

Then run the following command

```shell
yarn firestore-rules
# or ./node_modules/.bin/firestore-rules
```

Then you will get the following `firestore.rules`

```
// !!! DO NOT EDIT !!!
// This file is generated by @firestore-rules/cli
rules_version = "2";
service cloud.firestore {
  match /databases/{database}/documents {
    function isAuthenticated() {
      return (request.auth != null);
    }
    function isMe(uid) {
      return (isAuthenticated() && (uid == request.auth.uid));
    }
    match /users/{uid} {
      allow get: if isMe(uid);
      allow create: if isAuthenticated();
      allow update: if isMe(uid);
    }
  }
}
```

## Options

There are several ways to specify the options

- `"firestore-rules"` in `package.json`
- `.firestore-rulesrc` (both `json` or `yaml` format is ok)
- `.firestore-rulesrc.{json,yaml,yml,js,cjs}`
- `firestore-rules.config.{js,cjs}`

See [cosmiconfig](https://github.com/davidtheclark/cosmiconfig) for other specification methods.

### Option Properties

```yaml
rulesPath: string # path to firestore-rules file. (default is './src/firestore-rules.ts')
typescript: boolean # if you do not use typescript, turn off. (default is true)
tsconfigPath: string # path to your tsconfig file. (default is './tsconfig.json')
outputPath: string # path to generate file. (default is 'firestore.rules')
backupFile: boolean # if you don't want to create backup file, turn off. (default is true)
formatOption: # options for formatter
  indent: tab or space # indent type. (default is space)
  indentSize: number # indent size. (default is 2)
  lineLength: number # Maximum length of a line. (* Currently under development)
```

#### rulesPath

Specify the path of the entry file to be read by cli.

Default is `./src/firestore-rules.ts`.

#### typescript

Set to true if you want to use Typescript.

Default is true.

#### tsconfigPath

If you use TypeScript, you should provide valid tscondif file.

Default is `./tsconfig.json`

#### outputPath

Specify the path of the file to output.

Default is `firestore.rules`.

#### backupFile

Set to true if you want the original rule to be backed up when generating the file.

Default is true.

The name of the file to be backed up will be `outputPath + '.backup'`.

#### formatOption

Options to be passed to [@firestore-rules/formatter](https://github.com/YutaUra/firestore-rules/tree/main/packages/formatter).

Check [@firestore-rules/formatter](https://github.com/YutaUra/firestore-rules/tree/main/packages/formatter) for details

## Writing Style

### 1. Export default class that extends FirestoreRulesModel

```ts
import { FirestoreRulesModel } from '@firestore-rules/cli'

export default class extends FirestoreRulesModel {
  // ...
}
```

### Define a getter named path

The returned value will be the value to apply to the match statement in firestore.rules.

```ts
// ...
export default class extends FirestoreRulesModel {
  get path() {
    return '/databases/{database}/documents'
  }
}
```

### Define a function with no arguments

You can use a method called `defineFunc`.

You can use `this.ctx` to refer to a global value such as `request`.

The `literal` and `op` that can be imported from `@firestore-rules/cli` contain all types and operators except for the [`Bytes`](https://firebase.google.com/docs/reference/rules/rules.Bytes) type

```ts
import { FirestoreRulesModel, literal, op } from '@firestore-rules/cli'

export default class extends FirestoreRulesModel {
  /*
   * function isAuthenticated() {
   *   return (request.auth != null);
   * }
   */
  get isAuthenticated() {
    return this.defineFunc(
      op.ne(this.ctx.request.auth, new literal.FirestoreRulesLiteralNull())
    )
  }
}
```

### Define a function with arguments

You can use a method called `defineFunc`, too.

Arguments can be defined by using the `arg` method.

Also, if you want to use a predefined function, you can use `this.<funcName>.call()`.
The first argument of the `call` should be the argument to be passed to the function. `isAuthenticated` does not require any argument, so it will pass an empty array

```ts
import { FirestoreRulesModel, literal, op } from '@firestore-rules/cli'

export default class extends FirestoreRulesModel {
  /*
   * function isMe(uid) {
   *   return (isAuthenticated() && (uid == request.auth.uid));
   * }
   */
  get isMe() {
    const uid = this.args('uid') // create argument
    return this.defineFunc(
      op.and(
        this.isAuthenticated.call([]), // call defined function
        op.eq(uid, this.ctx.request.auth.uid)
      )
    )
  }
}
```

### Nesting match statements

You can use the `defineModel` method to nest match expressions.

The argument should be `clsass` as well as the `class` you are `default export`.

It is also a good idea to replace the `this` reference with a variable such as `self` so that it can be used again.

```ts
import { FirestoreRulesModel, literal, op } from '@firestore-rules/cli'

export default class extends FirestoreRulesModel {
  get userCollection() {
    const self = this
    return this.defineModel(
      class extends FirestoreRulesModel {
        // ...
      }
    )
  }
}
```

### Similarly, define the path

Define the path in the same way as you did for `/databases/{database}/documents`.

```ts
import { FirestoreRulesModel, literal, op } from '@firestore-rules/cli'

export default class extends FirestoreRulesModel {
  get userCollection() {
    const self = this
    return this.defineModel(
      class extends FirestoreRulesModel {
        get path() {
          return '/users/{uid}'
        }
      }
    )
  }
}
```

### Declare the permit conditions

Permission conditions can be specified by overriding the `get`, `list`, `create`, `update`, and `delete` methods.

Also, the document ID (uid in this example) will be automatically associated with `this.variables.uid`.

Since we just used `self`, we can also call functions in the parent hierarchy from `self`.

The `isMe` function needs an argument, so let's give it one.

```ts
import { FirestoreRulesModel, literal, op } from '@firestore-rules/cli'

export default class extends FirestoreRulesModel {
  /*
   * match /users/{uid} {
   *   allow get: if isMe(uid);
   *   allow create: if isAuthenticated();
   *   allow update: if isMe(uid);
   * }
   */
  get userCollection() {
    const self = this
    return this.defineModel(
      class extends FirestoreRulesModel {
        get path() {
          return '/users/{uid}'
        }
        get() {
          return self.isMe.call([this.variables.uid])
        }
        create() {
          return self.isAuthenticated.call([])
        }
        update() {
          return self.isMe.call([this.variables.uid])
        }
      }
    )
  }
}
```

## Others

If you have any questions, please open an Issue!