Collections

Nitric provides functionality for provisioning and interacting with collections in NoSQL databases.

Definitions

Collections

In these databases you store data in documents, which are then organized into collections. Collections can most often be thought of as a category of related documents. E.g. countries.

Documents

A document is a uniquely identifiable item within a collection. For example, if countries were a collection then usa might be a document within that collection. The documents themselves can be thought of as a simple JSON document.

Sub-collections

Sub-collections are collections that are stored within a document. If we use the previous example, then states might be a sub-collection that holds states within the usa document. Sub-collections can be thought of as an array of documents within a JSON document. Sub-collections behave identically to collections, but unlike collections, sub-collections are created at runtime rather than deploy time.

Relationship between Collections and Documents

Below is an example of a collection, documents, and a sub-collection in JSON format to demonstrate the relationship that each of the types have.

// Countries collection
[
  // Country document
  {
    "id": "USA",
    "population": 329500000,
    // States sub-collection
    "states": [
      // State document
      { "id": "Alabama" },
      { "id": "Alaska" },
      ...
      { "id": "Wyoming" }
    ]
  },
  // Country Document
  {
    "id": "Canada",
    "population": 38250000
  }
]

Creating Collections

Nitric allows you to define named collections. When defining collections, you can give the function permissions for reading, writing, or deleting documents in the collection.

Here's an example of how to create a collection, with permissions for reading, writing, and deleting:

import { collection } from '@nitric/sdk'

const countries = collection('Countries').for('reading', 'writing', 'deleting')

Creating Documents

Documents are created based on an id and the contents of the document. If a document with that id already exists in the collection, then the document will be overwritten.

Documents that are created using the Nitric SDK are compatible across cloud providers.

The below example first creates a collection that has permissions for writing. It then adds a document to that collection, with an id of USA and contents which describe the document.

import { collection } from '@nitric/sdk'

const countries = collection('Countries').for('writing')

await countries.doc('USA').set({
  name: 'United States of America',
  population: 329500000,
})

If you then want to update the document, you will have to write over the existing document by referencing it be it's id.

await countries.doc('USA').set({
  name: 'United States of America',
  population: 330000000,
})

Accessing Documents

To access documents you can either use an id lookup or a document query. An id lookup will return the exact document with that id, however you must know the id ahead of time. A query allows you to search for documents within a document that match specified criteria. Queries are discussed in "Querying Collections".

The below is an example of accessing a document from a collection.

import { collection } from '@nitric/sdk'

const countries = collection('Countries').for('reading')

const country = await country.doc('USA').get()

The below is a more complete example of searching for a document based on a country name provided by a path parameter.

import { api, collection } from '@nitric/sdk'

const countries = collection('Countries').for('reading')

const countriesApi = api('Countries')

countriesApi.get('/country/:name', async (ctx) => {
  const id = ctx.req.params['name']

  ctx.res.body = await countries.doc(id).get()
})

Deleting Documents

Documents can be deleted based on a documents id.

The below example first creates a collection that has permissions for deleting and writing. It then creates a document called USA, which is deleted using delete on the document reference.

import { collection } from '@nitric/sdk'

const countries = collection('Countries').for('deleting', 'writing')

await countries.doc('USA').set({
  name: 'United States of America',
  population: 329500000,
})

await countries.doc('USA').delete()

Querying Collections

Querying documents allows for searching for a set of documents that meet a certain constraint. An example of this is searching for any country that has a population of over 100 million.

const largeCountries = await countries
  .query()
  .where('population', '>', 100000000)
  .fetch()

To limit the amount of results returned you can use the limit function. This will limit the amount of responses up to or equal to the amount provided. The following example shows searching for countries whose names start with S but only returning 10 results.

const sCountries = await countries
  .query()
  .where('name', 'startsWith', 'S')
  .limit(10)
  .fetch()

Query Operators

The where() method takes three parameters: the field to filter on, a comparison operator, and a value. Nitric supports the following comparison operators:

  • < less than (Lt)
  • <= less than or equal to (Le)
  • == equal to (Eq)
  • > greater than (Gt)
  • >= greater than or equal to (Ge)
  • != not equal to (Ne)
  • startsWith

Compound (AND) queries

You can combine constraints with a logical AND by chaining multiple where() operations together.

The following example shows looking for a country that has over 100 million population and is not the United States of America.

const query = countries
  .query()
  .where('name', '!=', 'United States of America')
  .where('population', '>=', 100000000)

const results = await query.fetch()

Paging Results

Pagination divides results into "pages" of data which are more manageable than getting all the data at once. Once the application processes the first page, it can then process the next page, and so on. To enable an application to know when the last page ended and the new page starts, a paging token is used. The below example shows fetching 1000 documents, and then using a paging token to get the next 1000 documents.

const query = countries.query().limit(1000)

// Fetch first page
let results = await query.fetch()

let pagingToken = results.pagingToken

// Fetch next page
if (pagingToken) {
  results = await query.pagingFrom(pagingToken).fetch()

  pagingToken = results.pagingToken
}

Streaming Results

An alternative solution to paging is to stream the documents from the query. This way documents can be handled asynchronously rather than page by page.

const query = countries.query()

const stream = query.stream()

stream.on('data', (doc) => {
  console.log(doc.content)
})

Creating Sub-collections

Working with a sub-collection is very similar to working with a collection, except they can be created dynamically at runtime. You can construct a reference to sub-collection within an existing document to begin working with documents within that sub-collection.

const states = countries.doc('USA').collection('States')

// Get a document from the sub-collection
const stateOfColorado = await states.doc('Colorado').get()

Query Sub-collections

You can query sub-collections in the same way that you can query collections, using fetching, paging, or streaming.

const query = countries.doc('USA').collection('States').query()

// Fetching
const results = query.fetch()

// Paging
const results = query.pagingFrom(results.pagingToken).fetch()

// Streaming
const stream = query.stream()

stream.on('data', (doc) => {
  console.log(doc.content)
})

You can also query common sub-collections across multiple documents when they have the same name.

This sub-collection reference is only queryable, since it's really an aggregate of all States sub-collections across all Countries documents. i.e. Query every state in every country.

For example, to query every state from every country, you can use the following code.

const allStates = countries.collection('States')

const results = allStates.query().fetch()