# Firestore Schema

- Implement CRUD operations for your collection in just a couple of lines of code.<br/>
  `create`, `delete`, `update`, `get`, `get several`, `query`
- Get each field of a document validated when trying to `create` or `update`
- Define a field that reference a document on another collection and get all the behaviors that you expect from that
- Define events to occur when a schema is created, updated or deleted
- Define custom types to describe how you want to treat certain fields
- Use easy tools to implement the most common relations
- Use typescript's interfaces to get a better intellisense

## Usage

First you will need to pass a valid firebase app instance to FirestoreSchema to work

```js
import firebase from "@firebase/app";
import "@firebase/firestore";
import "@firebase/auth";
import FirestoreSchema from "firestore-schema";

const firebaseAppInstance = firebase.initializeApp(FIREBASE_CONFIG_OBJECT);

FirestoreSchema.setup(firebaseAppInstance); // <-- We need this. !important
```

Now that we have FirestoreSchema setted up..
Define the schema of your collections and firestore-schema will provide you an object with all the CRUD methods for that collection.

All the documents when retrieved will have an `id`, `updated_at` and `created_at` field.

```js
import FirestoreSchema, { type } from "firestore-schema";

const Article = new FirestoreSchema("Comments", {
  title: type.String,
  content: type.String,
  tags: type.List({ child: type.String, defaultValue: [] })
});
```

Now if we want to create a document, we can use the `create` method of the schema and it will validate all fields according to the Article schema

```js
Article.create({
  title: "Hello World",
  content: "Lorem ipsum dolor sit amet",
  tags: ["firestore"]
});
```

if you try to:

```js
Article.create({
  title: 123, // <- this will fail, it should be of type string
  content: "Lorem ipsum dolor sit amet",
  tags: ["firestore"]
});
```

## Create relations

Firestore Schema also offers more complex manipulation.<br/>
Lets say now we want to add a field `author` to our Article schema

```js
import FirestoreSchema, { type } from "firestore-schema";

const Users = new FirestoreSchema("Users", {
  name: type.String
});

const Article = new FirestoreSchema("Articles", {
  title: type.String,
  content: type.String,
  tags: type.List({ child: type.String }),
  author: type.LookUp({ relation: "Users" }) // <- This should do the work
});

// Now to create a document we just:
Article.create({
  author: "User-ID",
  title: "Hello World",
  content: "Lorem ipsum dolor sit amet",
  tags: ["firestore"]
});

// The author field will be validated and made sure that the document id passed actually exists

// Also, if you run
Article.get("Article-ID");
// the `author` field will be resolved, and instead of the user's id you will get the user document in place
```

## One-to-many relation

`createRelation` will return two fields that when implemented on your schemas they will add some events and connections between them./

This will use firestore sub-collections and this comes with some performance improvements when retrieving multiple user's articles at once.

```js
import FirestoreSchema, { type, createRelation } from "firestore-schema";

const USERS = "users",
  ARTICLES = "articles";

/**
 * createRelation will return a tuple
 * First value = type.List({ child: type.LookUp });
 * Second value = type.LookUp
 *
 * But why not just to use those fields directly on the schema?
 * Because by using `createRelation()` FirestoreSchema will handle this scenario in a smart way.
 * By creating document copies and sub-collections to improve performance, and mantaining all the copies in sync without having you to worry about anything.
 */
const [userArticles, articleAuthor] = createRelation();

const Users = new FirestoreSchema(USERS, {
  name: type.String,
  articles: userArticles // <- Here's one
});

const Article = new FirestoreSchema(ARTICLES, {
  title: type.String,
  content: type.String,
  tags: type.List({ child: type.String }),
  author: articleAuthor // <- Aaand the other
});
```

## Query

All your schemas will have a method `query` that will retrieve only the data you are interested in.\
First arg is the document id, second arg is a query object.

Query Object:

- Use `false` if you dont want to resolve a field and just get the raw value from the document\
- Use `true` if you want to resolve the field.
- Use an object if the field is of type `LookUp` and you want to only retrieve specific fields

```js
Article.query("Article-ID", {
  content: true,
  author: {
    name: true,
    // You can even do this if you want
    articles: {
      content: true
    }
    // You get the idea
    // you can retrieve whatever you want
  }
});
```

## Events

Sometimes you need to do something after a document is created/updated/deleted, you can do that by using schema events.
When defining a new schema theres a third optional argument that accepts an object with the following interface:

```js
interface SchemaEvents {
  onCreate: (newRecord, transaction?) => void | Promise<void>;
  onDelete: (recordId, transaction?) => void | Promise<void>;
  onUpdate: (getOldRecord, fieldsUpdated, transaction?) => void | Promise<void>;
}
```

- All events will run before the operation is executed.
- You can see how the record **created** will look like by accessing the `newRecord` argument
- You must only use the **write methods** of the Firestore.Transaction argument.<br/>
  This allows you to increase the performance by using the same transaction for all operations related)

## Types

Types are functions that describe how to treat a field.<br/>
Firestore Schema provides 8 types out of the box:

- `String`
- `Number`
- `Boolean`
- `DateTime`
- `Any`
- `List`
- `OneOf` (Like an enum)
- `LookUp` (A reference to another document)

To use a type you can just pass the function without calling it,
or you can use the function with an object as argument.
The argument object contains at least 2 properties: `required` and `defaultValue`, but they can have more.

```js
const Article = new FirestoreSchema("Articles", {
  title: type.String({ required: true }), // Here we call the function with some arguments
  content: type.String, // Here we pass just the function
  tags: type.List({ child: type.String, defaultValue: [] }),
  author: type.LookUp({ relation: "Users", required: true })
});
```

## Custom Types

Sometimes, you need a field to be treated different than what the default types offer.<br/>
For those cases, you can create a custom type by using the `createType` method.<br/>

When creating a custom type, you will be able to set the following:

- **validations**: Array of functions that receive a value and return `Boolean` or `Promise<Boolean>`. This functions are run on `create` and `update`.
  - Return `true` if the value is valid.
  - Return `false` if the value is invalid and should be ommited.
  - Throw an error if the value is invalid and should avoid the document creation/update.
- **convertTo**: Function that receives a value and returns another one. This function is run on `create` or `update`. You should use it if you want the value to change when storing it.
- **resolve**: Function that receives the value of the field and should return a new value. This function is run when retrieving a document
- **args**: Arguments that you will need to do all of the above ^

<br/>
For example, here we have a custom type that will take an object and will convert it to string, and then when retrieving it it will convert it back to an object:

```js
const JsonType = createType("json", (store, getArgs) => {
  const convertTo = (value: Object) => JSON.stringify(value);
  const resolve = (value: string) => JSON.parse(value);

  return {
    convertTo,
    resolve
  };
});

const Foo = new FirestoreSchema("Foo", {
  data: JsonType, // <- Just like the default types, you don't need to pass any args if you don't want to
  metadata: JsonType({ required: true })
});
```

## CodeSandbox example

You will need to provide a firebase config for this to work.
The files that may be of your interest are: `schema.setup.ts` and `actions.ts`

https://codesandbox.io/s/z3yr8525k4
