...
 
Commits (12)
{
"env": {
"browser": true,
"es6": true
},
"extends": "eslint:recommended",
"parserOptions": {
"sourceType": "module"
},
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
}
\ No newline at end of file
# Ignore dist folder. This will only be used when building.
# dist
# Ignore installed node_modules. These are only for development.
node_modules
This diff is collapsed.
This diff is collapsed.
{
"name": "openjac",
"version": "0.0.1",
"main": "dist/openjac.js",
"module": "src/openjac.js",
"description": "JsonAPI Client for REST APIs with schema discovery using OpenApi.",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.richgerdes.com/rich/openjac"
},
"engines": {
"node": ">=6.0.0"
},
"keywords": [
"api",
"rest",
"openapi",
"jsonapi",
"swagger",
"drupal"
],
"scripts": {
"build": "webpack --env build",
"watch": "webpack --watch --env dev",
"test": "mocha --compilers js:babel-core/register --colors ./test/**/*.spec.js",
"compile": "babel --presets es2015,stage-0 -d src/",
"lint": "eslint --ext .js *.js src test --quiet"
},
"devDependencies": {
"babel-core": "6.24.1",
"babel-eslint": "^7.2.2",
"babel-loader": "6.4.1",
"babel-plugin-add-module-exports": "0.1.2",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.23.0",
"babel-preset-env": "1.4.0",
"babel-preset-es2015": "^6.3.13",
"babel-runtime": "^6.23.0",
"chai": "^3.5.0",
"eslint": "^3.19.0",
"mocha": "^3.2.0",
"standard-version": "^4.0.0",
"webpack": "2.4.1",
"yargs": "^7.1.0"
},
"dependencies": {
"isomorphic-fetch": "^2.2.1",
"lodash": "^4.17.4"
}
}
'use strict';
import Resource from './resource/Resource';
import Definition from './schema/Definition';
import Path from './schema/Path';
class OpenJacClient {
/**
* Create a new OpenJAC client.
*/
constructor(config) {
this.config = config
this.loaded_queue = [];
this.is_load_finished = false;
this.definitions = [];
this.definitions_by_path = [];
this.paths = [];
this.tags = [];
this.auth = this.config.auth;
this.api_prefix = '';
if (this.config.schema !== undefined) {
this.load(this.config.schema);
} else if (this.config.schema_url) {
this.fetchSchema(this.config.schema_url);
}
}
auth () {
return this.auth;
}
/**
* Retrieve the openapi schema for the application from a url.
*
*/
fetchSchema(url) {
fetch(url, {})
.then(res => {
return res.json()
})
.catch(error => console.log('Unable to fetch api schema from remote'))
.then(schema => this.load(schema));
}
/**
* Parse and load the definitions and endpoints from a schema blob.
*/
load(schema) {
this.schema = schema;
if (this.schema.host !== undefined) {
var protocol = 'http';
if (this.schema.schemes) {
if (this.schema.schemes.includes('https')) {
protocol = 'https';
} else if (this.schema.schemes.includes('http')) {
// continue;
} else if (this.schema.schemes.length > 0) {
protocol = this.schema.schemes[0];
}
}
if (this.schema.basePath !== undefined) {
this.api_prefix = protocol + '://' + this.schema.host;
if (this.schema.basePath !== '/') {
this.api_prefix += this.schema.basePath;
}
}
}
this.api_prefix = this.api_prefix.replace(/(,\s*$)/g, '');
// Setup schema.
this.definitions = [];
this.definitions_by_path = [];
this.paths = [];
this.tags = [];
for (let tag of schema.tags) {
this.tags[tag.name] = tag;
}
for (let path in schema.paths) {
this.paths.push(new Path(this, path, schema.paths[path]));
}
for (let definition_name in schema.definitions) {
this.definitions[definition_name] = new Definition(this, schema.definitions[definition_name]);
}
this.fetch(this.api_prefix).then(jsonapi_links => {
for (let type in jsonapi_links.links) {
if (type == 'self') {
continue;
}
if (this.definitions[type] !== undefined) {
var definition = this.definitions[type];
var root_path = jsonapi_links.links[type];
if (root_path.startsWith(this.api_prefix)) {
root_path = root_path.substring(this.api_prefix.length);
}
// Map back root paths to definitions.
this.definitions_by_path[root_path] = definition;
var defintion_paths = {};
for (let path of this.paths) {
if (path.uri.startsWith(root_path)) {
// For each path for definition type, determine it's action.
if (root_path === path.uri) {
defintion_paths.collection = path;
path.action = 'collection';
} else if (path.uri.includes('/relationships/')) {
defintion_paths.relationships = path;
path.action = 'relationships';
} else if (path.uri.includes('/{related}')) {
defintion_paths.related = path;
path.action = 'related';
} else {
defintion_paths.individual = path;
path.action = 'individual';
}
}
}
definition.setPaths(defintion_paths);
}
}
})
.then(() => {
var callback = this.loaded_queue.shift();
while (typeof callback !== 'undefined') {
callback(this);
callback = this.loaded_queue.shift();
}
this.is_load_finished = true;
});
}
defintionForPath(path) {
for (root_path in this.definitions_by_path) {
if (path.startsWith(root_path)) {
return this.definitions_by_path[root_path];
}
}
return undefined;
}
/**
* Construct init parameter for fetch.
*/
buildRequest(data, method) {
// @TODO add auth headers.
method = method.toUpperCase();
var request = {
method: method,
redirect: 'follow',
headers: {}
}
if (!(['GET', 'HEAD'].includes(method))) {
request.body = JSON.stringify(data);
}
return request;
}
/**
* Execute an api request to a given path object with a given data set.
*/
fetchFrom(path, method, data = {}) {
var uri = path.buildUri(method, data);
return this.fetch(this.api_prefix + uri, method, data);
}
/**
* Execute an api request to a path with a given data set.
*/
fetch(path, method = 'GET', data = {}) {
return new Promise((resolve, reject) => {
this.auth.processRequest(this.buildRequest(data, method))
.then(requestData => {
fetch(path, requestData).then(response => {
if (response.ok) {
resolve(response)
} else {
reject(new Error('Unable to fetch ' + path + ' from remote.'))
}
}, error => {
reject(new Error(error.message))
});
});
})
.then(res => {
if (res.status !== 200) {
throw 'Authentication Requied';
Promise.reject();
}
return res.json();
})
.catch(error => console.error('Unable parse response. ', error));
}
/**
* Execute a function once the api has loaded the schema.
*
* If the schema is already loaded the callback will be executed automatically
* instead of being added to a queue.
*/
loaded(callback) {
if (this.is_load_finished) {
callback(this);
} else {
this.loaded_queue.push(callback);
}
}
/**
* Get the definition type.
*/
on(type) {
return this.definitions[type];
}
}
export default OpenJacClient;
'use strict';
class Auth {
/**
* Initialize authentication handler.
*/
init () {
// Do nothing by default.
}
/**
* Preprocess a request before its sent to the api.
*/
processRequest (data) {
// By default, no changes to data are required.
return Promise.resolve(data);
}
}
export default Auth;
'use strict';
import Path from './../schema/Path';
class Route {
/**
* Create a new OpenJAC client.
*/
constructor(path, client) {
this.path = path;
this.client = client;
}
/**
* Submit a request to the path with the given data blob.
*/
execute (data) {
this.client.execute(data, this);
}
}
export default Route;
import OpenJacClient from './OpenJacClient';
import Auth from './client/Auth';
export {OpenJacClient, Auth};
'use strict';
import Definition from './../schema/Definition';
class Resource {
/**
* Create a new Resource.
*/
constructor(definition, client) {
this.definition = definition;
this.client = client;
this._relationship_promises = [];
}
/**
* Intercept a property.
*/
get(target, propKey, receiver) {
if (propKey === 'save') {
// Update this resource.
return () => {
return this.definition.save(target);
}
}
else if (propKey === 'delete') {
// Delete this resource.
return () => {
return this.definition.delete(target);
}
}
else if (propKey in target.relationships) {
return this.definition.getRelated(target, propKey);
}
else if (propKey in target.attributes) {
// Return property if it exists.
return new Promise((resolve, reject) => {
resolve(target.attributes[propKey]);
});
}
}
}
export default Resource;
'use strict';
import Path from './Path';
import Route from './../client/Route';
import Resource from './../resource/Resource';
class Definition {
/**
* Create a new OpenJAC client.
*/
constructor(client, schema) {
this.client = client;
this.schema = schema;
this.paths = {};
}
/**
* Set paths list.
*/
setPaths(paths) {
this.paths = paths;
}
/**
* Retrieve a resource by id.
*/
get(id) {
if (this.paths.individual !== undefined) {
return this.paths.individual.execute('GET', id).then(response => {
return this.constructResource(response.data);
});
}
else {
throw 'No individual route for definition.';
}
}
/**
* Load multiple resources by uuid.
*/
getMultiple(ids) {
var or_filter = {
uuid: ids.join(',')
};
return this.getAll(or_filter);
}
save(object) {
console.log("save");
}
new(object) {
}
/**
* Retrieve a resource by id.
*/
getAll(filters = {}) {
if (this.paths.collection !== undefined) {
return this.paths.collection.execute('GET', filters).then(response => {
var resources = [];
for (let item of response.data) {
resources.push(this.constructResource(item));
}
return resources;
});
}
else {
throw 'No collection route for definition.';
}
}
/**
* Fetch the set of related resources using the related url.
*/
getRelated(parent, property) {
if (parent._relationship_promises === undefined) {
parent._relationship_promises = [];
}
// Get Relations.
if (!(property in parent._relationship_promises)) {
var relationship_schema = this.relationshipDefinition(property);
var definition_id = "";
if (relationship_schema.type === "array") {
definition_id = relationship_schema.items.properties.type.enum[0];
} else {
definition_id = relationship_schema.properties.type.enum[0];
}
var prop_definition = this.client.on(definition_id);
if (!prop_definition) {
return Promise.reject(new Error('Unable to load definition for property `' + property + '`'));
}
var related_url = parent.relationships[property].links.related;
parent._relationship_promises[property] = this.client.fetch(related_url).then(results => {
if (relationship_schema.type === "array") {
var resources = results.data.map(data => prop_definition.constructResource(data));
return Promise.resolve(resources);
} else {
var resource = prop_definition.constructResource(parent.relationships[property].data);
return Promise.resolve(resource);
}
});
}
return parent._relationship_promises[property];
}
/**
* Determine the definition type of an relationship property.
*/
relationshipDefinition(property) {
if (property in this.schema.properties.relationships.properties) {
return this.schema.properties.relationships.properties[property].properties.data;
}
return undefined;
}
/**
* Given a data blob, construct a resource.
*/
constructResource(data) {
return new Proxy(data, new Resource(this, this.client));
}
}
export default Definition;
'use strict';
class Path {
/**
* Create a new OpenJAC client.
*/
constructor(client, uri, methods) {
this.client = client;
this.uri = uri;
this.methods = methods;
this.action = 'collection';
}
/**
* Construct a uri injecting param data into the uri.
*/
buildUri(method, data) {
if (this.methods.hasOwnProperty(method)) {
var uri = this.uri;
var schema = this.methods[method];
for (let param of schema.parameters) {
if (param.in === 'path') {
if (uri.includes(param.name)) {
if (typeof data[param.name] === 'undefined') {
throw 'Path parameter "' + param.name + '" is missing.';
}
uri = uri.replace('{' + param.name + '}', data[param.name]);
}
delete data[param.name];
} else if (param.required) {
throw 'Required parameter "' + param.name +'" is missing.';
}
}
return uri;
}
else {
throw 'Invalid request method for path';
}
}
/**
* Execute request on path with an arbitrary data blob.
*
* The data blob will be converted to the correct format (array, id, etc),
* based upon the action.
*/
execute(method, data) {
method = method.toLowerCase();
if (!this.methods.hasOwnProperty(method)) {
throw 'Invalid Method ' + method + ' for ' + this.action + ' action.'
}
if (['get', 'delete'].includes(method) && this.action === 'individual') {
var param_regex = /{([^}]*:?)}/i;
var key_match = this.uri.match(param_regex);
if (key_match.length > 1) {
let uuid = data;
data = {};
data[key_match[1]] = uuid;
}
}
return this.client.fetchFrom(this, method, data);
}
}
export default Path;
import {
expect
} from 'chai';
import * as perimeter from './../../src/lib/perimeter';
describe('Perimeter tests', function() {
it('Should return the perimeter of square as 4 X length', function() {
expect(perimeter.square(5)).to.be.equal(20);
});
it('Should return the perimeter of rectangle as (2 X length + 2 X width)', function() {
expect(perimeter.rectangle(5, 4)).to.be.equal(18);
});
it('Should return the perimeter of circle as (2 X radius X PI)', function() {
expect(perimeter.circle(5)).to.be.equal(31.42);
});
it('Should return the perimeter of circle as (2 X radius X PI)', function() {
expect(perimeter.circle(10)).to.be.equal(62.84);
});
it('Should return the perimeter of circle as (0 X radius X PI)', function() {
expect(perimeter.circle(0)).to.be.equal(0);
});
});
import {
expect
} from 'chai';
import * as sampleLib from './../../dist/sampleLib';
describe('Given the Util library', function() {
it('Should return the weather of london city', function(done) {
sampleLib.weather('london')
.then(response => {
expect(response, 'response is not as expected').to.exist;
expect(response.description, 'response is not as expected').to.exist;
done();
}).catch(e => done(e));
});
});
import {
expect
} from 'chai';
import * as temperature from './../../src/lib/temperature';
describe('Temperature tests', function() {
it('Should return the temperature in Fareheit', function() {
expect(temperature.celsiusToFarenheit(37)).to.be.equal(98.6);
});
it('Should return the temperature in Fareheit', function() {
expect(temperature.celsiusToFarenheit(38.5)).to.be.equal(101.3);
});
it('Should return the temperature in Celsius', function() {
expect(temperature.farenheitToCelsius(98.6)).to.be.equal(37);
});
});
import {
expect
} from 'chai';
import weather from './../../src/lib/weather';
describe('Weather Tests', function() {
it('Should return the weather of london city', function(done) {
weather('london')
.then(response => {
expect(response, 'response is not as expected').to.exist;
expect(response.weather, 'response is not as expected').to.exist;
expect(response.description, 'response is not as expected').to.exist;
done();
}).catch(e => done(e));
});
});
const webpack = require('webpack');
const UglifyJsPlugin = webpack.optimize.UglifyJsPlugin;
const path = require('path');
const env = require('yargs').argv.env; // use --env with webpack 2
let plugins = [];
if (env === 'build') {
plugins.push(new UglifyJsPlugin({minimize: true}));
}
module.exports = {
entry: ['./src/index.es6'],
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'openjac.js',
library: 'openjac',
libraryTarget: 'commonjs2',
umdNamedDefine: true
},
devtool: 'source-map',
externals: {
'lodash': {
commonjs2: 'lodash'
},
'isomorphic-fetch': {
commonjs2: 'isomorphic-fetch'
}
},
module: {
loaders: [{
test: /\.es6$/,
loader: 'babel-loader',
query: {
presets: ['es2015']
},
// Skip any files outside of `src` directory
include: [
path.join(__dirname, 'src')
]
}, {
loader: 'babel-loader',
// Skip any files outside of `src` directory
include: [
path.join(__dirname, 'src')
],
// Only run `.js` files through Babel
test: /\.js$/
}, {
test: /\.json$/,
loader: 'json-loader'
}]
},
resolve: {
extensions: ['.js', '.es6']
},
stats: {
colors: true,
errors: true,
errorDetails: true,
progress: true
},
plugins: plugins
};
This source diff could not be displayed because it is too large. You can view the blob instead.