npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@simpleworkjs/orm

v0.2.8

Published

Base model-first ORM with multi-backend adapters (SQL, Redis, LDAP) for SimpleWorkJS.

Downloads

5,146

Readme

@simpleworkjs/orm

Base model-first ORM for SimpleWorkJS. Supports multiple backends: Sequelize/SQL, Redis, and LDAP.

Install

npm install @simpleworkjs/orm

Usage

const {init, Model} = require('@simpleworkjs/orm');

class Task extends Model {
  static fields = {
    id: {type: 'uuid', primaryKey: true},
    title: {type: 'string', isRequired: true},
    done: {type: 'boolean', default: false},
  };
}

(async function() {
  const models = await init({
    conf: {
      orm: {
        dialect: 'sqlite',
        storage: 'data.sqlite',
        logging: false,
      },
    },
    models: [Task],
  });

  const task = await models.Task.create({title: 'My first task'});
  console.log(task.toJSON());
})();

Field types

| Type | Description | Options | |------|-------------|---------| | string | VARCHAR string | min, max length | | text | Unlimited text | — | | int / integer | Integer | min, max | | float | Floating point | — | | boolean | Boolean | default | | json | JSON object | — | | date | Date/time | — | | uuid | UUID, defaults to UUIDV4 | primaryKey | | email | String with email validation | — | | password-bcrypt | String hashed with bcrypt, private | saltRounds | | hasOne | Foreign-key relationship | model, remoteKey, isRequired | | hasMany | Reverse relationship | model, remoteKey | | belongsToMany | Many-to-many through join model | model, through, foreignKey, otherKey |

Common field options

All field types accept:

  • isRequired — maps to allowNull: false.
  • default — default value for the column.
  • primaryKey — marks the field as the primary key.
  • unique — adds a unique constraint.
  • isPrivate — hidden from toJSON().
  • display — UI hints such as searchable, titleField, name.
  • form — form rendering hints.
  • validate — custom Sequelize validators.

Example model

class Task extends Model {
  static fields = {
    id: {type: 'uuid', primaryKey: true},
    title: {type: 'string', isRequired: true, max: 200, display: {searchable: true}},
    description: {type: 'text'},
    done: {type: 'boolean', default: false},
    createdBy: {type: 'hasOne', model: 'User'},
  };

  static display = {
    name: 'Task',
    titleField: 'title',
  };
}

Relationships

A hasOne field creates a foreign-key column on the model. For example, createdBy: {type: 'hasOne', model: 'User'} creates a createdById UUID column. The referenced model must be loaded into the same ORM instance.

A hasMany field is used for the reverse side of a relationship and is handled by the Sequelize adapter during associateModels().

Multi-backend

Set static adapterName on a model to choose its backend:

class CacheEntry extends Model {
  static adapterName = 'redis';
  static fields = { /* ... */ };
}

class DirectoryUser extends Model {
  static adapterName = 'ldap';
  static fields = { /* ... */ };
}

Adapters are created automatically when a model requests them.

Adapters

  • sequelize — default. Supports SQLite, Postgres, MySQL, etc.

    conf: {
      orm: {
        dialect: 'sqlite',
        storage: 'data.sqlite',
        logging: false,
      },
    }
  • redis — uses model-redis.

    conf: {
      orm: {
        redis: { /* model-redis options */ },
      },
    }
  • ldap — uses ldapts.

    conf: {
      orm: {
        ldap: { url: 'ldap://localhost', bindDN: '...', bindPassword: '...', userBase: '...' },
      },
    }

Model API

Models are classes that extend @simpleworkjs/orm. After init() resolves, each model class has static CRUD methods:

const task = await models.Task.create({title: 'New task'});
const list = await models.Task.list({where: {done: false}});
const one = await models.Task.get(task.id);
await one.update({done: true});
await one.delete();

Instance data is accessed as normal properties. toJSON() returns a plain object without private fields.

Hooks / lifecycle callbacks

Models can register hooks that run at various lifecycle points:

class Task extends Model {
  static fields = { /* ... */ };
}

// Hook registration (all return the Model for chaining)
Task.beforeCreate((data, options) => { /* modify data before insert */ });
Task.afterCreate((instance, options) => { /* e.g. send notification */ });
Task.beforeUpdate((data, options, instance) => { /* modify data before update */ });
Task.afterUpdate((instance, options) => { /* e.g. invalidate cache */ });
Task.beforeDestroy((instance, options) => { /* e.g. check dependencies */ });
Task.afterDestroy((instance, options) => { /* e.g. cleanup related records */ });
Task.beforeValidate((data, options, instance) => { /* pre-validation transform */ });
Task.afterValidate((data, options, instance) => { /* post-validation check */ });
Task.beforeSave((data, options, type, instance) => { /* runs before create or update */ });
Task.afterSave((instance, options, type) => { /* runs after create or update */ });

// Alternative: addHook method
Task.addHook('beforeCreate', fn);

Hooks fire in order — multiple hooks of the same type all execute in registration order.

Validation

Field-level and model-level validators run automatically before create() and update():

class Task extends Model {
  static fields = {
    title: {
      type: 'string',
      validate: {
        custom: (value) => {
          if (value.length < 3) throw new Error('title must be at least 3 chars');
        },
      },
    },
  };
}

// Model-level validator (e.g. cross-field validation)
Task.addValidator('dateOrder', (data) => {
  if (new Date(data.start) > new Date(data.end)) {
    throw new Error('start date must be before end date');
  }
});

// Async validators are supported (e.g. uniqueness check)
Task.addValidator('uniqueName', async (data) => {
  const existing = await Task.list({where: {name: data.name}});
  if (existing.length > 0) throw new Error('name already exists');
});

Validation errors throw an Error with a validationErrors array containing {field, validator, message} objects.

Soft-delete (paranoid mode)

Models can enable paranoid mode to soft-delete records instead of removing them:

class Task extends Model {
  static fields = {
    id: {type: 'uuid', primaryKey: true},
    name: {type: 'string'},
    is_deleted: {type: 'boolean', default: false},
  };
  static paranoid = true;        // Enable soft-delete
  static deletedField = 'is_deleted';  // Default field name
}

// Soft-delete a record (marks is_deleted = true)
await task.delete();  // or task.softDelete()

// Hard-delete (permanently remove)
await task.delete({force: true});

// Restore a soft-deleted record
await task.restore();

// list() and get() automatically filter out deleted records
const tasks = await Task.list();  // excludes deleted
const task = await Task.get(id);  // null if deleted

// Include deleted records
const task = await Task.get(id, {includeDeleted: true});
const all = await Task.list({where: {is_deleted: {ne: true}}});  // explicit filter

Query operators

The where clause supports operators for advanced filtering:

// Greater than / less than
Model.list({where: {value: {gt: 10}}});   // value > 10
Model.list({where: {value: {gte: 10}}});  // value >= 10
Model.list({where: {value: {lt: 10}}});   // value < 10
Model.list({where: {value: {lte: 10}}});  // value <= 10

// Not equal
Model.list({where: {status: {ne: 'deleted'}}});

// IN array
Model.list({where: {status: {in: ['active', 'pending']}}});

// LIKE pattern (SQL only)
Model.list({where: {name: {like: '%Doe%'}}});

// Combined operators
Model.list({
  where: {
    value: {gte: 10},
    status: 'active',
  },
});

Transactions

Transactions are supported for the Sequelize adapter:

// Auto-managed transaction (commits on success, rolls back on error)
await orm.transaction(async ({transaction}) => {
  const parent = await ParentModel.create({name: 'parent'}, {transaction});
  await ChildModel.create({parentId: parent.id, name: 'child'}, {transaction});
});

// Manual transaction control
const t = await orm.transaction();
try {
  await Model.create(data, {transaction: t});
  await t.commit();
} catch (e) {
  await t.rollback();
  throw e;
}

belongsToMany (many-to-many) relationships

Many-to-many relationships through a join model:

// Join model
class UserRole extends Model {
  static fields = {
    id: {type: 'uuid', primaryKey: true},
    userId: {type: 'uuid'},
    roleId: {type: 'uuid'},
  };
}

// Models with belongsToMany
class User extends Model {
  static fields = {
    id: {type: 'uuid', primaryKey: true},
    roles: {type: 'belongsToMany', model: 'Role', through: 'UserRole'},
  };
}

class Role extends Model {
  static fields = {
    id: {type: 'uuid', primaryKey: true},
    users: {type: 'belongsToMany', model: 'User', through: 'UserRole'},
  };
}

listDetail()

For Redis adapter parity, list() accepts {detail: true} to explicitly return full objects:

const results = await Model.list({detail: true});  // same as list() for SQL

Exposing custom methods

A model can declare static exposedMethods to mark its own instance or static methods for exposure (e.g. as REST endpoints by @simpleworkjs/backend). Model.getExposedMethods() validates and normalizes the declaration (auto-detecting instance vs. static, filling defaults, computing the route template), and Model.toPaths().methods surfaces them for discovery:

class Thread extends Model {
  static exposedMethods = [
    {method: 'inviteUser', route: 'invite', verb: 'post', args: {from: 'body', names: ['username', 'role']}},
    {method: 'search', verb: 'get', args: {from: 'query', names: ['q']}}, // static method
  ];
  async inviteUser(username, role) { /* ... */ }
  static async search(q) { /* ... */ }
}

See the @simpleworkjs/backend README for the full field reference and how they mount as routes.

Migrations

When used with @simpleworkjs/backend, the CLI can diff your model definitions against existing migration files and generate Sequelize-compatible migrations. See the backend CLI documentation for the migration workflow.

Tests

npm test

Related packages

  • @simpleworkjs/orm-identity — adds users, groups, roles, permissions, and token/session auth on top of this base ORM.
  • @simpleworkjs/backend — Express/Socket.IO framework that turns these models into a REST API, live-syncing UI, and CLI.

License

MIT