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';
const TASKS = 'tasks';
@Document(TASKS)
class Task {
id: string;
fulfilled = false;
constructor(public name: string) { }
}
(async () => {
const tasks = getCollection(Task);
const fulfilled = tasks.where('fulfilled', true);
for (let i = 0; i < 5; i++) {
const task = new Task('Task #' + i);
await tasks.create(task);
}
await tasks.count();
await fulfilled.count();
for await (const task of tasks.limit(4)) {
task.fulfilled = true;
await tasks.update(task);
}
await fulfilled.count();
for await (const task of fulfilled) {
await tasks.delete(task);
}
await tasks.count();
await fulfilled.count();
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);
const employee = new Employee();
await employees.create(employee);
const tasks = getCollection(Task);
const task = new Task(employee);
await tasks.create(task);
const status = await tasks.find({ assignee: employee });
console.log(status);
})();
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';
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('*');
builder = builder.select('id');
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');
builder = builder.where('id', WhereOperator.EqualTo, '1');
builder = builder.whereEqualTo('id', '1');
Less than (<
) example:
builder = builder.where('id', WhereOperator.LessThan, '1');
builder = builder.whereLessThan('id', '1');
Less than or equal to (<=
) example:
builder = builder.where('id', WhereOperator.LessThanOrEqualTo, '1');
builder = builder.whereLessThanOrEqualTo('id', '1');
Greater than (>
) example:
builder = builder.where('id', WhereOperator.GreaterThan, '1');
builder = builder.whereGreaterThan('id', '1');
Greater than or equal to (>=
) example:
builder = builder.where('id', WhereOperator.GreaterThanOrEqualTo, '1');
builder = builder.whereGreaterThanOrEqualTo('id', '1');
Array contains (array-contains
) example:
builder = builder.where('id', WhereOperator.ArrayContains, '1');
builder = builder.whereArrayContains('id', '1');
In (in
) example:
builder = builder.where('id', WhereOperator.In, ['1']);
builder = builder.whereIn('id', ['1']);
Array contains any (array-contains-any
) example:
builder = builder.where('id', WhereOperator.ArrayContainsAny, ['1']);
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');
builder = builder.orderBy('id', OrderByDirection.Ascending);
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.