1.0.0-alpha.2 • Published 5 years ago

mockfoundry v1.0.0-alpha.2

Weekly downloads
-
License
MIT
Repository
gitlab
Last release
5 years ago

MOCKFOUNDRY

Gitlab pipeline status Coverage License Bundle Size NPM Version NPM Collabs

Motivation

  • Prototyping a UI and want a quick way of testing API/CRUD functionalities?
  • Working on a feature with external incomplete API dependencies?
  • Ever wanted a very simple RESTful server with built-in CRUD operations?

If you answered yes to any of the questions stated above then mockfoundry may be right for you.

Goal

  • To drastically reduce the boilerplate needed for prototyping with actual data.
  • To enable true mocking-out of APIs and still achieving close to actual world behavior.

Installation

npm i --save mockfoundry // as a dependency
npm i --save-dev mockfoundry // as a development dependency. can be used for testing

Use Require

This library is meant to be run in Node.js and therefore is exported as a CommonJS library

const Mockfoundry = require('mockfoundry'); // require module

Port

Port is an essential part of mockfoundry as it is needed to create instances for the server. It is also used in the naming convention for the files in which data is saved.

If you start one instance on port 1000 and you start another instance on port 1001, I hope you would agree data saved on disk from those endpoints should be saved in their respective files. For that reason, mockfoundry treats ports as part of the data file name and maintains them accordingly,thus, mockfoundry fundamentally treats ports as databases.

The data will persist on disk even between restarts for the same port unless the override flag is set during instantiation so please tag ports accordingly.

Instance

const instance = new Mockfoundry(port [, override ]);

Run

const instance = new Mockfoundry(1000); // create instance on port 1000
instance.start(); // start server

API Primer

All APIs have a similar syntax convention:

host:port/action/collection?[optional-query-parameters]
TypeMeaningRequiredDefault(s)
hosthost of serverYeslocalhost (and is only localhost for now)
portport instance is running onYesNo default
actioncrud operation to be performed by the serverYessave, fetch, count, update, remove (and these are the only ones that can be used)
collectionthe type of data being saved. It is synonymous to a table in SQL or a type of document in a NoSQLYesNo Default
optional-query-parametersadditional query parameters that can be used along with the fetch action to optimize the data returnedNoskip, limit, sort (and these are the only ones that can be used)

HTTP Method Standards

Mockfoundry adheres to the HTTP standards with all API calls. Simply put, all actions use appropriate HTTP methods for respective calls.

ActionHTTP MethodAccepts BodyRequires BodyRequired In BodyOptional In Body
savePOSTYesYesarray of objects or an object---
fetchGETNo---------
fetchPOSTYesNo---fields and filters
countGETNo---------
countPOSTYesNo---filters
updatePUTYesYesfilters and update---
removeDELETEYesYesfilters---

APIs

Let's take a Vehicle Collection. We will proceed to learn how to do all CRUD operations in mockfoundry using our Vehicle Collection. Our Vehicle Collection has the following structure:

{
	brand
	model
	year
	kind
	countryOfOrigin
	spec {
		engine
		drivetrain
		acceleration
		speed
		range
	}
	class
	isElectric
	isStreetLegal
	features
	safetyRating
	awards
	tags
}

The way in which we wrote out this collection will become particularly important later on. Other than that, it is a simple list of fields a Vehicle Collection can have.

All our examples will be explained using axios --- a promise based HTTP client for the browser and node.js.

Save API

At this point, mockfoundry does not enforce any kind of schema validation on a collection level. Anything you save in the collection will be what gets saved. Mockfoundry is first and foremost a mocking library and as such, we want to reduce the boilerplate setup needed to achieve just that.

For the sake of brevity and code readability, we will shorten what we show as being saved but in actuality, we are saving the whole structure described above.

Example --- Single Entry

axios.post(
	'locahost:1000/save/vehicle',{
		brand: 'bmw',
		model: 'm4',
		year: '2020',
		...... // indicates more fields
	}
)
.then(response => console.log(response)) // prints true if saved, else false
.catch(error => console.error(error)); // prints kind of error

Example --- Multiple Entry of the same collection

axios.post(
	'locahost:1000/save/vehicle',[
		{
			brand: 'bmw',
			model: 'm4',
			year: '2020',
			...... // indicates more fields
		},
		{
			brand: 'toyota',
			model: 'prius,
			year: '2021',
			...... // indicates more fields
		}
	]
)
.then(response => console.log(response)) // prints true if saved, else false
.catch(error => console.error(error)); // prints kind of error

Fetch API

Looking back at the API Primer and HTTP Method Standards, you will notice that the fetch action is a very special action. To reiterate, let's list all the combinations of things it can have:

  • It can be called with or without a body
  • It is the only action that accepts optional query parameters
  • A new fun fact that was not apparent from before is, it can accept any arbitrary number of fields in exactly the structure described at the beginning of the APIs section. This way of requesting fields is inspired by the get-what-you-want model in graphql.

Example --- Simple Fetch

axios.get('locahost:1000/fetch/vehicle')
.then(data => console.log(data)) // prints an array of vehicle collection objects
.catch(error => console.error(error)); // prints kind of error

Example --- Fetch with Limit as Query Parameter

axios.get('locahost:1000/fetch/vehicle?limit=2')
.then(data => console.log(data)) // prints an array of two vehicle collection objects
.catch(error => console.error(error)); // prints kind of error

Example --- Fetch with Skip and Limit as Query Parameters

axios.get('locahost:1000/fetch/vehicle?skip=1&limit=2')
.then(data => {
	// prints an array of two vehicle collection objects
	// by skipping the first object it finds in the collection
	console.log(data)
})
.catch(error => console.error(error)); // prints kind of error

Example --- Fetch with Skip, Limit, and Sort as Query Parameters

axios.get('locahost:1000/fetch/vehicle?skip=1&limit=2&sort=year')
.then(data => {
	// prints an array of two vehicle collection objects
	// by skipping the first object it finds in the collection
	// and sorting by year ascending order by default
	console.log(data)
})
.catch(error => console.error(error)); // prints kind of error

To specify a particular sort order, other than the default ascending, you need to set it with a colon character:

sort=year:desc
sort=spec.acceleration:asc

For multiple sorts, use the pipe character to list them out :

sort=year:desc|spec.acceleration:asc
sort=year|isElectric:desc

Example --- Fetch with Filter(s)

axios.post(
	'locahost:1000/fetch/vehicle',{
		"filters": [
			{
				"field": "kind",
				"op": "$eq", // special operator character. $eq means equals to.
				"value": "sedan"
			}
		]
	}
)
.then(data => {
	// prints an array of vehicle collection objects
	// that matches the filter(s) passed in
	console.log(data)
})
.catch(error => console.error(error)); // prints kind of error

You can head over to the Filters section to learn how to use them. It is very straight forward.

Example --- Fetch with Field(s)

axios.post(
	'locahost:1000/fetch/vehicle',{
		"fields": `{
				brand
				model
				year
				kind
				spec {
					range
					speed
					engine
				}
				class
				tags
		}
		`
	}
)
.then(data => {
	// prints an array of vehicle collection objects,
	// wih only the fields listed in the fetch call
	console.log(data)
})
.catch(error => console.error(error)); // prints kind of error
A little about Fields

As mentioned earlier, the fetch action has special powers among which is the ability to list the fields you want. Thanks to ES6 string literals, we can list fields we want to return in a multiline format. It has many benefits such as, code readability, familiarity due to its object-like look and getting data in exactly the way fields are listed. Of course you can resort to using a single line string if the ES6 approach is not viable. The only issue with that is, you loose code readability and I know that will drive me nuts.

To maintain complaince with this structure, mockfoundry applies the following validation rules to a fields property:

  • Needs to start with an opening curly brace ({).
  • Needs to end with a closing curly brace (}).
  • May contain spaces, tabs or new lines.
  • Contents between opening and closing curly braces can only be any of the rules stated above along with alphanumerics, commas, and underscores.

Count API

The count action is similar to the fetch action but different in one obvious way --- it does not need fields or query parameters.

Example --- Simple Count

axios.get('locahost:1000/count/vehicle')
.then(data => console.log(data)) // prints the number of documents if any, zero if none
.catch(error => console.error(error)); // prints kind of error

Example --- Count with Filter(s)

axios.post(
	'locahost:1000/count/vehicle',{
		"filters": [
			{
				"field": "kind",
				"op": "$eq", // special operator character. $eq means equals to.
				"value": "sedan"
			}
		]
	}
)
.then(data => {
	// prints the number of documents
	// that matches the filter(s) passed in if anym zero f none
	console.log(data)
})
.catch(error => console.error(error)); // prints kind of error

Update API

The update action is similar to the count action. However, it requires another property called update. I know, very original.

Since mockfoundry does not enforce any schema validation due to reasons stated earlier, the update action merges new fields with the rest of the collection.

NOTE:

Updates of inner objects must be done with a dot notation (parent_prop.child_prop) if you want to keep other values that might be in there. Otherwise, if you have all the required data on the client side, you can use the spread operator or any other merge solution to add new data in before updating.

Use the fetch action to select the fields you want to return.

Example --- Update

axios.put(
	'locahost:1000/update/vehicle',{
		"filters": [
			{
				"field": "kind",
				"op": "$eq", // special operator character. $eq means equals to.
				"value": "sedan"
			}
		],
		"update": {
			"year": 2021,
			"spec.engine": "v6" // note dot notation
			"spec.newProp: "some new value"  // note dot notation
			"anotherNewProp: {}
		}
	}
)
.then(response => console.log(response)) // prints true if updated, else false
.catch(error => console.error(error)); // prints kind of error

Remove API

The remove action is similar to the count action but different one obvious way --- it requires the filters property.

Example --- Remove with required Filter(s)

axios.delete(
	'locahost:1000/remove/vehicle',{
		"filters": [
			{
				"field": "kind",
				"op": "$eq", // special operator character. $eq means equals to.
				"value": "sedan"
			}
		]
	}
)
.then(response => console.log(response)) // prints true if removed, else false
.catch(error => console.error(error)); // prints kind of error

Note that, mockfoundry treats empty filters array as match all. In that sense, an empty filters array is still an valid array and will cause all data of the passed collection to be removed. If that is your intention then ignore this caution.

Filters

All filter objects follow the field-op-value paradigm. That is because mockfoundry uses NeDB under the hood for database operations.

Besides the op property in a filter contstruction, the field and the value properties are self explanatory. The op property is either a logical or comparison operator. Below are the list of all supported operators:

OperatorMeaningWhat question does it asks of the collection?Value Type(s)
$eqequalfor the given field, is any value equals what was passed?string, number, regex, boolean, object, array
$ltless thanfor the given field, is any value less than what was passed?string, number
$gtgreater thanfor the given field, is any value greater than what was passed?string, number
$lteless than or equalfor the given field, is any value less than or equal to what was passed?string, number
$gtegreater than or equal tofor the given field, is any value greater than or equal to what was passed?string, number
$neqnot equalfor the given field, is any value not equal to what was passed?string, number, regex, boolean, object, array
$existsexistsfor the given field, does the field exist (when value is true) or not exist (when value is false)?boolean
$regexregexfor the given field, does any value match the regex expression passed?string
$notnotfor the given field, is the value passed not an exact match of anything in the collection?string, number, boolean, object, array
$ininfor the given field, is the value passed a member of the array of values in the collection?array of primitives or objects
$ninnot infor the given field, is the value passed not a member of the array of values in the collection?array of primitives or objects
$ororfor the given field, is any of the array of values passed exactly a value in the collection?string, number, boolean, object, array
$andandfor the given field, are all of the array of values passed exactly a value in the collection?string, number, boolean, object, array

Besides the $eq and $neq operators, the rest are direct replicas of what NeDB uses officially.

Error

Error is an unexpected but important part of any application. Mockfoundry uses fastify as its application server and draws on its error handling structure and even extends it where possible.

All errors in mockfoundry contain the following properties:

PropertyMeaningAlways Present
statusCodeHTTP CodeYes
errorType of errorYes
messageReason for the errorYes
optionalsA spread of any aditional properties specific to the action being performedNo

Besides the optionals property, all other properties are default and directly borrowed from fastify. The optionals property is an object that merges its contents with the defaults so there is no actual optionals property key in errors.

Issues

Please report issues as you see them during usage. It will help improve this library as a whole. Thank you.

Credits