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

polymod

v4.2.0

Published

A library for composing data models from any number of sources.

Downloads

18

Readme

Polymod

Travis CI Known Vulnerabilities npm

A library for composing data models from any number of sources. Inspired by GraphQL and Falcor.

Install

npm install --save polymod

License

MIT License

Documentation

Defining a model

Interacting with a model


Introduction

Polymod is a Node.js library for composing application data models. Unlike other data modeling libraries, such as Mongoose or Sequelize, Polymod is agnostic and is designed to interface with any data source so long as it conforms to a simple source interface. Additionally, each source in a Polymod model can come from a different data source. For instance, an application may need to pull data from a Postgres database as well as related session information from a Redis store. Similarly, an application may be an interface between two or web services. A Polymod model could consume resources from multiple RESTful APIs.

Models

A Model in Poly defines the data sources, queries, mutations, and data structure for a data model.

const { Model, Query } = require('polymod')

const OrderDetail = Model
	.create()

	// Add sources
	.addSource('order', new MemSource(store, 'orders'))
	.addSource('customer', new MemSource(store, 'customers'))
	.addSource('products', new MemSource(store, 'products'))
	
	// Add the default query
	.addQuery('default', orderQuery)

	// Add a mutation
	.addMutation('ship', shipOrder)

	// Describe the data structure
	.describe({
		shipped: {
			type: Boolean,
			required: true,
			default: () => false,
			data: ({ order }) => order.shipped
		},
		date: {
			type: {
				created: Date,
				payed: Date
			},
			default: () => ({ created: new Date() }),
			data: ({ order }) => ({
				created: order.dateCreated,
				payed: order.datePayed
			})
		},
		customer: {
			type: { name: String, address: String },
			required: true,
			data: ({ customer }) => ({
				name: customer.name,
				address: customer.address
			})
		},
		products: {
			type: [{ title: String, price: Number }],
			required: true,
			data: ({ products }) => products.map(product => ({
				title: product.title,
				price: product.price
			}))
		},
		total: {
			type: Number,
			data: ({ products }) => products.reduce((total, product) => {
				return total + product.price
			}, 0)
		}
	})

In this example, the model, OrderDetail defines data from three in-memory sources: orders, customers, and products. A default query is added to fetch the data from the sources (more on queries below), a mutation is added to ship an order (more on mutations below), and the data structure is defined with the following properties: shipped, date, customer, products, and total. Each of these properties defines a data function, which is used to transform the source data to the final document property. Three other attributes are defined on some of the data structure properties:

  • type: Defines the schema type of the property
  • default: A function to set the default value of a property
  • required: Whether the property is required or not

All of these properties, including data are optional. If data is not defined, then the property is considered write-only.

Sources

Sources are the interface between the model and the data sources. Polymod ships with a single source, MemSource, which interfaces with the in-memory storage provided by MemStore. Additionally, every model created with Polymod also implements the source interface, allowing models to also be used as sources for other models.

Creating a new source for use with Polymod is fairly straight forward. Sources are objects, which implement two methods: fetch and mutate.

The fetch method takes two parameters:

  • operation: The source operation to perform (i.e. 'read')
  • selector: The selector used to fetch the data

The mutate method takes a single parameter:

  • operations: An array of operations to be performed on the source. See mutations for more information.

Queries

Polymod queries are the interface for fetching a model's data from its sources. These are defined using the Query class. Every model must have at least one query, the 'default' query.

const orderQuery = Query
	.create()
	.addPopulation({
		name: 'order',
		operation: 'read',
		selector: ({ input }) => ({ id: input })
	})
	.addPopulation({
		name: 'customer',
		operation: 'read',
		requires: ['order'],
		selector: ({ order }) => ({ id: order.customer })
	})
	.addPopulation({
		name: 'products',
		operation: 'read',
		requires: ['order'],
		selector: ({ order }) => order.products.map(product => ({ id: product }))
	})

Queries define populations, which instruct the model on how to fetch data from a the source. A population is an object with the following properties:

  • name: The source name
  • operation: The source operation to use
  • requires: The populations which must be complete before this population
  • selector: A function that takes any available input and source data and returns a selector for the source

Mutations

By default Polymod sources are immutable. In order to allow source data to be mutated, mutations need to be defined by the model. Mutations are defined as an array of operations by source.

const shipOrder = [
	{
		source: 'post',
		operations: (input, { post }) => ([
			{
				name: 'update',
				selector: { id: post.id },
				data: { shipped: true }
			}
		]),
		results: ([ post ]) => post
	}
]

The mutation array should contain objects with the following properties:

  • source: The source being mutated
  • operations: A function returning an array of operations
  • results: A function that returns data from the operations

The operations function takes the mutation input data, and object containing the existing source data. The function should return an array of operations, which is an object containing the operation name, the mutation selector (optional), and the data to be mutated (also optional).

The results function is passed an array with each of the operation results as an element in the array.

get()

Every model has a get method, which executues the default query with the given input. The returned value should either be a Document or an array of Documents. The data for a document can be retrieved using the data property.

const doc = await OrderDetail.get(1)

console.log(doc.data)
/*
{
	shipped: false,
	date: {
		created: '2017-01-01',
		payed: null
	},
	customer: {
		name: { first: 'John', last: 'Smith' },
		adddress: {
			street: '300 BOYLSTON AVE E',
			city: 'SEATTLE',
			state: 'WA',
			zip: 98012
		}
	},
	products: [
		{
			title: 'You Don\'t Know JS: Up & Going',
			price: 4.99
		},
		{
			title: 'JavaScript: The Good Parts',
			price: 21.93
		}
	],
	total: 26.92
}
*/

query()

The query method executes a named query with the given input. For example, the default query could also be executed as and the result is the same:

const doc = await OrderDetail.query('default', 1)

mutate()

The mutate method allows access to the model's mutations and can be called in one of two ways:

  • mutate(name, data): This will execute the mutation with name name
  • mutate(dataObject): This will treat each property in dataObject as a mutation. In order to do this, the property name must be a defined mutation.
const [ newDoc, error ] = await doc.mutate('ship')

In either instance, the returned value of the mutate method is an array with two elements: the new document, and an error if the mutation failed for some reason. If the mutation was successful, error will be undefined. However, if there was an error, newDoc will be null.

create()

The create method, as its name suggests, is used to create new model documents. However, in order to create documents, an initializer needs to be defined for the model. This is done using the setInitializer model method.

OrderDetail.setInitializer([
	{
		source: 'order',
		operations: input => ([
			{
				name: 'create'
			}
		]),
		results: ([ order ]) => order
	}
], {
	customer: Number,
	products: [Number]
})

The initializer is simply a special mutation that is called to mutate the data sources as needed. The second, and optional argument, is the type schema for input data. If the model has a descriptor, the properties defined here, will override any types in the descriptor.

const [ doc, error ] = await OrderDetail.create({
	customer: 2,
	products: [1]
})

remove()

remove is a method of a Document instance, and like the create method, a special mutation needs to be defined before documents can be removed. This is achieved using the setRemove method.

OrderDetail.setRemove([
	{
		source: 'order',
		operations: ({ input }) => ([
			{
				name: 'remove',
				selector: { id: input }
			}
		])
	}
])

The returned value from the remove method is and object with properties for each mutated source, and values containing the operation results.

const removed = await doc.remove()

describe()

When calling the describe method without any parameters, the model will return the defined data descriptor types.

console.log(OrderDetail.describe())

/*
{
	shipped: {
		type: Boolean,
		required: true
	},
	date: {
		type: {
			created: Date,
			payed: Date
		}
	},
	customer: {
		type: { name: String, address: String },
		required: true
	},
	products: {
		type: [{ title: String, price: Number }],
		required: true
	},
	total: {
		type: Number
	}
}
*/