Skip to content

imbyr/firestore

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Firestore ORM

Server ORM to process Firestore operations.

Main goal of this library is to provide as simple as possible interface, good code design and batch of features đź’›

If you unable to find feature that you interested in, please report an issue and we'll discuss it!

@imbyr/firestore on npmjs.com

Contents

Configuration

Install @imbyr/firestore:

npm install @imbyr/firestore --save

Then you need to install @google-cloud/firestore. You can use @google-cloud/firestore or firebase-admin.

Using @google-cloud/firestore package:

npm install @google-cloud/firestore --save

Firestore configuration:

import { configureFirestore } from '@imbyr/firestore';
import { Firestore } from '@google-cloud/firestore';

const firestore = new Firestore({ keyFilename: 'path/to/serviceAccount.json' });
configureFirestore(firestore);

Using firebase-admin package:

npm install firebase-admin --save

Firestore configuration:

import { configureFirestore } from '@imbyr/firestore';
import * as admin from 'firebase-admin';

admin.initializeApp({ credential: admin.credential.cert('path/to/serviceAccount.json') });

const firestore = admin.firestore();
configureFirestore(firestore);

Usage example

Here the simple example how to manage tasks:

import { Document, getCollection } from '@imbyr/firestore';

// Collection name
const TASKS = 'tasks';

@Document(TASKS)
class Task {
  id: string;
  fulfilled = false;
  constructor(public name: string) { }
}

(async () => {
  // Instantiate collection.
  const tasks = getCollection(Task);

  // Prepare fulfilled query.
  const fulfilled = tasks.where('fulfilled', true);
  
  // Create task documents.
  for (let i = 0; i < 5; i++) {
    const task = new Task('Task #' + i);
    await tasks.create(task);
  }

  await tasks.count(); // returns: 5
  await fulfilled.count(); // returns: 0

  // Fulfill 4 tasks.
  for await (const task of tasks.limit(4)) {
    task.fulfilled = true;
    await tasks.update(task);
  } 

  await fulfilled.count(); // returns: 4

  // Delete all fulfilled tasks.
  for await (const task of fulfilled) {
     await tasks.delete(task);
  }

  await tasks.count(); // returns: 1
  await fulfilled.count(); // returns: 0

  // Find the remaining task.
  const task = await tasks.first();
})();

Document mapping

Signature: @Document(collectionName: string, idKey = 'id');

Current decorator maps class as document of particular collection.

The document must have the id property. By default key is id but you can use your own.

With default id property

Example:

import { Document } from '@imbyr/firestore';

@Document('tasks')
class Task {
  id: string;
  name: string;
}

With custom id property

Example:

import { Document } from '@imbyr/firestore';

@Document('tasks', 'tid')
class Task {
  tid: string;
  name: string;
  // ...
}

Document reference

Signature: @Reference(referentType: Type)

References encapsulates Firestore logic to make relation between documents.

Important: when document with reference reads, normalizer under the hood gets every reference as particular document. Optimization ideas are welcome.

Example:

import { Document, Reference, getCollection } from '@imbyr/firestore';

@Document('employees')
class Employee {
  id: string;
}

@Document('tasks')
class Task {
  id: string;

  @Reference(Employee)
  assignee: Employee;

  constructor(assignee: Employee) {
    this.assignee = assignee;
  }
}

(async () => { 
  const employees = getCollection(Employee);

  // Create a single employee.
  const employee = new Employee();
  await employees.create(employee);

  const tasks = getCollection(Task); 

  // Create a single taks for employee.
  const task = new Task(employee);
  await tasks.create(task);

  // Find task where assignee
  const status = await tasks.find({ assignee: employee });
  console.log(status);

  /*
   * Result:
   * 
   * Task {
   *   id: 'auto-generated id',
   *   assignee: Employee {
   *     id: 'auto-generated id',
   *   }
   * }
   */
})();

Collection

Collection extends QueryBuild and provides CRUD API for particular collection.

Get collection

Factory function that return instance of Collection<T>;

Signature: getCollection<T extends object>(documentType: Type<T>): Collection<T>;

Example:

import { getCollection } from '@imbyr/firestore';

// Get tasks collection
const collection = getCollection(Task);

Find first document

Signature: #first(): Promise<T>

If document not found DocumentNotFoundError will be thrown.

Example:

const task = await collection.first();

Query example:

const task = await collection.where('active', true).first();

Find document by id

Signature: #find<T>(id: string): Promise<T>

If document not found DocumentNotFoundError will be thrown.

Example:

const document = await collection.find('1');

Find one document where

Signature: #find<T>(where: Record<string, any | any[]>): Promise<T>

If record value is Array then WhereOperator.In will be used. In other cases WhereOperator.EqualTo uses.

If document not found DocumentNotFoundError will be thrown.

Example:

const document = await collection.find('1');

Count documents

Signature: #count(): Promise<number>

Example:

const count = await collection.count();

Fetch document array

Signature: #fetch(): Promise<T[]>

Example:

const documents = await collection.fetch();

Example with query:

const documents = await collection.limit(20).fetch();

Iterate documents

Collection can be iterated using async for/of.

Example:

for await (const document of collection) {
  console.log(document);
}
for await (const document of collection.limit(20)) {
  console.log(document);
}

Stream documents

If you familiar with NodeJS streams you can use it for different purposes.

Signature: #stream(): NodeJS.ReadableStream

Collection stream example:

const documents = [];

const stream = collection.stream();
stream.on('data', document => documents.push(document));
stream.on('end', () => {
    console.log('Found ' + documents.length ' documents')
});

Query stream example:

const stream = collection
  .select('id')
  .where('active', 'true')
  .orderBy('name')
  .limit(10)
  .offset(5)
  .stream();

stream.on('data', document => documents.push(document));
stream.on('end', () => {
    console.log('Found ' + documents.length ' documents')
});

Create document

Signature: #create(document: T): Promise<void>

Example:

const task = new Task();
await collection.create(task);

Update document

Signature: #update(document: T): Promise<void>

Example:

const document = await collection.first();
document.name = 'Another name';
await collection.update(document);

Delete document

Signature: #delete(document: T): Promise<void>

Example:

const document = await collection.first();
await tasks.delete(document);

Query builder

QueryBuilder is an interface to prepare filtering/sorting/pagination expressions for read operation.

Example:

const builder = new QueryBuilder()
  .select('id')
  .where('active', true)
  .whereIn('status', ['foo', 'bar'])
  .orderBy('created')
  .limit(20)
  .offset(10);

Every operation clones instance of QueryBuilder and increases #version value.

Version

Version specifies count of operations that was executed on current instance. This value uses in Collection to identify that instance was not modified.

Example:

const version = builder.version;

Select

Signature: #select(keys: '*' | string | string[]): this

Example:

builder = builder.select('*');
// or
builder = builder.select('id');
// or
builder = builder.select(['id', 'name']);

Where

Signatures:

  • #where(key: string, value: any): this shortcut for #where(key: string, WhereOperator.EqualTo, value: string): this
  • #where(key: string, operator: WhereOperator, value: any): this

Equal to (==) example:

builder = builder.where('id', '1');
// or
builder = builder.where('id', WhereOperator.EqualTo, '1');
// or
builder = builder.whereEqualTo('id', '1');

Less than (<) example:

builder = builder.where('id', WhereOperator.LessThan, '1');
// or
builder = builder.whereLessThan('id', '1');

Less than or equal to (<=) example:

builder = builder.where('id', WhereOperator.LessThanOrEqualTo, '1');
// or
builder = builder.whereLessThanOrEqualTo('id', '1');

Greater than (>) example:

builder = builder.where('id', WhereOperator.GreaterThan, '1');
// or
builder = builder.whereGreaterThan('id', '1');

Greater than or equal to (>=) example:

builder = builder.where('id', WhereOperator.GreaterThanOrEqualTo, '1');
// or
builder = builder.whereGreaterThanOrEqualTo('id', '1');

Array contains (array-contains) example:

builder = builder.where('id', WhereOperator.ArrayContains, '1');
// or
builder = builder.whereArrayContains('id', '1');

In (in) example:

builder = builder.where('id', WhereOperator.In, ['1']);
// or
builder = builder.whereIn('id', ['1']);

Array contains any (array-contains-any) example:

builder = builder.where('id', WhereOperator.ArrayContainsAny, ['1']);
// or
builder = builder.whereArrayContainsAny('id', ['1']);

Where record

WhereRecord decomposes to Where[]. When value is type of Array then WhereOperator.In will be applied. In other cases WhereOperator.EqualTo used.

Signatures:

  • type WhereRecord = Partial<Record<string, any | any[]>>
  • #where(record: WhereRecord): this

Example:

builder = builder.where({ 
  id: '1', 
  status: 'active',
  author: ['author-a', 'author-b'], 
});

Order by

Signatures: #orderBy(key: string, direction = OrderByDirection.Ascending): this

Example:

builder = builder.orderBy('id');
// or
builder = builder.orderBy('id', OrderByDirection.Ascending);
// or
builder = builder.orderBy('id', OrderByDirection.Descending);

Limit

Signatures: #limit(limit: number): this

Example:

builder = builder.limit(20);

Offset

Signatures: #offset(offset: number): this

Example:

builder = builder.offset(20);

To query

Signatures: #toQuery(): Query

Example:

const query = builder.toQuery();

Query object

Query object is the result or QueryBuilder.

Signature:

import { Where, OrderBy } from '@imbyr/firestore';

interface Query {
  select: string[];
  where: Where[];
  orderBy: OrderBy[];
  limit: number | null;
  offset: number | null;
}

Licence

See LICENSE for details.