A suite of repositories with semantic
versioning of objects and their
dependencies. Think Bower, but for anything, not just JS code
The Problem:
You've got lots of things (code, data, whatever), and you've got multiple
versions of your things. Furthermore your things only work when paired with
the right versions of other things.
The Solution:
Version-repo's are typed repositories that store named values, their
version, and their requirements (dependencies). Version
repo's take care of the logic of finding a complete and consistent set of
resources for a given query.
Furthermore, there are a variety of version-repo's which simplify the process
of storing your object on disk, in memory, or in a database; serving and
querying them http/https, and perfomning tranformations when storing and/or
fetching your resources (e.g. stringifying / parsing JSON objects stored on
disk).
API
Storing resources
Resources are stored using the create()
method, like so:
var R = require("version-repo");
var repo = new R.MemoryRepo();
repo.create({name:"A",version:"v1.1.1",value:"My great thing"});
repo.create({name:"A",version:"v1.1.2",value:"An even better thing"});
repo.create({name:"A",version:"v1.1.5",value:"The best thing yet"});
repo.create({name:"A",version:"2.0.0", value:"Something different"});
Retrieving resources
Resources are queried using the fetch()
method, which return an array of matching resources.
repo.fetch({name:"A"});
Note that because we didn't specify a version, the latest version of the resource was returned
In the event that you really only want the specified resource and not the
dependencies, the fetchOne()
method will do the trick:
repo.fetchOne([{name:"A"}]);
older versions of a resource can be queried by passing a version
parameter:
repo.fetch([{name:"A",version:"1.1.1"}]).value
and we can use the a query object of for as shorthand for an array of name/version pairs:
repo.fetch({"A":"1.1.1"}).value
repo.fetch({"A":"1.1.1"}).value
repo.fetch({"A":"1.1.x"}).value
repo.fetch({"A":"~1"}).value
repo.fetch({"A":"1.1.3 - 1.1.7 || >=2.5.0"}).value
Fetching Multiple Resources:
So far we've stored several vesions of a single resource, but the real purpose
of the version repo's is to manage multiple resources and their versions. For
this, we'll neeed a more complicated example:
repo.create({name:"A",version:"1.0.0",value:"abc"});
repo.create({name:"A",version:"1.1.1",value:"Abc"});
repo.create({name:"A",version:"1.1.2",value:"aBc"});
repo.create({name:"A",version:"1.1.3",value:"abC"});
repo.create({name:"A",version:"1.2.3",value:"ABc"});
repo.create({name:"A",version:"2.0.0",value:"ABC"});
repo.create({name:"B",version:"1.0.0",value:"def",depends:{"A":"~1.0.0"});
repo.create({name:"B",version:"1.1.1",value:"Def",depends:{"A":"~1.1.1"});
repo.create({name:"B",version:"1.1.3",value:"DEf",depends:{"A":"1.1.2"});
repo.create({name:"B",version:"1.1.4",value:"DEF",depends:{"A":"~2.0.0"});
repo.create({name:"C",version:"1.0.0",value:"efg");
repo.create({name:"C",version:"1.1.1",value:"Efg",depends:{"B":"~1.1.1"});
repo.create({name:"C",version:"1.1.2",value:"EFg",depends:{"B":"~1.1.2"});
repo.create({name:"C",version:"1.1.3",value:"EFg",depends:{"B":"1.1.1","A":"2.0.0"});
repo.create({name:"C",version:"1.1.4",value:"efG",depends:{"B":"~1.1.3"});
with our more mature repository, we can query the complete set of resources
that are required for by our query using the feth()
method:
repo.fetch([{name:"B",version:"1.1.4"}])
similarly, the depends()
method will return list of resources which match
your query, (but not the resources themselves...)
repo.depends({B:"1.1.4"})
repo.depends({B:"1.1.4",C:"1.1.1"})
and to get the list of resources, and their dependencies, but not the values,
we can pass an options object to fetch, specifying the novealue:true
repo.fetch({B:"1.1.4"},{novalue:true})
Conveniently, attempting to fetch an conflicted set of resources throws a
Version Conflict
error, when there is no set of resources
which satisfies your query:
repo.dependencies([{"B":"1.1.1"},{"A":"2.0.0"}])
repo.fetch([{"B":"1.1.1"},{"A":"2.0.0"}])
repo.fetch([{"C":"1.1.3"}])
Updating Resources:
A resource either can be updated using either update()
method or the
insert()
method with the option upsert:true
:
repo.update({name:'A',version:'2.0.0',value:"something else"})
repo.insert({name:'A',version:'2.0.0',value:"something else",upsert:true})
However by default you may only update the latest version of the resource,
which can be changed by setting update:"any"
or update:"none"
when
instantiating the repo.
Deleting Resources:
A resource can be deleted using the del()
method:
repo.del({name:'A',version:'2.0.0'})
However by default you may only delete the latest version of the resource,
which can be changed by setting delete:"any"
or delete:"none"
when
instantiating the repo.
Repositories Classes
MemoryRepo (API: Synchronous, Stored Types: Any)
A synchronous repository which keeps resources in memory.
Constructor parameters
- config: An object with the following attributes:
- update: (optional) one of "latest" (default), "any", "none"
- delete: (optional) one of "latest" (default), "any", "none"
Example :
var my_repo = new MemoryRepo()
ReadonlyBuffer (API: Async, Stored Types: Any)
A ReadOnly Buffer repository is a read-only wrapper which keeps local copies of
resources queried from another 'host' repository. This is particularly useful
if the host repo is on another physical machine, for example to reduce the number
of network requests of mobile apps. Local resources are stored in memory and
calls to create/update/delete methods are forwarded onto the host repository.
Constructor parameters
- repo: A version-repo instance
Example :
var host_repo = new MemoryRepo()
var my_readonly_repo =new ReadonlyBuffer(host_repo)
sTransform (API: Synchronous, Stored Types: Any)
A Synchronous repo which forwards all requests to another version repo and performs
transformations of the stored values on create / update (storifying) and fetch
(de-storifying).
This is particularly useful for wrapping string-only repositories, such as the
FileRepo
in the
version-repo-node package,
with parse-on-read and stringify-on-write logic. Another use case is for
storing and dispatching copies of objects stored in a repo, in whicn case one
of the many deep-copy functions may be used for storifying, and de-storifiying
values. Transformers could also provide validate-on-save logic by using an
object validator such as the awesome AJV library as a storify, and trivial
function for destorifying (e.g. function(x){return x;}
)
Constructor parameters
- repo: A version-repo instance of type
S
. - storify: A function used to transform objects as they are stored (
create()
ed) or updated. - destorify: A function used to transform stored objects when they are retrieved (
fetch()
ed).
Example :
var host_repo = new MemoryRepo()
var my_repo = new sTransform(host_repo,JSON.stringify,JSON.parse)
the same examlple in TypeScript with generic typing:
var host_repo = new MemoryRepo<string>()
var my_repo = new sTransform<string,any>(host_repo,JSON.stringify,JSON.parse)
dTransform (API: Synchronous, Stored Types: Any)
An asynchronous (i.e. Deffered) repo which forwards all requests to another version repo and
performs transformations of the objects in transit.
This is particularly useful for wrapping asynchronous repo's with limited storage types,
such as the File and Remote (HTTP/S) Repo's in
version-repo-node)
Constructor parameters
- repo: A version-repo instance of type
S
. - storify: A function used to transform objects on storage on create / update. (
funciton(x:T):S
) - destorify: A function used to transform objects from storage on fetch. (
funciton(x:T):S
)
Note that the host repo may have a synchronous API, and the storify and/or
de-storify functions may return transformed values or Promised for the
transformed values.
Examples:
var string_only_repo = new MemoryRepo()
var my_async_repo = new dTransform(string_only_repo,JSON.stringify,JSON.parse)
the same examlple in TypeScript with generic types:
var string_only_repo = new MemoryRepo<string>()
var my_async_repo = new dTransform<string,any>(string_only_repo,JSON.stringify,JSON.parse)
Working with TypeScript
Every repo's that can potentially store any type of object accept a type parameter:
import { MemoryRepo } from "version-repo"
const my_string_repo = new MemoryRepo<string>()
and synchronous and deferred transform repositories accept two type parameters which
specify the type of the underlying repo, and the type for the API it exposes.
In this example, a FileRepo is used to store serialized objects on disk, and
an deferred transform repo is used to manage the serialization / de-serialization:
repo: new repo.sTransform(new repo.MemoryRepo(), (x => x), (x => x))});
import { sTransform } from "version-repo"
import { FileRepo } from "version-repo-node"
const my_file_repo = new FileRepo({directory:"/some/place/nice")})
const my_object_store = new sTransform<string,any>( my_file_repo JSON.stringify, JSON.parse);
Some repo's can store only a limited set of values, eg. the FileRepo can only accept sting values.
General API
This package is written in typescripts so explicitly importing .d.ts
file should not be required.
However generic repo interfaces are defined in src/typings.d.ts
, and the
synchronous API is provided here for tautological purposes:
Synchronous API
export interface package_loc { name:string; version?:string; }
export interface resource_data<T> {
name:string;
version:string;
value?:T;
depends?:{[key:string]:string};
upsert?:boolean
force?:boolean
}
interface sync_repository<T> {
create(resource:resource_data<T>):boolean;
update(resource:resource_data<T>):boolean;
del(query:package_loc):boolean;
fetch(query:package_loc|package_loc[],
fetch_opts?:fetch_opts):resource_data<T>[];
fetchOne(query:package_loc,
opts?:fetch_opts):resource_data<T>;
depends(query:package_loc|package_loc[]|{[key: string]:string}):package_loc[];
packages():string[] ;
versions():{[x:string]:string[]};
versions(name:string):string[];
latest_version(name:string):string
}
Asynchronous API
Method signatures are the same as the synchronous versions, but return a bluebird
promise for each return value.
Examples:
Note that these examples also demonstrate
version-repo-node and make
use of temp
var repo = require('versioned-repo'),
my_mem_repo= repo.memory(),
var temp = require('temp'),
path = require('path'),
node_repo = require('versioned-repo-node'),
temp_dir = temp.mkdirSync(),
my_file_repo = node_repo.file({directory:path.join(temp_dir,"my_repo_files")}),
var buffered_file_repo = repo.readonly_buffer(my_file_repo);
var express = require('express'),
app = express();
app.use('/my_repo',
repo.router({
repository:buffered_file_repo,
version_repo:my_mem_repo,
}));
var server = ('function' === typeof app) ? http.createServer(app) : app;
server.listen(0);
var address = server.address();
if (!address) {
server.listen(0);
address = server.address();
}
var protocol = (server instanceof https.Server) ? 'https:' : 'http:';
var hostname = address.address;
if (hostname === '0.0.0.0' || hostname === '::') {
hostname = '127.0.0.1';
}
var base_url = protocol + '//' + hostname + ':' + address.port ;
var remote_repo = new repo.remote({
'base_url':base_url + '/my_repo',
})
json_repo = dTransform(
remote_repo,
JSON.stringify,
JSON.parse);
my_file_repo.creae({name:"my-resource",version:"1.2.3"},
'{"hello":"world"}'
).then(() => {
return json_repo.fetch({name:"my-resource",version:"^1.x"});
}).then((resource) => {
console.log(resource.object);
console.log(resource.version);
return json_repo.packages();
}).then((resources) => {
console.log(resources);
return json_repo.latest_version();
}).then((version) => {
console.log(version);
return json_repo.versions();
}).then((versions) => {
console.log(versions);
return calculate_dependencies([ {name:"my-resource",version:"^1.0.0"}, ],json_repo);
}).then((dependents) => {
console.log(dependents);
})