parsimonious

4.5.1 • Public • Published

buildstatus codecov

Utilities for Parse Server cloud code and JS SDK

Usage example: creating and saving parse objects

const parsm = require('parsimonious')
 
// Create an instance of 'Course' with a name attribute:
// Also creates the class if needed.
 
const course = await parsm.getClassInst('Course', {
  name: 'Sociology 201'
}).save()
 
// Create another with two attributes:
const student = await parsm.getClassInst('Student', {
  name: 'Maria',
  class: 2020
}).save()

Usage example: create many-to-many relationships with metadata

// Create a many-to-many relationship between students and courses,
// and record the fact that a student completed a course,
// with date of completion and grade earned:
 
const meta = {completed: new Date(2017, 11, 17), grade: 3.2}
 
const opts = {sessionToken: 'r:cbd9ac93162d0ba1287970fb85d8d168'}
 
const joinObj = await parsm.joinWithTable(student, course, meta, opts)
 
// joinObj is now an instance of the class 'Student2Course',
// which was created if it didn't exist.
// The Student2Course class has pointer columns 'student' and 'course',
// plus a date column named 'completed' and a numeric column named 'grade'.

Usage example: search many-to-many relationships

 
// Find the top 10 students who have taken a particular course
// and earned a grade of at least 3:
 
const classes = {
   Student: null,
   Course: course
}
 
const criteria = {
  descending: 'grade',
  greaterThanOrEqualTo: ['grade', 3],
  limit: 10,
  include: 'student'
}
 
const joinObjs = await parsm.getJoinQuery(classes, criteria).find()
 
// joinObjs is now an array of instances of the class 'Student2Course'
// with details of students in the 'student' column.

Override the Parse instance used:

// Initialize Parse JS SDK first:
Parse.initialize('myAppId')
Parse.masterKey = 'myMasterKey'
 
// Initialize parsimonious with the initialized Parse instance:
const parsm = require('parsimonious')
parsm.setParse(Parse)

Change Log

Parsimonious

Kind: global class

Utilities for Parse Server cloud code and JS SDK.

Parsimonious.setParse(parse)

Set the instance of the Parse JS SDK to be used by all methods:

Kind: static method of Parsimonious
Params

  • parse object - instance of the Parse JS SDK

Parsimonious.getClass(className) ⇒

Short-hand for Parse.Object.extend(className) or a special class like Parse.User

Kind: static method of Parsimonious
Returns: subclass of Parse.Object
Params

  • className string

Parsimonious.getClassInst(className, [attributes], [options]) ⇒ Parse.Object

Return instance of Parse.Object class.

Kind: static method of Parsimonious
Params

  • className string - Parse.Object subclass name.
  • [attributes] object - Properties to set on new object.
  • [options] object - Options to use when creating object.

Parsimonious.newQuery(aClass, [constraints]) ⇒ Parse.Query

Returns a new Parse.Query instance from a Parse Object class name.

Kind: static method of Parsimonious
Params

  • aClass Parse.Object | string - Parse class instance or name
  • [constraints] object - Plain object whose keys may be any Parse.Query constraint methods and whose values are arrays of arguments for those methods.

Example

// Generate a new Parse.Query on the User class,
 
const query = Parsimonious.newQuery('User')
 
// which is equivalent to:
 
const query = new Parse.Query(Parse.User)

Example

// Generate a new Parse.Query on a custom class,
 
const query = Parsimonious.newQuery('Company')
 
// which is equivalent to:
 
const Company = Parse.Object.extend('Company')
const query = new Parse.Query(Company)

Example

// Generate a new Parse.Query on the User class, adding constraints 'startsWith,' 'limit,' and 'select.' (See Parsimonious.constrainQuery for constraints parameter details.)
 
const query = Parsimonious.newQuery('Company', {
  startsWith: ['name', 'tar'],
  limit: 10, // If there is only one argument, does not need to be in an array
  select: [ ['name', 'address', 'url'] ] // If any argument for a constraint is an array, it must be passed to constrainQuery within another array to indicate that its array items are not individual arguments.
})
 
// which is equivalent to:
 
const Company = Parse.Object.extend('Company')
const query = new Parse.Query(Company)
query.startsWith('name', 'tar')
query.limit(10)
query.select('name')
query.select('address')
query.select('url')

Parsimonious.constrainQuery(query, constraints) ⇒ Parse.Query

Calls one or more query constraint methods on a query with arbitrary number of arguments for each method. This method is useful when, for example, building a complex query configuration to pass to another function that may modify the configuration further and then generate the actual query. Mutates the 'query' parameter because it calls constraint methods on it. Returns the query, so you can chain this call.

Kind: static method of Parsimonious
Params

  • query Parse.Query - The query on which to call the constraint methods
  • constraints object - Plain object containing query constraint methods as keys and arguments as values

Example

// Modify a query with startsWith, limit, select, equalTo, and notEqualTo constraints:
 
const query = Parsimonious.newQuery('User')
const constraints = {
  startsWith: ['name', 'Sal'],
  limit: 10, // If there is only one argument, does not need to be in an array
  select: [ ['name', 'email', 'birthDate'] ], // If a single constraint argument is an array, it must be within another array to indicate that its items are not individual arguments.
  equalTo: [ ['gender', 'f'], ['country', 'US'] ], // An array of 2 or more arrays indicates that the constraint method should be called once with each inner array as its arguments.
  notEqualTo: ['company', 'IBM'] // There is just one set of parameters, so there is no need to enclose in another array.
}
Parsimonious.constrainQuery(query, constraints)
 
// which is equivalent to:
 
const query = new Parse.Query(Parse.User)
query.startsWith('name', 'Sal')
query.limit(10)
query.select('name')
query.select('email')
query.select('birthDate')

Parsimonious.getObjById(aClass, id, [opts])

Return a Parse.Object instance from className and id.

Kind: static method of Parsimonious
Params

  • aClass string | object - class name or constructor
  • id string
  • [opts] object - A Backbone-style options object for Parse subclass methods that read/write to database. (See Parse.Query.find).

Parsimonious.getUserById(id, [opts]) ⇒ Parse.User

Return Parse.User instance from user id

Kind: static method of Parsimonious
Params

  • id string
  • [opts] object - A Backbone-style options object for Parse subclass methods that read/write to database. (See Parse.Query.find).

Parsimonious.fetchIfNeeded(thing, [opts]) ⇒ Parse.Promise

Given a value thing, return a promise that resolves to

  • thing if thing is a clean Parse.Object,
  • fetched Parse.Object if thing is a dirty Parse.Object,
  • fetched Parse.Object if thing is a pointer;
  • thing if otherwise

Kind: static method of Parsimonious
Params

  • thing *
  • [opts] object - A Backbone-style options object for Parse subclass methods that read/write to database. (See Parse.Query.find).

Parsimonious.getUserRoles(user, [opts]) ⇒ Parse.Promise

Return array of names of user's direct roles, or empty array. Requires that the Roles class has appropriate read permissions.

Kind: static method of Parsimonious
Params

  • user Parse.User
  • [opts] object - A Backbone-style options object for Parse subclass methods that read/write to database. (See Parse.Query.find).

Parsimonious.userHasRole(user, roles, [opts]) ⇒ Parse.Promise

Check if a user has a role, or any or all of multiple roles, return a promise resolving to true or false.

Kind: static method of Parsimonious
Params

  • user Parse.User
  • roles string | object - Can be single role name string, or object containing 'names' key whose value is an array of role names and 'op' key with value 'and' or 'or'
  • [opts] object - A Backbone-style options object for Parse subclass methods that read/write to database. (See Parse.Query.find).

Parsimonious.getJoinTableName(from, to) ⇒ string

Return the name of a table used to join two Parse.Object classes in a many-to-many relationship.

Kind: static method of Parsimonious
Params

  • from string - First class name
  • to string - Second class name

Parsimonious._getJoinTableClassVars() ⇒ object

Converts a variable number of arguments into 4 variables used by the joinWithTable, unJoinWithTable, getJoinQuery methods.

Kind: static method of Parsimonious

Parsimonious.joinWithTable(object1, object2, [metadata], [opts]) ⇒ Parse.Promise

Join two parse objects in a many-to-many relationship by adding a document to a third join table. Like Parse.Relation.add except that it allows you to add metadata to describe the relationship. Creates join tables which are named with the class names separated with the numeral 2; e.g.: Student2Course. (For backwards-compatibility with v4.1.0, this method may still be called with the 3 parameters 'classes', 'metadata', and 'opts', where 'classes' is a plain object whose two keys are the classes to join, and whose values are the Parse.Object instances.)

Kind: static method of Parsimonious
Params

  • object1 Parse.Object - Parse object or pointer
  • object2 Parse.Object - Parse object or pointer
  • [metadata] object - optional key/value pairs to set on the new document to describe relationship.
  • [opts] object - A Backbone-style options object for Parse subclass methods that read/write to database. (See Parse.Query.find).

Example

// Record the fact that a student completed a course, with date of completion and grade earned:
const student = <instance of Parse.Student subclass>
const course = <instance of Parse.Course subclass>
const meta = {completed: new Date(2017, 11, 17), grade: 3.2}
const opts = {sessionToken: 'r:cbd9ac93162d0ba1287970fb85d8d168'}
Parsimonious.joinWithTable(student, course, meta, opts)
   .then(joinObj => {
     // joinObj is now an instance of the class 'Student2Course', which was created if it didn't exist.
     // The Student2Course class has pointer columns 'student' and 'course',
     // plus a date column named 'completed' and a numeric column named 'grade'.
   })

Parsimonious.unJoinWithTable(object1, object2, [opts]) ⇒ Parse.Promise

Unjoin two parse objects previously joined by Parsimonious.joinWithTable If can't unjoin objects, returned promise resolves to undefined. (For backwards-compatibility with v4.1.0, this method may still be called with the 2 parameters 'classes' and 'opts', where 'classes' is a plain object whose two keys are the classes to join, and whose values are the Parse.Object instances.)

Kind: static method of Parsimonious
Params

  • object1 Parse.Object - Parse object or pointer
  • object2 Parse.Object - Parse object or pointer
  • [opts] object - A Backbone-style options object for Parse subclass methods that read/write to database. (See Parse.Query.find).

Parsimonious.getJoinQuery(classes, [constraints]) ⇒ Parse.Query

Return a query on a many-to-many join table created by Parsimonious.joinWithTable.

Kind: static method of Parsimonious
Params

  • classes object - Must contain two keys corresponding to existing classes. At least one key's value must be a valid parse object. If the other key's value is not a valid parse object, the query retrieves all objects of the 2nd key's class that are joined to the object of the 1st class. Same for vice-versa. If both values are valid parse objects, then the query should return zero or one row from the join table.
  • [constraints] object - (Options for Parsimonious.newQuery})

Example

// Find the join table record linking a particular student and course together:
const classes = {
   Student: <instance of Student class>,
   Course: <instance of Course class>
}
Parsimonious.getJoinQuery(classes)
   .first()
   .then(joinObj => {
     // joinObj is the instance of the class 'Student2Course'
     // that was created by Parsimonious.joinWithTable}
     // to link that particular student and course together,
     // along with any metadata describing the relationship.
   })

Example

// Find all courses taken by a particular student:
const classes = {
   Student: <instance of Student class>,
   Course: null
}
Parsimonious.getJoinQuery(classes)
   .find()
   .then(joinObjs => {
     // joinObj is an array of instances of the class 'Student2Course'
     // that were created by Parsimonious.joinWithTable}.
   })

Example

// Find the top 10 students who have taken a particular course and received a grade of at least 3:
const classes = {
   Student: null,
   Course: <instance of Course class>
}
Parsimonious.getJoinQuery(classes, {
   descending: 'grade',
   greaterThanOrEqualTo: ['grade', 3],
   limit: 10
}).find()

Parsimonious.getPointer(className, objectId) ⇒ object

Return a pointer to a Parse.Object.

Kind: static method of Parsimonious
Params

  • className string
  • objectId string

Parsimonious.isPFObject(thing, [ofClass]) ⇒ boolean

Return true if thing is a Parse.Object, or sub-class of Parse.Object (like Parse.User or Parse.MyCustomClass)

Kind: static method of Parsimonious
Params

  • thing *
  • [ofClass] string - Optionally check if it's of a specific ParseObjectSubclass

Parsimonious.isArrayOfPFObjects(thing, [ofClass]) ⇒ boolean

Return true if thing is array of Parse.Object

Kind: static method of Parsimonious
Params

  • thing *
  • [ofClass] string - Optionally check if it's of a specific ParseObjectSubclass

Parsimonious.isPointer(thing, [ofClass]) ⇒ boolean

Return true of thing is a valid pointer to a Parse.Object, regardless of whether the Parse.Object exists.

Kind: static method of Parsimonious
Params

  • thing *
  • [ofClass] string - Optionally check if it's of a specific ParseObjectSubclass

Parsimonious.isPFObjectOrPointer(thing, [ofClass]) ⇒ boolean

Return true if thing is a Parse.Object or pointer

Kind: static method of Parsimonious
Params

  • thing *
  • [ofClass] string - Optionally check if it's of a specific ParseObjectSubclass

Parsimonious.isUser(thing) ⇒ boolean

Return true if thing is an instance of Parse.User.

Kind: static method of Parsimonious
Params

  • thing *

Parsimonious.pfObjectMatch(thing1, thing2) ⇒ boolean

Return true if values both represent the same Parse.Object instance (same class and id) even if one is a pointer and the other is a Parse.Object instance.

Kind: static method of Parsimonious
Params

  • thing1 Parse.Object | object
  • thing2 Parse.Object | object

Parsimonious.getPFObject([thing], [className], [opts]) ⇒ Parse.Promise

Resolves thing to a Parse.Object, or attempts to retrieve from db if a pointer. Resolves as undefined otherwise.

Kind: static method of Parsimonious
Params

  • [thing] Parse.Object | object | string
  • [className] string - If set, and first param is a Parse.Object, resolves to the Parse.Object only if it is of this class.
  • [opts] object - A Backbone-style options object for Parse subclass methods that read/write to database. (See Parse.Query.find).

Parsimonious.toJsn(thing, [deep]) ⇒ *

Return a json representation of a Parse.Object, or of plain object that may contain Parse.Object instances, optionally recursively.

Kind: static method of Parsimonious
Params

  • thing * - Value to create json from.
  • [deep] boolean = false - If true, recursively converts all Parse.Objects and sub-classes of Parse.Objects contained in any plain objects found or created during recursion.

Parsimonious.getId(thing) ⇒ string | undefined

Attempt to return the ID of thing if it's a Parse.Object or pointer. If thing is a string, just return it. Otherwise, return undefined.

Kind: static method of Parsimonious
Params

  • thing *

Parsimonious.objPick(parseObj, keys) ⇒ object

Get some columns from a Parse object and return them in a plain object. If keys is not an array or comma-separated string, return undefined.

Kind: static method of Parsimonious
Params

  • parseObj Parse.Object
  • keys string | Array.<string>

Example

const car = new Parse.Object.extend('Car')
 
car.set('type', 'SUV')
car.set('interior', {
  seats:5,
  leather: {
    color: 'tan',
    seats: true,
    doors: false
  }
})
car.set('specs', {
  length: 8,
  height: 4,
  performance: {
    speed: 120,
    zeroTo60: 6
  }
})
 
Parsimonious.objPick(car, 'type,interior.leather,specs.performance.speed')
// returns
 {
   type: 'SUV',
   interior: {
     leather: {
      color: 'tan',
      seats: true,
     doors: false
   },
   specs: {
     performance: {
       speed: 120
     }
   }
 }

Parsimonious.objGetDeep(parseObj, path) ⇒ *

Get the value of a key from a Parse object and return the value of a nested key within it.

Kind: static method of Parsimonious
Params

  • parseObj Parse.Object
  • path string - Dot-notation path whose first segment is the column name.

Example

const car = new Parse.Object.extend('Car')
car.set('type', 'SUV')
car.set('interior', {
  seats:5,
  leather: {
    color: 'tan',
    seats: true,
    doors: false
  }
})
Parsimonious.objGetDeep(car, 'interior.leather.color')
// returns "tan"

Parsimonious.objSetMulti(parseObj, dataObj, [doMerge])

Set some columns on a Parse object. Mutates the Parse object.

Kind: static method of Parsimonious
Params

  • parseObj Parse.Object
  • dataObj object
  • [doMerge] boolean = false - If true, each column value is shallow-merged with existing value

Parsimonious.sortPFObjectsByKey([objs], key)

Sort an array of Parse objects by key (column name) Mutates array.

Kind: static method of Parsimonious
Params

  • [objs] Array.<object>
  • key string

Parsimonious.copyPFObjectAttributes(from, to, attributeNames)

Copy a set of attributes from one instance of Parse.Object to another. Mutates target Parse.Object.

Kind: static method of Parsimonious
Params

  • from Parse.Object
  • to Parse.Object - Is mutated.
  • attributeNames string | Array.<string>

Parsimonious.keysAreDirty(parseObj, keys) ⇒ boolean

Return true if any of the passed keys are dirty in parseObj

Kind: static method of Parsimonious
Params

  • parseObj Parse.Object
  • keys string | Array.<string> - Array of string or comma-separated string list of keys.

Parsimonious.getPFObjectClassName(thing) ⇒ string

Returns valid class name when passed either a subclass of Parse.Object or any string. Removes the underscore if it is one of the special classes with a leading underscore. Returns undefined if anything else.

Kind: static method of Parsimonious
Params

  • thing object | string

Parsimonious.classStringOrSpecialClass(thing) ⇒ *

Returns the corresponding special Parse class (like 'User') if passed the name of one; otherwise, returns the value unchanged.

Kind: static method of Parsimonious
Params

  • thing string

Parsimonious.classNameToParseClassName(className)

If className represents one of the special classes like 'User,' return prefixed with an underscore.

Kind: static method of Parsimonious
Params

  • className

Parsimonious._toArray(thing, [type]) ⇒ array

Return thing if array, string[] if string, otherwise array with thing as only item, even if undefined.

Kind: static method of Parsimonious
Params

  • thing *
  • [type] string - If set, only include values of this type in resulting array.

Change Log

4.5.1 - 12-01-18

Changed
  • Updated dependencies

4.5.0 - 24-11-17

Added
  • keysAreDirty method

4.4.3 - 24-11-17

Changed
  • minor documentation additions

4.4.2 - 24-11-17

Fixed
  • minor documentation errors

4.4.1 - 24-11-17

Fixed
  • minor documentation errors

4.4.0 - 24-11-17

Added
  • copyPFObjectAttributes method
  • pfObjectMatch method
  • getId method
  • isPFObjectOrPointer method
  • getPFObject method
  • sortPFObjectsByKey method
  • isArrayOfPFObjects method
Changed
  • isPFObject method now checks for ParseObjectSubclass constructor as well as instanceOf Parse.Object
  • constrainQuery and getJoinQuery methods now accept multiple constraints of the same type, such as three equalTo's.
  • getJoinQuery can now be passed pointers.
  • TypeError is now thrown when invalid parameters are passed to getJoinQuery, joinWithTable, unJoinWithTable methods.
  • Switched testing platform from Mocha/Chai to Jest.
  • Switched testing version of mongodb from parse-mockdb to mongodb-memory-server.

4.3.3 - 11-11-17

Changed
  • Remove links from change log as I can't get jsdoc-to-markdown to generate them correctly.

4.3.1 - 4.3.2 - 11-11-17

Fixed
  • README

4.3.0 - 11-11-17

Added
  • constrainQuery method, which newQuery now uses for optional constraints.
Changed
  • newQuery method now supports all query constraints (instead of just limit, skip, select and include).

4.2.1 - 08-11-17

Fixed
  • README

4.2.0 - 08-11-17

Added
  • Support for calling joinWithTable with two Parse.Object instances, while still supporting the original parameter format.
  • Examples for join-table methods.
Changed
  • Improved some other documentation.

4.1.0 - 05-11-17

Fixed
  • getPointer method was generating pseudo pointers that did not have a fetch function.
  • Addressed deprecation notice for the mocha "compilers" option
Added
  • Added optional 'ofClass' param to isPointer method to check if it is a pointer of a specific class.
  • 'include' option to newQuery method
  • Added support for getting nested values from pointer columns to objGetDeep method.

4.0.1 - 26-10-17

(trying to get NPM to update README)

4.0.0 - 26-10-17

BREAKING CHANGES
  • getClass method now creates constructors for special classes 'User,' 'Role,' and 'Session' with 'Parse[],' but still creates constructors for custom classes with 'Parse.Object.extend().'
  • getClass method now throws a TypeError if not passed a string, which it should have done anyway.
  • The above changes to getClass method affect getClassInst method because it always uses getClass method.
  • The above changes to getClass method affect newQuery method because it uses getClass method when passed a string.
  • Added back 'use strict'
Fixed
  • getUserById was passing the Parse.User constructor rather than an instance of Parse.User to getObjById, which was then trying to use it in a Parse.Query, but Parse.Query requires class instances or names.
Added
  • getPointer method
  • sample code for objGetDeep

3.7.6 - 3.7.7 - 23-10-17

Changed
  • Minor README changes, but published in order to get NPM to show the last few updates to README.

3.7.5 - 23-10-17

Changed
  • Minor jsdoc fixes.

3.7.4 - 23-10-17

Fixes
  • Uses 'browser' field in package.json to hint to webpack, browserify to substitute 'parse' for 'parse/node'

Versions 3.7.1 - 3.7.3 - 22-10-17

Changed
  • Minor README changes

3.7.0 - 22-10-17

Non-Breaking Change:
  • Exports a class with static methods, rather than a frozen singleton instance. It is still used the same way; it's just no longer a frozen instance of the class.
Added
  • setParse method, which allows you to override the instance of the Parse JS SDK used by the module. (By default, the module will try to use the Parse instance from the global scope. If none exists, it will use the node or browser based on which environment is detected by the 'detect-is-node' module.) If you wish to use the setParse method, do it after you initialize your Parse instance and set the masterKey. (See "Usage" section at the top of this file.)

3.6.0 - 21-10-17

Added
  • getClass method.
  • objGetDeep method.
  • classStringOrSpecialClass method.
Changed
  • userHasRole method rejects when passed invalid params
Fixed
  • Switched test framework from jest to mocha+chai because of issue between jest and parse-mockdb module.

3.5.4 - 17-10-17

Fixed
  • The way isPointer recognizes of one of the 3 different types of pointer objects it checks for.

3.5.3 - 17-10-17

Changed
  • isPointer method' recognizes 3 different types of pointer objects.
  • More thorough tests for isPFObject method, including invalidating pointers.
  • More thorough tests for isUser method.

3.5.2 - 16-10-17

Bug fixes
  • isPointer method was restricting to plain objects.

3.5.2 - 16-10-17

Changed
  • Minor jsdoc fixes.

3.5.0 - 16-10-17

Added
  • fetchIfNeeded method.
  • isPointer method.

3.4.0 - 14-10-17

Changed
  • Refactored into two files -- one for node environments and one for browsers. Reason: Runtime environment detection is too unreliable, even using "detect-node" module, because of problems running in webpack-dev-server.
  • a "browser" option in package.json as a hint to babel, browserify, etc. to use the browser version.

3.3.0 - 13-10-17

Added
  • isUser method

3.2.0 - 11-10-17

Added
  • getUserRoles method returns array of names of user's direct roles.

3.1.0 - 08-10-17

Added
  • userHasRole method can check if a user has any or all of an array of roles.
Changed
  • Improved documentation of newQuery method

3.0.0 - 03-10-17

BREAKING CHANGES
  • getJoinQuery signature has changed: The 'select' parameter has been removed. Instead, set a 'select' key in the 2nd options param object for use by newQuery method.
Added
  • newQuery method accepts a 'select' key in its 2nd parameter to select fields to restrict results to.
Other Changed
  • Improved documentation.

2.0.8 - 22-09-17

Changed
  • Improved ci config.
  • Moved Change Log to bottom of README.

2.0.7 - 21-09-17

Changed
  • Removed commitizen.

2.0.6 - 21-09-17

Changed
  • Removed semantic-release for now.

2.0.5 - 21-09-17

Changed
  • Reconfigured ci.

2.0.4 - 21-09-17

Changed
  • codecov reporting and badge.
  • Reduced minimum required node to 4.

2.0.3 - 21-09-17

Changed
  • 100% test coverage with jest.
  • Use different branch of parse-shim to account for parse already being loaded in cloud code.
Bug Fixed
  • Use different branch of parse-shim to correctly detect when running in browser or node to import correct parse version.

2.0.2 - 20-09-17

Added
  • userHasRole method
Changed
  • all methods that access the database now accept optional sessionToken
  • isPFObject now accepts an optional class name param
  • can pass array of field names, in addition to comma-separated list, to getJoinQuery
Breaking Changed
  • If unJoinWithTable can't unjoin objects, it returns a promise that resolves to undefined instead of null.

Package Sidebar

Install

npm i parsimonious

Weekly Downloads

2

Version

4.5.1

License

WTFPL

Last publish

Collaborators

  • charleskoehl