Ntt.js - Fs Based Persistence Framework
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.
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 parametercontent
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 aslistResources
but for entities. Returns a list of string representing the ids of entities in the resourcegetEntity(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 noentityId
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 stringdeserialize(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
.