{"id":15764475,"url":"https://github.com/ceejbot/polyclay","last_synced_at":"2025-04-30T07:25:47.109Z","repository":{"id":3047745,"uuid":"4069068","full_name":"ceejbot/polyclay","owner":"ceejbot","description":"a schema-enforcing model class for node.js, with optional key-value store persistence","archived":false,"fork":false,"pushed_at":"2017-04-29T07:08:56.000Z","size":347,"stargazers_count":7,"open_issues_count":53,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-24T00:48:28.353Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ceejbot.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2012-04-18T22:22:51.000Z","updated_at":"2019-07-11T16:09:21.000Z","dependencies_parsed_at":"2022-09-13T19:21:51.124Z","dependency_job_id":null,"html_url":"https://github.com/ceejbot/polyclay","commit_stats":null,"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ceejbot%2Fpolyclay","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ceejbot%2Fpolyclay/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ceejbot%2Fpolyclay/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ceejbot%2Fpolyclay/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ceejbot","download_url":"https://codeload.github.com/ceejbot/polyclay/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246326595,"owners_count":20759436,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-04T12:03:47.966Z","updated_at":"2025-03-31T13:31:12.048Z","avatar_url":"https://github.com/ceejbot.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Polyclay\n\nPolymer modeling clay for node.js. A model schema definition with type validations, dirty-state tracking, and rollback. Models are optionally persistable to CouchDB using [cradle](https://github.com/cloudhead/cradle), [Redis](http://redis.io/), [LevelUP](https://github.com/rvagg/node-levelup), and Cassandra. Polyclay gives you the safety of type-enforcing properties without making you write a lot of boilerplate.\n\n[![on npm](http://img.shields.io/npm/v/polyclay.svg?style=flat)](https://www.npmjs.org/package/polyclay)  [![Tests](http://img.shields.io/travis/ceejbot/polyclay.svg?style=flat)](http://travis-ci.org/ceejbot/polyclay) ![Coverage](http://img.shields.io/badge/coverage-83%25-yellow.svg?style=flat) [![Dependencies](http://img.shields.io/david/ceejbot/polyclay.svg?style=flat)](https://david-dm.org/ceejbot/polyclay)\n\n## Installing\n\n`npm install polyclay`\n\n## Building a model\n\nPolyclay builds a model constructor function from options that you pass to its `buildClass` function, similar to the way Backbone builds constructors.\n\n```javascript\nvar MyModel = polyclay.model.buildClass(options);\n```\n\nValid options:\n\n`properties`\n: hash of named properties with types; see detailed discussion below\n\n`optional`\n: Array of string names of properties the model might have. Optional properties don't have types, but they do have convenient getters \u0026 setters defined for you. They are also persisted in CouchDB if they are present.\n\n`required`\n: Array of string names of properties that must be present. The model will not validate if required properties are missing.\n\n`enumerables`\n: enum; a property that is constrained to values in the given array of strings. The provided setter accepts either integer or string name values. The provided getter returns the string. The value persisted in the database is an int representing the position in the array.\n\n`methods`\n: Hash of methods to add to the object. You can instead decorate the returned constructor prototype with object methods.\n\n`initialize`\n: Function to call as the last step of the returned constructor. Provide an implementation to do any custom initialization for your model.`this` will be the newly constructed object.\n\n`singular`\n: The singular noun to use to describe your model. For example, \"comment\". Added to the model prototype as `Model.prototype.singular`.\n\n`plural`\n: The plural noun to use to describe your model; used by the persistence layer to name a database when appropriate. For example, \"comments\". Added to the model prototype as `Model.prototype.plural`.\n\n### Valid data types\n\nPolyclay properties must have types declared. Getters and setter functions will be defined for each that enforce the types. Supported types are:\n\n`string`\n: string; undefined and null are disallowed; default is empty string\n\n`array`\n: array; default is [] or new Array()\n\n`number`\n: any number; default is 0\n\n`boolean`\n: true/false; default is false\n\n`date`\n: attribute setter can take a date object, a milliseconds number, or a parseable date string; default is new Date()\n\n`hash`\n: object/hashmap/associative array/dictionary/choose your lingo; default is {}\n\n`reference`\n: pointer to another polyclay object; see documentation below; default is `null`\n\n### References\n\n*Reference* properties are pointers to other polyclay-persisted objects. When Polyclay builds a reference property, it provides two sets of getter/setters. First, it defines a `model.reference_id` property, which is a string property that tracks the `key` of the referred-to object. It also defines `model.reference()` and `model.set_reference()` functions, used to define the js property `model.reference`. This provides runtime-only access to the pointed-to object. Inflating it later is an exercise for the application using this model and the persistence layer.\n\nIn this example, widgets have an `owner` property that points to another object:\n\n```javascript\nvar Widget = polyclay.Model.buildClass({\n    properties:\n    {\n        _id: 'string', // couchdb key\n        name: 'string',\n        owner: 'reference'\n    },\n    singular: 'widget',\n    plural: 'widgets'\n});\npolyclay.persist(Widget);\n\nvar widget = new Widget();\nwidget.name = 'eludium phosdex';\nwidget.owner = marvin; // marvin is an object we have already from somewhere else\nassert(widget.owner_id === marvin.key);\nassert(widget.owner === marvin);\nwidget.save(function(err)\n{\n    var id = widget.key.\n    Widget.get(id, function(err, dbwidget)\n    {\n    \t// the version from the db will have the id saved,\n    \t// but not the full marvin object\n        assert(dbwidget.owner_id === marvin.key);\n        assert(dbwidget.owner === undefined);\n    });\n});\n```\n\n### Temporary fields\n\nYou can set any other fields on an object that you want for run-time purposes. polyclay prefixes all of its internal properties with `__` (double underscore) to avoid conflicts with typical field names.\n\n### Validating\n\nValidate an object by calling `valid()`. This method returns a boolean: true if valid, false if not. It tests if all typed properties contain valid data and if all required properties. If your model prototype defines a `validator()` method, this method will be called by `valid()`.\n\nIf an object has errors, an `errors` field will be set. This field is a hash. Keys are the names of invalid fields and values are textual descriptions of the problem with the field.\n\n`invalid data`: property value does not match the required type  \n`missing`: required property is missing\n\n\n### Methods added to model prototypes\n\n`obj.valid()`\n\nReturns true if all required properties are present, the values of all typed properties are acceptable, and `validator()` (if defined on the model) returns true.\n\n`obj.rollback()`\n\nRoll back the values of fields to the last stored value. (Probably could be better.)\n\n`obj.serialize()`\n\nSerialize the model as a hash. Includes optional properties.\n\n`obj.toJSON()`\n\nSerialize the model as a string by calling `JSON.stringify()`. Includes optional properties.\n\n`obj.clearDirty()`\n\nClears the dirty bit. The object cannot be rolled back after this is called. This is called by the persistence layer on a successful save.\n\n## Persisting in CouchDB, Redis, LevelUP, or Cassandra\n\nFour key/value store adapters exist for polyclay:\n\n- [polyclay-couch](https://github.com/ceejbot/polyclay-couch): CouchDB\n- [polyclay-redis](https://github.com/ceejbot/polyclay-redis): Redis\n- [polyclay-levelup](https://github.com/ceejbot/polyclay-levelup): LevelUP\n- [polyclay-cassandra](https://github.com/ceejbot/polyclay-cassandra): Cassandra\n\nDocumentation for those adapters will eventually move from here to those repos.\n\n\nOnce you've built a polyclay model, you can mix persistence methods into it:\n\n````javascript\npolyclay.persist(ModelFunction, '_id');\npolyclay.persist(RedisModelFunc, 'name');\n```\n\nYou can then set up its access to CouchDB by giving it an existing Cradle connection object plus the name of the database where this model should store its objects. The couch adapter wants two fields in its options hash: a cradle connection and a database name. For instance:\n\n```javascript\nvar adapterOptions =\n{\n\tconnection: new cradle.Connection(),\n\tdbname: 'widgets'\n};\nModelFunction.setStorage(adapterOptions, polyclay.CouchAdapter);\n```\n\nIf you do not pass a dbname, the adapter will fall back to using the model's `plural`. This is often the expected name for a database.\n\nEvery model instance has a pointer to the adapter on its `adapter` field. The adapter in turn gives you access to the cradle connection on `obj.adapter.connection` and the database on `obj.adapter.db`.\n\nFor the redis adapter, specify host \u0026 port of your redis server. The 'dbname' option is used to namespace keys. The redis adapter will store models in hash keys of the form `\u003cdbname\u003e:\u003ckey\u003e`. It will also use a set at key `\u003cdbname\u003e:ids` to track model ids.\n\n```javascript\nvar options =\n{\n\thost: 'localhost',\n\tport: 6379,\n\tdbname: 'widgets' // optional\n};\nModelFunction.setStorage(options, polyclay.RedisAdapter);\n```\n\nThe redis client is available at `obj.adapter.redis`. The db name falls back to the model plural if you don't include it. The dbname is used to namespace model keys.\n\nFor LevelUP:\n\n```javascript\nvar options =\n{\n\tdbpath: '/path/to/leveldb/dir',\n\tdbname: 'widgets' // optional\n};\nModelFunction.setStorage(options, polyclay.LevelupAdapter);\n```\n\nThe Levelup object is available at `obj.adapter.db`. The attachments data store is available at `obj.adapter.attachdb`.\n\n### Defining views\n\nYou can define views to be added to your couch databases when they are created.  Add a `design` field to your constructor function directly.\n\nLet's add some simple views to the Widget model we created above, one to fetch widgets by owner and one to fetch them by name.\n\n```javascript\nWidget.design =\n{\n\tviews:\n\t{\n\t\tby_owner: { map: \"function(doc) {\\n  emit(doc.owner_id, doc);\\n}\", language: \"javascript\" },\n\t\tby_name: { map: \"function(doc) {\\n  emit(doc.name, doc);\\n}\", language: \"javascript\" }\n\t}\n};\n```\n\nCall `Widget.provision()` to create the 'widgets' database in your CouchDB instance. It will have a design document named \"_design/widgets\" with the two views above defined. The provision method nothing for Redis- or LevelUP-backed models.\n\n### Persistence class methods\n\nAll functions return promises if a callback is not provided.\n\n`provision(function(err, couchResponse))`\n\nCreate the database the model expects to use in couch. Create any views for the db that are specified in the `design` field. Does nothing for Redis and LevelUP. Creates the keyspace and model tables/columnfamilies for Cassandra.\n\n`ModelFunction.get(id, function(err, object))`\n\nFetch an object from the database using the provided id.\n\n`ModelFunction.all(function(err, objectArray))`\n\nFetch all objects from the database. It's up to you not to shoot yourself in the foot with this one.\n\n`ModelFunction.constructMany(couchDocs, function(err, objectArray))`\n\nTakes a list of couch response documents produced by calls to couch views, and uses them to inflate objects. You will use this class method when writing wrappers for couch views. For a simple example, see class Comment's findByOwner() method below. (Not exercised by the other adapters.)\n\n`ModelFunction.destroyMany(idArray, function(err, response))`\n\nTakes a list of object ids to remove. Responds with err if any failed, and an array of responses from couch or redis.\n\n\n### Persistence instance methods\n\n`obj.save(function(err, response))`\n\nSave the model to the db. Works on new objects as well as updated objects that have already been persisted. If the object was not given a `key` property before the call, the property will be filled in with whatever couch chose. The LevelUP and Redis adapters both demand that you provide a key before calling save(). Does nothing if the object is not marked as dirty.\n\n`obj.destroy(function(err, wasDestroyed))`\n\nRemoved the object from couch and set its `destroyed` flag. The object must have a `key`.\n\n`obj.merge(hash, function(err, response))`\n\nUpdate the model with fields in the supplied hash, then save the result to the backing store.\n\n`obj.removeAttachment(name, function(err, wasRemoved))`\n\nRemove the named attachment. Responds with wasRemoved == true if the operation was successful.\n\n`obj.initFromStorage(hash)`\n\nInitialize a model from data returned by the backing store. You are unlikely to call this, but it's available.\n\n\n### Attachments\n\nYou can define attachments for your polyclay models and several convenience methods will be added to the prototype for you. Give your attachment a name and a mime type:\n\n`ModelFunc.defineAttachment(name, mimetype);`\n\nThe prototype will have `get_name` and `set_name` functions added to it, wrapped into the property *name*. Also, `fetch_name` will be defined to fetch the attachment data asynchronously from backing storage. Attachment data is saved when the model is saved, not when it is set using the property.\n\nYou can also save and remove attachments directly:\n\n`obj.saveAttachment(name, function(err, response))`  \n`obj.removeAttachment(name, function(err, response))`\n\nA simple example:\n\n```javascript\nModelFunc.defineAttachment('rendered', 'text/html');\nModelFunc.defineAttachment('avatar', 'image/jpeg');\n\nvar obj = new ModelFunc();\nobj.avatar = fs.readFileSync('avatar.jpg');\nconsole.log(obj.isDirty); // true\n\nobj.save(function(err, resp)\n{\n\t// attachment is now persisted in storage.\n\t// Also, obj's _rev has been updated.\n\tobj.avatar = null;\n\tobj.save(function(err, resp2)\n\t{\n\t\t// the avatar attachment has been removed\n\t\t// obj._rev has been updated again\n\t});\n});\n```\n\n## Events\n\nPolyclay uses [LucidJS](http://robertwhurst.github.io/LucidJS/) to emit events when interesting things happen to your model object.\n\n`change`: when any property has been changed  \n`change.\u003cprop\u003e`: after a specific property has been changed  \n`update`: after the objects's properties have been updated in `update()`  \n`rollback`: after the object has been rolled back to a previous state  \n`after-load`: after the object has been loaded from storage \u0026 a model instantiated  \n`before-save`: before the object is saved to storage in `save()`  \n`after-save`: after a save to storage has succeeded, before callback  \n`before-destroy`: before deleting the object from storage in `destroy()`  \n`after-destroy`: after deleting the object from storage in `destroy()`  \n\nHere's an example of a property change listener:\n\n```javascript\nvar obj = new Widget();\nobj.on('change.name', function(newval)\n{\n\tassert(obj.name === newval);\n\tconsole.log(newval); // logs 'planet x'\n});\n\nwidget.name = 'planet x';\n\n```\n\nSee the Lucid documentation for other things you can do with triggering and listening to events on your polyclay objects.\n\n\n## Mixins\n\nA bundle of fields and methods that you wish to add to several model classes while allowing Polyclay to reduce the boilerplate for you.\n\nHere's an example. It defines two date fields and a method that uses one:\n\n```javascript\nvar HasTimestamps =\n{\n\tproperties:\n\t{\n\t\tcreated: 'date',\n\t\tmodified: 'date'\n\t},\n\tmethods:\n\t{\n\t\ttouch: function() { this.modified = Date.now(); }\n\t},\n\tstatics:\n\t{\n\t\tcomparator: function comparator(l, r) { return l.created.getTime() - r.created.getTime(); },\n\t}\n};\n\npolyclay.mixin(ModelClass, HasTimestamps);\n```\n\nMixin objects have three fields.\n\n`properties`: A hash of property names \u0026 types, exactly as in a base model definition.  \n`methods`: A hash of method names \u0026 implementations to add to the model prototype.  \n`statics`: Functions to add to the model function directly; class methods.  \n`custom`: A hash of custom functions to add to the model prototype as getters \u0026 setters.\n\nHere's a simple example of a custom property:\n\n```javascript\nvar SillyNameMixin =\n{\n\tcustom:\n\t{\n\t\tname:\n\t\t{\n\t\t\t'getter': function() { return this._name; },\n\t\t\t'setter': function(v) { this._name = v.toLowerCase(); }\n\t\t}\n\t}\n}\n```\n\n## Example\n\nHere's an example taken verbatim from the project I wrote this module for:\n\n```javascript\nvar Comment = polyclay.Model.buildClass(\n{\n    properties:\n    {\n        _id: 'string', // couchdb key\n        version: 'number',\n        owner: 'reference',\n        owner_handle: 'string',\n        target: 'reference',\n        parent: 'reference',\n        title: 'string',\n        content: 'string',\n        editable: 'boolean'\n    },\n    enumerables:\n    {\n        state: ['visible', 'hidden', 'deleted', 'usergone']\n    },\n    optional: [ '_rev' ],\n    required: [ 'owner_id', 'target_id', 'parent_id', 'state'],\n    initialize: function()\n    {\n        this.created = Date.now();\n        this.modified = this.created;\n        this.editable = true;\n        this.version = 1;\n        this.state = 0;\n    },\n});\n\nComment.design =\n{\n\tviews:\n\t{\n\t\tby_target: { map: \"function(doc) {\\n  emit(doc.target_id, doc);\\n}\", language: \"javascript\" },\n\t\tby_owner: { map: \"function(doc) {\\n  emit(doc.owner_id, doc);\\n}\", language: \"javascript\" },\n\t\tby_owner_target: { map: \"function(doc) {\\n  emit(doc.owner_id + '|' + doc.target_id, doc);\\n}\", language: \"javascript\" },\n\t}\n};\n\nComment.findByOwner = function(owner, callback)\n{\n\tif (typeof owner === 'object')\n\t\towner = owner.key;\n\n\tComment.adapter.db.view('comments/by_owner', { key: owner }, function(err, documents)\n\t{\n\t\tif (err) return callback(err);\n\t\tComment.constructMany(documents, callback);\n\t});\n};\n\npolyclay.mixin(Comment, HasTimestamps); // as defined above\npolyclay.persist(Comment, '_id');\n\nvar opts =\n{\n\tconnection: new cradle.Connection(),\n\tdbname: 'comments'\n};\nComment.setStorage(opts, polyclay.CouchAdapter);\nComment.provision(function(err, response)\n{\n\t// database is now created \u0026 the views available to use\n});\n\n\nvar comment = new Comment();\nconsole.log(comment.state);\ncomment.version = \"foo\"; // throws an error\ncomment.version = 2; // sets the attribute\ncomment.state = 'yoinks'; // throws an error\ncomment.state = 'deleted';\nconsole.log(comment.state);\ncomment.state = 1;\nconsole.log(comment.state);\ncomment.touch();\nconsole.log(comment.isDirty()); // true\n\ncomment.rollback(); // version is now 1 and modified the same as created\ncomment.tempfield = 'whatever'; // not persisted in couch\n```\n\n## Contributors\n\nC J Silverio @ceejbot  \nKit Cambridge @kitcambridge  \n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fceejbot%2Fpolyclay","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fceejbot%2Fpolyclay","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fceejbot%2Fpolyclay/lists"}