0.2.1 • Published 3 years ago

@sunjc826/mini-orm v0.2.1

Weekly downloads
-
License
ISC
Repository
github
Last release
3 years ago

mini-orm

A PostgreSQL data mapper based object relational mapper supporting features like batched CRUD, lazy loading and single table inheritance.

Getting started

You only need to create 3 categories of objects before starting to manipulate database rows using objects.

The code snippets used here are taken from library tests.

Table

The table represents the state of your table in the database. There should be 1 XXXTable object per database table. You can create this object using the createTable function. The options of the arguments passed to createTable is as follows:

  • tableName The snakecasedTableName or camel_cased_table_name, either is fine.
  • columns A hash of snakecasedColumnName to column options, which vary depending on the type of columns. See below for the full list of column options supported. In general, the user can expect universal options like nullable to be on all column types.
const AuthorTable = createTable({
  tableName: "authors",
  columns: {
    id: {
      type: "serial",
      options: {
        primaryKey: true,
      },
    },
    name: {
      type: "text",
      options: {
        nullable: false,
      },
    },
    age: {
      type: "int",
    },
  },
});

Model/Domain Object

In the data mapper pattern, we do not work directly with database rows, but rather domain objects that are more decoupled from the database table structures. Here, you can use the createDomainObject function to create a domain object class to inherit from.

The fields that you want your domain object to have are defined as instance attributes of your domain object class.

Instances created from the below Author class will have the attributes name, age and books.

class Author extends createDomainObject<Author>({ domainKey: "author" }) {
  name: string;
  age: number;
  books: HasMany<Book>;
}

For the basic usecase, it is very easy to use the createDomainObject helper. Simply pass a string, which we will refer to as the domainKey as an option to the createDomainObject helper.

A quick explanation on what is the domainKey

  • In a data mapper pattern, domain objects are decoupled from the underlying tables that are persisted on disk, however, there must still be some ways for the user to indicate which tables are associated with which domain object.
  • The domainKey is a key that makes this link.

Single Table Inheritance

Example:

class Footballer extends extendDomainObject<Footballer>()({
  domainKey: FOOTBALLER,
  ParentDomainObject: Player,
}) {
  club: string;
}

Data Mapper

Now that we have the table and the model, we need some way to translate the a table row to a model object, and vice versa. You can use the createMapper function to create a mapper class.

const AuthorMapper = createMapper({
  domainKey: "author",
  Table: AuthorTable,
  hasMany: {
    books: {},
  },
});

Registry

Finally, now we have a table, data mapper and domain object. Recall the above section where it is mentioned that domainKey associates a triplet of these together.

In our example, this would be AuthorTable, AuthorMapper and Author.

We will declare this association to the registry and register this triplet by the following command.

registry.register("author", AuthorTable, Author, AuthorMapper);

Using the models

After defining the table, data mapper and domain object, you can generally forget about the former two. Most interactions should only involve the domain object.

The basic CRUD operations are as follows.

Query

Queries are done in an ActiveRecord sort of manner, though the underlying pattern is that of a datamapper.

/**
 * Search by id
 */
let author = await Author.findById(1);

/**
 * Search by other sql conditions
 */
// if you do not specify an operator, it will default to equals
author = await Author.find({
    domainObjectField: "name",
    value: "TestAuthor",
});

Insert

Inserts are done using a create method available on every domain object class.

/**
 * Creates an author with name 'TestAuthor' and age 30
 */
Author.create({ name: "TestAuthor", age: 30 });

/**
 * This is needed to save your newly created object to the database.
 */
await DomainObject.commit();
// You can do this.
await Author.commit();
// or this.
await AnyOtherClassInheritingFromDomainObject.commit();

Update

/**
 * Updates author's age attribute to 55.
 */
author.update({ age: 55 });

/**
 * Like insert, this is needed for database persistence.
 */
await DomainObject.commit();

Delete

/**
 * Deletes author.
 */
author.destroy();

/**
 * Like insert and update, this is needed for database persistence.
 */
await DomainObject.commit();

Commit

You can chain a bunch of inserts, updates and deletes before calling commit, which will notify the database of these changes in a single transaction.

Full code snippet

No inheritance

const AUTHOR = "author";
const BOOK = "book";
const PUBLISHER = "publisher";

// tables
const AuthorTable = createTable({
  tableName: "authors",
  columns: {
    id: {
      type: "serial",
      options: {
        primaryKey: true,
      },
    },
    name: {
      type: "text",
      options: {
        nullable: false,
      },
    },
    age: {
      type: "int",
    },
  },
});

const BookTable = createTable({
  tableName: "books",
  columns: {
    id: {
      type: "serial",
      options: {
        primaryKey: true,
      },
    },
    name: {
      type: "text",
    },
    genre: {
      type: "varchar",
      options: {
        limit: 20,
      },
    },
    authorId: {
      type: "int",
      options: {
        references: {
          domainKey: "author",
          tableColumnKey: "id",
        },
      },
    },
  },
});

const PublisherTable = createTable({
  tableName: "publishers",
  columns: {
    id: {
      type: "serial",
      options: {
        primaryKey: true,
      },
    },
    region: {
      type: "text",
    },
    bookId: {
      type: "int",
      options: {
        references: {
          domainKey: "book",
          tableColumnKey: "id",
        },
      },
    },
  },
});

// mappers
const AuthorMapper = createMapper({
  domainKey: AUTHOR,
  Table: AuthorTable,
  hasMany: {
    books: {},
  },
});

const BookMapper = createMapper({
  domainKey: BOOK,
  Table: BookTable,
  belongsTo: {
    author: {},
  },
  hasOne: {
    publisher: {},
  },
});

const PublisherMapper = createMapper({
  domainKey: "publisher",
  Table: PublisherTable,
  belongsTo: {
    book: {},
  },
});

// models
class Author extends createDomainObject<Author>({ domainKey: AUTHOR }) {
  name: string;
  age: number;
  books: HasMany<Book>;
}

class Book extends createDomainObject<Book>({ domainKey: BOOK }) {
  name: string;
  genre: string;
  authorId: string;
  author: BelongsTo<Author>;
  publisher: HasOne<Publisher>;
}

class Publisher extends createDomainObject<Publisher>({
  domainKey: PUBLISHER,
}) {
  region: string;
  // this is only for testing purposes, of course in an actual db
  // it makes no sense for a publisher to belong to a book
  book: BelongsTo<Book>;
}

Single Table Inheritance

// Domain Keys
const PLAYER = "player";
const FOOTBALLER = "footballer";

// Table
const PlayerTable = createTable({
  tableName: "players",
  columns: {
    id: {
      type: "serial",
      options: {
        primaryKey: true,
      },
    },
    name: {
      type: "text",
      options: {
        nullable: false,
      },
    },
    club: {
      // footballer
      type: "text",
    },
    battingAverage: {
      // cricketer, bowler
      type: "numeric",
    },
    bowlingAverage: {
      // bowler
      type: "numeric",
    },
  },
  singleTableInheritance: true,
});

// Data Mapper
const PlayerMapper = createMapper({
  domainKey: PLAYER,
  Table: PlayerTable,
  customInheritanceOptions: {
    variant: "singleTable",
    ParentMapper: null,
  },
  customColumnMap: {
    name: "name",
  },
});

const FootballerMapper = createMapper({
  domainKey: FOOTBALLER,
  Table: PersonTable,
  customInheritanceOptions: {
    variant: "singleTable",
    ParentMapper: PlayerMapper,
  },
  customColumnMap: {
    club: "club",
  },
});

// Models
class Player extends createDomainObject({ domainKey: PLAYER }) {
  name: string;
}

class Footballer extends extendDomainObject<Footballer>()({
  domainKey: FOOTBALLER,
  ParentDomainObject: Player,
}) {
  club: string;
}

// registry
registry.register(PLAYER, PlayerTable, Player, PlayerMapper);
registry.register(FOOTBALLER, PlayerTable, Footballer, FootballerMapper, true);

Documentation

Notation adopted (outside of Typescript code blocks):

  • a: b means that the variable name is a and its type is b.
    • a: b, c means that the variable type is b or c
  • a means that the variable name is a and its type is a Javascript object.
  • [a] means, in the context of a key of a Javascript object, that it is a key with any name.

Column options

Column TypeDescriptionOptions
varcharA varying length string possibly with length restrictions.limit maximum number of characters allowed in a string
textA varying length string without length restrictions.None
intInteger.primaryKey: boolean whether column serves as primary key of tableforeignKey: boolean whether column is a foreign key to another tablereferences foreign key options variant: "small", "regular", "big" Size of int stored in PostgreSQL.
serialInteger with unique index on column.all options of int autoGenerateExclusively whether database allows you to manually set ids for example when inserting rows into a table
uuidUUID.version: "v1", "v2", "v3", "v4" Version of uuid.
numericPrecise floating point type.None
boolBoolean.None
timestampTimestamp/Timestamptz.timezone: boolean true for timestamptz, false for timestamp without timezone

Create Table options

Format: createTable(options)

Arguments:

options A Javascript object taking the following keys

  • tableName: string Snakecased or camelcased name of db table
  • columns A Javascript object with keys of the form [columnName]: columnOptions

Create Data Mapper options

Format: createMapper(options)

Arguments:

options A Javascript object taking the following keys

  • domainKey: string Domain Key associated with the data mapper
  • Table: typeof Table Table associated with the data mapper
  • customColumnMap A Javascript object of the form
    {
      [tableColumnKey]: {
        fieldConversionFunction: obj => value;
      }
    }
  • customObjectMap A Javascript object of the form
    {
      domainObjectFields: {
        [domainFieldName]: {
          [domainSubfieldName]: {
            tableColumns: [col1, col2],
            columnConversionFunction: ([col1, col2]) => value;
        }
      }
    }
    Currently, customObjectMap only supports 1 level of nesting.
  • belongsTo A Javascript object of the form
    {
      [tableColumnKey]: {
        foreignKey: string;
        otherDomainKey: string;
      }
    }
  • hasOne See belongsTo.
  • hasMany See belongsTo.
  • customInheritanceOptions Currently, there is only the option to choose Single Table Inheritance.

Create Domain Object options

Format: createDomainObject<T>(options)

  • T: Classname of domain object.

Arguments:

options A Javascript object taking the following keys

  • domainKey: string Domain key associated with the domain object.

Extend Domain Object options

Format: extendDomainObject<T>()(options)

  • T: Classname of domain object

Note: The double parentheses here is not a typo. Function currying is needed here due to Typescript limitations.

Arguments:

options A Javascript object taking the following keys

  • domainKey: string Domain key associated with the domain object.
  • ParentDomainObject: typeof DomainObject The DomainObject class that the current domain object inherits from.
0.2.1

3 years ago

0.2.0

3 years ago

0.1.0

3 years ago

0.0.1

3 years ago