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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@yadah/domain-model

v0.2.2

Published

Yadah domain class mixin to wrap Objection.js models

Downloads

4

Readme

Yadah Model mixin

A mixin for Yadah Service classes that adds methods for database access via an Objection.js model.

Basic usage

import ModelMixin, { Model } from "@yadah/service-model";
import { Service } from "@yadah/data-manager";
import { pipe } from "@yadah/mixin";

export class MyModel extends Model {
  static tableName = "myModelTable";

  static scopes() {
    return {
      ...super.scopes,
      field: (query, { field }) => query.where("field", field);
    };
  }
}

const mixins = pipe(Service, x => ModelMixin(x, MyModel));

export class MyService extends mixins {
  // business logic
}
// find functions
services.MyService.find(); // find one record by id
services.MyService.list(); // listing records
services.MyService.one(); // find one record by criteria
services.MyService.count(); // count records
services.MyService.sum(); // aggregate total
services.MyService.min(); // aggregate minimum
services.MyService.max(); // aggregate maximum
services.MyService.avg(); // aggregate average

// mutating functions
services.MyService.create();
services.MyService.update();
services.MyService.delete();
services.MyService.upsert();
services.MyService.copyFrom();

The Model class exported from @yadah/service-model/Model.js has the following Objection.js mixins:

  • @yadah/objection-iterator/IteratorMixin.js
  • @yadah/objection-scope/ScopeMixin.js
  • @yadah/service-model/NotUniqueMixin.js

These mixins are required for any Model passed to ModelMixin.

Find functions

ModelMixin adds the following functions to the Service class.

find(id)

find accepts either an integer ID to find, or an object that can be used in a .where() to identify a single record, or an instance of the model.

Throws a NotFoundError if no record is found. May be disabled by passing allowNotFound: true in the options.

Throws a NotUniqueError if a criteria object is used and more than one record is returned.

Returns a promise that resolves with the instance of the model that is found. If an instance of the model was passed as the id parameter the instance will be returned.

await MyService.find(123); // returns a MyModel instance
await MyService.find({ field: "bar" }); // returns a MyModel instance

node = await MyService.find(123);
found = await MyService.find(node);
node === found; // true

list(criteria)

Accepts a Model.scope() criteria object.

Returns a QueryBuilder instance that resolves with an array of records matching the scope.

await MyService.list({ field: "baz" }); // returns array of MyModel instances

// streaming list results
for await (const myModel of MyService.list({ field: "baz" })) {
  // myModel is an instance of MyModel
}

one(criteria)

Accepts a .scope() criteria object. The criteria should identify a unique record.

Returns a QueryBuilder instance that resolves with the record matching the scope or null if no record is found.

await MyService.one({ field: "bar" }); // returns a MyModel instance

many(items, callback)

Creates a union query. A typical use case is to combine the results of multiple criteria searches into a single consistent list.

Returns a QueryBuilder instance that resolves with an array of records.

await MyService.many([{ field: "bar" }, { field: "baz" }], (query, criteria) =>
  query.scope(criteria)
); // returns array of MyModel instances

count(criteria)

Accepts a .scope() criteria object.

Returns the number of records that match the criteria.

await MyService.count({ field: "baz" });

sum(field, criteria)

Accepts a .scope() criteria object and a field name to sum.

Returns a number containing the sum or null if there no were records that matched the criteria.

await MyService.sum("qty", { field: "baz" });

min(field, criteria)

Accepts a field name to find the minimum of and a .scope() criteria object.

Returns the minimum value in the specified field for all records that match the criteria.

await MyService.min("qty", { field: "baz" });

max(field, criteria)

Accepts a field name to find the maximum of and a .scope() criteria object.

Returns the maximum value in the specified field for all records that match the criteria.

await MyService.max("qty", { field: "baz" });

Mutating functions

create(json, onCreate)

Creates a new database record.

Returns an instance of the Model containing the newly created record.

Emits created event containing the newly created record.

await MyService.create({ foo: "bar", bar: "baz" }); // returns a MyModel instance

MyService.on("created", (after, before) => {
  after instanceof MyModel; // true
  before === undefined; // true
});

onCreate(model, transaction)

The onCreate() hook is executed after a newly created record is inserted. The first argument is an instance of the Model containing the newly created record.

A typical use-case for hooks is to overload the .create() function to provide additional logic for parts of a record that aren't simply inserted into the Model's database table.

function create(json) {
  const { localPart, ...rest } = json;
  const onCreate = async (model, transaction) => {
    await model.$relatedQuery("localPart", transaction).insert(localPart);
  };
  return super.create(rest, onCreate);
}

onCreate() returns void.

update(id, json, onUpdate)

Updates a database record.

The first argument is a .find() identifier to identify the record to update. A NotFoundError can be thrown if the record is not found.

The second argument is an object containing fields to update.

Returns an instance of the Model containing the updated data.

Emits updated event containing the updated record and the record prior to the update.

await MyService.update(123, { bar: "..." }); // return a MyModel instance

MyService.on("updated", (after, before) => {
  after instanceof MyModel; // true
  before instanceof MyModel; // true
});

onUpdate([after, before], transaction)

The onUpdate() hook is executed after a record is patched. The first argument is an tuple containing instances of the Model after the update and before the update.

A typical use-case for hooks is to overload the .update() function to provide additional logic for parts of a record that aren't simply inserted into the Model's database table.

function update(id, json) {
  const { localPart, ...rest } = json;
  const onUpdate = async ([after, before], transaction) => {
    await after.$relatedQuery("localPart", transaction).patch(localPart);
  };
  return super.update(id, rest, onUpdate);
}

The .update() function will emit the updated event only when it has executed a patch query. The onUpdate() hook may override this behaviour by returning true to indicate that there has been a change.

The onUpdate() hook may alternatively return an array with shape [after, before] containing instances of the Model to be emitted. If these instances are equal (ie. reference the same object) no updated event is emitted.

delete(id, callback)

Deletes a database record.

Returns an instance of the Model containing the deleted record.

Emits deleted event containing the deleted record.

await MyService.delete(123); // returns a MyModel instance

MyService.on("deleted", (after, before) => {
  after === undefined; // true
  before instanceof MyModel; // true
});

onDelete(model, transaction)

The onDelete() hook is executed after a record has been deleted. The first argument is the instance of the Model that was deleted.

A typical use-case for hooks is to overload the .delete() function to provide additional logic for parts of a record that aren't in the Model's database table.

function delete(id) {
  const onDelete = async (model, transaction) => {
    await model.$relatedQuery("localPart", transaction).delete();
  };
  return super.delete(id, onDelete);
}

A return value is optional. If onDelete() returns a "truthy" value it will be emitted as the before value of the deleted event, and returned as the value of the delete() function.

upsert(key, json)

Creates or updates a database record.

The first argument is a .where() criteria object that uniquely identifies a record to find.

The second argument contains data to set.

If a record is found it will be updated using .update(), otherwise a new record will be created using .create().

When creating a record the criteria and data are merged; it's not necessary to duplicate the criteria object fields in the data object.

Returns an instance of Model that was created or updated.

Emits created or updated events (depending on whether .create() or .update() was used).

await MyService.upsert({ foo: "bar" }, { bar: "..." });

copyFrom(data)

Perform a streaming import of records.

data is any async iterable-like object (arrays, object streams, generator output etc). Each yielded object is inserted into the model's database table (via COPY FROM)

Returns a count of the number of records inserted.

await App.services.MyService.copyFrom([{ foo: "bar", bar: "baz" }]);
// 1

Note: no events are emitted.

Transactions

All functions will attempt to use the current context's transaction.

The mutating functions wrap all logic in a transaction.

To run queries in a transaction wrap them in a call to the transaction() method.

function action(args) {
  return this.transaction(async () => {
    // perform logic for action (.one() uses the transaction from context)
    const model = await this.one(args.scope);

    // get the transaction from context to pass to Objection/Knex functions
    const trx = this.transactionOrKnex;
    await model.$relatedQuery("localPart", trx);
  });
}

To run a query outside the transaction, create a new context and set the transaction to null

function action(args) {
  return this.transaction(async () => {
    // uses the transaction
    const model = await this.one(args.scope);

    await this.context(() => {
      this.transaction = null;
      // this delete is performed _outside_ the transaction
      await this.delete(model);
    })

    // uses the transaction again; (will probably be a conflict? TODO: what's a good example?)
    await this.create(model);
  })
}