The usual conundrum faced when building a crapplication that has slightly more brains than a web page, is where to store the data.

On the rich side of the scale, you get databases of all kinds: relational, schema-less, or any of the fancy things that popped in the past 10 years. These are quite frankly expensive when all you need is to store a few documents in a semi-organized fashion.

On the ghetto end, you got file-systems, and the slightly more elaborated stuff like table-storage, which requires more work to abuse, and is one level of abstraction too close to punch cards for you to appreciate your life while doing your stuff.

My mid is in the needle: something that lets me represent hierarchical data without carrying too much about the technology, safest than files, does not require a subscription. El cheapo stuff. Not finding something I liked, I built one, and called it ntt.

npm

ntt is a low-tech REST persistence framework. It lets you persist a resource tree without all those fancy relational technologies, because quite frankly, you don’t need all that. Instead, it provides an abstraction layer over file systems (disk / cloud storage), which, in the day and age we live in, are dirt-cheap.

You can use ntt if your model is not strongly relational, or relations go only one way. If you need any form of indexation, you can also couple it with a search engine such as Azure Search, which also offer some good poor-man options.

ntt currently supports filesystem and Azure Blob Storage, planning on adding S3 one day (pull requests welcome). It’s published on npm.

Using

npm install nttjs

Then

const ntt = require("nttjs");

const adapter = ntt.adapters.fs("./data");
const rootEntity = ntt.entity(adapter);

rootEntity.createResource("examples")
    .then((resource) => resource.createEntity("1"))
    .then((entity) => entity.save("HURRAY"))
    .then(() => root.getResource("examples")
    .then((resource) => resource.getEntity("1")
    .then((entity) => entity.load())
    .then((content) => console.log(content));
    // HURRAY

FS Adapter

fs adapter is instantiated through ntt.adapters.fs(rootFolder). There really is nothing much more to it

Azure blob storage adapter

Adapter needs to be configured. This is done by providing:

  • account: the account name (e.g. mystorageaccount)
  • key: one of the storage account keys.
const containerName = "ntttest";
const configuration = {
  account: process.env.AZURE_STORAGE_ACCOUNT,
  key: process.env.AZURE_STORAGE_KEY
};
const fileAdapter = ntt.adapters.azure(config, containerName);

Entities and resources

ntt works with two intertwined classes: entities and resources. An entity has resources, a resource has entities, and so forth. Entities also have a body, which you can load or save.

Model ressembles this:

rootEntity
|_> resource-1
|   |_> entity-1.1
|   |   |_> resource-1.1.1
|   |_> entity-1.2
|       |_> resource-1.2.1
|       |_> resource-1.2.2
|_> resource-2
    |_> entity-2.1

When loading ntt, your first object is the root entity, which you get by simply passing the file adapter you picked to ntt.entity.

Entities are objects offering the following properties:

  • load() returns a promise, whose callback has one parameter content containing the de-serialized content of the entity.
  • save(entity) serializes, then saves the entity.
  • listResources() lists all sub-resources of the entity. This returns a promise which only parameter is a list of strings representing the name of the sub-resources.
  • getResource(resourceName) returns a promise, whose only parameter is a resource object to manipulate the resource (see below).
  • createResource(resourceName) creates a resource, and returns a resource object to manipulate it, as the only parameter to a promise. This method will not crash if resource already exists, and then just return the existing resource.
  • name is a string, represents the name of the entity.

Resources are objects offering the following properties:

  • listEntities() does the same thing as listResources but for entities. Returns a list of string representing the ids of entities in the resource
  • getEntity(entityId) returns a promise, whose only parameter is an entity object to manipulate the entity (see above).
  • createEntity(entityId) creates an entity with optional parameter to define its id. If no entityId is supplied, a new guid is generated. This will crash if entity already exists, and will return a promise whose only parameter is an entity object.

Other considerations

Serialization

You can specify another serializer than JSON by providing a second attribute to ntt.entity. This second attribute should be an object with two methods:

  • serialize(content) serializes an object and returns a string
  • deserialize(object) deserializes a string and returns an object.

Name and id validation

Ids and names are validated. Validation issues will trigger promise rejection. Default validation accepts only [ a-z0-9_-]+, ignoring case.

To change the default behavior of validation, you can override the validate method of the file adapter you’re using. validate(string) takes the name or id to validate, and returns a promise, resolved if the id or name is valid, and rejected with an error if it’s not.

Storage model

Entities are sub-folders of resources, named after their id. Resources are sub-folders of entities, named after their resource name. Entity body is stored in a entity.json file in the entity directory.

Azure file adapter also creates an empty file called ._ in new resources to persist the folder.

Building

Getting the code

The code is available on github.

Running the tests

Tests are run through npm test. Azure integration tests require to create a storage account, a container called ntttest inside the container, then setting environment variables AZURE_STORAGE_ACCOUNT and AZURE_STORAGE_KEY.