Skip to content

Repository files navigation

Welcome to @kv-orm/core 👋

GitHub Actions ChecksLGTM AlertsSynk VulnerabilitiesCodecovLGTM Code QualityCode Climate MaintainabilityVersionLicenseTypesGitHub Last Commit

kv-orm is an Node.JS ORM for key-value datastores. It is currently in beta.

Author

👤 Greg Brimble

🤝 Contributing

Contributions, issues and feature requests are welcome! Feel free to check issues page.

😍 Show your support

Please consider giving this project a ⭐️ if you use it, or if it provides some inspiration!

Supported Datastores

If there is any other datastore that you'd like to see supported, please create an issue, or make a pull request.

Features

  • Support for multiple key-value datastores in a single application.

    import{MemoryDatastore}from"@kv-orm/core";constlibraryDatastore=newMemoryDatastore();constapplicationSecrets=newMemoryDatastore();

    See above for the full list of Supported Datastores.

  • Easy construction of typed Entities using Typescript.

    import{Column,Entity}from"@kv-orm/core";
    @Entity({datastore: libraryDatastore})classAuthor{
    @Column()publicfirstName: string;
    @Column()publiclastName: string;// ...}
  • On-demand, asynchronous, lazy-loading: kv-orm won't load properties of an Entity until they're needed, and will do so seamlessly at the time of lookup.

    import{getRepository}from"@kv-orm/core";constauthorRepository=getRepository(Author);letauthor=awaitauthorRepository.load("william@shakespeare.com");// 1ms - no properties of the author have been loadedconsole.log(awaitauthor.firstName);// 60ms - author.firstName is fetched
  • No unnecessary reads: if a property is already in memory, kv-orm won't look it up again unless it needs to.

    letauthor=awaitauthorRepository.load("william@shakespeare.com");console.log(awaitauthor.lastName);// 60ms - author.lastName is fetchedconsole.log(awaitauthor.lastName);// 1ms - author.lastName is retrieved from memory (no lookup performed)
  • Indexable and Unique Columns allowing quick lookups for specific entity instances:

    import{UniqueColumn,IndexableColumn}from"@kv-orm/core";
    @Entity({datastore: libraryDatastore})classAuthor{// ...
    @IndexableColumn()publicbirthYear: number;
    @UniqueColumn()publicphoneNumber: string;// ...}letauthors=awaitauthorRepository.search("birthYear",1564);// An AsyncGenerator yielding authors born in 1564letauthor=awaitauthorRepository.find("phoneNumber","+1234567890");// A single author with the phone number +1234567890
  • Relationships (*-to-one & *-to-many) with backref support, and on-update & on-delete cascading.

    import{ToOne,ToMany}from"@kv-orm/core";
    @Entity({datastore: libraryDatastore})classAuthor{// ...
    @ToOne({type: Book,backRef: "author",cascade: true})publicbooks: Book[];// ...}
    @Entity({datastore: libraryDatastore})classBook{
    @ToMany({type: Author,backRef: "books",cascade: true})publicauthor: Author;// ...}

Usage

Install

npm install --save @kv-orm/core

Datastores

MemoryDatastore

MemoryDatastore is inbuilt into @kv-orm/core. It is a simple in-memory key-value datastore, and can be used for prototyping applications.

import{MemoryDatastore}from`@kv-orm/core`;constlibraryDatastore=newMemoryDatastore();

Cloudflare Workers KV

See @kv-orm/cf-workers for more information.

Entities

An Entity is an object which stores data about something e.g. an Author. The Entity decorator takes a datastore to save the Entity instances into.

Optionally, you can also pass in a key to the decorator, to rename the key name in the datastore.

You can initialize a new instance of the Entity as normal.

import{Entity}from"@kv-orm/core";
@Entity({datastore: libraryDatastore,key: "Author"})classAuthor{// ...}constauthorInstance=newAuthor();

Columns

Using the @Column() decorator on an Entity property is how you mark it as a savable property. You must await their value. This is because it might need to asynchronously query the datastore, if it doesn't have the value in memory.

Like with Entities, you can optionally pass in a key to the decorator.

import{Column,PrimaryColumn,UniqueColumn,IndexableColumn,}from"@kv-orm/core";import{Book}from"./Book";
@Entity({datastore: libraryDatastore})classAuthor{
@Column({key: "givenName"})publicfirstName: string;
@Column()publiclastName: string;
@Column()publicnickName: string|undefined;
@PrimaryColumn()publicemailAddress: string;
@IndexableColumn()publicbirthYear: number;
@UniqueColumn()publicphoneNumber: string;publicsomeUnsavedProperty: any;
@ToMany({type: ()=>Book,backRef: "author",cascade: true})// More on this laterpublicbooks: Book[]=[];publicconstructor({
firstName,
lastName,
emailAddress,
birthYear,
phoneNumber,}: {firstName: string;lastName: string;emailAddress: string;birthYear: number;phoneNumber: string;}){this.firstName=firstName;this.lastName=lastName;this.emailAddress=emailAddress;this.birthYear=birthYear;this.phoneNumber=phoneNumber;}}constwilliamShakespeare=newAuthor({firstName: "William",lastName: "Shakespeare",emailAddress: "william@shakespeare.com",birthYear: 1564,phoneNumber: "+1234567890",});williamShakespeare.nickName="Bill";williamShakespeare.someUnsavedProperty="Won't get saved!";// When in an async function, you can fetch the value with `await`(async()=>{console.log(awaitauthor.firstName);})();

Primary Columns

Any non-singleton class needs a PrimaryColumn used to differentiate Entity instances. For this reason, PrimaryColumn values are required and must be unique.

@Entity({datastore: libraryDatastore})classAuthor{// ...
@PrimaryColumn()publicemailAddress: string;// ...}

An example of a singleton class where you do not need a PrimaryColumn, might be a global configuration Entity where you store application secrets (e.g. API keys).

Indexable Columns

An IndexableColumn can be used to mark a property as one which you may wish to later lookup with. For example, in SQL, you might perform the following query: SELECT * FROM Author WHERE birthYear = 1564. In kv-orm, you can lookup Entity instances with a given IndexableColumn value with a repository's search method

@Entity({datastore: libraryDatastore})classAuthor{// ...
@IndexableColumn()publicbirthYear: number;// ...}

IndexableColumn types should be used to store non-unique values.

Unique Columns

Columns with unique values can be setup with UniqueColumn. This is more efficient that an IndexableColumn, and the loading mechanism is simpler.

@Entity({datastore: libraryDatastore})classAuthor{// ...
@UniqueColumn()publicphoneNumber: number;// ...}

Property Getters/Setters

If your property is particularly complex (i.e. can't be stored natively in the datastore), you may wish to use a property getter/setter for a Column, to allow you to serialize it before saving in the datastore.

For example, let's say you have a complex property on Author, somethingComplex:

@Entity({datastore: libraryDatastore})classAuthor{// ...
@Column()private_complex: string="";// place to store serialized value of somethingComplexsetsomethingComplex(value: any){// function serialize(value: any): stringthis._complex=serialize(value);}getsomethingComplex(): any{// function deserialize(serializedValue: string): anyreturn(async()=>deserialize(awaitthis._complex))();}// ...}

Repositories

To actually interact with the datastore, you'll need a Repository.

import{getRepository}from"@kv-orm/core";constauthorRepository=getRepository(Author);

Save

You can then save Entity instances.

constwilliamShakespeare=newAuthor({firstName: "William",lastName: "Shakespeare",emailAddress: "william@shakespeare.com",birthYear: 1564,phoneNumber: "+1234567890",});awaitauthorRepository.save(williamShakepseare);

Load

And subsequently, load them back again. If the Entity has a PrimaryColumn, you can load the specific instance by passing in the PrimaryColumn value.

constloadedWilliamShakespeare=awaitauthorRepository.load("william@shakespeare.com");console.log(awaitloadedWilliamShakespeare.nickName);// Bill

Search

If a property has been set as an IndexableColumn (is non-unique), you can search for instances with a saved value.

constsearchedAuthors=awaitauthorRepository.search("birthYear",1564);forawait(constsearchedAuthorofsearchedAuthors){console.log(awaitsearchedAuthor.nickName);// Bill}

Find

If a property has been set as a UniqueColumn, you can directly load an instance by a saved value. If no results are found, null is returned.

constfoundWilliamShakespeare=awaitauthorRepository.find("phoneNumber","+1234567890");console.log(awaitfoundWilliamShakespeare?.nickName);// BillconstfoundNonexistent=awaitauthorRepository.find("phoneNumber","+9999999999");console.log(foundNonexistent);// null

Relationships

All Relationships must supply the type of Entity as a function (to allow circular dependencies) and a backRef (the property name on the inverse-side of the Relationship).

In order to propagate changes automatically on update and on delete, cascade can be set to true.

Note: deleting may be disproportionally intensive as it load, and then must edit or delete every related instance.

One To One / Many To One

For * to one Relationships, use the ToOne decorator.

import{Author,authorRepository}from"./Author";
@Entity({datastore: libraryDatastore})classBook{// ...
@ToOne({type: ()=>Author,backRef: "books",cascade: true})publicauthor: Author;// ...}constbookRepository=getRepository(Book);constwilliamShakespeare=awaitauthorRepository.load("william@shakespeare.com");consthamlet=newBook({title: "Hamlet",author: williamShakespeare});console.log((awaithamlet.author)===williamShakespeare);// true// And because `cascade` was set to `true`, the Author instance has been updated as wellconstbooks=awaitwilliamShakespeare.books;forawait(constbookofbooks){console.log(awaitbook.title);// Hamlet}awaitbookRepository.save(hamlet);// Will also save williamShakespeare

One To Many / Many To Many

ToMany Relationships are slightly different to how Columns and ToOne Relationships work. Instead of setting its value and awaiting a Promise of its value, ToMany Relationships set an array of values, and await a Promise of an AsyncGenerator which yields its values.

@Entity({datastore: libraryDatastore})classAuthor{// ...
@ToMany({type: ()=>Book,backRef: "author",cascade: true})// More on this laterpublicbooks: Book[]=[];// ...}// ...consthamlet=newBook({title: "Hamlet"});williamShakespeare.books=[hamlet];constbooks=awaitwilliamShakespeare.books;forawait(constbookofbooks){console.log(awaitbook.title);// Hamlet}// And because `cascade` was set to `true`, the Book instance has been updated as wellconsole.log((awaithamlet.author)===williamShakespeare);// true - because `cascade` is set to trueawaitauthorRepository.save(williamShakespeare);// Will also save hamlet

Note: The order of returned Entities by the AsyncGenerator is not guaranteed. In fact, as items are loaded into memory, they will be pushed to the front to ensure tha other Entities are loaded only as needed.

Helpers

To simplify interacting with ToMany Relationships, some helpers are available.

Note: saving must still be completed afterwards to persist changes.

addTo

addTo simplifies pushing an element to a ToMany relaionship:

import{addTo}from"@kv-orm/core";addTo(williamShakespeare,"books",hamlet);
removeFrom

removeFrom simplifies splicing an element from a ToMany relaionship:

import{removeFrom}from"@kv-orm/core";removeFrom(williamShakespeare,"books",hamlet);

Types

columnType

When defining an Entity in TypeScript, you must provide types for the properties. For example, firstName and lastName are set as strings:

import{Column,Entity}from"@kv-orm/core";
@Entity({datastore: libraryDatastore})classAuthor{
@Column()publicfirstName: string;
@Column()publiclastName: string;// ...}

However, when reading these values with kv-orm, in fact a Promise<string> is returned. Simply switching these type definitions to Promise<T> is not valid either, as writing values is done synchronously.

Therefore, to improve the developer experience and prevent TypeScript errors such as 'await' has no effect on the type of this expression. ts(80007) and Property 'then' does not exist on type 'T'. ts(2339), when declaring the Entity, wrap the property types in the columnType helper. In the same example:

import{columnType}from"@kv-orm/core";
@Entity({datastore: libraryDatastore})classAuthor{
@Column()publicfirstName: columnType<string>;
@Column()publiclastName: columnType<string>;// ...}

columnType is simply an alias: type columnType<T> = T | Promise<T>.

toOneType

Similarly, for ToOne Relationships:

import{toOneType}from"@kv-orm/core";
@Entity({datastore: libraryDatastore})classBook{// ...
@ToOne({type: ()=>Author,backRef: "books",cascade: true})publicauthor: toOneType<Author>;// ...}

toManyType

And ToMany Relationships:

import{toManyType}from"@kv-orm/core";
@Entity({datastore: libraryDatastore})classAuthor{// ...
@ToMany({type: ()=>Book,backRef: "author",cascade: true})publicbooks: toManyType<Book>;// ...}

Development

Clone and Install Dependencies

git clone git@github.com:kv-orm/core.git
npm install

Run tests

npm run lint # 'npm run lint:fix' will automatically fix most problems
npm test

🚎 Roadmap

FeaturesBugs

📝 License

Copyright © 2019 Greg Brimble.
This project is MIT licensed.

FAQs

My Entity keys are getting mangled when they are saved into the datastore

If you're using a preprocessor that minifies class names, such as Babel, the class constructors names often get shortened. kv-orm will always use this class name, so, either disable minification in the preprocessor, or manually set the key value when creating an Entity e.g.

@Entity({key: "MyClass"})classMyClass{// ...}

How can I upgrade from the alpha (0.0.X)?

Thank you for trying out kv-orm in it's alpha period! Thanks to a generous sponsor, I have been able to complete work to elevate kv-orm to a more featureful beta. Unfortunately, this has meant a couple of minor breaking changes.

  • PrimaryColumn, IndexableColumn and UniqueColumn have been introduced to deprecate the isPrimary, isIndexable and isUnique options on the Column decorator.

  • Repository's find method has been renamted to search, and a different, new function find been added.

    This is probably the most confusing and frustrating breaking change (apologies—I will take efforts to make sure this doesn't happen again). With the introduction of UniqueColumn as a more specific type of IndexableColumn, we needed a way to take advantage of its simpler loading mechanism. Since UniqueColumn has unique-values, there should be only ever one instance with a given value (or none), and so find seemed a more appropriate verb for its loading. Whereas IndexableColumn can have non-unique values, search seemed more appropriate a verb when returning an AsyncGenerator of instances.

  • ToOne & ToMany have been formally introduced (renamed from their drafted ManyToOne & ManyToMany names—the parent's cardinality does not matter).

  • Various bugfixes and improvements. None should have unexpected breaking changes, but if I've missed a use-case, please file a GitHub Issue and tell me about it.

About

A Node.js ORM for key-value datastores

Resources

Code of conduct

Stars

54 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages